@@ -1468,11 +1468,24 @@ class GraphChart {
14681468 this . curveParallel = options . curveParallel !== false ;
14691469 this . curveStep = options . curveStep ?? 22 ;
14701470 this . endGap = options . endGap ?? this . nodeRadius + 7 ;
1471+ this . interactive = options . interactive !== false ;
1472+ this . minZoom = options . minZoom ?? 0.2 ;
1473+ this . maxZoom = options . maxZoom ?? 4 ;
1474+ this . tooltipRenderer = options . tooltipRenderer || null ;
14711475 this . graph = null ;
14721476 this . layout = null ;
14731477 this . _nodeEls = new Map ;
14741478 this . _linkEls = [ ] ;
14751479 this . _onTick = ( ) => this . _position ( ) ;
1480+ this . _tx = 0 ;
1481+ this . _ty = 0 ;
1482+ this . _scale = 1 ;
1483+ this . _adj = new Map ;
1484+ this . _hover = null ;
1485+ this . _drag = null ;
1486+ this . _pan = null ;
1487+ this . _tooltip = null ;
1488+ this . _listeners = [ ] ;
14761489 if ( this . doc && container )
14771490 this . _mount ( ) ;
14781491 }
@@ -1489,10 +1502,15 @@ class GraphChart {
14891502 this . linkLabelLayer . setAttribute ( "class" , "gjs-link-labels" ) ;
14901503 this . nodeLayer = this . _el ( "g" ) ;
14911504 this . nodeLayer . setAttribute ( "class" , "gjs-nodes" ) ;
1492- this . svg . appendChild ( this . linkLayer ) ;
1493- this . svg . appendChild ( this . linkLabelLayer ) ;
1494- this . svg . appendChild ( this . nodeLayer ) ;
1505+ this . scene = this . _el ( "g" ) ;
1506+ this . scene . setAttribute ( "class" , "gjs-viewport" ) ;
1507+ this . scene . appendChild ( this . linkLayer ) ;
1508+ this . scene . appendChild ( this . linkLabelLayer ) ;
1509+ this . scene . appendChild ( this . nodeLayer ) ;
1510+ this . svg . appendChild ( this . scene ) ;
14951511 this . container . appendChild ( this . svg ) ;
1512+ if ( this . interactive )
1513+ this . _bindInteraction ( ) ;
14961514 }
14971515 _defs ( ) {
14981516 const defs = this . _el ( "defs" ) ;
@@ -1624,6 +1642,18 @@ class GraphChart {
16241642 this . nodeLayer . appendChild ( g ) ;
16251643 this . _nodeEls . set ( node . id , g ) ;
16261644 }
1645+ this . _buildAdjacency ( ) ;
1646+ }
1647+ _buildAdjacency ( ) {
1648+ this . _adj = new Map ;
1649+ for ( const l of this . graph && this . graph . linkList || [ ] ) {
1650+ if ( ! this . _adj . has ( l . source . id ) )
1651+ this . _adj . set ( l . source . id , new Set ) ;
1652+ if ( ! this . _adj . has ( l . target . id ) )
1653+ this . _adj . set ( l . target . id , new Set ) ;
1654+ this . _adj . get ( l . source . id ) . add ( l . target . id ) ;
1655+ this . _adj . get ( l . target . id ) . add ( l . source . id ) ;
1656+ }
16271657 }
16281658 _buildTemplateNode ( node , g , spec ) {
16291659 if ( spec . cssClass )
@@ -1773,11 +1803,191 @@ class GraphChart {
17731803 nodeElement ( id ) {
17741804 return this . _nodeEls . get ( id ) ;
17751805 }
1806+ _applyTransform ( ) {
1807+ if ( this . scene )
1808+ this . scene . setAttribute ( "transform" , `translate(${ this . _tx } , ${ this . _ty } ) scale(${ this . _scale } )` ) ;
1809+ }
1810+ screenToWorld ( px , py ) {
1811+ return { x : ( px - this . _tx ) / this . _scale , y : ( py - this . _ty ) / this . _scale } ;
1812+ }
1813+ setZoom ( scale , cx , cy ) {
1814+ const s = Math . max ( this . minZoom , Math . min ( this . maxZoom , scale ) ) ;
1815+ if ( cx != null && cy != null ) {
1816+ const w = this . screenToWorld ( cx , cy ) ;
1817+ this . _scale = s ;
1818+ this . _tx = cx - w . x * s ;
1819+ this . _ty = cy - w . y * s ;
1820+ } else {
1821+ this . _scale = s ;
1822+ }
1823+ this . _applyTransform ( ) ;
1824+ return this ;
1825+ }
1826+ panBy ( dx , dy ) {
1827+ this . _tx += dx ;
1828+ this . _ty += dy ;
1829+ this . _applyTransform ( ) ;
1830+ return this ;
1831+ }
1832+ fitToView ( padding = 40 ) {
1833+ const nodes = this . graph ? this . graph . getNodes ( ) : [ ] ;
1834+ if ( ! nodes . length )
1835+ return this ;
1836+ let minX = Infinity , minY = Infinity , maxX = - Infinity , maxY = - Infinity ;
1837+ for ( const n of nodes ) {
1838+ if ( n . x < minX )
1839+ minX = n . x ;
1840+ if ( n . y < minY )
1841+ minY = n . y ;
1842+ if ( n . x > maxX )
1843+ maxX = n . x ;
1844+ if ( n . y > maxY )
1845+ maxY = n . y ;
1846+ }
1847+ const spanX = maxX - minX || 1 , spanY = maxY - minY || 1 ;
1848+ this . _scale = Math . max ( this . minZoom , Math . min ( this . maxZoom , Math . min ( ( this . width - 2 * padding ) / spanX , ( this . height - 2 * padding ) / spanY ) ) ) ;
1849+ this . _tx = ( this . width - ( minX + maxX ) * this . _scale ) / 2 ;
1850+ this . _ty = ( this . height - ( minY + maxY ) * this . _scale ) / 2 ;
1851+ this . _applyTransform ( ) ;
1852+ return this ;
1853+ }
1854+ centerOnNode ( id , scale ) {
1855+ const n = this . graph && this . graph . getNode ( id ) ;
1856+ if ( ! n )
1857+ return this ;
1858+ if ( scale != null )
1859+ this . _scale = Math . max ( this . minZoom , Math . min ( this . maxZoom , scale ) ) ;
1860+ this . _tx = this . width / 2 - n . x * this . _scale ;
1861+ this . _ty = this . height / 2 - n . y * this . _scale ;
1862+ this . _applyTransform ( ) ;
1863+ return this ;
1864+ }
1865+ hitTestNode ( px , py ) {
1866+ const w = this . screenToWorld ( px , py ) ;
1867+ const r = this . nodeRadius + 6 ;
1868+ let best = null , bestD = r * r ;
1869+ for ( const n of this . graph ? this . graph . getNodes ( ) : [ ] ) {
1870+ const dx = w . x - n . x , dy = w . y - n . y , d2 = dx * dx + dy * dy ;
1871+ if ( d2 <= bestD ) {
1872+ bestD = d2 ;
1873+ best = n ;
1874+ }
1875+ }
1876+ return best ;
1877+ }
1878+ setHighlight ( nodeId ) {
1879+ this . _hover = nodeId ;
1880+ const lit = nodeId != null ? new Set ( [ nodeId , ...this . _adj . get ( nodeId ) || [ ] ] ) : null ;
1881+ for ( const [ id , g ] of this . _nodeEls )
1882+ g . setAttribute ( "opacity" , lit && ! lit . has ( id ) ? "0.25" : "1" ) ;
1883+ for ( const { link, el, label } of this . _linkEls ) {
1884+ const incident = nodeId == null || link . source . id === nodeId || link . target . id === nodeId ;
1885+ const op = nodeId != null && ! incident ? "0.12" : "1" ;
1886+ el . setAttribute ( "opacity" , op ) ;
1887+ if ( label )
1888+ label . setAttribute ( "opacity" , op ) ;
1889+ }
1890+ return this ;
1891+ }
1892+ setTooltipRenderer ( fn ) {
1893+ this . tooltipRenderer = fn ;
1894+ return this ;
1895+ }
1896+ _showTooltip ( node , px , py ) {
1897+ if ( ! this . tooltipRenderer || ! this . doc || typeof this . doc . createElement !== "function" )
1898+ return ;
1899+ if ( ! this . _tooltip ) {
1900+ this . _tooltip = this . doc . createElement ( "div" ) ;
1901+ this . _tooltip . setAttribute ( "class" , "gjs-tooltip" ) ;
1902+ this . _tooltip . style . position = "absolute" ;
1903+ this . _tooltip . style . pointerEvents = "none" ;
1904+ this . container . appendChild ( this . _tooltip ) ;
1905+ }
1906+ this . _tooltip . innerHTML = this . tooltipRenderer ( node ) ;
1907+ this . _tooltip . style . display = "block" ;
1908+ this . _tooltip . style . left = px + 14 + "px" ;
1909+ this . _tooltip . style . top = py + 14 + "px" ;
1910+ }
1911+ _hideTooltip ( ) {
1912+ if ( this . _tooltip )
1913+ this . _tooltip . style . display = "none" ;
1914+ }
1915+ _bindInteraction ( ) {
1916+ const svg = this . svg ;
1917+ if ( ! svg || typeof svg . addEventListener !== "function" )
1918+ return ;
1919+ const add = ( t , ev , fn , opts ) => {
1920+ t . addEventListener ( ev , fn , opts ) ;
1921+ this . _listeners . push ( [ t , ev , fn ] ) ;
1922+ } ;
1923+ const pos = ( e ) => {
1924+ const r = svg . getBoundingClientRect ? svg . getBoundingClientRect ( ) : { left : 0 , top : 0 } ;
1925+ return { x : e . clientX - r . left , y : e . clientY - r . top } ;
1926+ } ;
1927+ add ( svg , "mousedown" , ( e ) => {
1928+ const p = pos ( e ) ;
1929+ const n = this . hitTestNode ( p . x , p . y ) ;
1930+ if ( n && this . layout && this . layout . pinNode ) {
1931+ const w = this . screenToWorld ( p . x , p . y ) ;
1932+ this . _drag = n ;
1933+ this . layout . pinNode ( n . id , w . x , w . y ) ;
1934+ if ( this . layout . restart )
1935+ this . layout . restart ( 0.5 ) ;
1936+ } else {
1937+ this . _pan = p ;
1938+ }
1939+ } ) ;
1940+ add ( svg , "mousemove" , ( e ) => {
1941+ const p = pos ( e ) ;
1942+ if ( this . _drag ) {
1943+ const w = this . screenToWorld ( p . x , p . y ) ;
1944+ this . layout . pinNode ( this . _drag . id , w . x , w . y ) ;
1945+ if ( this . layout . restart )
1946+ this . layout . restart ( 0.35 ) ;
1947+ } else if ( this . _pan ) {
1948+ this . panBy ( p . x - this . _pan . x , p . y - this . _pan . y ) ;
1949+ this . _pan = p ;
1950+ } else {
1951+ const n = this . hitTestNode ( p . x , p . y ) ;
1952+ const id = n ? n . id : null ;
1953+ if ( id !== this . _hover )
1954+ this . setHighlight ( id ) ;
1955+ if ( n )
1956+ this . _showTooltip ( n , p . x , p . y ) ;
1957+ else
1958+ this . _hideTooltip ( ) ;
1959+ if ( svg . style )
1960+ svg . style . cursor = n ? "pointer" : "grab" ;
1961+ }
1962+ } ) ;
1963+ add ( this . doc , "mouseup" , ( ) => {
1964+ if ( this . _drag && this . layout && this . layout . unpinNode ) {
1965+ this . layout . unpinNode ( this . _drag . id ) ;
1966+ if ( this . layout . restart )
1967+ this . layout . restart ( 0.2 ) ;
1968+ }
1969+ this . _drag = null ;
1970+ this . _pan = null ;
1971+ } ) ;
1972+ add ( svg , "wheel" , ( e ) => {
1973+ if ( e . preventDefault )
1974+ e . preventDefault ( ) ;
1975+ const p = pos ( e ) ;
1976+ this . setZoom ( this . _scale * Math . exp ( - ( e . deltaY || 0 ) * 0.0015 ) , p . x , p . y ) ;
1977+ } , { passive : false } ) ;
1978+ }
17761979 destroy ( ) {
17771980 if ( this . layout && this . layout . off ) {
17781981 this . layout . off ( "tick" , this . _onTick ) ;
17791982 this . layout . off ( "end" , this . _onTick ) ;
17801983 }
1984+ for ( const [ t , ev , fn ] of this . _listeners )
1985+ if ( t . removeEventListener )
1986+ t . removeEventListener ( ev , fn ) ;
1987+ this . _listeners = [ ] ;
1988+ if ( this . _tooltip && this . _tooltip . parentNode )
1989+ this . _tooltip . parentNode . removeChild ( this . _tooltip ) ;
1990+ this . _tooltip = null ;
17811991 if ( this . svg && this . svg . parentNode )
17821992 this . svg . parentNode . removeChild ( this . svg ) ;
17831993 this . _nodeEls . clear ( ) ;
0 commit comments