diff --git a/NAMESPACE b/NAMESPACE index 209d2ad..ed41e9d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -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) diff --git a/R/module-visualize-network-server.R b/R/module-visualize-network-server.R index c6b4102..60a8097 100644 --- a/R/module-visualize-network-server.R +++ b/R/module-visualize-network-server.R @@ -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, @@ -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 # ============================================================================= @@ -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 )) @@ -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) }) @@ -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 + ) + }) # Render data tables renderDataTables(output, render_data$nodes_table, render_data$edges_table) @@ -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) }) diff --git a/R/module-visualize-network-ui.R b/R/module-visualize-network-ui.R index 971a289..9c40afd 100644 --- a/R/module-visualize-network-ui.R +++ b/R/module-visualize-network-ui.R @@ -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 # ============================================================================= @@ -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") ) ) } @@ -462,9 +324,7 @@ createEdgesTableBox <- function(ns) { status = "warning", solidHeader = TRUE, width = 12, - div(style = "overflow-x: auto;", - DTOutput(ns("edgesTable")) - ) + DTOutput(ns("edgesTable")) ) } @@ -474,9 +334,7 @@ createNodesTableBox <- function(ns) { status = "info", solidHeader = TRUE, width = 12, - div(style = "overflow-x: auto;", - DTOutput(ns("nodesTable")) - ) + DTOutput(ns("nodesTable")) ) } @@ -556,7 +414,6 @@ networkUI <- function(id) { tagList( shinyjs::useShinyjs(), - createCytoscapeScripts(), dashboardPage( createDashboardHeader(), dashboardSidebar(disable = TRUE), diff --git a/man/generateCytoscapeJSForShiny.Rd b/man/generateCytoscapeJSForShiny.Rd deleted file mode 100644 index a983807..0000000 --- a/man/generateCytoscapeJSForShiny.Rd +++ /dev/null @@ -1,32 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/module-visualize-network-server.R -\name{generateCytoscapeJSForShiny} -\alias{generateCytoscapeJSForShiny} -\title{Generate Cytoscape JavaScript with Shiny event handling} -\usage{ -generateCytoscapeJSForShiny( - node_elements, - edge_elements, - display_label_type = "id", - container_id = "network-cy", - module_id = "network" -) -} -\arguments{ -\item{node_elements}{Node elements from package} - -\item{edge_elements}{Edge elements from package} - -\item{display_label_type}{Column to display the label from the node table} - -\item{container_id}{Network Visualization Container ID (default: 'network-cy')} - -\item{module_id}{Module ID for Shiny Application (default: 'network')} -} -\value{ -JavaScript code string with Shiny event handlers -} -\description{ -This function wraps the package's generateCytoscapeConfig() function -and adds Shiny-specific event handling for table highlighting -} diff --git a/tests/testthat/test_network_visualization.R b/tests/testthat/test_network_visualization.R index c8096fb..7a1b789 100644 --- a/tests/testthat/test_network_visualization.R +++ b/tests/testthat/test_network_visualization.R @@ -54,22 +54,6 @@ create_mock_subnetwork <- function() { ) } -# ============================================================================= -# TESTS FOR SHINY-SPECIFIC FUNCTIONS -# ============================================================================= - -test_that("generateCytoscapeJSForShiny includes Shiny event handlers", { - nodes = create_mock_subnetwork_nodes() - edges = create_mock_subnetwork_edges() - - js_code <- generateCytoscapeJSForShiny(nodes, edges) - - expect_type(js_code, "character") - expect_true(grepl("Shiny.setInputValue", js_code)) - expect_true(grepl("network-edgeClicked", js_code)) - expect_true(grepl("network-nodeClicked", js_code)) -}) - # ============================================================================= # TESTS FOR DATA PROCESSING HELPER FUNCTIONS # ============================================================================= @@ -206,43 +190,6 @@ test_that("extractSubnetwork works with mocked MSstatsBioNet function", { expect_equal(nrow(result$edges), 4) }) -# ============================================================================= -# INTEGRATION TESTS -# ============================================================================= - -test_that("Full pipeline works with mocked functions", { - - input_df <- create_mock_input_data() - subnetwork <- create_mock_subnetwork() - - # Mock the annotation function - mock_annotate_func <- function(df, id_type) { - df$HgncId <- c("TP53", "MDM2", "ATM", "BRCA1") - df$HgncName <- c("TP53", "MDM2", "ATM", "BRCA1") - return(df) - } - - # Mock the extraction function - mock_extract_func <- function(...) { - return(subnetwork) - } - - # Use mockery to stub the function calls - stub(annotateProteinData, "annotateProteinInfoFromIndra", mock_annotate_func) - stub(extractSubnetwork, "getSubnetworkFromIndra", mock_extract_func) - - # Test the full pipeline - filtered_df <- filterDataByLabel(input_df, "Treatment_vs_Control") - annotated <- annotateProteinData(filtered_df, "Uniprot") - subnet <- extractSubnetwork(annotated, 0.05, 5, NULL, NULL, 0.5, NULL, FALSE) - - # Generate configuration - js_code <- generateCytoscapeJSForShiny(subnet$nodes, subnet$edges) - - expect_type(js_code, "character") - expect_true(nchar(js_code) > 0) -}) - # ============================================================================= # TESTS FOR ERROR HANDLING # =============================================================================