From d1502a226c78e17f5086c5dc74bb3b58ff654cc7 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 12:49:47 -0500 Subject: [PATCH 01/17] initial attempt to refactor javascript code --- DESCRIPTION | 4 +- NAMESPACE | 4 + R/cytoscapeNetwork.R | 492 +++++++++++++++++++++++++ inst/htmlwidgets/cytoscapeNetwork.js | 356 ++++++++++++++++++ inst/htmlwidgets/cytoscapeNetwork.yaml | 40 ++ inst/script/examples.R | 139 +++++++ man/cytoscapeNetwork.Rd | 68 ++++ man/cytoscapeNetworkOutput.Rd | 18 + man/renderCytoscapeNetwork.Rd | 20 + 9 files changed, 1140 insertions(+), 1 deletion(-) create mode 100644 R/cytoscapeNetwork.R create mode 100644 inst/htmlwidgets/cytoscapeNetwork.js create mode 100644 inst/htmlwidgets/cytoscapeNetwork.yaml create mode 100644 inst/script/examples.R create mode 100644 man/cytoscapeNetwork.Rd create mode 100644 man/cytoscapeNetworkOutput.Rd create mode 100644 man/renderCytoscapeNetwork.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 502fc4f..aa964e1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -23,7 +23,9 @@ Imports: httr, jsonlite, r2r, - tidyr + tidyr, + htmlwidgets, + grDevices Suggests: data.table, BiocStyle, diff --git a/NAMESPACE b/NAMESPACE index 94b06a0..e7df7ff 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,10 +1,13 @@ # Generated by roxygen2: do not edit by hand export(annotateProteinInfoFromIndra) +export(cytoscapeNetwork) +export(cytoscapeNetworkOutput) export(exportNetworkToHTML) export(generateCytoscapeConfig) export(getSubnetworkFromIndra) export(previewNetworkInBrowser) +export(renderCytoscapeNetwork) export(visualizeNetworks) importFrom(RCy3,addAnnotationShape) importFrom(RCy3,addAnnotationText) @@ -18,6 +21,7 @@ importFrom(RCy3,mapVisualProperty) importFrom(RCy3,setVisualStyle) importFrom(grDevices,colorRamp) importFrom(grDevices,rgb) +importFrom(htmlwidgets,createWidget) importFrom(httr,GET) importFrom(httr,POST) importFrom(httr,add_headers) diff --git a/R/cytoscapeNetwork.R b/R/cytoscapeNetwork.R new file mode 100644 index 0000000..49c7578 --- /dev/null +++ b/R/cytoscapeNetwork.R @@ -0,0 +1,492 @@ +# R/cytoscapeNetwork.R +# +# htmlwidgets binding for the Cytoscape network visualisation. +# The heavy-lifting JS lives in inst/htmlwidgets/cytoscapeNetwork.js. +# This file is responsible for: +# 1. Pre-processing nodes / edges in R (colour mapping, element serialisation) +# 2. Calling htmlwidgets::createWidget() to hand everything to the JS side. + +# ── Internal helpers (not exported) ──────────────────────────────────────── + +#' Map logFC values to a blue-grey-red colour palette +#' @keywords internal +#' @noRd +.mapLogFCToColor <- function(logFC_values) { + colors <- c("#ADD8E6", "#ADD8E6", "#D3D3D3", "#FFA590", "#FFA590") + + if (all(is.na(logFC_values)) || + length(unique(logFC_values[!is.na(logFC_values)])) <= 1) { + return(rep("#D3D3D3", length(logFC_values))) + } + + default_max <- 2 + max_logFC <- max(c(abs(logFC_values), default_max), na.rm = TRUE) + min_logFC <- -max_logFC + color_map <- grDevices::colorRamp(colors) + normalized <- (logFC_values - min_logFC) / (max_logFC - min_logFC) + normalized[is.na(normalized)] <- 0.5 + rgb_colors <- color_map(normalized) + grDevices::rgb(rgb_colors[, 1], rgb_colors[, 2], rgb_colors[, 3], + maxColorValue = 255) +} + +#' Safely escape a string for embedding in a JS single-quoted literal +#' @keywords internal +#' @noRd +.escJS <- function(x) { + if (is.null(x)) return("") + x <- as.character(x) + x <- gsub("\\\\", "\\\\\\\\", x) + x <- gsub("'", "\\\\'", x) + x <- gsub("\r", "\\\\r", x) + x <- gsub("\n", "\\\\n", x) + x +} + +#' Relationship properties lookup +#' @keywords internal +#' @noRd +.relProps <- function() { + list( + complex = list( + types = "Complex", + color = "#8B4513", + style = "solid", + arrow = "none", + width = 4, + consolidate = "undirected" + ), + regulatory = list( + types = c("Inhibition", "Activation", "IncreaseAmount", "DecreaseAmount"), + colors = list(Inhibition = "#FF4444", + Activation = "#44AA44", + IncreaseAmount = "#4488FF", + DecreaseAmount = "#FF8844"), + style = "solid", + arrow = "triangle", + width = 3, + consolidate = "bidirectional" + ), + phosphorylation = list( + types = "Phosphorylation", + color = "#9932CC", + style = "dashed", + arrow = "triangle", + width = 2, + consolidate = "directed" + ), + other = list( + color = "#666666", + style = "dotted", + arrow = "triangle", + width = 2, + consolidate = "directed" + ) + ) +} + +#' Classify an interaction string into a relationship category +#' @keywords internal +#' @noRd +.classify <- function(interaction) { + props <- .relProps() + for (cat_name in names(props)) { + if (!is.null(props[[cat_name]]$types) && + interaction %in% props[[cat_name]]$types) { + return(cat_name) + } + } + "other" +} + +#' Retrieve edge colour / style / arrow / width +#' @keywords internal +#' @noRd +.edgeStyle <- function(interaction, category, edge_type) { + props <- .relProps() + p <- if (category %in% names(props)) props[[category]] else props$other + + color <- if (category == "regulatory" && !is.null(p$colors)) { + base <- sub(" \\(bidirectional\\)", "", interaction) + if (base %in% names(p$colors)) p$colors[[base]] else "#666666" + } else { + p$color + } + + arrow <- switch(edge_type, + undirected = "none", + bidirectional = "triangle", + p$arrow + ) + + list(color = color, style = p$style, arrow = arrow, width = p$width) +} + +#' Aggregate PTM overlap between edge targets and node Site columns +#' @keywords internal +#' @noRd +.ptmOverlap <- function(edges, nodes) { + if (nrow(edges) == 0 || is.null(nodes)) return(setNames(character(0), character(0))) + + edges$edge_key <- paste(edges$source, edges$target, edges$interaction, sep = "-") + unique_keys <- unique(edges$edge_key) + result <- setNames(character(length(unique_keys)), unique_keys) + + for (key in unique_keys) { + sub_edges <- edges[edges$edge_key == key, ] + all_sites <- c() + + for (i in seq_len(nrow(sub_edges))) { + e <- sub_edges[i, ] + if (!is.na(e$target) && "site" %in% names(e) && !is.na(e$site)) { + tnodes <- nodes[nodes$id == e$target, ] + if (nrow(tnodes) > 0 && "Site" %in% names(tnodes)) { + edge_sites <- trimws(unlist(strsplit(as.character(e$site), "[,;|]"))) + for (j in seq_len(nrow(tnodes))) { + if (!is.na(tnodes$Site[j])) { + node_sites <- trimws(unlist(strsplit(as.character(tnodes$Site[j]), "_"))) + overlap <- intersect(edge_sites, node_sites) + overlap <- overlap[overlap != "" & !is.na(overlap)] + all_sites <- c(all_sites, overlap) + } + } + } + } + } + + u <- unique(all_sites[all_sites != "" & !is.na(all_sites)]) + result[key] <- if (length(u) == 0) { + "" + } else if (length(u) == 1) { + paste0("Overlapping PTM site: ", u) + } else { + paste0("Overlapping PTM sites: ", paste(u, collapse = ", ")) + } + } + result +} + +#' Consolidate bidirectional / undirected edges +#' @keywords internal +#' @noRd +.consolidateEdges <- function(edges, nodes = NULL) { + if (nrow(edges) == 0) return(edges) + + ptm_map <- .ptmOverlap(edges, nodes) + props <- .relProps() + consolidated <- list() + processed <- c() + + for (i in seq_len(nrow(edges))) { + e <- edges[i, ] + pair_key <- paste(sort(c(e$source, e$target)), e$interaction, collapse = "-") + if (pair_key %in% processed) next + + cat <- .classify(e$interaction) + rev_edges <- edges[edges$source == e$target & + edges$target == e$source & + edges$interaction == e$interaction, ] + con_type <- props[[cat]]$consolidate + edge_key <- paste(e$source, e$target, e$interaction, sep = "-") + ptm_txt <- if (edge_key %in% names(ptm_map)) ptm_map[[edge_key]] else "" + + if (nrow(rev_edges) > 0 && con_type %in% c("undirected", "bidirectional")) { + new_interaction <- if (con_type == "undirected") e$interaction else + paste(e$interaction, "(bidirectional)") + new_edge <- data.frame(source = e$source, + target = e$target, + interaction = new_interaction, + edge_type = if (con_type == "undirected") "undirected" else "bidirectional", + category = cat, + ptm_overlap = ptm_txt, + stringsAsFactors = FALSE) + for (col in setdiff(names(e), c("source", "target", "interaction"))) { + new_edge[[col]] <- e[[col]] + } + key <- paste(e$source, e$target, new_interaction, sep = "-") + consolidated[[key]] <- new_edge + processed <- c(processed, pair_key) + } else { + de <- e + de$edge_type <- "directed" + de$category <- cat + de$ptm_overlap <- ptm_txt + key <- paste(e$source, e$target, e$interaction, sep = "-") + consolidated[[key]] <- de + } + } + + if (length(consolidated) > 0) { + result <- do.call(rbind, consolidated) + rownames(result) <- NULL + result + } else { + edges[0, ] + } +} + +#' Build the list of Cytoscape element objects (nodes + edges) +#' +#' Returns a list of named lists — jsonlite will serialise them cleanly. +#' @keywords internal +#' @noRd +.buildElements <- function(nodes, edges, display_label_type = "id") { + # ── node colours ────────────────────────────────────────────────────── + node_colors <- if ("logFC" %in% names(nodes)) { + .mapLogFCToColor(nodes$logFC) + } else { + rep("#D3D3D3", nrow(nodes)) + } + + label_col <- if (display_label_type == "hgncName" && + "hgncName" %in% names(nodes)) "hgncName" else "id" + + has_ptm_sites <- if ("Site" %in% names(nodes)) { + unique(nodes$id[!is.na(nodes$Site) & trimws(nodes$Site) != ""]) + } else { + character(0) + } + + elements <- list() + emitted_prots <- character(0) + emitted_cpds <- character(0) + emitted_ptm_n <- character(0) + emitted_ptm_e <- character(0) + + 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_col == "hgncName" && + !is.na(row$hgncName) && row$hgncName != "") + row$hgncName else row$id + + needs_compound <- row$id %in% has_ptm_sites + compound_id <- paste0(row$id, "__compound__") + + # Compound container + if (needs_compound && !(compound_id %in% emitted_cpds)) { + elements <- c(elements, list( + list(data = list(id = compound_id, + node_type = "compound")) + )) + emitted_cpds <- c(emitted_cpds, compound_id) + } + + # Protein node + if (!(row$id %in% emitted_prots)) { + nd <- list(id = row$id, + label = display_label, + color = color, + node_type = "protein") + if (needs_compound) nd$parent <- compound_id + elements <- c(elements, list(list(data = nd))) + emitted_prots <- c(emitted_prots, row$id) + } + + # PTM child nodes + attachment edges + if (has_site) { + sites <- unique(trimws(unlist(strsplit(as.character(row$Site), "[_,;|]")))) + sites <- sites[sites != ""] + + for (site in sites) { + ptm_nid <- paste0(row$id, "__ptm__", site) + if (!(ptm_nid %in% emitted_ptm_n)) { + elements <- c(elements, list(list(data = list( + id = ptm_nid, + label = site, + color = color, + parent_protein = row$id, + parent = compound_id, + node_type = "ptm" + )))) + emitted_ptm_n <- c(emitted_ptm_n, ptm_nid) + } + + ptm_eid <- paste0(row$id, "__ptm_edge__", site) + if (!(ptm_eid %in% emitted_ptm_e)) { + elements <- c(elements, list(list(data = list( + id = ptm_eid, + source = row$id, + target = ptm_nid, + edge_type = "ptm_attachment", + category = "ptm_attachment", + interaction = "", + color = color, + line_style = "dotted", + arrow_shape = "none", + width = 1.5, + tooltip = "" + )))) + emitted_ptm_e <- c(emitted_ptm_e, ptm_eid) + } + } + } + } + + # ── edges ───────────────────────────────────────────────────────────── + if (!is.null(edges) && nrow(edges) > 0) { + con <- .consolidateEdges(edges, nodes) + + for (i in seq_len(nrow(con))) { + row <- con[i, ] + sty <- .edgeStyle(row$interaction, row$category, row$edge_type) + eid <- paste(row$source, row$target, row$interaction, sep = "-") + elink <- if ("evidenceLink" %in% names(row)) { + ev <- row$evidenceLink + if (is.na(ev) || ev == "NA") "" else as.character(ev) + } else "" + + elements <- c(elements, list(list(data = list( + id = eid, + source = row$source, + target = row$target, + interaction = row$interaction, + edge_type = row$edge_type, + category = row$category, + evidenceLink = elink, + color = sty$color, + line_style = sty$style, + arrow_shape = sty$arrow, + width = sty$width, + tooltip = if (!is.null(row$ptm_overlap)) row$ptm_overlap else "" + )))) + } + } + + elements +} + + +# ── Public API ────────────────────────────────────────────────────────────── + +#' Render a Cytoscape network visualisation +#' +#' Creates an interactive network diagram powered by Cytoscape.js and the dagre +#' layout algorithm. Nodes can carry log fold-change (logFC) values which are +#' mapped to a blue-grey-red colour gradient. PTM (post-translational +#' modification) site information is shown as small satellite nodes and edge +#' overlaps are surfaced as hover tooltips. +#' +#' @param nodes Data frame with at minimum an \code{id} column. Optional +#' columns: \code{logFC} (numeric), \code{hgncName} +#' (character), \code{Site} (character, underscore-separated +#' PTM site list). +#' @param edges Data frame with columns \code{source}, \code{target}, +#' \code{interaction}. Optional: \code{site}, +#' \code{evidenceLink}. +#' @param displayLabelType \code{"id"} (default) or \code{"hgncName"} – +#' controls which column is used as the visible node label. +#' @param nodeFontSize Font size (px) for node labels. Default \code{12}. +#' @param layoutOptions Named list of dagre layout options to override the +#' defaults (e.g. \code{list(rankDir = "LR")}). +#' @param width,height Widget dimensions passed to +#' \code{\link[htmlwidgets]{createWidget}}. +#' @param elementId Optional explicit HTML element id. +#' +#' @return An \code{htmlwidget} object that renders in R Markdown, Shiny, or +#' the RStudio Viewer pane. +#' +#' @examples +#' \dontrun{ +#' nodes <- data.frame( +#' id = c("TP53", "MDM2", "CDKN1A"), +#' logFC = c(1.5, -0.8, 2.1), +#' stringsAsFactors = FALSE +#' ) +#' edges <- data.frame( +#' source = c("TP53", "MDM2"), +#' target = c("MDM2", "TP53"), +#' interaction = c("Activation", "Inhibition"), +#' stringsAsFactors = FALSE +#' ) +#' cytoscapeNetwork(nodes, edges) +#' } +#' +#' @importFrom htmlwidgets createWidget +#' @importFrom grDevices colorRamp rgb +#' @export +cytoscapeNetwork <- function(nodes, + edges = data.frame(), + displayLabelType = "id", + nodeFontSize = 12, + layoutOptions = NULL, + width = NULL, + height = NULL, + elementId = NULL) { + + # Validate inputs + if (!is.data.frame(nodes) || !("id" %in% names(nodes))) { + stop("`nodes` must be a data frame with at least an `id` column.") + } + if (!is.data.frame(edges)) edges <- data.frame() + + # Build layout config + default_layout <- list( + name = "dagre", + rankDir = "TB", + animate = TRUE, + fit = TRUE, + padding = 30, + spacingFactor = 1.5, + nodeSep = 50, + edgeSep = 20, + rankSep = 80 + ) + layout <- default_layout + if (!is.null(layoutOptions)) { + for (nm in names(layoutOptions)) layout[[nm]] <- layoutOptions[[nm]] + } + + # Build element list + elements <- .buildElements(nodes, edges, displayLabelType) + + # Package everything for the JS side + x <- list( + elements = elements, + layout = layout, + node_font_size = nodeFontSize + ) + + htmlwidgets::createWidget( + name = "cytoscapeNetwork", + x = x, + width = width, + height = height, + package = "MSstatsBioNet", + elementId = elementId + ) +} + + +# ── Shiny helpers ─────────────────────────────────────────────────────────── + +#' Shiny output binding for cytoscapeNetwork +#' @inheritParams htmlwidgets::shinyWidgetOutput +#' @export +cytoscapeNetworkOutput <- function(outputId, + width = "100%", + height = "500px") { + htmlwidgets::shinyWidgetOutput( + outputId = outputId, + name = "cytoscapeNetwork", + width = width, + height = height, + package = "cytoscapeNetwork" + ) +} + +#' Shiny render binding for cytoscapeNetwork +#' @inheritParams htmlwidgets::shinyRenderWidget +#' @export +renderCytoscapeNetwork <- function(expr, env = parent.frame(), quoted = FALSE) { + if (!quoted) expr <- substitute(expr) + htmlwidgets::shinyRenderWidget( + expr = expr, + outputFunction = cytoscapeNetworkOutput, + env = env, + quoted = TRUE + ) +} diff --git a/inst/htmlwidgets/cytoscapeNetwork.js b/inst/htmlwidgets/cytoscapeNetwork.js new file mode 100644 index 0000000..65fb626 --- /dev/null +++ b/inst/htmlwidgets/cytoscapeNetwork.js @@ -0,0 +1,356 @@ +/* ========================================================================== + cytoscapeNetwork.js + htmlwidgets binding for the cytoscapeNetwork R package. + + Dependencies (declared in cytoscapeNetwork.yaml): + - cytoscape.min.js + - dagre.min.js / graphlib.min.js + - cytoscape-dagre.js + + The `x` object passed from R (via createWidget / jsonlite serialisation): + { + nodes : [ { id, label, color, node_type, parent?, parent_protein? }, … ], + edges : [ { source, target, id, interaction, edge_type, category, + color, line_style, arrow_shape, width, tooltip, + evidenceLink? }, … ], + layout : { name, rankDir, … }, // dagre options + container_id : "network-cy", // ignored – we use el + node_font_size : 12 + } + ========================================================================== */ + +HTMLWidgets.widget({ + + name: "cytoscapeNetwork", + type: "output", + + /* ── factory ──────────────────────────────────────────────────────────── */ + factory: function (el, width, height) { + + // State kept between renderValue calls so we can destroy cleanly + var cy = null; + var tooltip = null; + + /* helper – open a URL safely in a new tab */ + function openSafe(url) { + if (!url || typeof url !== "string") return; + url = url.trim(); + if (!url || url === "NA") return; + if (!/^https?:\/\//i.test(url)) return; + var w = window.open(url, "_blank", "noopener,noreferrer"); + if (w) w.opener = null; + } + + /* helper – build Cytoscape stylesheet from x.node_font_size */ + function buildStyle(nodeFontSize) { + return [ + /* ── proteins / default nodes ─────────────────────────────────── */ + { + selector: "node[node_type = 'protein']", + style: { + "background-color": "data(color)", + "label": "data(label)", + "shape": "round-rectangle", + "font-size": (nodeFontSize || 12) + "px", + "font-weight": "bold", + "color": "#000", + "text-valign": "center", + "text-halign": "center", + "text-wrap": "wrap", + "text-max-width": "140px", + "border-width": 2, + "border-color": "#333", + "padding": "5px", + /* dynamic width/height via mappers */ + "width": "mapData(label.length, 0, 20, 60, 150)", + "height": 40 + } + }, + /* ── PTM child nodes ─────────────────────────────────────────── */ + { + selector: "node[node_type = 'ptm']", + style: { + "shape": "ellipse", + "width": 20, + "height": 20, + "background-color": "data(color)", + "border-color": "#333", + "border-width": 1.5, + "label": "data(label)", + "font-size": "8px", + "font-weight": "normal", + "color": "#000", + "text-valign": "center", + "text-halign": "center", + "text-wrap": "wrap", + "text-max-width": "18px" + } + }, + /* ── invisible compound containers ──────────────────────────── */ + { + selector: "node[node_type = 'compound']", + style: { + "background-opacity": 0, + "border-width": 0, + "border-opacity": 0, + "padding": "10px", + "label": "", + "z-index": 0 + } + }, + /* ── all edges (defaults) ────────────────────────────────────── */ + { + selector: "edge", + style: { + "width": "data(width)", + "line-color": "data(color)", + "line-style": "data(line_style)", + "label": "data(interaction)", + "curve-style": "bezier", + "target-arrow-shape": "data(arrow_shape)", + "target-arrow-color": "data(color)", + "source-arrow-shape": "none", + "source-arrow-color": "data(color)", + "edge-text-rotation": "autorotate", + "text-margin-y": -12, + "text-halign": "center", + "font-size": "11px", + "font-weight": "bold", + "color": "data(color)", + "text-background-color": "#ffffff", + "text-background-opacity": 0.8, + "text-background-padding": "2px" + } + }, + /* ── bidirectional edges – source arrow too ──────────────────── */ + { + selector: "edge[edge_type = 'bidirectional']", + style: { + "source-arrow-shape": "triangle", + "target-arrow-shape": "triangle" + } + }, + /* ── undirected (complex) edges ──────────────────────────────── */ + { + selector: "edge[category = 'complex']", + style: { + "line-style": "solid", + "target-arrow-shape": "none", + "source-arrow-shape": "none" + } + }, + /* ── phosphorylation edges ───────────────────────────────────── */ + { + selector: "edge[category = 'phosphorylation']", + style: { + "line-style": "dashed", + "width": 2 + } + }, + /* ── PTM attachment edges ────────────────────────────────────── */ + { + selector: "edge[edge_type = 'ptm_attachment']", + style: { + "line-style": "dotted", + "line-color": "#9932CC", + "width": 1.5, + "target-arrow-shape": "none", + "source-arrow-shape": "none", + "label": "", + "z-index": 0 + } + }, + /* ── selected highlight ──────────────────────────────────────── */ + { + selector: ":selected", + style: { + "border-width": 4, + "border-color": "#FFD700", + "line-color": "#FFD700" + } + } + ]; + } + + /* helper – reposition PTM nodes in a small arc below their parent */ + function repositionPTMNodes(cyInstance) { + var ptmNodes = cyInstance.nodes('[node_type = "ptm"]'); + ptmNodes.forEach(function (ptmNode) { + var parentId = ptmNode.data("parent_protein"); + var parentNode = cyInstance.getElementById(parentId); + if (!parentNode || parentNode.length === 0) return; + + var parentPos = parentNode.position(); + var parentW = parentNode.outerWidth(); + var parentH = parentNode.outerHeight(); + var ptmR = ptmNode.outerWidth() / 2; + + var siblings = cyInstance.nodes('[parent_protein = "' + parentId + '"]'); + var idx = siblings.indexOf(ptmNode); + var total = siblings.length; + + var angleStart = Math.PI * 0.15; + var angleEnd = Math.PI * 0.85; + var angle = (total === 1) + ? Math.PI / 2 + : angleStart + (angleEnd - angleStart) * (idx / (total - 1)); + + 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 + }); + }); + } + + /* helper – build the legend panel beside the network */ + function buildLegend(cyInstance, legendEl) { + if (!legendEl) return; + + var edgeTypeConfigs = [ + { type: "Activation", color: "#44AA44", label: "Activation", dash: false }, + { type: "Inhibition", color: "#FF4444", label: "Inhibition", dash: false }, + { type: "IncreaseAmount", color: "#4488FF", label: "Increase Amount", dash: false }, + { type: "DecreaseAmount", color: "#FF8844", label: "Decrease Amount", dash: false }, + { type: "Phosphorylation",color: "#9932CC", label: "Phosphorylation", dash: true }, + { type: "Complex", color: "#8B4513", label: "Complex", dash: false } + ]; + + var existingTypes = {}; + cyInstance.edges().forEach(function (e) { + var raw = e.data("interaction") || ""; + existingTypes[raw.replace(" (bidirectional)", "")] = true; + }); + + var edgeItems = edgeTypeConfigs + .filter(function (c) { return existingTypes[c.type]; }) + .map(function (c) { + var dash = c.dash ? "border-top: 2px dashed " + c.color + ";" : "background-color:" + c.color + ";"; + return '
' + + '
' + + '' + c.label + '
'; + }) + .join(""); + + legendEl.innerHTML = + '
Node color (logFC)
' + + '
' + + '
' + + '
' + + ' UpregulatedNeutralDownregulated' + + '
' + + (edgeItems ? '
Edge types
' + edgeItems : '') + + '
' + + 'PTM info: Hover over edges to see overlapping PTM sites.
'; + } + + /* ── renderValue ──────────────────────────────────────────────────── */ + return { + renderValue: function (x) { + + /* Destroy previous instance */ + if (cy) { cy.destroy(); cy = null; } + if (tooltip) { tooltip.parentNode && tooltip.parentNode.removeChild(tooltip); tooltip = null; } + + /* Ensure the container has explicit pixel dimensions */ + el.style.width = el.style.width || width + "px"; + el.style.height = el.style.height || height + "px"; + + /* Build combined elements array from pre-serialised strings. + R passes them as an array of JSON-string fragments; we re-parse. */ + var elements = (x.elements || []).map(function (frag) { + return (typeof frag === "string") ? JSON.parse(frag) : frag; + }); + + /* Layout – merge defaults with whatever R sends */ + var layout = Object.assign({ + name: "dagre", + rankDir: "TB", + animate: true, + fit: true, + padding: 30, + spacingFactor: 1.5, + nodeSep: 50, + edgeSep: 20, + rankSep: 80 + }, x.layout || {}); + + /* Initialise Cytoscape */ + cytoscape.use(cytoscapeDagre); // register dagre layout + + cy = cytoscape({ + container: el, + elements: elements, + style: buildStyle(x.node_font_size), + layout: layout + }); + + /* After layout, fan PTM nodes around their parent protein */ + cy.on("layoutstop", function () { + repositionPTMNodes(cy); + }); + + /* ── Tooltip ─────────────────────────────────────────────────── */ + tooltip = document.createElement("div"); + tooltip.style.cssText = [ + "position:fixed", + "background:rgba(0,0,0,.88)", + "color:#fff", + "padding:7px 11px", + "border-radius:4px", + "font-size:12px", + "font-family:Arial,sans-serif", + "pointer-events:none", + "z-index:99999", + "box-shadow:0 2px 8px rgba(0,0,0,.3)", + "display:none", + "max-width:300px", + "white-space:pre-wrap", + "word-wrap:break-word" + ].join(";"); + document.body.appendChild(tooltip); + + cy.on("mouseover", "edge", function (evt) { + var txt = evt.target.data("tooltip"); + if (txt && txt.trim()) { + tooltip.textContent = txt; + tooltip.style.display = "block"; + } + }); + + cy.on("mousemove", "edge", function (evt) { + if (tooltip.style.display === "block") { + tooltip.style.left = (evt.originalEvent.clientX + 12) + "px"; + tooltip.style.top = (evt.originalEvent.clientY - 28) + "px"; + } + }); + + cy.on("mouseout", "edge", function () { + tooltip.style.display = "none"; + }); + + /* ── Evidence link on edge click ─────────────────────────────── */ + cy.on("tap", "edge", function (evt) { + var link = evt.target.data("evidenceLink"); + openSafe(link); + }); + + /* ── Build legend in sibling element (if present) ────────────── */ + var legendEl = el.parentNode + ? el.parentNode.querySelector(".cytoscape-network-legend") + : null; + buildLegend(cy, legendEl); + }, + + /* ── resize ───────────────────────────────────────────────────── */ + resize: function (newWidth, newHeight) { + if (cy) { + cy.resize(); + cy.fit(); + } + } + }; + } +}); diff --git a/inst/htmlwidgets/cytoscapeNetwork.yaml b/inst/htmlwidgets/cytoscapeNetwork.yaml new file mode 100644 index 0000000..7c2d3a3 --- /dev/null +++ b/inst/htmlwidgets/cytoscapeNetwork.yaml @@ -0,0 +1,40 @@ +# inst/htmlwidgets/cytoscapeNetwork.yaml +# +# Declares the JavaScript (and optional CSS) dependencies for the +# cytoscapeNetwork htmlwidget. +# +# All libraries are loaded from CDN via the `url` field so no local copies +# are needed. If you want to vendor them locally, download the files into +# inst/htmlwidgets/lib/ and replace the `url` entries with `script` entries +# pointing at the local paths. +# +# Load order matters: graphlib → dagre → cytoscape → cytoscape-dagre + +dependencies: + - name: graphlib + version: 2.1.8 + src: + url: "https://cdnjs.cloudflare.com/ajax/libs/graphlib/2.1.8/graphlib.min.js" + head: > + + + - name: dagre + version: 0.8.5 + src: "." + url: "https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js" + head: > + + + - name: cytoscape + version: 3.32.0 + src: "." + url: "https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.32.0/cytoscape.min.js" + head: > + + + - name: cytoscape-dagre + version: 2.3.0 + src: "." + url: "https://unpkg.com/cytoscape-dagre@2.3.0/cytoscape-dagre.js" + head: > + diff --git a/inst/script/examples.R b/inst/script/examples.R new file mode 100644 index 0000000..c875b85 --- /dev/null +++ b/inst/script/examples.R @@ -0,0 +1,139 @@ +# ============================================================================= +# cytoscapeNetwork – usage examples +# ============================================================================= +# Install (once the package is built): +# devtools::install_local("path/to/cytoscapeNetwork") +# # or, during development: +# devtools::load_all("path/to/cytoscapeNetwork") +# ============================================================================= + +library(cytoscapeNetwork) + +# ── Example 2 · logFC colour gradient ─────────────────────────────────────── +# Nodes coloured on a blue (down) → grey (neutral) → red (up) scale. + +nodes_fc <- data.frame( + id = c("TP53", "MDM2", "CDKN1A", "BCL2", "BAX"), + logFC = c( 1.5, -0.8, 2.1, -1.9, 0.3), + stringsAsFactors = FALSE +) + +edges_fc <- data.frame( + source = c("TP53", "TP53", "MDM2", "BCL2"), + target = c("MDM2", "CDKN1A", "TP53", "BAX"), + interaction = c("Activation", "IncreaseAmount", "Inhibition", "Complex"), + stringsAsFactors = FALSE +) + +widget = cytoscapeNetwork(nodes_fc, edges_fc) + + +# ── Example 3 · PTM satellite nodes ───────────────────────────────────────── +# The `Site` column (underscore-separated) creates small circle child-nodes +# clustered around the parent protein. Hover over edges to see overlap +# information when an edge target shares a PTM site with node data. + +nodes_ptm <- data.frame( + id = c("EGFR", "SRC", "AKT1"), + logFC = c( 1.2, 0.5, -0.3), + Site = c("Y1068_Y1173", "Y416", NA), + stringsAsFactors = FALSE +) + +edges_ptm <- data.frame( + source = c("EGFR", "SRC"), + target = c("SRC", "AKT1"), + interaction = c("Phosphorylation", "Activation"), + site = c("Y416", NA), # edge targets a specific site + stringsAsFactors = FALSE +) + +cytoscapeNetwork(nodes_ptm, edges_ptm, nodeFontSize = 14) + + +# ── Example 4 · HGNC labels + left-to-right layout ───────────────────────── + +nodes_hgnc <- data.frame( + id = c("ENSG001", "ENSG002", "ENSG003"), + hgncName = c("TP53", "MDM2", "CDKN1A"), + logFC = c( 1.0, -0.5, 2.0), + stringsAsFactors = FALSE +) + +edges_hgnc <- data.frame( + source = c("ENSG001", "ENSG001"), + target = c("ENSG002", "ENSG003"), + interaction = c("Activation", "IncreaseAmount"), + stringsAsFactors = FALSE +) + +cytoscapeNetwork( + nodes_hgnc, edges_hgnc, + displayLabelType = "hgncName", + layoutOptions = list(rankDir = "LR", rankSep = 120) +) + + +# ── Example 5 · Evidence links ─────────────────────────────────────────────── +# Click an edge to open the evidence URL in a new tab. + +edges_ev <- data.frame( + source = c("TP53", "MDM2"), + target = c("MDM2", "TP53"), + interaction = c("Activation", "Inhibition"), + evidenceLink = c( + "https://www.ncbi.nlm.nih.gov/pubmed/10490031", + "https://www.ncbi.nlm.nih.gov/pubmed/16474400" + ), + stringsAsFactors = FALSE +) + +cytoscapeNetwork(nodes_min, edges_ev) + + +# ── Example 6 · Shiny integration ─────────────────────────────────────────── + +if (requireNamespace("shiny", quietly = TRUE)) { + library(shiny) + + ui <- fluidPage( + titlePanel("Protein Interaction Network"), + sidebarLayout( + sidebarPanel( + sliderInput("font_size", "Node font size", min = 8, max = 24, value = 12), + selectInput("layout_dir", "Layout direction", + choices = c("Top-Bottom" = "TB", "Left-Right" = "LR"), + selected = "TB") + ), + mainPanel( + # Use the Shiny output binding + cytoscapeNetworkOutput("network", height = "600px") + ) + ) + ) + + server <- function(input, output, session) { + output$network <- renderCytoscapeNetwork({ + cytoscapeNetwork( + nodes = nodes_fc, + edges = edges_fc, + nodeFontSize = input$font_size, + layoutOptions = list(rankDir = input$layout_dir) + ) + }) + } + + # shinyApp(ui, server) # uncomment to launch +} + + +# ── Example 7 · Save to a standalone HTML file ────────────────────────────── + +widget <- cytoscapeNetwork(nodes_ptm, edges_ptm) + +htmlwidgets::saveWidget( + widget, + file = "network.html", + selfcontained = TRUE # bundles all JS/CSS into one file +) +# browseURL("network.html") # open in browser diff --git a/man/cytoscapeNetwork.Rd b/man/cytoscapeNetwork.Rd new file mode 100644 index 0000000..4b0863b --- /dev/null +++ b/man/cytoscapeNetwork.Rd @@ -0,0 +1,68 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/cytoscapeNetwork.R +\name{cytoscapeNetwork} +\alias{cytoscapeNetwork} +\title{Render a Cytoscape network visualisation} +\usage{ +cytoscapeNetwork( + nodes, + edges = data.frame(), + displayLabelType = "id", + nodeFontSize = 12, + layoutOptions = NULL, + width = NULL, + height = NULL, + elementId = NULL +) +} +\arguments{ +\item{nodes}{Data frame with at minimum an \code{id} column. Optional +columns: \code{logFC} (numeric), \code{hgncName} +(character), \code{Site} (character, underscore-separated +PTM site list).} + +\item{edges}{Data frame with columns \code{source}, \code{target}, +\code{interaction}. Optional: \code{site}, +\code{evidenceLink}.} + +\item{displayLabelType}{\code{"id"} (default) or \code{"hgncName"} – +controls which column is used as the visible node label.} + +\item{nodeFontSize}{Font size (px) for node labels. Default \code{12}.} + +\item{layoutOptions}{Named list of dagre layout options to override the +defaults (e.g. \code{list(rankDir = "LR")}).} + +\item{width, height}{Widget dimensions passed to +\code{\link[htmlwidgets]{createWidget}}.} + +\item{elementId}{Optional explicit HTML element id.} +} +\value{ +An \code{htmlwidget} object that renders in R Markdown, Shiny, or + the RStudio Viewer pane. +} +\description{ +Creates an interactive network diagram powered by Cytoscape.js and the dagre +layout algorithm. Nodes can carry log fold-change (logFC) values which are +mapped to a blue-grey-red colour gradient. PTM (post-translational +modification) site information is shown as small satellite nodes and edge +overlaps are surfaced as hover tooltips. +} +\examples{ +\dontrun{ +nodes <- data.frame( + id = c("TP53", "MDM2", "CDKN1A"), + logFC = c(1.5, -0.8, 2.1), + stringsAsFactors = FALSE +) +edges <- data.frame( + source = c("TP53", "MDM2"), + target = c("MDM2", "TP53"), + interaction = c("Activation", "Inhibition"), + stringsAsFactors = FALSE +) +cytoscapeNetwork(nodes, edges) +} + +} diff --git a/man/cytoscapeNetworkOutput.Rd b/man/cytoscapeNetworkOutput.Rd new file mode 100644 index 0000000..39ec60f --- /dev/null +++ b/man/cytoscapeNetworkOutput.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/cytoscapeNetwork.R +\name{cytoscapeNetworkOutput} +\alias{cytoscapeNetworkOutput} +\title{Shiny output binding for cytoscapeNetwork} +\usage{ +cytoscapeNetworkOutput(outputId, width = "100\%", height = "500px") +} +\arguments{ +\item{outputId}{output variable to read from} + +\item{width, height}{Must be a valid CSS unit (like \code{"100\%"}, +\code{"400px"}, \code{"auto"}) or a number, which will be coerced to a +string and have \code{"px"} appended.} +} +\description{ +Shiny output binding for cytoscapeNetwork +} diff --git a/man/renderCytoscapeNetwork.Rd b/man/renderCytoscapeNetwork.Rd new file mode 100644 index 0000000..f9e77d2 --- /dev/null +++ b/man/renderCytoscapeNetwork.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/cytoscapeNetwork.R +\name{renderCytoscapeNetwork} +\alias{renderCytoscapeNetwork} +\title{Shiny render binding for cytoscapeNetwork} +\usage{ +renderCytoscapeNetwork(expr, env = parent.frame(), quoted = FALSE) +} +\arguments{ +\item{expr}{An expression that generates an HTML widget (or a +\href{https://rstudio.github.io/promises/}{promise} of an HTML widget).} + +\item{env}{The environment in which to evaluate \code{expr}.} + +\item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This +is useful if you want to save an expression in a variable.} +} +\description{ +Shiny render binding for cytoscapeNetwork +} From 424712d6b625de8c39af40938f82f34265380e74 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 12:51:14 -0500 Subject: [PATCH 02/17] initial refactor actually produces network viz --- inst/htmlwidgets/cytoscapeNetwork.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/inst/htmlwidgets/cytoscapeNetwork.yaml b/inst/htmlwidgets/cytoscapeNetwork.yaml index 7c2d3a3..df29adb 100644 --- a/inst/htmlwidgets/cytoscapeNetwork.yaml +++ b/inst/htmlwidgets/cytoscapeNetwork.yaml @@ -14,27 +14,27 @@ dependencies: - name: graphlib version: 2.1.8 src: - url: "https://cdnjs.cloudflare.com/ajax/libs/graphlib/2.1.8/graphlib.min.js" + href: https://cdnjs.cloudflare.com/ajax/libs/graphlib/2.1.8/graphlib.min.js head: > - name: dagre version: 0.8.5 - src: "." - url: "https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js" + src: + href: https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js head: > - name: cytoscape version: 3.32.0 - src: "." - url: "https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.32.0/cytoscape.min.js" + src: + href: https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.32.0/cytoscape.min.js head: > - name: cytoscape-dagre version: 2.3.0 - src: "." - url: "https://unpkg.com/cytoscape-dagre@2.3.0/cytoscape-dagre.js" + src: + href: https://unpkg.com/cytoscape-dagre@2.3.0/cytoscape-dagre.js head: > From 6f0ee3adef2967e425ceb2baa0ec07df9c54956c Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 13:49:06 -0500 Subject: [PATCH 03/17] fix row$id bug --- R/cytoscapeNetwork.R | 2 +- inst/script/examples.R | 73 ++++++++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/R/cytoscapeNetwork.R b/R/cytoscapeNetwork.R index 49c7578..15a5acf 100644 --- a/R/cytoscapeNetwork.R +++ b/R/cytoscapeNetwork.R @@ -254,7 +254,7 @@ emitted_ptm_e <- character(0) for (i in seq_len(nrow(nodes))) { - row <- nodes[i, ] + row <- nodes[i, , drop = FALSE] color <- node_colors[i] has_site <- "Site" %in% names(nodes) && !is.na(row$Site) && trimws(row$Site) != "" diff --git a/inst/script/examples.R b/inst/script/examples.R index c875b85..f242b9c 100644 --- a/inst/script/examples.R +++ b/inst/script/examples.R @@ -9,6 +9,21 @@ library(cytoscapeNetwork) +nodes_min <- data.frame( + id = c("TP53", "MDM2", "CDKN1A"), + stringsAsFactors = FALSE +) + +edges_min <- data.frame( + source = c("TP53", "MDM2"), + target = c("MDM2", "TP53"), + interaction = c("Activation", "Inhibition"), + stringsAsFactors = FALSE +) + +# Renders in RStudio Viewer / R Markdown / browser +cytoscapeNetwork(nodes_min, edges_min) + # ── Example 2 · logFC colour gradient ─────────────────────────────────────── # Nodes coloured on a blue (down) → grey (neutral) → red (up) scale. @@ -25,7 +40,7 @@ edges_fc <- data.frame( stringsAsFactors = FALSE ) -widget = cytoscapeNetwork(nodes_fc, edges_fc) +cytoscapeNetwork(nodes_fc, edges_fc) # ── Example 3 · PTM satellite nodes ───────────────────────────────────────── @@ -92,40 +107,36 @@ cytoscapeNetwork(nodes_min, edges_ev) # ── Example 6 · Shiny integration ─────────────────────────────────────────── - -if (requireNamespace("shiny", quietly = TRUE)) { - library(shiny) - - ui <- fluidPage( - titlePanel("Protein Interaction Network"), - sidebarLayout( - sidebarPanel( - sliderInput("font_size", "Node font size", min = 8, max = 24, value = 12), - selectInput("layout_dir", "Layout direction", - choices = c("Top-Bottom" = "TB", "Left-Right" = "LR"), - selected = "TB") - ), - mainPanel( - # Use the Shiny output binding - cytoscapeNetworkOutput("network", height = "600px") - ) - ) +library(shiny) +ui <- fluidPage( +titlePanel("Protein Interaction Network"), +sidebarLayout( + sidebarPanel( + sliderInput("font_size", "Node font size", min = 8, max = 24, value = 12), + selectInput("layout_dir", "Layout direction", + choices = c("Top-Bottom" = "TB", "Left-Right" = "LR"), + selected = "TB") + ), + mainPanel( + # Use the Shiny output binding + cytoscapeNetworkOutput("network", height = "600px") ) +) +) - server <- function(input, output, session) { - output$network <- renderCytoscapeNetwork({ - cytoscapeNetwork( - nodes = nodes_fc, - edges = edges_fc, - nodeFontSize = input$font_size, - layoutOptions = list(rankDir = input$layout_dir) - ) - }) - } - - # shinyApp(ui, server) # uncomment to launch +server <- function(input, output, session) { +output$network <- renderCytoscapeNetwork({ + cytoscapeNetwork( + nodes = nodes_ptm, + edges = edges_ptm, + nodeFontSize = input$font_size, + layoutOptions = list(rankDir = input$layout_dir) + ) +}) } +# shinyApp(ui, server) # uncomment to launch + # ── Example 7 · Save to a standalone HTML file ────────────────────────────── From d9793514c581ed729e6a4ad1dbd062d3b1e173a0 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 13:52:20 -0500 Subject: [PATCH 04/17] fixed shiny rendering --- DESCRIPTION | 3 ++- R/cytoscapeNetwork.R | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index aa964e1..9c7fd68 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -33,7 +33,8 @@ Suggests: rmarkdown, testthat (>= 3.0.0), mockery, - MSstatsConvert + MSstatsConvert, + shiny VignetteBuilder: knitr biocViews: ImmunoOncology, MassSpectrometry, Proteomics, Software, QualityControl, NetworkEnrichment, Network diff --git a/R/cytoscapeNetwork.R b/R/cytoscapeNetwork.R index 15a5acf..bd82594 100644 --- a/R/cytoscapeNetwork.R +++ b/R/cytoscapeNetwork.R @@ -474,7 +474,7 @@ cytoscapeNetworkOutput <- function(outputId, name = "cytoscapeNetwork", width = width, height = height, - package = "cytoscapeNetwork" + package = "MSstatsBioNet" ) } From edd3b99f65ad5f727080af83c646174d2c934677 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 13:57:47 -0500 Subject: [PATCH 05/17] save vignettes for visualization tutorial --- inst/script/examples.R | 150 -------------------------- vignettes/Cytoscape-Visualization.Rmd | 148 +++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 150 deletions(-) delete mode 100644 inst/script/examples.R create mode 100644 vignettes/Cytoscape-Visualization.Rmd diff --git a/inst/script/examples.R b/inst/script/examples.R deleted file mode 100644 index f242b9c..0000000 --- a/inst/script/examples.R +++ /dev/null @@ -1,150 +0,0 @@ -# ============================================================================= -# cytoscapeNetwork – usage examples -# ============================================================================= -# Install (once the package is built): -# devtools::install_local("path/to/cytoscapeNetwork") -# # or, during development: -# devtools::load_all("path/to/cytoscapeNetwork") -# ============================================================================= - -library(cytoscapeNetwork) - -nodes_min <- data.frame( - id = c("TP53", "MDM2", "CDKN1A"), - stringsAsFactors = FALSE -) - -edges_min <- data.frame( - source = c("TP53", "MDM2"), - target = c("MDM2", "TP53"), - interaction = c("Activation", "Inhibition"), - stringsAsFactors = FALSE -) - -# Renders in RStudio Viewer / R Markdown / browser -cytoscapeNetwork(nodes_min, edges_min) - -# ── Example 2 · logFC colour gradient ─────────────────────────────────────── -# Nodes coloured on a blue (down) → grey (neutral) → red (up) scale. - -nodes_fc <- data.frame( - id = c("TP53", "MDM2", "CDKN1A", "BCL2", "BAX"), - logFC = c( 1.5, -0.8, 2.1, -1.9, 0.3), - stringsAsFactors = FALSE -) - -edges_fc <- data.frame( - source = c("TP53", "TP53", "MDM2", "BCL2"), - target = c("MDM2", "CDKN1A", "TP53", "BAX"), - interaction = c("Activation", "IncreaseAmount", "Inhibition", "Complex"), - stringsAsFactors = FALSE -) - -cytoscapeNetwork(nodes_fc, edges_fc) - - -# ── Example 3 · PTM satellite nodes ───────────────────────────────────────── -# The `Site` column (underscore-separated) creates small circle child-nodes -# clustered around the parent protein. Hover over edges to see overlap -# information when an edge target shares a PTM site with node data. - -nodes_ptm <- data.frame( - id = c("EGFR", "SRC", "AKT1"), - logFC = c( 1.2, 0.5, -0.3), - Site = c("Y1068_Y1173", "Y416", NA), - stringsAsFactors = FALSE -) - -edges_ptm <- data.frame( - source = c("EGFR", "SRC"), - target = c("SRC", "AKT1"), - interaction = c("Phosphorylation", "Activation"), - site = c("Y416", NA), # edge targets a specific site - stringsAsFactors = FALSE -) - -cytoscapeNetwork(nodes_ptm, edges_ptm, nodeFontSize = 14) - - -# ── Example 4 · HGNC labels + left-to-right layout ───────────────────────── - -nodes_hgnc <- data.frame( - id = c("ENSG001", "ENSG002", "ENSG003"), - hgncName = c("TP53", "MDM2", "CDKN1A"), - logFC = c( 1.0, -0.5, 2.0), - stringsAsFactors = FALSE -) - -edges_hgnc <- data.frame( - source = c("ENSG001", "ENSG001"), - target = c("ENSG002", "ENSG003"), - interaction = c("Activation", "IncreaseAmount"), - stringsAsFactors = FALSE -) - -cytoscapeNetwork( - nodes_hgnc, edges_hgnc, - displayLabelType = "hgncName", - layoutOptions = list(rankDir = "LR", rankSep = 120) -) - - -# ── Example 5 · Evidence links ─────────────────────────────────────────────── -# Click an edge to open the evidence URL in a new tab. - -edges_ev <- data.frame( - source = c("TP53", "MDM2"), - target = c("MDM2", "TP53"), - interaction = c("Activation", "Inhibition"), - evidenceLink = c( - "https://www.ncbi.nlm.nih.gov/pubmed/10490031", - "https://www.ncbi.nlm.nih.gov/pubmed/16474400" - ), - stringsAsFactors = FALSE -) - -cytoscapeNetwork(nodes_min, edges_ev) - - -# ── Example 6 · Shiny integration ─────────────────────────────────────────── -library(shiny) -ui <- fluidPage( -titlePanel("Protein Interaction Network"), -sidebarLayout( - sidebarPanel( - sliderInput("font_size", "Node font size", min = 8, max = 24, value = 12), - selectInput("layout_dir", "Layout direction", - choices = c("Top-Bottom" = "TB", "Left-Right" = "LR"), - selected = "TB") - ), - mainPanel( - # Use the Shiny output binding - cytoscapeNetworkOutput("network", height = "600px") - ) -) -) - -server <- function(input, output, session) { -output$network <- renderCytoscapeNetwork({ - cytoscapeNetwork( - nodes = nodes_ptm, - edges = edges_ptm, - nodeFontSize = input$font_size, - layoutOptions = list(rankDir = input$layout_dir) - ) -}) -} - -# shinyApp(ui, server) # uncomment to launch - - -# ── Example 7 · Save to a standalone HTML file ────────────────────────────── - -widget <- cytoscapeNetwork(nodes_ptm, edges_ptm) - -htmlwidgets::saveWidget( - widget, - file = "network.html", - selfcontained = TRUE # bundles all JS/CSS into one file -) -# browseURL("network.html") # open in browser diff --git a/vignettes/Cytoscape-Visualization.Rmd b/vignettes/Cytoscape-Visualization.Rmd new file mode 100644 index 0000000..d58774d --- /dev/null +++ b/vignettes/Cytoscape-Visualization.Rmd @@ -0,0 +1,148 @@ +--- +title: "Cytoscape-Visualization" +output: html_document +--- + +```{r} +library(MSstatsBioNet) + +nodes_min <- data.frame( + id = c("TP53", "MDM2", "CDKN1A"), + stringsAsFactors = FALSE +) + +edges_min <- data.frame( + source = c("TP53", "MDM2"), + target = c("MDM2", "TP53"), + interaction = c("Activation", "Inhibition"), + stringsAsFactors = FALSE +) + +# Renders in RStudio Viewer / R Markdown / browser +cytoscapeNetwork(nodes_min, edges_min) + +# ── Example 2 · logFC colour gradient ─────────────────────────────────────── +# Nodes coloured on a blue (down) → grey (neutral) → red (up) scale. + +nodes_fc <- data.frame( + id = c("TP53", "MDM2", "CDKN1A", "BCL2", "BAX"), + logFC = c( 1.5, -0.8, 2.1, -1.9, 0.3), + stringsAsFactors = FALSE +) + +edges_fc <- data.frame( + source = c("TP53", "TP53", "MDM2", "BCL2"), + target = c("MDM2", "CDKN1A", "TP53", "BAX"), + interaction = c("Activation", "IncreaseAmount", "Inhibition", "Complex"), + stringsAsFactors = FALSE +) + +cytoscapeNetwork(nodes_fc, edges_fc) + + +# ── Example 3 · PTM satellite nodes ───────────────────────────────────────── +# The `Site` column (underscore-separated) creates small circle child-nodes +# clustered around the parent protein. Hover over edges to see overlap +# information when an edge target shares a PTM site with node data. + +nodes_ptm <- data.frame( + id = c("EGFR", "SRC", "AKT1"), + logFC = c( 1.2, 0.5, -0.3), + Site = c("Y1068_Y1173", "Y416", NA), + stringsAsFactors = FALSE +) + +edges_ptm <- data.frame( + source = c("EGFR", "SRC"), + target = c("SRC", "AKT1"), + interaction = c("Phosphorylation", "Activation"), + site = c("Y416", NA), # edge targets a specific site + stringsAsFactors = FALSE +) + +cytoscapeNetwork(nodes_ptm, edges_ptm, nodeFontSize = 14) + + +# ── Example 4 · HGNC labels + left-to-right layout ───────────────────────── + +nodes_hgnc <- data.frame( + id = c("ENSG001", "ENSG002", "ENSG003"), + hgncName = c("TP53", "MDM2", "CDKN1A"), + logFC = c( 1.0, -0.5, 2.0), + stringsAsFactors = FALSE +) + +edges_hgnc <- data.frame( + source = c("ENSG001", "ENSG001"), + target = c("ENSG002", "ENSG003"), + interaction = c("Activation", "IncreaseAmount"), + stringsAsFactors = FALSE +) + +cytoscapeNetwork( + nodes_hgnc, edges_hgnc, + displayLabelType = "hgncName", + layoutOptions = list(rankDir = "LR", rankSep = 120) +) + + +# ── Example 5 · Evidence links ─────────────────────────────────────────────── +# Click an edge to open the evidence URL in a new tab. + +edges_ev <- data.frame( + source = c("TP53", "MDM2"), + target = c("MDM2", "TP53"), + interaction = c("Activation", "Inhibition"), + evidenceLink = c( + "https://www.ncbi.nlm.nih.gov/pubmed/10490031", + "https://www.ncbi.nlm.nih.gov/pubmed/16474400" + ), + stringsAsFactors = FALSE +) + +cytoscapeNetwork(nodes_min, edges_ev) + + +# ── Example 6 · Shiny integration ─────────────────────────────────────────── +library(shiny) +ui <- fluidPage( + titlePanel("Protein Interaction Network"), + sidebarLayout( + sidebarPanel( + sliderInput("font_size", "Node font size", min = 8, max = 24, value = 12), + selectInput("layout_dir", "Layout direction", + choices = c("Top-Bottom" = "TB", "Left-Right" = "LR"), + selected = "TB") + ), + mainPanel( + # Use the Shiny output binding + cytoscapeNetworkOutput("network", height = "600px") + ) + ) +) + +server <- function(input, output, session) { + output$network <- renderCytoscapeNetwork({ + cytoscapeNetwork( + nodes = nodes_ptm, + edges = edges_ptm, + nodeFontSize = input$font_size, + layoutOptions = list(rankDir = input$layout_dir) + ) + }) +} + +# shinyApp(ui, server) # uncomment to launch + + +# ── Example 7 · Save to a standalone HTML file ────────────────────────────── + +widget <- cytoscapeNetwork(nodes_ptm, edges_ptm) + +htmlwidgets::saveWidget( + widget, + file = "network.html", + selfcontained = TRUE # bundles all JS/CSS into one file +) +# browseURL("network.html") # open in browser +``` \ No newline at end of file From 4d4b8ae032aab11aeb9b27007b49eaa143a1297e Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 14:02:01 -0500 Subject: [PATCH 06/17] adjust ptm node size --- inst/htmlwidgets/cytoscapeNetwork.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inst/htmlwidgets/cytoscapeNetwork.js b/inst/htmlwidgets/cytoscapeNetwork.js index 65fb626..e021928 100644 --- a/inst/htmlwidgets/cytoscapeNetwork.js +++ b/inst/htmlwidgets/cytoscapeNetwork.js @@ -71,13 +71,13 @@ HTMLWidgets.widget({ selector: "node[node_type = 'ptm']", style: { "shape": "ellipse", - "width": 20, - "height": 20, + "width": 35, + "height": 35, "background-color": "data(color)", "border-color": "#333", "border-width": 1.5, "label": "data(label)", - "font-size": "8px", + "font-size": "10px", "font-weight": "normal", "color": "#000", "text-valign": "center", From 1ad77b14fb0953b0891320fba282a0cafc5adb24 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 14:20:45 -0500 Subject: [PATCH 07/17] add an export PNG button --- inst/htmlwidgets/cytoscapeNetwork.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/inst/htmlwidgets/cytoscapeNetwork.js b/inst/htmlwidgets/cytoscapeNetwork.js index e021928..3b0d38e 100644 --- a/inst/htmlwidgets/cytoscapeNetwork.js +++ b/inst/htmlwidgets/cytoscapeNetwork.js @@ -286,6 +286,25 @@ HTMLWidgets.widget({ style: buildStyle(x.node_font_size), layout: layout }); + + // Inject an export PNG button above the container + var btn = document.createElement("button"); + btn.textContent = "Export PNG"; + btn.style.cssText = "margin-bottom:6px;padding:5px 12px;cursor:pointer;font-size:12px;"; + el.parentNode.insertBefore(btn, el); + + btn.addEventListener("click", function () { + var png = cy.png({ + output: "base64uri", + bg: "white", + full: true, + scale: 8 + }); + var a = document.createElement("a"); + a.href = png; + a.download = "network.png"; + a.click(); + }); /* After layout, fan PTM nodes around their parent protein */ cy.on("layoutstop", function () { From 98810e00cb389190b71f8bd197f13d22e541fe4a Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 14:35:11 -0500 Subject: [PATCH 08/17] export button looks prettier --- inst/htmlwidgets/cytoscapeNetwork.js | 56 ++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/inst/htmlwidgets/cytoscapeNetwork.js b/inst/htmlwidgets/cytoscapeNetwork.js index 3b0d38e..131f260 100644 --- a/inst/htmlwidgets/cytoscapeNetwork.js +++ b/inst/htmlwidgets/cytoscapeNetwork.js @@ -279,36 +279,78 @@ HTMLWidgets.widget({ /* Initialise Cytoscape */ cytoscape.use(cytoscapeDagre); // register dagre layout + + el.innerHTML = ""; // clear on re-render + // Outer flex wrapper — fills the widget element + var wrapper = document.createElement("div"); + wrapper.style.cssText = "display:flex;width:100%;height:100%;"; + + // Left: Cytoscape canvas + var cyContainer = document.createElement("div"); + cyContainer.style.cssText = "flex:1;height:100%;min-width:0;"; + + // Right: legend panel + var legendPanel = document.createElement("div"); + legendPanel.className = "cytoscape-network-legend"; + legendPanel.style.cssText = [ + "width:180px", + "flex-shrink:0", + "padding:12px", + "background:#f8f9fa", + "border-left:1px solid #dee2e6", + "overflow-y:auto", + "font-family:Arial,sans-serif", + "box-sizing:border-box" + ].join(";"); + + wrapper.appendChild(cyContainer); + wrapper.appendChild(legendPanel); + el.appendChild(wrapper); cy = cytoscape({ - container: el, + container: cyContainer, elements: elements, style: buildStyle(x.node_font_size), layout: layout }); // Inject an export PNG button above the container + var btnBar = document.createElement("div"); + btnBar.style.cssText = "display:flex;justify-content:flex-end;margin-bottom:6px;"; + var btn = document.createElement("button"); btn.textContent = "Export PNG"; - btn.style.cssText = "margin-bottom:6px;padding:5px 12px;cursor:pointer;font-size:12px;"; - el.parentNode.insertBefore(btn, el); + btn.style.cssText = [ + "padding:5px 12px", + "cursor:pointer", + "font-size:12px", + "background:#28a745", + "color:white", + "border:none", + "border-radius:4px", + "font-family:Arial,sans-serif" + ].join(";"); btn.addEventListener("click", function () { var png = cy.png({ - output: "base64uri", - bg: "white", - full: true, - scale: 8 + output: "base64uri", + bg: "white", + full: true, + scale: 3 }); var a = document.createElement("a"); a.href = png; a.download = "network.png"; a.click(); }); + + btnBar.appendChild(btn); + el.parentNode.insertBefore(btnBar, el); /* After layout, fan PTM nodes around their parent protein */ cy.on("layoutstop", function () { repositionPTMNodes(cy); + buildLegend(cy, legendPanel); }); /* ── Tooltip ─────────────────────────────────────────────────── */ From 1dea93f719cf4ed19029e6b5e73cdd59498ac5fa Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 14:45:58 -0500 Subject: [PATCH 09/17] enable download of legend --- inst/htmlwidgets/cytoscapeNetwork.js | 26 ++++++++++++++++++++------ inst/htmlwidgets/cytoscapeNetwork.yaml | 7 +++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/inst/htmlwidgets/cytoscapeNetwork.js b/inst/htmlwidgets/cytoscapeNetwork.js index 131f260..a7006f1 100644 --- a/inst/htmlwidgets/cytoscapeNetwork.js +++ b/inst/htmlwidgets/cytoscapeNetwork.js @@ -332,16 +332,30 @@ HTMLWidgets.widget({ ].join(";"); btn.addEventListener("click", function () { - var png = cy.png({ + var networkPng = cy.png({ output: "base64uri", bg: "white", full: true, - scale: 3 + scale: 8 }); - var a = document.createElement("a"); - a.href = png; - a.download = "network.png"; - a.click(); + var a1 = document.createElement("a"); + a1.href = networkPng; + a1.download = "network.png"; + a1.click(); + + // ── 2. Legend PNG via html2canvas ────────────────────────────────── + // Small delay so the two download dialogs don't collide in some browsers + setTimeout(function () { + html2canvas(legendPanel, { + backgroundColor: "#ffffff", + scale: 8 + }).then(function (canvas) { + var a2 = document.createElement("a"); + a2.href = canvas.toDataURL("image/png"); + a2.download = "network_legend.png"; + a2.click(); + }); + }, 300); }); btnBar.appendChild(btn); diff --git a/inst/htmlwidgets/cytoscapeNetwork.yaml b/inst/htmlwidgets/cytoscapeNetwork.yaml index df29adb..4287338 100644 --- a/inst/htmlwidgets/cytoscapeNetwork.yaml +++ b/inst/htmlwidgets/cytoscapeNetwork.yaml @@ -38,3 +38,10 @@ dependencies: href: https://unpkg.com/cytoscape-dagre@2.3.0/cytoscape-dagre.js head: > + + - name: html2canvas + version: 1.4.1 + src: + href: https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js + head: > + From 51d5ea67fbe1f9abe9e3050b2e9cdb57bd3f9b90 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 15:40:30 -0500 Subject: [PATCH 10/17] modify button to be above the legend --- inst/htmlwidgets/cytoscapeNetwork.js | 162 ++++++++++++++------------- 1 file changed, 85 insertions(+), 77 deletions(-) diff --git a/inst/htmlwidgets/cytoscapeNetwork.js b/inst/htmlwidgets/cytoscapeNetwork.js index a7006f1..14c6b56 100644 --- a/inst/htmlwidgets/cytoscapeNetwork.js +++ b/inst/htmlwidgets/cytoscapeNetwork.js @@ -254,9 +254,91 @@ HTMLWidgets.widget({ if (cy) { cy.destroy(); cy = null; } if (tooltip) { tooltip.parentNode && tooltip.parentNode.removeChild(tooltip); tooltip = null; } - /* Ensure the container has explicit pixel dimensions */ - el.style.width = el.style.width || width + "px"; - el.style.height = el.style.height || height + "px"; + el.innerHTML = ""; + el.style.cssText = "display:flex;width:100%;height:100%;box-sizing:border-box;"; + + /* Left: Cytoscape canvas */ + var cyContainer = document.createElement("div"); + cyContainer.style.cssText = "flex:1;min-width:0;height:100%;"; + + /* Right panel — shared background for button + legend */ + var PANEL_BG = "#f8f9fa"; + var rightPanel = document.createElement("div"); + rightPanel.style.cssText = [ + "width:190px", + "flex-shrink:0", + "display:flex", + "flex-direction:column", + "background:" + PANEL_BG, + "border-left:1px solid #dee2e6", + "box-sizing:border-box" + ].join(";"); + + /* Button bar — right-aligned inside the panel */ + var btnBar = document.createElement("div"); + btnBar.style.cssText = [ + "display:flex", + "justify-content:flex-start", + "padding:8px 10px 6px 10px", + "background:" + PANEL_BG, + "border-bottom:1px solid #dee2e6" + ].join(";"); + + var btn = document.createElement("button"); + btn.textContent = "Export PNG"; + btn.style.cssText = [ + "padding:4px 10px", + "cursor:pointer", + "font-size:13px", + "background:#28a745", + "color:white", + "border:none", + "border-radius:4px", + "font-family:Arial,sans-serif", + "white-space:nowrap" + ].join(";"); + + btn.addEventListener("click", function () { + /* Network PNG via Cytoscape */ + var networkPng = cy.png({ output: "base64uri", bg: "white", full: true, scale: 8 }); + var a1 = document.createElement("a"); + a1.href = networkPng; + a1.download = "network.png"; + a1.click(); + + /* Legend PNG via html2canvas (if available) */ + setTimeout(function () { + if (typeof html2canvas === "function") { + html2canvas(legendPanel, { backgroundColor: PANEL_BG, scale: 8 }) + .then(function (canvas) { + var a2 = document.createElement("a"); + a2.href = canvas.toDataURL("image/png"); + a2.download = "network_legend.png"; + a2.click(); + }); + } + }, 300); + }); + + btnBar.appendChild(btn); + + /* Legend panel — fills remaining vertical space, scrolls if needed */ + var legendPanel = document.createElement("div"); + legendPanel.className = "cytoscape-network-legend"; + legendPanel.style.cssText = [ + "flex:1", + "overflow-y:auto", + "padding:10px", + "font-family:Arial,sans-serif", + "box-sizing:border-box", + "background:" + PANEL_BG + ].join(";"); + + rightPanel.appendChild(btnBar); + rightPanel.appendChild(legendPanel); + + el.appendChild(cyContainer); + el.appendChild(rightPanel); /* Build combined elements array from pre-serialised strings. R passes them as an array of JSON-string fragments; we re-parse. */ @@ -279,33 +361,6 @@ HTMLWidgets.widget({ /* Initialise Cytoscape */ cytoscape.use(cytoscapeDagre); // register dagre layout - - el.innerHTML = ""; // clear on re-render - // Outer flex wrapper — fills the widget element - var wrapper = document.createElement("div"); - wrapper.style.cssText = "display:flex;width:100%;height:100%;"; - - // Left: Cytoscape canvas - var cyContainer = document.createElement("div"); - cyContainer.style.cssText = "flex:1;height:100%;min-width:0;"; - - // Right: legend panel - var legendPanel = document.createElement("div"); - legendPanel.className = "cytoscape-network-legend"; - legendPanel.style.cssText = [ - "width:180px", - "flex-shrink:0", - "padding:12px", - "background:#f8f9fa", - "border-left:1px solid #dee2e6", - "overflow-y:auto", - "font-family:Arial,sans-serif", - "box-sizing:border-box" - ].join(";"); - - wrapper.appendChild(cyContainer); - wrapper.appendChild(legendPanel); - el.appendChild(wrapper); cy = cytoscape({ container: cyContainer, @@ -313,53 +368,6 @@ HTMLWidgets.widget({ style: buildStyle(x.node_font_size), layout: layout }); - - // Inject an export PNG button above the container - var btnBar = document.createElement("div"); - btnBar.style.cssText = "display:flex;justify-content:flex-end;margin-bottom:6px;"; - - var btn = document.createElement("button"); - btn.textContent = "Export PNG"; - btn.style.cssText = [ - "padding:5px 12px", - "cursor:pointer", - "font-size:12px", - "background:#28a745", - "color:white", - "border:none", - "border-radius:4px", - "font-family:Arial,sans-serif" - ].join(";"); - - btn.addEventListener("click", function () { - var networkPng = cy.png({ - output: "base64uri", - bg: "white", - full: true, - scale: 8 - }); - var a1 = document.createElement("a"); - a1.href = networkPng; - a1.download = "network.png"; - a1.click(); - - // ── 2. Legend PNG via html2canvas ────────────────────────────────── - // Small delay so the two download dialogs don't collide in some browsers - setTimeout(function () { - html2canvas(legendPanel, { - backgroundColor: "#ffffff", - scale: 8 - }).then(function (canvas) { - var a2 = document.createElement("a"); - a2.href = canvas.toDataURL("image/png"); - a2.download = "network_legend.png"; - a2.click(); - }); - }, 300); - }); - - btnBar.appendChild(btn); - el.parentNode.insertBefore(btnBar, el); /* After layout, fan PTM nodes around their parent protein */ cy.on("layoutstop", function () { From 9ed3b3855d8c37a1d71e4c1be95cbeeb73164600 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 15:42:38 -0500 Subject: [PATCH 11/17] adjust size of right legend panel --- inst/htmlwidgets/cytoscapeNetwork.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inst/htmlwidgets/cytoscapeNetwork.js b/inst/htmlwidgets/cytoscapeNetwork.js index 14c6b56..8b9c269 100644 --- a/inst/htmlwidgets/cytoscapeNetwork.js +++ b/inst/htmlwidgets/cytoscapeNetwork.js @@ -265,7 +265,7 @@ HTMLWidgets.widget({ var PANEL_BG = "#f8f9fa"; var rightPanel = document.createElement("div"); rightPanel.style.cssText = [ - "width:190px", + "width:160px", "flex-shrink:0", "display:flex", "flex-direction:column", From c8ede77289fe129ea80fa1088e70ecc44e09bcb6 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 16:17:55 -0500 Subject: [PATCH 12/17] save vignette --- vignettes/Cytoscape-Visualization.Rmd | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/vignettes/Cytoscape-Visualization.Rmd b/vignettes/Cytoscape-Visualization.Rmd index d58774d..5aec40b 100644 --- a/vignettes/Cytoscape-Visualization.Rmd +++ b/vignettes/Cytoscape-Visualization.Rmd @@ -115,18 +115,17 @@ ui <- fluidPage( selected = "TB") ), mainPanel( - # Use the Shiny output binding - cytoscapeNetworkOutput("network", height = "600px") + cytoscapeNetworkOutput("network", height = "600px"), + style = "height: 650px;" ) ) ) - server <- function(input, output, session) { output$network <- renderCytoscapeNetwork({ cytoscapeNetwork( - nodes = nodes_ptm, - edges = edges_ptm, - nodeFontSize = input$font_size, + nodes = nodes_ptm, + edges = edges_ptm, + nodeFontSize = input$font_size, layoutOptions = list(rankDir = input$layout_dir) ) }) From 621a09a5b6aa543474248792343f4cb33f4ddf69 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 16:30:46 -0500 Subject: [PATCH 13/17] html is not breaking anymore --- inst/htmlwidgets/cytoscapeNetwork.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/inst/htmlwidgets/cytoscapeNetwork.js b/inst/htmlwidgets/cytoscapeNetwork.js index 8b9c269..da559fc 100644 --- a/inst/htmlwidgets/cytoscapeNetwork.js +++ b/inst/htmlwidgets/cytoscapeNetwork.js @@ -255,17 +255,32 @@ HTMLWidgets.widget({ if (tooltip) { tooltip.parentNode && tooltip.parentNode.removeChild(tooltip); tooltip = null; } el.innerHTML = ""; - el.style.cssText = "display:flex;width:100%;height:100%;box-sizing:border-box;"; - - /* Left: Cytoscape canvas */ + var PANEL_W = 160; + var elW = el.offsetWidth || width || 800; + var elH = el.offsetHeight || height || 600; + + el.style.cssText = [ + "display:flex", + "width:" + elW + "px", + "height:" + elH + "px", + "box-sizing:border-box" + ].join(";"); + + /* Left: Cytoscape canvas — explicit px so Cytoscape always gets + real dimensions regardless of flex/CSS resolution order */ var cyContainer = document.createElement("div"); - cyContainer.style.cssText = "flex:1;min-width:0;height:100%;"; + cyContainer.style.cssText = [ + "flex:1", + "min-width:0", + "width:" + (elW - PANEL_W) + "px", + "height:" + elH + "px" + ].join(";"); /* Right panel — shared background for button + legend */ var PANEL_BG = "#f8f9fa"; var rightPanel = document.createElement("div"); rightPanel.style.cssText = [ - "width:160px", + "width:" + PANEL_W + "px",, "flex-shrink:0", "display:flex", "flex-direction:column", From 4670c8f9fff2911cfd63159e32f5079c9cfec5fc Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 16:47:52 -0500 Subject: [PATCH 14/17] add shiny event handlers --- inst/htmlwidgets/cytoscapeNetwork.js | 34 ++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/inst/htmlwidgets/cytoscapeNetwork.js b/inst/htmlwidgets/cytoscapeNetwork.js index da559fc..04be46f 100644 --- a/inst/htmlwidgets/cytoscapeNetwork.js +++ b/inst/htmlwidgets/cytoscapeNetwork.js @@ -431,10 +431,40 @@ HTMLWidgets.widget({ /* ── Evidence link on edge click ─────────────────────────────── */ cy.on("tap", "edge", function (evt) { - var link = evt.target.data("evidenceLink"); - openSafe(link); + var edge = evt.target; + // skip compound/ptm attachment edges + if (edge.data("edge_type") === "ptm_attachment") return; + openSafe(edge.data("evidenceLink")); + if (window.Shiny) { + Shiny.setInputValue(el.id + "_edge_clicked", { + source: edge.data("source"), + target: edge.data("target"), + interaction: edge.data("interaction"), + edge_type: edge.data("edge_type"), + category: edge.data("category"), + evidenceLink: edge.data("evidenceLink") + }); + } }); + /* ── Node click — report to Shiny ───────────────────────────── */ + cy.on("tap", "node", function (evt) { + var node = evt.target; + // skip compound and ptm satellite nodes + if (node.data("node_type") === "compound") return; + if (window.Shiny) { + Shiny.setInputValue(el.id + "_node_clicked", { + id: node.data("id"), + label: node.data("label"), + color: node.data("color"), + node_type: node.data("node_type") + }); + } + }); + + /* ── Expose cy instance for external access (e.g. Shiny) ─────── */ + el._cytoscapeInstance = cy; + /* ── Build legend in sibling element (if present) ────────────── */ var legendEl = el.parentNode ? el.parentNode.querySelector(".cytoscape-network-legend") From 0734e88c6e043d01e09cacb11710e83a196691c2 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 20:24:44 -0500 Subject: [PATCH 15/17] address coderabbit comments --- R/cytoscapeNetwork.R | 12 ++++- inst/htmlwidgets/cytoscapeNetwork.js | 6 +-- vignettes/Cytoscape-Visualization.Rmd | 63 +++++++++++++++------------ 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/R/cytoscapeNetwork.R b/R/cytoscapeNetwork.R index bd82594..bcd6d53 100644 --- a/R/cytoscapeNetwork.R +++ b/R/cytoscapeNetwork.R @@ -280,7 +280,9 @@ nd <- list(id = row$id, label = display_label, color = color, - node_type = "protein") + node_type = "protein", + width = max(60, min(nchar(display_label) * 8 + 20, 150)), + height = max(40, min(nchar(display_label) * 2 + 30, 60))) if (needs_compound) nd$parent <- compound_id elements <- c(elements, list(list(data = nd))) emitted_prots <- c(emitted_prots, row$id) @@ -421,7 +423,13 @@ cytoscapeNetwork <- function(nodes, if (!is.data.frame(nodes) || !("id" %in% names(nodes))) { stop("`nodes` must be a data frame with at least an `id` column.") } - if (!is.data.frame(edges)) edges <- data.frame() + if (!is.data.frame(edges)) { + stop("`edges` must be a data frame.") + } + required_edge_cols <- c("source", "target", "interaction") + if (nrow(edges) > 0 && !all(required_edge_cols %in% names(edges))) { + stop("`edges` must contain columns: source, target, interaction.") + } # Build layout config default_layout <- list( diff --git a/inst/htmlwidgets/cytoscapeNetwork.js b/inst/htmlwidgets/cytoscapeNetwork.js index 04be46f..d493838 100644 --- a/inst/htmlwidgets/cytoscapeNetwork.js +++ b/inst/htmlwidgets/cytoscapeNetwork.js @@ -62,8 +62,8 @@ HTMLWidgets.widget({ "border-color": "#333", "padding": "5px", /* dynamic width/height via mappers */ - "width": "mapData(label.length, 0, 20, 60, 150)", - "height": 40 + "width": "data(width)", + "height": "data(height)" } }, /* ── PTM child nodes ─────────────────────────────────────────── */ @@ -280,7 +280,7 @@ HTMLWidgets.widget({ var PANEL_BG = "#f8f9fa"; var rightPanel = document.createElement("div"); rightPanel.style.cssText = [ - "width:" + PANEL_W + "px",, + "width:" + PANEL_W + "px", "flex-shrink:0", "display:flex", "flex-direction:column", diff --git a/vignettes/Cytoscape-Visualization.Rmd b/vignettes/Cytoscape-Visualization.Rmd index 5aec40b..47837e1 100644 --- a/vignettes/Cytoscape-Visualization.Rmd +++ b/vignettes/Cytoscape-Visualization.Rmd @@ -104,44 +104,49 @@ cytoscapeNetwork(nodes_min, edges_ev) # ── Example 6 · Shiny integration ─────────────────────────────────────────── -library(shiny) -ui <- fluidPage( - titlePanel("Protein Interaction Network"), - sidebarLayout( - sidebarPanel( - sliderInput("font_size", "Node font size", min = 8, max = 24, value = 12), - selectInput("layout_dir", "Layout direction", - choices = c("Top-Bottom" = "TB", "Left-Right" = "LR"), - selected = "TB") - ), - mainPanel( - cytoscapeNetworkOutput("network", height = "600px"), - style = "height: 650px;" +if (requireNamespace("shiny", quietly = TRUE)) { + library(shiny) + ui <- fluidPage( + titlePanel("Protein Interaction Network"), + sidebarLayout( + sidebarPanel( + sliderInput("font_size", "Node font size", min = 8, max = 24, value = 12), + selectInput("layout_dir", "Layout direction", + choices = c("Top-Bottom" = "TB", "Left-Right" = "LR"), + selected = "TB") + ), + mainPanel( + cytoscapeNetworkOutput("network", height = "600px"), + style = "height: 650px;" + ) ) ) -) -server <- function(input, output, session) { - output$network <- renderCytoscapeNetwork({ - cytoscapeNetwork( - nodes = nodes_ptm, - edges = edges_ptm, - nodeFontSize = input$font_size, - layoutOptions = list(rankDir = input$layout_dir) - ) - }) + server <- function(input, output, session) { + output$network <- renderCytoscapeNetwork({ + cytoscapeNetwork( + nodes = nodes_ptm, + edges = edges_ptm, + nodeFontSize = input$font_size, + layoutOptions = list(rankDir = input$layout_dir) + ) + }) + } + # shinyApp(ui, server) # uncomment to launch } -# shinyApp(ui, server) # uncomment to launch # ── Example 7 · Save to a standalone HTML file ────────────────────────────── widget <- cytoscapeNetwork(nodes_ptm, edges_ptm) -htmlwidgets::saveWidget( - widget, - file = "network.html", - selfcontained = TRUE # bundles all JS/CSS into one file -) +if (requireNamespace("rmarkdown", quietly = TRUE) && + rmarkdown::pandoc_available()) { + htmlwidgets::saveWidget( + widget, + file = tempfile("network-", fileext = ".html"), + selfcontained = FALSE + ) +} # browseURL("network.html") # open in browser ``` \ No newline at end of file From bb128204468be2d6461677363e8c7fff34600afd Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 20:33:16 -0500 Subject: [PATCH 16/17] make width variable while height stays fixed --- inst/htmlwidgets/cytoscapeNetwork.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/inst/htmlwidgets/cytoscapeNetwork.js b/inst/htmlwidgets/cytoscapeNetwork.js index d493838..0b65590 100644 --- a/inst/htmlwidgets/cytoscapeNetwork.js +++ b/inst/htmlwidgets/cytoscapeNetwork.js @@ -256,12 +256,11 @@ HTMLWidgets.widget({ el.innerHTML = ""; var PANEL_W = 160; - var elW = el.offsetWidth || width || 800; var elH = el.offsetHeight || height || 600; el.style.cssText = [ "display:flex", - "width:" + elW + "px", + "width:100%", "height:" + elH + "px", "box-sizing:border-box" ].join(";"); @@ -272,7 +271,6 @@ HTMLWidgets.widget({ cyContainer.style.cssText = [ "flex:1", "min-width:0", - "width:" + (elW - PANEL_W) + "px", "height:" + elH + "px" ].join(";"); From 8b3ecc1b8b45da717040f6ed43347f708f71bd3b Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Thu, 26 Feb 2026 20:44:33 -0500 Subject: [PATCH 17/17] address more coderabbit comments --- R/cytoscapeNetwork.R | 4 ++++ inst/htmlwidgets/cytoscapeNetwork.js | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/R/cytoscapeNetwork.R b/R/cytoscapeNetwork.R index bcd6d53..149ca72 100644 --- a/R/cytoscapeNetwork.R +++ b/R/cytoscapeNetwork.R @@ -89,6 +89,10 @@ #' @keywords internal #' @noRd .classify <- function(interaction) { + if (is.null(interaction) || is.na(interaction) || !nzchar(trimws(as.character(interaction)))) { + return("other") + } + interaction <- as.character(interaction) props <- .relProps() for (cat_name in names(props)) { if (!is.null(props[[cat_name]]$types) && diff --git a/inst/htmlwidgets/cytoscapeNetwork.js b/inst/htmlwidgets/cytoscapeNetwork.js index 0b65590..9c1fed6 100644 --- a/inst/htmlwidgets/cytoscapeNetwork.js +++ b/inst/htmlwidgets/cytoscapeNetwork.js @@ -355,8 +355,17 @@ HTMLWidgets.widget({ /* Build combined elements array from pre-serialised strings. R passes them as an array of JSON-string fragments; we re-parse. */ - var elements = (x.elements || []).map(function (frag) { - return (typeof frag === "string") ? JSON.parse(frag) : frag; + var elements = []; + (x.elements || []).forEach(function (frag) { + if (typeof frag === "string") { + try { + elements.push(JSON.parse(frag)); + } catch (err) { + console.warn("Skipping invalid element JSON fragment:", err); + } + } else if (frag && typeof frag === "object") { + elements.push(frag); + } }); /* Layout – merge defaults with whatever R sends */ @@ -449,7 +458,8 @@ HTMLWidgets.widget({ cy.on("tap", "node", function (evt) { var node = evt.target; // skip compound and ptm satellite nodes - if (node.data("node_type") === "compound") return; + if (node.data("node_type") === "compound" || + node.data("node_type") === "ptm") return; if (window.Shiny) { Shiny.setInputValue(el.id + "_node_clicked", { id: node.data("id"),