TL;DR
Pick Cytoscape.js when the graph is an analysis object: algorithms, layouts, styles, centrality, path finding, and scientific/network workflows matter. Pick vis-network when the product is an interactive network diagram with physics, drag/drop, clustering, and quick editing controls. Pick Sigma.js when the graph is large enough that WebGL rendering and a graphology data layer matter more than built-in algorithms.
The practical 2026 rule: Cytoscape for graph analysis, vis-network for interactive diagrams, Sigma for large WebGL graph rendering. Benchmark with representative node/edge counts before promising exact limits; layout cost, edge density, labels, and hit-testing usually dominate the user experience.
Key Takeaways
- Cytoscape.js is the richest all-in-one graph toolkit. Use it when algorithms and layouts are part of the product, not just setup code.
- vis-network is the fastest path to an interactive network canvas. It is strong for physics diagrams, editable nodes, grouping, and exploratory views.
- Sigma.js is a renderer plus graphology, not a batteries-included analysis library. That split is a strength for large graphs and typed data flows.
- Do not cite universal node-count benchmarks. Test your own graph shape, labels, edge density, layout strategy, and target hardware.
- React integration is a lifecycle problem. Whichever library owns the canvas/SVG/WebGL subtree should be initialized through a ref and cleaned up explicitly.
Pick by graph job
| Graph job | Best first choice | Why | Watch out for |
|---|---|---|---|
| Dependency graph with path/centrality queries | Cytoscape.js | Built-in algorithms, layouts, and styling are one API | Large animated layouts can block the main thread |
| User-editable network map | vis-network | Physics, drag/drop, clustering, DataSet updates, and events are direct | Physics needs stabilization and may need to be disabled for static views |
| Large knowledge graph or social graph viewer | Sigma.js | WebGL renderer and graphology data layer scale better for dense exploration | More assembly work for algorithms, labels, and custom interactions |
| React dashboard with small graph widget | vis-network or Cytoscape.js | Faster to integrate than a full WebGL/graphology stack | Destroy/recreate carefully on prop changes |
| Scientific or bioinformatics graph analysis | Cytoscape.js | Ecosystem and API are built around graph analysis | Verify extension/layout compatibility for your dataset |
2026 source snapshot
| Library | Latest npm package checked 2026-06-15 | License | Package metadata caveat | GitHub status checked 2026-06-15 |
|---|---|---|---|---|
| Cytoscape.js | cytoscape@3.34.0 | MIT | npm unpacked size about 5.7 MB | Active repo; 11k+ stars; updated 2026-06-14 |
| vis-network | vis-network@10.1.0 | Apache-2.0 OR MIT | npm unpacked size is large because examples/assets ship with the package | Active repo; 3.5k+ stars; updated 2026-06-14 |
| Sigma.js | sigma@3.0.3 | MIT | npm unpacked size about 1 MB | Active repo; 12k+ stars; updated 2026-06-14 |
Use package size and stars as context only. They do not prove runtime speed or fit for your graph.
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 rendering pattern
import Sigma from "sigma"
import Graph from "graphology"
import forceAtlas2 from "graphology-layout-forceatlas2"
const graph = new Graph()
// Generate a representative stress graph for your product.
// Start below your target size, then raise nodeCount/edgeCount on the
// browsers and devices you support; do not reuse these values as a benchmark.
const nodeCount = 10000
const edgeCount = 20000
for (let i = 0; i < nodeCount; 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 with roughly the same density as the real graph:
for (let i = 0; i < edgeCount; i++) {
const source = Math.floor(Math.random() * nodeCount)
const target = Math.floor(Math.random() * nodeCount)
if (source !== target && !graph.hasEdge(source, target)) {
graph.addEdge(source, target, { size: 1 })
}
}
// Apply force layout:
forceAtlas2.assign(graph, { iterations: 100 })
// Render, then profile zoom/pan, labels, search, and hit-testing:
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 |
| Practical scale posture | Small/medium interactive graphs; test complex layouts | Small/medium interactive networks; tune physics | Larger interactive graphs through WebGL; still test labels and edge density |
| 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 |
| Source snapshot | npm/GitHub checked 2026-06-15 | npm/GitHub checked 2026-06-15 | npm/GitHub checked 2026-06-15 |
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 where WebGL, graphology, and explicit profiling matter
- Need WebGL performance for complex networks
- Using graphology for graph algorithms
- Building social network or knowledge graph visualizations
TypeScript Type Safety and Graph Data Modeling
All three libraries offer TypeScript support, but the quality of type inference differs significantly. Cytoscape.js provides types through @types/cytoscape and uses a flexible but loosely-typed element data model — ele.data() returns any, so you need to cast or validate the shape yourself. The typical pattern is to define your own node and edge data interfaces and cast at read time: const nodeData = node.data() as PackageNodeData. This works but requires discipline, since the compiler doesn't enforce that data added to the graph matches your interfaces.
Sigma.js with graphology provides stronger typing through graphology's generic type parameters. new Graph<NodeAttributes, EdgeAttributes, GraphAttributes>() locks the graph to specific attribute shapes, and graph.getNodeAttributes(node) returns your typed attributes rather than any. The graphology-types package defines the base interfaces, and the broader graphology ecosystem (layout packages, metrics, generators) all respect these generics. This makes Sigma.js with graphology the most type-safe option for teams that need compile-time guarantees about what data lives on their graph elements.
vis-network sits in the middle — it ships its own TypeScript types and the DataSet<T> generic enforces the shape of node and edge objects you add. nodes.add({ id: 1, label: "React", group: 3 }) is type-checked against your defined node type. However, vis-network's physics configuration objects are complex and the types can be incomplete for some advanced physics solver options, occasionally requiring @ts-ignore annotations in heavily customized configurations.
Choosing a Layout Algorithm for Your Graph Structure
All three libraries are DOM-based and require a container element, which creates a seam with React's virtual DOM that each library handles differently. The baseline pattern for all three is using a ref to get the container element and initializing the library instance inside useEffect with a cleanup function that destroys the instance on unmount.
Cytoscape.js with React works well with this pattern because Cytoscape manages its own internal state — you hand it the container element and let it own that DOM subtree. The challenge is data updates: when your graph data changes in React state, you need to call cy.elements().remove() followed by cy.add(newElements), or diff the changes yourself with cy.getElementById() to find added/removed nodes. The react-cytoscapejs library wraps this pattern with a <CytoscapeComponent> that accepts elements and stylesheet as React props and handles incremental updates, but it's a thin wrapper that still requires understanding Cytoscape's core API.
Sigma.js has official React bindings in the @react-sigma package, which provides <SigmaContainer>, <LoadGraph>, and useLoadGraph hooks. The graphology graph object is the data layer — you update it with graph.addNode(), graph.addEdge(), graph.mergeNodeAttributes() and Sigma re-renders automatically because it listens to graphology's event system. This event-driven reactivity is more natural in React than Cytoscape's imperative update API.
vis-network's DataSet objects are the reactive data layer: nodes.add(), nodes.update(), edges.remove() trigger immediate visual updates without reinitializing the network. This makes React integration straightforward — store the DataSet instances in a ref, update them when props change in a useEffect, and vis-network reacts automatically.
Layout algorithm selection has a larger impact on graph readability than any styling choice. Force-directed layouts (Cytoscape's COSE, vis-network's ForceAtlas2Based, graphology's ForceAtlas2) work by simulating spring forces between connected nodes, pushing unconnected nodes apart and pulling connected ones together. They produce natural-looking graph embeddings where clusters of highly-connected nodes group visually, but they are computationally expensive and non-deterministic — the same graph can produce different layouts on different runs. For graphs with clear hierarchical structure (file system trees, organizational charts, dependency trees), breadth-first or Dagre layouts produce cleaner results at lower computational cost.
The choice of layout algorithm should be driven by graph structure. Biological networks and social graphs benefit from force-directed layouts that reveal community structure. Knowledge graphs and concept maps often do well with force-directed as well. Package dependency graphs — where a few popular packages have many dependents — often look better with hierarchical layouts where the depth corresponds to dependency distance from the root. Cytoscape.js's built-in breadthfirst and concentric layouts, or the dagre extension, are the right choices here.
Graphology: The Data Layer Behind Sigma.js
Sigma.js doesn't manage its own graph data structure — it renders a graphology Graph object. Graphology is a separate library (npm install graphology) with its own extensive ecosystem: graphology-layout-forceatlas2 for force-directed layouts, graphology-communities-louvain for community detection, graphology-shortest-path for path finding, graphology-generators for creating test graphs, and graphology-metrics for centrality calculations. This separation of concerns is intentional — Sigma is purely a renderer, and graphology is purely a graph data structure with algorithms.
The graphology ecosystem covers most of what Cytoscape.js builds in: shortest path (Dijkstra, A*), centrality (betweenness, degree, eigenvector), community detection (Louvain, Label Propagation), and force-directed layout (ForceAtlas2, FA2 with Barnes-Hut optimization). The difference is that these are separate npm packages rather than built-in methods. When you're running ForceAtlas2 — graphology's primary layout algorithm — the graphology-layout-forceatlas2 package includes a WebWorker mode (FA2Layout) that runs the physics simulation on a background thread, keeping the main thread free for rendering. That worker-mode path is useful when layout work threatens interactivity; Cytoscape.js COSE should be treated as main-thread layout work that can make interaction feel blocked during expensive runs, so profile it against representative graphs before promising an interactive limit.
Performance Boundaries and When to Switch Libraries
Treat graph performance as a workload test, not a universal node-count promise. Cytoscape.js and vis-network both render through Canvas, so layout calculations, event hit-testing, labels, edge density, and redraw frequency compete for main-thread time. Both libraries provide tuning levers — Cytoscape.js has options such as motionBlur, and vis-network exposes physics solvers including Barnes-Hut-style approximations — but those levers still need to be profiled against your actual graph shape.
Sigma.js is the better starting point when interactive rendering scale is the main risk because its WebGL renderer batches node and edge drawing through GPU-oriented paths while graphology owns the data layer. That does not make Sigma a guaranteed answer for every large graph: labels, edge density, custom node renderers, search/highlight reducers, layout generation, and the target browser/GPU can still dominate the experience. If you need complex custom shapes, be prepared for WebGL shader work or overlays.
Use a representative benchmark before switching libraries or promising a limit. Build a fixture with your real node attributes, edge density, label policy, layout algorithm, zoom/search interactions, and target hardware. If Canvas-based interaction becomes visibly unstable after physics/layout tuning, move the rendering spike to Sigma.js; if Sigma still struggles, consider server-side pre-layout, level-of-detail sampling, clustering, or separate overview/detail views before increasing client-side graph size.
Methodology
This refresh used official docs, npm registry metadata, and GitHub repository metadata checked on 2026-06-15 for Cytoscape.js, vis-network, and Sigma.js. Feature comparison is based on current documented library capabilities, not a synthetic benchmark. Treat dataset-size guidance as directional until you test representative graphs on your target devices.
Compare graph visualization and data libraries on PkgPulse →
See also: AVA vs Jest and Mermaid vs D3.js vs Chart.js 2026, Fabric.js vs Konva vs PixiJS: Canvas & 2D Graphics 2026.
