A TypeScript graph library built on plain JSON objects. Supports directed/undirected graphs, hierarchical nodes, graph algorithms, visual properties, and serialization to DOT, GraphML, Mermaid, and more.
Made from our experience at stately.ai, where we build visual tools for complex systems.
Install
npm install @statelyai/graph
Optional peers are only needed for specific adapters:
| Package | Needed for |
|---|---|
fast-xml-parser |
@statelyai/graph/gexf, @statelyai/graph/graphml |
dotparser |
@statelyai/graph/dot parsing |
cytoscape |
Cytoscape integration tests and consumer typing |
d3-force |
D3 force integration tests and consumer typing |
elkjs |
@statelyai/graph/elk |
zod |
@statelyai/graph/schemas |
Highlights
- Plain JSON graphs with no runtime wrappers required
- Standalone functions with a consistent
get*/gen*/is*/add*naming model - Directed, undirected, hierarchical, and visual graph support
- Ports for node-editor and dataflow-style graphs
- Algorithms for traversal, paths, centrality, communities, connectivity, isomorphism, ordering, MST, and walks
- Diff/patch utilities for graph state changes
- Multi-format conversion via package subpaths
- Small, fast test suite with broad format coverage
Quick Start
Graphs are plain JSON-serializable objects. All operations are standalone functions — no classes, no DOM, no rendering engine.
import { createGraph, addNode, addEdge, getShortestPath } from '@statelyai/graph'; const graph = createGraph({ nodes: [ { id: 'a', label: 'Start' }, { id: 'b', label: 'Middle' }, { id: 'c', label: 'End' }, ], edges: [ { id: 'e1', sourceId: 'a', targetId: 'b' }, { id: 'e2', sourceId: 'b', targetId: 'c' }, ], }); // Mutate in place addNode(graph, { id: 'd', label: 'Shortcut' }); addEdge(graph, { id: 'e3', sourceId: 'a', targetId: 'd' }); // Algorithms work on the plain object const path = getShortestPath(graph, { from: 'a', to: 'c' });
Graph Manipulation
Look up, add, delete, and update nodes and edges. Query neighbors, predecessors, successors, degree, and more.
import { getNode, deleteNode, getNeighbors, getSources } from '@statelyai/graph'; const node = getNode(graph, 'a'); // lookup by id deleteNode(graph, 'd'); // removes node + connected edges const neighbors = getNeighbors(graph, 'a'); // adjacent nodes const roots = getSources(graph); // nodes with no incoming edges
Batch operations (addEntities, deleteEntities, updateEntities) let you apply multiple changes at once.
Hierarchy
Nodes support parent-child relationships for compound/nested graphs. Query children, ancestors, descendants, depth, and least common ancestor. Use flatten() to decompose into a flat leaf-node graph.
import { createGraph, getChildren, getLCA, flatten } from '@statelyai/graph'; const graph = createGraph({ nodes: [ { id: 'a' }, { id: 'b', initialNodeId: 'b1' }, { id: 'b1', parentId: 'b' }, { id: 'b2', parentId: 'b' }, { id: 'c' }, ], edges: [ { id: 'e1', sourceId: 'a', targetId: 'b' }, // resolves to a -> b1 { id: 'e2', sourceId: 'b1', targetId: 'b2' }, { id: 'e3', sourceId: 'b', targetId: 'c' }, // expands from all leaves of b ], }); const children = getChildren(graph, 'b'); // [b1, b2] const flat = flatten(graph); // only leaf nodes, edges resolved
Ports
Ports are optional named connection points on nodes. They are useful for flow-based systems, node editors, and dataflow graphs where edges need to target a specific input or output.
import { createGraph, getEdgesByPort, getPorts } from '@statelyai/graph'; const graph = createGraph({ nodes: [ { id: 'fetch', ports: [{ name: 'result', direction: 'out' }], }, { id: 'render', ports: [{ name: 'input', direction: 'in' }], }, ], edges: [ { id: 'e1', sourceId: 'fetch', sourcePort: 'result', targetId: 'render', targetPort: 'input', }, ], }); getPorts(graph, 'fetch'); // [{ name: 'result', ... }] getEdgesByPort(graph, 'render', 'input'); // [e1]
Algorithms
Includes traversal (BFS, DFS), pathfinding (shortest path, simple paths, all-pairs shortest paths), centrality/link analysis (degree, closeness, betweenness, PageRank, HITS, eigenvector), community detection (label propagation, Girvan-Newman, greedy modularity, modularity scoring), cycle detection, connected/strongly-connected components, bridges, articulation points, biconnected components, isomorphism, topological sort, minimum spanning tree, and more. Many algorithms have lazy generator variants (gen*) for early exit.
import { bfs, dfs, hasPath, isAcyclic, getShortestPath, getCycles, getTopologicalSort, getConnectedComponents, getMinimumSpanningTree, getPageRank, getLabelPropagationCommunities, genGirvanNewmanCommunities, getBridges, isIsomorphic, } from '@statelyai/graph'; for (const node of bfs(graph, 'a')) { /* breadth-first */ } for (const node of dfs(graph, 'a')) { /* depth-first */ } hasPath(graph, 'a', 'c'); // reachability isAcyclic(graph); // cycle check getShortestPath(graph, { from: 'a', to: 'c' }); // single shortest path getTopologicalSort(graph); // topological order (or null) getConnectedComponents(graph); // connected components getMinimumSpanningTree(graph, { weight: e => e.data?.weight ?? 1 }); // MST getPageRank(graph); // link analysis scores getLabelPropagationCommunities(graph); // community detection [...genGirvanNewmanCommunities(graph)]; // lazy community splits getBridges(graph); // bridge edges isIsomorphic(graph, otherGraph); // structural equivalence
Diff & Walks
Beyond classic graph algorithms, the library also includes utilities for evolving and exploring graph state:
getDiff(),getPatches(),applyPatches()for graph change trackinggenRandomWalk(),genWeightedRandomWalk(), and coverage helpers for model-based testing and simulationgetSubgraph()andreverseGraph()for structural transforms
Visual Graphs
createVisualGraph() guarantees x, y, width, height on all nodes and edges (default 0).
import { createVisualGraph } from '@statelyai/graph'; const diagram = createVisualGraph({ direction: 'right', nodes: [ { id: 'a', x: 0, y: 0, width: 120, height: 60, shape: 'rectangle' }, { id: 'b', x: 200, y: 0, width: 120, height: 60, shape: 'ellipse' }, ], edges: [{ id: 'e1', sourceId: 'a', targetId: 'b', width: 100, height: 100 }], });
Format Conversion
Import and export graphs to many formats. Converters are available as subpath imports.
import { toDOT } from '@statelyai/graph/dot'; import { fromGEXF } from '@statelyai/graph/gexf'; import { toCytoscapeJSON } from '@statelyai/graph/cytoscape'; import { toD3Graph } from '@statelyai/graph/d3'; const dot = toDOT(graph); // Graphviz DOT const cytoData = toCytoscapeJSON(graph); // Cytoscape.js JSON const d3Data = toD3Graph(graph); // D3.js { nodes, links } const imported = fromGEXF(gexfXmlString); // GEXF (Gephi)
Supported formats: Cytoscape.js JSON, D3.js JSON, JSON Graph Format, GEXF, GraphML, GML, TGF, DOT, Mermaid (flowchart, state, sequence, class, ER, mindmap, block), adjacency list, and edge list.
Each bidirectional format also has a converter object:
import { cytoscapeConverter } from '@statelyai/graph/cytoscape'; const cyto = cytoscapeConverter.to(graph); const back = cytoscapeConverter.from(cyto);
Some formats have optional peer dependencies: fast-xml-parser (GEXF, GraphML) and dotparser (DOT). All other formats are dependency-free.
Format-specific docs live alongside the source:
Examples
The repo includes runnable examples under examples/:
- Flow-based math shows ports, topological ordering, and value propagation.
- Async workflow models an n8n/Zapier-style workflow with ports and dependency-aware execution.
Development
See CONTRIBUTING.md for contributor conventions, format-module checklist, and release notes guidance.
Why this library?
Graph file formats define how to store graphs. Visualization libraries define how to render them. This library is the computational layer in between: plain JSON objects in, algorithms and mutations, plain JSON objects out.
GEXF file → fromGEXF() → Graph → run algorithms, mutate → toCytoscapeJSON() → render
Your Graph is a plain object that survives JSON.stringify, structuredClone, postMessage, and localStorage without adapters.
License
MIT