<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/cytoscape-vs-vis-network-vs-sigma-graph-visualization-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/cytoscape-vs-vis-network-vs-sigma-graph-visualization-2026/raw.md -->
<!-- Source path: content/guides/cytoscape-vs-vis-network-vs-sigma-graph-visualization-2026.mdx -->

---
og_image: "/images/guides/cytoscape-vs-vis-network-vs-sigma-graph-visualization-2026.webp"
title: "Cytoscape.js vs vis-network vs Sigma.js 2026"
description: "Compare Cytoscape.js, vis-network, and Sigma.js for graph and network visualization in JavaScript. Force-directed layouts, large graph rendering here."
date: "2026-03-09"
author: "PkgPulse Team"
tags: ["javascript", "typescript", "frontend", "data-visualization"]
---

## 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](https://js.cytoscape.org) — graph theory + visualization:

### Basic graph

```typescript
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

```typescript
// 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

```typescript
// 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](https://visjs.github.io/vis-network/) — interactive networks:

### Basic network

```typescript
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

```typescript
// 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

```typescript
// 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

```typescript
// 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](https://www.sigmajs.org) — WebGL graph renderer:

### Basic graph

```typescript
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)

```typescript
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

```typescript
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

```typescript
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

---

## 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. For large graphs where 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. This is a significant advantage over Cytoscape.js's COSE layout, which runs synchronously on the main thread and blocks UI interaction during layout calculation on graphs above a few thousand nodes.

## Performance Boundaries and When to Switch Libraries

The performance limit of Canvas-based rendering (Cytoscape.js and vis-network) is approximately 3,000-5,000 nodes before frame rate degrades noticeably. At this scale, layout calculations, event hit-testing, and redrawing all compete for CPU time on the main thread. Both libraries offer partial mitigations: Cytoscape.js has a `motionBlur` option that reduces rendering fidelity during animation, and vis-network's `physics.solver` options include Barnes-Hut approximation which reduces force calculation complexity from O(n²) to O(n log n) for large graphs.

Sigma.js with WebGL has a practical performance ceiling around 100,000-500,000 nodes, depending on edge density and hardware. The WebGL renderer batches all node and edge draws into GPU draw calls, and Sigma's custom shaders are optimized for the graph rendering use case. The trade-off is that WebGL rendering is less flexible for custom node shapes — Sigma natively renders circles and edges, and custom node renderers require writing GLSL shader code or using a WebGL canvas overlay for complex shapes.

For graphs between 5,000 and 50,000 nodes, both Sigma.js and a performance-tuned vis-network can work, but Sigma's WebGL rendering will produce smoother interactions. Above 50,000 nodes, Sigma.js is the only practical option among these three without server-side pre-rendering or level-of-detail sampling.

---

## 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 →](https://www.pkgpulse.com)*

*See also: [AVA vs Jest](/compare/ava-vs-jest) and [Mermaid vs D3.js vs Chart.js 2026](/guides/mermaid-vs-d3-vs-chartjs-diagrams-data-visualization-2026), [Fabric.js vs Konva vs PixiJS: Canvas & 2D Graphics 2026](/guides/fabricjs-vs-konva-vs-pixijs-canvas-2d-graphics-2026).*
