Skip to content

Commit 675ae97

Browse files
committed
UI: When user panned away and the graph is seemingly empty, show a Recenter button
1 parent 6957cd8 commit 675ae97

File tree

2 files changed

+137
-7
lines changed

2 files changed

+137
-7
lines changed

scripts/static/js/graph.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,40 @@ Object.defineProperty(window, 'g', {
121121
set: function(val) { g = val; }
122122
});
123123

124+
// Recenter Button Overlay
125+
function showRecenterButton(onClick) {
126+
let btn = document.getElementById('graph-recenter-btn');
127+
if (!btn) {
128+
btn = document.createElement('button');
129+
btn.id = 'graph-recenter-btn';
130+
btn.textContent = 'Recenter';
131+
btn.style.position = 'absolute';
132+
btn.style.left = '50%';
133+
btn.style.top = '50%';
134+
btn.style.transform = 'translate(-50%, -50%)';
135+
btn.style.zIndex = 1000;
136+
btn.style.fontSize = '2em';
137+
btn.style.padding = '0.5em 1.5em';
138+
btn.style.background = '#fff';
139+
btn.style.border = '2px solid #2196f3';
140+
btn.style.borderRadius = '12px';
141+
btn.style.boxShadow = '0 2px 16px #0002';
142+
btn.style.cursor = 'pointer';
143+
btn.style.display = 'block';
144+
document.getElementById('graph').appendChild(btn);
145+
}
146+
btn.style.display = 'block';
147+
btn.onclick = function() {
148+
btn.style.display = 'none';
149+
if (typeof onClick === 'function') onClick();
150+
};
151+
}
152+
153+
function hideRecenterButton() {
154+
const btn = document.getElementById('graph-recenter-btn');
155+
if (btn) btn.style.display = 'none';
156+
}
157+
124158
function ensureGraphSvg() {
125159
// Get latest width/height from state.js
126160
let svgEl = d3.select('#graph').select('svg');
@@ -264,6 +298,35 @@ function renderGraph(data, options = {}) {
264298
.scaleExtent([0.2, 10])
265299
.on('zoom', function(event) {
266300
g.attr('transform', event.transform);
301+
// Check if all content is out of view
302+
setTimeout(() => {
303+
try {
304+
const svgRect = svg.node().getBoundingClientRect();
305+
const allCircles = g.selectAll('circle').nodes();
306+
if (allCircles.length === 0) { hideRecenterButton(); return; }
307+
let anyVisible = false;
308+
for (const c of allCircles) {
309+
const bbox = c.getBoundingClientRect();
310+
if (
311+
bbox.right > svgRect.left &&
312+
bbox.left < svgRect.right &&
313+
bbox.bottom > svgRect.top &&
314+
bbox.top < svgRect.bottom
315+
) {
316+
anyVisible = true;
317+
break;
318+
}
319+
}
320+
if (!anyVisible) {
321+
showRecenterButton(() => {
322+
// Reset zoom/pan
323+
svg.transition().duration(400).call(zoomBehavior.transform, d3.zoomIdentity);
324+
});
325+
} else {
326+
hideRecenterButton();
327+
}
328+
} catch {}
329+
}, 0);
267330
});
268331
svg.call(zoomBehavior);
269332
if (prevTransform) {

scripts/static/js/performance.js

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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
179212
export 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
655717
function 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

Comments
 (0)