@@ -277,8 +277,112 @@ function Filters({ counts, filters, onFilter }) {
277277
278278function Graph ( props ) {
279279 const [ error , setState ] = useState ( "" ) ;
280+ const containerRef = React . useRef ( null ) ;
280281 const url = urls . migrations . graph . replace ( "<NAME>" , props . children ) ;
281282 const onError = ( error ) => setState ( error ) ;
283+
284+ useEffect ( ( ) => {
285+ if ( ! containerRef . current || error ) return ;
286+
287+ const container = d3 . select ( containerRef . current ) ;
288+ let timer = null ;
289+
290+ const setupZoom = ( ) => {
291+ const svgElement = container . select ( 'svg' ) . node ( ) ;
292+ if ( ! svgElement ) {
293+ // Wait a bit for SVG to load
294+ timer = setTimeout ( setupZoom , 100 ) ;
295+ return ;
296+ }
297+
298+ const svg = d3 . select ( svgElement ) ;
299+
300+ // Check if group already exists
301+ let svgGroup = svg . select ( 'g.zoom-group' ) ;
302+ if ( svgGroup . empty ( ) ) {
303+ svgGroup = svg . append ( 'g' ) . attr ( 'class' , 'zoom-group' ) ;
304+
305+ // Move all existing children into the group (except the group itself)
306+ svg . selectAll ( '*' ) . each ( function ( ) {
307+ const node = this ;
308+ if ( node !== svgGroup . node ( ) && node . parentNode === svgElement ) {
309+ svgGroup . node ( ) . appendChild ( node ) ;
310+ }
311+ } ) ;
312+ }
313+
314+ // Get SVG dimensions (use viewBox if available, otherwise use bounding rect)
315+ const viewBox = svgElement . viewBox ?. baseVal ;
316+ const svgWidth = viewBox ? viewBox . width : ( svgElement . getBoundingClientRect ( ) . width || containerRef . current . clientWidth ) ;
317+ const svgHeight = viewBox ? viewBox . height : ( svgElement . getBoundingClientRect ( ) . height || containerRef . current . clientHeight || 600 ) ;
318+
319+ // Get bounding box of the content (relative to SVG coordinate system)
320+ const bbox = svgElement . getBBox ( ) ;
321+
322+ // Calculate initial transform values
323+ const initialScale = Math . min (
324+ svgWidth / bbox . width ,
325+ svgHeight / bbox . height ,
326+ 1
327+ ) * 0.9 ;
328+
329+ // Calculate center position in SVG coordinate system
330+ const centerX = svgWidth / 2 ;
331+ const centerY = svgHeight / 2 ;
332+
333+ // The bbox center in SVG coordinates
334+ const bboxCenterX = bbox . x + bbox . width / 2 ;
335+ const bboxCenterY = bbox . y + bbox . height / 2 ;
336+
337+ // Translate so that the bbox center (scaled) maps to the SVG center
338+ const initialTranslate = [
339+ centerX - bboxCenterX * initialScale ,
340+ centerY - bboxCenterY * initialScale ,
341+ ] ;
342+
343+ // Store initial transform for reset
344+ const initialTransform = d3 . zoomIdentity
345+ . translate ( initialTranslate [ 0 ] , initialTranslate [ 1 ] )
346+ . scale ( initialScale ) ;
347+
348+ // Set up zoom behavior - apply to SVG element for proper drag sensitivity
349+ const zoom = d3 . zoom ( )
350+ . scaleExtent ( [ 0.1 , 4 ] )
351+ . on ( "zoom" , ( event ) => {
352+ svgGroup . attr ( "transform" , event . transform ) ;
353+ } ) ;
354+
355+ // Apply zoom to the SVG element itself for proper drag behavior
356+ svg . call ( zoom ) ;
357+
358+ // Center and scale initially (only if not already transformed)
359+ if ( ! svgGroup . attr ( "transform" ) ) {
360+ svg . call ( zoom . transform , initialTransform ) ;
361+ }
362+
363+ // Double-click to reset zoom/pan to initial state
364+ svg . on ( "dblclick.zoom" , null ) ; // Remove default double-click zoom
365+ svg . on ( "dblclick" , function ( ) {
366+ svg . transition ( )
367+ . duration ( 750 )
368+ . call ( zoom . transform , initialTransform ) ;
369+ } ) ;
370+ } ;
371+
372+ setupZoom ( ) ;
373+
374+ // Cleanup
375+ return ( ) => {
376+ if ( timer ) clearTimeout ( timer ) ;
377+ const svgElement = container . select ( 'svg' ) . node ( ) ;
378+ if ( svgElement ) {
379+ const svg = d3 . select ( svgElement ) ;
380+ svg . on ( ".zoom" , null ) ;
381+ svg . on ( "dblclick" , null ) ;
382+ }
383+ } ;
384+ } , [ error , url ] ) ;
385+
282386 return (
283387 < div >
284388 < p style = { { textAlign : "center" } } >
@@ -291,12 +395,23 @@ function Graph(props) {
291395 < p style = { { textAlign : "center" } } >
292396 Graph is unavailable.
293397 </ p > :
294- < div style = { { overflowX : "auto" } } >
398+ < div
399+ ref = { containerRef }
400+ style = { {
401+ width : "100%" ,
402+ height : "600px" ,
403+ overflow : "hidden" ,
404+ border : "1px solid var(--ifm-color-emphasis-300)" ,
405+ borderRadius : "8px" ,
406+ cursor : "move"
407+ } }
408+ >
295409 < SVG
296410 onError = { onError }
297411 src = { url }
298412 title = { props . children }
299413 description = { `Migration graph for ${ props . children } ` }
414+ style = { { width : "100%" , height : "100%" } }
300415 />
301416 </ div >
302417 }
0 commit comments