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

@zvk/graphs

v0.1.3

Published

Accessible SVG-first node and edge graph utilities, deterministic layouts, and static React renderers for ZVK applications.

Readme

@zvk/graphs

Accessible SVG-first node and edge graph primitives for ZVK applications.

@zvk/graphs is for relationship visualizations: dependency graphs, workflows, state machines, lineage views, and topology maps. It is not a charting package and should stay deterministic, SSR-safe, and zero-runtime-dependency.

Public Imports

Use the package root and documented subpaths only:

import "@zvk/graphs/styles.css";

Public subpaths are @zvk/graphs, @zvk/graphs/model, @zvk/graphs/diagnostics, @zvk/graphs/algorithms, @zvk/graphs/serialization, @zvk/graphs/layout, @zvk/graphs/svg, @zvk/graphs/styles.css, and @zvk/graphs/package.json.

Do not import from src, dist, package internals, or private relative paths. Treat packages/graphs/package.json exports as the public API contract.

Package Docs And Examples

Package-local guides live in packages/graphs/docs/README.md. They cover modeling, algorithms, diagnostics, serialization, deterministic layout, static SVG rendering, accessibility, app composition boundaries, and anti-goals. Runnable examples live in packages/graphs/examples/ and must use only public @zvk/graphs imports.

Styles

Import graph styles once from the public stylesheet:

import "@zvk/graphs/styles.css";

When an app also uses @zvk/ui, import UI styles first so graph CSS variables can inherit UI tokens:

import "@zvk/ui/styles.css";
import "@zvk/graphs/styles.css";

The graph package should own --zvk-graphs-* variables, .zvk-graphs* classes, and graph-specific data-* attributes. It may fall back to --zvk-ui-* tokens when present, but package source must not import @zvk/ui JavaScript or component internals for styling.

Graph typography uses --zvk-graphs-font-primary, --zvk-graphs-font-secondary, and --zvk-graphs-font-tertiary. These inherit --zvk-ui-font-family-primary, --zvk-ui-font-family-secondary, and --zvk-ui-font-family-tertiary when UI styles are present.

Accessibility And SSR

  • Require a graph title, node labels, and meaningful edge labels or derivable edge descriptions.
  • Render SVG as a complex image by default: stable title/description IDs, aria-labelledby/aria-describedby, and structured fallback content.
  • Do not rely on color alone for status, selection, errors, or direction.
  • Keep focus, selection, and keyboard behavior opt-in and fully documented.
  • Keep root imports and static SVG rendering SSR-safe. Do not read window, document, ResizeObserver, SVG measurements, or browser layout at module initialization.
  • Use deterministic IDs and layout output. Do not use random IDs, timestamps, or DOM measurement for default layout.

Fallback Details

GraphFigure renders structured fallback content by default. Node fallback details include documented summary fields such as summary, accessibility, ariaDescription, kind, groupId, status, presentation, structured primitive metadata, and primitive string/number/boolean data values. Edge fallback details include source and target labels, route kind, accessibility text, status, summary, structured primitive metadata, and primitive edge data. Arbitrary object data is not dumped into the DOM by default.

Use fallbackMode="tree" when a graph is a one-root tree and should expose a nested fallback. Invalid or multi-root trees degrade to the list fallback. Use fallbackMode="none" only when the host application renders equivalent graph details elsewhere.

Static Metadata And Presentation

Nodes and edges can expose app-owned details through serializable metadata and accessibility fields:

const graph = {
  title: "Deploy graph",
  description: "Release approval flow.",
  nodes: [
    {
      id: "gate",
      label: "Deploy gate",
      labelLines: ["Deploy", "gate"],
      metadata: [
        { key: "owner", label: "Owner", value: "Platform", visibility: "summary", tone: "info" },
        { key: "risk", label: "Risk", value: "Medium", visibility: "detail", tone: "warning" }
      ],
      accessibility: {
        label: "Deploy gate decision",
        description: "Decision node for deployment approval."
      },
      presentation: {
        shape: "diamond",
        density: "compact",
        tone: "warning",
        badge: "Gate",
        glyph: "?"
      },
      status: "warning",
      position: { x: 0, y: 0 }
    }
  ],
  edges: []
};

