Skip to content

Refactor viz#72

Merged
tonywu1999 merged 17 commits intodevelfrom
refactor-viz
Feb 27, 2026
Merged

Refactor viz#72
tonywu1999 merged 17 commits intodevelfrom
refactor-viz

Conversation

@tonywu1999
Copy link
Contributor

@tonywu1999 tonywu1999 commented Feb 26, 2026

Motivation and Context

There's a lot of javascript embedded as strings in the visualization module, and I realized visualization can be upgraded by placing these JS embeddings into JS files, and then performing logic in R files. This makes extensibility way easier, and now, we can have a widget that allows users to download PNGs, display legends, all in one spot.

In a future PR, I will do some clean up, unit testing, and dependency handling

Motivation and Context

This PR adds a full Cytoscape.js-based htmlwidget to MSstatsBioNet to provide interactive network visualizations (proteins, interactions, PTMs, compounds). The widget supports log-fold-change coloring, PTM satellite nodes, dagre layout configuration, Shiny bindings, PNG export, and a client-side legend. The intent is to refactor/centralize visualization functionality into a reusable htmlwidget.

Short solution summary

Introduce a new htmlwidget ("cytoscapeNetwork") implemented in R and JavaScript, wire up Shiny output/render bindings, declare required package and JS dependencies, add documentation and a vignette with usage examples.

Detailed changes

  • DESCRIPTION

    • Added Imports: htmlwidgets, grDevices
    • Added Suggests: shiny
    • Minor formatting fixes (tidyr, MSstatsConvert entries)
  • NAMESPACE

    • Exported: cytoscapeNetwork, cytoscapeNetworkOutput, renderCytoscapeNetwork
    • ImportFrom: htmlwidgets::createWidget
  • R implementation (R/cytoscapeNetwork.R)

    • Added cytoscapeNetwork(nodes, edges = data.frame(), displayLabelType = "id", nodeFontSize = 12, layoutOptions = NULL, width = NULL, height = NULL, elementId = NULL)
    • Added Shiny helpers: cytoscapeNetworkOutput(outputId, width = "100%", height = "500px"), renderCytoscapeNetwork(expr, env = parent.frame(), quoted = FALSE)
    • Extensive helper logic (~500 lines) for:
      • mapping logFC to colors
      • escaping strings for JS
      • classifying and consolidating edges
      • computing PTM overlaps and building PTM satellite nodes
      • assembling Cytoscape elements (nodes, edges, attachments)
    • Commit message indicates a sizing tweak: "make width variable while height stays fixed"
  • JavaScript widget (inst/htmlwidgets/cytoscapeNetwork.js)

    • New HTMLWidgets.widget factory "cytoscapeNetwork" with renderValue and resize
    • Stylesheet builder for node/edge types; PTM repositioning; legend builder
    • Interactivity: edge tooltips, edge tap opens evidence link, node/edge click events reported to Shiny
    • Export PNG (with optional legend capture via html2canvas)
    • Exposes Cytoscape instance at el._cytoscapeInstance
  • JS manifest (inst/htmlwidgets/cytoscapeNetwork.yaml)

    • Declared JS dependencies loaded from CDN: graphlib, dagre, cytoscape, cytoscape-dagre, html2canvas (load order specified)
  • Documentation

    • man/cytoscapeNetwork.Rd — documents main widget API, args, and examples
    • man/cytoscapeNetworkOutput.Rd — documents Shiny output binding
    • man/renderCytoscapeNetwork.Rd — documents Shiny render binding
    • vignettes/Cytoscape-Visualization.Rmd — comprehensive vignette with multiple usage examples and a Shiny example
  • Size/lines

    • R/cytoscapeNetwork.R: +~500 lines
    • inst/htmlwidgets/cytoscapeNetwork.js: +~484 lines
    • inst/htmlwidgets/cytoscapeNetwork.yaml: +~47 lines
    • man and vignette files added (+~238 lines total)
    • Total lines increased by 288 per Codecov report (repository lines: 1559 → 1847)

Unit tests

  • No unit tests were added or modified in this PR.
  • Codecov report details:
    • Patch coverage: 0% for this change
    • R/cytoscapeNetwork.R reported as 288 missing lines (0.00% patch coverage)
    • Project coverage decreased from 59.91% (base) to 50.56% (head), a -9.35% change
    • Overall hits remain at 934; misses increased by 288 (625 → 913)

