From a2f6eacdf8ddc6bd6814fb2cd116b5dca8e715a5 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Wed, 25 Feb 2026 08:35:09 -0500 Subject: [PATCH 1/4] first attempt for PTM site as a node --- R/visualizeNetworksWithHTML.R | 98 ++++++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/R/visualizeNetworksWithHTML.R b/R/visualizeNetworksWithHTML.R index db8b64c..3885c44 100644 --- a/R/visualizeNetworksWithHTML.R +++ b/R/visualizeNetworksWithHTML.R @@ -296,32 +296,74 @@ getEdgeStyle <- function(interaction, category, edge_type) { } createNodeElements <- function(nodes, displayLabelType = "id") { - # Map logFC to colors if logFC column exists if ("logFC" %in% names(nodes)) { node_colors <- mapLogFCToColor(nodes$logFC) } else { - node_colors <- rep("#D3D3D3", nrow(nodes)) # Default color + node_colors <- rep("#D3D3D3", nrow(nodes)) } - # Determine which column to use for labels - label_column <- if(displayLabelType == "hgncName" && "hgncName" %in% names(nodes)) { + label_column <- if (displayLabelType == "hgncName" && "hgncName" %in% names(nodes)) { "hgncName" } else { "id" } - apply(cbind(nodes, color = node_colors), 1, function(row) { - # Use the appropriate label, fallback to id if hgncName is missing/empty - display_label <- if(label_column == "hgncName" && !is.na(row['hgncName']) && row['hgncName'] != "") { - row['hgncName'] + node_elements <- apply(cbind(nodes, color = node_colors), 1, function(row) { + display_label <- if (label_column == "hgncName" && !is.na(row["hgncName"]) && row["hgncName"] != "") { + row["hgncName"] } else { - row['id'] + row["id"] } - - paste0("{ data: { id: '", row['id'], "', label: '", display_label, "', color: '", row['color'], "' } }") + paste0("{ data: { id: '", row["id"], "', label: '", display_label, + "', color: '", row["color"], "', node_type: 'protein' } }") }) + + # Generate PTM site child nodes + edges + ptm_elements <- c() + if ("Site" %in% names(nodes)) { + for (i in seq_len(nrow(nodes))) { + site_val <- nodes$Site[i] + if (!is.na(site_val) && trimws(site_val) != "") { + parent_id <- nodes$id[i] + # Sites may be delimited by _, comma, semicolon, or pipe + sites <- trimws(unlist(strsplit(as.character(site_val), "[_,;|]"))) + sites <- sites[sites != ""] + for (site in sites) { + ptm_node_id <- paste0(parent_id, "__ptm__", site) + # Escape for JS string safety + safe_site <- escape_js_string(site) + safe_parent <- escape_js_string(parent_id) + safe_ptm_id <- escape_js_string(ptm_node_id) + + # PTM node + ptm_elements <- c(ptm_elements, + paste0("{ data: { id: '", safe_ptm_id, + "', label: '", safe_site, + "', parent_protein: '", safe_parent, + "', node_type: 'ptm' } }") + ) + # Edge connecting PTM node to parent protein + ptm_edge_id <- paste0(parent_id, "__ptm_edge__", site) + ptm_elements <- c(ptm_elements, + paste0("{ data: { id: '", escape_js_string(ptm_edge_id), + "', source: '", safe_parent, + "', target: '", safe_ptm_id, + "', edge_type: 'ptm_attachment',", + " category: 'ptm_attachment',", + " interaction: '',", + " color: '#9932CC',", + " line_style: 'dotted',", + " arrow_shape: 'none',", + " width: 1.5,", + " tooltip: '' } }") + ) + } + } + } + } + + return(c(node_elements, ptm_elements)) } - createEdgeElements <- function(edges, nodes = NULL) { if (nrow(edges) == 0) return(list()) @@ -493,6 +535,38 @@ generateCytoscapeConfig <- function(nodes, edges, `source-arrow-shape` = "triangle", `target-arrow-shape` = "triangle" ) + ), + list( + selector = "node[node_type = 'ptm']", + style = list( + shape = "ellipse", + width = "20px", + height = "20px", + `background-color` = "#9932CC", + `border-color` = "#5B0080", + `border-width` = 1.5, + label = "data(label)", + `font-size` = "8px", + `font-weight` = "normal", + color = "#ffffff", + `text-valign` = "center", + `text-halign` = "center", + `text-wrap` = "wrap", + `text-max-width` = "18px" + ) + ), + # --- PTM attachment edge style (hide label, keep it subtle) --- + list( + selector = "edge[edge_type = 'ptm_attachment']", + style = list( + `line-style` = "dotted", + `line-color` = "#9932CC", + width = 1.5, + `target-arrow-shape` = "none", + `source-arrow-shape` = "none", + label = "", # no label on these connector edges + `z-index` = 0 # render behind main edges + ) ) ) From b4b84d6df40b32de2d74790e17fdb4381dde4199 Mon Sep 17 00:00:00 2001 From: tonywu1999 Date: Wed, 25 Feb 2026 10:38:34 -0500 Subject: [PATCH 2/4] change PTM site color --- R/visualizeNetworksWithHTML.R | 112 ++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/R/visualizeNetworksWithHTML.R b/R/visualizeNetworksWithHTML.R index 3885c44..49ad9f4 100644 --- a/R/visualizeNetworksWithHTML.R +++ b/R/visualizeNetworksWithHTML.R @@ -308,62 +308,72 @@ createNodeElements <- function(nodes, displayLabelType = "id") { "id" } - node_elements <- apply(cbind(nodes, color = node_colors), 1, function(row) { - display_label <- if (label_column == "hgncName" && !is.na(row["hgncName"]) && row["hgncName"] != "") { - row["hgncName"] + node_elements <- c() + ptm_elements <- c() + emitted_proteins <- c() + + for (i in seq_len(nrow(nodes))) { + row <- nodes[i, ] + color <- node_colors[i] + has_site <- "Site" %in% names(nodes) && !is.na(row$Site) && trimws(row$Site) != "" + + display_label <- if (label_column == "hgncName" && !is.na(row$hgncName) && row$hgncName != "") { + row$hgncName } else { - row["id"] + row$id } - paste0("{ data: { id: '", row["id"], "', label: '", display_label, - "', color: '", row["color"], "', node_type: 'protein' } }") - }) - - # Generate PTM site child nodes + edges - ptm_elements <- c() - if ("Site" %in% names(nodes)) { - for (i in seq_len(nrow(nodes))) { - site_val <- nodes$Site[i] - if (!is.na(site_val) && trimws(site_val) != "") { - parent_id <- nodes$id[i] - # Sites may be delimited by _, comma, semicolon, or pipe - sites <- trimws(unlist(strsplit(as.character(site_val), "[_,;|]"))) - sites <- sites[sites != ""] - for (site in sites) { - ptm_node_id <- paste0(parent_id, "__ptm__", site) - # Escape for JS string safety - safe_site <- escape_js_string(site) - safe_parent <- escape_js_string(parent_id) - safe_ptm_id <- escape_js_string(ptm_node_id) - - # PTM node - ptm_elements <- c(ptm_elements, - paste0("{ data: { id: '", safe_ptm_id, - "', label: '", safe_site, - "', parent_protein: '", safe_parent, - "', node_type: 'ptm' } }") - ) - # Edge connecting PTM node to parent protein - ptm_edge_id <- paste0(parent_id, "__ptm_edge__", site) - ptm_elements <- c(ptm_elements, - paste0("{ data: { id: '", escape_js_string(ptm_edge_id), - "', source: '", safe_parent, - "', target: '", safe_ptm_id, - "', edge_type: 'ptm_attachment',", - " category: 'ptm_attachment',", - " interaction: '',", - " color: '#9932CC',", - " line_style: 'dotted',", - " arrow_shape: 'none',", - " width: 1.5,", - " tooltip: '' } }") - ) - } + + # Always emit the protein node, but only once per unique id + if (!(row$id %in% emitted_proteins)) { + node_elements <- c(node_elements, + paste0("{ data: { id: '", escape_js_string(row$id), + "', label: '", escape_js_string(display_label), + "', color: '", color, + "', node_type: 'protein' } }") + ) + emitted_proteins <- c(emitted_proteins, row$id) + } + + # Emit one PTM child node + attachment edge per individual site + if (has_site) { + sites <- trimws(unlist(strsplit(as.character(row$Site), "[_,;|]"))) + sites <- sites[sites != ""] + + for (site in sites) { + ptm_node_id <- paste0(row$id, "__ptm__", site) + safe_ptm_id <- escape_js_string(ptm_node_id) + safe_parent <- escape_js_string(row$id) + safe_site <- escape_js_string(site) + + ptm_elements <- c(ptm_elements, + paste0("{ data: { id: '", safe_ptm_id, + "', label: '", safe_site, + "', color: '", color, + "', parent_protein: '", safe_parent, + "', node_type: 'ptm' } }") + ) + + ptm_edge_id <- escape_js_string(paste0(row$id, "__ptm_edge__", site)) + ptm_elements <- c(ptm_elements, + paste0("{ data: { id: '", ptm_edge_id, + "', source: '", safe_parent, + "', target: '", safe_ptm_id, + "', edge_type: 'ptm_attachment',", + " category: 'ptm_attachment',", + " interaction: '',", + " color: '", color, "',", + " line_style: 'dotted',", + " arrow_shape: 'none',", + " width: 1.5,", + " tooltip: '' } }") + ) } } } return(c(node_elements, ptm_elements)) } + createEdgeElements <- function(edges, nodes = NULL) { if (nrow(edges) == 0) return(list()) @@ -542,13 +552,13 @@ generateCytoscapeConfig <- function(nodes, edges, shape = "ellipse", width = "20px", height = "20px", - `background-color` = "#9932CC", - `border-color` = "#5B0080", + `background-color` = "data(color)", # <-- was hardcoded "#9932CC" + `border-color` = "#333", # <-- neutral border instead of purple `border-width` = 1.5, label = "data(label)", `font-size` = "8px", `font-weight` = "normal", - color = "#ffffff", + color = "#000000", # <-- dark text works across the logFC palette `text-valign` = "center", `text-halign` = "center", `text-wrap` = "wrap", From 4ff6f5d491eff50386eb0197f2005a0980a36b80 Mon Sep 17 00:00:00 2001 From: tonywu1999 Date: Wed, 25 Feb 2026 10:54:27 -0500 Subject: [PATCH 3/4] place child PTM nodes adjacent to parent nodes and add compound nodes --- R/visualizeNetworksWithHTML.R | 85 +++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/R/visualizeNetworksWithHTML.R b/R/visualizeNetworksWithHTML.R index 49ad9f4..6f68882 100644 --- a/R/visualizeNetworksWithHTML.R +++ b/R/visualizeNetworksWithHTML.R @@ -311,6 +311,16 @@ createNodeElements <- function(nodes, displayLabelType = "id") { node_elements <- c() ptm_elements <- c() emitted_proteins <- c() + emitted_compounds <- c() + + # Pre-compute which protein ids have at least one PTM site row, + # so we know upfront whether a compound wrapper is needed + has_ptm_sites <- if ("Site" %in% names(nodes)) { + ids_with_sites <- unique(nodes$id[!is.na(nodes$Site) & trimws(nodes$Site) != ""]) + ids_with_sites + } else { + c() + } for (i in seq_len(nrow(nodes))) { row <- nodes[i, ] @@ -323,13 +333,32 @@ createNodeElements <- function(nodes, displayLabelType = "id") { row$id } - # Always emit the protein node, but only once per unique id + needs_compound <- row$id %in% has_ptm_sites + compound_id <- paste0(row$id, "__compound__") + + # Emit invisible compound container node once per protein that has PTM children + if (needs_compound && !(compound_id %in% emitted_compounds)) { + node_elements <- c(node_elements, + paste0("{ data: { id: '", escape_js_string(compound_id), + "', node_type: 'compound' } }") + ) + emitted_compounds <- c(emitted_compounds, compound_id) + } + + # Emit protein node once, assigning it to the compound if one exists if (!(row$id %in% emitted_proteins)) { + parent_field <- if (needs_compound) { + paste0(", parent: '", escape_js_string(compound_id), "'") + } else { + "" + } node_elements <- c(node_elements, paste0("{ data: { id: '", escape_js_string(row$id), "', label: '", escape_js_string(display_label), "', color: '", color, - "', node_type: 'protein' } }") + "', node_type: 'protein'", + parent_field, + " } }") ) emitted_proteins <- c(emitted_proteins, row$id) } @@ -345,12 +374,14 @@ createNodeElements <- function(nodes, displayLabelType = "id") { safe_parent <- escape_js_string(row$id) safe_site <- escape_js_string(site) + # PTM node also belongs to the same compound container ptm_elements <- c(ptm_elements, paste0("{ data: { id: '", safe_ptm_id, "', label: '", safe_site, "', color: '", color, "', parent_protein: '", safe_parent, - "', node_type: 'ptm' } }") + "', parent: '", escape_js_string(compound_id), "'", + ", node_type: 'ptm' } }") ) ptm_edge_id <- escape_js_string(paste0(row$id, "__ptm_edge__", site)) @@ -577,6 +608,17 @@ generateCytoscapeConfig <- function(nodes, edges, label = "", # no label on these connector edges `z-index` = 0 # render behind main edges ) + ), + list( + selector = "node[node_type = 'compound']", + style = list( + `background-opacity` = 0, + `border-width` = 0, + `border-opacity` = 0, + `padding` = "10px", + label = "", + `z-index` = 0 + ) ) ) @@ -639,6 +681,43 @@ generateJavaScriptCode <- function(config) { layout: ", layout_js, " }); + // After layout completes, reposition PTM nodes directly beside their parent protein + cy.on('layoutstop', function() { + var ptmNodes = cy.nodes('[node_type = \"ptm\"]'); + ptmNodes.forEach(function(ptmNode) { + var parentId = ptmNode.data('parent_protein'); + var parentNode = cy.getElementById(parentId); + if (parentNode.length === 0) return; + + var parentPos = parentNode.position(); + var parentW = parentNode.outerWidth(); + var parentH = parentNode.outerHeight(); + var ptmR = ptmNode.outerWidth() / 2; // PTM node is a small circle + + // Collect all PTM siblings so we can fan them around the parent + var siblings = cy.nodes('[parent_protein = \"' + parentId + '\"]'); + var idx = siblings.indexOf(ptmNode); + var total = siblings.length; + + // Distribute siblings evenly across the bottom arc of the parent + // angleStart/End in radians: spread across bottom 180 degrees + var angleStart = Math.PI * 0.15; + var angleEnd = Math.PI * 0.85; + var angle = total === 1 + ? Math.PI / 2 // single PTM: directly below center + : angleStart + (angleEnd - angleStart) * (idx / (total - 1)); + + // Place PTM node just outside the parent border + var offsetX = (parentW / 2 + ptmR + 4) * Math.cos(angle); + var offsetY = (parentH / 2 + ptmR + 4) * Math.sin(angle); + + ptmNode.position({ + x: parentPos.x + offsetX, + y: parentPos.y + offsetY + }); + }); + }); + // Create tooltip element var tooltip = document.createElement('div'); tooltip.style.cssText = ` From c5d4a5074e7eecab310be34c0ff008dc6ad8cc75 Mon Sep 17 00:00:00 2001 From: tonywu1999 Date: Wed, 25 Feb 2026 16:48:55 -0500 Subject: [PATCH 4/4] coderabbit comment --- R/visualizeNetworksWithHTML.R | 55 ++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/R/visualizeNetworksWithHTML.R b/R/visualizeNetworksWithHTML.R index 6f68882..7a2316d 100644 --- a/R/visualizeNetworksWithHTML.R +++ b/R/visualizeNetworksWithHTML.R @@ -312,6 +312,8 @@ createNodeElements <- function(nodes, displayLabelType = "id") { ptm_elements <- c() emitted_proteins <- c() emitted_compounds <- c() + emitted_ptm_nodes <- c() + emitted_ptm_edges <- c() # Pre-compute which protein ids have at least one PTM site row, # so we know upfront whether a compound wrapper is needed @@ -366,7 +368,7 @@ createNodeElements <- function(nodes, displayLabelType = "id") { # Emit one PTM child node + attachment edge per individual site if (has_site) { sites <- trimws(unlist(strsplit(as.character(row$Site), "[_,;|]"))) - sites <- sites[sites != ""] + sites <- unique(sites[sites != ""]) for (site in sites) { ptm_node_id <- paste0(row$id, "__ptm__", site) @@ -375,29 +377,36 @@ createNodeElements <- function(nodes, displayLabelType = "id") { safe_site <- escape_js_string(site) # PTM node also belongs to the same compound container - ptm_elements <- c(ptm_elements, - paste0("{ data: { id: '", safe_ptm_id, - "', label: '", safe_site, - "', color: '", color, - "', parent_protein: '", safe_parent, - "', parent: '", escape_js_string(compound_id), "'", - ", node_type: 'ptm' } }") - ) + if (!(ptm_node_id %in% emitted_ptm_nodes)) { + ptm_elements <- c(ptm_elements, + paste0("{ data: { id: '", safe_ptm_id, + "', label: '", safe_site, + "', color: '", color, + "', parent_protein: '", safe_parent, + "', parent: '", escape_js_string(compound_id), "'", + ", node_type: 'ptm' } }") + ) + emitted_ptm_nodes <- c(emitted_ptm_nodes, ptm_node_id) + } - ptm_edge_id <- escape_js_string(paste0(row$id, "__ptm_edge__", site)) - ptm_elements <- c(ptm_elements, - paste0("{ data: { id: '", ptm_edge_id, - "', source: '", safe_parent, - "', target: '", safe_ptm_id, - "', edge_type: 'ptm_attachment',", - " category: 'ptm_attachment',", - " interaction: '',", - " color: '", color, "',", - " line_style: 'dotted',", - " arrow_shape: 'none',", - " width: 1.5,", - " tooltip: '' } }") - ) + ptm_edge_id_raw <- paste0(row$id, "__ptm_edge__", site) + if (!(ptm_edge_id_raw %in% emitted_ptm_edges)) { + ptm_edge_id <- escape_js_string(ptm_edge_id_raw) + ptm_elements <- c(ptm_elements, + paste0("{ data: { id: '", ptm_edge_id, + "', source: '", safe_parent, + "', target: '", safe_ptm_id, + "', edge_type: 'ptm_attachment',", + " category: 'ptm_attachment',", + " interaction: '',", + " color: '", color, "',", + " line_style: 'dotted',", + " arrow_shape: 'none',", + " width: 1.5,", + " tooltip: '' } }") + ) + emitted_ptm_edges <- c(emitted_ptm_edges, ptm_edge_id_raw) + } } } }