Graph Layout
Green · 2026-07-02 Node + Browser GraphHierarchical (Sugiyama-style) DAG layout. Single-call spec → positions + edge routing.
- Targets
- Node + Browser
- Version
- 0.1.1
Install
pnpm add @amigo-labs/graph-layoutBenchmarks
Trend (6 pts)Benchmark
small (20 nodes, 25 edges)
- @amigo-labs/graph-layout napi 8.94K hz · 62.26×
- @dagrejs/dagre 144 hz
Benchmark
medium (100 nodes, 140 edges)
- @amigo-labs/graph-layout napi 1.71K hz · 74.97×
- @dagrejs/dagre 22.8 hz
README
@amigo-labs/graph-layout
Hierarchical (Sugiyama-style) DAG layout. One call per graph — spec in, positions + edge routing out. Replaces
dagre/@dagrejs/dagrefor Node-side rendering (Mermaid, ReactFlow-SSR, Docusaurus-build).
Install
pnpm add @amigo-labs/graph-layout
Usage
import { layout, layoutMany } from '@amigo-labs/graph-layout'
const result = layout({
nodes: [
{ id: 'a', width: 100, height: 40 },
{ id: 'b', width: 100, height: 40 },
{ id: 'c', width: 100, height: 40 },
],
edges: [
{ source: 'a', target: 'b' },
{ source: 'a', target: 'c' },
],
options: { rankdir: 'TB', nodesep: 60, ranksep: 80 },
})
// result.nodes → [{ id, x, y, width, height }, ...]
// result.edges → [{ source, target, points: [{x,y}, {x,y}] }, ...]
// result.width → total bounding-box width
// result.height → total bounding-box height
// Batch N layouts in a single FFI call (CI-time graph rendering):
layoutMany([spec1, spec2, spec3])
Install for the browser
The same import works in Angular, React, Vite, esbuild, and webpack ≥ 5 — the bundler picks the WASM build via the browser conditional export:
import { layout, layoutMany } from '@amigo-labs/graph-layout'
The _graph-layout-core engine is the same code on both sides, so node positions are identical between Node and the browser.
Options
interface LayoutOptions {
rankdir?: 'TB' | 'BT' | 'LR' | 'RL' // default 'TB'
nodesep?: number // default 50
ranksep?: number // default 50
marginx?: number // default 0
marginy?: number // default 0
}
interface NodeSpec {
id: string
width: number
height: number
rank?: number // pin to a specific rank
}
interface EdgeSpec {
source: string
target: string
minlen?: number // minimum rank distance. default 1
weight?: number // crossing-reduction weight. default 1
}
Scope
- Sugiyama-style hierarchical layout (layered DAG).
- Longest-path ranker with topological fallback for cycles.
- Barycentric crossing-reduction (4 sweeps).
- Straight-line two-point edge routing (renderers add splines).
- Pinned ranks (per-node
rankoverride).
Not in scope (v0.1)
- graphlib chain-API (
g.setNode(...),g.setEdge(...)). Each call would cost an FFI crossing. Use the one-spec form. - network-simplex / tight-tree rankers. Longest-path covers the typical use-cases.
- Spline edge routing. Renderers apply their own.
- Edge labels. No dummy-node insertion for label positioning.
See __conformance__/divergences.md.
License
MIT
Perf review
Perf-Review: @amigo-labs/graph-layout
Status: 🟢 Green (measured) · Reviewed: 2026-07-02 · Version: 0.1.1
Verdict
62.3× (20 nodes / 25 edges) and 74.97× (100 nodes / 140 edges) vs. @dagrejs/dagre (bench 2026-06-10). The candidate review predicted Green with a 2–5× expectation; the measurement lands far above it. Honest caveat on the multiplier: part of it is algorithm choice, not a pure Rust win — we ship a longest-path ranker with 4 barycentric crossing-reduction sweeps, while dagre runs network-simplex ranking plus 24 sweeps. Same visual class of layout, materially less work per layout. Crossing counts can be 5–15 % higher than dagre’s on dense graphs.
Evidence
Measured speedup (docs/benchmarks/graph-layout.json, 2026-06-10, commit 8c743bf)
| Scenario | @amigo-labs/graph-layout | @dagrejs/dagre | Speedup |
|---|---|---|---|
| small DAG (20 nodes, 25 edges) | 8 936.36 Hz | 143.54 Hz | 62.3× |
| medium DAG (100 nodes, 140 edges) | 1 708.74 Hz | 22.79 Hz | 74.97× |
docs/packages.jsonspeedup:"62–75× faster".- Install size: 436 KB vs
dagre’s 2.7 MB /@dagrejs/dagre’s 1.6 MB.
What shipped vs. the candidate prediction
- Not a drop-in — a spec-object API (
layout(spec) → positions) instead of dagre’s mutable graphlib graph. - Longest-path ranker only (no network-simplex option yet).
- Straight-line 2-point edges — no dagre-style edge points/label nodes.
- Cycle reversal is skipped in v0.1 (inputs must be DAGs).
- Extras beyond dagre:
layoutManybatch API and pinned ranks.
Divergences
Coordinates are not dagre-identical (different ranker + sweep count); rank ordering and layer assignment match on the conformance corpus. On dense graphs expect somewhat more edge crossings than dagre. See crates/graph-layout/__conformance__/divergences.md.
Pre-port assessment: dagre.md
References
- Crate:
crates/graph-layout - Bench shard:
docs/benchmarks/graph-layout.json docs/packages.jsonspeedup:"62–75× faster"