π Full documentation: docs here
A declarative, builder-based library for creating animated SVG network visualizations and algorithm demos.
VizCraft is designed to make creating beautiful, animated node-link diagrams and complex visualizations intuitive and powerful. Whether you are building an educational tool, explaining an algorithm, or just need a great looking graph, VizCraft provides the primitives you need.
- Fluent Builder API: Define your visualization scene using a readable, chainable API.
- Grid System: Built-in 2D grid system for easy, structured layout of nodes.
- Auto-Layout Algorithms: Built-in circular and grid layouts, custom sync/async algorithm support (e.g. ELK), and a
getNodeBoundingBoxutility for robust layout integration. - Two Animation Systems: Lightweight registry/CSS animations (e.g. edge
flow) and data-only timeline animations (AnimationSpec). - Framework Agnostic: The core logic is pure TypeScript and can be used with any framework or Vanilla JS.
- Custom Overlays: Create complex, custom UI elements that float on top of your visualization.
- Dangling Edges: Create edges with free endpoints for drag-to-connect interactions.
- Text Badges: Pin 1β2 character indicators (class kind, status, etc.) to any corner of a node.
npm install vizcraft
# or
pnpm add vizcraft
# or
yarn add vizcraftYou can use the core library directly to generate SVG content or mount to a DOM element.
import { viz } from 'vizcraft';
const builder = viz().view(800, 600);
builder
.node('a')
.at(100, 100)
.circle(15)
.label('A')
.node('b')
.at(400, 100)
.circle(15)
.label('B')
.edge('a', 'b')
.arrow();
const container = document.getElementById('viz-basic');
if (container) builder.mount(container);If you prefer to describe a scene as a plain JSON object β or you're generating scenes programmatically / from an LLM β use fromSpec. It returns an ordinary VizBuilder you can chain or mount normally:
import { fromSpec } from 'vizcraft';
const builder = fromSpec({
view: { width: 900, height: 360 },
nodes: [
{ id: 'client', label: 'Client', x: 80, y: 180, fill: '#0e7490' },
{ id: 'lb', label: 'Load Balancer', x: 420, y: 180, fill: '#7c3aed' },
{ id: 's1', label: 'Server 1', shape: 'cylinder', x: 760, y: 100 },
],
edges: [
{ from: 'client', to: 'lb' },
{ from: 'lb', to: 's1' },
],
});
builder.mount(document.getElementById('canvas')!);For more examples and best practices, see docs here.
Full documentation site: docs here
Docs topics (same as the sidebar):
Run the docs locally:
pnpm install
pnpm -C packages/docs startThe heart of VizCraft is the VizBuilder. It allows you to construct a VizScene which acts as the blueprint for your visualization.
b.view(width, height) // Set the coordinate space
.grid(cols, rows) // (Optional) Define layout grid
.node(id) // Start defining a node
.edge(from, to) // Start defining an edgeCommon lifecycle:
builder.build()creates a serializableVizScene.builder.mount(container)renders into an SVG inside your container.builder.destroy()tears down a mounted scene securely, cleaning up DOM and event listeners.builder.play()plays any compiled timeline specs.builder.patchRuntime(container)applies runtime-only updates (useful for per-frame updates without remounting).builder.svg({ includeRuntime: true })exports an SVG snapshot including the current animated pose (useful for GIF/video pipelines).
Extend the builder's functionality seamlessly using .use(). Plugins are functions that take the builder instance and optional configuration, allowing you to encapsulate reusable behaviors, export utilities, or composite nodes.
import { viz, VizPlugin } from 'vizcraft';
const watermarkPlugin: VizPlugin<{ text: string }> = (builder, opts) => {
builder.node('watermark', {
at: { x: 50, y: 20 },
rect: { w: 100, h: 20 },
label: opts?.text ?? 'Draft',
opacity: 0.5
});
};
viz()
.view(800, 600)
.node('n1', { circle: { r: 20 } })
.use(watermarkPlugin, { text: 'Confidential' })
.build();Event Hooks
Plugins (or your own code) can also tap into the builder's lifecycle using .on(). This is particularly useful for interactive plugins that need to append HTML elements (like export buttons or tooltips) after VizCraft mounts the SVG to the DOM.
const exportUiPlugin: VizPlugin = (builder) => {
// Listen for the 'mount' event to inject a button next to the SVG
builder.on('mount', ({ container }) => {
const btn = document.createElement('button');
btn.innerText = "Download PNG";
btn.onclick = () => { /* export logic */ };
// Position the button absolutely over the container
btn.style.position = 'absolute';
btn.style.top = '10px';
btn.style.right = '10px';
container.appendChild(btn);
});
};You can also configure nodes and edges in a single declarative call by passing an options object:
// Declarative β pass all options at once, returns VizBuilder
b.node('a', { at: { x: 100, y: 100 }, rect: { w: 80, h: 40 }, fill: 'steelblue', label: 'A' })
.node('b', { circle: { r: 20 }, at: { x: 300, y: 100 }, label: 'B' })
.edge('a', 'b', { arrow: true, stroke: 'red', dash: 'dashed' })
.build();Both NodeOptions and EdgeOptions types are exported for full type-safety. See the Essentials docs for the complete options reference.
Nodes are the primary entities in your graph. They can have shapes, labels, and styles.
b.node('n1')
.at(x, y) // Absolute position
// OR
.cell(col, row) // Grid position
.circle(radius) // Circle shape
.rect(w, h, [rx]) // Rectangle (optional corner radius)
.diamond(w, h) // Diamond shape
.cylinder(w, h, [arcHeight]) // Cylinder (database symbol)
.hexagon(r, [orientation]) // Hexagon ('pointy' or 'flat')
.ellipse(rx, ry) // Ellipse / oval
.arc(r, start, end, [closed]) // Arc / pie slice
.blockArrow(len, bodyW, headW, headLen, [dir]) // Block arrow
.callout(w, h, [opts]) // Speech bubble / callout
.cloud(w, h) // Cloud / thought bubble
.cross(size, [barWidth]) // Cross / plus sign
.cube(w, h, [depth]) // 3D isometric cube
.path(d, w, h) // Custom SVG path
.document(w, h, [wave]) // Document (wavy bottom)
.note(w, h, [foldSize]) // Note (folded corner)
.parallelogram(w, h, [skew]) // Parallelogram (I/O)
.star(points, outerR, [innerR]) // Star / badge
.trapezoid(topW, bottomW, h) // Trapezoid
.triangle(w, h, [direction]) // Triangle
.label('Text', { dy: 5 }) // Label with offset
.richLabel((l) => l.text('Hello ').bold('World')) // Rich / mixed-format label
.image(href, w, h, opts?) // Embed an <image> inside the node
.icon(id, opts?) // Embed an icon from the icon registry (see registerIcon)
.svgContent(svg, opts) // Embed inline SVG content inside the node
.fill('#f0f0f0') // Fill color
.stroke('#333', 2) // Stroke color and optional width
.opacity(0.8) // Opacity
.dashed() // Dashed border (8, 4)
.dotted() // Dotted border (2, 4)
.dash('12, 3, 3, 3') // Custom dash pattern
.shadow() // Drop shadow (default: dx=2 dy=2 blur=4)
.shadow({ dx: 4, dy: 4, blur: 10, color: 'rgba(0,0,0,0.35)' }) // Custom shadow
.sketch() // Sketch / hand-drawn look (SVG turbulence filter)
.sketch({ seed: 42 }) // Sketch with explicit seed for deterministic jitter
.class('css-class') // Custom CSS class
.data({ ... }) // Attach custom data
.port('out', { x: 50, y: 0 }) // Named connection port
.container(config?) // Mark as container / group node
.parent('containerId') // Make child of a container
.compartment(id, cb?) // Add a UML-style compartment section
.collapsed(state?) // Collapse to header-only (compact mode)
.collapseIndicator(opts) // Customise or hide the collapse chevron
.collapseAnchor(anchor) // 'top' | 'center' (default) | 'bottom'Divide a node into horizontal sections β ideal for UML class diagrams:
b.node('user')
.at(250, 110)
.rect(200, 0, 6)
.fill('#f5f5f5')
.stroke('#333')
.compartment('name', (c) => c.label('User').height(36))
.compartment('attrs', (c) =>
c.label('- id: number\n- name: string', { fontSize: 12, textAnchor: 'start' })
)
.compartment('methods', (c) =>
c.label('+ getName()\n+ setName()', { fontSize: 12, textAnchor: 'start' })
)
.done();The node height auto-sizes to fit all compartments. Each section is separated by a divider line in the rendered SVG. Set height: 0 on the shape and let compartments compute the total.
Use .entry(id, text, opts?) inside a compartment callback to make each line independently clickable, hoverable, and styled:
b.node('service')
.at(250, 125)
.rect(220, 0, 6)
.compartment('name', (c) => c.label('UserService').height(36))
.compartment('methods', (c) => {
c.entry('create', '+ createUser()', {
onClick: () => console.log('create clicked'),
tooltip: 'Creates a new user record',
style: { fontWeight: 'bold' },
});
c.entry('find', '+ findById(id)');
})
.done();Each entry also accepts padding (uniform number or { top, bottom }) for vertical spacing and className for custom CSS targeting.
Entries and labels are mutually exclusive within a compartment. Hovered entries receive the CSS class viz-entry-hover. hitTest() returns entryId alongside compartmentId for entry-based compartments.
Use .collapsed() to show only the first compartment (header) while keeping all data intact β useful for compact UML overviews:
b.node('cls')
.rect(160, 0, 6)
.compartment('name', (c) => c.label('MyClass').height(36))
.compartment('attrs', (c) => c.label('- field: string'))
.collapsed() // only header shown; triangle indicator rendered
.done();The node auto-sizes to the first compartment height. A collapse indicator triangle is rendered. The group receives the CSS class viz-node-collapsed.
Add .onClick(handler) on a compartment to wire up interactive collapse/expand with a toggle() helper:
b.node('cls')
.rect(160, 0, 6)
.compartment('name', (c) =>
c.label('MyClass').height(36).onClick((ctx) => ctx.toggle({ animate: 200 }))
)
.compartment('attrs', (c) => c.label('- field: string'))
.done();Customise the indicator with .collapseIndicator() β change its colour, hide it, or supply custom SVG:
.collapseIndicator({ color: 'crimson' }) // custom colour
.collapseIndicator(false) // hide entirely
.collapseIndicator({ render: (collapsed) => `<text>${collapsed ? 'βΆ' : 'βΌ'}</text>` })Control which edge stays fixed during the animation with .collapseAnchor():
.collapseAnchor('top') // top edge fixed, grows/shrinks downward
.collapseAnchor('center') // symmetric (default)
.collapseAnchor('bottom') // bottom edge fixed, grows/shrinks upwardYou can also pass anchor per-toggle: ctx.toggle({ animate: 200, anchor: 'top' }).
Group related nodes into visual containers (swimlanes, sub-processes, etc.).
b.node('lane')
.at(250, 170)
.rect(460, 300)
.label('Process Phase')
.container({ headerHeight: 36 })
b.node('step1').at(150, 220).rect(100, 50).parent('lane')
b.node('step2').at(350, 220).rect(100, 50).parent('lane')Container children are nested inside the container <g> in the SVG and follow the container when moved at runtime.
Edges connect nodes and can be styled, directed, or animated.
All edges are rendered as <path> elements supporting three routing modes.
b.edge('n1', 'n2')
.arrow() // Add an arrowhead
.straight() // (Default) Straight line
.label('Connection')
.richLabel((l) => l.text('p').sup('95').text(' = ').bold('10ms'))
.animate('flow') // Add animation
// Curved edge
b.edge('a', 'b').curved().arrow()
// Orthogonal (right-angle) edge
b.edge('a', 'c').orthogonal().arrow()
// Waypoints β intermediate points the edge passes through
b.edge('x', 'y').curved().via(150, 50).via(200, 100).arrow()
// Arbitrary edge metadata (for routing flags, categories, etc.)
b.edge('a', 'b').meta({ customRouting: true, padding: 10 })
// Override edge path computation with a resolver hook
b.setEdgePathResolver((edge, scene, defaultResolver) => {
if (edge.meta?.customRouting) {
// Return an SVG path `d` string
return `M 0 0 L 10 10`;
}
return defaultResolver(edge, scene);
});
// Per-edge styling (overrides CSS defaults)
b.edge('a', 'b').stroke('#ff0000', 3).fill('none').opacity(0.8)
// Dashed, dotted, and custom dash patterns
b.edge('a', 'b').dashed().stroke('#6c7086') // dashed line
b.edge('a', 'b').dotted() // dotted line
b.edge('a', 'b').dash('12, 3, 3, 3').stroke('#cba6f7') // custom pattern
// Sketch / hand-drawn edges
b.edge('a', 'b').sketch() // sketchy look
// Multi-position edge labels (start / mid / end)
b.edge('a', 'b')
.label('1', { position: 'start' })
.label('*', { position: 'end' })
.arrow()
// Rich text labels (mixed formatting)
b.edge('a', 'b')
.richLabel((l) => l.text('p').sup('95').text(' ').bold('12ms'))
.arrow()
// Edge markers / arrowhead types
b.edge('a', 'b').markerEnd('arrowOpen') // Open arrow (inheritance)
b.edge('a', 'b').markerStart('diamond').markerEnd('arrow') // UML composition
b.edge('a', 'b').markerStart('diamondOpen').markerEnd('arrow') // UML aggregation
b.edge('a', 'b').arrow('both') // Bidirectional arrows
b.edge('a', 'b').markerStart('circleOpen').markerEnd('arrow') // Association
// Self-loops (exits and enters the same node)
b.edge('n1', 'n1').loopSide('right').loopSize(40).arrow()
// Straight-line edges via bounding-box overlap (vertical when nodes overlap
// horizontally, horizontal when they overlap vertically)
b.edge('a', 'b').straightLine().arrow() // both ends
b.edge('a', 'b').straightLineFrom().arrow() // source end only
// Angle utility for manual perimeter anchoring
import { angleBetween } from 'vizcraft'
const angle = angleBetween(nodeA.pos, nodeB.pos)
b.edge('a', 'b').fromAngle(angle).toAngle(angle + 180)
b.edge('a', 'b').markerEnd('bar') // ER cardinality
// Connection ports β edges attach to specific points on nodes
b.node('srv').at(100, 100).rect(80, 60)
.port('out-1', { x: 40, y: -15 })
.port('out-2', { x: 40, y: 15 })
b.node('db').at(400, 100).cylinder(80, 60)
.port('in', { x: -40, y: 0 })
b.edge('srv', 'db').fromPort('out-1').toPort('in').arrow()
// Default ports (no .port() needed) β every shape has built-in ports
b.edge('a', 'b').fromPort('right').toPort('left').arrow()
// Equidistant port distribution β stable, location-based IDs
import { getEquidistantPorts, toNodePorts, findPortNearest } from 'vizcraft'
const ports = getEquidistantPorts({ kind: 'rect', w: 120, h: 60 }, 8)
// β [{ id: 'top-0', β¦ }, { id: 'top-1', β¦ }, { id: 'right-0', β¦ }, β¦]
const nodePorts = toNodePorts(ports) // β NodePort[] ready for node.ports
// Snap to nearest port (node-local coordinates)
const nearest = findPortNearest(node, clickX - node.pos.x, clickY - node.pos.y)
if (nearest) b.edge('a', 'b').toPort(nearest.id)
// Dangling edges β one or both endpoints at a free coordinate
b.danglingEdge('preview')
.from('srv')
.toAt({ x: 300, y: 200 })
.arrow()
.dashed()
// Declarative dangling edge
b.danglingEdge('e1', { from: 'srv', toAt: { x: 300, y: 200 }, arrow: true })resolveEdgeGeometry(scene, edgeId) resolves all rendered geometry for an edge in a single call β anchor points, SVG path, midpoint, label positions, waypoints, and self-loop detection:
import { resolveEdgeGeometry } from 'vizcraft';
const geo = resolveEdgeGeometry(scene, 'edge-1');
if (!geo) return; // edge not found or unresolvable
overlayPath.setAttribute('d', geo.d); // SVG path
positionToolbar(geo.mid); // midpoint
drawHandle(geo.startAnchor); // true boundary exit point
drawHandle(geo.endAnchor); // true boundary entry point
positionSourceLabel(geo.startLabel); // ~15% along path
positionTargetLabel(geo.endLabel); // ~85% along path
geo.waypoints.forEach(drawDot); // waypoints
if (geo.isSelfLoop) { /* ... */ } // self-loop flag| Method | Description |
|---|---|
.straight() |
Direct line (default). With waypoints β polyline. |
.curved() |
Smooth bezier curve. With waypoints β Catmull-Rom spline. |
.orthogonal() |
Right-angle elbows. |
.routing(mode) |
Set mode programmatically. |
.via(x, y) |
Add an intermediate waypoint (chainable). Waypoints also influence endpoint anchoring β the source boundary anchor aims toward the first waypoint and the target anchor aims toward the last, enabling clean edge bundling. |
.label(text, opts?) |
Add a text label. Chain multiple calls for multi-position labels. opts.position can be 'start', 'mid' (default), or 'end'. |
.richLabel(cb, opts?) |
Add a rich / mixed-format label (nested SVG <tspan>s). Use .newline() in the callback to control line breaks. |
.arrow([enabled]) |
Shorthand for arrow markers. true/no-arg β markerEnd arrow. 'both' β both ends. 'start'/'end' β specific end. false β none. |
.markerEnd(type) |
Set marker type at the target end (see EdgeMarkerType). |
.markerStart(type) |
Set marker type at the source end (see EdgeMarkerType). |
.fromPort(portId) |
Connect from a specific named port on the source node. |
.toPort(portId) |
Connect to a specific named port on the target node. |
.fromAngle(deg) |
Set a fixed perimeter angle (degrees, 0 = right, 90 = down) on the source node. |
.toAngle(deg) |
Set a fixed perimeter angle (degrees, 0 = right, 90 = down) on the target node. |
.from(nodeId) |
Attach the source end to an existing node (useful with danglingEdge()). |
.to(nodeId) |
Attach the target end to an existing node (useful with danglingEdge()). |
.fromAt(pos) |
Set the free-endpoint coordinate for the source end ({ x, y }). |
.toAt(pos) |
Set the free-endpoint coordinate for the target end ({ x, y }). |
.stroke(color, width?) |
Set stroke color and optional width. |
.fill(color) |
Set fill color. |
.opacity(value) |
Set opacity (0β1). |
.dashed() |
Dashed stroke (8, 4). |
.dotted() |
Dotted stroke (2, 4). |
.dash(pattern) |
Custom SVG dasharray or preset ('dashed', 'dotted', 'dash-dot', 'solid'). |
EdgeMarkerType values: 'none', 'arrow', 'arrowOpen', 'diamond', 'diamondOpen', 'circle', 'circleOpen', 'square', 'bar', 'halfArrow'.
See the full Animations guide docs here.
VizCraft supports two complementary animation approaches:
- Registry/CSS animations (simple, reusable effects)
Attach an animation by name to a node/edge. The default core registry includes:
flow(edge)
import { viz } from 'vizcraft';
const b = viz().view(520, 160);
b.node('a').at(70, 80).circle(18).label('A')
.node('b').at(450, 80).rect(70, 44, 10).label('B')
.edge('a', 'b')
.arrow()
.animate('flow', { duration: '1s' })
.done();- Data-only timeline animations (
AnimationSpec) (sequenced tweens)
- Author with
builder.animate((aBuilder) => ...). - VizCraft stores compiled specs on the scene as
scene.animationSpecs. - Play them with
builder.play().
import { viz } from 'vizcraft';
const b = viz().view(520, 240);
b.node('a').at(120, 120).circle(20).label('A')
.node('b').at(400, 120).rect(70, 44, 10).label('B')
.edge('a', 'b').arrow()
.done();
// Create + store a data-only AnimationSpec
b.animate((aBuilder) =>
aBuilder
.node('a').to({ x: 200, opacity: 0.35 }, { duration: 600 })
.node('b').to({ x: 440, y: 170 }, { duration: 700 })
.edge('a->b').to({ strokeDashoffset: -120 }, { duration: 900 })
);
const container = document.getElementById('viz-basic');
if (container) {
b.mount(container);
b.play(); // Warns + no-ops if mount wasn't called
}Edges can have any id (you can pass it as the optional third argument to builder.edge(from, to, id)):
const b = viz().view(520, 240);
b.node('a').at(120, 120).circle(20).label('A')
.node('b').at(400, 120).rect(70, 44, 10).label('B')
.edge('a', 'b', 'e1').arrow()
.done();
b.animate((aBuilder) =>
aBuilder.edge('a', 'b', 'e1').to({ strokeDashoffset: -120 }, { duration: 900 })
);When you donβt provide an explicit edge id, the default convention is "from->to".
You can animate properties that arenβt in the core set by extending the adapter for a specific spec:
b.animate((aBuilder) =>
aBuilder
.extendAdapter((adapter) => {
// adapter may support register(kind, prop, { get, set })
adapter.register?.('node', 'r', {
get: (target) => adapter.get(target, 'r'),
set: (target, v) => adapter.set(target, 'r', v),
});
})
.node('a')
.to({ r: 42 }, { duration: 500 })
);See the docs for the recommended get/set implementations for SVG attributes.
builder.play() returns a controller with pause(), play() (resume), and stop().
const controller = b.play();
controller?.pause();
controller?.play();
controller?.stop();Out of the box, timeline playback supports these numeric properties:
- Node:
x,y,opacity,scale,rotation - Edge:
opacity,strokeDashoffset
VizCraft generates standard SVG elements with predictable classes, making it easy to style with CSS.
/* Custom node style */
.viz-node-shape {
fill: #fff;
stroke: #333;
stroke-width: 2px;
}
/* Specific node class */
.my-node .viz-node-shape {
fill: #ff6b6b;
}
/* Edge styling (CSS defaults) */
.viz-edge {
stroke: #ccc;
stroke-width: 2;
}Edges can also be styled per-edge via the builder (inline SVG attributes override CSS):
b.edge('a', 'b').stroke('#e74c3c', 3).fill('none').opacity(0.8)For deeper guides and API references, see docs here.
- Interactivity: attach
onClickhandlers and.tooltip()hover info to nodes/edges. - Text badges: pin corner indicators to nodes with
.badge(text, opts?). - Overlays: add non-node/edge visuals using
.overlay(id, params, key?). - React integration: see the workspace package packages/react-vizcraft (monorepo).
rect, circle, text, and group overlays can anchor themselves to a node with nodeId. circle and text use the resolved node center directly, rect centers itself on that point, and group uses it as the group origin. offsetX / offsetY let you stack anchored overlays without maintaining a separate scene-coordinate lookup table. Omitting nodeId preserves the existing absolute x / y behavior.
builder.overlay((o) =>
o
.circle(
{
nodeId: 'producer',
offsetX: 36,
offsetY: -18,
r: 6,
fill: '#f59e0b',
stroke: '#b45309',
strokeWidth: 2,
},
{ key: 'producer-marker' }
)
.rect(
{
nodeId: 'broker',
w: 72,
h: 28,
rx: 14,
fill: '#dcfce7',
stroke: '#16a34a',
strokeWidth: 2,
},
{ key: 'broker-slot' }
)
.text(
{
nodeId: 'store',
offsetY: 40,
text: '12 persisted',
textAnchor: 'middle',
fontWeight: 700,
},
{ key: 'store-label' }
)
.group(
{ nodeId: 'broker', offsetX: -40, offsetY: -8 },
(g) => {
g.circle({
x: 0,
y: -10,
r: 4,
fill: '#93c5fd',
stroke: '#1d4ed8',
strokeWidth: 2,
});
g.circle({
x: 0,
y: 0,
r: 4,
fill: '#93c5fd',
stroke: '#1d4ed8',
strokeWidth: 2,
});
g.circle({
x: 0,
y: 10,
r: 4,
fill: '#93c5fd',
stroke: '#1d4ed8',
strokeWidth: 2,
});
},
{ key: 'broker-batch' }
)
);The built-in signal overlay can follow the actual rendered edge path instead of moving center-to-center:
const builder = viz()
.view(620, 280)
.node('broker', { at: { x: 150, y: 180 }, rect: { w: 120, h: 72, rx: 16 } })
.node('p2', { at: { x: 470, y: 80 }, rect: { w: 110, h: 60, rx: 14 } })
.edge('broker', 'p2', 'broker-p2')
.routing('curved')
.via(250, 250)
.via(380, 40)
.done()
.overlay(
'signal',
{
from: 'broker',
to: 'p2',
edgeId: 'broker-p2',
progress: 0.6,
magnitude: 0.8,
},
'sig'
);Use followEdge: true when there is only one from -> to edge. If the edge is missing or ambiguous, VizCraft falls back to the existing straight center-to-center interpolation.
To keep a signal visible after arrival without switching overlay kinds, set resting: true. Use parkAt to override the parked node and parkOffsetX / parkOffsetY to stack multiple arrived signals inside the same node:
builder.overlay(
'signal',
{
from: 'broker',
to: 'p2',
edgeId: 'broker-p2',
progress: 1,
resting: true,
parkOffsetX: 12,
parkOffsetY: -8,
},
'sig-resting'
);Use chain to move one signal overlay across multiple hops declaratively. floor(progress) selects the active hop and the fractional part drives that hop locally. Once progress >= chain.length, the signal parks at the final hop's to node automatically. Each hop can still set followEdge or edgeId.
builder.overlay(
'signal',
{
chain: [
{ from: 'producer', to: 'dispatcher', edgeId: 'producer-dispatcher' },
{ from: 'dispatcher', to: 'adapter', edgeId: 'dispatcher-adapter' },
{ from: 'adapter', to: 'broker', edgeId: 'adapter-broker' },
{ from: 'broker', to: 'p0', edgeId: 'broker-p0' },
],
progress: 2.4,
magnitude: 0.9,
},
'sig-chain'
);Set color to override the default blue fill on individual signals. An optional glowColor adds a drop-shadow halo (defaults to color).
// Green ball β majority committed
builder.overlay(
'signal',
{
from: 'primary',
to: 'reader',
edgeId: 'primary-reader',
progress: 0.6,
magnitude: 0.85,
color: '#22c55e',
},
'committed'
);
// Amber ball β stale snapshot
builder.overlay(
'signal',
{
from: 'primary',
to: 'reader',
edgeId: 'primary-reader',
progress: 0.6,
magnitude: 0.85,
color: '#f59e0b',
},
'stale'
);Contributions are welcome! This is a monorepo managed with Turbo.
- Clone the repo
- Install dependencies:
pnpm install - Run dev server:
pnpm dev
MIT License