event.preventDefault()}
+ >
+
+
+
+
+
+
+
+
+ Nodes:
+
+
+
+
+
+
+
+
+ Edges:
+
+
+
+
+
{tooltip.visible && (
)}
+
+ {contextMenu.visible && (
+
+
+ {contextMenu.nodeLabel}
+
+
+
+ Configure this expansion before adding it to the graph request.
+
+
+
+
+
+ Threshold
+ {contextMenu.threshold.toFixed(1)}
+
+
setContextMenu((prev) => ({
+ ...prev,
+ threshold: Number(Number(event.target.value).toFixed(1)),
+ }))}
+ />
+
+
+
+
+ Depth
+ {contextMenu.maxLevels}
+
+
setContextMenu((prev) => ({
+ ...prev,
+ maxLevels: Number(event.target.value),
+ }))}
+ />
+
+
+
+ Regulation mode
+
+
+
+
+
+
+ )}
)
}
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GraphControls.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GraphControls.tsx
index 4800444c..a1036ed8 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GraphControls.tsx
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GraphControls.tsx
@@ -41,6 +41,10 @@ export const GraphControls = ({
}}
>
+
+ These values update the BRAF root while it is the only active filter. After expanding nodes, they are used for the next right-click expansion.
+
+
new Promise((resolve) => setTimeout(resolve, ms))
const passesThreshold = (correlation: number, threshold: number) =>
Math.abs(correlation) >= threshold
+const getNodeLabel = (nodeId: string) =>
+ MOCK_NODES.find((node) => node.id === nodeId)?.label ?? nodeId
+
+const getEdgesForFilter = (rootNodeId: string) => [
+ ...MOCK_EDGES,
+ ...(MOCK_EXPANSION_EDGES_BY_ROOT[rootNodeId] ?? []),
+]
+
const buildSummary = (map: Map
) =>
Array.from(map.entries())
.sort((a, b) => a[0] - b[0])
@@ -20,12 +35,36 @@ const buildSummary = (map: Map) =>
nodes: [...nodes].sort((a, b) => a.localeCompare(b)),
}))
+const mergeSummaries = (summaries: DepthSummaryItem[][]) => {
+ const summaryMap = new Map>()
+
+ for (const summary of summaries) {
+ for (const item of summary) {
+ const nodes = summaryMap.get(item.depth) ?? new Set()
+
+ for (const node of item.nodes) {
+ nodes.add(node)
+ }
+
+ summaryMap.set(item.depth, nodes)
+ }
+ }
+
+ return Array.from(summaryMap.entries())
+ .sort((a, b) => a[0] - b[0])
+ .map(([depth, nodes]): DepthSummaryItem => ({
+ depth,
+ nodes: Array.from(nodes).sort((a, b) => a.localeCompare(b)),
+ }))
+}
+
const walkOutgoing = (rootNodeId: string, edges: GraphEdge[], maxLevels: number) => {
const visitedNodes = new Map()
const includedEdgeIds = new Set()
const summaryMap = new Map()
const queue: Array<{ nodeId: string; depth: number }> = [{ nodeId: rootNodeId, depth: 0 }]
+
visitedNodes.set(rootNodeId, 0)
while (queue.length > 0) {
@@ -49,7 +88,7 @@ const walkOutgoing = (rootNodeId: string, edges: GraphEdge[], maxLevels: number)
if (!visitedNodes.has(edge.target) || nextDepth < visitedNodes.get(edge.target)!) {
visitedNodes.set(edge.target, nextDepth)
- const targetLabel = MOCK_NODES.find((node) => node.id === edge.target)?.label ?? edge.target
+ const targetLabel = getNodeLabel(edge.target)
const arr = summaryMap.get(nextDepth) || []
if (!arr.includes(targetLabel)) {
@@ -75,6 +114,7 @@ const walkIncoming = (rootNodeId: string, edges: GraphEdge[], maxLevels: number)
const summaryMap = new Map()
const queue: Array<{ nodeId: string; depth: number }> = [{ nodeId: rootNodeId, depth: 0 }]
+
visitedNodes.set(rootNodeId, 0)
while (queue.length > 0) {
@@ -98,7 +138,7 @@ const walkIncoming = (rootNodeId: string, edges: GraphEdge[], maxLevels: number)
if (!visitedNodes.has(edge.source) || nextDepth < visitedNodes.get(edge.source)!) {
visitedNodes.set(edge.source, nextDepth)
- const sourceLabel = MOCK_NODES.find((node) => node.id === edge.source)?.label ?? edge.source
+ const sourceLabel = getNodeLabel(edge.source)
const arr = summaryMap.get(nextDepth) || []
if (!arr.includes(sourceLabel)) {
@@ -118,26 +158,21 @@ const walkIncoming = (rootNodeId: string, edges: GraphEdge[], maxLevels: number)
}
}
-const collectResponse = (
- rootNodeId: string,
- traversalMode: TraversalMode,
- threshold: number,
- maxLevels: number
-): FetchGeneGraphResponse => {
- const thresholdEdges = MOCK_EDGES.filter((edge) =>
- passesThreshold(edge.correlation, threshold)
+const collectFilterResponse = (filter: GraphQueryFilter): FetchGeneGraphResponse => {
+ const thresholdEdges = getEdgesForFilter(filter.rootNodeId).filter((edge) =>
+ passesThreshold(edge.correlation, filter.threshold)
)
- const outgoing = traversalMode === 'outgoing' || traversalMode === 'both'
- ? walkOutgoing(rootNodeId, thresholdEdges, maxLevels)
+ const outgoing = filter.traversalMode === 'outgoing' || filter.traversalMode === 'both'
+ ? walkOutgoing(filter.rootNodeId, thresholdEdges, filter.maxLevels)
: {
visitedNodes: new Map(),
includedEdgeIds: new Set(),
summary: [] as DepthSummaryItem[],
}
- const incoming = traversalMode === 'incoming' || traversalMode === 'both'
- ? walkIncoming(rootNodeId, thresholdEdges, maxLevels)
+ const incoming = filter.traversalMode === 'incoming' || filter.traversalMode === 'both'
+ ? walkIncoming(filter.rootNodeId, thresholdEdges, filter.maxLevels)
: {
visitedNodes: new Map(),
includedEdgeIds: new Set(),
@@ -150,7 +185,6 @@ const collectResponse = (
])
const edges = thresholdEdges.filter((edge) => includedEdgeIds.has(edge.id))
-
const includedNodeIdsFromEdges = new Set()
for (const edge of edges) {
@@ -158,11 +192,9 @@ const collectResponse = (
includedNodeIdsFromEdges.add(edge.target)
}
- // Keep the root visible even when no edges match the current filters.
- includedNodeIdsFromEdges.add(rootNodeId)
+ includedNodeIdsFromEdges.add(filter.rootNodeId)
const nodes = MOCK_NODES.filter((node) => includedNodeIdsFromEdges.has(node.id))
-
const validLabels = new Set(nodes.map((node) => node.label))
const outgoingSummary = outgoing.summary
@@ -180,11 +212,38 @@ const collectResponse = (
.filter((item) => item.nodes.length > 0)
return {
- rootNodeId,
+ rootNodeId: filter.rootNodeId,
nodes,
edges,
outgoingSummary,
incomingSummary,
+ filters: [filter],
+ }
+}
+
+const collectResponse = (filters: GraphQueryFilter[]): FetchGeneGraphResponse => {
+ const safeFilters = filters.length > 0 ? filters : [FALLBACK_FILTER]
+ const responses = safeFilters.map(collectFilterResponse)
+ const nodeIds = new Set()
+ const edgesById = new Map()
+
+ for (const response of responses) {
+ for (const node of response.nodes) {
+ nodeIds.add(node.id)
+ }
+
+ for (const edge of response.edges) {
+ edgesById.set(edge.id, edge)
+ }
+ }
+
+ return {
+ rootNodeId: safeFilters[0].rootNodeId,
+ nodes: MOCK_NODES.filter((node) => nodeIds.has(node.id)),
+ edges: Array.from(edgesById.values()),
+ outgoingSummary: mergeSummaries(responses.map((response) => response.outgoingSummary)),
+ incomingSummary: mergeSummaries(responses.map((response) => response.incomingSummary)),
+ filters: safeFilters,
}
}
@@ -193,10 +252,5 @@ export const fetchGeneGraph = async (
): Promise => {
await sleep(350)
- return collectResponse(
- params.rootNodeId,
- params.traversalMode,
- params.threshold,
- params.maxLevels
- )
+ return collectResponse(params.filters)
}
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/mockNetworkData.ts b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/mockNetworkData.ts
index b39c6147..39080864 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/mockNetworkData.ts
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/mockNetworkData.ts
@@ -18,6 +18,48 @@ export const MOCK_NODES: GraphNode[] = [
{ id: 'cna_7q34', label: '7q34 gain', type: 'CNA', size: 34 },
{ id: 'meth_rassf1', label: 'RASSF1 meth', type: 'Methylation', size: 34 },
{ id: 'drug_vemurafenib', label: 'Vemurafenib', type: 'Drug', size: 36 },
+ { id: 'gene_akt1', label: 'AKT1', type: 'Gene', size: 32 },
+ { id: 'gene_grb2', label: 'GRB2', type: 'Gene', size: 30 },
+ { id: 'gene_raf1', label: 'RAF1', type: 'Gene', size: 32 },
+ { id: 'drug_trametinib', label: 'Trametinib', type: 'Drug', size: 34 },
+ { id: 'gene_mapkapk2', label: 'MAPKAPK2', type: 'Gene', size: 30 },
+ { id: 'gene_rps6ka1', label: 'RPS6KA1', type: 'Gene', size: 30 },
+ { id: 'gene_dusp4', label: 'DUSP4', type: 'Gene', size: 30 },
+ { id: 'gene_jun', label: 'JUN', type: 'Gene', size: 32 },
+ { id: 'gene_etv4', label: 'ETV4', type: 'Gene', size: 30 },
+ { id: 'gene_dusp5', label: 'DUSP5', type: 'Gene', size: 30 },
+ { id: 'gene_rps6ka3', label: 'RPS6KA3', type: 'Gene', size: 30 },
+ { id: 'gene_elk4', label: 'ELK4', type: 'Gene', size: 30 },
+ { id: 'gene_ets1', label: 'ETS1', type: 'Gene', size: 30 },
+ { id: 'gene_cdk4', label: 'CDK4', type: 'Gene', size: 30 },
+ { id: 'gene_tert', label: 'TERT', type: 'Gene', size: 30 },
+ { id: 'gene_max', label: 'MAX', type: 'Gene', size: 30 },
+ { id: 'mir_34a', label: 'miR-34a', type: 'miRNA', size: 28 },
+ { id: 'gene_junb', label: 'JUNB', type: 'Gene', size: 30 },
+ { id: 'gene_atf3', label: 'ATF3', type: 'Gene', size: 30 },
+ { id: 'gene_mmp9', label: 'MMP9', type: 'Gene', size: 30 },
+ { id: 'gene_cdk6', label: 'CDK6', type: 'Gene', size: 30 },
+ { id: 'gene_rb1', label: 'RB1', type: 'Gene', size: 30 },
+ { id: 'drug_palbociclib', label: 'Palbociclib', type: 'Drug', size: 34 },
+ { id: 'gene_egr1', label: 'EGR1', type: 'Gene', size: 30 },
+ { id: 'gene_srf', label: 'SRF', type: 'Gene', size: 30 },
+ { id: 'gene_fgfr1', label: 'FGFR1', type: 'Gene', size: 30 },
+ { id: 'gene_spry4', label: 'SPRY4', type: 'Gene', size: 30 },
+ { id: 'gene_egfr', label: 'EGFR', type: 'Gene', size: 32 },
+ { id: 'gene_fgfr2', label: 'FGFR2', type: 'Gene', size: 30 },
+ { id: 'gene_nfkb1', label: 'NFKB1', type: 'Gene', size: 30 },
+ { id: 'gene_rela', label: 'RELA', type: 'Gene', size: 30 },
+ { id: 'gene_pten', label: 'PTEN', type: 'Gene', size: 32 },
+ { id: 'gene_e2f1', label: 'E2F1', type: 'Gene', size: 30 },
+ { id: 'gene_bcl2l11', label: 'BCL2L11', type: 'Gene', size: 30 },
+ { id: 'gene_pdcd4', label: 'PDCD4', type: 'Gene', size: 30 },
+ { id: 'gene_tpm1', label: 'TPM1', type: 'Gene', size: 30 },
+ { id: 'gene_braf_fusion', label: 'BRAF fusion', type: 'Gene', size: 32 },
+ { id: 'gene_kiaa1549', label: 'KIAA1549', type: 'Gene', size: 30 },
+ { id: 'gene_rassf1', label: 'RASSF1', type: 'Gene', size: 30 },
+ { id: 'gene_cdkn2a', label: 'CDKN2A', type: 'Gene', size: 30 },
+ { id: 'gene_axl', label: 'AXL', type: 'Gene', size: 30 },
+ { id: 'gene_dusp1', label: 'DUSP1', type: 'Gene', size: 30 },
]
export const MOCK_EDGES: GraphEdge[] = [
@@ -50,3 +92,86 @@ export const MOCK_EDGES: GraphEdge[] = [
{ id: 'e_22', source: 'gene_spry2', target: 'gene_braf', correlation: -0.32 },
{ id: 'e_23', source: 'drug_vemurafenib', target: 'gene_mek1', correlation: -0.41 },
]
+
+export const MOCK_EXPANSION_EDGES_BY_ROOT: Record = {
+ gene_mek1: [
+ { id: 'e_exp_mek1_1', source: 'gene_raf1', target: 'gene_mek1', correlation: 0.86 },
+ { id: 'e_exp_mek1_2', source: 'gene_grb2', target: 'gene_mek1', correlation: 0.62 },
+ { id: 'e_exp_mek1_3', source: 'gene_mek1', target: 'gene_akt1', correlation: 0.74 },
+ { id: 'e_exp_mek1_4', source: 'drug_trametinib', target: 'gene_mek1', correlation: -0.91 },
+ { id: 'e_exp_mek1_5', source: 'gene_akt1', target: 'gene_myc', correlation: 0.58 },
+ ],
+ gene_mek2: [
+ { id: 'e_exp_mek2_1', source: 'gene_mek2', target: 'gene_mapkapk2', correlation: 0.73 },
+ { id: 'e_exp_mek2_2', source: 'gene_mek2', target: 'gene_rps6ka1', correlation: 0.69 },
+ { id: 'e_exp_mek2_3', source: 'gene_dusp4', target: 'gene_mek2', correlation: -0.61 },
+ ],
+ gene_erk1: [
+ { id: 'e_exp_erk1_1', source: 'gene_erk1', target: 'gene_jun', correlation: 0.84 },
+ { id: 'e_exp_erk1_2', source: 'gene_erk1', target: 'gene_etv4', correlation: 0.78 },
+ { id: 'e_exp_erk1_3', source: 'gene_dusp5', target: 'gene_erk1', correlation: -0.71 },
+ { id: 'e_exp_erk1_4', source: 'gene_jun', target: 'gene_fos', correlation: 0.65 },
+ ],
+ gene_erk2: [
+ { id: 'e_exp_erk2_1', source: 'gene_erk2', target: 'gene_rps6ka3', correlation: 0.82 },
+ { id: 'e_exp_erk2_2', source: 'gene_erk2', target: 'gene_elk4', correlation: 0.73 },
+ { id: 'e_exp_erk2_3', source: 'gene_ets1', target: 'gene_erk2', correlation: 0.59 },
+ ],
+ gene_myc: [
+ { id: 'e_exp_myc_1', source: 'gene_myc', target: 'gene_cdk4', correlation: 0.82 },
+ { id: 'e_exp_myc_2', source: 'gene_myc', target: 'gene_tert', correlation: 0.76 },
+ { id: 'e_exp_myc_3', source: 'gene_max', target: 'gene_myc', correlation: 0.88 },
+ { id: 'e_exp_myc_4', source: 'mir_34a', target: 'gene_myc', correlation: -0.79 },
+ ],
+ gene_fos: [
+ { id: 'e_exp_fos_1', source: 'gene_fos', target: 'gene_junb', correlation: 0.84 },
+ { id: 'e_exp_fos_2', source: 'gene_fos', target: 'gene_atf3', correlation: 0.76 },
+ { id: 'e_exp_fos_3', source: 'gene_junb', target: 'gene_mmp9', correlation: 0.71 },
+ { id: 'e_exp_fos_4', source: 'mir_21', target: 'gene_fos', correlation: -0.62 },
+ ],
+ gene_ccnd1: [
+ { id: 'e_exp_ccnd1_1', source: 'gene_ccnd1', target: 'gene_cdk6', correlation: 0.81 },
+ { id: 'e_exp_ccnd1_2', source: 'gene_ccnd1', target: 'gene_rb1', correlation: -0.64 },
+ { id: 'e_exp_ccnd1_3', source: 'drug_palbociclib', target: 'gene_ccnd1', correlation: -0.88 },
+ ],
+ gene_elk1: [
+ { id: 'e_exp_elk1_1', source: 'gene_elk1', target: 'gene_egr1', correlation: 0.79 },
+ { id: 'e_exp_elk1_2', source: 'gene_srf', target: 'gene_elk1', correlation: 0.61 },
+ ],
+ gene_dusp6: [
+ { id: 'e_exp_dusp6_1', source: 'gene_dusp6', target: 'gene_fgfr1', correlation: -0.75 },
+ { id: 'e_exp_dusp6_2', source: 'gene_dusp6', target: 'gene_spry4', correlation: 0.66 },
+ ],
+ gene_spry2: [
+ { id: 'e_exp_spry2_1', source: 'gene_spry2', target: 'gene_egfr', correlation: -0.72 },
+ { id: 'e_exp_spry2_2', source: 'gene_fgfr2', target: 'gene_spry2', correlation: 0.69 },
+ ],
+ gene_map3k8: [
+ { id: 'e_exp_map3k8_1', source: 'gene_map3k8', target: 'gene_nfkb1', correlation: 0.8 },
+ { id: 'e_exp_map3k8_2', source: 'gene_map3k8', target: 'gene_rela', correlation: 0.73 },
+ ],
+ mir_17: [
+ { id: 'e_exp_mir17_1', source: 'mir_17', target: 'gene_pten', correlation: -0.82 },
+ { id: 'e_exp_mir17_2', source: 'mir_17', target: 'gene_e2f1', correlation: -0.69 },
+ { id: 'e_exp_mir17_3', source: 'gene_e2f1', target: 'gene_bcl2l11', correlation: 0.64 },
+ ],
+ mir_21: [
+ { id: 'e_exp_mir21_1', source: 'mir_21', target: 'gene_pdcd4', correlation: -0.86 },
+ { id: 'e_exp_mir21_2', source: 'mir_21', target: 'gene_pten', correlation: -0.72 },
+ { id: 'e_exp_mir21_3', source: 'mir_21', target: 'gene_tpm1', correlation: -0.61 },
+ ],
+ cna_7q34: [
+ { id: 'e_exp_cna7q34_1', source: 'cna_7q34', target: 'gene_braf_fusion', correlation: 0.93 },
+ { id: 'e_exp_cna7q34_2', source: 'gene_kiaa1549', target: 'gene_braf_fusion', correlation: 0.77 },
+ { id: 'e_exp_cna7q34_3', source: 'gene_braf_fusion', target: 'gene_braf', correlation: 0.84 },
+ ],
+ meth_rassf1: [
+ { id: 'e_exp_meth_rassf1_1', source: 'meth_rassf1', target: 'gene_rassf1', correlation: -0.88 },
+ { id: 'e_exp_meth_rassf1_2', source: 'gene_rassf1', target: 'gene_cdkn2a', correlation: 0.62 },
+ ],
+ drug_vemurafenib: [
+ { id: 'e_exp_vemurafenib_1', source: 'drug_vemurafenib', target: 'gene_axl', correlation: 0.72 },
+ { id: 'e_exp_vemurafenib_2', source: 'drug_vemurafenib', target: 'gene_egfr', correlation: 0.68 },
+ { id: 'e_exp_vemurafenib_3', source: 'gene_dusp1', target: 'gene_braf', correlation: -0.55 },
+ ],
+}
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/types.ts b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/types.ts
index 2034a57c..3075e70e 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/types.ts
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/types.ts
@@ -16,6 +16,13 @@ export type GraphEdge = {
correlation: number;
}
+export type GraphQueryFilter = {
+ rootNodeId: string;
+ threshold: number;
+ traversalMode: TraversalMode;
+ maxLevels: number;
+}
+
export type SelectedEdgeInfo = {
id: string;
source: string;
@@ -30,10 +37,7 @@ export type DepthSummaryItem = {
}
export type FetchGeneGraphParams = {
- rootNodeId: string;
- threshold: number;
- traversalMode: TraversalMode;
- maxLevels: number;
+ filters: GraphQueryFilter[];
}
export type FetchGeneGraphResponse = {
@@ -42,4 +46,5 @@ export type FetchGeneGraphResponse = {
edges: GraphEdge[];
outgoingSummary: DepthSummaryItem[];
incomingSummary: DepthSummaryItem[];
+ filters: GraphQueryFilter[];
}
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/useGeneGraphQuery.ts b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/useGeneGraphQuery.ts
index e97402e6..9dbc5872 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/useGeneGraphQuery.ts
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/useGeneGraphQuery.ts
@@ -47,12 +47,7 @@ export const useGeneGraphQuery = (params: FetchGeneGraphParams) => {
return () => {
cancelled = true
}
- }, [
- params.rootNodeId,
- params.threshold,
- params.traversalMode,
- params.maxLevels,
- ])
+ }, [params])
return state
}
From 5e70b1c3cf09efb22a17744a4f7853867f629571 Mon Sep 17 00:00:00 2001
From: Juan Nicolas Herrera <54152074+juanNH@users.noreply.github.com>
Date: Fri, 24 Apr 2026 23:47:36 -0300
Subject: [PATCH 05/13] Comment access to tab to implement backend in future
---
.../molecules/CurrentMoleculeDetails.tsx | 7 ++++---
.../molecules/MoleculesDetailsMenu.tsx | 3 ++-
.../molecules/genes/GeneAssociationsNetwork.tsx | 9 +++++++++
3 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx
index cc17d21f..d4235c5b 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx
@@ -10,7 +10,7 @@ import { GeneOntologyPanel } from './gene-ontology/GeneOntologyPanel'
import { MiRNADrugsPanel } from '../../../pipeline/experiment-result/gene-gem-details/MiRNADrugsPanel'
import { MiRNADiseasesPanel } from '../../../pipeline/experiment-result/gene-gem-details/MiRNADiseasesPanel'
import { ActionableCancerGenesPanel } from './genes/ActionableCancerGenesPanel'
-import { GeneExpressionRegulationAssociationNetworkPanel } from './genes/gene-association-network/GeneExpressionRegulationNetworkPanel'
+/* import { GeneExpressionRegulationAssociationNetworkPanel } from './genes/gene-association-network/GeneExpressionRegulationNetworkPanel' */
import { GeneAssociationsNetworkPanel } from './genes/GeneAssociationsNetworkPanel'
// const MENU_DEFAULT: ActiveBiomarkerMoleculeItemMenu = ActiveBiomarkerMoleculeItemMenu.DETAILS // TODO: use this
@@ -53,8 +53,9 @@ export const CurrentMoleculeDetails = (props: CurrentMoleculeDetailsProps) => {
return
case ActiveBiomarkerMoleculeItemMenu.GENE_ONTOLOGY:
return
- case ActiveBiomarkerMoleculeItemMenu.GENE_REGULATION_ASSOCIATIONS:
- return
+ /* Todo: implement when experiment regulation associations are available
+ case ActiveBiomarkerMoleculeItemMenu.GENE_REGULATION_ASSOCIATIONS:
+ return */
case ActiveBiomarkerMoleculeItemMenu.DISEASES:
return
case ActiveBiomarkerMoleculeItemMenu.DRUGS:
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/MoleculesDetailsMenu.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/MoleculesDetailsMenu.tsx
index ec0b914c..e0318e2e 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/MoleculesDetailsMenu.tsx
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/MoleculesDetailsMenu.tsx
@@ -63,13 +63,14 @@ export const MoleculesDetailsMenu = (props: MoleculesDetailsMenuProps) => {
popupInfo: 'Gene Ontology (GO) is a powerful tool for understanding the biological processes, molecular functions, and cellular components associated with a gene',
isVisible: isGene
},
+ /* TODO: implement when experiment regulation associations are available
{
name: 'Gene regulation associations',
onClick: () => props.setActiveItem(ActiveBiomarkerMoleculeItemMenu.GENE_REGULATION_ASSOCIATIONS),
isActive: props.activeItem === ActiveBiomarkerMoleculeItemMenu.GENE_REGULATION_ASSOCIATIONS,
popupInfo: 'Gene regulation associations provide insights into the regulatory relationships between genes, helping to unravel the complex mechanisms that control gene expression and cellular function',
isVisible: isGene
- },
+ }, */
// TODO: implement
// {
// name: 'Actionable/Cancer genes',
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetwork.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetwork.tsx
index 85a7b886..f87b7b60 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetwork.tsx
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/GeneAssociationsNetwork.tsx
@@ -50,7 +50,9 @@ const roundThreshold = (value: number) => Number(value.toFixed(1))
const getEdgeColor = (correlation: number, threshold: number) => {
if (correlation <= -threshold) { return '#dc2626' }
+
if (correlation >= threshold) { return '#2563eb' }
+
return 'transparent'
}
@@ -58,7 +60,9 @@ const getEdgeOpacity = (correlation: number, threshold: number) => {
const abs = Math.abs(correlation)
if (abs < threshold) { return 0 }
+
if (abs >= 0.8) { return 0.95 }
+
if (abs >= 0.6) { return 0.8 }
return 0.7
@@ -68,8 +72,11 @@ const getEdgeWidth = (correlation: number, threshold: number) => {
const abs = Math.abs(correlation)
if (abs < threshold) { return 0 }
+
if (abs >= 0.9) { return 6 }
+
if (abs >= 0.8) { return 5 }
+
if (abs >= 0.7) { return 4 }
return 3
@@ -80,6 +87,7 @@ const getDirectionLabel = (
threshold: number
): SelectedEdgeInfo['direction'] => {
if (correlation <= -threshold) { return 'Down regulate' }
+
return 'Up regulate'
}
@@ -739,6 +747,7 @@ export const GeneExpressionRegulationNetworkPanel = ({
})
const defaultRoot = cy.getElementById('gene_braf')
+
if (defaultRoot && defaultRoot.nonempty()) {
applyTraversal(defaultRoot, traversalMode, maxLevels)
cy.center(defaultRoot)
From 1ea5264f82410060b1d36400fed62ffbb969325a Mon Sep 17 00:00:00 2001
From: Juan Nicolas Herrera <54152074+juanNH@users.noreply.github.com>
Date: Tue, 5 May 2026 09:24:33 -0300
Subject: [PATCH 06/13] Fixs comments, params, interfaces. Remove unused code
---
.../molecules/CurrentMoleculeDetails.tsx | 4 +-
.../GeneExpressionRegulationNetworkPanel.tsx | 178 ---------------
.../GeneRegulationAssociationsPanel.tsx | 205 ++++++++++++++++++
...nel.tsx => GeneRegulationFiltersPanel.tsx} | 52 +++--
...tworkGraph.tsx => GeneRegulationGraph.tsx} | 94 ++++++--
.../GraphControls.tsx | 184 ----------------
.../gene-association-network/LevelBadge.tsx | 20 +-
.../SelectedEdgesPanel.tsx | 21 +-
.../TraversalSummaryPanel.tsx | 27 ++-
.../gene-association-network/graphApi.ts | 89 ++++++--
.../gene-association-network/graphStyle.ts | 2 +
.../genes/gene-association-network/legend.tsx | 99 ++++++---
.../mockNetworkData.ts | 3 +
.../genes/gene-association-network/types.ts | 39 +++-
...uery.ts => useGeneRegulationGraphQuery.ts} | 25 ++-
15 files changed, 570 insertions(+), 472 deletions(-)
delete mode 100644 src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneExpressionRegulationNetworkPanel.tsx
create mode 100644 src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneRegulationAssociationsPanel.tsx
rename src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/{ActiveGraphFiltersPanel.tsx => GeneRegulationFiltersPanel.tsx} (80%)
rename src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/{GeneNetworkGraph.tsx => GeneRegulationGraph.tsx} (85%)
delete mode 100644 src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GraphControls.tsx
rename src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/{useGeneGraphQuery.ts => useGeneRegulationGraphQuery.ts} (51%)
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx
index d4235c5b..07fd58eb 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/CurrentMoleculeDetails.tsx
@@ -10,7 +10,7 @@ import { GeneOntologyPanel } from './gene-ontology/GeneOntologyPanel'
import { MiRNADrugsPanel } from '../../../pipeline/experiment-result/gene-gem-details/MiRNADrugsPanel'
import { MiRNADiseasesPanel } from '../../../pipeline/experiment-result/gene-gem-details/MiRNADiseasesPanel'
import { ActionableCancerGenesPanel } from './genes/ActionableCancerGenesPanel'
-/* import { GeneExpressionRegulationAssociationNetworkPanel } from './genes/gene-association-network/GeneExpressionRegulationNetworkPanel' */
+/* import { GeneRegulationAssociationsPanel } from './genes/gene-association-network/GeneRegulationAssociationsPanel' */
import { GeneAssociationsNetworkPanel } from './genes/GeneAssociationsNetworkPanel'
// const MENU_DEFAULT: ActiveBiomarkerMoleculeItemMenu = ActiveBiomarkerMoleculeItemMenu.DETAILS // TODO: use this
@@ -55,7 +55,7 @@ export const CurrentMoleculeDetails = (props: CurrentMoleculeDetailsProps) => {
return
/* Todo: implement when experiment regulation associations are available
case ActiveBiomarkerMoleculeItemMenu.GENE_REGULATION_ASSOCIATIONS:
- return */
+ return */
case ActiveBiomarkerMoleculeItemMenu.DISEASES:
return
case ActiveBiomarkerMoleculeItemMenu.DRUGS:
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneExpressionRegulationNetworkPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneExpressionRegulationNetworkPanel.tsx
deleted file mode 100644
index 6ae451f9..00000000
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneExpressionRegulationNetworkPanel.tsx
+++ /dev/null
@@ -1,178 +0,0 @@
-import React, { useMemo, useState } from 'react'
-import { Loader, Message } from 'semantic-ui-react'
-import { ActiveGraphFiltersPanel } from './ActiveGraphFiltersPanel'
-import { GeneNetworkGraph } from './GeneNetworkGraph'
-import { SelectedEdgesPanel } from './SelectedEdgesPanel'
-import { TraversalSummaryPanel } from './TraversalSummaryPanel'
-import { useGeneGraphQuery } from './useGeneGraphQuery'
-import { GraphQueryFilter, SelectedEdgeInfo } from './types'
-
-type Props = {
- height?: number | string;
- width?: number | string;
-}
-
-const DEFAULT_ROOT_FILTER: GraphQueryFilter = {
- rootNodeId: 'gene_braf',
- threshold: 0.5,
- traversalMode: 'both',
- maxLevels: 3,
-}
-
-export const GeneExpressionRegulationAssociationNetworkPanel = ({
- height = 650,
- width = '100%',
-}: Props) => {
- const [selectedEdges, setSelectedEdges] = useState([])
- const [filters, setFilters] = useState([DEFAULT_ROOT_FILTER])
- const [draftFilters, setDraftFilters] = useState([DEFAULT_ROOT_FILTER])
-
- const defaultExpansionFilter = useMemo(() => ({
- threshold: draftFilters[0]?.threshold ?? DEFAULT_ROOT_FILTER.threshold,
- traversalMode: draftFilters[0]?.traversalMode ?? DEFAULT_ROOT_FILTER.traversalMode,
- maxLevels: draftFilters[0]?.maxLevels ?? DEFAULT_ROOT_FILTER.maxLevels,
- }), [draftFilters])
-
- const queryParams = useMemo(() => ({
- filters,
- }), [filters])
-
- const { data, loading, error } = useGeneGraphQuery(queryParams)
- const hasPendingFilterChanges = filters.length !== draftFilters.length || filters.some((filter, index) => {
- const draftFilter = draftFilters[index]
-
- if (!draftFilter) { return true }
-
- return filter.rootNodeId !== draftFilter.rootNodeId ||
- filter.threshold !== draftFilter.threshold ||
- filter.traversalMode !== draftFilter.traversalMode ||
- filter.maxLevels !== draftFilter.maxLevels
- })
-
- const handleExpandNode = (filter: GraphQueryFilter) => {
- setSelectedEdges([])
- setDraftFilters((prev) => {
- if (prev.some((item) => item.rootNodeId === filter.rootNodeId)) {
- setFilters(prev)
- return prev
- }
-
- const nextFilters = [
- ...prev,
- filter,
- ]
-
- setFilters(nextFilters)
-
- return nextFilters
- })
- }
-
- const handleUpdateFilter = (rootNodeId: string, partialFilter: Partial) => {
- setDraftFilters((prev) => prev.map((filter) => {
- if (filter.rootNodeId !== rootNodeId) { return filter }
-
- return {
- ...filter,
- ...partialFilter,
- }
- }))
- }
-
- const handleRemoveFilter = (rootNodeId: string) => {
- setDraftFilters((prev) => prev.filter((filter, index) => index === 0 || filter.rootNodeId !== rootNodeId))
- }
-
- const handleClearExpansions = () => {
- setDraftFilters((prev) => {
- if (prev.length === 0) { return [DEFAULT_ROOT_FILTER] }
-
- return [prev[0]]
- })
- }
-
- const handleResetFilters = () => {
- setDraftFilters(filters.map((filter) => ({
- ...filter,
- })))
- }
-
- const handleApplyFilters = () => {
- setSelectedEdges([])
- setFilters(draftFilters)
- }
-
- return (
-
- {loading && (
-
-
-
- )}
-
- {error && (
-
- Error fetching graph
- {error}
-
- )}
-
- {!loading && !error && data && (
-
filter.rootNodeId)}
- defaultExpansionFilter={defaultExpansionFilter}
- onExpandNode={handleExpandNode}
- />
- )}
-
- {!loading && !error && data && (
- data.nodes.find((node) => node.id === nodeId)?.label ?? nodeId}
- onUpdateFilter={handleUpdateFilter}
- onClearExpansions={handleClearExpansions}
- onRemoveFilter={handleRemoveFilter}
- onResetFilters={handleResetFilters}
- onApplyFilters={handleApplyFilters}
- />
- )}
-
- {!loading && !error && data && (
-
- setSelectedEdges((prev) => prev.filter((edge) => edge.id !== edgeId))}
- onClearAll={() => setSelectedEdges([])}
- />
-
- node.id === data.rootNodeId)?.label ?? null}
- maxLevels={filters[0]?.maxLevels ?? DEFAULT_ROOT_FILTER.maxLevels}
- depthSummary={data.outgoingSummary}
- incomingSummary={data.incomingSummary}
- />
-
- )}
-
- )
-}
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneRegulationAssociationsPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneRegulationAssociationsPanel.tsx
new file mode 100644
index 00000000..c893c493
--- /dev/null
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneRegulationAssociationsPanel.tsx
@@ -0,0 +1,205 @@
+import React, { useMemo, useState } from 'react'
+import { Loader, Message } from 'semantic-ui-react'
+import { GeneRegulationFiltersPanel } from './GeneRegulationFiltersPanel'
+import { GeneRegulationGraph } from './GeneRegulationGraph'
+import { SelectedEdgesPanel } from './SelectedEdgesPanel'
+import { TraversalSummaryPanel } from './TraversalSummaryPanel'
+import { useGeneRegulationGraphQuery } from './useGeneRegulationGraphQuery'
+import { GraphQueryFilter, SelectedEdgeInfo } from './types'
+
+/** Props accepted by the gene regulation associations tab. */
+interface GeneRegulationAssociationsPanelProps {
+ /** Height assigned to the graph area. */
+ height?: number | string;
+ /** Width assigned to the graph area. */
+ width?: number | string;
+}
+
+const DEFAULT_BRAF_GRAPH_FILTER: GraphQueryFilter = {
+ rootNodeId: 'gene_braf',
+ threshold: 0.5,
+ traversalMode: 'both',
+ maxLevels: 3,
+}
+
+/**
+ * Renders the mock gene regulation associations experience for the selected biomarker molecule.
+ * @param props Component props.
+ * @param props.height Graph container height.
+ * @param props.width Graph container width.
+ * @returns The complete gene regulation associations tab.
+ */
+export const GeneRegulationAssociationsPanel = (props: GeneRegulationAssociationsPanelProps): JSX.Element => {
+ const {
+ height = 650,
+ width = '100%',
+ } = props
+ const [selectedEdges, setSelectedEdges] = useState([])
+ const [appliedFilters, setAppliedFilters] = useState([DEFAULT_BRAF_GRAPH_FILTER])
+ const [editableFilters, setEditableFilters] = useState([DEFAULT_BRAF_GRAPH_FILTER])
+
+ const nextExpansionFilter = useMemo(() => ({
+ threshold: editableFilters[0]?.threshold ?? DEFAULT_BRAF_GRAPH_FILTER.threshold,
+ traversalMode: editableFilters[0]?.traversalMode ?? DEFAULT_BRAF_GRAPH_FILTER.traversalMode,
+ maxLevels: editableFilters[0]?.maxLevels ?? DEFAULT_BRAF_GRAPH_FILTER.maxLevels,
+ }), [editableFilters])
+
+ const graphQueryParams = useMemo(() => ({
+ filters: appliedFilters,
+ }), [appliedFilters])
+
+ const { data, loading, error } = useGeneRegulationGraphQuery(graphQueryParams)
+ const hasPendingEditableFilterChanges = appliedFilters.length !== editableFilters.length || appliedFilters.some((filter, index) => {
+ const editableFilter = editableFilters[index]
+
+ if (!editableFilter) { return true }
+
+ return filter.rootNodeId !== editableFilter.rootNodeId ||
+ filter.threshold !== editableFilter.threshold ||
+ filter.traversalMode !== editableFilter.traversalMode ||
+ filter.maxLevels !== editableFilter.maxLevels
+ })
+
+ /**
+ * Applies a node expansion immediately and keeps the editable state in sync.
+ * @param filter Expansion filter created from the graph context menu.
+ */
+ const handleExpandGraphFromNode = (filter: GraphQueryFilter) => {
+ setSelectedEdges([])
+ setEditableFilters((prev) => {
+ if (prev.some((item) => item.rootNodeId === filter.rootNodeId)) {
+ setAppliedFilters(prev)
+ return prev
+ }
+
+ const nextEditableFilters = [
+ ...prev,
+ filter,
+ ]
+
+ setAppliedFilters(nextEditableFilters)
+
+ return nextEditableFilters
+ })
+ }
+
+ /**
+ * Stages the edition of a filter without applying the request yet.
+ * @param rootNodeId Root node associated with the filter being edited.
+ * @param partialFilter Partial values that should overwrite the current draft filter.
+ */
+ const handleEditableFilterUpdate = (rootNodeId: string, partialFilter: Partial) => {
+ setEditableFilters((prev) => prev.map((filter) => {
+ if (filter.rootNodeId !== rootNodeId) { return filter }
+
+ return {
+ ...filter,
+ ...partialFilter,
+ }
+ }))
+ }
+
+ /**
+ * Removes an expansion from the editable request payload.
+ * @param rootNodeId Root node identifier of the expansion to remove.
+ */
+ const handleEditableExpansionRemoval = (rootNodeId: string) => {
+ setEditableFilters((prev) => prev.filter((filter, index) => index === 0 || filter.rootNodeId !== rootNodeId))
+ }
+
+ /** Keeps only the root filter in the editable request payload. */
+ const handleEditableExpansionClear = () => {
+ setEditableFilters((prev) => {
+ if (prev.length === 0) { return [DEFAULT_BRAF_GRAPH_FILTER] }
+
+ return [prev[0]]
+ })
+ }
+
+ /** Restores the editable filters to the last filters applied to the graph. */
+ const handleEditableFiltersReset = () => {
+ setEditableFilters(appliedFilters.map((filter) => ({
+ ...filter,
+ })))
+ }
+
+ /** Applies the staged filters and triggers a new graph query. */
+ const handleEditableFiltersApply = () => {
+ setSelectedEdges([])
+ setAppliedFilters(editableFilters)
+ }
+
+ return (
+
+ {loading && (
+
+
+
+ )}
+
+ {error && (
+
+ Error fetching graph
+ {error}
+
+ )}
+
+ {!loading && !error && data && (
+
filter.rootNodeId)}
+ defaultExpansionFilter={nextExpansionFilter}
+ onExpandNode={handleExpandGraphFromNode}
+ />
+ )}
+
+ {!loading && !error && data && (
+ data.nodes.find((node) => node.id === nodeId)?.label ?? nodeId}
+ onUpdateFilter={handleEditableFilterUpdate}
+ onClearExpansions={handleEditableExpansionClear}
+ onRemoveFilter={handleEditableExpansionRemoval}
+ onResetFilters={handleEditableFiltersReset}
+ onApplyFilters={handleEditableFiltersApply}
+ />
+ )}
+
+ {!loading && !error && data && (
+
+ setSelectedEdges((prev) => prev.filter((edge) => edge.id !== edgeId))}
+ onClearAll={() => setSelectedEdges([])}
+ />
+
+ node.id === data.rootNodeId)?.label ?? null}
+ maxLevels={appliedFilters[0]?.maxLevels ?? DEFAULT_BRAF_GRAPH_FILTER.maxLevels}
+ depthSummary={data.outgoingSummary}
+ incomingSummary={data.incomingSummary}
+ />
+
+ )}
+
+ )
+}
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/ActiveGraphFiltersPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneRegulationFiltersPanel.tsx
similarity index 80%
rename from src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/ActiveGraphFiltersPanel.tsx
rename to src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneRegulationFiltersPanel.tsx
index 56fc2e3f..f95f2883 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/ActiveGraphFiltersPanel.tsx
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneRegulationFiltersPanel.tsx
@@ -14,27 +14,51 @@ const traversalOptions = [
{ key: 'both', value: 'both', text: traversalLabels.both },
]
-type Props = {
- filters: GraphQueryFilter[];
+/** Props accepted by the editable graph filters panel. */
+interface GeneRegulationFiltersPanelProps {
+ /** Draft filters currently being edited by the user. */
+ editableFilters: GraphQueryFilter[];
+ /** Indicates whether the draft differs from the last applied filters. */
hasPendingChanges: boolean;
+ /** Resolves the visible label for a node identifier. */
getNodeLabel: (nodeId: string) => string;
+ /** Updates one draft filter without querying the graph yet. */
onUpdateFilter: (rootNodeId: string, partialFilter: Partial) => void;
+ /** Clears every expansion while keeping the root filter. */
onClearExpansions: () => void;
+ /** Removes a specific expansion from the draft filter list. */
onRemoveFilter: (rootNodeId: string) => void;
+ /** Restores the draft state from the last applied filters. */
onResetFilters: () => void;
+ /** Applies the draft filter list to the graph query. */
onApplyFilters: () => void;
}
-export const ActiveGraphFiltersPanel = ({
- filters,
- hasPendingChanges,
- getNodeLabel,
- onUpdateFilter,
- onClearExpansions,
- onRemoveFilter,
- onResetFilters,
- onApplyFilters,
-}: Props) => {
+/**
+ * Renders the editable list of filters used to build the graph request payload.
+ * @param props Component props.
+ * @param props.editableFilters Draft filters currently being edited in the UI.
+ * @param props.hasPendingChanges Indicates whether the draft differs from the applied graph filters.
+ * @param props.getNodeLabel Resolves the visible label for a graph node identifier.
+ * @param props.onUpdateFilter Updates a single draft filter without querying the graph yet.
+ * @param props.onClearExpansions Removes every draft expansion except for the root filter.
+ * @param props.onRemoveFilter Removes a specific expansion from the draft request payload.
+ * @param props.onResetFilters Restores the draft state from the last applied filters.
+ * @param props.onApplyFilters Applies the current draft filters to the graph query.
+ * @returns The editable filters panel rendered below the graph.
+ */
+export const GeneRegulationFiltersPanel = (props: GeneRegulationFiltersPanelProps): JSX.Element => {
+ const {
+ editableFilters,
+ hasPendingChanges,
+ getNodeLabel,
+ onUpdateFilter,
+ onClearExpansions,
+ onRemoveFilter,
+ onResetFilters,
+ onApplyFilters,
+ } = props
+
return (
- {filters.map((filter, index) => (
+ {editableFilters.map((filter, index) => (
Clear expansions
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneNetworkGraph.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneRegulationGraph.tsx
similarity index 85%
rename from src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneNetworkGraph.tsx
rename to src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneRegulationGraph.tsx
index d4ad4e48..949a3d1f 100644
--- a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneNetworkGraph.tsx
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/GeneRegulationGraph.tsx
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'
import cytoscape, { Core, ElementDefinition } from 'cytoscape'
import { NODE_COLORS, REGULATION_COLORS } from './graphStyle'
import { LegendArrow, LegendDot } from './legend'
-import { FetchGeneGraphResponse, GraphQueryFilter, SelectedEdgeInfo, TraversalMode } from './types'
+import { FetchGeneRegulationGraphResponse, GraphQueryFilter, SelectedEdgeInfo, TraversalMode } from './types'
const MIN_ZOOM = 0.4
const MAX_ZOOM = 2
@@ -14,12 +14,22 @@ const traversalLabels: Record
= {
both: 'Both',
}
+/**
+ * Resolves the edge color according to the correlation sign.
+ * @param correlation Correlation value stored in the graph edge.
+ * @returns The color used to render the edge.
+ */
const getEdgeColor = (correlation: number) => {
if (correlation < 0) { return REGULATION_COLORS.down }
return REGULATION_COLORS.up
}
+/**
+ * Maps the edge correlation strength into the rendered edge opacity.
+ * @param correlation Correlation value stored in the graph edge.
+ * @returns The opacity used to render the edge.
+ */
const getEdgeOpacity = (correlation: number) => {
const abs = Math.abs(correlation)
@@ -30,6 +40,11 @@ const getEdgeOpacity = (correlation: number) => {
return 0.7
}
+/**
+ * Maps the edge correlation strength into the rendered edge width.
+ * @param correlation Correlation value stored in the graph edge.
+ * @returns The width used to render the edge.
+ */
const getEdgeWidth = (correlation: number) => {
const abs = Math.abs(correlation)
@@ -42,51 +57,93 @@ const getEdgeWidth = (correlation: number) => {
return 3
}
+/**
+ * Converts the edge correlation sign into the label shown by the UI.
+ * @param correlation Correlation value stored in the graph edge.
+ * @returns The semantic direction label shown in tooltips and panels.
+ */
const getDirectionLabel = (correlation: number): SelectedEdgeInfo['direction'] => {
if (correlation < 0) { return 'Down-regulation' }
return 'Up-regulation'
}
+/** Tooltip state used while hovering graph edges. */
type TooltipState = {
+ /** Whether the tooltip is currently visible. */
visible: boolean;
+ /** Horizontal position relative to the graph container. */
x: number;
+ /** Vertical position relative to the graph container. */
y: number;
+ /** Text shown inside the tooltip. */
content: string;
}
+/** Context menu state used when expanding a node from the graph. */
type ContextMenuState = {
+ /** Whether the expansion menu is currently visible. */
visible: boolean;
+ /** Horizontal position relative to the graph container. */
x: number;
+ /** Vertical position relative to the graph container. */
y: number;
+ /** Node identifier being expanded. */
nodeId: string;
+ /** Visible label of the node being expanded. */
nodeLabel: string;
+ /** Threshold draft shown in the expansion form. */
threshold: number;
+ /** Traversal mode draft shown in the expansion form. */
traversalMode: TraversalMode;
+ /** Max depth draft shown in the expansion form. */
maxLevels: number;
}
-type Props = {
- data: FetchGeneGraphResponse | null;
+/** Props accepted by the Cytoscape-based regulation graph. */
+interface GeneRegulationGraphProps {
+ /** Graph payload currently rendered in Cytoscape. */
+ data: FetchGeneRegulationGraphResponse | null;
+ /** Height assigned to the graph area. */
height?: number | string;
+ /** Width assigned to the graph area. */
width?: number | string;
+ /** Edges currently selected by the user. */
selectedEdges: SelectedEdgeInfo[];
+ /** Callback used to sync edge selections with the side panel. */
onSelectedEdgesChange: (edges: SelectedEdgeInfo[]) => void;
+ /** Node identifiers already expanded into the current request. */
expandedNodeIds: string[];
+ /** Default expansion values reused when opening the node context menu. */
defaultExpansionFilter: Omit;
+ /** Callback used to append a new node expansion to the query. */
onExpandNode: (filter: GraphQueryFilter) => void;
}
-export const GeneNetworkGraph = ({
- data,
- height = 650,
- width = '100%',
- selectedEdges,
- onSelectedEdgesChange,
- expandedNodeIds,
- defaultExpansionFilter,
- onExpandNode,
-}: Props): JSX.Element => {
+/**
+ * Renders the Cytoscape graph along with local zoom and expansion controls.
+ * @param props Component props.
+ * @param props.data Graph payload rendered in Cytoscape.
+ * @param props.height Graph container height.
+ * @param props.width Graph container width.
+ * @param props.selectedEdges Edges currently selected by the user.
+ * @param props.onSelectedEdgesChange Callback used to sync the selected edge panel.
+ * @param props.expandedNodeIds Node identifiers already expanded into the request payload.
+ * @param props.defaultExpansionFilter Filter values used to initialize the context-menu expansion form.
+ * @param props.onExpandNode Callback used to add a new expansion from the graph context menu.
+ * @returns The rendered graph with overlays for legend, zoom and node expansion.
+ */
+export const GeneRegulationGraph = (props: GeneRegulationGraphProps): JSX.Element => {
+ const {
+ data,
+ height = 650,
+ width = '100%',
+ selectedEdges,
+ onSelectedEdgesChange,
+ expandedNodeIds,
+ defaultExpansionFilter,
+ onExpandNode,
+ } = props
const containerRef = useRef(null)
const cyRef = useRef(null)
const defaultExpansionFilterRef = useRef(defaultExpansionFilter)
@@ -265,6 +322,7 @@ export const GeneNetworkGraph = ({
cy.center(rootNode)
}
+ /** Synchronizes the selected edge list shown in the side panel. */
const syncSelectedEdges = () => {
const picked = cy
.edges('.edge-picked')
@@ -377,7 +435,11 @@ export const GeneNetworkGraph = ({
}
}, [selectedEdges])
- const zoomGraph = (direction: 'in' | 'out') => {
+ /**
+ * Applies a centered zoom step from the overlay controls.
+ * @param direction Zoom direction requested by the user.
+ */
+ const handleGraphZoom = (direction: 'in' | 'out') => {
const cy = cyRef.current
if (!cy) { return }
@@ -423,7 +485,7 @@ export const GeneNetworkGraph = ({
>