Cytoscape.js vs vis-network vs Sigma.js: Graph Visualization (2026)
TL;DR
Cytoscape.js is the full-featured graph library — graph theory algorithms, multiple layouts, styling, analysis, used in bioinformatics and network analysis. vis-network is the interactive network visualization — drag/drop nodes, physics simulation, clustering, part of the vis.js suite. Sigma.js is the WebGL graph renderer — renders large graphs (100K+ nodes) with GPU acceleration, designed for performance. In 2026: Cytoscape.js for graph analysis + visualization, vis-network for interactive network diagrams, Sigma.js for large-scale graph rendering.
Key Takeaways
- Cytoscape.js: ~500K weekly downloads — graph algorithms, layouts, bioinformatics
- vis-network: ~200K weekly downloads — interactive, physics, drag/drop, clustering
- Sigma.js: ~50K weekly downloads — WebGL, large graphs, 100K+ nodes, performance
- Cytoscape.js has the richest algorithm library (shortest path, centrality, etc.)
- vis-network has the best out-of-box interactivity (physics, drag, zoom)
- Sigma.js handles the largest graphs (WebGL rendering)
Cytoscape.js
Cytoscape.js — graph theory + visualization:
Basic graph
import cytoscape from "cytoscape"
const cy = cytoscape({
container: document.getElementById("graph"),
elements: [
// Nodes:
{ data: { id: "react", label: "React" } },
{ data: { id: "next", label: "Next.js" } },
{ data: { id: "remix", label: "Remix" } },
{ data: { id: "vue", label: "Vue" } },
{ data: { id: "nuxt", label: "Nuxt" } },
// Edges:
{ data: { source: "react", target: "next" } },
{ data: { source: "react", target: "remix" } },
{ data: { source: "vue", target: "nuxt" } },
],
style: [
{
selector: "node",
style: {
"background-color": "#3b82f6",
"label": "data(label)",
"color": "#fff",
"text-valign": "center",
"font-size": "12px",
},
},
{
selector: "edge",
style: {
"width": 2,
"line-color": "#64748b",
"target-arrow-color": "#64748b",
"target-arrow-shape": "triangle",
"curve-style": "bezier",
},
},
],
layout: { name: "cose" }, // Force-directed layout
})
Graph algorithms
// Shortest path:
const dijkstra = cy.elements().dijkstra({
root: cy.getElementById("react"),
weight: (edge) => edge.data("weight") || 1,
})
const pathToVue = dijkstra.pathTo(cy.getElementById("vue"))
console.log("Distance:", dijkstra.distanceTo(cy.getElementById("vue")))
// Centrality:
const centrality = cy.elements().betweennessCentrality()
cy.nodes().forEach((node) => {
console.log(`${node.id()}: ${centrality.betweenness(node)}`)
})
// Community detection:
const clusters = cy.elements().markovClustering()
clusters.forEach((cluster, i) => {
cluster.style("background-color", colors[i])
})
// BFS/DFS:
cy.elements().bfs({
root: cy.getElementById("react"),
visit(v) { console.log("Visited:", v.id()) },
})
Layouts
// Built-in layouts:
cy.layout({ name: "cose" }).run() // Force-directed
cy.layout({ name: "circle" }).run() // Circular
cy.layout({ name: "grid" }).run() // Grid
cy.layout({ name: "breadthfirst" }).run() // Hierarchical
cy.layout({ name: "concentric" }).run() // Concentric circles
cy.layout({ name: "random" }).run() // Random
// Force-directed with options:
cy.layout({
name: "cose",
idealEdgeLength: 100,
nodeOverlap: 20,
gravity: 80,
numIter: 1000,
animate: true,
}).run()
vis-network
vis-network — interactive networks:
Basic network
import { Network, DataSet } from "vis-network/standalone"
const nodes = new DataSet([
{ id: 1, label: "React", color: "#61dafb" },
{ id: 2, label: "Next.js", color: "#000000" },
{ id: 3, label: "Remix", color: "#121212" },
{ id: 4, label: "Vue", color: "#42b883" },
{ id: 5, label: "Nuxt", color: "#00dc82" },
])
const edges = new DataSet([
{ from: 1, to: 2, label: "powers" },
{ from: 1, to: 3, label: "powers" },
{ from: 4, to: 5, label: "powers" },
])
const container = document.getElementById("network")!
const network = new Network(container, { nodes, edges }, {
physics: {
forceAtlas2Based: {
gravitationalConstant: -50,
centralGravity: 0.01,
springLength: 200,
},
solver: "forceAtlas2Based",
},
nodes: {
shape: "dot",
size: 25,
font: { size: 14, color: "#ffffff" },
borderWidth: 2,
},
edges: {
width: 2,
arrows: { to: { enabled: true, scaleFactor: 0.5 } },
font: { size: 10, align: "middle" },
},
})
Dynamic updates
// Add nodes/edges dynamically:
nodes.add({ id: 6, label: "Svelte", color: "#ff3e00" })
edges.add({ from: 6, to: 7, label: "powers" })
// Remove:
nodes.remove(6)
// Update:
nodes.update({ id: 1, label: "React 19", color: "#00d8ff" })
// Physics — stabilize then freeze:
network.once("stabilized", () => {
network.setOptions({ physics: false })
})
Events
// Click events:
network.on("click", (params) => {
if (params.nodes.length > 0) {
const nodeId = params.nodes[0]
console.log("Clicked node:", nodes.get(nodeId))
}
})
// Hover:
network.on("hoverNode", (params) => {
console.log("Hovering:", params.node)
})
// Drag:
network.on("dragEnd", (params) => {
if (params.nodes.length > 0) {
const pos = network.getPositions(params.nodes)
console.log("New positions:", pos)
}
})
// Double-click to edit:
network.on("doubleClick", (params) => {
if (params.nodes.length > 0) {
const node = nodes.get(params.nodes[0])
const newLabel = prompt("New label:", node.label)
if (newLabel) nodes.update({ id: node.id, label: newLabel })
}
})
Clustering
// Cluster by property:
network.cluster({
joinCondition: (nodeOptions) => nodeOptions.group === "frontend",
clusterNodeProperties: {
label: "Frontend",
shape: "diamond",
color: "#3b82f6",
},
})
// Cluster by connection:
network.clusterByConnection(1) // Cluster around node 1
// Open cluster on click:
network.on("selectNode", (params) => {
if (network.isCluster(params.nodes[0])) {
network.openCluster(params.nodes[0])
}
})
Sigma.js
Sigma.js — WebGL graph renderer:
Basic graph
import Sigma from "sigma"
import Graph from "graphology"
const graph = new Graph()
// Add nodes:
graph.addNode("react", {
label: "React",
x: 0, y: 0,
size: 20,
color: "#61dafb",
})
graph.addNode("next", {
label: "Next.js",
x: 1, y: 1,
size: 15,
color: "#000000",
})
graph.addNode("vue", {
label: "Vue",
x: -1, y: 1,
size: 15,
color: "#42b883",
})
// Add edges:
graph.addEdge("react", "next", { size: 2, color: "#64748b" })
graph.addEdge("react", "vue", { size: 1, color: "#64748b" })
// Render:
const renderer = new Sigma(graph, document.getElementById("graph")!, {
renderEdgeLabels: true,
})
Large graph (100K+ nodes)
import Sigma from "sigma"
import Graph from "graphology"
import forceAtlas2 from "graphology-layout-forceatlas2"
const graph = new Graph()
// Generate large graph:
for (let i = 0; i < 100000; i++) {
graph.addNode(i, {
label: `Node ${i}`,
x: Math.random() * 1000,
y: Math.random() * 1000,
size: 2 + Math.random() * 5,
color: `hsl(${Math.random() * 360}, 70%, 50%)`,
})
}
// Add random edges:
for (let i = 0; i < 200000; i++) {
const source = Math.floor(Math.random() * 100000)
const target = Math.floor(Math.random() * 100000)
if (source !== target && !graph.hasEdge(source, target)) {
graph.addEdge(source, target, { size: 1 })
}
}
// Apply force layout:
forceAtlas2.assign(graph, { iterations: 100 })
// Render — WebGL handles 100K+ nodes at 60fps:
const renderer = new Sigma(graph, container)
Search and highlight
import Sigma from "sigma"
const renderer = new Sigma(graph, container)
// Search nodes:
function searchNode(query: string) {
const matching = graph.filterNodes((_, attrs) =>
attrs.label.toLowerCase().includes(query.toLowerCase())
)
// Highlight matching:
renderer.setSetting("nodeReducer", (node, data) => {
if (matching.length > 0 && !matching.includes(node)) {
return { ...data, color: "#333", label: "" }
}
return data
})
renderer.refresh()
}
Events
const renderer = new Sigma(graph, container)
// Click:
renderer.on("clickNode", ({ node }) => {
console.log("Clicked:", graph.getNodeAttributes(node))
})
// Hover:
renderer.on("enterNode", ({ node }) => {
// Highlight neighbors:
const neighbors = new Set(graph.neighbors(node))
renderer.setSetting("nodeReducer", (n, data) => {
if (n === node || neighbors.has(n)) return data
return { ...data, color: "#333", label: "" }
})
renderer.refresh()
})
renderer.on("leaveNode", () => {
renderer.setSetting("nodeReducer", (_, data) => data)
renderer.refresh()
})
Feature Comparison
| Feature | Cytoscape.js | vis-network | Sigma.js |
|---|---|---|---|
| Renderer | Canvas | Canvas | WebGL |
| Max nodes (smooth) | ~5,000 | ~5,000 | 100,000+ |
| Graph algorithms | ✅ (many) | ❌ | Via graphology |
| Force layout | ✅ | ✅ (physics) | Via graphology |
| Built-in drag/drop | ✅ | ✅ | ✅ |
| Clustering | ❌ | ✅ | Via graphology |
| Edge labels | ✅ | ✅ | ✅ |
| Node shapes | ✅ (many) | ✅ (many) | Circle/custom |
| Animations | ✅ | ✅ (physics) | ❌ |
| React bindings | Via ref | Via ref | @react-sigma |
| TypeScript | ✅ | ✅ | ✅ |
| Used by | Bioinformatics | Dashboards | Large networks |
| Weekly downloads | ~500K | ~200K | ~50K |
When to Use Each
Use Cytoscape.js if:
- Need graph algorithms (shortest path, centrality, BFS/DFS)
- Building bioinformatics or scientific network analysis
- Need multiple layout algorithms
- Want the richest styling and customization
Use vis-network if:
- Building interactive network diagrams
- Need physics-based layout simulation
- Want drag-and-drop node editing
- Need clustering and grouping features
Use Sigma.js if:
- Rendering large graphs (10K–100K+ nodes)
- Need WebGL performance for complex networks
- Using graphology for graph algorithms
- Building social network or knowledge graph visualizations
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on Cytoscape.js v3.x, vis-network v9.x, and Sigma.js v3.x.
Compare graph visualization and data libraries on PkgPulse →