@@ -11,7 +11,7 @@ import { selectListNodeById } from './list.js';
1111 if ( ! toggleDiv ) {
1212 toggleDiv = document . createElement ( 'div' ) ;
1313 toggleDiv . id = 'perf-island-toggle' ;
14- toggleDiv . style = 'display:flex;align-items:center;gap:0.7em;' ;
14+ toggleDiv . style = 'display:flex;align-items:center;gap:0.7em;margin-left:3em; ' ;
1515 toggleDiv . innerHTML = `
1616 <label class="toggle-switch">
1717 <input type="checkbox" id="show-islands-toggle">
@@ -166,15 +166,48 @@ import { selectListNodeById } from './list.js';
166166
167167 // Initial render
168168 if ( typeof allNodeData !== 'undefined' && allNodeData . length ) {
169- updatePerformanceGraph ( allNodeData , { autoZoom : true } ) ;
170- // --- Zoom to fit after initial render ---
169+ updatePerformanceGraph ( allNodeData ) ;
170+ // Zoom to fit after initial render
171171 setTimeout ( ( ) => {
172172 zoomPerformanceGraphToFit ( ) ;
173173 } , 0 ) ;
174174 }
175175 } ) ;
176176} ) ( ) ;
177177
178+ // Recenter Button Overlay
179+ function showRecenterButton ( onClick ) {
180+ let btn = document . getElementById ( 'performance-recenter-btn' ) ;
181+ if ( ! btn ) {
182+ btn = document . createElement ( 'button' ) ;
183+ btn . id = 'performance-recenter-btn' ;
184+ btn . textContent = 'Recenter' ;
185+ btn . style . position = 'absolute' ;
186+ btn . style . left = '50%' ;
187+ btn . style . top = '50%' ;
188+ btn . style . transform = 'translate(-50%, -50%)' ;
189+ btn . style . zIndex = 1000 ;
190+ btn . style . fontSize = '2em' ;
191+ btn . style . padding = '0.5em 1.5em' ;
192+ btn . style . background = '#fff' ;
193+ btn . style . border = '2px solid #2196f3' ;
194+ btn . style . borderRadius = '12px' ;
195+ btn . style . boxShadow = '0 2px 16px #0002' ;
196+ btn . style . cursor = 'pointer' ;
197+ btn . style . display = 'block' ;
198+ document . getElementById ( 'view-performance' ) . appendChild ( btn ) ;
199+ }
200+ btn . style . display = 'block' ;
201+ btn . onclick = function ( ) {
202+ btn . style . display = 'none' ;
203+ if ( typeof onClick === 'function' ) onClick ( ) ;
204+ } ;
205+ }
206+ function hideRecenterButton ( ) {
207+ const btn = document . getElementById ( 'performance-recenter-btn' ) ;
208+ if ( btn ) btn . style . display = 'none' ;
209+ }
210+
178211// Select a node by ID and update graph and sidebar
179212export function selectPerformanceNodeById ( id , opts = { } ) {
180213 setSelectedProgramId ( id ) ;
@@ -290,6 +323,35 @@ function updatePerformanceGraph(nodes, options = {}) {
290323 . on ( 'zoom' , function ( event ) {
291324 g . attr ( 'transform' , event . transform ) ;
292325 lastTransform = event . transform ;
326+ // Check if all content is out of view
327+ setTimeout ( ( ) => {
328+ try {
329+ const svgRect = svg . node ( ) . getBoundingClientRect ( ) ;
330+ const allCircles = g . selectAll ( 'circle' ) . nodes ( ) ;
331+ if ( allCircles . length === 0 ) { hideRecenterButton ( ) ; return ; }
332+ let anyVisible = false ;
333+ for ( const c of allCircles ) {
334+ const bbox = c . getBoundingClientRect ( ) ;
335+ if (
336+ bbox . right > svgRect . left &&
337+ bbox . left < svgRect . right &&
338+ bbox . bottom > svgRect . top &&
339+ bbox . top < svgRect . bottom
340+ ) {
341+ anyVisible = true ;
342+ break ;
343+ }
344+ }
345+ if ( ! anyVisible ) {
346+ showRecenterButton ( ( ) => {
347+ // Reset zoom/pan
348+ svg . transition ( ) . duration ( 400 ) . call ( zoomBehavior . transform , d3 . zoomIdentity ) ;
349+ } ) ;
350+ } else {
351+ hideRecenterButton ( ) ;
352+ }
353+ } catch { }
354+ } , 0 ) ;
293355 } ) ;
294356 svg . call ( zoomBehavior ) ;
295357 }
@@ -500,7 +562,7 @@ function updatePerformanceGraph(nodes, options = {}) {
500562 } )
501563 . attr ( 'stroke-width' , d => ( selectedProgramId && ( d . source . id === selectedProgramId || d . target . id === selectedProgramId ) ) ? 3 : 1.5 )
502564 . attr ( 'opacity' , d => ( selectedProgramId && ( d . source . id === selectedProgramId || d . target . id === selectedProgramId ) ) ? 0.9 : 0.5 ) ;
503- // --- Ensure edge highlighting updates after node selection ---
565+ // Ensure edge highlighting updates after node selection
504566 function updateEdgeHighlighting ( ) {
505567 g . selectAll ( 'line.performance-edge' )
506568 . attr ( 'stroke' , d => ( selectedProgramId && ( d . source . id === selectedProgramId || d . target . id === selectedProgramId ) ) ? 'red' : '#888' )
@@ -651,7 +713,7 @@ function updatePerformanceGraph(nodes, options = {}) {
651713 }
652714}
653715
654- // --- Zoom-to-fit helper ---
716+ // Zoom-to-fit helper
655717function zoomPerformanceGraphToFit ( ) {
656718 if ( ! svg || ! g ) return ;
657719 // Get all node positions (valid and NaN)
@@ -679,9 +741,14 @@ function zoomPerformanceGraphToFit() {
679741 minX -= pad ; minY -= pad ; maxX += pad ; maxY += pad ;
680742 const graphW = svg . attr ( 'width' ) ;
681743 const graphH = svg . attr ( 'height' ) ;
744+ // Bias the center to the left so the left edge is always visible
745+ // Instead of centering on the middle, center at 35% from the left
746+ const centerFrac = 0.35 ;
747+ const centerX = minX + ( maxX - minX ) * centerFrac ;
748+ const centerY = minY + ( maxY - minY ) / 2 ;
682749 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 ) ;
750+ const tx = graphW / 2 - scale * centerX ;
751+ const ty = graphH / 2 - scale * centerY ;
685752 const t = d3 . zoomIdentity . translate ( tx , ty ) . scale ( scale ) ;
686753 svg . transition ( ) . duration ( 400 ) . call ( zoomBehavior . transform , t ) ;
687754 lastTransform = t ;
0 commit comments