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.

Sliding-window over "aababcb" with at most 2 distinct chars
freq map
.size = 1distinct 1 / 2
'a'
1
Frequency map: 'a' = 1. size = 1. Cap = 2.

Installation

npx shadcn@latest add https://craftbits.dev/r/freq-map-tracker.json

Usage

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

  1. Caller owns the snapshot. counts is a plain Record<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.
  2. .size tells the truth — when the map is honest. The chip displays mapSize = Object.keys(counts).length. When any row has count === 0, the chip switches to the error tone and pulses, and a "vs actual" comparison strip appears beneath the map showing size = N next to actual = trueDistinct.
  3. Optional distinct cap. Pass maxDistinct and a distinct N / K chip renders next to the size chip. The chip switches to the error tone and pulses once whenever trueDistinct > maxDistinct, so callers teaching the "shrink while over cap" loop get the violation cue for free.
  4. Floating delta annotation. Each delta retriggers a +step / -step float over the row whose key matches. The component tracks identity changes on the delta prop — pass a fresh object per step (do not reuse) so the annotation animates again on the same key. null clears it immediately.
  5. Stagger on entry. New rows enter with a small y: 8 -> 0 slide and a layout-aware reorder. The stagger between siblings is the canonical STAGGER token (40 ms) so the entry rhythm matches the rest of the library.
  6. Reduced motion. Row entries, annotations, the over-cap pulse, and the size-chip flip all collapse to instant under prefers-reduced-motion: reduce.

Props

PropTypeDefaultDescription
countsRecord<string, number>requiredFrequency snapshot. Iteration order = render order. Zero counts render as ghost rows.
delta{ key: string; step: number } | nullnullMost recent change; floats over the matching row. Use a fresh object per step to retrigger.
maxDistinctnumber | nullnullDistinct cap. Shows a distinct N / K chip and pulses when violated.
titleReactNodeContent rendered above the header.
footerReactNodeContent rendered below the map.
tone"default" | "accent" | "success" | "warning" | "error""accent"Semantic tone for the fills and the size chip.
emptyLabelstring"Empty — no keys tracked yet."Empty-state copy when counts has no entries.
sizeLabelstring".size"Prefix on the size chip. Empty string drops the chip.
annotationDurationMsnumber600Annotation float-out duration in ms.
transitionTransitionSPRINGS.smoothOverride transitions. Reduced-motion users snap regardless.
classNamestringMerged onto the outer <div> via cn().

Accessibility

  • The map body is a role="list" with an aria-labelledby pointing at the "freq map" header label, and each row is a role="listitem" with an explicit aria-label naming 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-label naming 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 ghost micro-label so the distinction is visible without colour alone.
  • The component exposes data-tone, data-ghost, and data-over-cap on 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 .size lie?" trap, a MagicMoveBlock morph 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 — the key -> count row list, the .size chip 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 the title / footer slots.