module import { EventEmitter } from "events" ; import Graph from "graphology" ; import Camera from "./core/camera" ; import MouseCaptor from "./core/captors/mouse" ; import QuadTree from "./core/quadtree" ; import { CameraState , Coordinates , Dimensions , EdgeDisplayData , Extent , Listener , MouseCoords , NodeDisplayData , PlainObject , CoordinateConversionOverride , } from "./types" ; import { createElement , getPixelRatio , createNormalizationFunction , NormalizationFunction , assign , cancelFrame , matrixFromCamera , requestFrame , validateGraph , zIndexOrdering , getMatrixImpact , graphExtent , } from "./utils" ; import { edgeLabelsToDisplayFromNodes , LabelGrid } from "./core/labels" ; import { Settings , DEFAULT_SETTINGS , validateSettings } from "./settings" ; import { INodeProgram } from "./rendering/webgl/programs/common/node" ; import { IEdgeProgram } from "./rendering/webgl/programs/common/edge" ; import TouchCaptor from "./core/captors/touch" ; import { identity , multiplyVec } from "./utils/matrices" ; import { doEdgeCollideWithPoint , isPixelColored } from "./utils/edge-collisions" ; /**
 * Constants.
 */ const PIXEL_RATIO = getPixelRatio ( ) ; /**
 * Important functions.
 */ function applyNodeDefaults ( settings , key , data ) { if ( ! data . hasOwnProperty ( "x" ) || ! data . hasOwnProperty ( "y" ) ) throw new Error ( `Sigma: could not find a valid position (x, y) for node "${ key }". All your nodes must have a number "x" and "y". Maybe your forgot to apply a layout or your "nodeReducer" is not returning the correct data?` ) ; if ( ! data . color ) data . color = settings . defaultNodeColor ; if ( ! data . label && data . label !== "" ) data . label = null ; if ( data . label !== undefined && data . label !== null ) data . label = "" + data . label ; else data . label = null ; if ( ! data . size ) data . size = 2 ; if ( ! data . hasOwnProperty ( "hidden" ) ) data . hidden = false ; if ( ! data . hasOwnProperty ( "highlighted" ) ) data . highlighted = false ; if ( ! data . hasOwnProperty ( "forceLabel" ) ) data . forceLabel = false ; if ( ! data . type || data . type === "" ) data . type = settings . defaultNodeType ; if ( ! data . zIndex ) data . zIndex = 0 ; return data as NodeDisplayData ; } function applyEdgeDefaults ( settings , key , data ) { if ( ! data . color ) data . color = settings . defaultEdgeColor ; if ( ! data . label ) data . label = "" ; if ( ! data . size ) data . size = 0.5 ; if ( ! data . hasOwnProperty ( "hidden" ) ) data . hidden = false ; if ( ! data . hasOwnProperty ( "forceLabel" ) ) data . forceLabel = false ; if ( ! data . type || data . type === "" ) data . type = settings . defaultEdgeType ; if ( ! data . zIndex ) data . zIndex = 0 ; return data as EdgeDisplayData ; } constructor param { Graph } graph param { HTMLElement } container param { object } settings export default class Sigma extends EventEmitter { private settings : Settings ; private graph : Graph ; private mouseCaptor : MouseCaptor ; private touchCaptor : TouchCaptor ; private container : HTMLElement ; private elements : PlainObject < HTMLCanvasElement > = { } ; private canvasContexts : PlainObject < CanvasRenderingContext2D > = { } ; private webGLContexts : PlainObject < WebGLRenderingContext > = { } ; private activeListeners : PlainObject < Listener > = { } ; private quadtree : QuadTree = new QuadTree ( ) ; private labelGrid : LabelGrid = new LabelGrid ( ) ; private nodeDataCache : Record < string , NodeDisplayData > = { } ; private edgeDataCache : Record < string , EdgeDisplayData > = { } ; private nodesWithForcedLabels : string [ ] = [ ] ; private edgesWithForcedLabels : string [ ] = [ ] ; private nodeKeyToIndex : Record < string , number > = { } ; private edgeKeyToIndex : Record < string , number > = { } ; private nodeExtent : { x : Extent ; y : Extent ; } = { x : [ 0 , 1 ] , y : [ 0 , 1 ] } ; private matrix : Float32Array = identity ( ) ; private invMatrix : Float32Array = identity ( ) ; private correctionRatio = 1 ; private customBBox : { x : Extent ; y : Extent ; } | null = null ; private normalizationFunction : NormalizationFunction = createNormalizationFunction ( { x : [ - Infinity , Infinity ] , y : [ - Infinity , Infinity ] , } ) ; private cameraSizeRatio = 1 ; private width = 0 ; private height = 0 ; private displayedLabels : Set < string > = new Set ( ) ; private highlightedNodes : Set < string > = new Set ( ) ; private hoveredNode : string | null = null ; private hoveredEdge : string | null = null ; private renderFrame : number | null = null ; private renderHighlightedNodesFrame : number | null = null ; private needToProcess = false ; private needToSoftProcess = false ; private checkEdgesEventsFrame : number | null = null ; private nodePrograms : { [ key ] : INodeProgram ; } = { } ; private hoverNodePrograms : { [ key ] : INodeProgram ; } = { } ; private edgePrograms : { [ key ] : IEdgeProgram ; } = { } ; private camera : Camera ; constructor ( graph , container , settings = { } ) { super ( ) ; this . settings = assign < Settings > ( { } , DEFAULT_SETTINGS , settings ) ; validateSettings ( this . settings ) ; validateGraph ( graph ) ; if ( ! ( container instanceof HTMLElement ) ) throw new Error ( "Sigma: container should be an html element." ) ; this . graph = graph ; this . container = container ; this . initializeCache ( ) ; this . createWebGLContext ( "edges" , { preserveDrawingBuffer : true } ) ; this . createCanvasContext ( "edgeLabels" ) ; this . createWebGLContext ( "nodes" ) ; this . createCanvasContext ( "labels" ) ; this . createCanvasContext ( "hovers" ) ; this . createWebGLContext ( "hoverNodes" ) ; this . createCanvasContext ( "mouse" ) ; for ( const key in this . webGLContexts ) { const gl = this . webGLContexts [ key ] ; gl . blendFunc ( gl . ONE , gl . ONE_MINUS_SRC_ALPHA ) ; gl . enable ( gl . BLEND ) ; } for ( const type in this . settings . nodeProgramClasses ) { const NodeProgramClass = this . settings . nodeProgramClasses [ type ] ; this . nodePrograms [ type ] = new NodeProgramClass ( this . webGLContexts . nodes , this ) ; this . hoverNodePrograms [ type ] = new NodeProgramClass ( this . webGLContexts . hoverNodes , this ) ; } for ( const type in this . settings . edgeProgramClasses ) { const EdgeProgramClass = this . settings . edgeProgramClasses [ type ] ; this . edgePrograms [ type ] = new EdgeProgramClass ( this . webGLContexts . edges , this ) ; } this . resize ( ) ; this . camera = new Camera ( ) ; this . bindCameraHandlers ( ) ; this . mouseCaptor = new MouseCaptor ( this . elements . mouse , this ) ; this . touchCaptor = new TouchCaptor ( this . elements . mouse , this ) ; this . bindEventHandlers ( ) ; this . bindGraphHandlers ( ) ; this . handleSettingsUpdate ( ) ; this . process ( ) ; this . render ( ) ; } /**---------------------------------------------------------------------------
     * Internal methods.
     **---------------------------------------------------------------------------
     */ param { string } id return { Sigma } private createCanvas ( id ) { const canvas = createElement < HTMLCanvasElement > ( "canvas" , { position : "absolute" , } , { class : `sigma-${ id }` , } ) ; this . elements [ id ] = canvas ; this . container . appendChild ( canvas ) ; return canvas ; } param { string } id return { Sigma } private createCanvasContext ( id ) { const canvas = this . createCanvas ( id ) ; const contextOptions = { preserveDrawingBuffer : false , antialias : false , } ; this . canvasContexts [ id ] = canvas . getContext ( "2d" , contextOptions ) as CanvasRenderingContext2D ; return this ; } param { string } id param { object ? } options return { Sigma } private createWebGLContext ( id , options ? ) { const canvas = this . createCanvas ( id ) ; const contextOptions = { preserveDrawingBuffer : false , antialias : false , ... ( options || { } ) , } ; let context ; context = canvas . getContext ( "webgl2" , contextOptions ) ; if ( ! context ) context = canvas . getContext ( "webgl" , contextOptions ) ; if ( ! context ) context = canvas . getContext ( "experimental-webgl" , contextOptions ) ; this . webGLContexts [ id ] = context as WebGLRenderingContext ; return this ; } return { Sigma } private initializeCache ( ) { const graph = this . graph ; let i = 0 ; graph . forEachNode ( ( key ) => { this . nodeKeyToIndex [ key ] = i ++ ; } ) ; i = 0 ; graph . forEachEdge ( ( key ) => { this . edgeKeyToIndex [ key ] = i ++ ; } ) ; } return { Sigma } private bindCameraHandlers ( ) { this . activeListeners . camera = ( ) => { this . _scheduleRefresh ( ) ; } ; this . camera . on ( "updated" , this . activeListeners . camera ) ; return this ; } return { Sigma } private bindEventHandlers ( ) { this . activeListeners . handleResize = ( ) => { this . needToSoftProcess = true ; this . _scheduleRefresh ( ) ; } ; window . addEventListener ( "resize" , this . activeListeners . handleResize ) ; const mouseIsOnNode = ( mouseX , mouseY , nodeX , nodeY , size ) : boolean => { return ( mouseX > nodeX - size && mouseX < nodeX + size && mouseY > nodeY - size && mouseY < nodeY + size && Math . sqrt ( Math . pow ( mouseX - nodeX , 2 ) + Math . pow ( mouseY - nodeY , 2 ) ) < size ) ; } ; const getQuadNodes = ( mouseX , mouseY ) => { const mouseGraphPosition = this . viewportToFramedGraph ( { x : mouseX , y : mouseY } ) ; return this . quadtree . point ( mouseGraphPosition . x , 1 - mouseGraphPosition . y ) ; } ; this . activeListeners . handleMove = ( e ) : void => { const quadNodes = getQuadNodes ( e . x , e . y ) ; let minDistance = Infinity , nodeToHover = null ; for ( let i = 0 , l = quadNodes . length ; i < l ; i ++ ) { const node = quadNodes [ i ] ; const data = this . nodeDataCache [ node ] ; const pos = this . framedGraphToViewport ( data ) ; const size = this . scaleSize ( data . size ) ; if ( ! data . hidden && mouseIsOnNode ( e . x , e . y , pos . x , pos . y , size ) ) { const distance = Math . sqrt ( Math . pow ( e . x - pos . x , 2 ) + Math . pow ( e . y - pos . y , 2 ) ) ; if ( distance < minDistance ) { minDistance = distance ; nodeToHover = node ; } } } if ( nodeToHover && this . hoveredNode !== nodeToHover && ! this . nodeDataCache [ nodeToHover ] . hidden ) { if ( this . hoveredNode ) this . emit ( "leaveNode" , { node : this . hoveredNode } ) ; this . hoveredNode = nodeToHover ; this . emit ( "enterNode" , { node : nodeToHover } ) ; this . scheduleHighlightedNodesRender ( ) ; return ; } if ( this . hoveredNode ) { const data = this . nodeDataCache [ this . hoveredNode ] ; const pos = this . framedGraphToViewport ( data ) ; const size = this . scaleSize ( data . size ) ; if ( ! mouseIsOnNode ( e . x , e . y , pos . x , pos . y , size ) ) { const node = this . hoveredNode ; this . hoveredNode = null ; this . emit ( "leaveNode" , { node } ) ; return this . scheduleHighlightedNodesRender ( ) ; } } const mousePosition = { x : e . x , y : e . y } ; if ( this . settings . enableEdgeHoverEvents === true ) { this . checkEdgeHoverEvents ( mousePosition ) ; } else if ( this . settings . enableEdgeHoverEvents === "debounce" ) { if ( ! this . checkEdgesEventsFrame ) this . checkEdgesEventsFrame = requestFrame ( ( ) => { this . checkEdgeHoverEvents ( mousePosition ) ; this . checkEdgesEventsFrame = null ; } ) ; } } ; const createMouseListener = ( eventType ) : ( ( e : MouseCoords ) => void ) => { return ( e ) => { const baseEvent = { event : e , preventSigmaDefault ( ) { this . event . preventSigmaDefault ( ) ; } , } ; if ( this . hoveredNode ) return this . emit ( `${ eventType }Node` , { ... baseEvent , node : this . hoveredNode , } ) ; if ( eventType === "wheel" ? this . settings . enableEdgeWheelEvents : this . settings . enableEdgeClickEvents ) { const edge = this . getEdgeAtPoint ( e . x , e . y ) ; if ( edge ) return this . emit ( `${ eventType }Edge` , { ... baseEvent , edge } ) ; } return this . emit ( `${ eventType }Stage` , baseEvent ) ; } ; } ; this . activeListeners . handleClick = createMouseListener ( "click" ) ; this . activeListeners . handleRightClick = createMouseListener ( "rightClick" ) ; this . activeListeners . handleDoubleClick = createMouseListener ( "doubleClick" ) ; this . activeListeners . handleWheel = createMouseListener ( "wheel" ) ; this . activeListeners . handleDown = createMouseListener ( "down" ) ; this . mouseCaptor . on ( "mousemove" , this . activeListeners . handleMove ) ; this . mouseCaptor . on ( "click" , this . activeListeners . handleClick ) ; this . mouseCaptor . on ( "rightClick" , this . activeListeners . handleRightClick ) ; this . mouseCaptor . on ( "doubleClick" , this . activeListeners . handleDoubleClick ) ; this . mouseCaptor . on ( "wheel" , this . activeListeners . handleWheel ) ; this . mouseCaptor . on ( "mousedown" , this . activeListeners . handleDown ) ; return this ; } return { Sigma } private bindGraphHandlers ( ) { const graph = this . graph ; this . activeListeners . graphUpdate = ( ) => { this . needToProcess = true ; this . _scheduleRefresh ( ) ; } ; this . activeListeners . softGraphUpdate = ( ) => { this . needToSoftProcess = true ; this . _scheduleRefresh ( ) ; } ; this . activeListeners . addNodeGraphUpdate = ( e ) : void => { this . nodeKeyToIndex [ e . key ] = graph . order - 1 ; this . activeListeners . graphUpdate ( ) ; } ; this . activeListeners . addEdgeGraphUpdate = ( e ) : void => { this . nodeKeyToIndex [ e . key ] = graph . order - 1 ; this . activeListeners . graphUpdate ( ) ; } ; graph . on ( "nodeAdded" , this . activeListeners . addNodeGraphUpdate ) ; graph . on ( "nodeDropped" , this . activeListeners . graphUpdate ) ; graph . on ( "nodeAttributesUpdated" , this . activeListeners . softGraphUpdate ) ; graph . on ( "eachNodeAttributesUpdated" , this . activeListeners . graphUpdate ) ; graph . on ( "edgeAdded" , this . activeListeners . addEdgeGraphUpdate ) ; graph . on ( "edgeDropped" , this . activeListeners . graphUpdate ) ; graph . on ( "edgeAttributesUpdated" , this . activeListeners . softGraphUpdate ) ; graph . on ( "eachEdgeAttributesUpdated" , this . activeListeners . graphUpdate ) ; graph . on ( "edgesCleared" , this . activeListeners . graphUpdate ) ; graph . on ( "cleared" , this . activeListeners . graphUpdate ) ; return this ; } return { Sigma } private checkEdgeHoverEvents ( mousePosition ) { const edgeToHover = this . hoveredNode ? null : this . getEdgeAtPoint ( mousePosition . x , mousePosition . y ) ; if ( edgeToHover !== this . hoveredEdge ) { if ( this . hoveredEdge ) this . emit ( "leaveEdge" , { edge : this . hoveredEdge } ) ; if ( edgeToHover ) this . emit ( "enterEdge" , { edge : edgeToHover } ) ; this . hoveredEdge = edgeToHover ; } return this ; } /**
     * Method looking for an edge colliding with a given point at (x, y). Returns
     * the key of the edge if any, or null else.
     */ private getEdgeAtPoint ( x , y ) { const { edgeDataCache , nodeDataCache } = this ; if ( ! isPixelColored ( this . webGLContexts . edges , x * PIXEL_RATIO , y * PIXEL_RATIO ) ) return null ; const { x : graphX , y : graphY } = this . viewportToGraph ( { x , y } ) ; let transformationRatio = 0 ; this . graph . someEdge ( ( key , _ , sourceId , targetId , { x : xs , y : ys } , { x : xt , y : yt } ) => { if ( edgeDataCache [ key ] . hidden || nodeDataCache [ sourceId ] . hidden || nodeDataCache [ targetId ] . hidden ) return false ; if ( xs !== xt || ys !== yt ) { const graphLength = Math . sqrt ( Math . pow ( xt - xs , 2 ) + Math . pow ( yt - ys , 2 ) ) ; const { x : vp_xs , y : vp_ys } = this . graphToViewport ( { x : xs , y : ys } ) ; const { x : vp_xt , y : vp_yt } = this . graphToViewport ( { x : xt , y : yt } ) ; const viewportLength = Math . sqrt ( Math . pow ( vp_xt - vp_xs , 2 ) + Math . pow ( vp_yt - vp_ys , 2 ) ) ; transformationRatio = graphLength / viewportLength ; return true ; } } ) ; if ( ! transformationRatio ) return null ; return ( this . graph . findEdge ( ( key , edgeAttributes , _s , _t , sourcePosition , targetPosition ) => { if ( doEdgeCollideWithPoint ( graphX , graphY , sourcePosition . x , sourcePosition . y , targetPosition . x , targetPosition . y , ( edgeDataCache [ key ] . size * transformationRatio ) / this . cameraSizeRatio ) ) { return true ; } } ) || null ) ; } return { Sigma } private process ( keepArrays = false ) { const graph = this . graph ; const settings = this . settings ; const dimensions = this . getDimensions ( ) ; const nodeZExtent = [ Infinity , - Infinity ] ; const edgeZExtent = [ Infinity , - Infinity ] ; this . quadtree . clear ( ) ; this . labelGrid . resizeAndClear ( dimensions , settings . labelGridCellSize ) ; this . highlightedNodes = new Set ( ) ; this . nodeExtent = graphExtent ( graph ) ; this . nodesWithForcedLabels = [ ] ; this . edgesWithForcedLabels = [ ] ; const nullCamera = new Camera ( ) ; const nullCameraMatrix = matrixFromCamera ( nullCamera . getState ( ) , this . getDimensions ( ) , this . getGraphDimensions ( ) , this . getSetting ( "stagePadding" ) || 0 ) ; this . normalizationFunction = createNormalizationFunction ( this . customBBox || this . nodeExtent ) ; const nodesPerPrograms = { } ; let nodes = graph . nodes ( ) ; for ( let i = 0 , l = nodes . length ; i < l ; i ++ ) { const node = nodes [ i ] ; let attr = Object . assign ( { } , graph . getNodeAttributes ( node ) ) ; if ( settings . nodeReducer ) attr = settings . nodeReducer ( node , attr ) ; const data = applyNodeDefaults ( this . settings , node , attr ) ; nodesPerPrograms [ data . type ] = ( nodesPerPrograms [ data . type ] || 0 ) + 1 ; this . nodeDataCache [ node ] = data ; this . normalizationFunction . applyTo ( data ) ; if ( data . forceLabel ) this . nodesWithForcedLabels . push ( node ) ; if ( this . settings . zIndex ) { if ( data . zIndex < nodeZExtent [ 0 ] ) nodeZExtent [ 0 ] = data . zIndex ; if ( data . zIndex > nodeZExtent [ 1 ] ) nodeZExtent [ 1 ] = data . zIndex ; } } for ( const type in this . nodePrograms ) { if ( ! this . nodePrograms . hasOwnProperty ( type ) ) { throw new Error ( `Sigma: could not find a suitable program for node type "${ type }"!` ) ; } if ( ! keepArrays ) this . nodePrograms [ type ] . allocate ( nodesPerPrograms [ type ] || 0 ) ; nodesPerPrograms [ type ] = 0 ; } if ( this . settings . zIndex && nodeZExtent [ 0 ] !== nodeZExtent [ 1 ] ) nodes = zIndexOrdering < string > ( nodeZExtent , ( node ) : number => this . nodeDataCache [ node ] . zIndex , nodes ) ; for ( let i = 0 , l = nodes . length ; i < l ; i ++ ) { const node = nodes [ i ] ; const data = this . nodeDataCache [ node ] ; this . quadtree . add ( node , data . x , 1 - data . y , data . size / this . width ) ; if ( data . label ) this . labelGrid . add ( node , data . size , this . framedGraphToViewport ( data , { matrix : nullCameraMatrix } ) ) ; this . nodePrograms [ data . type ] . process ( data , data . hidden , nodesPerPrograms [ data . type ] ++ ) ; if ( data . highlighted && ! data . hidden ) this . highlightedNodes . add ( node ) ; this . nodeKeyToIndex [ node ] = i ; } this . labelGrid . organize ( ) ; const edgesPerPrograms = { } ; let edges = graph . edges ( ) ; for ( let i = 0 , l = edges . length ; i < l ; i ++ ) { const edge = edges [ i ] ; let attr = Object . assign ( { } , graph . getEdgeAttributes ( edge ) ) ; if ( settings . edgeReducer ) attr = settings . edgeReducer ( edge , attr ) ; const data = applyEdgeDefaults ( this . settings , edge , attr ) ; edgesPerPrograms [ data . type ] = ( edgesPerPrograms [ data . type ] || 0 ) + 1 ; this . edgeDataCache [ edge ] = data ; if ( data . forceLabel && ! data . hidden ) this . edgesWithForcedLabels . push ( edge ) ; if ( this . settings . zIndex ) { if ( data . zIndex < edgeZExtent [ 0 ] ) edgeZExtent [ 0 ] = data . zIndex ; if ( data . zIndex > edgeZExtent [ 1 ] ) edgeZExtent [ 1 ] = data . zIndex ; } } for ( const type in this . edgePrograms ) { if ( ! this . edgePrograms . hasOwnProperty ( type ) ) { throw new Error ( `Sigma: could not find a suitable program for edge type "${ type }"!` ) ; } if ( ! keepArrays ) this . edgePrograms [ type ] . allocate ( edgesPerPrograms [ type ] || 0 ) ; edgesPerPrograms [ type ] = 0 ; } if ( this . settings . zIndex && edgeZExtent [ 0 ] !== edgeZExtent [ 1 ] ) edges = zIndexOrdering ( edgeZExtent , ( edge ) : number => this . edgeDataCache [ edge ] . zIndex , edges ) ; for ( let i = 0 , l = edges . length ; i < l ; i ++ ) { const edge = edges [ i ] ; const data = this . edgeDataCache [ edge ] ; const extremities = graph . extremities ( edge ) , sourceData = this . nodeDataCache [ extremities [ 0 ] ] , targetData = this . nodeDataCache [ extremities [ 1 ] ] ; const hidden = data . hidden || sourceData . hidden || targetData . hidden ; this . edgePrograms [ data . type ] . process ( sourceData , targetData , data , hidden , edgesPerPrograms [ data . type ] ++ ) ; this . nodeKeyToIndex [ edge ] = i ; } for ( const type in this . edgePrograms ) { const program = this . edgePrograms [ type ] ; if ( ! keepArrays && typeof program . computeIndices === "function" ) program . computeIndices ( ) ; } return this ; } private private handleSettingsUpdate ( ) { this . camera . minRatio = this . settings . minCameraRatio ; this . camera . maxRatio = this . settings . maxCameraRatio ; this . camera . setState ( this . camera . validateState ( this . camera . getState ( ) ) ) ; return this ; } return { Sigma } private _refresh ( ) { if ( this . needToProcess ) { this . process ( ) ; } else if ( this . needToSoftProcess ) { this . process ( true ) ; } this . needToProcess = false ; this . needToSoftProcess = false ; this . render ( ) ; return this ; } return { Sigma } private _scheduleRefresh ( ) { if ( ! this . renderFrame ) { this . renderFrame = requestFrame ( ( ) => { this . _refresh ( ) ; this . renderFrame = null ; } ) ; } return this ; } return { Sigma } private renderLabels ( ) { if ( ! this . settings . renderLabels ) return this ; const cameraState = this . camera . getState ( ) ; let visibleNodes ; if ( cameraState . ratio >= 1 ) { visibleNodes = new Set ( this . graph . nodes ( ) ) ; } else { const viewRectangle = this . viewRectangle ( ) ; visibleNodes = new Set ( this . quadtree . rectangle ( viewRectangle . x1 , 1 - viewRectangle . y1 , viewRectangle . x2 , 1 - viewRectangle . y2 , viewRectangle . height ) ) ; } const labelsToDisplay = this . labelGrid . getLabelsToDisplay ( cameraState . ratio , this . settings . labelDensity ) . concat ( this . nodesWithForcedLabels ) ; this . displayedLabels = new Set ( ) ; const context = this . canvasContexts . labels ; for ( let i = 0 , l = labelsToDisplay . length ; i < l ; i ++ ) { const node = labelsToDisplay [ i ] ; const data = this . nodeDataCache [ node ] ; if ( this . displayedLabels . has ( node ) ) continue ; if ( data . hidden ) continue ; const { x , y } = this . framedGraphToViewport ( data ) ; const size = this . scaleSize ( data . size ) ; if ( ! data . forceLabel && size < this . settings . labelRenderedSizeThreshold ) continue ; if ( ! visibleNodes . has ( node ) ) continue ; this . displayedLabels . add ( node ) ; this . settings . labelRenderer ( context , { key : node , ... data , size , x , y , } , this . settings ) ; } return this ; } return { Sigma } private renderEdgeLabels ( ) { if ( ! this . settings . renderEdgeLabels ) return this ; const context = this . canvasContexts . edgeLabels ; context . clearRect ( 0 , 0 , this . width , this . height ) ; const edgeLabelsToDisplay = edgeLabelsToDisplayFromNodes ( { graph : this . graph , hoveredNode : this . hoveredNode , displayedNodeLabels : this . displayedLabels , highlightedNodes : this . highlightedNodes , } ) . concat ( this . edgesWithForcedLabels ) ; const displayedLabels = new Set < string > ( ) ; for ( let i = 0 , l = edgeLabelsToDisplay . length ; i < l ; i ++ ) { const edge = edgeLabelsToDisplay [ i ] , extremities = this . graph . extremities ( edge ) , sourceData = this . nodeDataCache [ extremities [ 0 ] ] , targetData = this . nodeDataCache [ extremities [ 1 ] ] , edgeData = this . edgeDataCache [ edge ] ; if ( displayedLabels . has ( edge ) ) continue ; if ( edgeData . hidden || sourceData . hidden || targetData . hidden ) { continue ; } this . settings . edgeLabelRenderer ( context , { key : edge , ... edgeData , size : this . scaleSize ( edgeData . size ) , } , { key : extremities [ 0 ] , ... sourceData , ... this . framedGraphToViewport ( sourceData ) , size : this . scaleSize ( sourceData . size ) , } , { key : extremities [ 1 ] , ... targetData , ... this . framedGraphToViewport ( targetData ) , size : this . scaleSize ( targetData . size ) , } , this . settings ) ; displayedLabels . add ( edge ) ; } return this ; } return { Sigma } private renderHighlightedNodes ( ) { const context = this . canvasContexts . hovers ; context . clearRect ( 0 , 0 , this . width , this . height ) ; const render = ( node ) : void => { const data = this . nodeDataCache [ node ] ; const { x , y } = this . framedGraphToViewport ( data ) ; const size = this . scaleSize ( data . size ) ; this . settings . hoverRenderer ( context , { key : node , ... data , size , x , y , } , this . settings ) ; } ; const nodesToRender = [ ] ; if ( this . hoveredNode && ! this . nodeDataCache [ this . hoveredNode ] . hidden ) { nodesToRender . push ( this . hoveredNode ) ; } this . highlightedNodes . forEach ( ( node ) => { if ( node !== this . hoveredNode ) nodesToRender . push ( node ) ; } ) ; nodesToRender . forEach ( ( node ) => render ( node ) ) ; const nodesPerPrograms = { } ; nodesToRender . forEach ( ( node ) => { const type = this . nodeDataCache [ node ] . type ; nodesPerPrograms [ type ] = ( nodesPerPrograms [ type ] || 0 ) + 1 ; } ) ; for ( const type in this . hoverNodePrograms ) { this . hoverNodePrograms [ type ] . allocate ( nodesPerPrograms [ type ] || 0 ) ; nodesPerPrograms [ type ] = 0 ; } nodesToRender . forEach ( ( node ) => { const data = this . nodeDataCache [ node ] ; this . hoverNodePrograms [ data . type ] . process ( data , data . hidden , nodesPerPrograms [ data . type ] ++ ) ; } ) ; for ( const type in this . hoverNodePrograms ) { const program = this . hoverNodePrograms [ type ] ; program . bind ( ) ; program . bufferData ( ) ; program . render ( { matrix : this . matrix , width : this . width , height : this . height , ratio : this . camera . ratio , correctionRatio : this . correctionRatio / this . camera . ratio , scalingRatio : PIXEL_RATIO , } ) ; } } /**
     * Method used to schedule a hover render.
     *
     */ private scheduleHighlightedNodesRender ( ) { if ( this . renderHighlightedNodesFrame || this . renderFrame ) return ; this . renderHighlightedNodesFrame = requestFrame ( ( ) => { this . renderHighlightedNodesFrame = null ; this . renderHighlightedNodes ( ) ; this . renderEdgeLabels ( ) ; } ) ; } return { Sigma } private render ( ) { if ( this . renderFrame ) { cancelFrame ( this . renderFrame ) ; this . renderFrame = null ; this . needToProcess = false ; this . needToSoftProcess = false ; } this . resize ( ) ; this . clear ( ) ; this . updateCachedValues ( ) ; if ( ! this . graph . order ) return this ; const mouseCaptor = this . mouseCaptor ; const moving = this . camera . isAnimated ( ) || mouseCaptor . isMoving || mouseCaptor . draggedEvents || mouseCaptor . currentWheelDirection ; const cameraState = this . camera . getState ( ) ; const viewportDimensions = this . getDimensions ( ) ; const graphDimensions = this . getGraphDimensions ( ) ; const padding = this . getSetting ( "stagePadding" ) || 0 ; this . matrix = matrixFromCamera ( cameraState , viewportDimensions , graphDimensions , padding ) ; this . invMatrix = matrixFromCamera ( cameraState , viewportDimensions , graphDimensions , padding , true ) ; this . correctionRatio = getMatrixImpact ( this . matrix , cameraState , viewportDimensions ) ; for ( const type in this . nodePrograms ) { const program = this . nodePrograms [ type ] ; program . bind ( ) ; program . bufferData ( ) ; program . render ( { matrix : this . matrix , width : this . width , height : this . height , ratio : cameraState . ratio , correctionRatio : this . correctionRatio / cameraState . ratio , scalingRatio : PIXEL_RATIO , } ) ; } if ( ! this . settings . hideEdgesOnMove || ! moving ) { for ( const type in this . edgePrograms ) { const program = this . edgePrograms [ type ] ; program . bind ( ) ; program . bufferData ( ) ; program . render ( { matrix : this . matrix , width : this . width , height : this . height , ratio : cameraState . ratio , correctionRatio : this . correctionRatio / cameraState . ratio , scalingRatio : PIXEL_RATIO , } ) ; } } if ( this . settings . hideLabelsOnMove && moving ) return this ; this . renderLabels ( ) ; this . renderEdgeLabels ( ) ; this . renderHighlightedNodes ( ) ; this . emit ( "afterRender" ) ; return this ; } /**
     * Internal method used to update expensive and therefore cached values
     * each time the camera state is updated.
     */ private updateCachedValues ( ) { const { ratio } = this . camera . getState ( ) ; this . cameraSizeRatio = Math . sqrt ( ratio ) ; } /**---------------------------------------------------------------------------
     * Public API.
     **---------------------------------------------------------------------------
     */ return { Camera } getCamera ( ) { return this . camera ; } return { Graph } getGraph ( ) { return this . graph ; } return { MouseCaptor } getMouseCaptor ( ) { return this . mouseCaptor ; } return { TouchCaptor } getTouchCaptor ( ) { return this . touchCaptor ; } return { Dimensions } getDimensions ( ) { return { width : this . width , height : this . height } ; } return { Dimensions } getGraphDimensions ( ) { const extent = this . customBBox || this . nodeExtent ; return { width : extent . x [ 1 ] - extent . x [ 0 ] || 1 , height : extent . y [ 1 ] - extent . y [ 0 ] || 1 , } ; } param { string } key return { NodeDisplayData | undefined } getNodeDisplayData ( key ) { const node = this . nodeDataCache [ key as string ] ; return node ? Object . assign ( { } , node ) : undefined ; } param { string } key return { EdgeDisplayData | undefined } getEdgeDisplayData ( key ) { const edge = this . edgeDataCache [ key as string ] ; return edge ? Object . assign ( { } , edge ) : undefined ; } return { Settings } getSettings ( ) { return { ... this . settings } ; } param { string } key return { any } getSetting < K extends keyof Settings > ( key ) { return this . settings [ key ] ; } param { string } key param { any } value return { Sigma } setSetting < K extends keyof Settings > ( key , value ) { this . settings [ key ] = value ; validateSettings ( this . settings ) ; this . handleSettingsUpdate ( ) ; this . needToProcess = true ; this . _scheduleRefresh ( ) ; return this ; } param { string } key param { function } updater return { Sigma } updateSetting < K extends keyof Settings > ( key , updater ) { this . settings [ key ] = updater ( this . settings [ key ] ) ; validateSettings ( this . settings ) ; this . handleSettingsUpdate ( ) ; this . needToProcess = true ; this . _scheduleRefresh ( ) ; return this ; } return { Sigma } resize ( ) { const previousWidth = this . width , previousHeight = this . height ; this . width = this . container . offsetWidth ; this . height = this . container . offsetHeight ; if ( this . width === 0 ) throw new Error ( "Sigma: container has no width." ) ; if ( this . height === 0 ) throw new Error ( "Sigma: container has no height." ) ; if ( previousWidth === this . width && previousHeight === this . height ) return this ; for ( const id in this . elements ) { const element = this . elements [ id ] ; element . style . width = this . width + "px" ; element . style . height = this . height + "px" ; } for ( const id in this . canvasContexts ) { this . elements [ id ] . setAttribute ( "width" , this . width * PIXEL_RATIO + "px" ) ; this . elements [ id ] . setAttribute ( "height" , this . height * PIXEL_RATIO + "px" ) ; if ( PIXEL_RATIO !== 1 ) this . canvasContexts [ id ] . scale ( PIXEL_RATIO , PIXEL_RATIO ) ; } for ( const id in this . webGLContexts ) { this . elements [ id ] . setAttribute ( "width" , this . width * PIXEL_RATIO + "px" ) ; this . elements [ id ] . setAttribute ( "height" , this . height * PIXEL_RATIO + "px" ) ; this . webGLContexts [ id ] . viewport ( 0 , 0 , this . width * PIXEL_RATIO , this . height * PIXEL_RATIO ) ; } return this ; } return { Sigma } clear ( ) { this . webGLContexts . nodes . clear ( this . webGLContexts . nodes . COLOR_BUFFER_BIT ) ; this . webGLContexts . edges . clear ( this . webGLContexts . edges . COLOR_BUFFER_BIT ) ; this . webGLContexts . hoverNodes . clear ( this . webGLContexts . nodes . COLOR_BUFFER_BIT ) ; this . canvasContexts . labels . clearRect ( 0 , 0 , this . width , this . height ) ; this . canvasContexts . hovers . clearRect ( 0 , 0 , this . width , this . height ) ; this . canvasContexts . edgeLabels . clearRect ( 0 , 0 , this . width , this . height ) ; return this ; } return { Sigma } refresh ( ) { this . needToProcess = true ; this . _refresh ( ) ; return this ; } return { Sigma } scheduleRefresh ( ) { this . needToProcess = true ; this . _scheduleRefresh ( ) ; return this ; } param viewportTarget param newRatio return { CameraState } getViewportZoomedState ( viewportTarget , newRatio ) { const { ratio , angle , x , y } = this . camera . getState ( ) ; const ratioDiff = newRatio / ratio ; const center = { x : this . width / 2 , y : this . height / 2 , } ; const graphMousePosition = this . viewportToFramedGraph ( viewportTarget ) ; const graphCenterPosition = this . viewportToFramedGraph ( center ) ; return { angle , x : ( graphMousePosition . x - graphCenterPosition . x ) * ( 1 - ratioDiff ) + x , y : ( graphMousePosition . y - graphCenterPosition . y ) * ( 1 - ratioDiff ) + y , ratio : newRatio , } ; } return { object } viewRectangle ( ) { const marginX = ( 0 * this . width ) / 8 , marginY = ( 0 * this . height ) / 8 ; const p1 = this . viewportToFramedGraph ( { x : 0 - marginX , y : 0 - marginY } ) , p2 = this . viewportToFramedGraph ( { x : this . width + marginX , y : 0 - marginY } ) , h = this . viewportToFramedGraph ( { x : 0 , y : this . height + marginY } ) ; return { x1 : p1 . x , y1 : p1 . y , x2 : p2 . x , y2 : p2 . y , height : p2 . y - h . y , } ; } /**
     * Method returning the coordinates of a point from the framed graph system to the viewport system. It allows
     * overriding anything that is used to get the translation matrix, or even the matrix itself.
     *
     * Be careful if overriding dimensions, padding or cameraState, as the computation of the matrix is not the lightest
     * of computations.
     */ framedGraphToViewport ( coordinates , override = { } ) { const recomputeMatrix = ! ! override . cameraState || ! ! override . viewportDimensions || ! ! override . graphDimensions ; const matrix = override . matrix ? override . matrix : recomputeMatrix ? matrixFromCamera ( override . cameraState || this . camera . getState ( ) , override . viewportDimensions || this . getDimensions ( ) , override . graphDimensions || this . getGraphDimensions ( ) , override . padding || this . getSetting ( "stagePadding" ) || 0 ) : this . matrix ; const framedGraphVec = [ coordinates . x , coordinates . y , 1 ] ; const viewportVec = multiplyVec ( matrix , framedGraphVec ) ; return { x : ( ( 1 + viewportVec [ 0 ] ) * this . width ) / 2 , y : ( ( 1 - viewportVec [ 1 ] ) * this . height ) / 2 , } ; } /**
     * Method returning the coordinates of a point from the viewport system to the framed graph system. It allows
     * overriding anything that is used to get the translation matrix, or even the matrix itself.
     *
     * Be careful if overriding dimensions, padding or cameraState, as the computation of the matrix is not the lightest
     * of computations.
     */ viewportToFramedGraph ( coordinates , override = { } ) { const recomputeMatrix = ! ! override . cameraState || ! ! override . viewportDimensions || ! override . graphDimensions ; const invMatrix = override . matrix ? override . matrix : recomputeMatrix ? matrixFromCamera ( override . cameraState || this . camera . getState ( ) , override . viewportDimensions || this . getDimensions ( ) , override . graphDimensions || this . getGraphDimensions ( ) , override . padding || this . getSetting ( "stagePadding" ) || 0 , true ) : this . invMatrix ; const viewportVec = [ ( coordinates . x / this . width ) * 2 - 1 , 1 - ( coordinates . y / this . height ) * 2 , 1 ] ; const framedGraphVec = multiplyVec ( invMatrix , viewportVec ) ; return { x : framedGraphVec [ 0 ] , y : framedGraphVec [ 1 ] , } ; } param { Coordinates } viewportPoint viewportToGraph ( viewportPoint , override = { } ) { return this . normalizationFunction . inverse ( this . viewportToFramedGraph ( viewportPoint , override ) ) ; } param { Coordinates } graphPoint graphToViewport ( graphPoint , override = { } ) { return this . framedGraphToViewport ( this . normalizationFunction ( graphPoint ) , override ) ; } return { { x : Extent , y : Extent } } getBBox ( ) { return graphExtent ( this . graph ) ; } return { { x : Extent , y : Extent } | null } getCustomBBox ( ) { return this . customBBox ; } return { Sigma } setCustomBBox ( customBBox ) { this . customBBox = customBBox ; this . _scheduleRefresh ( ) ; return this ; } return { undefined } kill ( ) { const graph = this . graph ; this . emit ( "kill" ) ; this . removeAllListeners ( ) ; this . camera . removeListener ( "updated" , this . activeListeners . camera ) ; window . removeEventListener ( "resize" , this . activeListeners . handleResize ) ; this . mouseCaptor . kill ( ) ; this . touchCaptor . kill ( ) ; graph . removeListener ( "nodeAdded" , this . activeListeners . addNodeGraphUpdate ) ; graph . removeListener ( "nodeDropped" , this . activeListeners . graphUpdate ) ; graph . removeListener ( "nodeAttributesUpdated" , this . activeListeners . softGraphUpdate ) ; graph . removeListener ( "eachNodeAttributesUpdated" , this . activeListeners . graphUpdate ) ; graph . removeListener ( "edgeAdded" , this . activeListeners . addEdgeGraphUpdate ) ; graph . removeListener ( "edgeDropped" , this . activeListeners . graphUpdate ) ; graph . removeListener ( "edgeAttributesUpdated" , this . activeListeners . softGraphUpdate ) ; graph . removeListener ( "eachEdgeAttributesUpdated" , this . activeListeners . graphUpdate ) ; graph . removeListener ( "edgesCleared" , this . activeListeners . graphUpdate ) ; graph . removeListener ( "cleared" , this . activeListeners . graphUpdate ) ; this . quadtree = new QuadTree ( ) ; this . nodeDataCache = { } ; this . edgeDataCache = { } ; this . nodesWithForcedLabels = [ ] ; this . edgesWithForcedLabels = [ ] ; this . highlightedNodes . clear ( ) ; if ( this . renderFrame ) { cancelFrame ( this . renderFrame ) ; this . renderFrame = null ; } if ( this . renderHighlightedNodesFrame ) { cancelFrame ( this . renderHighlightedNodesFrame ) ; this . renderHighlightedNodesFrame = null ; } const container = this . container ; while ( container . firstChild ) container . removeChild ( container . firstChild ) ; } param { number } size return { number } scaleSize ( size ) { return size / this . cameraSizeRatio ; } return { PlainObject < HTMLCanvasElement > } getCanvases ( ) { return { ... this . elements } ; } }