🎨 Graph Visualization
To visualize the connections within the Digital Twin's brain, the frontend displays an interactive, force-directed graph. Instead of importing heavy libraries like D3.js or Sigma.js, we engineered the engine from scratch using the HTML Canvas API to ensure optimal performance and complete layout control.
🏎️ Why Raw Canvas?
- High Performance: Canvas handles hundreds of animated nodes and edges at 60 FPS by bypassing DOM element updates.
- Custom Shader-like Styling: Gives us full control to draw glow effects, cyber-grids, and interactive scanning sweeps.
- Bundle Optimization: Zero dependencies, reducing our Javascript bundle size by over 100KB.
📐 Force-Directed Physics Engine
A force-directed layout models the graph as a physical system:
- Coulomb's Law (Repulsion): Nodes push away from each other to prevent overlaps.
- Hooke's Law (Attraction): Linked nodes pull together like springs.
- Friction / Damping: Slows down node velocities over time to stabilize the layout.
class ForceDirectedGraph {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.nodes = []; // { id, x, y, vx, vy, radius }
this.links = []; // { sourceNode, targetNode, length }
this.repulsionStrength = 200;
this.attractionStrength = 0.05;
this.damping = 0.85;
}
updatePhysics() {
// 1. Repulsion (between all node pairs)
for (let i = 0; i < this.nodes.length; i++) {
let nodeA = this.nodes[i];
for (let j = i + 1; j < this.nodes.length; j++) {
let nodeB = this.nodes[j];
let dx = nodeB.x - nodeA.x;
let dy = nodeB.y - nodeA.y;
let dist = Math.sqrt(dx * dx + dy * dy) || 1;
// Calculate force vector
let force = this.repulsionStrength / (dist * dist);
let fx = (dx / dist) * force;
let fy = (dy / dist) * force;
nodeA.vx -= fx;
nodeA.vy -= fy;
nodeB.vx += fx;
nodeB.vy += fy;
}
}
// 2. Attraction (along link connections)
for (let link of this.links) {
let nodeA = link.source;
let nodeB = link.target;
let dx = nodeB.x - nodeA.x;
let dy = nodeB.y - nodeA.y;
let dist = Math.sqrt(dx * dx + dy * dy) || 1;
let displacement = dist - link.length;
let fx = (dx / dist) * displacement * this.attractionStrength;
let fy = (dy / dist) * displacement * this.attractionStrength;
nodeA.vx += fx;
nodeA.vy += fy;
nodeB.vx -= fx;
nodeB.vy -= fy;
}
// 3. Apply position updates & velocity damping
for (let node of this.nodes) {
if (node.isDragging) continue; // Skip if user holds node
node.x += node.vx;
node.y += node.vy;
node.vx *= this.damping;
node.vy *= this.damping;
}
}
}
🖌️ Rendering Logic
We draw nodes with gradient fills and links with semi-transparent lines to create a clean, modern aesthetic.
draw() {
const { ctx, canvas } = this;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw Link Lines
ctx.strokeStyle = 'rgba(6, 182, 212, 0.15)';
ctx.lineWidth = 1;
for (let link of this.links) {
ctx.beginPath();
ctx.moveTo(link.source.x, link.source.y);
ctx.lineTo(link.target.x, link.target.y);
ctx.stroke();
}
// Draw Nodes
for (let node of this.nodes) {
ctx.beginPath();
ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
// Gradient node fill
let grad = ctx.createRadialGradient(node.x, node.y, 2, node.x, node.y, node.radius);
grad.addColorStop(0, '#06b6d4'); // Cyan highlight
grad.addColorStop(1, '#8b5cf6'); // Purple secondary
ctx.fillStyle = grad;
ctx.shadowBlur = 10;
ctx.shadowColor = '#06b6d4';
ctx.fill();
ctx.shadowBlur = 0; // Reset shadow for lines
}
}
Canvas Interpolation
To make node drag-and-drop transitions smooth on high-refresh-rate displays (120Hz+), the canvas utilizes requestAnimationFrame to interpolate physical velocities dynamically, keeping frame pacing fluid.