Graph Builder

A click-to-evaluate computation graph. The expression y = (a × b) + c is rendered as a bottom-to-top DAG: three leaf nodes hold the operands, two operation nodes hold ?, and the output y waits at the top. Tapping an operation node flashes its fill, reveals the computed value, activates the outgoing edge, and animates a value pulse along the edge to the next consumer. The learner discovers topological ordering directly — only the node whose inputs are all ready accepts a click.

This is the canonical "what is a computation graph" primitive. The same UI drives any y = (a · b) + c walkthrough — the three operands and three labels are parameterised, so the component generalises beyond the default 3, 4, 5.

The expression y = (a × b) + c is a computation graph. Leaf values are already known: a = 3, b = 4, c = 5. The operation nodes show "?" — they haven't computed yet. Click the × node first — both its inputs are ready.

Customize
Operands
3
4
5

Installation

npx shadcn@latest add https://craftbits.dev/r/graph-builder.json

Usage

import { GraphBuilder } from "@craft-bits/viz/graph-builder";
 
<GraphBuilder />

Override the operands:

<GraphBuilder a={2} b={6} c={1} />

Relabel the nodes — handy when the same component is reused under different variable names:

<GraphBuilder
  a={7}
  b={3}
  c={4}
  aLabel="x"
  bLabel="w"
  cLabel="b"
  yLabel="z"
/>

Subscribe to evaluation events to drive a sibling transcript or progress bar:

<GraphBuilder
  onEvaluate={(stage) => {
    /* "mul-done" → "add-done" */
  }}
  onReset={() => {
    /* user reset the graph */
  }}
/>

Understanding the component

  1. Three-tier layout. Leaves are pinned to the bottom row, the two operation nodes sit on a middle band, and the output node anchors the top. Edge endpoints are computed from each pair of node centres minus the node radii, so the line never crosses into a circle.
  2. Click-to-evaluate machine. The component tracks a three-stage finite state: start → mul-done → add-done. The multiply node accepts clicks only in start; the add node only in mul-done. Wrong-order clicks no-op, and the SVG role="button" / tabIndex / aria-label attributes are flipped on the same condition so assistive tech sees the same gating.
  3. Animation cadence. Evaluating an operation node fires a fixed sequence — flash fill, reveal the computed value, idle briefly, activate the outgoing edge, send a value pulse along the edge, settle a value label on the midpoint, light up the next node as ready. Every step uses motion's animate() against a single SVG element so the broader scene never re-renders.
  4. Idle breathing pulse. Whichever operation node is currently clickable shows a soft sonar pulse around its rim — a <animate> SVG element with repeatCount="indefinite". It cancels as soon as the node is evaluated and is suppressed under prefers-reduced-motion: reduce.
  5. Reduced motion. Under prefers-reduced-motion: reduce, every staged animation collapses to an instant attribute set, the value pulses jump straight to their destination at zero opacity, and the breathing pulse is suppressed entirely.
  6. Live region narration. A polite aria-live paragraph below the SVG narrates the current stage in plain prose; an assertive aria-live SR-only region announces each evaluation result the moment it commits. Sighted users see the narration tinted by the stage's accent colour.

Props

PropTypeDefaultDescription
anumber3First multiplicand.
bnumber4Second multiplicand.
cnumber5Addend.
aLabelstring"a"Label for the first leaf.
bLabelstring"b"Label for the second leaf.
cLabelstring"c"Label for the third leaf.
yLabelstring"y"Label for the output node.
transitionTransitionSPRINGS.snapOverride the per-node-evaluation transition.
popTransitionTransitionSPRINGS.bouncyOverride the value-reveal pop transition.
onEvaluate(stage) => voidFires after each operation node settles.
onReset() => voidFires when the user resets the graph.
classNamestringMerged onto the root via cn().

Accessibility

  • The SVG is role="img" with an aria-label summarising the expression, the operand values, and the current stage.
  • The clickable operation node renders as role="button" with a descriptive aria-label and tabIndex={0} — only while it is actually clickable. Out-of-turn nodes lose role, tabIndex, and aria-label so the tab order skips them.
  • Enter and Space trigger evaluation, matching the native button contract.
  • An assertive live region announces each evaluation result the moment it commits; a polite live region carries the per-stage narration so screen-reader users get the same explanation as sighted users.
  • The decorative arrows, axes, glow rings, breathing pulses, edge labels, and value pulses are aria-hidden — the same information is encoded in the SVG label and the live regions.
  • Colour is never the only signal — the stage is also encoded in the narration text, the live-region status, and the textual ? → value transition inside every node.
  • Motion respects prefers-reduced-motion: reduce — the staged animation collapses to an instant attribute set and the breathing pulses are suppressed.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/math/GraphBuilder.tsx). The source was a tightly bundled lesson component — it consumed SvgLabel from the SVG primitives and ChallengeBtn from the lesson chrome, hardcoded a = 3, b = 4, c = 5 and the labels a, b, c, y, baked the narration prose directly into the file, and depended on the per-track palette tokens. The viz extract drops the lesson chrome, parameterises the three operand values and the four labels so the same graph teaches z = w · x + b or any other (a · b) + c expression, remaps the colour palette to var(--cb-*) semantic tokens so consumer themes repaint freely, and re-keys the per-stage springs to SPRINGS.snap and SPRINGS.bouncy so all motion comes from the same place as every other craft-bits component.