Skip to main content

Advanced Techniques

Power-user features for building editors, dynamic visualizations, and complex diagrams.

Self-loop edges

Edges that connect a node to itself. Control placement and size with .loopSide() and .loopSize():

Freeform perimeter anchors

Control the exact angle at which edges leave or arrive at nodes using .fromAngle(deg) and .toAngle(deg):

viz()
.node('a')
.at(100, 100)
.circle(30)
.node('b')
.at(300, 100)
.circle(30)
// Edge leaves A from bottom (90°), enters B from top (270°)
.edge('a', 'b')
.fromAngle(90)
.toAngle(270)
.arrow()
.build();

Angle convention: 0 = right, 90 = down, 180 = left, 270 = up.

When fromAngle/toAngle is set, it takes precedence over fromPort/toPort and auto boundary resolution. You can mix: .fromPort('right').toAngle(180).

computeNodeAnchorAtAngle

For advanced use (custom renderers), resolve where an angle intersects a shape's perimeter:

import { computeNodeAnchorAtAngle } from 'vizcraft';
const point = computeNodeAnchorAtAngle(node, 45); // { x, y }

angleBetween

Return the angle (VizCraft degrees) from one point to another. Useful for computing fromAngle / toAngle manually:

import { angleBetween } from 'vizcraft';

const angle = angleBetween(sourceNode.pos, targetNode.pos);
eb.fromAngle(angle).toAngle(angle + 180);

Auto straight-line edges

When two nodes are not axis-aligned, using fixed angles like fromAngle(90) / toAngle(270) forces the edge to exit straight down and enter straight up — but the resulting path is diagonal and skewed. .straightLine() finds the bounding-box overlap between the two nodes and routes the edge through it — producing a perfectly vertical edge when nodes overlap horizontally, or a perfectly horizontal edge when they overlap vertically. When there is no overlap it falls back to a center-to-center diagonal:

You can also apply it to just one end:

.edge('a', 'b').straightLineFrom().arrow()  // source end only
.edge('a', 'b').straightLineTo().arrow() // target end only

The declarative form accepts straightLine in EdgeOptions:

.edge('a', 'b', { straightLine: true, arrow: true })

If you also set an explicit fromAngle or toAngle, it takes precedence over the auto-computed value for that end.

Equidistant port distribution

getEquidistantPorts(shape, count?) computes N points equally spaced by perimeter arc length along any shape. Useful for port-based editors, circuit diagrams, and dynamic port placement:

Each EquidistantPort includes:

FieldDescription
idStable identifier (p0, p1, …)
angleAngle from center in degrees
tParametric proportion along perimeter [0, 1)
x, yOffset from node center

Use toNodePorts() to convert to standard NodePort[] for the builder.

Runtime node resizing

Resize nodes at runtime without full DOM reconciliation:

import { viz } from 'vizcraft';

const builder = viz()
.view(400, 200)
.node('dynamic', { rect: { w: 100, h: 50 }, at: { x: 200, y: 100 } })
.node('left', { circle: { r: 20 }, at: { x: 50, y: 100 } })
.edge('left', 'dynamic', { arrow: true });

builder.mount(container);

// Later: resize + patch
builder.resizeNode('dynamic', { w: 180, h: 80 });
builder.patchRuntime(container);

Incremental scene mutations

Add, update, and remove nodes/edges at runtime:

import { viz } from 'vizcraft';

const builder = viz()
.view(600, 300)
.node('root', { circle: { r: 30 }, at: { x: 100, y: 150 } });

builder.mount(container, { panZoom: true });

// Mutate imperatively
builder.addNode({
id: 'n1',
pos: { x: 250, y: 150 },
shape: { kind: 'rect', w: 80, h: 40 },
});
builder.addEdge({ id: 'e1', from: 'root', to: 'n1', arrow: 'end' });
builder.commit(container);

Z-ordering

Control overlapping node draw order with zIndex:

import { viz } from 'vizcraft';

const builder = viz()
.view(600, 300)
.node('back', {
circle: { r: 50 },
at: { x: 250, y: 150 },
zIndex: 1,
fill: 'blue',
})
.node('front', {
circle: { r: 50 },
at: { x: 300, y: 150 },
zIndex: 5,
fill: 'green',
})
.node('dynamic', {
circle: { r: 60 },
at: { x: 275, y: 100 },
zIndex: 0,
fill: 'red',
});

builder.mount(container);

// Bring to front at runtime
builder.node('dynamic', { zIndex: 10 });
builder.patchRuntime(container);

Plugins

Extend VizCraft with reusable plugins. A plugin is a function that receives a VizBuilder and can mutate the scene, register behaviors, or execute utilities:

import { viz, VizPlugin } from 'vizcraft';

const watermarkPlugin: VizPlugin<{ text: string }> = (builder, options) => {
builder.node('watermark', {
at: { x: 50, y: 50 },
rect: { w: 100, h: 30 },
label: options?.text ?? 'Watermark',
zIndex: 999,
opacity: 0.5,
});
};

const builder = viz().view(400, 200);
builder
.node('a', { circle: { r: 20 }, at: { x: 150, y: 100 } })
.use(watermarkPlugin, { text: 'Draft Version' });

Event hooks

Plugins can listen for lifecycle events with builder.on():

const exportPlugin: VizPlugin = (builder) => {
builder.on('mount', ({ container, controller }) => {
const btn = document.createElement('button');
btn.innerText = 'Download PNG';
btn.onclick = () => {
// Export builder.svg() to PNG
};
btn.style.position = 'absolute';
btn.style.top = '10px';
btn.style.right = '10px';
container.appendChild(btn);
});
};

Serialization & SVG export

Build a VizScene

const scene = builder.build();
// scene is a plain data object: { nodes: [], edges: [], viewBox, ... }

Export SVG string

const svgString = builder.svg();

// Include runtime state (animation frame, overlay positions)
const animatedSvg = builder.svg({ includeRuntime: true });

Found a problem? Open an issue on GitHub.