Skip to content

Commit 7e83834

Browse files
committed
Make Migration SVG graph zoomable/panable.
Extracted from conda-forge#2670, should be less controversial, and easier to review.
1 parent c91054d commit 7e83834

File tree

1 file changed

+107
-1
lines changed

1 file changed

+107
-1
lines changed

src/pages/status/migration/index.jsx

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import TabItem from '@theme/TabItem';
1212
import moment from 'moment';
1313
import { compare } from '@site/src/components/StatusDashboard/current_migrations';
1414
import { useSorting, SortableHeader } from '@site/src/components/SortableTable';
15+
import * as d3 from "d3";
1516

1617
// GitHub GraphQL MergeStateStatus documentation
1718
// Reference: https://docs.github.com/en/graphql/reference/enums#mergestatestatus
@@ -309,8 +310,104 @@ function Filters({ counts, filters, onFilter }) {
309310

310311
function Graph(props) {
311312
const [error, setState] = useState("");
313+
const containerRef = React.useRef(null);
312314
const url = urls.migrations.graph.replace("<NAME>", props.children);
313315
const onError = (error) => setState(error);
316+
317+
useEffect(() => {
318+
if (!containerRef.current || error) return;
319+
320+
const container = d3.select(containerRef.current);
321+
let timer = null;
322+
323+
const setupZoom = () => {
324+
const svgElement = container.select('svg').node();
325+
if (!svgElement) {
326+
// Wait a bit for SVG to load
327+
timer = setTimeout(setupZoom, 100);
328+
return;
329+
}
330+
331+
const svg = d3.select(svgElement);
332+
333+
// Check if group already exists
334+
let svgGroup = svg.select('g.zoom-group');
335+
if (svgGroup.empty()) {
336+
svgGroup = svg.append('g').attr('class', 'zoom-group');
337+
338+
// Move all existing children into the group (except the group itself)
339+
svg.selectAll('*').each(function() {
340+
const node = this;
341+
if (node !== svgGroup.node() && node.parentNode === svgElement) {
342+
svgGroup.node().appendChild(node);
343+
}
344+
});
345+
}
346+
347+
// Get SVG dimensions (use viewBox if available, otherwise use bounding rect)
348+
const viewBox = svgElement.viewBox?.baseVal;
349+
const svgWidth = viewBox ? viewBox.width : (svgElement.getBoundingClientRect().width || containerRef.current.clientWidth);
350+
const svgHeight = viewBox ? viewBox.height : (svgElement.getBoundingClientRect().height || containerRef.current.clientHeight || 600);
351+
352+
const bbox = svgElement.getBBox();
353+
354+
const initialScale = Math.min(
355+
svgWidth / bbox.width,
356+
svgHeight / bbox.height,
357+
1
358+
) * 0.9;
359+
360+
const centerX = svgWidth / 2;
361+
const centerY = svgHeight / 2;
362+
363+
const bboxCenterX = bbox.x + bbox.width / 2;
364+
const bboxCenterY = bbox.y + bbox.height / 2;
365+
366+
const initialTranslate = [
367+
centerX - bboxCenterX * initialScale,
368+
centerY - bboxCenterY * initialScale,
369+
];
370+
371+
// Store initial transform for reset
372+
const initialTransform = d3.zoomIdentity
373+
.translate(initialTranslate[0], initialTranslate[1])
374+
.scale(initialScale);
375+
376+
// Set up zoom behavior - apply to SVG element for proper drag sensitivity
377+
const zoom = d3.zoom()
378+
.scaleExtent([0.1, 4])
379+
.on("zoom", (event) => {
380+
svgGroup.attr("transform", event.transform);
381+
});
382+
383+
svg.call(zoom);
384+
385+
if (!svgGroup.attr("transform")) {
386+
svg.call(zoom.transform, initialTransform);
387+
}
388+
389+
// Double-click to reset zoom/pan to initial state
390+
svg.on("dblclick.zoom", null); // Remove default double-click zoom
391+
svg.on("dblclick", function() {
392+
svg.transition()
393+
.duration(750)
394+
.call(zoom.transform, initialTransform);
395+
});
396+
};
397+
398+
setupZoom();
399+
400+
return () => {
401+
if (timer) clearTimeout(timer);
402+
const svgElement = container.select('svg').node();
403+
if (svgElement) {
404+
const svg = d3.select(svgElement);
405+
svg.on(".zoom", null);
406+
svg.on("dblclick", null);
407+
}
408+
};
409+
}, [error, url]);
410+
314411
return (
315412
<div>
316413
<p style={{textAlign: "center"}}>
@@ -323,12 +420,21 @@ function Graph(props) {
323420
<p style={{textAlign: "center"}}>
324421
Graph is unavailable.
325422
</p> :
326-
<div style={{ overflowX: "auto" }}>
423+
<div
424+
ref={containerRef}
425+
style={{
426+
width: "100%",
427+
height: "600px",
428+
overflow: "hidden",
429+
cursor: "move"
430+
}}
431+
>
327432
<SVG
328433
onError={onError}
329434
src={url}
330435
title={props.children}
331436
description={`Migration graph for ${props.children}`}
437+
style={{ width: "100%", height: "100%" }}
332438
/>
333439
</div>
334440
}

0 commit comments

Comments
 (0)