@@ -99,6 +99,14 @@ const createORCAVisual = (
9999 let HOVERED_NODE = null ;
100100 let CLICK_ACTIVE = false ;
101101 let CLICKED_NODE = null ;
102+ let zoomTransform = d3 . zoomIdentity ;
103+ let zoomBehavior ;
104+ let zoomPanning = false ;
105+ let zoomLastInteraction = 0 ;
106+ let zoomMoved = false ;
107+ let zoomMovedAt = 0 ;
108+ let zoomStartTransform = d3 . zoomIdentity ;
109+ const ZOOM_CLICK_SUPPRESS_MS = 150 ;
102110
103111 // Visual Settings - Based on SF = 1
104112 const CENTRAL_RADIUS = 35 ; // The radius of the central repository node (reduced for less prominence)
@@ -421,13 +429,39 @@ const createORCAVisual = (
421429 /////////////////////////////////////////////////////////////
422430 setupHover ( ) ;
423431 setupClick ( ) ;
432+ setupZoom ( ) ;
424433
425434 /////////////////////////////////////////////////////////////
426435 ///////////// Set the Sizes and Draw the Visual /////////////
427436 /////////////////////////////////////////////////////////////
428437 chart . resize ( ) ;
429438 } // function chart
430439
440+ /////////////////////////////////////////////////////////////////
441+ /////////////////////// Zoom Helpers ////////////////////////////
442+ /////////////////////////////////////////////////////////////////
443+
444+ function applyZoomTransform ( context ) {
445+ context . translate ( zoomTransform . x * PIXEL_RATIO , zoomTransform . y * PIXEL_RATIO ) ;
446+ context . scale ( zoomTransform . k , zoomTransform . k ) ;
447+ context . translate ( WIDTH / 2 , HEIGHT / 2 ) ;
448+ } // function applyZoomTransform
449+
450+ function redrawAll ( ) {
451+ draw ( ) ;
452+ if ( CLICK_ACTIVE && CLICKED_NODE ) {
453+ context_click . clearRect ( 0 , 0 , WIDTH , HEIGHT ) ;
454+ drawHoverState ( context_click , CLICKED_NODE , false ) ;
455+ } else {
456+ context_click . clearRect ( 0 , 0 , WIDTH , HEIGHT ) ;
457+ }
458+ if ( HOVER_ACTIVE && HOVERED_NODE ) {
459+ drawHoverState ( context_hover , HOVERED_NODE ) ;
460+ } else {
461+ context_hover . clearRect ( 0 , 0 , WIDTH , HEIGHT ) ;
462+ }
463+ } // function redrawAll
464+
431465 /////////////////////////////////////////////////////////////////
432466 //////////////////////// Draw the visual ////////////////////////
433467 /////////////////////////////////////////////////////////////////
@@ -440,7 +474,7 @@ const createORCAVisual = (
440474
441475 // Move the visual to the center
442476 context . save ( ) ;
443- context . translate ( WIDTH / 2 , HEIGHT / 2 ) ;
477+ applyZoomTransform ( context ) ;
444478
445479 /////////////////////////////////////////////////////////////
446480 // Draw the remaining contributors as small circles outside the ORCA circles
@@ -548,8 +582,8 @@ const createORCAVisual = (
548582 // // Test to see if the delaunay works
549583 // testDelaunay(delaunay, context_hover)
550584
551- // Draw the visual
552- draw ( ) ;
585+ // Draw the visual (and overlays if active)
586+ redrawAll ( ) ;
553587 } ; //function resize
554588
555589 /////////////////////////////////////////////////////////////////
@@ -2228,6 +2262,71 @@ const createORCAVisual = (
22282262 /////////////////////////////////////////////////////////////////
22292263 //////////////////////// Hover Functions ////////////////////////
22302264 /////////////////////////////////////////////////////////////////
2265+ function setupZoom ( ) {
2266+ zoomBehavior = d3
2267+ . zoom ( )
2268+ . filter ( ( event ) => event . type !== "wheel" && event . type !== "dblclick" )
2269+ . scaleExtent ( [ 0.4 , 6 ] )
2270+ . on ( "start" , ( ) => {
2271+ zoomPanning = true ;
2272+ zoomMoved = false ;
2273+ zoomStartTransform = zoomTransform ;
2274+ } )
2275+ . on ( "zoom" , ( event ) => {
2276+ zoomTransform = event . transform ;
2277+ zoomLastInteraction = Date . now ( ) ;
2278+ if (
2279+ zoomTransform . k !== zoomStartTransform . k ||
2280+ zoomTransform . x !== zoomStartTransform . x ||
2281+ zoomTransform . y !== zoomStartTransform . y
2282+ ) {
2283+ zoomMoved = true ;
2284+ }
2285+ redrawAll ( ) ;
2286+ } )
2287+ . on ( "end" , ( ) => {
2288+ zoomPanning = false ;
2289+ zoomLastInteraction = Date . now ( ) ;
2290+ if ( zoomMoved ) zoomMovedAt = zoomLastInteraction ;
2291+ } ) ;
2292+
2293+ const zoomTarget = d3 . select ( "#canvas-hover" ) ;
2294+ zoomTarget . call ( zoomBehavior ) ;
2295+
2296+ const zoomInBtn = document . getElementById ( "zoom-in" ) ;
2297+ const zoomOutBtn = document . getElementById ( "zoom-out" ) ;
2298+ const zoomResetBtn = document . getElementById ( "zoom-reset" ) ;
2299+ function getZoomCenter ( ) {
2300+ const rect = canvas_hover . getBoundingClientRect ( ) ;
2301+ return [ rect . width / 2 , rect . height / 2 ] ;
2302+ }
2303+
2304+ if ( zoomInBtn ) {
2305+ zoomInBtn . onclick = ( ) => {
2306+ zoomTarget
2307+ . transition ( )
2308+ . duration ( 150 )
2309+ . call ( zoomBehavior . scaleBy , 1.2 , getZoomCenter ( ) ) ;
2310+ } ;
2311+ }
2312+ if ( zoomOutBtn ) {
2313+ zoomOutBtn . onclick = ( ) => {
2314+ zoomTarget
2315+ . transition ( )
2316+ . duration ( 150 )
2317+ . call ( zoomBehavior . scaleBy , 1 / 1.2 , getZoomCenter ( ) ) ;
2318+ } ;
2319+ }
2320+ if ( zoomResetBtn ) {
2321+ zoomResetBtn . onclick = ( ) => {
2322+ zoomTarget
2323+ . transition ( )
2324+ . duration ( 150 )
2325+ . call ( zoomBehavior . transform , d3 . zoomIdentity ) ;
2326+ } ;
2327+ }
2328+ }
2329+
22312330 // Setup the hover on the top canvas, get the mouse position and call the drawing functions
22322331 function setupHover ( ) {
22332332 d3 . select ( "#canvas-hover" ) . on ( "mousemove" , function ( event ) {
@@ -2285,7 +2384,7 @@ const createORCAVisual = (
22852384 // Draw the hover canvas
22862385 context . save ( ) ;
22872386 context . clearRect ( 0 , 0 , WIDTH , HEIGHT ) ;
2288- context . translate ( WIDTH / 2 , HEIGHT / 2 ) ;
2387+ applyZoomTransform ( context ) ;
22892388
22902389 /////////////////////////////////////////////////
22912390 // Get all the connected links (if not done before)
@@ -2413,6 +2512,9 @@ const createORCAVisual = (
24132512
24142513 function setupClick ( ) {
24152514 d3 . select ( "#canvas-hover" ) . on ( "click" , function ( event ) {
2515+ if ( zoomPanning || ( zoomMoved && Date . now ( ) - zoomMovedAt < ZOOM_CLICK_SUPPRESS_MS ) ) {
2516+ return ;
2517+ }
24162518 // Get the position of the mouse on the canvas
24172519 let [ mx , my ] = d3 . pointer ( event , this ) ;
24182520 let [ d , FOUND ] = findNode ( mx , my ) ;
@@ -2458,8 +2560,16 @@ const createORCAVisual = (
24582560
24592561 // Turn the mouse position into a canvas x and y location and see if it's close enough to a node
24602562 function findNode ( mx , my ) {
2461- mx = ( mx * PIXEL_RATIO - WIDTH / 2 ) / SF ;
2462- my = ( my * PIXEL_RATIO - HEIGHT / 2 ) / SF ;
2563+ const mxDevice = mx * PIXEL_RATIO ;
2564+ const myDevice = my * PIXEL_RATIO ;
2565+ mx =
2566+ ( ( mxDevice - zoomTransform . x * PIXEL_RATIO ) / zoomTransform . k -
2567+ WIDTH / 2 ) /
2568+ SF ;
2569+ my =
2570+ ( ( myDevice - zoomTransform . y * PIXEL_RATIO ) / zoomTransform . k -
2571+ HEIGHT / 2 ) /
2572+ SF ;
24632573
24642574 // Check if mouse is within the visualization bounds (with some margin)
24652575 const MAX_RADIUS = RADIUS_CONTRIBUTOR_NON_ORCA + ORCA_RING_WIDTH + 200 ;
@@ -2471,7 +2581,7 @@ const createORCAVisual = (
24712581 //Get the closest hovered node
24722582 let point = delaunay . find ( mx , my ) ;
24732583 let d = nodes_delaunay [ point ] ;
2474-
2584+
24752585 // Safety check - if no node found, return early
24762586 if ( ! d ) {
24772587 return [ null , false ] ;
@@ -3268,6 +3378,7 @@ const createORCAVisual = (
32683378 // Re-setup interaction handlers
32693379 setupHover ( ) ;
32703380 setupClick ( ) ;
3381+ setupZoom ( ) ;
32713382
32723383 // Redraw with new scale factors
32733384 chart . resize ( ) ;
0 commit comments