HeapTree
An SVG visualisation of a binary heap (or any complete binary tree) backed by a flat array. The caller passes values in array-backing order — values[0] is the root, values[1] and values[2] are its children, and so on — plus an optional highlightedIndex that points at the currently-relevant node (the root being extracted, the parent being compared, a sift-down target, a violation site).
Pure visualisation primitive — it does not own the comparison rule, the swap pair, the sift direction, or any per-list colour palette. Generic enough to drive min-heap, max-heap, sift-down, sift-up, build-heap-from-array, and k-way-merge-with-priority-queue walkthroughs.
Installation
npx shadcn@latest add https://craftbits.dev/r/heap-tree.jsonUsage
import { HeapTree } from "@craft-bits/core";
<HeapTree values={[1, 4, 3, 8, 5, 9, 7]} highlightedIndex={0} />Driving it from a sift-down loop — pass the current index along the sift path so the matching node breathes:
const [heap, setHeap] = useState<number[]>([1, 4, 3, 8, 5, 9, 7]);
const [cursor, setCursor] = useState<number | null>(0);
<HeapTree values={heap} highlightedIndex={cursor} tone="accent" />Without a highlight — drop highlightedIndex (or pass null) for a calm steady-state render:
<HeapTree values={[2, 5, 6, 9, 7, 8]} />Compact tier — for side-by-side comparisons or tight thumbnails:
<HeapTree values={[3, 7, 5, 10, 9]} size="sm" />Warning tone — for an "about to violate the heap property" beat in a sift narration:
<HeapTree values={[10, 4, 3]} highlightedIndex={0} tone="warning" />Understanding the component
- Array-backing input.
values[0]is the root,values[2i + 1]is the left child ofi, andvalues[2i + 2]is the right child. Capacity is read fromvalues.length; pass an empty array to render the empty state. - Complete-binary-tree layout. Positions are computed from each index by deriving the level (
floor(log2(i + 1))) and the index within that level. Every level is evenly spaced across the horizontal padding band, so the tree never drifts off-canvas. highlightedIndexis in source space. It indexesvalues, not the rendered order (the two are the same — array-backing — but the contract still matters when wiring the prop from a reducer).- Breath at the highlight. When
highlightedIndexis set, the matching node's opacity loops between0.88and1on a1.2sease and a contrasting dashed ring sits behind it. Layout stays pixel-stable — no scale shift on the breathing node. - Layout-driven motion. Positional changes (new values, removed values, depth changes) animate via Framer Motion's
motion.gx/y transitions on the canonicalSPRINGS.smoothcurve. Edges fade in viapathLengthso a freshly-grown tree paints rather than pops. - Reduced motion. The breath loop, edge draw, and node entry all collapse to instant under
prefers-reduced-motion: reduce. Position updates still run, but as zero-duration snaps. - Tone palette. The five tones —
default,accent,success,warning,error— drive the node fill, stroke, and highlight ring via the--cb-*semantic tokens. - Empty state. Renders a muted
emptyplaceholder instead of an empty box.
Variants
Standard md size, default accent tone:
<HeapTree values={[1, 4, 3, 8, 5, 9, 7]} highlightedIndex={0} />Compact sm — narrower viewBox, smaller nodes:
<HeapTree values={[1, 4, 3]} size="sm" />Custom node aria-label — pair the index with a domain-specific announcement (e.g. "Source list 2, value 7"):
<HeapTree
values={[1, 4, 3, 8, 5]}
highlightedIndex={0}
nodeAriaLabel={(v, i) => `Heap slot ${i}: value ${v}, from list ${(i % 3) + 1}`}
/>Success tone — when the heap property has just been re-established:
<HeapTree values={[1, 4, 3, 8, 5, 9, 7]} highlightedIndex={3} tone="success" />Error tone — to flag a violation site mid-sift:
<HeapTree values={[10, 4, 3]} highlightedIndex={0} tone="error" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
values | readonly number[] | required | Heap values in array-backing order. values[0] is the root. Empty array renders the empty state. |
highlightedIndex | number | null | null | Index of the currently-highlighted node. Gets a contrasting ring and a breathing opacity loop. |
tone | "default" | "accent" | "success" | "warning" | "error" | "accent" | Semantic palette for the node fill, stroke, and highlight ring. |
size | "sm" | "md" | "md" | Visual size tier. sm shrinks the viewBox and node radius. |
ariaLabel | string | derived | Aria-label for the root SVG. Defaults to a generic node-count summary. |
nodeAriaLabel | (value, index) => string | derived | Per-node aria-label resolver. Defaults to a Node {index}: value {value} style string. |
transition | Transition | SPRINGS.smooth | Override the layout transition. Reduced-motion users snap regardless. |
className | string | — | Merged onto the root SVG via cn(). |
Accessibility
- The root SVG is
role="img"with anaria-labelsummarising the node count. Override viaariaLabelto tailor the announcement (e.g. include the heap type or the current operation). - Every node is a nested
role="img"with an explicitaria-labelnaming the index and the value. Override vianodeAriaLabelfor richer per-node announcements (source list, ghost state, violation site). - The highlighted node carries
aria-current="true"so assistive tech can announce focus when the highlight moves. - The component exposes
data-tone,data-size,data-heap-size, anddata-highlightedon the relevant elements so consumer apps can hook custom styles or assistive tooling. - Emphasis stacks fill, stroke width, and ring presence — emphasis survives high-contrast and grayscale rendering, not just colour.
- Motion respects
prefers-reduced-motion: reduce— the breath loop, edge draw, and node entry all collapse to instant.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/viz/HeapTree.tsx). The source was a min-heap tree viz used across the k-way-merge lesson. It bundled per-source-list colour plumbing (colors[entry.listIdx]), an entry-shape input (HeapEntrywithvalue/listIdx/elementIdx), an explicit sift-down swap pair animation (swapPair: [from, to]with both nodes animating to each other's positions), root-extraction highlight, heap-violation ring tone, ghost (lazily-deleted) node rendering with strikethrough, per-node tap targets, and a "tap here" hint arrow. The library extract drops the per-list colour plumbing, the swap-pair animation contract, the ghost render, the tap targets, and the hint arrow in favour of a flatvalues: number[]API and a singlehighlightedIndex. Layout-driven motion viamotion.gx/y transitions handles positional shifts (including the visual effect of a swap, when the caller mutates the array). Tones route through the--cb-*semantic tokens, and the breath / edge / entry transitions all useSPRINGS.smoothfrom the shared motion module.