Recovery Race
A multi-line chart that races several optimizers on a shared log-loss axis. You hand it pre-computed loss curves; it plots them as a polyline per optimizer with a synchronised vertical cursor and per-track head dots. Use it to teach optimizer robustness (Adam recovers, SGD plateaus), learning-rate sensitivity, and what a good optimizer looks like when the initialisation is poor.
Recovery race: SGD, Adam, RMSProp.Step 45 of 99. SGD: loss 0.555. Adam: loss 0.079. RMSProp: loss 0.209.
Recovery racestep 045 / 099
- SGDloss 0.555
- Adamloss 0.079
- RMSProploss 0.209
Customize
Curves
1.00
Cursor
45
Playback
Installation
npx shadcn@latest add https://craftbits.dev/r/recovery-race.jsonUsage
import { RecoveryRace } from "@craft-bits/core";
<RecoveryRace
optimizers={[
{ id: "sgd", label: "SGD", lossCurve: sgdLoss, tone: "warning" },
{ id: "adam", label: "Adam", lossCurve: adamLoss, tone: "accent" },
{ id: "rmsprop", label: "RMSProp", lossCurve: rmsLoss, tone: "success" },
]}
/>Drive playback from outside the component, e.g. synced to a scrollytelling scrubber:
const [step, setStep] = useState(0);
const [playing, setPlaying] = useState(true);
<RecoveryRace
optimizers={runs}
currentStep={step}
onCurrentStepChange={setStep}
playing={playing}
onPlayingChange={setPlaying}
playSpeed={40}
/>Anchor on a specific step to set up a teaching beat:
<RecoveryRace optimizers={runs} defaultCurrentStep={50} />Understanding the component
- Pure plotting primitive — no math inside. The component does not simulate optimizers. You hand it
lossCurve: number[]per track, and it renders. Compute curves however you like — analytic, recorded from a real training run, or synthesised for teaching. - Log y-axis with a small floor. Losses are plotted on a
log10axis so the spread between1e-4and1reads cleanly. Values below1e-6are clamped — a curve that genuinely hits zero still draws. - The longest curve sets the race length. Shorter curves stop drawing at their last point; the cursor and scrubber span the longest curve. Mix-and-match runs of different lengths without padding.
- One shared cursor. A single vertical dashed line marks
currentStep; each optimizer's head dot springs to its position at that step. PaircurrentStepwithonCurrentStepChangeto scrub from outside, or usedefaultCurrentStepfor uncontrolled anchoring. - Three tones, by intent.
accentis the protagonist (e.g. the recovering optimizer).successis a converged baseline.warningis a struggler (the curve that plateaus or overshoots). Pick tones to encode the story, not the optimizer name. - Autoplay is opt-in.
playing/defaultPlayingflips asetTimeoutloop withplaySpeedms between steps (default60). Reduced-motion users have autoplay disabled and the cursor snaps to the last step on mount.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
optimizers | readonly RecoveryRaceOptimizer[] | — | Required. Pre-computed loss curves with id, label, lossCurve, optional tone. |
currentStep | number | — | Controlled cursor step. Pair with onCurrentStepChange. |
defaultCurrentStep | number | 0 | Uncontrolled initial step. |
onCurrentStepChange | (step: number) => void | — | Fires on every tick and manual scrub. |
playing | boolean | — | Controlled play state. Pair with onPlayingChange. |
defaultPlaying | boolean | false | Uncontrolled initial play state. |
onPlayingChange | (playing: boolean) => void | — | Fires when play / pause flips. |
playSpeed | number | 60 | Milliseconds between autoplay steps. |
transition | Transition | SPRINGS.snap | Spring for the cursor and head dots. |
className | string | — | Merged onto the root <div> via cn(). |
The RecoveryRaceOptimizer shape:
| Field | Type | Description |
|---|---|---|
id | string | Unique key — React key + aria summary token. |
label | string | Legend label. |
lossCurve | readonly number[] | One loss value per step. Non-finite entries are skipped. |
tone | 'accent' | 'success' | 'warning' | Trace tone. Defaults to 'accent'. |
Accessibility
- The figure is
role="figure"with a labelled title that names the optimizers in the race. - An
aria-live="polite"summary above the chart announces the current step and per-optimizer loss whenever the step changes — so the race is readable without color. - The play / pause button uses
aria-pressed; the label flips between "Play race" and "Pause race". - The scrubber is a native
<input type="range">witharia-label="Step scrubber"— keyboard arrows nudge the cursor. - Each trace pairs a tone swatch with a textual label and a numerical loss in the legend, so color is never the only signal.
prefers-reduced-motion: reducesnaps the cursor to the last step on mount and disables autoplay; no per-step motion is played.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/nn/RecoveryRace.tsx). The source was a bespoke stage-machine for the dead-ReLU recovery story (1D neuron, ReLU vs Leaky ReLU, dramatic zero-crossing flash, hand-coded narration). Reframed here into a general multi-line loss-race primitive: stripped the stage state, theChallengeBtn/SvgLabeldependencies, the inlinemotion/reactimperative tweens, and the lesson-specific narration. The new API takes pre-computed loss curves and plots them on a shared log axis with controlled and uncontrolledcurrentStepandplaying.