LayeredDAGRenderer
A directed-acyclic-graph renderer for algorithm narratives — Kahn topological sort, DFS three-colouring, Dijkstra-style shortest path, MST construction. Pass a flat node list with optional x / y coordinates and a directed edges array, then drive the visuals with three optional sets: activeNodes (currently processing), visitedNodes (fully processed), highlightedEdges (per-edge accent). Optional edgeWeights paint a small monospace label at each edge midpoint for weighted-graph problems.
Preview
Installation
npx shadcn@latest add https://craftbits.dev/r/dag-renderer.jsonUsage
import { LayeredDAGRenderer, edgeKey } from "@craft-bits/core";
const nodes = [
{ id: "A" },
{ id: "B" },
{ id: "C" },
{ id: "D" },
];
const edges = [
{ from: "A", to: "C" },
{ from: "B", to: "C" },
{ from: "C", to: "D" },
];
<LayeredDAGRenderer nodes={nodes} edges={edges} />;Drive node state from the outside — the renderer is purely visual:
<LayeredDAGRenderer
nodes={nodes}
edges={edges}
activeNodes={new Set(["C"])}
visitedNodes={new Set(["A", "B"])}
/>;Light specific edges with canonical keys (order-independent):
<LayeredDAGRenderer
nodes={nodes}
edges={edges}
highlightedEdges={new Set([edgeKey("A", "C"), edgeKey("C", "D")])}
/>;Paint edge weights for a Dijkstra-style problem:
const weights = new Map<string, number>([
[edgeKey("A", "C"), 4],
[edgeKey("B", "C"), 2],
[edgeKey("C", "D"), 5],
]);
<LayeredDAGRenderer nodes={nodes} edges={edges} edgeWeights={weights} />;Understanding the component
- Two layout modes. When every node carries explicit
xandy, the renderer trusts the caller's coordinates. Otherwise it falls back to a longest-path layered auto-layout: each node sits one level below its deepest dependency, with levels centred horizontally for visual balance. - Per-node state drives colour. Membership in
activeNodesflips a node to the bright "active" treatment (accent stroke + accent-muted fill); membership invisitedNodesmutes it. Otherwise the node sits at "rest" — calm neutral palette. - Edge highlights use canonical keys.
highlightedEdgesis a Set ofedgeKey(from, to)values — alphabetically sorted, so callers don't have to think about direction. - Auto-dimmed edges. When both endpoints of an edge are in
visitedNodes, the edge dims automatically. Pass an explicitdimmedEdgesset to override. - Edge weight labels. Optional
edgeWeightspaints a small monospace integer at each edge midpoint, offset perpendicular to avoid the arrow line. - Shared SvgDefs. Arrows are drawn from the cross-
algo-vizSvgDefs block — the same arrow markersDepGraphVizandCycleRingVizuse. - Reduced motion. Node entries, edge draws, and stroke-colour transitions all collapse to instant under
prefers-reduced-motion: reduce.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
nodes | readonly LayeredDAGNode[] | required | Each node has a stable id, optional label (falls back to id), and optional x / y coordinates. |
edges | readonly LayeredDAGEdge[] | required | Directed edges as { from, to } id pairs. |
activeNodes | ReadonlySet<string> | — | Node ids currently being processed. |
visitedNodes | ReadonlySet<string> | — | Node ids that have been fully processed. |
highlightedEdges | ReadonlySet<string> | — | Canonical edgeKey strings to light up. |
dimmedEdges | ReadonlySet<string> | — | Override the auto-dim set. |
edgeWeights | ReadonlyMap<string, number> | — | Per-edge integer labels keyed by edgeKey. |
nodeRadius | number | 20 | Node circle radius in px. |
hSpacing | number | 70 | Horizontal spacing between auto-laid-out nodes. |
vSpacing | number | 60 | Vertical spacing between auto-laid-out levels. |
ariaLabel | string | derived | Accessible label for the outer SVG. |
onNodeTap | (id: string) => void | — | Node tap handler. Makes the node focusable. |
onEdgeTap | (edge: LayeredDAGEdge) => void | — | Edge tap handler. Adds an invisible 44px-deep hitbox. |
className | string | — | Merged onto the outer SVG element. |
Accessibility
- The outer SVG is
role="img"with anaria-labelsummarising node and edge counts. Pass an explicitariaLabelto tailor the message. - Every node renders its label as a child text element, so identity is never communicated through colour alone.
- When
onNodeTapis provided, each node becomes arole="button"focusable element that responds toEnterandSpace, with anaria-labelthat includes the current state. - Edge taps get an invisible hitbox sized to the 44 px touch-target minimum.
- Highlighted nodes and edges carry both a colour shift and a stroke-width bump, so emphasis survives high-contrast and grayscale rendering.
- Motion respects
prefers-reduced-motion: node entries, edge draws, and stroke-colour transitions all collapse to instant.
Credits
- Extracted from:
AlgoFlashcards(src/lessons/primitives/graph/DAGRenderer.tsx). The source rendered nodes as absolutely-positioned HTMLGlowNodeoverlays driven by ahexprop and a project-specificnodeColorsDFS shorthand. The library extract drops the HTML overlay layer for a single SVG render path, replaces the per-callhexwith the shared--cb-accenttoken, generalises the DFS three-colour map into anactiveNodes/visitedNodesset pair that maps cleanly onto BFS, topological sort, Dijkstra, and MST, switches edge highlighting to canonicaledgeKeystrings, and routes arrow geometry through the shared SvgDefs block. The component is exported asLayeredDAGRendererto avoid colliding with the sibling Kahn-rankDAGRendererin@craft-bits/core/dsa-viz.