@@ -132,21 +132,24 @@ import { selectListNodeById } from './list.js';
132132 const island = showIslands ? d . target . island : null ;
133133 return yScales [ island ] ( d . target . generation ) ;
134134 } )
135- . attr ( 'stroke' , '#888' )
136- . attr ( 'stroke-width' , 1.5 )
135+ . attr ( 'stroke' , d => ( selectedProgramId && ( d . source . id === selectedProgramId || d . target . id === selectedProgramId ) ) ? 'red' : '#888' )
136+ . attr ( 'stroke-width' , d => ( selectedProgramId && ( d . source . id === selectedProgramId || d . target . id === selectedProgramId ) ) ? 3 : 1.5 )
137137 . attr ( 'opacity' , 0.5 ) ;
138138 }
139139 const metricSelect = document . getElementById ( 'metric-select' ) ;
140140 metricSelect . addEventListener ( 'change' , function ( ) {
141141 updatePerformanceGraph ( allNodeData ) ;
142+ setTimeout ( updateEdgeHighlighting , 0 ) ; // ensure edges update after node positions change
142143 } ) ;
143144 const highlightSelect = document . getElementById ( 'highlight-select' ) ;
144145 highlightSelect . addEventListener ( 'change' , function ( ) {
145146 animatePerformanceGraphAttributes ( ) ;
147+ setTimeout ( updateEdgeHighlighting , 0 ) ; // ensure edges update after animation
146148 } ) ;
147149 document . getElementById ( 'tab-performance' ) . addEventListener ( 'click' , function ( ) {
148150 if ( typeof allNodeData !== 'undefined' && allNodeData . length ) {
149- updatePerformanceGraph ( allNodeData ) ;
151+ updatePerformanceGraph ( allNodeData , { autoZoom : true } ) ;
152+ setTimeout ( ( ) => { zoomPerformanceGraphToFit ( ) ; } , 0 ) ;
150153 }
151154 } ) ;
152155 // Show islands yes/no toggle event
@@ -163,7 +166,11 @@ import { selectListNodeById } from './list.js';
163166
164167 // Initial render
165168 if ( typeof allNodeData !== 'undefined' && allNodeData . length ) {
166- updatePerformanceGraph ( allNodeData ) ;
169+ updatePerformanceGraph ( allNodeData , { autoZoom : true } ) ;
170+ // --- Zoom to fit after initial render ---
171+ setTimeout ( ( ) => {
172+ zoomPerformanceGraphToFit ( ) ;
173+ } , 0 ) ;
167174 }
168175 } ) ;
169176} ) ( ) ;
@@ -226,6 +233,40 @@ let g = null;
226233let zoomBehavior = null ;
227234let lastTransform = null ;
228235
236+ function autoZoomPerformanceGraph ( nodes , x , yScales , islands , graphHeight , margin , undefinedBoxWidth , width , svg , g ) {
237+ // Compute bounding box for all nodes (including NaN box)
238+ let minX = Infinity , maxX = - Infinity , minY = Infinity , maxY = - Infinity ;
239+ // Valid nodes
240+ nodes . forEach ( n => {
241+ let cx , cy ;
242+ if ( n . metrics && typeof n . metrics [ getSelectedMetric ( ) ] === 'number' ) {
243+ cx = x ( n . metrics [ getSelectedMetric ( ) ] ) ;
244+ cy = yScales [ document . getElementById ( 'show-islands-toggle' ) ?. checked ? n . island : null ] ( n . generation ) ;
245+ } else if ( typeof n . _nanX === 'number' ) {
246+ cx = n . _nanX ;
247+ cy = yScales [ document . getElementById ( 'show-islands-toggle' ) ?. checked ? n . island : null ] ( n . generation ) ;
248+ }
249+ if ( typeof cx === 'number' && typeof cy === 'number' ) {
250+ minX = Math . min ( minX , cx ) ;
251+ maxX = Math . max ( maxX , cx ) ;
252+ minY = Math . min ( minY , cy ) ;
253+ maxY = Math . max ( maxY , cy ) ;
254+ }
255+ } ) ;
256+ // Include NaN box
257+ minX = Math . min ( minX , margin . left ) ;
258+ // Add some padding
259+ const padX = 60 , padY = 60 ;
260+ minX -= padX ; maxX += padX ; minY -= padY ; maxY += padY ;
261+ const svgW = + svg . attr ( 'width' ) ;
262+ const svgH = + svg . attr ( 'height' ) ;
263+ const scale = Math . min ( svgW / ( maxX - minX ) , svgH / ( maxY - minY ) , 1.5 ) ;
264+ const tx = svgW / 2 - scale * ( minX + ( maxX - minX ) / 2 ) ;
265+ const ty = svgH / 2 - scale * ( minY + ( maxY - minY ) / 2 ) ;
266+ const t = d3 . zoomIdentity . translate ( tx , ty ) . scale ( scale ) ;
267+ svg . transition ( ) . duration ( 500 ) . call ( zoomBehavior . transform , t ) ;
268+ }
269+
229270function updatePerformanceGraph ( nodes , options = { } ) {
230271 // Get or create SVG
231272 if ( ! svg ) {
@@ -274,6 +315,7 @@ function updatePerformanceGraph(nodes, options = {}) {
274315 } )
275316 . attr ( 'stroke-width' , 1.5 ) ;
276317 selectListNodeById ( null ) ;
318+ setTimeout ( updateEdgeHighlighting , 0 ) ; // ensure edges update after selectedProgramId is null
277319 }
278320 } ) ;
279321 // Sizing
@@ -421,25 +463,52 @@ function updatePerformanceGraph(nodes, options = {}) {
421463 // Data join for edges
422464 const nodeById = Object . fromEntries ( nodes . map ( n => [ n . id , n ] ) ) ;
423465 const edges = nodes . filter ( n => n . parent_id && nodeById [ n . parent_id ] ) . map ( n => ( { source : nodeById [ n . parent_id ] , target : n } ) ) ;
424- const edgeSel = g . selectAll ( 'line.performance-edge' )
425- . data ( edges , d => d . target . id ) ;
426- edgeSel . enter ( )
466+ // Remove all old edges before re-adding (fixes missing/incorrect edges after metric change)
467+ g . selectAll ( 'line.performance-edge' ) . remove ( ) ;
468+ // Helper to get x/y for a node (handles NaN and valid nodes)
469+ function getNodeXY ( node , x , yScales , showIslands , metric ) {
470+ // Returns [x, y] for a node, handling both valid and NaN nodes
471+ if ( ! node ) return [ null , null ] ;
472+ const y = yScales [ showIslands ? node . island : null ] ( node . generation ) ;
473+ if ( node . metrics && typeof node . metrics [ metric ] === 'number' ) {
474+ return [ x ( node . metrics [ metric ] ) , y ] ;
475+ } else if ( typeof node . _nanX === 'number' ) {
476+ return [ node . _nanX , y ] ;
477+ } else {
478+ // fallback: center of NaN box if _nanX not set
479+ // This should not happen, but fallback for safety
480+ return [ x . range ( ) [ 0 ] - 100 , y ] ;
481+ }
482+ }
483+ g . selectAll ( 'line.performance-edge' )
484+ . data ( edges , d => d . target . id )
485+ . enter ( )
427486 . append ( 'line' )
428487 . attr ( 'class' , 'performance-edge' )
429488 . attr ( 'stroke' , '#888' )
430489 . attr ( 'stroke-width' , 1.5 )
431490 . attr ( 'opacity' , 0.5 )
432- . attr ( 'x1' , d => ( typeof d . source . _nanX === 'number' ) ? d . source . _nanX : x ( d . source . metrics && typeof d . source . metrics [ metric ] === 'number' ? d . source . metrics [ metric ] : null ) )
433- . attr ( 'y1' , d => yScales [ showIslands ? d . source . island : null ] ( d . source . generation ) )
434- . attr ( 'x2' , d => ( typeof d . target . _nanX === 'number' ) ? d . target . _nanX : x ( d . target . metrics && typeof d . target . metrics [ metric ] === 'number' ? d . target . metrics [ metric ] : null ) )
435- . attr ( 'y2' , d => yScales [ showIslands ? d . target . island : null ] ( d . target . generation ) )
436- . merge ( edgeSel )
437- . transition ( ) . duration ( 500 )
438- . attr ( 'x1' , d => ( typeof d . source . _nanX === 'number' ) ? d . source . _nanX : x ( d . source . metrics && typeof d . source . metrics [ metric ] === 'number' ? d . source . metrics [ metric ] : null ) )
439- . attr ( 'y1' , d => yScales [ showIslands ? d . source . island : null ] ( d . source . generation ) )
440- . attr ( 'x2' , d => ( typeof d . target . _nanX === 'number' ) ? d . target . _nanX : x ( d . target . metrics && typeof d . target . metrics [ metric ] === 'number' ? d . target . metrics [ metric ] : null ) )
441- . attr ( 'y2' , d => yScales [ showIslands ? d . target . island : null ] ( d . target . generation ) ) ;
442- edgeSel . exit ( ) . transition ( ) . duration ( 300 ) . attr ( 'opacity' , 0 ) . remove ( ) ;
491+ . attr ( 'x1' , d => getNodeXY ( d . source , x , yScales , showIslands , metric ) [ 0 ] )
492+ . attr ( 'y1' , d => getNodeXY ( d . source , x , yScales , showIslands , metric ) [ 1 ] )
493+ . attr ( 'x2' , d => getNodeXY ( d . target , x , yScales , showIslands , metric ) [ 0 ] )
494+ . attr ( 'y2' , d => getNodeXY ( d . target , x , yScales , showIslands , metric ) [ 1 ] )
495+ . attr ( 'stroke' , d => {
496+ if ( selectedProgramId && ( d . source . id === selectedProgramId || d . target . id === selectedProgramId ) ) {
497+ return 'red' ;
498+ }
499+ return '#888' ;
500+ } )
501+ . attr ( 'stroke-width' , d => ( selectedProgramId && ( d . source . id === selectedProgramId || d . target . id === selectedProgramId ) ) ? 3 : 1.5 )
502+ . attr ( 'opacity' , d => ( selectedProgramId && ( d . source . id === selectedProgramId || d . target . id === selectedProgramId ) ) ? 0.9 : 0.5 ) ;
503+ // --- Ensure edge highlighting updates after node selection ---
504+ function updateEdgeHighlighting ( ) {
505+ g . selectAll ( 'line.performance-edge' )
506+ . attr ( 'stroke' , d => ( selectedProgramId && ( d . source . id === selectedProgramId || d . target . id === selectedProgramId ) ) ? 'red' : '#888' )
507+ . attr ( 'stroke-width' , d => ( selectedProgramId && ( d . source . id === selectedProgramId || d . target . id === selectedProgramId ) ) ? 3 : 1.5 )
508+ . attr ( 'opacity' , d => ( selectedProgramId && ( d . source . id === selectedProgramId || d . target . id === selectedProgramId ) ) ? 0.9 : 0.5 ) ;
509+ }
510+ updateEdgeHighlighting ( ) ;
511+
443512 // Data join for nodes
444513 const highlightFilter = document . getElementById ( 'highlight-select' ) . value ;
445514 const highlightNodes = getHighlightNodes ( nodes , highlightFilter , metric ) ;
@@ -491,6 +560,7 @@ function updatePerformanceGraph(nodes, options = {}) {
491560 showSidebarContent ( d , false ) ;
492561 showSidebar ( ) ;
493562 selectProgram ( selectedProgramId ) ;
563+ updateEdgeHighlighting ( ) ;
494564 } )
495565 . merge ( nodeSel )
496566 . transition ( ) . duration ( 500 )
@@ -557,6 +627,7 @@ function updatePerformanceGraph(nodes, options = {}) {
557627 showSidebarContent ( d , false ) ;
558628 showSidebar ( ) ;
559629 selectProgram ( selectedProgramId ) ;
630+ updateEdgeHighlighting ( ) ;
560631 } )
561632 . merge ( nanSel )
562633 . transition ( ) . duration ( 500 )
@@ -574,4 +645,44 @@ function updatePerformanceGraph(nodes, options = {}) {
574645 . classed ( 'node-selected' , selectedProgramId === d . id ) ;
575646 } ) ;
576647 nanSel . exit ( ) . transition ( ) . duration ( 300 ) . attr ( 'opacity' , 0 ) . remove ( ) ;
648+ // Auto-zoom to fit on initial render or when requested
649+ if ( options . autoZoom || ( ! lastTransform && nodes . length ) ) {
650+ autoZoomPerformanceGraph ( nodes , x , yScales , islands , graphHeight , margin , undefinedBoxWidth , width , svg , g ) ;
651+ }
652+ }
653+
654+ // --- Zoom-to-fit helper ---
655+ function zoomPerformanceGraphToFit ( ) {
656+ if ( ! svg || ! g ) return ;
657+ // Get all node positions (valid and NaN)
658+ const nodeCircles = g . selectAll ( 'circle.performance-node, circle.performance-nan' ) . nodes ( ) ;
659+ if ( ! nodeCircles . length ) return ;
660+ let minX = Infinity , minY = Infinity , maxX = - Infinity , maxY = - Infinity ;
661+ nodeCircles . forEach ( node => {
662+ const bbox = node . getBBox ( ) ;
663+ minX = Math . min ( minX , bbox . x ) ;
664+ minY = Math . min ( minY , bbox . y ) ;
665+ maxX = Math . max ( maxX , bbox . x + bbox . width ) ;
666+ maxY = Math . max ( maxY , bbox . y + bbox . height ) ;
667+ } ) ;
668+ // Also include the NaN box if present
669+ const nanBox = g . select ( 'rect.nan-box' ) . node ( ) ;
670+ if ( nanBox ) {
671+ const bbox = nanBox . getBBox ( ) ;
672+ minX = Math . min ( minX , bbox . x ) ;
673+ minY = Math . min ( minY , bbox . y ) ;
674+ maxX = Math . max ( maxX , bbox . x + bbox . width ) ;
675+ maxY = Math . max ( maxY , bbox . y + bbox . height ) ;
676+ }
677+ // Add some padding
678+ const pad = 32 ;
679+ minX -= pad ; minY -= pad ; maxX += pad ; maxY += pad ;
680+ const graphW = svg . attr ( 'width' ) ;
681+ const graphH = svg . attr ( 'height' ) ;
682+ const scale = Math . min ( graphW / ( maxX - minX ) , graphH / ( maxY - minY ) , 1.5 ) ;
683+ const tx = graphW / 2 - scale * ( minX + ( maxX - minX ) / 2 ) ;
684+ const ty = graphH / 2 - scale * ( minY + ( maxY - minY ) / 2 ) ;
685+ const t = d3 . zoomIdentity . translate ( tx , ty ) . scale ( scale ) ;
686+ svg . transition ( ) . duration ( 400 ) . call ( zoomBehavior . transform , t ) ;
687+ lastTransform = t ;
577688}
0 commit comments