Coding guidelines / issues violated

  • Testing: The addition of ~288 lines of production code with no accompanying unit tests violates project testing standards and causes a large coverage drop (-9.35%). New code (notably R/cytoscapeNetwork.R) must have unit tests covering core logic (element construction, edge consolidation/classification, PTM handling, input validation) and Shiny bindings where practical.
  • PR description quality: The PR description contains only a template and lacks substantive motivation, implementation rationale, and testing notes — this violates expectations for descriptive PR metadata.
  • Size & reviewability: Large, monolithic additions across R and JS (~1,000+ lines total) increase review burden. Consider splitting into smaller commits or separate PRs (e.g., core R API + JS widget + docs/vignette) to improve reviewability.
  • Dependency sourcing: The widget manifest points to CDN-hosted JS dependencies; repository policy may require vendoring or providing an option to vendor libraries — clarify maintainers' preference.
  • API stability: New exported functions change the public API surface; ensure versioning notes and NEWS entry are added and that exports are intentional.

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

Adds a Cytoscape.js-based htmlwidget to MSstatsBioNet with R APIs and Shiny bindings, client-side rendering/interaction logic, dependency declarations (htmlwidgets, grDevices, shiny), documentation, and a vignette demonstrating usage and examples.

Changes

Cohort / File(s) Summary
Package Metadata
DESCRIPTION, NAMESPACE
Added htmlwidgets and grDevices to Imports; added shiny to Suggests. Exported cytoscapeNetwork, cytoscapeNetworkOutput, renderCytoscapeNetwork and imported htmlwidgets::createWidget in NAMESPACE. Minor punctuation/formatting tweaks in DESCRIPTION (tidyr,, MSstatsConvert,).
Core R Implementation
R/cytoscapeNetwork.R
New htmlwidget constructor and Shiny bindings: input validation, element construction (nodes, PTM satellites, attachment edges), color mapping, edge consolidation/styling, PTM overlap/tooltips, and call to htmlwidgets::createWidget.
JavaScript Widget Binding
inst/htmlwidgets/cytoscapeNetwork.js
New HTMLWidgets factory implementing Cytoscape.js rendering (dagre), PTM repositioning, dynamic legend, tooltips, event handlers (hover/tap), Shiny reporting, PNG export (html2canvas), and exposes el._cytoscapeInstance.
JS Dependency Manifest
inst/htmlwidgets/cytoscapeNetwork.yaml
New YAML declaring CDN dependencies and load order: graphlib, dagre, cytoscape, cytoscape-dagre, html2canvas.
Documentation
man/cytoscapeNetwork.Rd, man/cytoscapeNetworkOutput.Rd, man/renderCytoscapeNetwork.Rd
Added Rd docs for the three public functions (signatures, arguments, usage, descriptions).
Vignette
vignettes/Cytoscape-Visualization.Rmd
New vignette with multiple examples: basic usage, logFC coloring, PTM satellites, HGNC labels, evidence links, Shiny integration, and widget export.

Sequence Diagram(s)

sequenceDiagram
    participant User as R User
    participant R as R (cytoscapeNetwork)
    participant HW as htmlwidgets (createWidget)
    participant JS as JS Binding (cytoscapeNetwork.js)
    participant Cyt as Cytoscape.js
    participant Browser as Browser/DOM

    User->>R: Call cytoscapeNetwork(nodes, edges, ...)
    R->>R: Validate inputs, build elements (nodes, PTMs, edges), compute colors/tooltips
    R->>HW: createWidget(payload)
    HW->>JS: Initialize widget with payload
    JS->>Cyt: Initialize with elements & dagre layout
    Cyt->>JS: Emit layoutstop
    JS->>JS: Reposition PTMs, build legend
    JS->>Browser: Render canvas and controls, register event handlers
    User->>Browser: Interact (hover/click)
    Browser->>JS: Event → show tooltip / open evidence link / report to Shiny
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

Review effort 3/5

