event.preventDefault()}
+ >
+
+
+
+
+
+
+
+
+
+
+ Nodes:
+
+
+
+
+
+
+
+
+ Edges:
+
+
+
+
+
+ {tooltip.visible && (
+
+ {tooltip.content}
+
+ )}
+
+ {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/LevelBadge.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/LevelBadge.tsx
new file mode 100644
index 00000000..dd1652be
--- /dev/null
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/LevelBadge.tsx
@@ -0,0 +1,50 @@
+import React from 'react'
+
+const LEVEL_COLORS = {
+ level1: '#0ea5e9',
+ level2: '#22c55e',
+ level3: '#a855f7',
+ other: '#94a3b8',
+}
+
+interface LevelBadgeProps {
+ depth: number;
+ incoming?: boolean;
+}
+
+/**
+ * Displays the visual badge used to identify a traversal depth level.
+ * @param props Component props.
+ * @param props.depth Depth level represented by the badge.
+ * @param props.incoming Whether the badge should use the incoming traversal style.
+ * @returns The colored level badge rendered inline.
+ */
+export const LevelBadge = (props: LevelBadgeProps): JSX.Element => {
+ const {
+ depth,
+ incoming = false,
+ } = props
+ const color =
+ depth === 1
+ ? LEVEL_COLORS.level1
+ : depth === 2
+ ? LEVEL_COLORS.level2
+ : depth === 3
+ ? LEVEL_COLORS.level3
+ : LEVEL_COLORS.other
+
+ return (
+
+
+
Selected edges
+
+
+
+
+ {selectedEdges.length === 0
+ ? (
+
+ Click multiple edges to inspect each correlation value.
+
+ )
+ : (
+
+ {selectedEdges.map((edge) => (
+
+
+
+ {edge.source} -> {edge.target}
+
+
Direction: {edge.direction}
+
Correlation: {edge.correlation}
+
+
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/TraversalSummaryPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/TraversalSummaryPanel.tsx
new file mode 100644
index 00000000..9777b084
--- /dev/null
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/TraversalSummaryPanel.tsx
@@ -0,0 +1,139 @@
+import React from 'react'
+import { LevelBadge } from './LevelBadge'
+import { DepthSummaryItem, TraversalMode } from './types'
+
+interface TraversalSummaryPanelProps {
+ traversalMode: TraversalMode;
+ selectedRootNode: string | null;
+ maxLevels: number;
+ depthSummary: DepthSummaryItem[];
+ incomingSummary: DepthSummaryItem[];
+}
+
+/**
+ * Summarizes the traversal results for the active root graph filter.
+ * @param props Component props.
+ * @param props.traversalMode Active traversal mode applied to the root filter.
+ * @param props.selectedRootNode Visible label of the current root node.
+ * @param props.maxLevels Maximum depth configured for the root filter.
+ * @param props.depthSummary Outgoing traversal summary grouped by level.
+ * @param props.incomingSummary Incoming traversal summary grouped by level.
+ * @returns The traversal summary panel rendered below the graph.
+ */
+export const TraversalSummaryPanel = (props: TraversalSummaryPanelProps): JSX.Element => {
+ const {
+ traversalMode,
+ selectedRootNode,
+ maxLevels,
+ depthSummary,
+ incomingSummary,
+ } = props
+ return (
+
+
+ {traversalMode === 'outgoing'
+ ? 'Depth levels'
+ : traversalMode === 'incoming'
+ ? 'Upstream regulation levels'
+ : 'Relationship levels'}
+
+
+ {!selectedRootNode
+ ? (
+
+ Select a node to calculate levels.
+
+ )
+ : (
+
+
+ Root node: {selectedRootNode}
+
+
+
+ Search limit: {maxLevels} level{maxLevels > 1 ? 's' : ''}
+
+
+ {(traversalMode === 'outgoing' || traversalMode === 'both') && (
+
+
+ Regulates
+
+
+ {depthSummary.length === 0
+ ? (
+
+ No visible regulated nodes match the current filter.
+
+ )
+ : (
+ depthSummary.map((item) => (
+
+
+
+ Level {item.depth}
+
+
{item.nodes.join(', ')}
+
+ ))
+ )}
+
+ )}
+
+ {(traversalMode === 'incoming' || traversalMode === 'both') && (
+
+
+ Regulated by
+
+
+ {incomingSummary.length === 0
+ ? (
+
+ No visible regulators match the current filter.
+
+ )
+ : (
+ incomingSummary.map((item) => (
+
+
+
+ Level {item.depth}
+
+
{item.nodes.join(', ')}
+
+ ))
+ )}
+
+ )}
+
+ )}
+
+ )
+}
diff --git a/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/graphApi.ts b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/graphApi.ts
new file mode 100644
index 00000000..94bcbae5
--- /dev/null
+++ b/src/frontend/static/frontend/src/components/biomarkers/biomarker-details-modal/molecules/genes/gene-association-network/graphApi.ts
@@ -0,0 +1,319 @@
+import { MOCK_EDGES, MOCK_EXPANSION_EDGES_BY_ROOT, MOCK_NODES } from './mockNetworkData'
+import {
+ DepthSummaryItem,
+ FetchGeneRegulationGraphParams,
+ FetchGeneRegulationGraphResponse,
+ GraphEdge,
+ GraphQueryFilter,
+} from './types'
+
+const FALLBACK_FILTER: GraphQueryFilter = {
+ rootNodeId: 'gene_braf',
+ threshold: 0.5,
+ traversalMode: 'both',
+ maxLevels: 3,
+}
+
+type TraversalWalkResult = {
+ visitedNodes: Map