Graph Layout

Green Node + Browser Graph
napi62–75× faster

Hierarchical (Sugiyama-style) DAG layout. Single-call spec → positions + edge routing.

Version
0.1.1

Install

pnpm add @amigo-labs/graph-layout

Benchmarks

Trend (6 pts)

Benchmark

small (20 nodes, 25 edges)

62.26× vs slowest
  • @amigo-labs/graph-layout napi 8.94K hz · 62.26×
  • @dagrejs/dagre 144 hz

Benchmark

medium (100 nodes, 140 edges)

74.97× vs slowest
  • @amigo-labs/graph-layout napi 1.71K hz · 74.97×
  • @dagrejs/dagre 22.8 hz
Performance trend for Graph Layout
6 commits · last 2026-06-10

README

@amigo-labs/graph-layout

Hierarchical (Sugiyama-style) DAG layout. One call per graph — spec in, positions + edge routing out. Replaces dagre / @dagrejs/dagre for 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 rank override).

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/dagreSpeedup
small DAG (20 nodes, 25 edges)8 936.36 Hz143.54 Hz62.3×
medium DAG (100 nodes, 140 edges)1 708.74 Hz22.79 Hz74.97×
  • docs/packages.json speedup: "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: layoutMany batch 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.json speedup: "62–75× faster"