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

@rankonelabs/livid-svg

v0.4.0

Published

LaidOutDiagram to SVG string. Build-time, zero client JS.

Downloads

353

Readme

@rankonelabs/livid-svg

LaidOutDiagram to an SVG string, for livid.

Build-time only. No DOM, no client JS, and nothing measured — geometry arrives from @rankonelabs/livid-core already computed, so this package draws and does not lay out. That is what makes the SVG in a post and the React canvas in an app the same map rather than two drawings that drifted.

Install

npm install @rankonelabs/livid-svg @rankonelabs/livid-core

Core is a peer dependency, and the only one. Nothing is imported at runtime — this package emits zero dependencies of its own.

Use

import { layout, validateDiagram } from '@rankonelabs/livid-core'
import { renderFigure, renderSvg } from '@rankonelabs/livid-svg'

const valid = validateDiagram(registry, spec)
if (!valid.ok) return valid.error

const laid = await layout(valid.value)
if (!laid.ok) return laid.error

const svg = renderSvg(laid.value, {
  title: 'Refund approval at HITL',
  theme: { palette: { lines: { 'line-critical': '#D64500', 'line-parallel': '#1B6CA8' } } },
})

Use renderFigure instead when the page has to size a container. It returns the same markup plus the size the figure wants:

const { svg, width, height } = renderFigure(laid.value, { title: 'Refund approval at HITL' })

Those numbers cannot be derived from diagram.bounds — core does not know where labels go, so the figure is wider than the layout. An embedded diagram needs them to choose between scaling down and scrolling when it is wider than its column, and CSS cannot read an SVG attribute to decide.

Labels

Boxes carry their own text. Circles and diamonds cannot — core holds them to a fixed width because growing one distorts it past recognition, and shape carries type — so their labels sit alongside with a leader tick, which is also how a transit map names a junction. Set metrics.labelSide to right for a top-to-bottom layout.

Text is estimated rather than measured (there is no DOM at build time) and the estimate errs wide: a label with room to spare reads fine, a clipped one does not. The viewport always includes outside labels, so nothing is cut off.

Colour

LineSpec.color is a token, not a colour — core has no palette. Configure tokens through palette.lines; any token left unconfigured takes a colour from palette.ramp by line order, so an unthemed diagram still renders as a map rather than one grey tangle.

An edge takes the colour of the resolved line where it is going. Under the pipeline profile, track leaving a router onto another line is marked data-kind="branch" and drawn at branchWeight, lighter than the track it leaves. Under dependency, every edge uses lineWeight and is marked data-kind="edge"; line differences are independent relationships rather than pipeline branches.

Direction

Dependency diagrams draw arrowheads by default because their direction is part of reading the graph. Pipeline diagrams preserve the quieter transit-map default and draw none unless you ask for them:

const svg = renderSvg(laid.value, { theme: { metrics: { edgeArrowhead: 'target' } } })

Set edgeArrowhead: 'none' to suppress the dependency default. The setting is theme config rather than a structural option because every livid edge is already directed in the data — core gives an edge a source and a target — so the renderer is only choosing whether to show that direction.

One <marker> is defined per figure and shared by every edge. It fills from context-stroke, so a head is whatever colour its edge is — including when the palette is CSS custom properties (var(--accent)) that only resolve at paint time, which a per-colour definition could never anticipate. It is sized in strokeWidth units, so the same head serves a track at lineWeight and a branch at branchWeight in proportion. Tune the proportions with arrowLength and arrowWidth, both in stroke widths rather than px.

The head is drawn back from the route's last point, which core puts on the target's boundary — so it points at the node from outside rather than disappearing under it. renderFigure reserves the room it needs, so turning heads on never clips a figure sized from width and height.

Styling hooks, and where motion lives

Every node and edge carries hooks for the page's own stylesheet:

| Element | Class | Attributes | | --- | --- | --- | | node <g> | livid-node | data-type, data-node, data-line, data-state, data-tint, data-anim | | edge <polyline> | livid-edge | data-type, data-line, data-kind (track | branch for pipelines, edge for dependencies), data-edge-id for dependencies, data-state, data-tint, data-anim |

Dependency edges carry data-edge-id, so parallel relationships between the same pair of nodes remain independently selectable even when their endpoints match.

Edge labels

When core supplies a LaidOutEdge.label box, the renderer draws the edge's label centered in that box. A background rectangle keeps the label readable over routes; configure it with palette.edgeLabelBackground. If theme typography needs more room than core reserved, the renderer expands the box around its centre. The rendered box is part of the SVG viewport, and core's label geometry is part of the figure fingerprint, so an edge label is neither clipped nor invisible to consumers that key figures by identity.

Motion is not a renderer option, and that is on purpose. An inline SVG is stylable by the document around it, so hover states, transitions, and flow animation are already the consumer's to write — a renderer shipping animation config would be deciding something the page is better placed to decide. What the renderer owes is identity: which line, which type, track or branch. That is the one thing CSS cannot recover from geometry.

Pass a validated frame as the second argument to render a state snapshot:

renderSvg(laid.value, frame, { levels: 'root' })

The renderer resolves the declared tint through theme.palette.states and emits the closed animation token as data-anim; the surrounding stylesheet can then implement transitions while respecting prefers-reduced-motion.

/* Artifact flowing along the critical path, in the consumer's stylesheet. */
@media (prefers-reduced-motion: no-preference) {
  .livid-edge[data-kind='track'] {
    stroke-dasharray: 1 14;
    stroke-linecap: round;
    animation: livid-flow 1.4s linear infinite;
  }
}
@keyframes livid-flow { to { stroke-dashoffset: -15; } }

Drill-down

Levels are stacked, not made interactive: a static file has to contain every level it can reveal, and stacking reveals them without script. Pass levels: 'root' to draw only the top. Descending on demand belongs to the React renderer.

Feed it layoutDeep() output if you want nested levels to have geometry. In the dependency presentation, nodes with embedded or deferred detail carry a small, non-interactive corner indicator. Embedded detail uses a chevron and deferred detail a ring; leaf nodes have no indicator. Embedded levels are still stacked from the node's resolved children, while deferred nodes only advertise detail that can be loaded by an interactive consumer. Pipeline output keeps its existing visual treatment.

Configuration

Everything visual is config — palette, typography, metrics — and nothing is component injection. Both renderers are closed, which is what guarantees they agree.

MIT.