Layout & Positioning
Three approaches to positioning nodes: manual coordinates, a grid system, or automatic layout algorithms.
Manual positioning
Use .at(x, y) to place nodes at exact coordinates within the viewBox:
import { viz } from 'vizcraft';
const builder = viz().view(500, 300);
builder.node('a').at(100, 150).circle(24);
builder.node('b').at(400, 150).rect(80, 40);
builder.edge('a', 'b').arrow();
builder.mount(document.getElementById('container')!);
Grid system
Define a grid with .grid(cols, rows, opts?) and position nodes with .cell(col, row):
- Preview
- Code
import { viz } from 'vizcraft';
const builder = viz().view(500, 300);
builder
.grid(5, 3, { x: 20, y: 20 }) // 5 cols, 3 rows, 20px padding
.node('n1').cell(0, 0).label('0,0').rect(60, 40)
.node('n2').cell(2, 1).label('2,1').circle(30)
.node('n3').cell(4, 2).label('4,2').diamond(60, 60)
.done();
builder.mount(document.getElementById('container'));
Grid options:
| Option | Type | Description |
|---|---|---|
x | number | Left padding |
y | number | Top padding |
Auto-layout algorithms
Instead of positioning nodes manually, let an algorithm compute coordinates for you.
Circular layout
Arrange nodes in a circle:
- Preview
- Code
import { viz, circularLayout } from 'vizcraft';
const builder = viz().view(500, 300);
builder.node('a').circle(30).fill('#89b4fa');
builder.node('b').circle(30).fill('#a6e3a1');
builder.node('c').circle(30).fill('#f9e2af');
builder.node('d').circle(30).fill('#f38ba8');
builder.edge('a', 'b');
builder.edge('b', 'c');
builder.edge('c', 'd');
builder.edge('d', 'a');
builder.layout(circularLayout, { cx: 250, cy: 150, radius: 80 });
builder.mount(document.getElementById('container'));
Grid layout
Arrange nodes in rows and columns:
- Preview
- Code
import { viz, gridLayout } from 'vizcraft';
const builder = viz().view(500, 300);
builder.node('1').rect(40, 40).fill('#cba6f7');
builder.node('2').rect(40, 40).fill('#89b4fa');
builder.node('3').rect(40, 40).fill('#a6e3a1');
builder.node('4').rect(40, 40).fill('#f9e2af');
builder.node('5').rect(40, 40).fill('#fab387');
builder.node('6').rect(40, 40).fill('#f38ba8');
builder.layout(gridLayout, { cols: 3, x: 100, y: 70, colSpacing: 150, rowSpacing: 160 });
builder.edge('1', '2').routing('orthogonal');
builder.edge('2', '3').routing('orthogonal');
builder.edge('4', '5').routing('orthogonal');
builder.edge('5', '6').routing('orthogonal');
builder.mount(document.getElementById('container'));
Custom algorithms
A layout algorithm is a function conforming to the LayoutAlgorithm<Options> signature — it receives a LayoutGraph (nodes + edges) and returns a LayoutResult mapping node IDs to { x, y }:
import type { LayoutAlgorithm } from 'vizcraft';
const myLayout: LayoutAlgorithm<{ spacing: number }> = (graph, opts) => {
const result: Record<string, { x: number; y: number }> = {};
graph.nodes.forEach((node, i) => {
result[node.id] = { x: i * (opts?.spacing ?? 100), y: 100 };
});
return result;
};
builder.layout(myLayout, { spacing: 120 });
You can also integrate third-party layout engines (like Dagre, ELK, or D3-force) by wrapping them in a LayoutAlgorithm function.
Async layout algorithms
Some layout engines (e.g. ELK via web workers) are asynchronous. VizCraft's LayoutAlgorithm type accepts both sync and Promise-returning functions. Use .layoutAsync() to apply an async algorithm:
import type { LayoutAlgorithm } from 'vizcraft';
import ELK from 'elkjs/lib/elk.bundled';
const elk = new ELK();
const elkLayout: LayoutAlgorithm = async (graph) => {
// Convert VizCraft graph → ELK graph, run layout, convert back
const elkGraph = toElkGraph(graph);
const result = await elk.layout(elkGraph);
return fromElkResult(result);
};
// Use .layoutAsync() — returns a Promise<VizBuilder>
await builder.layoutAsync(elkLayout);
builder.mount(container);
Note: The synchronous
.layout()method throws a helpful error if it receives aPromise. Always use.layoutAsync()for async engines.
getNodeBoundingBox utility
When writing custom layout algorithms you often need each node's dimensions. Use the canonical getNodeBoundingBox(shape) utility instead of duplicating shape-specific size logic:
import { getNodeBoundingBox } from 'vizcraft';
import type { LayoutAlgorithm } from 'vizcraft';
const spacedLayout: LayoutAlgorithm = (graph) => {
let x = 0;
const nodes: Record<string, { x: number; y: number }> = {};
for (const node of graph.nodes) {
const { width } = getNodeBoundingBox(node.shape);
nodes[node.id] = { x: x + width / 2, y: 100 };
x += width + 20; // 20px gap
}
return { nodes };
};
getNodeBoundingBox supports all current NodeShape variants — circles, rects, diamonds, stars, hexagons, callouts, block arrows, and others — and centralizes the bounding-box logic so your layout code doesn't need to special-case shapes. It accounts for orientation, direction, pointer height, and other shape-specific parameters.
Found a problem? Open an issue on GitHub.