The SVG renderer only displays metadata marked visibility: "summary" so callers have to opt into compact visible details. Fallback content renders all primitive metadata values. Use renderNode for advanced custom SVG output, but prefer built-in shape, badge, glyph, labelLines, and metadata summaries for common static graph details.

Static Groups

Graphs can define static groups and attach nodes through groupId:

const graph = {
  title: "Service map",
  groups: [
    {
      id: "backend",
      label: "Backend services",
      summary: "Platform-owned services.",
      tone: "info",
      status: "active"
    }
  ],
  nodes: [
    { id: "api", label: "API", groupId: "backend", position: { x: 0, y: 0 } },
    { id: "worker", label: "Worker", groupId: "backend", position: { x: 140, y: 0 } }
  ],
  edges: []
};

Static groups render as non-interactive SVG containers beneath edges and nodes. When a group has explicit position and size, those bounds are used; otherwise deriveGraphGroupBounds derives padded bounds from positioned member nodes. Group fallback details include summaries, metadata, status, tone, and member labels.

Groups are visual and fallback organization only. They do not provide compound layout, nested group layout, port constraints, edge routing around group boundaries, collapse/expand behavior, or editor workflows.

Edge Labels And Routes

Static SVG edges include accessible titles and descriptions derived from accessibility.label, ariaLabel, label, source label, target label, status, accessibility.description, and summary where available. Visible edge labels render at deterministic label anchors and include a tokenized background rectangle for contrast. Use deriveGraphEdgeLabelAnchor when applications need the same estimated label bounds and anchor metadata before rendering. Use getGraphEdgeLabelOverlapDiagnostics on positioned graphs, or set warnEdgeLabelOverlaps on layout helpers, when applications need opt-in warnings for estimated label bounds that overlap. This diagnostic is a deterministic risk signal; it does not measure rendered text, relocate labels, or solve collisions.

The layout helpers expose deterministic straight, curved, elbow, self-loop, and parallel-offset route primitives. These are simple SVG path primitives, not full orthogonal routing, port routing, obstacle avoidance, edge bundling, or Graphviz/ELK-style label placement.

Use edgeRouteKind for a layout-wide default route kind and routeByEdgeId when a specific edge needs a different route:

const layout = layoutManualGraph(graph, {
  edgeRouteKind: "straight",
  routeByEdgeId: {
    "audit-edge": "curve",
    "fallback-edge": "elbow"
  }
});

Per-edge route hints take precedence over the layout default. Self-loop edges still render with the self-loop route kind so loops remain recognizable.

Label anchors are deterministic estimates based on the label text length and route label position. They do not use DOM measurement, getBBox, label collision solving, obstacle avoidance, or route-aware label placement.

Layout Diagnostics And Bounds

Manual, tree, and narrow DAG layouts return diagnostics rather than throwing for expected graph-shape limits. Layout options can warn about large graphs, dense graphs, missing explicit node sizes, duplicate manual positions, self-loops, and parallel edges. estimateGraphRenderCost returns deterministic counts for nodes, edges, groups, fallback rows, visible labels, summary metadata, route points, complex routes, and a rough SVG element estimate. Use threshold options such as warnFallbackRowsAt, warnVisibleLabelsAt, warnRoutePointsAt, and warnMetadataSummariesAt to surface opt-in performance warnings when a static graph should be filtered, collapsed, summarized, or delegated to a specialized graph engine.

DAG layout supports rankStrategy: "longest-path" and "source-depth" for small acyclic graphs. Apps can also provide rankByNodeId and orderByNodeId hints when a workflow or dependency view has known phases or stable author-defined ordering:

import { layoutDagGraph } from "@zvk/graphs";

const layout = layoutDagGraph(workflowGraph, {
  direction: "left-right",
  rankByNodeId: {
    change: 0,
    preflight: 1,
    review: 1,
    release: 2
  },
  orderByNodeId: {
    review: 0,
    preflight: 1
  }
});

Rank hints override computed DAG ranks for matching node IDs. Order hints sort nodes within their resolved rank; missing order hints fall back to graph input order. Unknown hint keys are ignored. These hints are deterministic layout inputs, not a crossing-minimization engine, compound layout system, or Graphviz/ELK replacement.

