@@ -82,7 +82,7 @@ import { selectListNodeById } from './list.js';
8282 g . selectAll ( 'circle' )
8383 . filter ( function ( d ) { return undefinedNodes . includes ( d ) ; } )
8484 . transition ( ) . duration ( 400 )
85- . attr ( 'cx' , margin . left + undefinedBoxWidth / 2 )
85+ . attr ( 'cx' , d => d . _nanX || ( margin . left + undefinedBoxWidth / 2 ) )
8686 . attr ( 'cy' , d => yScales [ showIslands ? d . island : null ] ( d . generation ) )
8787 . attr ( 'r' , d => getNodeRadius ( d ) )
8888 . attr ( 'fill' , d => getNodeColor ( d ) )
@@ -319,11 +319,11 @@ function updatePerformanceGraph(nodes, options = {}) {
319319 . attr ( 'class' , 'axis' )
320320 . attr ( 'transform' , `translate(${ margin . left + graphXOffset } ,0)` )
321321 . call ( d3 . axisLeft ( yScales [ island ] ) . ticks ( Math . min ( 12 , genCount ) ) ) ;
322- // Y axis label
322+ // Y axis label (always at start of main graph)
323323 g . append ( 'text' )
324324 . attr ( 'class' , 'axis-label' )
325325 . attr ( 'transform' , `rotate(-90)` ) // vertical
326- . attr ( 'y' , margin . left + 8 )
326+ . attr ( 'y' , margin . left + graphXOffset + 8 )
327327 . attr ( 'x' , - ( margin . top + i * graphHeight + ( graphHeight - margin . top - margin . bottom ) / 2 ) )
328328 . attr ( 'dy' , '-2.2em' )
329329 . attr ( 'text-anchor' , 'middle' )
@@ -367,26 +367,56 @@ function updatePerformanceGraph(nodes, options = {}) {
367367 . text ( metric ) ;
368368 // NaN box
369369 if ( undefinedNodes . length ) {
370+ // Group NaN nodes by (generation, island)
371+ const nanGroups = { } ;
372+ undefinedNodes . forEach ( n => {
373+ const key = `${ n . generation } |${ showIslands ? n . island : '' } ` ;
374+ if ( ! nanGroups [ key ] ) nanGroups [ key ] = [ ] ;
375+ nanGroups [ key ] . push ( n ) ;
376+ } ) ;
377+ // Find max group size
378+ const maxGroupSize = Math . max ( ...Object . values ( nanGroups ) . map ( g => g . length ) ) ;
379+ // Box width should be based on the full intended spread, not the reduced spread
380+ const spreadWidth = Math . max ( 38 , 24 * maxGroupSize ) ;
381+ undefinedBoxWidth = spreadWidth / 2 + 32 ; // 16px padding on each side
382+ // Add a fixed offset so the NaN box is further left of the main graph
383+ const nanBoxGap = 64 ; // px gap between NaN box and main graph
384+ const nanBoxRight = margin . left + graphXOffset - nanBoxGap ;
385+ const nanBoxLeft = nanBoxRight - undefinedBoxWidth ;
370386 const boxTop = margin . top ;
371387 const boxBottom = showIslands ? ( margin . top + islands . length * graphHeight - margin . bottom ) : ( margin . top + graphHeight - margin . bottom ) ;
372388 g . append ( 'text' )
373389 . attr ( 'class' , 'nan-label' )
374- . attr ( 'x' , margin . left + undefinedBoxWidth / 2 )
390+ . attr ( 'x' , nanBoxLeft + undefinedBoxWidth / 2 )
375391 . attr ( 'y' , boxTop - 10 )
376392 . attr ( 'text-anchor' , 'middle' )
377393 . attr ( 'font-size' , '0.92em' )
378394 . attr ( 'fill' , '#888' )
379395 . text ( 'NaN' ) ;
380396 g . append ( 'rect' )
381397 . attr ( 'class' , 'nan-box' )
382- . attr ( 'x' , margin . left )
398+ . attr ( 'x' , nanBoxLeft )
383399 . attr ( 'y' , boxTop )
384400 . attr ( 'width' , undefinedBoxWidth )
385401 . attr ( 'height' , boxBottom - boxTop )
386402 . attr ( 'fill' , 'none' )
387403 . attr ( 'stroke' , '#bbb' )
388404 . attr ( 'stroke-width' , 1.5 )
389405 . attr ( 'rx' , 12 ) ;
406+ // Assign x offset for each NaN node (spread only in the center half of the box)
407+ undefinedNodes . forEach ( n => {
408+ const key = `${ n . generation } |${ showIslands ? n . island : '' } ` ;
409+ const group = nanGroups [ key ] ;
410+ if ( ! group ) return ;
411+ if ( group . length === 1 ) {
412+ n . _nanX = nanBoxLeft + undefinedBoxWidth / 2 ;
413+ } else {
414+ const idx = group . indexOf ( n ) ;
415+ const innerSpread = spreadWidth / 2 ; // only use half the box for node spread
416+ const innerStart = nanBoxLeft + ( undefinedBoxWidth - innerSpread ) / 2 ;
417+ n . _nanX = innerStart + innerSpread * ( idx + 0.5 ) / group . length ;
418+ }
419+ } ) ;
390420 }
391421 // Data join for edges
392422 const nodeById = Object . fromEntries ( nodes . map ( n => [ n . id , n ] ) ) ;
@@ -399,15 +429,15 @@ function updatePerformanceGraph(nodes, options = {}) {
399429 . attr ( 'stroke' , '#888' )
400430 . attr ( 'stroke-width' , 1.5 )
401431 . attr ( 'opacity' , 0.5 )
402- . attr ( 'x1' , d => x ( d . source . metrics && typeof d . source . metrics [ metric ] === 'number' ? d . source . metrics [ metric ] : null ) || ( margin . left + undefinedBoxWidth / 2 ) )
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 ) )
403433 . attr ( 'y1' , d => yScales [ showIslands ? d . source . island : null ] ( d . source . generation ) )
404- . attr ( 'x2' , d => x ( d . target . metrics && typeof d . target . metrics [ metric ] === 'number' ? d . target . metrics [ metric ] : null ) || ( margin . left + undefinedBoxWidth / 2 ) )
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 ) )
405435 . attr ( 'y2' , d => yScales [ showIslands ? d . target . island : null ] ( d . target . generation ) )
406436 . merge ( edgeSel )
407437 . transition ( ) . duration ( 500 )
408- . attr ( 'x1' , d => x ( d . source . metrics && typeof d . source . metrics [ metric ] === 'number' ? d . source . metrics [ metric ] : null ) || ( margin . left + undefinedBoxWidth / 2 ) )
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 ) )
409439 . attr ( 'y1' , d => yScales [ showIslands ? d . source . island : null ] ( d . source . generation ) )
410- . attr ( 'x2' , d => x ( d . target . metrics && typeof d . target . metrics [ metric ] === 'number' ? d . target . metrics [ metric ] : null ) || ( margin . left + undefinedBoxWidth / 2 ) )
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 ) )
411441 . attr ( 'y2' , d => yScales [ showIslands ? d . target . island : null ] ( d . target . generation ) ) ;
412442 edgeSel . exit ( ) . transition ( ) . duration ( 300 ) . attr ( 'opacity' , 0 ) . remove ( ) ;
413443 // Data join for nodes
@@ -485,7 +515,7 @@ function updatePerformanceGraph(nodes, options = {}) {
485515 nanSel . enter ( )
486516 . append ( 'circle' )
487517 . attr ( 'class' , 'performance-nan' )
488- . attr ( 'cx' , margin . left + undefinedBoxWidth / 2 )
518+ . attr ( 'cx' , d => d . _nanX )
489519 . attr ( 'cy' , d => yScales [ showIslands ? d . island : null ] ( d . generation ) )
490520 . attr ( 'r' , d => getNodeRadius ( d ) )
491521 . attr ( 'fill' , d => getNodeColor ( d ) )
@@ -530,7 +560,7 @@ function updatePerformanceGraph(nodes, options = {}) {
530560 } )
531561 . merge ( nanSel )
532562 . transition ( ) . duration ( 500 )
533- . attr ( 'cx' , margin . left + undefinedBoxWidth / 2 )
563+ . attr ( 'cx' , d => d . _nanX )
534564 . attr ( 'cy' , d => yScales [ showIslands ? d . island : null ] ( d . generation ) )
535565 . attr ( 'r' , d => getNodeRadius ( d ) )
536566 . attr ( 'fill' , d => getNodeColor ( d ) )
0 commit comments