Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
8689cda
chore: add dev deps for new layout engines (FA2, d3-hierarchy, webcol…
davidkpiano Jun 12, 2026
9ee5a99
feat(layout): genLayoutTransition, translateGraph/centerGraph, constr…
davidkpiano Jun 12, 2026
96ab665
feat(layout): ForceAtlas2, tidy-tree (d3-hierarchy), and WebCola adap…
davidkpiano Jun 12, 2026
fe67196
feat(layout): cytoscape headless layout bridge
davidkpiano Jun 12, 2026
e65f46e
build: wire forceatlas2/d3-hierarchy/webcola/cytoscape layout subpath…
davidkpiano Jun 12, 2026
a0a3957
perf(paths): lazy path materialization + typed-array heap in Dijkstra…
davidkpiano Jun 12, 2026
7742b52
docs: benchmarks page, graphlib migration guide, React Flow + ELK pip…
davidkpiano Jun 12, 2026
0498d52
feat(algorithms): k-core, Katz, hardened eigenvector, bipartite match…
davidkpiano Jun 12, 2026
f33ed2a
fix(layout): elk constraint spread typecheck (closure-mutated flag na…
davidkpiano Jun 12, 2026
ffe8033
fix(layout): ELK seed→randomSeed, partition flag CFA fix, dagre peer …
davidkpiano Jun 12, 2026
51ef0de
docs(readme): sync peers table, algorithms, layout section (8 adapter…
davidkpiano Jun 12, 2026
f73edb6
docs: algorithms reference (70 functions, complexity + semantics)
davidkpiano Jun 12, 2026
51b06ed
perf(queries): O(1) getDegree via cached non-directed self-loop count…
davidkpiano Jun 12, 2026
0e5982a
feat(xyflow): emit labels where renderers read them (edge.label top-l…
davidkpiano Jun 12, 2026
079491d
docs(benchmarks): refresh from 2026-06-12 run — degree sweep at parit…
davidkpiano Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/bright-layout-suite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@statelyai/graph': minor
---

Layout suite round two: transitions, geometry utilities, portable constraints, and four more engines.

- **`genLayoutTransition(from, to, options?)`** (`@statelyai/graph/layout`, zero-dep) — tween between two layouts of the same graph: yields interpolated `LayoutFrame`s (drive with `applyLayoutFrame`, one per animation frame) and returns the target layout. Lay out with one engine, re-lay out with another, morph live. Options: `steps` (default 30), `ease` (default smoothstep).
- **Geometry utilities** (`@statelyai/graph/layout`) — `translateGraph(graph, dx, dy)` and `centerGraph(graph, rect)` (**mutable**, in place): shift/center node positions, edge route `points`, and edge label rects. Hierarchy-aware — parent-relative children and container-relative edge routes are left alone.
- **`LayoutOptions.constraints`** — portable, advisory layout constraints. First constraint: `layer(node)` assigns nodes to ordered layers along the flow axis. ELK maps it to partitions (`elk.partitioning.partition`); the Graphviz `dot` engine maps it to `{ rank=same; … }` groups; engines without a layer concept ignore it.
- **`@statelyai/graph/layout/forceatlas2`** — `getForceAtlas2Layout` (sync; optional peers `graphology` + `graphology-layout-forceatlas2`): seeded determinism, native pinning via `isFixed`, edge `weight` influence.
- **`@statelyai/graph/layout/d3-hierarchy`** — `getTidyTreeLayout` (sync; optional peer `d3-hierarchy`): Reingold–Tilford tidy tree. Root from `rootId` → `initialNodeId` → unique source; forests supported; non-tree extra edges preserved (spanning-tree layout).
- **`@statelyai/graph/layout/webcola`** — `getColaLayout` (sync; optional peer `webcola`): constraint-based layout with overlap avoidance, seeded determinism, `isFixed` pinning, DAG flow via `direction`.
- **`@statelyai/graph/layout/cytoscape`** — `getCytoscapeLayout` (async; optional peer `cytoscape`, headless): bridges cytoscape's layout ecosystem (`grid`, `circle`, `concentric`, `breadthfirst`, `cose`, plus caller-registered extensions via the injectable `cy` option). Compound nodes map to cytoscape parents.

The package smoke test exercises all nine layout entry points against the packed tarball.
5 changes: 5 additions & 0 deletions .changeset/calm-degree-sweep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@statelyai/graph': patch
---

`getDegree` is now O(1) per call: `|out| + |in|` corrected by a cached per-node count of non-directed self-loops (revalidated by index version + graph mode, like the CSR snapshot). A full degree sweep over a 100k-node/300k-edge graph drops from ~148 ms to ~10 ms — at parity with ngraph and graphology, which was the one benchmark cell this library lost across the board.
13 changes: 13 additions & 0 deletions .changeset/keen-analysis-tail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@statelyai/graph': minor
---

Analytical coverage tail: cores, Katz, bipartite matching, min-cut, seeded label propagation, and graph generators.

- **k-core** — `getCoreNumbers(graph)` (Batagelj–Zaveršnik, O(m)) and `getKCore(graph, k)`; degrees are undirected per the standard definition.
- **Katz centrality** — `getKatzCentrality(graph, { alpha, beta, getWeight, ... })`; throws a descriptive error when `alpha` exceeds the spectral bound and iteration diverges.
- **Eigenvector centrality** hardened — `(A+I)`-shifted power iteration (no more bipartite oscillation), `getWeight` support, descriptive non-convergence error. Differentially tested against graphology.
- **Bipartite** — `isBipartite(graph)` and `getMaximumBipartiteMatching(graph)` (Hopcroft–Karp, O(m√n)); the non-bipartite error names the edge that closes the odd cycle.
- **Min-cut** — `getMinCut(graph, { source, sink, getCapacity? })` → `{ value, cutEdges, partition }`, sharing the max-flow solver (`value` always equals `getMaxFlow(...)` by construction).
- **Seeded label propagation** — `getLabelPropagationCommunities` gains `seed`: asynchronous LPA with seeded shuffling/tie-breaking, deterministic per seed.
- **Generators** — `createCompleteGraph(n)`, `createGridGraph(rows, cols)`, `createRandomGraph(n, p, { seed })` (G(n,p), deterministic per seed) in the root export.
5 changes: 5 additions & 0 deletions .changeset/quick-lazy-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@statelyai/graph': patch
---

Pathfinding internals: lazy path materialization and a typed-array heap. `genShortestPaths` now reconstructs a path only when it is actually yielded (abandoning the generator early skips the work), and the Dijkstra/A*/bidirectional hot loops use a Float64Array/Int32Array binary heap instead of object nodes. Same API, same results — measured −70% on first-path-then-stop, −41% on all-targets, −71% on single-target early exit (10k-node graph).
5 changes: 5 additions & 0 deletions .changeset/true-label-spots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@statelyai/graph': minor
---

xyflow: labels now land where the renderers actually read them. `toXYFlow` emits edge labels as the top-level `edge.label` (the prop React Flow / Svelte Flow render — previously the label went to `edge.data.label`, which built-in edges ignore) and node labels as `data.label` (what React Flow's default node renders). `fromXYFlow` reads both spots back for external React Flow input, and full-fidelity round-tripping via the `__statelyai` metadata is unchanged. If you relied on `edge.data.label` in `toXYFlow` output, read `edge.label` instead.
52 changes: 38 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,28 @@ Optional peers are only needed for specific adapters:

<!-- optional peer dependencies derived from package.json#peerDependencies -->

| 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` |
| Package | Needed for |
| ------------------------------------------- | --------------------------------------------------- |
| `fast-xml-parser` | `@statelyai/graph/gexf`, `@statelyai/graph/graphml` |
| `dotparser` | `@statelyai/graph/dot` parsing |
| `zod` | `@statelyai/graph/schemas` |
| `elkjs` | `@statelyai/graph/elk`, `@statelyai/graph/layout/elk` |
| `@dagrejs/dagre` | `@statelyai/graph/layout/dagre` |
| `@hpcc-js/wasm-graphviz` | `@statelyai/graph/layout/graphviz` |
| `d3-force` | `@statelyai/graph/layout/d3-force` |
| `graphology`, `graphology-layout-forceatlas2` | `@statelyai/graph/layout/forceatlas2` |
| `d3-hierarchy` | `@statelyai/graph/layout/d3-hierarchy` |
| `webcola` | `@statelyai/graph/layout/webcola` |
| `cytoscape` | `@statelyai/graph/layout/cytoscape`, Cytoscape format typing |

## Highlights

- Plain JSON graphs with no runtime wrappers required; omitted `data` defaults to `null`
- 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
- Algorithms for traversal, paths, centrality, communities, connectivity, flow/cuts, matching, cores, isomorphism, ordering, MST, and walks
- Pluggable layout over eight external engines (ELK, Graphviz, dagre, d3-force, ForceAtlas2, tidy tree, WebCola, cytoscape) — pure functions, optional peers
- Diff/patch utilities for graph state changes
- Multi-format conversion via package subpaths, with fidelity claims tested against fixtures
- Small, fast test suite with broad format coverage
Expand Down Expand Up @@ -188,7 +194,7 @@ const parsed = GraphSchema.parse(unknownValue);

<!-- algorithm functions exported from src/algorithms.ts -->

Includes traversal (BFS, DFS, preorder/postorder), pathfinding (shortest path, simple paths, all-pairs shortest paths, A*), centrality/link analysis (degree, closeness, betweenness, PageRank, HITS, eigenvector), community detection (Louvain, label propagation, Girvan-Newman, greedy modularity, modularity scoring), flow (max-flow/min-cut), cycle detection, connected/strongly-connected components, bridges, articulation points, biconnected components, dominator trees, transitive reduction, isomorphism, topological sort, minimum spanning tree, and more. Many algorithms have lazy generator variants (`gen*`) for early exit.
Includes traversal (BFS, DFS, preorder/postorder), pathfinding (shortest path, simple paths, all-pairs shortest paths, A*, bidirectional Dijkstra), centrality/link analysis (degree, closeness, betweenness, PageRank, HITS, eigenvector, Katz), community detection (Louvain, label propagation, Girvan-Newman, greedy modularity, modularity scoring), flow & cuts (`getMaxFlow`, `getMinCut`), bipartite analysis (`isBipartite`, Hopcroft–Karp `getMaximumBipartiteMatching`), k-cores (`getCoreNumbers`, `getKCore`), cycle detection, connected/strongly-connected components, bridges, articulation points, biconnected components, dominator trees, transitive reduction, isomorphism, topological sort, minimum spanning tree, and seeded graph generators (`createCompleteGraph`, `createGridGraph`, `createRandomGraph`). Many algorithms have lazy generator variants (`gen*`) for early exit. See [docs/algorithms.md](./docs/algorithms.md) for the full reference.

Hot algorithm loops (centrality, components) run on an internal compressed-sparse-row snapshot — cached and invalidated transparently like the rest of the index — so they stay fast on large graphs without changing the plain-JSON model. Algorithm results are differential-tested against graphology on seeded random graphs.

Expand Down Expand Up @@ -240,17 +246,24 @@ isIsomorphic(graph, otherGraph); // structural equivalence

## Layout

Plug-and-play layout over external engines — pure functions in, positioned `VisualGraph` out (node positions, routed edge `points`, computed edge-label rects). No layout algorithms of our own; each adapter is a subpath with an optional peer dependency.
<!-- layout adapters under src/layout/*.ts and helpers exported from src/layout/index.ts -->

Plug-and-play layout over external engines — pure functions in, positioned `VisualGraph` out. No layout algorithms of our own; each adapter is a subpath with an optional peer dependency. The hierarchical engines (ELK, dagre, Graphviz) also produce routed edge `points` and computed edge-label rects; the physics/tree/cytoscape engines position nodes only.

```ts
import { getElkLayout } from '@statelyai/graph/layout/elk'; // elkjs
import { getDagreLayout } from '@statelyai/graph/layout/dagre'; // @dagrejs/dagre
import { getGraphvizLayout } from '@statelyai/graph/layout/graphviz'; // @hpcc-js/wasm-graphviz
import { getGraphvizLayout } from '@statelyai/graph/layout/graphviz'; // @hpcc-js/wasm-graphviz (8 engines)
import { genForceLayout } from '@statelyai/graph/layout/d3-force'; // d3-force
import { applyLayoutFrame, getLayoutBounds } from '@statelyai/graph/layout';
import { getForceAtlas2Layout } from '@statelyai/graph/layout/forceatlas2'; // graphology FA2
import { getTidyTreeLayout } from '@statelyai/graph/layout/d3-hierarchy'; // d3-hierarchy
import { getColaLayout } from '@statelyai/graph/layout/webcola'; // webcola (constraints)
import { getCytoscapeLayout } from '@statelyai/graph/layout/cytoscape'; // cytoscape ecosystem
import { applyLayoutFrame, getLayoutBounds, centerGraph } from '@statelyai/graph/layout';

const laidOut = await getElkLayout(graph, {
measure: (node) => measureText(node.label), // text measurement stays yours
constraints: { layer: (node) => node.data?.tier }, // portable layer constraint
});

// Physics layouts are generators — one tick per frame, cancel by stopping
Expand All @@ -260,7 +273,7 @@ for (const frame of genForceLayout(graph, { seed: 42 })) {
}
```

Edge `x`/`y`/`width`/`height` are canonically the edge-label rect; routes live in `edge.points` (`routing` says how to interpret them). Layouts are plain JSON — diff them with `getPatches` to animate transitions between engines.
Edge `x`/`y`/`width`/`height` are canonically the edge-label rect; routes live in `edge.points` (`routing` says how to interpret them). Layouts are plain JSON — tween between engines with `genLayoutTransition`, or diff them with `getPatches`. See [docs/layout.md](./docs/layout.md) and [docs/layout-transitions.md](./docs/layout-transitions.md).

## Diff & Walks

Expand Down Expand Up @@ -371,6 +384,17 @@ Format-specific docs live alongside the source:
- [xyflow](./src/formats/xyflow/README.md)
- [Converter helpers](./src/formats/converter/README.md)

## Guides

<!-- guide documents under docs/*.md -->

- [Layout guide](./docs/layout.md) — the adapter contract, all eight engines, constraints, sizing, web workers
- [Layout transitions](./docs/layout-transitions.md) — tween between engines; layouts are just data
- [Algorithms reference](./docs/algorithms.md) — every algorithm with complexity and semantics notes
- [Benchmarks](./docs/benchmarks.md) — measured against graphology, ngraph, graphlib, and cytoscape
- [Migrating from graphlib](./docs/migrating-from-graphlib.md)
- [React Flow + ELK pipeline](./docs/react-flow-elk-pipeline.md) — measured nodes, worker layout, live re-layout

## Examples

<!-- runnable example files under examples/ -->
Expand Down
Loading
Loading