Graph Visualization with Sigma.js: Building Interactive Network Views
Why Sigma.js?
Web-based graph visualization has three main options:
| Library | Rendering | Max Nodes (60fps) | Best For |
|---|---|---|---|
| D3.js | SVG / Canvas | ~5,000 | Custom layouts, small bespoke visualisations |
| Cytoscape.js | Canvas + DOM | ~10,000 | Biological / pathway networks |
| vis.js | Canvas | ~8,000 | Timeline / network dashboards |
| Sigma.js | WebGL | ~50,000+ | Large knowledge graphs, real-time interaction |
Sigma.js is the right choice for knowledge graphs. It is lightweight (~80KB gzipped), uses WebGL for hardware-accelerated rendering, and pairs naturally with the Graphology library, which provides a comprehensive graph data model with built-in traversal algorithms, filtering, and serialisation.
This article walks through everything you need to build a production-quality interactive graph visualisation, from first render to advanced performance optimisation.
Basic Setup
npm install sigma graphology graphology-layout-forceatlas2
import Sigma from "sigma";
import Graph from "graphology";
const graph = new Graph();
// Add nodes
graph.addNode("neo4j", { label: "Neo4j", size: 15, color: "#10b981", x: 0, y: 0 });
graph.addNode("graphrag", { label: "GraphRAG", size: 12, color: "#6366f1", x: 1, y: 1 });
// Add edge
graph.addEdge("neo4j", "graphrag", { size: 2, color: "#374151" });
// Render
const sigma = new Sigma(graph, document.getElementById("container"));
Sigma.js renders into a <canvas> element using WebGL. Unlike SVG-based renderers, it does not create DOM nodes per element, which is why it can maintain 60fps at scale.
Force-Directed Layout
Raw positioning produces unusable graphs. ForceAtlas2 applies physical simulation — nodes repel each other while edges attract connected nodes, producing a readable layout:
import forceAtlas2 from "graphology-layout-forceatlas2";
// Assign positions
forceAtlas2.assign(graph, {
iterations: 100,
settings: {
gravity: 0.5,
scalingRatio: 10,
slowDown: 1,
},
});
Tuning these parameters dramatically changes the final layout:
| Parameter | Effect | Typical Range |
|---|---|---|
gravity | Pulls nodes toward the centre, preventing disconnected clusters from floating away | 0.1 – 1.0 |
scalingRatio | Controls overall spacing between nodes. Higher values spread clusters further apart | 1 – 40 |
slowDown | Dampens movement each iteration. Higher values reduce oscillation in dense graphs | 0.1 – 10 |
barnesHutOptimize | Enables Barnes–Hut approximation for faster convergence on graphs over 1,000 nodes | true / false |
For graphs larger than 1,000 nodes, enable barnesHutOptimize: true with barnesHutTheta: 0.5 — this swaps exact pairwise repulsion for a spatial tree approximation, reducing complexity from O(n²) to O(n log n).
Graphology Algorithms
Graphology ships with a suite of algorithms that operate directly on your graph. Two of the most useful for knowledge graphs are Louvain community detection and PageRank centrality.
Community Detection
import louvain from "graphology-communities-louvain";
const communities = louvain(graph);
// Returns a map: { nodeId: communityId, ... }
Louvain partitions the graph into densely connected clusters. Each node gets a community ID that you can use to assign colours or group layouts.
Centrality
import { pageRank } from "graphology-metrics/centrality/pageRank";
const ranks = pageRank(graph, { alpha: 0.85 });
// Returns a map: { nodeId: score, ... }
PageRank identifies the most influential nodes — useful for highlighting key entities or serving as a starting point for exploration.
Node Styling and Categories
Once you have algorithm results, map them to visual properties. The nodeReducer callback lets you override rendering attributes without mutating the graph:
const sigma = new Sigma(graph, container, {
nodeReducer: (node, data) => {
const community = communities[node];
const palette = {
"0": "#10b981",
"1": "#6366f1",
"2": "#f59e0b",
"3": "#ef4444",
};
return {
...data,
color: palette[community] || "#6b7280",
size: Math.max(5, (ranks[node] || 0) * 50),
};
},
});
This approach keeps the underlying graph data clean while giving you full control over appearance. Colour nodes by community, scale size by centrality, or hide low-relevance nodes entirely.
Rendering Large Graphs
For graphs over 5,000 nodes, computing the layout on the main thread blocks the UI. Use graphology-layout-forceatlas2 in a Web Worker:
import FA2Layout from "graphology-layout-forceatlas2/worker";
const layout = new FA2Layout(graph, {
settings: { gravity: 0.5, scalingRatio: 10, barnesHutOptimize: true },
});
layout.start();
// Layout runs asynchronously — the UI stays responsive
// Stop after convergence
setTimeout(() => layout.kill(), 5000);
The worker version offloads the physics simulation to a separate thread, keeping the main thread free for user interaction. You can also use graphology-layout-worker for custom layout functions.
Dynamic Graph Updates
Knowledge graphs are rarely static. Add and remove nodes at runtime with no performance penalty:
// Add a new node mid-session
graph.addNode("new-topic", {
label: "New Topic",
size: 10,
color: "#8b5cf6",
x: Math.random() * 10 - 5,
y: Math.random() * 10 - 5,
});
graph.addEdge("new-topic", "neo4j", { size: 1, color: "#6b7280" });
// Remove a node and its edges
graph.dropNode("stale-topic");
// Sigma.js reacts automatically — no re-render call needed
For bulk updates, batch mutations inside graph.updateEachNodeAttributes to avoid redundant layout recalculations.
Interactivity
Beyond basic click handling, Sigma.js supports camera controls, node highlighting, and hover neighbourhood inspection:
sigma.on("clickNode", (event) => {
const nodeId = event.node;
const data = graph.getNodeAttributes(nodeId);
window.location.href = `/tag/${encodeURIComponent(nodeId)}`;
});
sigma.on("enterNode", (event) => {
document.getElementById("tooltip").textContent = graph.getNodeAttribute(event.node, "label");
});
sigma.on("leaveNode", () => {
document.getElementById("tooltip").textContent = "";
});
Neighbourhood Highlighting
For large graphs, highlighting a node's immediate neighbourhood helps users explore without getting lost:
sigma.setSetting("nodeReducer", (node, data) => {
const highlighted = sigma.getNodes().includes(node);
if (!highlighted) {
return { ...data, color: "#e5e7eb", size: data.size * 0.3 };
}
return data;
});
sigma.on("clickNode", ({ node }) => {
const neighbors = graph.neighbors(node);
sigma.setNodes([node, ...neighbors]); // reduces to highlighted set
sigma.refresh();
});
Graphology Algorithms in Practice
Graphology is not just a data store — it ships with graph algorithms you can leverage at runtime:
Finding Connected Components
import connectedComponents from "graphology-components";
const components = connectedComponents(graph);
console.log(`The graph has ${components.length} connected components`);
Computing Degree Centrality
graph.forEachNode((node) => {
const degree = graph.degree(node);
graph.setNodeAttribute(node, "size", Math.max(5, Math.min(30, degree * 3)));
});
sigma.refresh();
This creates a visualisation where more-connected nodes render larger — a common pattern for knowledge graph exploration.
Filtering Subgraphs
import subgraph from "graphology-operators/subgraph";
// Extract nodes matching a condition
const bigNodes = graph.filterNodes((node) => graph.degree(node) > 5);
const core = subgraph(graph, bigNodes);
Filtering is especially useful when rendering graphs incrementally — start with the most central nodes, then expand on user interaction.
Advanced Visualisation Techniques
Edge Labels
Sigma.js v2 supports edge labels out of the box. Enable them when edge relationships carry meaning (for example, DEPENDS_ON, REFERENCES, OWNS):
// Add edge with label
graph.addEdge("neo4j", "graphrag", {
label: "enables",
size: 2,
color: "#9ca3af",
});
// In Sigma constructor or via settings
const sigma = new Sigma(graph, container, {
renderEdgeLabels: true,
defaultEdgeLabelColor: "#6b7280",
defaultEdgeLabelSize: 10,
});
Use edge labels sparingly — on graphs with more than 200 edges, labels create visual clutter. A better approach is to show edge labels only when a node is hovered or selected.
Camera Controls
Sigma's camera supports smooth transitions, which are useful when a user clicks a node and you want to centre the view:
function focusOnNode(nodeId) {
const { x, y } = graph.getNodeAttributes(nodeId);
sigma.getCamera().animate({ x, y, ratio: 0.01 }, { duration: 500 });
}
The animate method accepts standard x, y, and ratio (zoom level) parameters. Duration is in milliseconds. Combined with minCameraRatio and maxCameraRatio settings, this gives you full control over the visible area.
Visual Variables
Map data properties to visual channels to encode additional dimensions:
graph.forEachNode((node, attrs) => {
// Size encodes importance
attrs.size = 5 + attrs.importance * 25;
// Color encodes category
const palette = { AI: "#6366f1", infra: "#10b981", security: "#ef4444" };
attrs.color = palette[attrs.category] || "#9ca3af";
});
Fine-Tuning the Renderer
Sigma.js exposes a range of settings to control rendering behaviour. Pass them as the third argument to the Sigma constructor:
const sigma = new Sigma(graph, document.getElementById("container"), {
minCameraRatio: 0.1,
maxCameraRatio: 10,
renderLabels: true,
labelRenderedSizeThreshold: 8,
defaultNodeColor: "#6366f1",
defaultEdgeColor: "#9ca3af",
defaultEdgeType: "arrow",
enableEdgeEvents: false,
labelFont: "Inter, sans-serif",
});
Key settings explained:
minCameraRatio/maxCameraRatio— Clamp zoom to prevent users getting lost in empty space or zooming too far inlabelRenderedSizeThreshold— Only render labels when a node's rendered size exceeds this value; improves performance at low zoomdefaultEdgeType—"arrow"for directed graphs,"line"for undirectedenableEdgeEvents— Keepfalseunless you need click or hover on edges (saves render cycles)
These options give fine-grained control over appearance and performance without writing custom rendering code.
Advanced Styling and Interaction
Dynamic Node Sizing and Colouring
Real-world graphs need visual encodings that communicate meaning at a glance. Size and colour are the two most effective channels:
// Set node size proportionally to degree (number of connections)
graph.forEachNode((node, attributes) => {
const degree = graph.degree(node);
graph.setNodeAttribute(node, "size", Math.max(3, Math.min(20, degree * 2)));
graph.setNodeAttribute(node, "color", degree > 5 ? "#ef4444" : "#6366f1");
});
Camera Animation
Programmatic camera control focuses attention on specific regions. The animateCamera method moves the viewport smoothly:
// Focus on a specific node with animation
sigma.getCamera().animate(
{
x: graph.getNodeAttribute("neo4j", "x"),
y: graph.getNodeAttribute("neo4j", "y"),
ratio: 0.05,
},
{ duration: 500 },
);
Node Filtering
Filter the displayed graph to reduce visual clutter during exploration:
// Toggle visibility for nodes below a degree threshold
function filterByDegree(minDegree) {
graph.forEachNode((node) => {
const visible = graph.degree(node) >= minDegree;
graph.setNodeAttribute(node, "hidden", !visible);
});
sigma.refresh();
}
filterByDegree(2); // Show only nodes with 2+ connections
These patterns transform a static visualisation into an interactive exploration tool. Combine them with Sigma.js's built-in setSettings for fine-grained control over node rendering, edge labels, and hover states.
Performance Tips
- Use
minCameraRatio/maxCameraRatioto limit zoom bounds - Batch node additions: add all nodes, then all edges
- Use worker-based layout computation for large graphs
- Set
enableEdgeEvents: falseunless you need edge interactions - Switch Sigma's WebGL renderer to
webgl(default) for speed; fall back tocanvasif you need pixel-perfect labels
The interactive knowledge graph on this site's homepage is built with Sigma.js — open your browser's dev tools to inspect how it works. Start with our Basic Setup, tune the ForceAtlas2 parameters, layer in graphology algorithms, and you'll have a production-grade graph visualisation in under 200 lines of code.