🐰 I hopped through nodes and edges tonight,
Colors and PTMs in tidy light,
Tooltips whisper links to explore,
Cytoscape twirls across the browser floor,
A rabbit's cheer — visualize delight! 🐇🎨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description lacks substantive detail. Changes section contains only a placeholder bullet, testing section is empty, and the pre-review checklist is entirely unchecked. Complete the Changes section with detailed bullet points of all modifications, describe any testing performed, and verify checklist items before requesting review. Ensure contributor guidelines have been followed.
Title check ❓ Inconclusive The title 'Refactor viz' is too vague and does not specifically describe the main changes in the pull request, which center on adding a new cytoscapeNetwork htmlwidget for protein network visualization. Replace with a more specific title that captures the primary change, such as 'Add cytoscapeNetwork htmlwidget for protein network visualization' or 'Implement Cytoscape.js widget for network visualization'.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-viz

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
inst/htmlwidgets/cytoscapeNetwork.yaml (1)

13-47: Vendor widget dependencies locally for reproducible/offline execution.

All five dependencies (graphlib, dagre, cytoscape, cytoscape-dagre, html2canvas) are loaded exclusively from CDNs. The htmlwidgets framework explicitly recommends bundling third-party JS/CSS under inst/htmlwidgets/lib/ to ensure the widget works offline and remains reproducible. CRAN policy also strongly prefers bundling external dependencies rather than requiring internet access at runtime.

Consider vendoring these assets locally and updating the dependency manifest to reference the local copies (or providing both local and CDN paths with local as the primary source).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inst/htmlwidgets/cytoscapeNetwork.yaml` around lines 13 - 47, The manifest
lists external CDN-only dependencies (graphlib, dagre, cytoscape,
cytoscape-dagre, html2canvas) which must be vendored for offline/reproducible
use; download the corresponding min.js files into inst/htmlwidgets/lib/, update
the dependency entries in cytoscapeNetwork.yaml to point src.href and the head
script tags to the local lib/* filenames (or make local the primary href and
keep the CDN as a fallback), and commit the vendored assets so the widget loads
from inst/htmlwidgets/lib/ instead of only from the CDNs.
inst/htmlwidgets/cytoscapeNetwork.js (3)

377-378: Register Cytoscape plugin once, not on every render.

cytoscape.use(cytoscapeDagre) is called inside renderValue, meaning the plugin is re-registered on every render. While Cytoscape.js handles duplicate registrations gracefully, this is inefficient and may log warnings in the console. Consider registering once at module load or guarding with a flag.

♻️ Proposed fix—register once at top of factory
   factory: function (el, width, height) {

     // State kept between renderValue calls so we can destroy cleanly
     var cy      = null;
     var tooltip = null;
+    var dagreRegistered = false;

Then in renderValue:

         /* Initialise Cytoscape */
-        cytoscape.use(cytoscapeDagre);   // register dagre layout
+        if (!dagreRegistered) {
+          cytoscape.use(cytoscapeDagre);   // register dagre layout
+          dagreRegistered = true;
+        }

         cy = cytoscape({

Alternatively, register at module scope before the widget definition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inst/htmlwidgets/cytoscapeNetwork.js` around lines 377 - 378, Move the
cytoscape.use(cytoscapeDagre) call out of the renderValue function so the dagre
plugin is registered only once; locate the renderValue implementation and remove
the cytoscape.use(cytoscapeDagre) line, then add a single registration at module
scope (top of the factory file) or guard it with a module-level flag (e.g.,
registeredDagre) so cytoscape.use(cytoscapeDagre) is invoked only once during
module load/initialization rather than on every render.

468-472: Clarify or remove redundant buildLegend call.

buildLegend is invoked twice: once in the layoutstop handler (line 390) with legendPanel, and again here (line 472) searching for .cytoscape-network-legend in the parent. Since legendPanel is already created and appended to rightPanel, the second call at line 472 will find the same element (or a different external one if present).

