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
ABCDEF

Installation

npx shadcn@latest add https://craftbits.dev/r/dag-renderer.json

Usage

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

  1. Two layout modes. When every node carries explicit x and y, 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.
  2. Per-node state drives colour. Membership in activeNodes flips a node to the bright "active" treatment (accent stroke + accent-muted fill); membership in visitedNodes mutes it. Otherwise the node sits at "rest" — calm neutral palette.
  3. Edge highlights use canonical keys. highlightedEdges is a Set of edgeKey(from, to) values — alphabetically sorted, so callers don't have to think about direction.
  4. Auto-dimmed edges. When both endpoints of an edge are in visitedNodes, the edge dims automatically. Pass an explicit dimmedEdges set to override.
  5. Edge weight labels. Optional edgeWeights paints a small monospace integer at each edge midpoint, offset perpendicular to avoid the arrow line.
  6. Shared SvgDefs. Arrows are drawn from the cross-algo-viz SvgDefs block — the same arrow markers DepGraphViz and CycleRingViz use.
  7. Reduced motion. Node entries, edge draws, and stroke-colour transitions all collapse to instant under prefers-reduced-motion: reduce.

Props

PropTypeDefaultDescription
nodesreadonly LayeredDAGNode[]requiredEach node has a stable id, optional label (falls back to id), and optional x / y coordinates.
edgesreadonly LayeredDAGEdge[]requiredDirected edges as { from, to } id pairs.
activeNodesReadonlySet<string>Node ids currently being processed.
visitedNodesReadonlySet<string>Node ids that have been fully processed.
highlightedEdgesReadonlySet<string>Canonical edgeKey strings to light up.
dimmedEdgesReadonlySet<string>Override the auto-dim set.
edgeWeightsReadonlyMap<string, number>Per-edge integer labels keyed by edgeKey.
nodeRadiusnumber20Node circle radius in px.
hSpacingnumber70Horizontal spacing between auto-laid-out nodes.
vSpacingnumber60Vertical spacing between auto-laid-out levels.
ariaLabelstringderivedAccessible label for the outer SVG.
onNodeTap(id: string) => voidNode tap handler. Makes the node focusable.
onEdgeTap(edge: LayeredDAGEdge) => voidEdge tap handler. Adds an invisible 44px-deep hitbox.
classNamestringMerged onto the outer SVG element.

Accessibility

  • The outer SVG is role="img" with an aria-label summarising node and edge counts. Pass an explicit ariaLabel to tailor the message.
  • Every node renders its label as a child text element, so identity is never communicated through colour alone.
  • When onNodeTap is provided, each node becomes a role="button" focusable element that responds to Enter and Space, with an aria-label that 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 HTML GlowNode overlays driven by a hex prop and a project-specific nodeColors DFS shorthand. The library extract drops the HTML overlay layer for a single SVG render path, replaces the per-call hex with the shared --cb-accent token, generalises the DFS three-colour map into an activeNodes / visitedNodes set pair that maps cleanly onto BFS, topological sort, Dijkstra, and MST, switches edge highlighting to canonical edgeKey strings, and routes arrow geometry through the shared SvgDefs block. The component is exported as LayeredDAGRenderer to avoid colliding with the sibling Kahn-rank DAGRenderer in @craft-bits/core/dsa-viz.