@@ -112,7 +112,9 @@ export default function App() {
112112 const [ layoutMode , setLayoutMode ] = useState < LayoutMode > ( "data_flow" ) ;
113113 const [ focusMode , setFocusMode ] = useState < FocusMode > ( "neighborhood" ) ;
114114 const [ edgeFilters , setEdgeFilters ] = useState < EdgeFilters > ( defaultEdgeFilters ) ;
115+ const [ collapsedScales , setCollapsedScales ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
115116 const [ pinnedFocus , setPinnedFocus ] = useState < FocusState | null > ( null ) ;
117+ const [ selectedEdge , setSelectedEdge ] = useState < GraphEdgeData | null > ( null ) ;
116118 const [ flowInstance , setFlowInstance ] = useState < ReactFlowInstance < Node < RuntimeGraphNodeData > , Edge < GraphEdgeData > > | null > ( null ) ;
117119 const [ nodes , setNodes , onNodesChange ] = useNodesState < Node < RuntimeGraphNodeData > > ( [ ] ) ;
118120 const [ edges , setEdges , onEdgesChange ] = useEdgesState < Edge < GraphEdgeData > > ( [ ] ) ;
@@ -126,7 +128,13 @@ export default function App() {
126128 const warningItems = useMemo ( ( ) => deriveValidationWarnings ( graph , requiredInputPortIds , incomingByPort ) , [ graph , incomingByPort , requiredInputPortIds ] ) ;
127129 const actionableWarningItems = useMemo ( ( ) => warningItems . filter ( ( item ) => item . severity !== "info" ) , [ warningItems ] ) ;
128130 const searchResults = useMemo ( ( ) => deriveSearchResults ( graph , searchQuery ) , [ graph , searchQuery ] ) ;
129- const visibleEdgeData = useMemo ( ( ) => graph . edges . filter ( ( edge ) => edgeMatchesFilters ( edge , edgeFilters ) ) , [ edgeFilters , graph . edges ] ) ;
131+ const visibleNodeData = useMemo ( ( ) => graph . nodes . filter ( ( node ) => ! collapsedScales . has ( node . scale ) ) , [ collapsedScales , graph . nodes ] ) ;
132+ const visibleNodeIds = useMemo ( ( ) => new Set ( visibleNodeData . map ( ( node ) => node . id ) ) , [ visibleNodeData ] ) ;
133+ const visibleEdgeData = useMemo ( ( ) => graph . edges . filter ( ( edge ) => (
134+ edgeMatchesFilters ( edge , edgeFilters ) &&
135+ visibleNodeIds . has ( edge . source ) &&
136+ visibleNodeIds . has ( edge . target )
137+ ) ) , [ edgeFilters , graph . edges , visibleNodeIds ] ) ;
130138 const hoverHighlight = useMemo ( ( ) => deriveHighlight ( graph , activePort ) , [ activePort , graph ] ) ;
131139 const traversalFocus = useMemo (
132140 ( ) => deriveFocus ( graph , selected ?. id ?? null , activePort , focusMode ) ,
@@ -135,7 +143,7 @@ export default function App() {
135143 const focus = useMemo ( ( ) => pinnedFocus ?. active ? pinnedFocus : traversalFocus , [ pinnedFocus , traversalFocus ] ) ;
136144
137145 useEffect ( ( ) => {
138- const nextNodes = graph . nodes . map ( ( node ) => ( {
146+ const nextNodes = visibleNodeData . map ( ( node ) => ( {
139147 id : node . id ,
140148 type : "model" ,
141149 position : { x : 0 , y : 0 } ,
@@ -155,7 +163,7 @@ export default function App() {
155163 setNodes ( layouted ) ;
156164 setEdges ( nextEdges ) ;
157165 } ) ;
158- } , [ graph , layoutMode , requiredInputPortIds , setEdges , setNodes , visibleEdgeData ] ) ;
166+ } , [ graph . cycleNodes , layoutMode , requiredInputPortIds , setEdges , setNodes , visibleEdgeData , visibleNodeData ] ) ;
159167
160168 useEffect ( ( ) => {
161169 const focusEdges = focus . active ? focus . edges : new Set < string > ( ) ;
@@ -192,8 +200,15 @@ export default function App() {
192200
193201 const focusNode = useCallback ( ( node : GraphNodeData , port ?: GraphPort | null ) => {
194202 setPinnedFocus ( null ) ;
203+ setSelectedEdge ( null ) ;
195204 setSelected ( node ) ;
196205 setActivePort ( port ?? null ) ;
206+ setCollapsedScales ( ( current ) => {
207+ if ( ! current . has ( node . scale ) ) return current ;
208+ const next = new Set ( current ) ;
209+ next . delete ( node . scale ) ;
210+ return next ;
211+ } ) ;
197212 const renderedNode = nodes . find ( ( item ) => item . id === node . id ) ;
198213 if ( renderedNode && flowInstance ) {
199214 flowInstance . setCenter ( renderedNode . position . x + 156 , renderedNode . position . y + 90 , { zoom : 0.85 , duration : 520 } ) ;
@@ -210,6 +225,21 @@ export default function App() {
210225 setEdgeFilters ( ( current ) => ( { ...current , [ key ] : ! current [ key ] } ) ) ;
211226 } , [ ] ) ;
212227
228+ const toggleScale = useCallback ( ( scale : string ) => {
229+ setSelected ( null ) ;
230+ setSelectedEdge ( null ) ;
231+ setActivePort ( null ) ;
232+ setPinnedFocus ( null ) ;
233+ setCollapsedScales ( ( current ) => {
234+ const next = new Set ( current ) ;
235+ if ( next . has ( scale ) ) next . delete ( scale ) ;
236+ else next . add ( scale ) ;
237+ return next ;
238+ } ) ;
239+ } , [ ] ) ;
240+
241+ const expandAllScales = useCallback ( ( ) => setCollapsedScales ( new Set ( ) ) , [ ] ) ;
242+
213243 const focusWarning = useCallback ( ( warning : ValidationWarning ) => {
214244 if ( warning . portIds ?. length ) {
215245 const nextFocus = emptyFocusState ( ) ;
@@ -224,6 +254,7 @@ export default function App() {
224254 const first = portById . get ( warning . portIds [ 0 ] ) ;
225255 if ( first ) {
226256 setSelected ( null ) ;
257+ setSelectedEdge ( null ) ;
227258 setActivePort ( null ) ;
228259 if ( flowInstance && warning . nodeIds && warning . nodeIds . length > 1 ) {
229260 flowInstance . fitView ( {
@@ -243,6 +274,7 @@ export default function App() {
243274 }
244275
245276 setPinnedFocus ( null ) ;
277+ setSelectedEdge ( null ) ;
246278 if ( warning . edgeId ) {
247279 const edge = graph . edges . find ( ( item ) => item . id === warning . edgeId ) ;
248280 if ( edge ) focusEdge ( edge ) ;
@@ -305,7 +337,7 @@ export default function App() {
305337 </ div >
306338
307339 < div className = "metrics" >
308- < span > { graph . nodes . length } models</ span >
340+ < span > { visibleNodeData . length } / { graph . nodes . length } models</ span >
309341 < span > { visibleEdgeData . length } /{ graph . edges . length } links</ span >
310342 { requiredInputs . length > 0 && (
311343 < button
@@ -348,6 +380,7 @@ export default function App() {
348380 </ div >
349381
350382 < RelationshipLegend filters = { edgeFilters } onToggle = { toggleEdgeFilter } />
383+ < ScaleControls scales = { graph . scales } collapsedScales = { collapsedScales } onToggle = { toggleScale } onExpandAll = { expandAllScales } />
351384
352385 { showRequiredPanel && (
353386 < FloatingPanel className = "required-panel" title = "Required Initializations" subtitle = { `${ requiredInputs . length } inputs` } onClose = { ( ) => setShowRequiredPanel ( false ) } >
@@ -371,7 +404,16 @@ export default function App() {
371404 onConnect = { onConnect }
372405 onInit = { setFlowInstance }
373406 onPaneClick = { ( ) => setShowSearchResults ( false ) }
407+ onEdgeClick = { ( _ , edge ) => {
408+ if ( edge . data ) {
409+ setSelectedEdge ( edge . data ) ;
410+ setSelected ( null ) ;
411+ setActivePort ( null ) ;
412+ setPinnedFocus ( null ) ;
413+ }
414+ } }
374415 onNodeClick = { ( _ , node ) => {
416+ setSelectedEdge ( null ) ;
375417 setSelected ( node . data ) ;
376418 } }
377419 fitView
@@ -392,6 +434,7 @@ export default function App() {
392434 </ header >
393435 < InspectorDetails
394436 selected = { selected }
437+ selectedEdge = { selectedEdge }
395438 activePort = { activePort }
396439 requiredInputPortIds = { requiredInputPortIds }
397440 incomingEdges = { activePort ? incomingByPort . get ( activePort . id ) ?? [ ] : [ ] }
@@ -421,6 +464,36 @@ function RelationshipLegend({ filters, onToggle }: { filters: EdgeFilters; onTog
421464 ) ;
422465}
423466
467+ function ScaleControls ( {
468+ scales,
469+ collapsedScales,
470+ onToggle,
471+ onExpandAll,
472+ } : {
473+ scales : string [ ] ;
474+ collapsedScales : Set < string > ;
475+ onToggle : ( scale : string ) => void ;
476+ onExpandAll : ( ) => void ;
477+ } ) {
478+ return (
479+ < div className = "scale-controls" >
480+ < div className = "legend-title" > < Network size = { 13 } /> Scales</ div >
481+ < div className = "scale-list" >
482+ { scales . map ( ( scale ) => {
483+ const collapsed = collapsedScales . has ( scale ) ;
484+ return (
485+ < button key = { scale } className = { collapsed ? "collapsed" : "active" } onClick = { ( ) => onToggle ( scale ) } >
486+ < span > { scale } </ span >
487+ < small > { collapsed ? "collapsed" : "visible" } </ small >
488+ </ button >
489+ ) ;
490+ } ) }
491+ </ div >
492+ { collapsedScales . size > 0 && < button className = "scale-reset" onClick = { onExpandAll } > Show all scales</ button > }
493+ </ div >
494+ ) ;
495+ }
496+
424497function FloatingPanel ( { className, title, subtitle, onClose, children } : { className : string ; title : string ; subtitle : string ; onClose : ( ) => void ; children : ReactNode } ) {
425498 return (
426499 < div className = { `floating-panel ${ className } ` } >
@@ -494,6 +567,7 @@ function WarningList({
494567
495568function InspectorDetails ( {
496569 selected,
570+ selectedEdge,
497571 activePort,
498572 requiredInputPortIds,
499573 incomingEdges,
@@ -503,6 +577,7 @@ function InspectorDetails({
503577 onFocusEdge,
504578} : {
505579 selected : GraphNodeData | null ;
580+ selectedEdge : GraphEdgeData | null ;
506581 activePort : GraphPort | null ;
507582 requiredInputPortIds : Set < string > ;
508583 incomingEdges : GraphEdgeData [ ] ;
@@ -513,6 +588,9 @@ function InspectorDetails({
513588} ) {
514589 return (
515590 < >
591+ { selectedEdge && (
592+ < EdgeDetails edge = { selectedEdge } nodeById = { nodeById } portById = { portById } />
593+ ) }
516594 { selected ? (
517595 < div className = "details" >
518596 < Row label = "Process" value = { selected . process } />
@@ -528,9 +606,9 @@ function InspectorDetails({
528606 < div className = "edit-suggestion" key = { port . id } > < ScissorsLineDashed size = { 14 } /> { port . name } uses previous timestep</ div >
529607 ) ) }
530608 </ div >
531- ) : (
609+ ) : ! selectedEdge ? (
532610 < div className = "empty-state" > Select a model node.</ div >
533- ) }
611+ ) : null }
534612
535613 < h3 > Variable Provenance</ h3 >
536614 { activePort ? (
@@ -554,6 +632,38 @@ function InspectorDetails({
554632 ) ;
555633}
556634
635+ function EdgeDetails ( {
636+ edge,
637+ nodeById,
638+ portById,
639+ } : {
640+ edge : GraphEdgeData ;
641+ nodeById : Map < string , GraphNodeData > ;
642+ portById : Map < string , { node : GraphNodeData ; port : GraphPort } > ;
643+ } ) {
644+ const source = nodeById . get ( edge . source ) ;
645+ const target = nodeById . get ( edge . target ) ;
646+ const sourcePort = edge . sourcePort ? portById . get ( edge . sourcePort ) ?. port : null ;
647+ const targetPort = edge . targetPort ? portById . get ( edge . targetPort ) ?. port : null ;
648+ return (
649+ < div className = "edge-detail-card" >
650+ < div className = "variable-card-title" >
651+ < span > { edgeKindLabel ( edge ) } </ span >
652+ < small > { edge . scaleRelation } </ small >
653+ </ div >
654+ < Row label = "Source" value = { source ? `${ source . scale } .${ source . process } ` : edge . source } />
655+ < Row label = "Source var" value = { sourcePort ?. name ?? edge . sourceVariable ?? "model call" } />
656+ < Row label = "Target" value = { target ? `${ target . scale } .${ target . process } ` : edge . target } />
657+ < Row label = "Target var" value = { targetPort ?. name ?? edge . targetVariable ?? "model call" } />
658+ < Row label = "Kind" value = { edge . kind } />
659+ < Row label = "Label" value = { edge . label || "none" } />
660+ { edge . diagnostics . length > 0 ? edge . diagnostics . map ( ( item ) => (
661+ < div className = "diagnostic" key = { item } > { item } </ div >
662+ ) ) : < div className = "empty-state compact" > No edge diagnostics.</ div > }
663+ </ div >
664+ ) ;
665+ }
666+
557667function EdgeList ( {
558668 title,
559669 edges,
0 commit comments