Unified Multi-Engine Graph Physics synchronizes hover tooltips, 60fps drag adaptation, dynamic hop z-ordering, and 750ms sine transitions across five heterogeneous web renderers, spanning D3.js SVG [BOH11], vis-network Canvas, Cytoscape.js Canvas [Fra16], Sigma.js WebGL [JVHB14], and Three.js 3d-force-graph, by decoupling visual tokens in graph-payload.ts and Preact overlays in vis-tooltip.tsx from engine-specific draw loops.
| Graph Engine | Render Pipeline | Live Drag Solver (force / radial & columns) | Z-Order Strategy |
|---|---|---|---|
d3/engine.ts | Retained SVG DOM [BOH11] | Native d3.forceSimulation alphaTarget(0.3) / live recomputeSlotTargets(true) | .sort() on SVG <line> & <g> DOM nodes |
vis-network/engine.ts | 2D HTML5 Canvas | Live forceAtlas2Based simulation / cubic-eased animateTo() slot interpolation | Sorting network.body.edgeIndices & nodeIndices |
cytoscape/engine.ts | 2D HTML5 Canvas [Fra16] | Auxiliary d3.forceSimulation coupled via cy.batch() / live animateTo() slots | Stylesheet z-index mapper on displayZIndex |
sigma/engine.ts | 2D WebGL Shaders | Per-frame forceAtlas2.assign [JVHB14] + 8% centroid shift / animateTo() | Graphology zIndex attributes (zIndex: true) |
3d-force-graph/engine.ts | Three.js WebGL | d3ReheatSimulation() on unpinned nodes / 3D cubic-eased animateTo() | True 3D WebGL depth buffer + volume scaling |
This five-engine matrix allows direct empirical comparison of SVG, HTML5 Canvas, and WebGL rendering pipelines under identical interaction physics.
How Do We Unify Hover Tooltips and Styling Tokens Across Five Renderers?
We unify visual identity across all five engines by suppressing cluttered on-canvas text labels globally, reading centralized radii, stroke widths, and domain palettes from getGraphStyleTokens in graph-payload.ts, and mounting a shared Preact hover card via createGraphTooltipController in vis-tooltip.tsx with deterministic viewport boundary clamping across every interactive graph container [BOH11] [Fra16].
Rendering permanent text labels next to 100+ nodes turns dense knowledge graphs into illegible typographic noise, whereas native tooltip implementations diverge wildly across SVG, Canvas, and WebGL. By setting label: '' across D3, Vis.js, and Cytoscape, renderLabels: false in Sigma, and .nodeLabel(() => '') in 3d-force-graph, we delegate all metadata inspection to vis-tooltip.tsx. In 3d-force-graph/engine.ts, we explicitly mount createGraphTooltipController(container) after invoking ForceGraph3D()(container) so Kapsule’s internal DOM initialization does not wipe the overlay element, and fall back to graph2ScreenCoords(x, y, z) whenever pointer capture precedes cursor movement.
How Do We Achieve 60fps Drag Adaptation Without Camera Jumps?
Achieving 60fps drag adaptation without disorienting camera jumps requires coupling pinned node coordinates to live force simulations in force mode while gently recentering the graph centroid instead of invoking fit(), dynamically re-sorting angular and linear slots during radial and columns drags, and enforcing touch-action: pan-y on all 2D canvases [BOH11] [JVHB14].
Because Cytoscape’s initial cose layout is static and Sigma’s WebGL renderer lacks a built-in drag spring loop, we drive live force dragging in cytoscape/engine.ts via an auxiliary d3.forceSimulation flushed inside cy.batch() and in sigma/engine.ts by running two forceAtlas2.assign iterations per animation frame while locking the camera (setCustomBBox) and shifting unpinned nodes by 8% of mean centroid drift. In radial and columns modes, dragging a node recomputes normalized polar angles or column coordinates (useCurrentOrder = true) and smoothly animates peer nodes into newly vacated slots when the ordering signature changes:
// src/content/visualizations/sigma/engine.ts — 60fps ForceAtlas2 drag loop with steady centroid
const pinnedX = this.graph.getNodeAttribute(draggedNode, 'x') as number;
const pinnedY = this.graph.getNodeAttribute(draggedNode, 'y') as number;
forceAtlas2.assign(this.graph, { iterations: 2, settings: { gravity: 0.8, slowDown: 6 } });
this.graph.setNodeAttribute(draggedNode, 'x', pinnedX);
this.graph.setNodeAttribute(draggedNode, 'y', pinnedY);
let sumX = 0, sumY = 0, count = 0;
this.graph.forEachNode(nId => {
sumX += (this.graph?.getNodeAttribute(nId, 'x') as number) || 0;
sumY += (this.graph?.getNodeAttribute(nId, 'y') as number) || 0;
count++;
});
const shiftX = (sumX / count) * 0.08, shiftY = (sumY / count) * 0.08;
this.graph.forEachNode(nId => {
if (nId !== draggedNode) {
this.graph?.setNodeAttribute(nId, 'x', (this.graph.getNodeAttribute(nId, 'x') as number) - shiftX);
this.graph?.setNodeAttribute(nId, 'y', (this.graph.getNodeAttribute(nId, 'y') as number) - shiftY);
}
});
this.sigma.refresh();Subtracting 8% of mean centroid displacement per frame keeps the dragged sub-network centered within the locked viewport without triggering abrupt camera zoom resets.
Why Are Dynamic Z-Ordering and 750ms Sine Transitions Essential for Readability?
Without dynamic z-ordering, dimmed background edges drawn later in canvas or SVG paint order slice across bright foreground hubs and active ego-network links; we eliminate this occlusion by sorting draw order through getNodeHopZIndex and getEdgeHopZIndex while interpolating colors and radii over a 750ms sine ease-in-out requestAnimationFrame loop [Fra16].
When a user hovers or drags a node, graph-payload.ts maps effective hop distances to discrete z-layers (40 for the focus node, 30 for 1-hop neighbors and incident edges, 20 for 2-hop, 10 for 3-hop, and 1 for background elements) and eases every node and link over HIGHLIGHT_TRANSITION_MS = 750 via :
- D3.js SVG DOM: Invokes
.sort()onlinkElementsandnodeElementswhile guardingisReorderingDom = trueso DOM mutation does not trigger spuriousmouseleaveevents [BOH11]. - vis-network & Cytoscape.js 2D Canvas: Sorts
this.network.body.edgeIndicesandnodeIndicesprior toredraw()invis-network/engine.ts, and batchesdisplayZIndexdata updates mapped to stylesheetz-indexincytoscape/engine.ts[Fra16]. - Sigma.js & Three.js WebGL: Updates Graphology
zIndexattributes (zIndex: true) insigma/engine.tsand scales 3D sphere volume by in3d-force-graph/engine.tsso Three.js sphere radius matches our 2D linear radius progression.
Coupling hop-sorted draw order with a 750ms sine-eased interpolation loop ensures that foreground ego-neighborhoods remain crisp and unoccluded across all five rendering technologies—combining the mathematical decay curves of Focus-Aware Hop Attenuation in Knowledge Graphs, the touch-first spatial indexing of Voronoi Proximity Picking, the XSS-safe reactive patterns of our Zero-innerHTML Preact and Nanostores Architecture, and the palette semantics of our Ten-Domain Color Spectrum across the live Cytoscape.js, Sigma.js, D3.js, Vis-Network, and 3D Force-Graph explorers.
References
- [BOH11]Michael Bostock, Vadim Ogievetsky and Jeffrey Heer (2011). D3: Data-Driven Documents. IEEE Transactions on Visualization and Computer Graphics, 17(12), pp. 2301--2309. DOI: 10.1109/TVCG.2011.185.
- [JVHB14]Mathieu Jacomy, Tommaso Venturini, Sebastien Heymann and Michael Bastian (2014). ForceAtlas2, a Continuous Graph Layout Algorithm for Handy Network Visualization Designed for the Gephi Software. PLOS ONE, 9(6), pp. e98679. DOI: 10.1371/journal.pone.0098679.
- [Fra16]Max Franz, Christian T. Lopes, Gerardo Huck, Yue Dong, Onur Sumer and Gary D. Bader (2016). Cytoscape.js: a graph theory library for visualisation and analysis. Bioinformatics, 32(2), pp. 309--311. DOI: 10.1093/bioinformatics/btv557.