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.
Installation
npx shadcn@latest add https://craftbits.dev/r/graph-builder.jsonUsage
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
- 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.
- Click-to-evaluate machine. The component tracks a three-stage finite state:
start → mul-done → add-done. The multiply node accepts clicks only instart; the add node only inmul-done. Wrong-order clicks no-op, and the SVGrole="button"/tabIndex/aria-labelattributes are flipped on the same condition so assistive tech sees the same gating. - 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. - Idle breathing pulse. Whichever operation node is currently clickable shows a soft sonar pulse around its rim — a
<animate>SVG element withrepeatCount="indefinite". It cancels as soon as the node is evaluated and is suppressed underprefers-reduced-motion: reduce. - 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. - Live region narration. A polite
aria-liveparagraph below the SVG narrates the current stage in plain prose; an assertivearia-liveSR-only region announces each evaluation result the moment it commits. Sighted users see the narration tinted by the stage's accent colour.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
a | number | 3 | First multiplicand. |
b | number | 4 | Second multiplicand. |
c | number | 5 | Addend. |
aLabel | string | "a" | Label for the first leaf. |
bLabel | string | "b" | Label for the second leaf. |
cLabel | string | "c" | Label for the third leaf. |
yLabel | string | "y" | Label for the output node. |
transition | Transition | SPRINGS.snap | Override the per-node-evaluation transition. |
popTransition | Transition | SPRINGS.bouncy | Override the value-reveal pop transition. |
onEvaluate | (stage) => void | — | Fires after each operation node settles. |
onReset | () => void | — | Fires when the user resets the graph. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The SVG is
role="img"with anaria-labelsummarising the expression, the operand values, and the current stage. - The clickable operation node renders as
role="button"with a descriptivearia-labelandtabIndex={0}— only while it is actually clickable. Out-of-turn nodes loserole,tabIndex, andaria-labelso the tab order skips them. EnterandSpacetrigger 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 consumedSvgLabelfrom the SVG primitives andChallengeBtnfrom the lesson chrome, hardcodeda = 3, b = 4, c = 5and the labelsa, 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 teachesz = w · x + bor any other(a · b) + cexpression, remaps the colour palette tovar(--cb-*)semantic tokens so consumer themes repaint freely, and re-keys the per-stage springs toSPRINGS.snapandSPRINGS.bouncyso all motion comes from the same place as every other craft-bits component.