Force Layout

Green Node + Browser Graph
napi3.6–7.2× faster

Force-directed graph simulation. Batch-mode d3-force replacement for SSR / precompute workloads.

Version
0.1.1

Install

pnpm add @amigo-labs/force-layout

Benchmarks

Trend (6 pts)

Benchmark

small (20 nodes)

7.18× vs slowest
  • @amigo-labs/force-layout napi 1.22K hz · 7.18×
  • d3-force 170 hz

Benchmark

medium (100 nodes)

3.58× vs slowest
  • @amigo-labs/force-layout napi 56.6 hz · 3.58×
  • d3-force 15.8 hz
Performance trend for Force Layout
6 commits · last 2026-06-10

README

@amigo-labs/force-layout

Force-directed graph layout — batch-mode simulation (many-body + spring + centre + collision). Replaces d3-force for SSR / precompute workloads where you don’t need per-tick callbacks.

Install

pnpm add @amigo-labs/force-layout

Usage

import { simulate } from '@amigo-labs/force-layout'

const { nodes } = simulate(
  [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
  [
    { source: 'a', target: 'b', distance: 60 },
    { source: 'b', target: 'c', distance: 60 },
  ],
  { iterations: 300, charge: -30, centerStrength: 0.1 },
)

// nodes → [{ id, x, y, vx, vy }, ...]

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 { simulate } from '@amigo-labs/force-layout'

The _force-layout-core simulation is the same code on both sides, so layouts are identical between Node and the browser.

Options

interface SimulationOptions {
  iterations?: number      // default 300
  charge?: number          // many-body strength (negative = repulsion). default -30
  collideRadius?: number   // collision radius (0 disables). default 0
  centerX?: number          // default 0
  centerY?: number          // default 0
  centerStrength?: number  // default 0.1
  alpha?: number           // default 1
  alphaDecay?: number      // default ≈ computed from iterations
  velocityDecay?: number   // default 0.4
}

interface NodeSpec {
  id: string
  x?: number      // starting x (default phyllotaxis spiral)
  y?: number      // starting y
  fixed?: boolean // pin to (x, y) — skip forces
}

interface EdgeSpec {
  source: string
  target: string
  distance?: number    // target link length. default 30
  strength?: number    // spring strength in [0,1]. default 1 / min(inDeg, outDeg)
}

Scope

  • Many-body (repulsion) — O(V²) brute-force.
  • Link (Hooke spring with degree-weighted bias).
  • Centering.
  • Collision (hard-sphere overlap resolution).
  • Pinned nodes.

Scope cuts

  • No tick callback. One-shot simulate() returns final positions. Animation loops stay on d3.
  • No force composition. simulate() takes a fixed force stack.
  • No per-node / per-link strength functions. Constants only.
  • O(V²) many-body — at >1000 nodes the Barnes-Hut wins. v0.2 will add a quadtree.

See __conformance__/divergences.md.

License

MIT

Perf review

Perf-Review: @amigo-labs/force-layout

Status: 🟢 Green (measured) · Reviewed: 2026-07-02 · Version: 0.1.1

Verdict

7.18× (20 nodes) and 3.58× (100 nodes) vs. d3-force (bench 2026-06-10). The candidate review predicted Yellow leaning Green and feared sub-2× on small graphs; the measurement beats that prediction on both buckets — the small-graph bucket clears the candidate’s ≥1× gate by a wide margin, and the median bucket sits above the ≥2× Green threshold. The structural lever is the batch shape: one simulate() call runs all iterations in Rust, so the per-tick FFI crossings that would dominate a d3-style tick loop never happen.

Evidence

Measured speedup (docs/benchmarks/force-layout.json, 2026-06-10, commit 8c743bf)

Scenario@amigo-labs/force-layoutd3-forceSpeedup
small graph (20 nodes)1 221.01 Hz170.16 Hz7.18×
medium graph (100 nodes)56.61 Hz15.83 Hz3.58×
  • docs/packages.json speedup: "3.6–7.2× faster".
  • Install size: 404 KB vs d3-force’s 169 KB — we are larger; the win is compute, not footprint.

Benchmark gaps

  • Large bucket (500/800 nodes) not benched. Relevant because of the O(V²) many-body caveat below — the crossover point vs. d3’s Barnes–Hut is unmeasured.

What shipped vs. the candidate prediction

  • Batch simulate() only — no per-tick callback API. Layouts that need animated ticks stay on d3-force by design.
  • O(V²) many-body force instead of Barnes–Hut. Honest caveat: above roughly 1000 nodes d3’s O(V log V) approximation should win; the shipped sweet spot is the ≤ a-few-hundred-nodes dashboard graph.
  • Multiplicative alpha decay and a deterministic RNG — same layout for the same input, unlike d3’s Math.random() jitter.

Divergences

Not coordinate-identical to d3-force (deterministic seeding, decay-schedule differences); topology-level parity (cluster separation, link-length distribution) is what the conformance suite asserts. See crates/force-layout/__conformance__/divergences.md.

Pre-port assessment: d3-force.md

References

  • Crate: crates/force-layout
  • Bench shard: docs/benchmarks/force-layout.json
  • docs/packages.json speedup: "3.6–7.2× faster"