If this is intentional to support an external legend container, consider adding a comment explaining the dual-path behavior. Otherwise, this call may be redundant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inst/htmlwidgets/cytoscapeNetwork.js` around lines 468 - 472, The second
invocation of buildLegend (the call that queries
el.parentNode.querySelector(".cytoscape-network-legend")) is redundant with the
call made in the layoutstop handler that uses legendPanel appended to
rightPanel; either remove this second call or make its intent explicit by
guarding it and documenting it: if you want to support an external legend
container, change the call to only run when an external legend element exists
and the internal legendPanel is not already present (e.g., detect the presence
of legendPanel/rightPanel vs an external legendEl), and add a short comment near
the buildLegend invocation explaining the dual-path behavior (symbols to locate:
buildLegend, legendPanel, rightPanel, and the layoutstop handler).

358-362: Consider adding error handling for JSON parsing.

If malformed JSON is passed, JSON.parse will throw and prevent the widget from rendering. Wrapping in a try-catch would provide more graceful degradation.

♻️ Proposed defensive parsing
         /* 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 || []).map(function (frag) {
+          if (typeof frag === "string") {
+            try {
+              return JSON.parse(frag);
+            } catch (e) {
+              console.warn("cytoscapeNetwork: failed to parse element", e);
+              return null;
+            }
+          }
+          return frag;
+        }).filter(Boolean);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inst/htmlwidgets/cytoscapeNetwork.js` around lines 358 - 362, The current map
that builds elements uses JSON.parse directly on fragments (variable elements in
cytoscapeNetwork.js), which will throw on malformed JSON; update the map
callback inside the elements = (x.elements || []).map(...) to wrap JSON.parse in
a try-catch, log or console.warn/console.error the parse error including the
offending frag and index, and return a safe fallback (e.g., the original frag or
null/skip) so a single bad fragment does not break widget rendering; ensure
downstream code that consumes elements can handle the fallback (filter out nulls
if you choose to return null).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@inst/htmlwidgets/cytoscapeNetwork.js`:
- Around line 282-290: The array used to build rightPanel.style.cssText contains
a stray empty element caused by a double comma after the "width" entry,
producing a double semicolon; locate the array assigned to
rightPanel.style.cssText (referencing rightPanel, PANEL_W and PANEL_BG) and
remove the extra comma or explicitly supply only the intended strings so the
width entry becomes "width:" + PANEL_W + "px" (single element) and the array
joins without producing an empty slot.
- Around line 64-66: The mapData() call is using the invalid expression
"label.length" which is treated as a literal property name; update the node data
to include a precomputed numeric field (e.g., label_length) in R and change the
mapping to use that field by replacing "mapData(label.length, 0, 20, 60, 150)"
with "mapData(label_length, 0, 20, 60, 150)" in
inst/htmlwidgets/cytoscapeNetwork.js so mapData receives a direct data property;
ensure the data property name matches the key you add to each node's data.

In `@R/cytoscapeNetwork.R`:
- Around line 420-425: The current input validation silently replaces invalid
edges with an empty data.frame; instead, update the validation so that if edges
is not a data.frame you stop with a clear error, and if it is a data.frame
validate that it contains the required columns "source", "target", and
"interaction" (and optionally "id" if used elsewhere) before proceeding; modify
the block that currently checks nodes and edges (the validation around variables
nodes and edges) to call stop() with a descriptive message when edges is invalid
or missing required columns rather than assigning edges <- data.frame().

In `@vignettes/Cytoscape-Visualization.Rmd`:
- Around line 141-145: Wrap the htmlwidgets::saveWidget call so it only runs
when pandoc is available: check pandoc_available() before calling
saveWidget(widget, ...); when running, write to a temporary file via tempfile()
instead of "network.html" and set selfcontained = FALSE to avoid bundling heavy
resources; keep the call using the same widget variable and ensure any
downstream references use the tempfile path if needed.
- Around line 107-135: The vignette loads shiny unconditionally causing failures
when shiny is only in Suggests; wrap the UI/server/reactive block in a check
using requireNamespace("shiny", quietly = TRUE) (or similar) so the code only
calls library(shiny), defines ui, server, and output$network
(renderCytoscapeNetwork/cytoscapeNetwork) when shiny is available, and otherwise
skip or emit a brief message so the vignette build won’t error; ensure the
shinyApp(...) launch line remains commented or is also guarded.

---

Nitpick comments:
In `@inst/htmlwidgets/cytoscapeNetwork.js`:
- Around line 377-378: Move the cytoscape.use(cytoscapeDagre) call out of the
renderValue function so the dagre plugin is registered only once; locate the
renderValue implementation and remove the cytoscape.use(cytoscapeDagre) line,
then add a single registration at module scope (top of the factory file) or
guard it with a module-level flag (e.g., registeredDagre) so
cytoscape.use(cytoscapeDagre) is invoked only once during module
load/initialization rather than on every render.
- Around line 468-472: The second invocation of buildLegend (the call that
queries el.parentNode.querySelector(".cytoscape-network-legend")) is redundant
with the call made in the layoutstop handler that uses legendPanel appended to
rightPanel; either remove this second call or make its intent explicit by
guarding it and documenting it: if you want to support an external legend
container, change the call to only run when an external legend element exists
and the internal legendPanel is not already present (e.g., detect the presence
of legendPanel/rightPanel vs an external legendEl), and add a short comment near
the buildLegend invocation explaining the dual-path behavior (symbols to locate:
buildLegend, legendPanel, rightPanel, and the layoutstop handler).
- Around line 358-362: The current map that builds elements uses JSON.parse
directly on fragments (variable elements in cytoscapeNetwork.js), which will
throw on malformed JSON; update the map callback inside the elements =
(x.elements || []).map(...) to wrap JSON.parse in a try-catch, log or
console.warn/console.error the parse error including the offending frag and
index, and return a safe fallback (e.g., the original frag or null/skip) so a
single bad fragment does not break widget rendering; ensure downstream code that
consumes elements can handle the fallback (filter out nulls if you choose to
return null).

In `@inst/htmlwidgets/cytoscapeNetwork.yaml`:
- Around line 13-47: The manifest lists external CDN-only dependencies
(graphlib, dagre, cytoscape, cytoscape-dagre, html2canvas) which must be
vendored for offline/reproducible use; download the corresponding min.js files
into inst/htmlwidgets/lib/, update the dependency entries in
cytoscapeNetwork.yaml to point src.href and the head script tags to the local
lib/* filenames (or make local the primary href and keep the CDN as a fallback),
and commit the vendored assets so the widget loads from inst/htmlwidgets/lib/
instead of only from the CDNs.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb4bb9f and 4670c8f.

📒 Files selected for processing (9)
  • DESCRIPTION
  • NAMESPACE
  • R/cytoscapeNetwork.R
  • inst/htmlwidgets/cytoscapeNetwork.js
  • inst/htmlwidgets/cytoscapeNetwork.yaml
  • man/cytoscapeNetwork.Rd
  • man/cytoscapeNetworkOutput.Rd
  • man/renderCytoscapeNetwork.Rd
  • vignettes/Cytoscape-Visualization.Rmd

@codecov-commenter
Copy link

codecov-commenter commented Feb 27, 2026

Codecov Report

❌ Patch coverage is 0% with 297 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.32%. Comparing base (fb4bb9f) to head (8b3ecc1).
⚠️ Report is 2 commits behind head on devel.

Files with missing lines Patch % Lines
R/cytoscapeNetwork.R 0.00% 297 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            devel      #72      +/-   ##
==========================================
- Coverage   59.91%   50.32%   -9.59%     
==========================================
  Files           7        8       +1     
  Lines        1559     1856     +297     
==========================================
  Hits          934      934              
- Misses        625      922     +297     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
R/cytoscapeNetwork.R (1)

250-357: Consider avoiding repeated c(elements, list(...)) in tight loops.

This pattern reallocates repeatedly and can degrade performance on large networks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@R/cytoscapeNetwork.R` around lines 250 - 357, The code repeatedly grows the
elements list inside the node and edge loops (variables elements, emitted_prots,
emitted_cpds, emitted_ptm_n, emitted_ptm_e), causing repeated reallocations;
change the accumulation to use indexed assignment (e.g., maintain a single
integer counter like elem_idx and do elements[[elem_idx]] <- <item>; elem_idx <-
elem_idx + 1) or collect node/ptm/edge items into separate preallocated lists
and concatenate once after the loops (references: the for-loops that build
elements in cytoscapeNetwork.R, the variables elements/emitted_* and the edge
loop that uses .consolidateEdges and .edgeStyle), preserving the exact list
structure and emitted_* checks but avoiding c(elements, list(...)) inside tight
loops.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@inst/htmlwidgets/cytoscapeNetwork.js`:
- Around line 262-277: renderValue() currently hard-sets pixel widths/heights
(el.style.cssText and cyContainer.style.cssText) and resize() never reapplies
newWidth/newHeight, causing stale/truncated layouts on container resize; update
renderValue() to avoid locking to initial px (use flex + 100% height/width for
el and cyContainer or compute sizes dynamically) and modify
resize(newWidth,newHeight) to set el.style.cssText and cyContainer.style.cssText
using the incoming newWidth/newHeight (recompute width as (newWidth - PANEL_W)px
if PANEL_W applies), then call the Cytoscape instance's resize() and rerun
layout/fit (e.g., cy.resize(); cy.layout(...).run(); or cy.fit()) so the canvas
reflows correctly; refer to renderValue, resize, cyContainer, el, PANEL_W, and
the Cytoscape instance (cy) when making these changes.
- Around line 360-362: The JSON.parse call inside the elements mapping can throw
on malformed fragments and break rendering; update the map over (x.elements ||
[]) in cytoscapeNetwork.js to guard parsing by wrapping the parse in a try/catch
inside the map callback (the function that handles frag), returning the original
frag (or null/skip) when parse fails and optionally console.warn or
console.error the problematic frag and error; ensure downstream code handles any
nulls/invalid entries (filter them out) so a single bad fragment cannot crash
the widget.
- Around line 451-455: The node tap handler registered with cy.on("tap", "node",
function (evt) { ... }) only filters out compound nodes but the intent is to
also skip PTM nodes; update the handler to return early when
node.data("node_type") indicates a PTM (e.g. check for "ptm" or the project's
PTM node_type value) so taps on PTM nodes do not trigger the Shiny/_node_clicked
emission; modify the condition in the cy tap callback that currently checks
node.data("node_type") === "compound" to also include the PTM type before
emitting the _node_clicked event.

In `@R/cytoscapeNetwork.R`:
- Around line 423-425: The current validation only ensures nodes is a data.frame
with an id column but does not assert that nodes$id values are non-missing,
non-empty, and unique; add checks in the same validation block (the one that
currently tests is.data.frame(nodes) and "id" %in% names(nodes)) to (1) ensure
no NA or empty string values in nodes$id and (2) ensure all(nodes$id) are
unique, and if either check fails call stop() with a clear error message; this
prevents downstream silent drops in .buildElements that use emitted_prots to
deduplicate IDs.
- Around line 91-99: The .classify function can hit if(NA) when interaction is
missing; update the check inside the loop to guard against NA before using %in%
by testing interaction for missingness (e.g. with is.na(interaction)) and only
evaluating interaction %in% props[[cat_name]]$types when interaction is not NA
and props[[cat_name]]$types is present; keep the existing return(cat_name)
behavior and fallback "other" result. Use the .classify function name and the
interaction and props[[cat_name]]$types symbols to locate the change.

---

Nitpick comments:
In `@R/cytoscapeNetwork.R`:
- Around line 250-357: The code repeatedly grows the elements list inside the
node and edge loops (variables elements, emitted_prots, emitted_cpds,
emitted_ptm_n, emitted_ptm_e), causing repeated reallocations; change the
accumulation to use indexed assignment (e.g., maintain a single integer counter
like elem_idx and do elements[[elem_idx]] <- <item>; elem_idx <- elem_idx + 1)
or collect node/ptm/edge items into separate preallocated lists and concatenate
once after the loops (references: the for-loops that build elements in
cytoscapeNetwork.R, the variables elements/emitted_* and the edge loop that uses
.consolidateEdges and .edgeStyle), preserving the exact list structure and
emitted_* checks but avoiding c(elements, list(...)) inside tight loops.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4670c8f and 0734e88.

📒 Files selected for processing (3)
  • R/cytoscapeNetwork.R
  • inst/htmlwidgets/cytoscapeNetwork.js
  • vignettes/Cytoscape-Visualization.Rmd
🚧 Files skipped from review as they are similar to previous changes (1)
  • vignettes/Cytoscape-Visualization.Rmd

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
inst/htmlwidgets/cytoscapeNetwork.js (3)

358-360: ⚠️ Potential issue | 🟠 Major

Guard JSON.parse to prevent render failures on malformed input.

If x.elements contains a malformed JSON string, the unguarded JSON.parse will throw and crash the entire widget render. This is still unaddressed from a previous review.

🛡️ Proposed fix
-        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("cytoscapeNetwork: skipping invalid JSON fragment", err);
+            }
+          } else if (frag && typeof frag === "object") {
+            elements.push(frag);
+          }
+        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inst/htmlwidgets/cytoscapeNetwork.js` around lines 358 - 360, The mapping
over x.elements uses an unguarded JSON.parse which will throw on malformed
strings and break render; update the map logic in the elements creation (the
anonymous map callback that references x.elements and JSON.parse) to safely
parse by wrapping JSON.parse in a try/catch or delegating to a small safeParse
helper that returns the parsed object on success and falls back to the original
frag (or null) on failure, then filter out null/invalid entries so malformed
JSON does not crash the widget render.

