Skip to main content

Guide

Cytoscape.js vs vis-network vs Sigma.js 2026: Graph Visualization Decision Guide

Choose a JavaScript graph visualization library in 2026: Cytoscape.js, vis-network, and Sigma.js by graph size, layouts, WebGL, TypeScript, and interaction.

·PkgPulse Team·
0
Hero image for Cytoscape.js vs vis-network vs Sigma.js 2026: Graph Visualization Decision Guide

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 jobBest first choiceWhyWatch out for
Dependency graph with path/centrality queriesCytoscape.jsBuilt-in algorithms, layouts, and styling are one APILarge animated layouts can block the main thread
User-editable network mapvis-networkPhysics, drag/drop, clustering, DataSet updates, and events are directPhysics needs stabilization and may need to be disabled for static views
Large knowledge graph or social graph viewerSigma.jsWebGL renderer and graphology data layer scale better for dense explorationMore assembly work for algorithms, labels, and custom interactions
React dashboard with small graph widgetvis-network or Cytoscape.jsFaster to integrate than a full WebGL/graphology stackDestroy/recreate carefully on prop changes
Scientific or bioinformatics graph analysisCytoscape.jsEcosystem and API are built around graph analysisVerify extension/layout compatibility for your dataset

2026 source snapshot

LibraryLatest npm package checked 2026-06-15LicensePackage metadata caveatGitHub status checked 2026-06-15
Cytoscape.jscytoscape@3.34.0MITnpm unpacked size about 5.7 MBActive repo; 11k+ stars; updated 2026-06-14
vis-networkvis-network@10.1.0Apache-2.0 OR MITnpm unpacked size is large because examples/assets ship with the packageActive repo; 3.5k+ stars; updated 2026-06-14
Sigma.jssigma@3.0.3MITnpm unpacked size about 1 MBActive 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

FeatureCytoscape.jsvis-networkSigma.js
RendererCanvasCanvasWebGL
Practical scale postureSmall/medium interactive graphs; test complex layoutsSmall/medium interactive networks; tune physicsLarger interactive graphs through WebGL; still test labels and edge density
Graph algorithms✅ (many)Via graphology
Force layout✅ (physics)Via graphology
Built-in drag/drop
ClusteringVia graphology
Edge labels
Node shapes✅ (many)✅ (many)Circle/custom
Animations✅ (physics)
React bindingsVia refVia ref@react-sigma
TypeScript
Used byBioinformaticsDashboardsLarge networks
Source snapshotnpm/GitHub checked 2026-06-15npm/GitHub checked 2026-06-15npm/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.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.