npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vizcraft

v1.13.0

Published

A fluent, type-safe SVG scene builder for composing nodes, edges, animations, and overlays with incremental DOM updates and no framework dependency.

Readme

VizCraft

npm version npm downloads CI Release Snapshot license

📖 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.

✨ Features

  • 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 getNodeBoundingBox utility 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.

📦 Installation

npm install vizcraft
# or
pnpm add vizcraft
# or
yarn add vizcraft

🚀 Getting Started

You 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);

More walkthroughs and examples: docs here.

📚 Documentation (Topics)

Full documentation site: docs here

Docs topics (same as the sidebar):

Run the docs locally (monorepo):

pnpm install
pnpm -C packages/docs start

📖 Core Concepts

The Builder (VizBuilder)

The heart of VizCraft is the VizBuilder. It allows you to construct a VizScene which acts as the blueprint for your visualization.

For exporting frame snapshots during data-only playback, you can export an SVG that includes runtime overrides:

const svg = builder.svg({ includeRuntime: true });
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 edge

Plugins

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);
  });
};

Declarative Options Overloads

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

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'

Compartmented Nodes

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.

Per-entry interactivity

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.

Collapsed / compact mode

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 upward

You can also pass anchor per-toggle: ctx.toggle({ animate: 200, anchor: 'top' }).

Container / Group Nodes

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

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 });

Resolving edge geometry

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'.

Animations

See the full Animations guide docs here.

VizCraft supports two complementary animation approaches:

  1. 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();
  1. 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();

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();
}

Animating edges with custom ids

If you create an edge with a custom id (third arg), target it explicitly in animations:

const b = viz().view(520, 240);
b.node('a')
  .at(120, 120)
  .circle(20)
  .node('b')
  .at(400, 120)
  .rect(70, 44, 10)
  .edge('a', 'b', 'e1')
  .done();

b.animate((aBuilder) =>
  aBuilder
    .edge('a', 'b', 'e1')
    .to({ strokeDashoffset: -120 }, { duration: 900 })
);

Custom animatable properties (advanced)

Specs can carry adapter extensions so you can animate your own numeric properties:

b.animate((aBuilder) =>
  aBuilder
    .extendAdapter((adapter) => {
      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 })
);

Playback controls

builder.play() returns a controller with pause(), play() (resume), and stop().

const controller = b.play();
controller?.pause();
controller?.play();
controller?.stop();

Supported properties (core adapter)

Out of the box, timeline playback supports these numeric properties:

  • Node: x, y, opacity, scale, rotation
  • Edge: opacity, strokeDashoffset

🎨 Styling

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);

🤝 Contributing

Contributions are welcome! This is a monorepo managed with Turbo.

  1. Clone the repo
  2. Install dependencies: pnpm install
  3. Run dev server: pnpm dev

📄 License

MIT License