473-479: ⚠️ Potential issue | 🟠 Major

Resize handler doesn't apply new dimensions to the container.

The resize method receives newWidth and newHeight but never applies them to el or cyContainer. Since renderValue sets fixed pixel heights (line 264), the widget won't properly reflow when its container resizes. This was noted in a previous review.

💡 Proposed fix
       resize: function (newWidth, newHeight) {
         if (cy) {
+          if (newHeight) {
+            el.style.height = newHeight + "px";
+            var cyContainer = el.querySelector("div:first-child");
+            if (cyContainer) cyContainer.style.height = newHeight + "px";
+          }
           cy.resize();
           cy.fit();
         }
       }

Alternatively, use percentage-based heights in renderValue so the container naturally reflows:

-          "height:" + elH + "px",
+          "height:100%",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inst/htmlwidgets/cytoscapeNetwork.js` around lines 473 - 479, The resize
handler currently ignores its newWidth/newHeight params; update the resize
function to set the container size (e.g., assign newWidth/newHeight as CSS
width/height on el and/or cyContainer, using pixels or appropriate units) before
calling cy.resize() and cy.fit(); also ensure this complements any sizing logic
in renderValue (which currently sets fixed pixel heights) so the container
actually reflows when resized.

449-461: ⚠️ Potential issue | 🟡 Minor

Comment-code mismatch: PTM nodes are not actually skipped.

The comment at line 451 says "skip compound and ptm satellite nodes" but only compound is filtered at line 452. This was flagged in a previous review. Either add the PTM check to match the intent, or correct the comment if PTM clicks should be reported.

🐛 Proposed fix (if PTM should be skipped)
         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) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inst/htmlwidgets/cytoscapeNetwork.js` around lines 449 - 461, The code claims
