Skip to content

Commit 150c8da

Browse files
committed
Highlighting of edges for the selected node added
1 parent e2d8ed9 commit 150c8da

File tree

2 files changed

+141
-18
lines changed

2 files changed

+141
-18
lines changed

scripts/static/js/graph.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function updateGraphNodeSelection() {
5252
.attr('stroke', d => selectedProgramId === d.id ? 'red' : '#333')
5353
.attr('stroke-width', d => selectedProgramId === d.id ? 3 : 1.5)
5454
.classed('node-selected', d => selectedProgramId === d.id);
55+
updateGraphEdgeSelection(); // update edge highlight when node selection changes
5556
}
5657

5758
export function getNodeColor(d) {
@@ -106,6 +107,7 @@ export function selectProgram(programId) {
106107
});
107108
// Dispatch event for list view sync
108109
window.dispatchEvent(new CustomEvent('node-selected', { detail: { id: programId } }));
110+
updateGraphEdgeSelection(); // update edge highlight on selection
109111
}
110112

111113
let svg = null;
@@ -254,6 +256,7 @@ function renderGraph(data, options = {}) {
254256
node
255257
.attr("cx", d => d.x)
256258
.attr("cy", d => d.y);
259+
updateGraphEdgeSelection(); // update edge highlight on tick
257260
});
258261

259262
// Intelligent zoom/pan
@@ -311,6 +314,7 @@ function renderGraph(data, options = {}) {
311314
}
312315

313316
selectProgram(selectedProgramId);
317+
updateGraphEdgeSelection(); // update edge highlight after render
314318
applyDragHandlersToAllNodes();
315319

316320
svg.on("click", function(event) {
@@ -387,6 +391,14 @@ export function centerAndHighlightNodeInGraph(nodeId) {
387391
}
388392
}
389393

394+
export function updateGraphEdgeSelection() {
395+
if (!g) return;
396+
g.selectAll('line')
397+
.attr('stroke', d => (selectedProgramId && (d.source.id === selectedProgramId || d.target.id === selectedProgramId)) ? 'red' : '#999')
398+
.attr('stroke-width', d => (selectedProgramId && (d.source.id === selectedProgramId || d.target.id === selectedProgramId)) ? 4 : 2)
399+
.attr('stroke-opacity', d => (selectedProgramId && (d.source.id === selectedProgramId || d.target.id === selectedProgramId)) ? 0.95 : 0.6);
400+
}
401+
390402
function dragstarted(event, d) {
391403
if (!event.active && simulation) simulation.alphaTarget(0.3).restart(); // Keep simulation alive
392404
d.fx = d.x;

scripts/static/js/performance.js

Lines changed: 129 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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;
226233
let zoomBehavior = null;
227234
let 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+
229270
function 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

Comments
 (0)