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.

1438597

Installation

npx shadcn@latest add https://craftbits.dev/r/heap-tree.json

Usage

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

  1. Array-backing input. values[0] is the root, values[2i + 1] is the left child of i, and values[2i + 2] is the right child. Capacity is read from values.length; pass an empty array to render the empty state.
  2. 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.
  3. highlightedIndex is in source space. It indexes values, not the rendered order (the two are the same — array-backing — but the contract still matters when wiring the prop from a reducer).
  4. Breath at the highlight. When highlightedIndex is set, the matching node's opacity loops between 0.88 and 1 on a 1.2s ease and a contrasting dashed ring sits behind it. Layout stays pixel-stable — no scale shift on the breathing node.
  5. Layout-driven motion. Positional changes (new values, removed values, depth changes) animate via Framer Motion's motion.g x/y transitions on the canonical SPRINGS.smooth curve. Edges fade in via pathLength so a freshly-grown tree paints rather than pops.
  6. 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.
  7. Tone palette. The five tones — default, accent, success, warning, error — drive the node fill, stroke, and highlight ring via the --cb-* semantic tokens.
  8. Empty state. Renders a muted empty placeholder 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

PropTypeDefaultDescription
valuesreadonly number[]requiredHeap values in array-backing order. values[0] is the root. Empty array renders the empty state.
highlightedIndexnumber | nullnullIndex 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.
ariaLabelstringderivedAria-label for the root SVG. Defaults to a generic node-count summary.
nodeAriaLabel(value, index) => stringderivedPer-node aria-label resolver. Defaults to a Node {index}: value {value} style string.
transitionTransitionSPRINGS.smoothOverride the layout transition. Reduced-motion users snap regardless.
classNamestringMerged onto the root SVG via cn().

Accessibility

  • The root SVG is role="img" with an aria-label summarising the node count. Override via ariaLabel to tailor the announcement (e.g. include the heap type or the current operation).
  • Every node is a nested role="img" with an explicit aria-label naming the index and the value. Override via nodeAriaLabel for 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, and data-highlighted on 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 (HeapEntry with value / 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 flat values: number[] API and a single highlightedIndex. Layout-driven motion via motion.g x/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 use SPRINGS.smooth from the shared motion module.