Native TypeScript graph layout algorithms built directly on
@statelyai/graph.
This is not a new graph interchange format. Public APIs consume Graph and
return VisualGraph; positions remain node fields. Standalone routing adds
immutable structured routes and incremental patches alongside the existing
GraphEdge.points layout output.
The native layered implementation covers the complete 152-option elkjs 0.11.1 layered inventory with one simplified typed name per ELK option. Flat and compound graphs, cross-hierarchy edges, ports, labels, self-loops, wrapping, four directions, constraints, and replaceable phases are differential-tested against elkjs, including a curated complex state-machine corpus in both primary layout orientations. Native layered layout also supports partial selection, route-only execution, and geometric constraints. Incremental layout remains explicitly unsupported.
pnpm add @statelyai/layout @statelyai/graphimport { createGraph } from "@statelyai/graph";
import { getLayeredLayout, getLayout } from "@statelyai/layout";
const graph = createGraph({
nodes: [{ id: "a" }, { id: "b" }],
edges: [{ id: "ab", sourceId: "a", targetId: "b" }],
});
const visualGraph = getLayeredLayout(graph, { direction: "right" });
const result = await getLayout({
graph,
algorithm: "layered",
options: { direction: "right" },
});
result.graph;
result.patches;
result.diagnostics;
result.metrics;const result = getLayeredLayout(graph, {
direction: "down",
padding: { top: 24, right: 40, bottom: 24, left: 40 },
compound: (node) => ({
header: { width: 200, height: 60, side: "top" },
direction: "right",
}),
edgeAttachment: (edge) => ({
source: "content", // Use "outer" for an outward/reentering transition.
}),
});
result.compoundGeometry.get("parent"); // { bounds, header, content }
result.compoundRoutes; // World-space sections, label gaps, routing diagnostics.Each scope reserves children and measured labels before its ancestors are laid
out. Header measurements cannot replace finalized compound dimensions. Node
positions and compound bounds are parent-relative; edge label positions and
routes are world-relative, marked by edgeCoordinateSpace: "world". Header and content rectangles are local to the
compound. For ancestor-to-descendant edges, content attachments leave inward;
outer attachments leave outward. Explicit ports retain their node-local geometry.
The native compound router emits orthogonal route sections for the original endpoints.
Use compoundRoutes or getLayoutRoutes(result) to retain label gaps and
fallback diagnostics; legacy edge.points flattens the sections. getLayout
also reports routing diagnostics in its result. Compound routing currently uses
orthogonal sections independently of the flat layout routing setting.
These options belong to the native contract; they do not change the pinned ELK compatibility contract. The existing simplified advanced options still map to ELK option IDs, but identical geometry is not guaranteed between contracts.
import { getDiff } from "@statelyai/graph";
import { orthogonalRouting, toSvgPath } from "@statelyai/layout/routing";
const previous = orthogonalRouting.route(graph);
const { snapshot, patches } = orthogonalRouting.update(
nextGraph,
previous,
getDiff(graph, nextGraph),
);
const route = snapshot.routes.get("ab");
const paths = route?.sections.map((section) => toSvgPath(section.path));Incremental results match fresh routing for the same graph and settings. Previous paths are cached outputs, never constraints on new routes.
Every strategy supports incremental updates with immutable snapshots and shared indexes. Nodes, ports, and labels stay fixed. Every edge receives a route; fallbacks expose status and diagnostics. Deterministic soft crossing/overlap costs coordinate unrelated groups; residual conflicts stay visible and reported. Native TypeScript strategies cover straight, Bézier, orthogonal, polyline, octilinear, curved, organic, parallel, self-loop, fan, bus, and bundled routes. Render curves directly or flatten them for a lines-only renderer. ELK/native layout adapters preserve existing output.
For efficient dragging, supply the edit's diff directly: getDiff scans the
whole graph. See routing contracts, algorithms, and examples.
Select nodes and edges independently. Edge selection never moves endpoints:
import { c, getLayout } from "@statelyai/layout";
const result = await getLayout({
graph,
scope: {
mode: "partial",
edgeIds: ["ab", "bc"],
edgeGeometry: "labels",
routing: "selected",
},
constraints: [
c.align({
id: "label-centers",
entities: [
{ edgeId: "ab", part: "label" },
{ edgeId: "bc", part: "label" },
],
axis: "x",
anchor: "center",
}),
],
});nodeIds permits position changes; edgeIds permits routes, label positions,
or both. routing: "affected" (default) also repairs edges affected by moved
nodes, within edgeGeometry permissions. Unselected nodes, dimensions, ports,
and topology remain fixed. Results include field-specific graph patches and
repair/conflict diagnostics. Constraints support alignment, distribution, pins,
linear equalities/inequalities, and route waypoints.
See authoring layout for baseline requirements, selection semantics, routing limits, and examples. Existing ELK compatibility behavior is unchanged.
Legacy consumers can migrate through an isolated compatibility entry point:
import ELK from "@statelyai/layout/elkjs";
const elk = new ELK();
const legacyResult = await elk.layout(elkJsonGraph);Migration-compatible package aliases are also available for
lib/main.js, lib/elk-api.js, lib/elk.bundled.js, lib/elk-worker.js,
and lib/elk-worker.min.js. The worker entries implement elkjs's message
protocol in-process, including custom workerFactory construction and
termination. Both ESM imports and the original CommonJS require() style are
supported. Worker calls merge constructor defaults with per-layout overrides;
terminal worker errors reject pending and later requests.
The adapter accepts ELK JSON and option aliases, translates to
@statelyai/graph, runs native algorithms, and translates the result back.
Native algorithms never consume ELK JSON directly. getLayout and the
compatibility adapter dispatch through one typed internal engine; direct native
functions expose those same algorithm implementations. ELK-specific defaults
and quirks remain local to the pinned compatibility adapter.
Compatibility policies currently preserve exact elkjs 0.11.1 Random geometry and Box SIMPLE geometry, including provider bounds and whether edge sections are routed or left authored. Rectangle Packing has an exact default baseline; its full Java packing strategy remains in progress.
The compatibility entry's named graph, edge, option, and result types are
mutually assignable with the declarations shipped by elkjs@0.11.1. Its
default class also accepts the library's broader internal graph inputs.
Fixed port sides constrain routes without collapsing fan-out targets onto each other. Port preferences cannot reintroduce cycles before layering. Inline center labels use reserved inter-rank space; orthogonally routed labels use distinct cross-axis lanes that avoid other labels and states. Dedicated label layers and route-track compaction retain their placement strategies. Inline self-loop labels reserve clearance on their assigned sides from both their owner and neighboring nodes. Hierarchy decomposition preserves native self-loop routes for ancestor-to-descendant edges. FIRST/FIRST_SEPARATE nodes may have self-loops.
Advanced layered settings use shorter names such as
layering.strategy, spacing.edgeNode, and nodePlacement.strategy.
toElkLayeredOptions and fromElkLayeredOptionId provide the exact one-to-one
mapping when migration tooling needs ELK IDs. elkLayeredOptionDefinitions
exposes the complete mapping, value type, and valid graph-element targets;
elkLayeredEnumValues exposes every accepted enum value.
The browser lab contains the same 45 categorized examples as ELK Live, sourced
from the canonical eclipse/elk-models catalog. Each is pre-laid out with the
elkjs oracle after zero-sized nodes and ports receive consistent visual bounds.
The canonical ELKT source remains unchanged; elkjs is never bundled into the
browser.
The workbench places a CodeMirror JSON5 editor beside a coordinate-faithful SVG viewer in keyboard-accessible shadcn resizable panels. Selecting an example loads its complete XGraph into the editor; pasted or edited XGraph redraws automatically. Existing visual geometry is preserved; topology-only graphs run through native layered layout. Invalid input is marked inline while the last valid preview remains visible. Pan, zoom, selection details, and optional overlays expose exact node coordinates, edge-label rectangles, route points, routing modes, and node-relative ports.
pnpm demo:generate
pnpm demoThe demo opens at https://layout.localhost through Portless.
pnpm demo:sync refreshes the pinned ELK Live catalog and its converted ELK
JSON inputs. Normal generation and browser use remain offline.
The embed target defaults to http://localhost:3000. Override it with
?editor=http://localhost:4864 when the Viz editor runs elsewhere.
Layered phases are replaceable independently:
breakCyclesassignLayersminimizeCrossingsplaceNodesrouteEdges
Strategies exchange typed artifacts keyed by graph entity IDs. They never convert the public graph into an ELK-shaped API.
const result = getLayeredLayout(graph, {
strategies: {
routeEdges(input, orientation, placement) {
return myRouter(input, orientation, placement);
},
},
});See API reference, Architecture, Roadmap, and Upstream and provenance. Parity tracks API coverage separately from native algorithm fidelity.
pnpm install
pnpm verify
pnpm bench
pnpm demo
pnpm storybook
pnpm changeset
pnpm releasepnpm verify checks Oxfmt, Oxlint, source and repository TypeScript projects,
generated layered-option and demo-corpus freshness, tests,
declarations/runtime builds, demo and Storybook bundles, and the packed package surface.
pnpm storybook opens the authoring workbench at http://127.0.0.1:6018.
The Layout / Partial selection story starts with a fully laid-out graph.
Click nodes (or select the review branch), choose a direction, and press
Auto-layout selection. Unselected nodes remain fixed; dashed outlines show
previous positions. Reset restores the original full layout.
Eight additional stories cover edge-only routing, independently editable constraint groups,
selected-node placement, affected-route repair, constraint conflicts, overlap
diagnostics, nested coordinates, and unsupported incremental layout. Controls
rerun the real API; each story shows matched before/after geometry, patches, and
current limitations. pnpm storybook:build writes a static build to dist-storybook.
The source project checks indexed reads with noUncheckedIndexedAccess so
published TypeScript source supports consumers using that option.
Add a release note with pnpm changeset. When it reaches main, the release
workflow opens or updates a version pull request. Merging that pull request
publishes the package to npm and creates the GitHub release and tag.
Publishing uses npm Trusted Publishing through .github/workflows/release.yml.
The opt-in layoutStatechart export from @statelyai/layout/elkjs compiles
initial-state and preferred-path hints into scoped settings, then selects among
at most three fresh layout attempts. It returns all quality scores, including
remaining defects. See statechart policies for the
API, supported controls, tradeoffs and reproducible visual comparison.