@@ -12,6 +12,7 @@ import TabItem from '@theme/TabItem';
1212import moment from 'moment' ;
1313import { compare } from '@site/src/components/StatusDashboard/current_migrations' ;
1414import { 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
310311function 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