Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ importFrom(DT,renderDT)
importFrom(DT,renderDataTable)
importFrom(Hmisc,describe)
importFrom(MSstatsBioNet,annotateProteinInfoFromIndra)
importFrom(MSstatsBioNet,generateCytoscapeConfig)
importFrom(MSstatsBioNet,getSubnetworkFromIndra)
importFrom(MSstatsConvert,MSstatsLogsSettings)
importFrom(MSstatsPTM,FragPipetoMSstatsPTMFormat)
Expand Down
112 changes: 15 additions & 97 deletions R/module-visualize-network-server.R
Original file line number Diff line number Diff line change
@@ -1,62 +1,3 @@
# =============================================================================
# UI RENDERING FUNCTIONS (SHINY-SPECIFIC)
# These functions stay in your Shiny application
# =============================================================================

#' Generate Cytoscape JavaScript with Shiny event handling
#'
#' This function wraps the package's generateCytoscapeConfig() function
#' and adds Shiny-specific event handling for table highlighting
#'
#' @param node_elements Node elements from package
#' @param edge_elements Edge elements from package
#' @param display_label_type Column to display the label from the node table
#' @param container_id Network Visualization Container ID (default: 'network-cy')
#' @param module_id Module ID for Shiny Application (default: 'network')
#' @importFrom MSstatsBioNet generateCytoscapeConfig
#' @return JavaScript code string with Shiny event handlers
generateCytoscapeJSForShiny <- function(node_elements, edge_elements,
display_label_type = "id",
container_id = "network-cy", module_id = "network") {

# Define Shiny-specific event handlers for both edges and nodes
shiny_event_handlers <- list(
edge_click = paste0("function(evt) {
var edge = evt.target;
const edgeId = edge.id();
Shiny.setInputValue('", module_id, "-edgeClicked', {
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 = paste0("function(evt) {
var node = evt.target;
const nodeId = node.id();
Shiny.setInputValue('", module_id, "-nodeClicked', {
id: node.data('id'),
label: node.data('label'),
color: node.data('color')
});
}")
)

# Use the package function to generate configuration
config <- generateCytoscapeConfig(
nodes = node_elements,
edges = edge_elements,
display_label_type = display_label_type,
container_id = container_id,
event_handlers = shiny_event_handlers
)

# Return the JavaScript code
return(config$js_code)
}

renderDataTables <- function(output, nodes_table, edges_table) {
output$nodesTable <- renderDT({
datatable(nodes_table,
Expand Down Expand Up @@ -133,22 +74,6 @@ highlightEdgeInTable <- function(output, edge_data, edges_table) {
}
}

# Open evidence link in new tab
openEvidenceLink <- function(session, evidence_link) {
if (is.null(evidence_link) || is.na(evidence_link)) return(invisible(NULL))
link <- trimws(as.character(evidence_link)[1])
if (!nzchar(link)) return(invisible(NULL))
# Allow only http(s)
if (!grepl("^https?://", link, ignore.case = TRUE)) {
showNotification("Blocked non-http(s) evidence link.", type = "warning")
return(invisible(NULL))
}
session$sendCustomMessage(
type = 'openLinkInNewTab',
message = list(url = link)
)
}

# =============================================================================
# UPDATED SERVER CODE - Using the decoupled architecture
# =============================================================================
Expand Down Expand Up @@ -541,18 +466,7 @@ visualizeNetworkServer <- function(id, parent_session, dataComparison) {
networkVisualization <- reactive({
network_data <- renderNetwork()
if (is.null(network_data)) return(NULL)

# Generate JavaScript code with Shiny-specific event handling
js_code <- generateCytoscapeJSForShiny(
network_data$nodes_table,
network_data$edges_table,
display_label_type = input$displayLabelType,
container_id = session$ns("cy"),
module_id = session$ns(NULL)
)

return(list(
js_code = js_code,
edges_table = network_data$edges_table,
nodes_table = network_data$nodes_table
))
Expand Down Expand Up @@ -626,8 +540,9 @@ visualizeNetworkServer <- function(id, parent_session, dataComparison) {
codes <- paste(codes, "write.csv(subnetwork$edges, \"network_edges.csv\", row.names = FALSE)\n", sep = "")
codes <- paste(codes, "# Visualize network on web browser and export as an HTML file\n", sep = "")
displayLabelTypeStr <- paste0("\"", paste(input$displayLabelType, collapse = "\", \""), "\"")
codes <- paste(codes, "previewNetworkInBrowser(subnetwork$nodes, subnetwork$edges, displayLabelType=", displayLabelTypeStr, ")\n", sep = "")
codes <- paste(codes, "exportNetworkToHTML(subnetwork$nodes, subnetwork$edges, displayLabelType=", displayLabelTypeStr, ")\n", sep = "")
codes <- paste(codes, "cytoscapeNetwork(subnetwork$nodes, subnetwork$edges, displayLabelType=", displayLabelTypeStr, ")\n", sep = "")
codes <- paste(codes, "widget = cytoscapeNetwork(subnetwork$nodes, subnetwork$edges, displayLabelType=", displayLabelTypeStr, ")\n", sep = "")
codes <- paste(codes, "htmlwidgets::saveWidget(widget,\n file = \"network.html\",\n selfcontained = TRUE\n)")

return(codes)
})
Expand All @@ -650,8 +565,14 @@ visualizeNetworkServer <- function(id, parent_session, dataComparison) {
return()
}

# Send JavaScript code to frontend
session$sendCustomMessage(type = 'runCytoscape', message = render_data$js_code)
output$network <- MSstatsBioNet::renderCytoscapeNetwork({
MSstatsBioNet::cytoscapeNetwork(
nodes = render_data$nodes_table,
edges = render_data$edges_table,
nodeFontSize = 12,
displayLabelType = input$displayLabelType
)
})
Comment on lines +568 to +575
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Persist rendered network data to keep click highlighting in sync.

output$network is rendered from a snapshot created on Display Network, but the click handlers read renderNetwork() (live inputs). If inputs change after rendering, edge/node clicks can map against a different dataset than what is shown.

💡 Suggested fix (cache displayed network snapshot and reuse it in click observers)
   # Reactive value to store search results
   proteinSearchResults <- reactiveVal(NULL)
+  
+  # Snapshot of the network currently displayed in the UI
+  displayedNetworkData <- reactiveVal(NULL)

@@
   render_data <- networkVisualization()
   if (is.null(render_data)) {
     # Hide loading indicator and re-enable button if there's an error
     shinyjs::hide("loadingIndicator")
     shinyjs::enable("showNetwork")
     return()
   }
+  displayedNetworkData(render_data)

@@
   observeEvent(input$network_edge_clicked, {
     edge_data <- input$network_edge_clicked
-    network_data <- renderNetwork()
+    network_data <- displayedNetworkData()
     req(network_data)
     edges_table <- network_data$edges_table
     highlightEdgeInTable(output, edge_data, edges_table)
   })

@@
   observeEvent(input$network_node_clicked, {
     node_data <- input$network_node_clicked
-    network_data <- renderNetwork()
+    network_data <- displayedNetworkData()
     req(network_data)
     nodes_table <- network_data$nodes_table
     highlightNodeInTable(output, node_data, nodes_table)
   })

Also applies to: 612-627

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

In `@R/module-visualize-network-server.R` around lines 568 - 575, The rendered
network uses a snapshot (render_data) for display but the click handlers read
the live renderNetwork() inputs, causing mismatches; persist the displayed
network snapshot (e.g., assign the snapshot produced for output$network to a
reactiveVal or reactiveValues like displayedNetworkSnapshot) when creating the
cytoscapeNetwork in output$network, and update the click/selection observers to
read from that persisted displayedNetworkSnapshot instead of calling
renderNetwork() (also update the other observers referenced around the block
handling clicks at the later section currently reading renderNetwork()); ensure
the snapshot contains both nodes_table and edges_table and is updated only when
the Display Network action is performed so clicks map to the shown graph.


# Render data tables
renderDataTables(output, render_data$nodes_table, render_data$edges_table)
Expand Down Expand Up @@ -688,23 +609,20 @@ visualizeNetworkServer <- function(id, parent_session, dataComparison) {
)

# Observe edge click events
observeEvent(input$edgeClicked, {
edge_data <- input$edgeClicked
observeEvent(input$network_edge_clicked, {
edge_data <- input$network_edge_clicked
network_data <- renderNetwork()
req(network_data)
edges_table <- network_data$edges_table

highlightEdgeInTable(output, edge_data, edges_table)
openEvidenceLink(session, edge_data$evidenceLink)
})

# Observe node click events
observeEvent(input$nodeClicked, {
node_data <- input$nodeClicked
observeEvent(input$network_node_clicked, {
node_data <- input$network_node_clicked
network_data <- renderNetwork()
req(network_data)
nodes_table <- network_data$nodes_table

highlightNodeInTable(output, node_data, nodes_table)
})

Expand Down
151 changes: 4 additions & 147 deletions R/module-visualize-network-ui.R
Original file line number Diff line number Diff line change
@@ -1,39 +1,3 @@
# =============================================================================
# HELPER FUNCTIONS - External Dependencies
# =============================================================================

createCytoscapeScripts <- function() {
tags$head(
tags$script(src = "https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.32.0/cytoscape.min.js"),
tags$script(src = "https://cdnjs.cloudflare.com/ajax/libs/graphlib/2.1.8/graphlib.min.js"),
tags$script(src = "https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js"),
tags$script(src = "https://unpkg.com/cytoscape-dagre@2.3.0/cytoscape-dagre.js"),
tags$script("
Shiny.addCustomMessageHandler('runCytoscape', function(code) {
try {
new Function(code)();
} catch (err) {
console.error('Error running Cytoscape code:', err);
}
});
"),
tags$script("
Shiny.addCustomMessageHandler('openLinkInNewTab', function(message) {
try {
if (!message || typeof message.url !== 'string') return;
var url = message.url.trim();
if (!url) return;
if (!/^https?:\\/\\//i.test(url)) { console.warn('Blocked non-http(s) URL'); return; }
var win = window.open(url, '_blank', 'noopener,noreferrer');
if (win) { win.opener = null; }
} catch (err) {
console.error('Error opening link:', err);
}
});
")
)
}

# =============================================================================
# HELPER FUNCTIONS - UI Components
# =============================================================================
Expand Down Expand Up @@ -341,117 +305,15 @@ createDataUploadBox <- function(ns) {
)
}

createNetworkLegends <- function() {
div(
style = "position: absolute; top: 10px; right: 10px; z-index: 1000; display: flex; flex-direction: column; gap: 8px;",

# LogFC Legend
div(
style = "background-color: rgba(255, 255, 255, 0.95); border: 2px solid #ddd; border-radius: 4px;
padding: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.15); min-width: 180px; max-width: 200px;",

# Legend title
div(
style = "font-weight: bold; font-size: 16px; margin-bottom: 8px; text-align: center; color: #333;",
"LogFC"
),

# Color gradient bar
div(
style = "height: 20px; width: 100%; background: linear-gradient(to right, #ADD8E6, #D3D3D3, #FFA590);
border: 1px solid #ccc; border-radius: 3px; margin-bottom: 6px;"
),

# Labels container
div(
style = "display: flex; justify-content: space-between; font-size: 14px; color: #333;",
tags$span("Down", style = "font-weight: bold;"),
tags$span("0", style = "font-weight: bold;"),
tags$span("Up", style = "font-weight: bold;")
)
),

# Edge Legend
div(
style = "background-color: rgba(255, 255, 255, 0.95); border: 2px solid #ddd; border-radius: 4px;
padding: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.15); min-width: 180px; max-width: 200px;",

# Legend title
div(
style = "font-weight: bold; font-size: 16px; margin-bottom: 8px; text-align: center; color: #333;",
"Edges"
),

# Edge type entries
div(
style = "font-size: 13px; line-height: 1.6;",

# Two-column layout with proper row alignment
div(
div(
style = "display: flex; justify-content: space-between; margin: 3px 0;",
div(
style = "display: flex; align-items: center; flex: 1;",
tags$span("-", style = "color: #8B4513; font-weight: bold; margin-right: 5px; font-size: 14px;"),
tags$span("Complex", style = "color: #333; font-size: 13px;")
),
div(
style = "display: flex; align-items: center; flex: 1; padding-left: 6px;",
tags$span("- -", style = "color: #9932CC; font-weight: bold; margin-right: 5px; font-size: 14px;"),
tags$span("Phospho", style = "color: #333; font-size: 13px;")
)
),
div(
style = "display: flex; justify-content: space-between; margin: 3px 0;",
div(
style = "display: flex; align-items: center; flex: 1;",
tags$span("->", style = "color: #44AA44; font-weight: bold; margin-right: 5px; font-size: 14px;"),
tags$span("Activate", style = "color: #333; font-size: 13px;")
),
div(
style = "display: flex; align-items: center; flex: 1; padding-left: 6px;",
tags$span("->", style = "color: #FF4444; font-weight: bold; margin-right: 5px; font-size: 14px;"),
tags$span("Inhibit", style = "color: #333; font-size: 13px;")
)
),

div(
style = "display: flex; justify-content: space-between; margin: 3px 0;",
div(
style = "display: flex; align-items: center; flex: 1;",
tags$span("->", style = "color: #4488FF; font-weight: bold; margin-right: 5px; font-size: 14px;"),
tags$span("Increase", style = "color: #333; font-size: 13px;")
),
div(
style = "display: flex; align-items: center; flex: 1; padding-left: 6px;",
tags$span("->", style = "color: #FF8844; font-weight: bold; margin-right: 5px; font-size: 14px;"),
tags$span("Decrease", style = "color: #333; font-size: 13px;")
)
)
)
)
)
)
}

createNetworkVisualizationBox <- function(ns) {
box(
title = "Network Visualization",
status = "success",
solidHeader = TRUE,
width = 12,
# Container with relative positioning for the legends
div(
style = "position: relative; width: 100%; height: 500px;",

# Main network container
tags$div(
id = ns("cy"),
style = "width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;"
),

# Combined legend overlay
createNetworkLegends()
style = "width: 100%; height: 600px;",
MSstatsBioNet::cytoscapeNetworkOutput(ns("network"), height = "600px")
)
)
}
Expand All @@ -462,9 +324,7 @@ createEdgesTableBox <- function(ns) {
status = "warning",
solidHeader = TRUE,
width = 12,
div(style = "overflow-x: auto;",
DTOutput(ns("edgesTable"))
)
DTOutput(ns("edgesTable"))
)
}

Expand All @@ -474,9 +334,7 @@ createNodesTableBox <- function(ns) {
status = "info",
solidHeader = TRUE,
width = 12,
div(style = "overflow-x: auto;",
DTOutput(ns("nodesTable"))
)
DTOutput(ns("nodesTable"))
)
}

Expand Down Expand Up @@ -556,7 +414,6 @@ networkUI <- function(id) {

tagList(
shinyjs::useShinyjs(),
createCytoscapeScripts(),
dashboardPage(
createDashboardHeader(),
dashboardSidebar(disable = TRUE),
Expand Down
32 changes: 0 additions & 32 deletions man/generateCytoscapeJSForShiny.Rd

This file was deleted.

Loading
Loading