Use validateGraph(graph, { auditAccessibility: true }) for opt-in accessibility audit info entries about missing graph descriptions, node descriptions, and edge descriptions. These diagnostics are guidance for complex static diagrams; they do not turn ordinary validation failures into throws.

Use getGraphBounds as a pure static helper for viewBox sizing with padding and minimum dimensions. It does not own pan/zoom state, fit-to-screen behavior, viewport persistence, or responsive layout state.

Pure Graph Transforms

Use algorithm transform helpers when an application needs focused graph views without moving search, filters, routing, or detail panels into the package:

import { getGraphNeighborhood, mapGraphNodes, pickGraphSubgraph } from "@zvk/graphs";

const neighborhood = getGraphNeighborhood(graph, {
  rootId: "model",
  direction: "outgoing",
  maxDepth: 2
});

const selectedPath = pickGraphSubgraph(graph, {
  nodeIds: ["model", "diagnostics", "layout"]
});

const highlighted = mapGraphNodes(neighborhood, (node) => ({
  ...node,
  status: node.id === "model" ? "success" : node.status
}));

pickGraphSubgraph preserves node and edge input order while pruning edges to selected nodes by default. includeConnectedEdges: false returns node-only projections, and includeDanglingEdges: true keeps edges touching selected nodes for app-owned inspection flows. getGraphNeighborhood returns a depth-limited incoming, outgoing, or bidirectional projection. mapGraphNodes and mapGraphEdges preserve graph metadata while letting applications annotate nodes or edges for a derived view.

Edge And Adjacency Interop

Use edge-list and adjacency-list helpers when an app imports relationship data from a database query, config file, or debug fixture and needs a graph model without adopting a graph language parser:

import { graphFromAdjacencyList, graphFromEdgeList, graphToAdjacencyList } from "@zvk/graphs";

const importedGraph = graphFromEdgeList(
  [
    { id: "api-db", source: "api", target: "db", label: "queries" },
    { source: "api", target: "cache", label: "reads" }
  ],
  {
    title: "Service dependencies",
    getNodeLabel: (nodeId) => nodeId.toUpperCase()
  }
);

const adjacencyList = graphToAdjacencyList(importedGraph);
const debugGraph = graphFromAdjacencyList(adjacencyList, {
  title: "Debug dependency graph"
});

graphFromEdgeList preserves explicit edge fields and appends missing nodes in first-seen edge order. Missing edge IDs are generated deterministically from the source and target IDs, with numeric suffixes for repeated pairs. Provide stable edge IDs when IDs are persisted, linked, or shown to users.

graphToAdjacencyList returns ordered { source, targets } items in graph node order, including isolated nodes. This format is intentionally structural and lossy: it preserves source IDs, ordered target IDs, and duplicate targets, but not edge labels, metadata, data, kind, status, or IDs.

Stable JSON Serialization

Use serialization helpers when an app needs deterministic graph JSON for storage, debug exports, snapshots, or support bundles:

import { normalizeGraphForJson, stringifyGraphForJson } from "@zvk/graphs/serialization";

const portableGraph = normalizeGraphForJson(graph);
const stableJson = stringifyGraphForJson(graph, {
  sortItems: true,
  space: 2
});

normalizeGraphForJson removes unsupported object properties such as undefined, functions, symbols, and bigints. Object keys are sorted for stable diffs. Graph groups, nodes, and edges preserve input order by default; set sortItems: true to sort those arrays by id for persisted output.

stringifyGraphForJson serializes the normalized graph. It is not a runtime validator and does not parse or emit DOT, Mermaid, Graphviz, or layout-engine formats. Use validateGraph and validateGraphLayout for imported data that must be checked before rendering.

Package Boundaries

@zvk/graphs owns:

  • serializable graph model types;
  • graph diagnostics and validation reports;
  • pure graph algorithms such as adjacency, degree, cycle, topological, reachability, component, tree, subgraph, neighborhood, and mapping helpers;
  • deterministic manual, tree, narrow DAG, bounds, and static group-bound utilities;
  • static React/SVG graph rendering;
  • graph CSS and token inheritance contracts.

Applications own product data, filtering, search, detail panels, routing to domain entities, and any editor or viewport state. Use @zvk/charts for axes, scales, numeric series, legends, bars, lines, areas, scatterplots, sparklines, pie/donut charts, and other quantitative visualization.

App-owned detail panel composition should keep state in the application:

import { GraphFigure, layoutManualGraph } from "@zvk/graphs";
import "@zvk/graphs/styles.css";

const layout = layoutManualGraph(graph);
const selectedNode = layout.graph.nodes.find((node) => node.id === selectedNodeId);

return (
  <>
    <GraphFigure graph={layout.graph} selectedNodeIds={selectedNodeId ? [selectedNodeId] : []} />
    <aside>{selectedNode ? selectedNode.label : "No node selected"}</aside>
  </>
);

This pattern is application composition. @zvk/graphs does not own product detail fields, routes, filtering, search, keyboard selection, or client state.

Client Boundary Decision

There is no @zvk/graphs/client public subpath yet. Keep client behavior app-owned until the static core needs shared helpers that cannot be expressed with public model, layout, SVG, and CSS contracts.

A future client subpath must stay narrow: focusable nodes and edges, roving focus or tab-order helpers, controlled selection helpers, keyboard shortcuts, persistent inspector helpers, and live-region guidance. Product detail panels, filters, search, routing, editing, drag-to-connect, pan/zoom, minimaps, and viewport persistence remain application responsibilities.

CSS Contract

Graph styles use .zvk-graphs* classes, data-zvk-graphs, data-node-id, data-edge-id, data-kind, data-node-shape, data-edge-kind, data-edge-label-anchor, data-edge-label-side, data-edge-route-kind, data-density, data-tone, data-status, and data-selected hooks. Treat these as styling and diagnostics hooks, not a replacement for accessible labels or public imports.

Important variables include:

  • --zvk-graphs-canvas-bg
  • --zvk-graphs-group-bg
  • --zvk-graphs-group-border
  • --zvk-graphs-group-label-text
  • --zvk-graphs-node-bg
  • --zvk-graphs-node-border
  • --zvk-graphs-node-text
  • --zvk-graphs-node-badge-text
  • --zvk-graphs-muted-text
  • --zvk-graphs-edge-stroke
  • --zvk-graphs-edge-label-bg
  • --zvk-graphs-edge-label-border
  • --zvk-graphs-selected-bg
  • --zvk-graphs-selected-stroke
  • --zvk-graphs-success
  • --zvk-graphs-warning
  • --zvk-graphs-destructive
  • --zvk-graphs-info
  • --zvk-graphs-font-primary
  • --zvk-graphs-font-secondary
  • --zvk-graphs-font-tertiary

The default CSS adds non-color cues for several statuses with dash patterns, stroke width changes, and fallback/status text. Host applications should still verify contrast and forced-colors behavior in their own theme context.

Anti-Goals

  • No runtime dependencies.
  • No D3, Dagre, ELK, Cytoscape, React Flow, Mermaid, Graphviz, Pixi, canvas, WebGL, CSS-in-JS, Tailwind-only assumptions, or runtime class helpers.
  • No graph editor, drag editing, pan/zoom, minimap, force simulation, orthogonal routing, edge bundling, compound-node layout, or foreignObject node rendering in the core package.
  • No unlabeled graph nodes, inaccessible edge relationships, color-only states, or browser-only behavior leaking into SSR-safe imports.

Source Policy Validation

bun run --filter @zvk/graphs validate:source-policy checks the static package boundary. It fails on browser globals or forbidden graph/runtime dependency imports in production graph source, nondeterministic time/random APIs, graph CSS that escapes the token contract, and private src or dist graph imports in the README, package docs, or examples. The package preflight runs this check after export validation.

bun run --filter @zvk/graphs verify:style-contract checks the positive style surface: graph layers, public .zvk-graphs* selectors, graph token definitions, UI-token inheritance, state selectors, and forced-colors coverage.

Repo Skill

Use .codex/skills/use-zvk-graphs/SKILL.md when maintaining this package.