From 60ccc422316b90e4366d559710eaa82aa5560427 Mon Sep 17 00:00:00 2001 From: Benoit Simard Date: Fri, 15 Sep 2023 16:20:08 +0200 Subject: [PATCH] Adding rePaint function Function rePaint updates all dataCache, update the programs data and call a render. It bypass all actiosn related to nodes coordinates (like quadtree, labelGrid). Moreover, you can specify a list of nodes/edges to repaint. --- src/sigma.ts | 229 +++++++++++++++++++++++++++++++++++---------------- src/types.ts | 8 ++ 2 files changed, 167 insertions(+), 70 deletions(-) diff --git a/src/sigma.ts b/src/sigma.ts index 7573d7164..576586275 100644 --- a/src/sigma.ts +++ b/src/sigma.ts @@ -14,10 +14,12 @@ import { Coordinates, Dimensions, EdgeDisplayData, + EdgeCacheData, Extent, Listener, MouseCoords, NodeDisplayData, + NodeCacheData, PlainObject, CoordinateConversionOverride, TypedEventEmitter, @@ -166,8 +168,8 @@ export default class Sigma extends TypedEventEm // indices related to graph data private quadtree: QuadTree = new QuadTree(); private labelGrid: LabelGrid = new LabelGrid(); - private nodeDataCache: Record = {}; - private edgeDataCache: Record = {}; + private nodeDataCache: Record = {}; + private edgeDataCache: Record = {}; private nodesWithForcedLabels: string[] = []; private edgesWithForcedLabels: string[] = []; private nodeExtent: { x: Extent; y: Extent } = { x: [0, 1], y: [0, 1] }; @@ -564,24 +566,26 @@ export default class Sigma extends TypedEventEm private bindGraphHandlers(): this { const graph = this.graph; - this.activeListeners.eachNodeAttributesUpdatedGraphUpdate = () => { - // we clear node's indices - this.clearNodeIndices(); - const nodes = this.graph.nodes(); + this.activeListeners.eachNodeAttributesUpdatedGraphUpdate = (e: { hints?: { attributes?: string[] } }) => { + const updatedFields = e.hints?.attributes; // we process all nodes - nodes.forEach((node) => this.nodeAdd(node)); - // schedule a render - this.scheduleRefresh(); + const nodes = this.graph.nodes(); + nodes.forEach((node) => this.nodeUpdate(node)); + + // if coord, size, type or zIndex have changed, we need to schedule a render + // (size is needed in the quadtree and zIndex for the programIndex) + if (updatedFields && ["x", "y", "size", "zIndex", "type"].some((f) => updatedFields?.includes(f))) + this.scheduleRefresh(); + else this.rePaint({ nodes: graph.nodes() }); }; - this.activeListeners.eachEdgeAttributesUpdatedGraphUpdate = () => { - // we clear edge's indices - this.clearEdgeIndices(); + this.activeListeners.eachEdgeAttributesUpdatedGraphUpdate = (e: { hints?: { attributes?: string[] } }) => { + const updatedFields = e.hints?.attributes; // we process all edges const edges = this.graph.edges(); - edges.forEach((edge) => this.edgeAdd(edge)); - // schedule a render - this.scheduleRefresh(); + edges.forEach((edge) => this.edgeUpdate(edge)); + if (updatedFields && ["zIndex", "type"].some((f) => updatedFields?.includes(f))) this.scheduleRefresh(); + else this.rePaint({ edges: graph.edges() }); }; // On add node, we add the node in indices and then call for a render @@ -599,6 +603,7 @@ export default class Sigma extends TypedEventEm // we process the node this.nodeUpdate(node); // schedule a render for the node + // TODO: If we do a diff to know which fields has been updated, we will know if a repaint is possible this.scheduleRefresh(); }; @@ -625,8 +630,9 @@ export default class Sigma extends TypedEventEm const edge = payload.key; // we process the edge this.edgeUpdate(edge); - // schedule a render for the edge - this.scheduleRefresh(); + // schedule a repaint for the edge + // TODO: If we do a diff to know which fields has been updated, we will know if a repaint is possible + this.rePaint({ edges: [edge] }); }; // On drop edge, we remove the edge from indices and then call for a refresh @@ -828,7 +834,7 @@ export default class Sigma extends TypedEventEm const node = nodes[i]; const data = this.nodeDataCache[node]; - // get initial coordinates + // Get initial coordinates const attrs = graph.getNodeAttributes(node); data.x = attrs.x; data.y = attrs.y; @@ -868,10 +874,7 @@ export default class Sigma extends TypedEventEm for (let i = 0, l = nodes.length; i < l; i++) { const node = nodes[i]; const data = this.nodeDataCache[node]; - - const nodeProgram = this.nodePrograms[data.type]; - if (!nodeProgram) throw new Error(`Sigma: could not find a suitable program for node type "${data.type}"!`); - nodeProgram.process(nodesPerPrograms[data.type]++, data); + this.addNodeToProgram(node, nodesPerPrograms[data.type]++); } // @@ -909,12 +912,7 @@ export default class Sigma extends TypedEventEm 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]]; - - this.edgePrograms[data.type].process(edgesPerPrograms[data.type]++, sourceData, targetData, data); + this.addEdgeToProgram(edge, edgesPerPrograms[data.type]++); } return this; @@ -1564,7 +1562,6 @@ export default class Sigma extends TypedEventEm */ refresh(): this { // Re-index graph data - this.nodeExtent = graphExtent(this.graph); this.clearEdgeIndices(); this.clearNodeIndices(); this.graph.forEachNode((node) => this.nodeAdd(node)); @@ -1572,11 +1569,53 @@ export default class Sigma extends TypedEventEm // Call the render this.needToProcess = true; + this.render(); return this; } + /** + * Method used to re paint some nodes & edges. + * + * This function recall the node/edge reducers, replace the newly computed data + * to the program. + * It can be usefull when you need to change the display of the graph, like for example + * when you hovered a node. + * Performances are better than a refresh, because it doesn't recompute indices related + * to node's position (ie. quadtree, labelgrid, ...) + * + * @return {Sigma} + */ + rePaint(graph: { nodes?: string[]; edges?: string[] }): this { + if (graph.nodes) { + for (let i = 0, l = graph.nodes.length; i < l; i++) { + const node = graph.nodes[i]; + // Recompute node's data (ie. apply reducer) + this.nodeUpdate(node); + // Add node to the program + const data = this.nodeDataCache[node]; + if (data.programIndex === undefined) throw new Error(`Sigma: node "${node}" can't be repaint`); + this.addNodeToProgram(node, data.programIndex); + } + } + if (graph.edges) { + for (let i = 0, l = graph.edges.length; i < l; i++) { + const edge = graph.edges[i]; + // Recompute edge's data (ie. apply reducer) + this.edgeUpdate(edge); + // Add edge to the program + const data = this.edgeDataCache[edge]; + if (data.programIndex === undefined) throw new Error(`Sigma: edge "${edge}" can't be repaint`); + this.addEdgeToProgram(edge, data.programIndex); + } + } + + this.scheduleRender(); + + return this; + } + /** * Method used to schedule a render at the next available frame. * This method can be safely called on a same frame because it basically @@ -1880,59 +1919,56 @@ export default class Sigma extends TypedEventEm /** * Add a node in the internal data structures. - * @returns true if the node changed the extents of the graph, so if we need to reprocess the whole graph. - * - * Node display data resolution: - * 1. First we get the node's attributes - * 2. We optionally reduce them using the function provided by the user - * Note that this function must return a total object and won't be merged - * 3. We apply our defaults, while running some vital checks - * 4. We apply the normalization function - */ - private nodeAdd(key: string): boolean { - // Node cache + * @private + * @param key The node's graphology ID + */ + private nodeAdd(key: string): void { + // Node display data resolution: + // 1. First we get the node's attributes + // 2. We optionally reduce them using the function provided by the user + // Note that this function must return a total object and won't be merged + // 3. We apply our defaults, while running some vital checks + // 4. We apply the normalization function // We shallow copy node data to avoid dangerous behaviors from reducers let attr = Object.assign({}, this.graph.getNodeAttributes(key)); if (this.settings.nodeReducer) attr = this.settings.nodeReducer(key, attr); const data = applyNodeDefaults(this.settings, key, attr); this.nodeDataCache[key] = data; + // Label + // We also check that the key is not already in + // because this function is also used for update + if (data.forceLabel && !this.nodesWithForcedLabels.includes(key)) this.nodesWithForcedLabels.push(key); + // Highlighted if (data.highlighted && !data.hidden) this.highlightedNodes.add(key); - // Label - if (data.forceLabel) this.nodesWithForcedLabels.push(key); // zIndex if (this.settings.zIndex) { if (data.zIndex < this.nodeZExtent[0]) this.nodeZExtent[0] = data.zIndex; if (data.zIndex > this.nodeZExtent[1]) this.nodeZExtent[1] = data.zIndex; } - - if ( - data.x < this.nodeExtent.x[0] || - data.x > this.nodeExtent.x[1] || - data.y < this.nodeExtent.x[0] || - data.y > this.nodeExtent.y[1] - ) { - return true; - } - - // this.normalizationFunction.applyTo(data); - // this.quadtree.add(key, data.x, data.y, data.size); - - return false; } /** * Update a node the internal data structures. + * @private + * @param key The node's graphology ID */ - private nodeUpdate(key: string): boolean { - return this.nodeAdd(key); + private nodeUpdate(key: string): void { + const prevValue = Object.assign({}, this.nodeDataCache[key]); + + this.nodeAdd(key); + + // Restore some values + const data = this.nodeDataCache[key]; + this.normalizationFunction.applyTo(data); + data.programIndex = prevValue.programIndex; } /** * Remove a node from the internal data structures. - * + * @private * @param key The node's graphology ID */ private nodeRemove(key: string): void { @@ -1948,15 +1984,15 @@ export default class Sigma extends TypedEventEm /** * Add an edge into the internal data structures. - * - * Edge display data resolution: - * 1. First we get the edge's attributes - * 2. We optionally reduce them using the function provided by the user - * Note that this function must return a total object and won't be merged - * 3. We apply our defaults, while running some vital checks + * @private + * @param key The edge's graphology ID */ private edgeAdd(key: string): void { - // Cache data + // Edge display data resolution: + // 1. First we get the edge's attributes + // 2. We optionally reduce them using the function provided by the user + // 3. Note that this function must return a total object and won't be merged + // 4. We apply our defaults, while running some vital checks // We shallow copy edge data to avoid dangerous behaviors from reducers let attr = Object.assign({}, this.graph.getEdgeAttributes(key)); if (this.settings.edgeReducer) attr = this.settings.edgeReducer(key, attr); @@ -1964,8 +2000,12 @@ export default class Sigma extends TypedEventEm this.edgeDataCache[key] = data; // Forced label - if (data.forceLabel && !data.hidden) this.edgesWithForcedLabels.push(key); + // We also check that the key is not already in + // because this function is also used for update + if (data.forceLabel && !data.hidden && !this.edgesWithForcedLabels.includes(key)) + this.edgesWithForcedLabels.push(key); + // Check zIndex if (this.settings.zIndex) { if (data.zIndex < this.edgeZExtent[0]) this.edgeZExtent[0] = data.zIndex; if (data.zIndex > this.edgeZExtent[1]) this.edgeZExtent[1] = data.zIndex; @@ -1973,16 +2013,25 @@ export default class Sigma extends TypedEventEm } /** - * Update an edge in the internal data structures + * Update an edge in the internal data structures. + * @private + * @param key The edge's graphology ID */ private edgeUpdate(key: string): void { + // Keep previous value + const prevValue = Object.assign({}, this.edgeDataCache[key]); + this.edgeRemove(key); this.edgeAdd(key); + + // Restore some values + const data = this.edgeDataCache[key]; + data.programIndex = prevValue.programIndex; } /** * Remove an edge from the internal data structures. - * + * @private * @param key The edge's graphology ID */ private edgeRemove(key: string): void { @@ -1996,18 +2045,21 @@ export default class Sigma extends TypedEventEm /** * Clear all indices related to nodes. + * @private */ private clearNodeIndices(): void { + // Quadtree, labelGrid & nodeExtent are only manage/populated in the process function this.quadtree = new QuadTree(); this.labelGrid = new LabelGrid(); + this.nodeExtent = { x: [0, 1], y: [0, 1] }; this.nodeDataCache = {}; this.nodesWithForcedLabels = []; - this.nodeExtent = { x: [0, 1], y: [0, 1] }; this.nodeZExtent = [Infinity, -Infinity]; } /** * Clear all indices related to edges. + * @private */ private clearEdgeIndices(): void { this.edgeDataCache = {}; @@ -2017,6 +2069,7 @@ export default class Sigma extends TypedEventEm /** * Clear all indices. + * @private */ private clearIndices(): void { this.clearEdgeIndices(); @@ -2025,6 +2078,7 @@ export default class Sigma extends TypedEventEm /** * Clear all graph state related to nodes. + * @private */ private clearNodeState(): void { this.displayedNodeLabels = new Set(); @@ -2034,6 +2088,7 @@ export default class Sigma extends TypedEventEm /** * Clear all graph state related to edges. + * @private */ private clearEdgeState(): void { this.displayedEdgeLabels = new Set(); @@ -2043,9 +2098,43 @@ export default class Sigma extends TypedEventEm /** * Clear all graph state. + * @private */ private clearState(): void { this.clearEdgeState(); this.clearNodeState(); } + + /** + * Add the node data to its program. + * @private + * @param node The node's graphology ID + * @param index The index where to place the edge in the program + */ + private addNodeToProgram(node: string, index: number): void { + const data = this.nodeDataCache[node]; + const nodeProgram = this.nodePrograms[data.type]; + if (!nodeProgram) throw new Error(`Sigma: could not find a suitable program for node type "${data.type}"!`); + nodeProgram.process(index, data); + // Saving program index in nodeDataCache + data.programIndex = index; + } + + /** + * Add the edge data to its program. + * @private + * @param edge The edge's graphology ID + * @param index The index where to place the edge in the program + */ + private addEdgeToProgram(edge: string, index: number): void { + const data = this.edgeDataCache[edge]; + const edgeProgram = this.edgePrograms[data.type]; + if (!edgeProgram) throw new Error(`Sigma: could not find a suitable program for edge type "${data.type}"!`); + const extremities = this.graph.extremities(edge), + sourceData = this.nodeDataCache[extremities[0]], + targetData = this.nodeDataCache[extremities[1]]; + edgeProgram.process(index, sourceData, targetData, data); + // Saving program index in edgeDataCache + data.programIndex = index; + } } diff --git a/src/types.ts b/src/types.ts index f6c97a1f3..61feaf186 100644 --- a/src/types.ts +++ b/src/types.ts @@ -69,8 +69,16 @@ export interface NodeDisplayData extends Coordinates, DisplayData { highlighted: boolean; } +export interface NodeCacheData extends NodeDisplayData { + programIndex?: number; +} + export interface EdgeDisplayData extends DisplayData {} +export interface EdgeCacheData extends EdgeDisplayData { + programIndex?: number; +} + export type CoordinateConversionOverride = { cameraState?: CameraState; matrix?: Float32Array;