to skip "compound and ptm satellite nodes" but only checks for compound; update
the cy.on("tap", "node", ...) handler to also return early for PTM satellite
nodes by checking node.data("node_type") (e.g., add a condition like
node.data("node_type") === "ptm" or === "ptm_satellite" alongside the existing
compound check) before the Shiny.setInputValue call, or alternatively update the
comment to remove the PTM reference if PTM clicks should be reported.
🧹 Nitpick comments (2)
inst/htmlwidgets/cytoscapeNetwork.js (2)

376-376: Consider registering the dagre extension once, outside the render loop.

cytoscape.use(cytoscapeDagre) is called on every renderValue invocation. While Cytoscape.js handles duplicate registration gracefully, this is unnecessarily repeated work.

♻️ Proposed fix
+  // Register dagre extension once at module load
+  var dagreRegistered = false;
+
   /* ── factory ──────────────────────────────────────────────────────────── */
   factory: function (el, width, height) {
     // ...
     return {
       renderValue: function (x) {
         // ...
-        cytoscape.use(cytoscapeDagre);   // register dagre layout
+        if (!dagreRegistered) {
+          cytoscape.use(cytoscapeDagre);
+          dagreRegistered = true;
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inst/htmlwidgets/cytoscapeNetwork.js` at line 376, Move the cytoscapeDagre
registration out of the per-render code: remove the call to
cytoscape.use(cytoscapeDagre) from inside renderValue and instead register the
extension once at module initialization (e.g., at top-level when cytoscape and
cytoscapeDagre are first imported/required). Ensure renderValue still assumes
dagre is registered but does not call cytoscape.use(cytoscapeDagre) itself,
leaving function names cytoscape.use, cytoscapeDagre and renderValue as the
reference points to change.

314-334: Consider adding user feedback during PNG export.

The export button triggers two downloads with a 300ms delay between them, but provides no visual indication that the export is in progress. For large networks, cy.png({ scale: 8 }) can be slow and memory-intensive.

💡 Suggested improvement
         btn.addEventListener("click", function () {
+          btn.disabled = true;
+          btn.textContent = "Exporting...";
           /* Network PNG via Cytoscape */
           var networkPng = cy.png({ output: "base64uri", bg: "white", full: true, scale: 8 });
           // ... download logic ...
           
           setTimeout(function () {
             if (typeof html2canvas === "function") {
               html2canvas(legendPanel, { backgroundColor: PANEL_BG, scale: 8 })
                 .then(function (canvas) {
                   // ... download logic ...
+                }).finally(function () {
+                  btn.disabled = false;
+                  btn.textContent = "Export PNG";
                 });
+            } else {
+              btn.disabled = false;
+              btn.textContent = "Export PNG";
             }
           }, 300);
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inst/htmlwidgets/cytoscapeNetwork.js` around lines 314 - 334, Wrap the export
flow in a user-feedback sequence: when the export button (btn) click handler
starts, disable btn and update its text (or add a spinner) so users see
"Exporting…" before calling cy.png({ scale: 8, ... }) and starting the
html2canvas flow for legendPanel; ensure the UI update takes effect by yielding
to the event loop (e.g., schedule the heavy work after a short setTimeout or
Promise.resolve) and then re-enable btn and restore its label after both the
cy.png download and the html2canvas promise resolve or if either throws an
error, handling errors by showing a brief status (or console.log) and
re-enabling the button; keep the existing setTimeout(…,300) logic but integrate
the disable/enable and status updates around cy.png, html2canvas, and the
downloads.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@inst/htmlwidgets/cytoscapeNetwork.js`:
- Around line 358-360: The mapping over x.elements uses an unguarded JSON.parse
which will throw on malformed strings and break render; update the map logic in
the elements creation (the anonymous map callback that references x.elements and
JSON.parse) to safely parse by wrapping JSON.parse in a try/catch or delegating
to a small safeParse helper that returns the parsed object on success and falls
back to the original frag (or null) on failure, then filter out null/invalid
entries so malformed JSON does not crash the widget render.
- Around line 473-479: The resize handler currently ignores its
newWidth/newHeight params; update the resize function to set the container size
(e.g., assign newWidth/newHeight as CSS width/height on el and/or cyContainer,
using pixels or appropriate units) before calling cy.resize() and cy.fit(); also
ensure this complements any sizing logic in renderValue (which currently sets
fixed pixel heights) so the container actually reflows when resized.
- Around line 449-461: The code claims to skip "compound and ptm satellite
nodes" but only checks for compound; update the cy.on("tap", "node", ...)
handler to also return early for PTM satellite nodes by checking
node.data("node_type") (e.g., add a condition like node.data("node_type") ===
"ptm" or === "ptm_satellite" alongside the existing compound check) before the
Shiny.setInputValue call, or alternatively update the comment to remove the PTM
reference if PTM clicks should be reported.

---

Nitpick comments:
In `@inst/htmlwidgets/cytoscapeNetwork.js`:
- Line 376: Move the cytoscapeDagre registration out of the per-render code:
remove the call to cytoscape.use(cytoscapeDagre) from inside renderValue and
instead register the extension once at module initialization (e.g., at top-level
when cytoscape and cytoscapeDagre are first imported/required). Ensure
renderValue still assumes dagre is registered but does not call
cytoscape.use(cytoscapeDagre) itself, leaving function names cytoscape.use,
cytoscapeDagre and renderValue as the reference points to change.
- Around line 314-334: Wrap the export flow in a user-feedback sequence: when
the export button (btn) click handler starts, disable btn and update its text
(or add a spinner) so users see "Exporting…" before calling cy.png({ scale: 8,
... }) and starting the html2canvas flow for legendPanel; ensure the UI update
takes effect by yielding to the event loop (e.g., schedule the heavy work after
a short setTimeout or Promise.resolve) and then re-enable btn and restore its
label after both the cy.png download and the html2canvas promise resolve or if
either throws an error, handling errors by showing a brief status (or
console.log) and re-enabling the button; keep the existing setTimeout(…,300)
logic but integrate the disable/enable and status updates around cy.png,
html2canvas, and the downloads.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0734e88 and bb12820.

📒 Files selected for processing (1)
  • inst/htmlwidgets/cytoscapeNetwork.js

@tonywu1999 tonywu1999 merged commit b5835d2 into devel Feb 27, 2026
4 checks passed
@tonywu1999 tonywu1999 deleted the refactor-viz branch February 27, 2026 01:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants