@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
foreignObjectnode 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.
