FreqMapTracker
A pure playback viz for a hash-map frequency tracker. The caller passes a counts snapshot — a plain Record<string, number> — plus an optional delta that names which key just changed and by how much. The component renders the map as a list of key -> count rows, surfaces a .size chip that tells the truth about distinct keys, flags zero-count "ghost" rows, and floats a +1 / -1 annotation over the row that just moved.
Pure visualisation primitive — it does not own the state machine, the deletion rule, the prediction gates, or the audio cues the source lesson layered on. Generic enough to cover any "watch the hash map mutate over a sliding window" lesson: at-most-K-distinct, longest-substring-with-K, character replacement, anagram windows.
"aababcb" with at most 2 distinct charsInstallation
npx shadcn@latest add https://craftbits.dev/r/freq-map-tracker.jsonUsage
import { FreqMapTracker } from "@craft-bits/core";
<FreqMapTracker
counts={{ a: 2, b: 1 }}
maxDistinct={2}
/>Driving it from a sliding-window reducer — pass the most recent delta so the matching row floats +1 or -1:
const [counts, setCounts] = useState<Record<string, number>>({});
const [delta, setDelta] = useState<{ key: string; step: number } | null>(null);
function expand(ch: string) {
setCounts((c) => ({ ...c, [ch]: (c[ch] ?? 0) + 1 }));
setDelta({ key: ch, step: 1 });
}
function shrink(ch: string) {
setCounts((c) => {
const next = { ...c, [ch]: (c[ch] ?? 0) - 1 };
if (next[ch] === 0) delete next[ch];
return next;
});
setDelta({ key: ch, step: -1 });
}
<FreqMapTracker counts={counts} delta={delta} maxDistinct={2} />Without a cap — drop maxDistinct to suppress the distinct chip and the over-cap pulse:
<FreqMapTracker counts={{ a: 3, b: 2, c: 1 }} />Showing a ghost row — keep a key in counts after its count hits zero and the row paints in the error tone, the size chip pulses, and a "vs actual" comparison appears beneath the map:
<FreqMapTracker
counts={{ a: 2, b: 0 }}
delta={{ key: "b", step: -1 }}
maxDistinct={2}
/>Custom size label — the chip prefix is configurable so the same viz can teach a .length / .distinctCount() / len() API without hard-coding .size:
<FreqMapTracker counts={counts} sizeLabel="len" />Understanding the component
- Caller owns the snapshot.
countsis a plainRecord<string, number>. Iteration order of the object is the render order of the rows. Zero counts are kept and rendered as "ghost" rows so the caller can choose whether to delete the key after each shrink. .sizetells the truth — when the map is honest. The chip displaysmapSize = Object.keys(counts).length. When any row hascount === 0, the chip switches to the error tone and pulses, and a "vs actual" comparison strip appears beneath the map showingsize = Nnext toactual = trueDistinct.- Optional distinct cap. Pass
maxDistinctand adistinct N / Kchip renders next to the size chip. The chip switches to the error tone and pulses once whenevertrueDistinct > maxDistinct, so callers teaching the "shrink while over cap" loop get the violation cue for free. - Floating delta annotation. Each
deltaretriggers a+step/-stepfloat over the row whosekeymatches. The component tracks identity changes on thedeltaprop — pass a fresh object per step (do not reuse) so the annotation animates again on the same key.nullclears it immediately. - Stagger on entry. New rows enter with a small
y: 8 -> 0slide and a layout-aware reorder. The stagger between siblings is the canonicalSTAGGERtoken (40 ms) so the entry rhythm matches the rest of the library. - Reduced motion. Row entries, annotations, the over-cap pulse, and the size-chip flip all collapse to instant under
prefers-reduced-motion: reduce.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
counts | Record<string, number> | required | Frequency snapshot. Iteration order = render order. Zero counts render as ghost rows. |
delta | { key: string; step: number } | null | null | Most recent change; floats over the matching row. Use a fresh object per step to retrigger. |
maxDistinct | number | null | null | Distinct cap. Shows a distinct N / K chip and pulses when violated. |
title | ReactNode | — | Content rendered above the header. |
footer | ReactNode | — | Content rendered below the map. |
tone | "default" | "accent" | "success" | "warning" | "error" | "accent" | Semantic tone for the fills and the size chip. |
emptyLabel | string | "Empty — no keys tracked yet." | Empty-state copy when counts has no entries. |
sizeLabel | string | ".size" | Prefix on the size chip. Empty string drops the chip. |
annotationDurationMs | number | 600 | Annotation float-out duration in ms. |
transition | Transition | SPRINGS.smooth | Override transitions. Reduced-motion users snap regardless. |
className | string | — | Merged onto the outer <div> via cn(). |
Accessibility
- The map body is a
role="list"with anaria-labelledbypointing at the "freq map" header label, and each row is arole="listitem"with an explicitaria-labelnaming the key, the count, and whether the row is a ghost. - The size chip uses
aria-live="polite"so screen-reader users hear every flip to and from the "size is lying" state. - The distinct-cap chip carries an
aria-labelnaming the current distinct count and the cap. - An off-screen
role="status"paragraph summarises the entire map, the size chip, and the cap on every render so consumers of the live region get the same narration sighted users see. - The "ghost" affordance is signalled by colour, opacity, AND an explicit
ghostmicro-label so the distinction is visible without colour alone. - The component exposes
data-tone,data-ghost, anddata-over-capon the root so consumer apps can hook custom styles or assistive tooling. - Motion respects
prefers-reduced-motion: reduce— row entries, the size-chip pulse, the annotation float, and the over-cap chip pulse all collapse to instant.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/observation/FreqMapTracker.tsx). The source was an 1851-line five-phase lesson component bundling a full reducer-driven state machine (expand / naive-shrink / ghost-predict / ghost-reveal / fixed-practice / code-bridge / micro-queries / done), a prediction gate for the "will.sizelie?" trap, aMagicMoveBlockmorph between the naive and fixed code, audio cues (freq_increment,freq_decrement,ghost_reveal,freq_delete), per-distractor feedback, a celebratory completion screen, and a 5-phase mood / container palette. The library extract keeps only the pure visualisation primitive — thekey -> countrow list, the.sizechip with ghost flagging, the distinct cap, and the floating delta annotation — and lets the caller compose any phases, prediction gates, scoring, narration, or sound on top via thetitle/footerslots.