Use a shipped layout when you want a graph arranged from its connections rather than hand-authored coordinates. Start with a left-to-right pipeline, rerun it from a button, then insert a node with an incremental pass that preserves positions outside the affected neighborhood.
Specs describe the graph; the engine owns its geometry. The framework component's layout prop selects the initial arrangement. For subsequent work, get the DiagramEngine through the mounted DiagramInstance and await layout().
1. Choose an algorithm
You do not need to register adapters before using these names.
| Graph | Layout name | What you get |
|---|---|---|
| Flowcharts, pipelines, DAGs | elk, layered, dagre | Layered ranking; ELK also handles ports and nesting. |
| System diagrams with zones | architecture | Regions on a grid, boxes sized to their words, and bends in the gutters. |
| Hierarchies, org charts | tree | A tidy, parent-centered hierarchy. |
| Networks, clusters | force, community, spectral | Physical spread or grouping by related nodes. |
| Catalogs, galleries | grid, circular, radial | Uniform placement. |
| No predetermined choice | auto | Algorithm selection based on the graph. |
An unknown name throws an error listing the registered layouts. Calling layout() without a name uses auto.
Install the packages for your framework in your own browser application.
JavaScript:
bashnpm install @grafloria/element @grafloria/engine @grafloria/renderer
Angular:
bashnpm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/engine @grafloria/renderer rxjs @grafloria/element
Qwik:
bashnpm install @grafloria/qwik @grafloria/engine @grafloria/renderer @builder.io/qwik @grafloria/element
React:
bashnpm install @grafloria/react @grafloria/engine @grafloria/renderer react react-dom @grafloria/element
Vue:
bashnpm install @grafloria/vue @grafloria/engine @grafloria/renderer vue @grafloria/element
2. Define a pipeline and its insertion operation
Save this shared file beside the framework sample you choose below. The typed NodeSpec and EdgeSpec arrays describe six connected boxes, initially stacked at the origin. The layered layout separates them left to right. UnifiedLayoutOptions types the shared request's options.
insertNode() adds a labeled box and two connections to the live graph. Its incremental pass allows the new box and its immediate neighbors to move, while anchoring the rest. It returns the layout result, including a movement report and a tween plan; the samples repaint the committed positions rather than animate the plan.
This page requires the next release of @grafloria/engine: direction: 'LR' is unreleased and is not available in version 0.4.0. Use the samples after that release is available.
tsimport type { DiagramEngine, UnifiedLayoutOptions } from '@grafloria/engine';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
export const nodes: NodeSpec[] = ['n0', 'n1', 'n2', 'n3', 'n4', 'n5'].map((id) => ({
id,
label: id,
position: { x: 0, y: 0 },
size: { width: 110, height: 46 },
}));
export const edges: EdgeSpec[] = [
{ id: 'e0', source: 'n0', target: 'n1' },
{ id: 'e1', source: 'n1', target: 'n2' },
{ id: 'e2', source: 'n2', target: 'n3' },
{ id: 'e3', source: 'n3', target: 'n4' },
{ id: 'e4', source: 'n4', target: 'n5' },
];
const options: UnifiedLayoutOptions = { direction: 'LR', nodeSpacing: 40, rankSpacing: 80 };
export const layout = {
name: 'layered',
options,
};
export async function insertNode(engine: DiagramEngine) {
const model = engine.getDiagram();
const source = model?.getNode('n2');
const target = model?.getNode('n4');
const sourcePort = source?.getPorts().find((port) => port.alignment.side === 'right');
const targetPort = target?.getPorts().find((port) => port.alignment.side === 'left');
if (!sourcePort || !targetPort) throw new Error('The pipeline is not mounted');
const inserted = await engine.addNode({
type: 'rect',
position: { x: 0, y: 0 },
size: { width: 110, height: 46 },
});
inserted.setLabel('Inserted');
const input = inserted.getPorts().find((port) => port.alignment.side === 'left');
const output = inserted.getPorts().find((port) => port.alignment.side === 'right');
if (!input || !output) throw new Error('The new node has no side ports');
await engine.addLink({ sourcePortId: sourcePort.id, targetPortId: input.id });
await engine.addLink({ sourcePortId: output.id, targetPortId: targetPort.id });
return engine.layoutIncremental({ changed: [inserted.id], direction: 'LR', radius: 1 });
}
The new NodeModel comes from addNode(); its default side ports supply the endpoints for addLink(). The helper never replaces the existing nodes with their original coordinates. Each inserted node gets the engine's generated id.
3. Mount, rerun, and insert
Build on the framework mounting patterns in Edit nodes: here, the layout request arranges the pipeline, and the buttons rerun it or insert a node with an incremental pass.
On load you see n0 through n5 arranged left to right. Drag a box, then choose Rerun layout to arrange the whole graph again. Choose Insert node to add a branch through Inserted from n2 to n4; the incremental pass leaves nodes outside that one-hop region in place. The readout reports the total distance traveled by pre-existing nodes, in pixels.
tsimport { render } from '@grafloria/element';
import { nodes, edges, layout, insertNode } from './layout-demo';
export async function mountPipeline(container: HTMLElement): Promise<() => void> {
container.innerHTML = `
<button type="button" data-rerun disabled>Rerun layout</button>
<button type="button" data-insert disabled>Insert node</button>
<span data-report>Preparing layout</span>
<div data-canvas style="height:400px"></div>`;
const canvas = container.querySelector<HTMLElement>('[data-canvas]')!;
const rerun = container.querySelector<HTMLButtonElement>('[data-rerun]')!;
const insert = container.querySelector<HTMLButtonElement>('[data-insert]')!;
const report = container.querySelector<HTMLElement>('[data-report]')!;
const instance = render({ nodes, edges }, canvas);
async function run(add: boolean) {
rerun.disabled = insert.disabled = true;
try {
if (add) {
const result = await insertNode(instance.getEngine());
report.textContent = `Existing nodes moved ${result.movement.total.toFixed(1)} px`;
} else {
const result = await instance.getEngine().layout(layout.name, layout.options);
report.textContent = result.algorithm;
}
instance.fitView(40);
} finally {
rerun.disabled = insert.disabled = false;
}
}
rerun.onclick = () => { void run(false); };
insert.onclick = () => { void run(true); };
await run(false);
return () => { instance.dispose(); container.replaceChildren(); };
}
const container = document.createElement('section');
document.body.appendChild(container);
void mountPipeline(container);
tsimport { ChangeDetectorRef, Component, inject, signal, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
import { nodes, edges, layout, insertNode } from './layout-demo';
@Component({
selector: 'app-root',
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<button (click)="run(false)" [disabled]="!ready()">Rerun layout</button>
<button (click)="run(true)" [disabled]="!ready()">Insert node</button>
<span>{{ report() }}</span>
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
[layout]="layout" (layoutDone)="onLayoutDone()"
style="display:block; height:400px" />
`,
})
export class AppComponent {
private readonly cdr = inject(ChangeDetectorRef);
canvas = viewChild.required(DiagramCanvasComponent);
nodes: NodeSpec[] = nodes;
edges: EdgeSpec[] = edges;
layout = layout;
busy = signal(false);
ready = signal(false);
report = signal('Preparing layout');
onLayoutDone() {
this.canvas().fitToContent(40);
this.ready.set(true);
this.report.set('layered');
}
async run(add: boolean) {
const canvas = this.canvas();
const engine = canvas.activeEngine();
if (!engine) return;
this.busy.set(true);
try {
if (add) {
const result = await insertNode(engine);
this.report.set(`Existing nodes moved ${result.movement.total.toFixed(1)} px`);
} else {
await canvas.applyLayout();
}
canvas.scheduleRender();
canvas.fitToContent(40);
} finally {
this.busy.set(false);
this.cdr.detectChanges();
}
}
}
tsximport { component$, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';
import { nodes, edges, layout, insertNode } from './layout-demo';
export default component$(() => {
const instance = useSignal<NoSerialize<DiagramInstance>>();
const ready = useSignal(false);
const busy = useSignal(false);
const report = useSignal('Preparing layout');
return (
<section>
<button disabled={!ready.value || busy.value} onClick$={async () => {
const api = instance.value;
if (!api) return;
busy.value = true;
try {
const result = await api.getEngine().layout(layout.name, layout.options);
api.renderNow();
api.fitView(40);
report.value = result.algorithm;
} finally { busy.value = false; }
}}>Rerun layout</button>
<button disabled={!ready.value || busy.value} onClick$={async () => {
const api = instance.value;
if (!api) return;
busy.value = true;
try {
const result = await insertNode(api.getEngine());
api.renderNow();
api.fitView(40);
report.value = `Existing nodes moved ${result.movement.total.toFixed(1)} px`;
} finally { busy.value = false; }
}}>Insert node</button>
<span>{report.value}</span>
<div style={{ height: '400px' }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} layout={layout}
onInit$={(api) => { instance.value = noSerialize(api); }}
onLayoutDone$={() => {
instance.value?.renderNow();
instance.value?.fitView(40);
ready.value = true;
report.value = 'layered';
}} />
</div>
</section>
);
});
tsximport { useRef, useState } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';
import { nodes, edges, layout, insertNode } from './layout-demo';
export default function Pipeline() {
const instance = useRef<DiagramInstance | null>(null);
const [ready, setReady] = useState(false);
const [busy, setBusy] = useState(false);
const [report, setReport] = useState('Preparing layout');
async function run(add: boolean) {
const api = instance.current;
if (!api) return;
setBusy(true);
try {
if (add) {
const result = await insertNode(api.getEngine());
setReport(`Existing nodes moved ${result.movement.total.toFixed(1)} px`);
} else {
const result = await api.getEngine().layout(layout.name, layout.options);
setReport(result.algorithm);
}
api.fitView(40);
} finally { setBusy(false); }
}
return (
<section>
<button disabled={!ready || busy} onClick={() => void run(false)}>Rerun layout</button>
<button disabled={!ready || busy} onClick={() => void run(true)}>Insert node</button>
<span>{report}</span>
<div style={{ height: 400 }}>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} layout={layout}
onInit={(api) => { instance.current = api; }}
onLayoutDone={() => {
instance.current?.fitView(40);
setReady(true);
setReport('layered');
}} />
</div>
</section>
);
}
vue<script setup lang="ts"> import { ref, shallowRef } from 'vue'; import { GrafloriaFlow, type DiagramInstance } from '@grafloria/vue'; import { nodes, edges, layout, insertNode } from './layout-demo'; const instance = shallowRef<DiagramInstance>(); const ready = ref(false); const busy = ref(false); const report = ref('Preparing layout'); function onInit(api: DiagramInstance) { instance.value = api; } function onLayoutDone() { instance.value?.renderNow(); instance.value?.fitView(40); ready.value = true; report.value = 'layered'; } async function run(add: boolean) { const api = instance.value; if (!api) return; busy.value = true; try { if (add) { const result = await insertNode(api.getEngine()); report.value = `Existing nodes moved ${result.movement.total.toFixed(1)} px`; } else { const result = await api.getEngine().layout(layout.name, layout.options); report.value = result.algorithm; } api.renderNow(); api.fitView(40); } finally { busy.value = false; } } </script> <template> <section> <button :disabled="!ready || busy" @click="run(false)">Rerun layout</button> <button :disabled="!ready || busy" @click="run(true)">Insert node</button> <span>{{ report }}</span> <div style="height:400px"> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" :layout="layout" @init="onInit" @layout-done="onLayoutDone" /> </div> </section> </template>
The JavaScript sample shows the six-node chain and its two layout buttons.
Angular renders the same chain through its canvas component.
Its Rerun layout and Insert node buttons sit above the six connected boxes and the layered readout.
Qwik renders the initial pipeline before any insertion.
React starts with the same layered arrangement.
Vue also starts with all six boxes in a single row.
The JavaScript mount function returns a cleanup function: call it when your application removes this view. Framework bindings own their canvas teardown.
Why layout does not follow every data change
Changing node data does not rerun the layout prop. That is deliberate: a drag can round-trip through your state without an automatic layout undoing the user's placement. Change the layout request to select another algorithm; to rerun the same request, await instance.getEngine().layout(layout.name, layout.options). The canvas repaints the changed positions. In Angular, applyLayout() without an argument reruns the bound request and resolves with its result; it returns undefined when there is no engine or request.
Preserve the mental map
Start with layered when you intend to use incremental layout. layoutIncremental() defaults to that engine because it honors anchors during coordinate assignment. An initial layout from a different engine can require a substantial rearrangement on the first incremental pass.
The result's movement measures pre-existing nodes, excluding ids in changed. Use total, average, max, and withinBudget to judge disruption in your own graph. A budget is measured and reported; do not treat withinBudget as a guarantee that the engine refuses an over-budget result. The returned tween is a plan for a host-driven animation, not an animation that runs automatically.
4. Compose architecture zones
Use architecture when the drawing is a composition of regions rather than a graph ranking. Declare zone membership through GroupSpec: children contains node ids, and direction: 'LR' lays the zone's boxes in a row. Without explicit bounds, a zone fits its children. Relations such as sourceHandle: 'top' can place a connected region above another, and a node's near relation places a note beside its subject.
This complete JavaScript sample draws a user outside a services zone, with Auth and Billing inside it. It uses the same shipped composition that the framework layout="architecture" prop selects.
tsimport { render } from '@grafloria/element';
import type { NodeSpec, EdgeSpec, GroupSpec } from '@grafloria/renderer';
export function mountArchitecture(container: HTMLElement): () => void {
container.style.height = '400px';
const nodes: NodeSpec[] = [
{ id: 'user', label: 'User' },
{ id: 'auth', label: 'Auth' },
{ id: 'billing', label: 'Billing' },
];
const edges: EdgeSpec[] = [
{ source: 'user', target: 'auth' },
{ source: 'auth', target: 'billing' },
];
const groups: GroupSpec[] = [
{ id: 'services', label: 'SERVICES', children: ['auth', 'billing'], direction: 'LR' },
];
const instance = render({ nodes, edges, groups, layout: 'architecture' }, container);
instance.fitView(40);
return () => instance.dispose();
}
const container = document.createElement('section');
document.body.appendChild(container);
mountArchitecture(container);
For React, Vue, or Qwik, pass these typed arrays as defaultNodes, defaultEdges, and defaultGroups, and select layout="architecture" on the flow component. For Angular, zones belong to the canvas's active engine: await addGroup({ name: 'SERVICES' }), then await addToGroup(group.id, nodeId) for each member before calling applyLayout('architecture'). These are memberships, not decorative rectangles; see Group and nest nodes.
Options that matter
Pass common graph-layout options in the request's options object or as the second argument to layout(). UnifiedLayoutOptions normalizes adapter vocabulary so you use direction, not adapter-specific direction keys.
| Option | Type | Default | What it does |
|---|---|---|---|
direction | 'LR' | 'RL' | 'TB' | 'BT' | Algorithm-dependent | Sets the primary flow direction. |
nodeSpacing | number | Algorithm-dependent | Sets the gap between nodes in a rank or row. |
rankSpacing | number | Algorithm-dependent | Sets the gap between ranks or layers. |
seed | number | 0x5eed | Makes randomized layouts reproducible. |
nested | boolean | Enabled when groups exist | Arranges grouped content recursively; architecture composes its own containers. |
removeOverlaps | boolean | true | Separates boxes left overlapping by an algorithm. |
columns | number | ceil(sqrt(n)) | Sets the number of columns for grid. |
For incremental passes, use IncrementalOptions, not the separate adapter-level incremental options interface.
| Option | Type | Default | What it does |
|---|---|---|---|
changed | string[] | [] | Identifies newly added or edited nodes. |
strategy | 'region' | 'pin-existing' | 'minimal-shift' | 'region' | Allows neighborhood movement, anchors all unchanged nodes, or allows free movement with realignment. |
radius | number | 1 | Expands the changed region by graph hops. |
budget | { maxPerNode?: number; averagePerNode?: number } | No budget limits | Sets thresholds for the returned movement report. |
Live demos and related guides
- Auto layout: switch among algorithms on one graph. Source.
- Layout portfolio: compare tree, radial, circular, grid, and force arrangements.
- Dynamic layouting: compare incremental movement with a full relayout.
- Architecture layout: compare architecture composition with layered ranking on the same text.
- Theme a canvas covers canvas sizing and presentation.
- Import diagram text and files covers architecture composition from Mermaid.
- Extend layout execution covers customizing how layouts run.
Was this page helpful?