Height Detective

A histogram-height detective viz. The caller passes a heights number array, a selectedIndex (the bar under inspection), a question plus options and a correctIndex. The component renders the histogram with the selected bar accented, a dashed target-height reference line when supplied, and a radiogroup of tappable options that locks on the first correct pick and surfaces optional per-option feedback.

Generic enough to cover any "is the inspected height related to some target value?" interaction — prefix-sum subarray-summing-to-k detective puzzles, monotonic-stack height comparisons, largest-rectangle leader checks, water-trapping height duels — without baking in scoring, audio, or phase progression.

Histogram with heights 0, 1, 3, 6, 4, 9. Inspecting bar 3 at height 6. Target height 1.0126345

You're at height 6 and need a gap of 5. Has the staircase been at height 1 before?

Installation

npx shadcn@latest add https://craftbits.dev/r/height-detective.json

Usage

import { HeightDetective } from "@craft-bits/core";
 
<HeightDetective
  heights={[0, 1, 3, 6, 4, 9]}
  selectedIndex={3}
  targetHeight={1}
  question="Has the staircase been at height 1 before?"
  options={["Yes", "No"]}
  correctIndex={0}
/>

Controlled — parent owns the answer:

const [answer, setAnswer] = useState<number | null>(null);
 
<HeightDetective
  heights={heights}
  selectedIndex={i}
  question={q}
  options={opts}
  correctIndex={correct}
  answer={answer}
  onAnswerChange={setAnswer}
  onCorrect={() => playSound("insight")}
/>

Per-option feedback (renders below the option row after a pick):

<HeightDetective
  heights={heights}
  selectedIndex={3}
  question="Has the staircase been at height 1 before?"
  options={["Yes", "No"]}
  correctIndex={0}
  feedback={{
    0: "Height 1 was recorded at step 1. Gap 6 - 1 = 5 means a matching subarray.",
    1: "Look again — the notebook holds {0, 1, 3}. Height 1 IS there.",
  }}
/>

Axis labels below each bar:

<HeightDetective
  heights={[0, 1, 3, 6, 4, 9]}
  selectedIndex={3}
  question={q}
  options={opts}
  correctIndex={0}
  labels={["0", "1", "2", "3", "4", "5"]}
/>

Understanding the component

  1. One selected bar. Every bar paints in a neutral chip; only the bar at selectedIndex gets the active tone.
  2. Target reference line. Pass targetHeight and a dashed horizontal line spans the chart at that y-value. Useful for current - k relations in subarray-sum patterns or leader heights in largest-rectangle duels.
  3. Question + options + correctIndex. The answer panel is a radiogroup with one option per string. Picking sets the answer; the option locks in the success tone when correct and the error tone when wrong.
  4. Controlled + uncontrolled answer. answer + onAnswerChange is the Radix controlled pattern; defaultAnswer makes the component own its own selection. onCorrect fires once when the first correct pick lands.
  5. Per-option feedback. Pass feedback keyed by option index — the string for the picked index renders below the option row.
  6. Axis fits selected + target. The y-axis scales to the maximum of every height and the target so the dashed line is always in frame.
  7. Reduced motion. Bar entry stagger, value-change transitions, and feedback fades collapse to instant under prefers-reduced-motion: reduce.

Props

PropTypeDefaultDescription
heightsnumber[]requiredBar heights.
selectedIndexnumberrequiredIndex of the bar painted in the active tone.
questionstringrequiredDetective question rendered above the answer row.
optionsstring[]requiredTappable answer options.
correctIndexnumberrequiredIndex of the correct option.
targetHeightnumberDashed reference line drawn across the chart at this y-value.
answernumber | nullControlled answer index.
defaultAnswernumber | nullnullUncontrolled initial answer index.
onAnswerChange(next: number) => voidFires on every tap.
onCorrect() => voidFires once when the first correct pick lands.
feedbackRecord<number, string>Per-option feedback.
lockOnCorrectbooleantrueLock the answer row after the first correct pick.
labelsstring[]One-line strip below each bar.
tone"default" | "accent" | "success" | "warning" | "error""accent"Highlight tone for the selected bar and target line.
barWidthnumber36Bar width in pixels.
barGapnumber6Gap between bars in pixels.
plotHeightnumber140Plot area height in pixels.
headerReactNodeContent rendered above the histogram.
footerReactNodeContent rendered below the answer row.
transitionTransitionSPRINGS.smoothOverride transitions.
classNamestringMerged onto the outer <div> via cn().

Accessibility

  • The outer <svg> is role="img" with a <title> summarising the heights, the selected bar, and the target height.
  • The answer row is role="radiogroup" with aria-labelledby pointing at the question paragraph, and each option is role="radio" with aria-checked.
  • Every option carries an explicit aria-label naming the option number and its text.
  • After a pick, a role="status" aria-live="polite" paragraph announces the feedback.
  • The component exposes data-locked and data-correct on the root so consumer apps can hook custom styles or assistive tooling.
  • Tone is never the only signal — correct / wrong picks switch a yes / no micro-label in addition to the colour.
  • Motion respects prefers-reduced-motion: reduce.
  • Every option button is at least 44px tall so the tap target satisfies WCAG 2.5.8 AAA.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/decision/HeightDetective.tsx). The source was a 1500-line five-phase lesson component bundling brute-force enumeration, prefix-sum staircase walk, hashmap notebook, magic-move code reveal, prediction gates, hint bars, audio cues, and act-by-act score rollup. The library extract keeps only the height-detective primitive — a histogram with one selected bar, an optional target-height reference line, a question with options + correctIndex, per-option feedback, controlled / uncontrolled answer — and lets the caller compose any phases, scoring, narration, or sound on top via the header / footer slots.