Overfitting Gap Viz
A teaching visualisation for why we early-stop, regularise, and prefer simpler models. Two loss curves — training and validation — are drawn over epochs. Early on, both fall together. Past the knee, training loss keeps dropping while validation loss plateaus and rises again. The area between them is the overfitting gap, shaded in warning tint. A dashed vertical guide marks the validation-loss argmin — the epoch where early stopping would have caught the best generaliser.
Overfitting gap visualisation.Epoch 0. Train 3.68, val 3.35, gap 0.330.
Overfitting gapepoch 00/50 gap 0.330
train 3.68 · val 3.35
Customize
Schedule
50
120ms
Display
Installation
npx shadcn@latest add https://craftbits.dev/r/overfitting-gap-viz.jsonUsage
import { OverfittingGapViz } from "@craft-bits/core";
<OverfittingGapViz epochs={50} defaultPlaying />Override the curves with real training logs:
<OverfittingGapViz
epochs={trainLogs.length - 1}
trainCurve={trainLogs.map((r) => r.trainLoss)}
valCurve={trainLogs.map((r) => r.valLoss)}
bestEpoch={trainLogs.findIndex((r) => r.bestSoFar)}
/>Drive the cursor externally — pin a specific epoch with no autoplay:
<OverfittingGapViz epochs={50} currentEpoch={28} playing={false} />Anatomy
- Two curves, one chart. Training loss is drawn solid in
cb-accent; validation loss is drawn dashed incb-warning. The dashing doubles as a colour-blind-safe shape signal — validation is distinguishable from training without relying on hue. - The gap is the lesson. A
cb-warning-tinted polygon hugs the area between the two curves. When training and validation track together, the band is invisible; once they diverge, the band widens — the literal area being memorised rather than learned. - Best-val epoch as an early-stop guide. A dashed vertical line at the argmin of the validation curve (or a caller-supplied
bestEpoch) marks the point where early stopping would have captured the best generaliser. The early-stop label makes the recipe explicit. - Autoplay sweeps the cursor. When playing, an interval advances
currentEpochby 1 everyplaySpeedms and wraps at the end of the run. The cursor is a vertical dashed line plus a train-coloured dot on the training curve and a val-coloured dot on the validation curve — both spring smoothly withSPRINGS.snap. - Synthetic defaults that read like a real run. When neither
trainCurvenorvalCurveis supplied, the component generates a monotonically-decreasing training curve and a U-shaped validation curve whose knee scales withepochs— so the silhouette reads at any horizon, from 20 epochs to 500. - Controlled or uncontrolled everywhere.
currentEpochandplayingeach have controlled and uncontrolled forms (the Radix pattern). Pass real training logs throughtrainCurve/valCurveand the same component works as a scrubbable post-mortem rather than a synthetic toy. - Reduced motion.
prefers-reduced-motion: reducecollapses every spring to an instant swap and disables autoplay; manual scrubbing still works.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
epochs | number | 50 | Total epochs plotted along the x-axis. Clamped to at least 2. |
trainCurve | readonly number[] | — | Override training-loss samples. Resampled to epochs + 1 points. |
valCurve | readonly number[] | — | Override validation-loss samples. Resampled to epochs + 1 points. |
bestEpoch | number | argmin of val | Epoch highlighted by the early-stop guide. |
showGap | boolean | true | Fill the area between the curves with a cb-warning tint. |
currentEpoch | number | — | Controlled cursor epoch. Pair with onCurrentEpochChange. |
defaultCurrentEpoch | number | 0 | Uncontrolled initial cursor. |
onCurrentEpochChange | (epoch) => void | — | Fires on autoplay tick and scrub. |
playing | boolean | — | Controlled play state. Pair with onPlayingChange. |
defaultPlaying | boolean | true | Uncontrolled initial play state. |
onPlayingChange | (playing) => void | — | Fires when play / pause flips. |
playSpeed | number | 120 | Milliseconds per autoplay tick. |
transition | Transition | SPRINGS.snap | Spring for cursor dots. Curves always use SPRINGS.smooth. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The figure is
role="figure"with anaria-labeldescribing the current epoch, both losses, and the resolved best epoch. - An
aria-live="polite"summary announces the epoch and losses whenever the cursor moves. - The play / pause button uses
aria-pressed; its label flips between "Play training playback" and "Pause training playback". - The validation curve is dashed and the training curve is solid — divergence is signalled by shape and stroke style, never by colour alone.
- The scrubber is a native range input with an explicit
aria-label; arrow keys nudge the cursor by one epoch and screen readers narrate the value. prefers-reduced-motion: reducecollapses every spring to an instant swap and disables autoplay; manual scrubbing still works.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/nn/OverfittingGapViz.tsx). Replaced the lesson-specific drag-thumb slider,SvgLabelchrome, phase-machine narration, raw colour vars, and inlineanimate()loop with the standardmotion.pathplus spring transition pattern. Generalised the synthetic curves so the U-shape knee scales withepochs, addedtrainCurve/valCurveoverrides for real training logs, surfaced an argmin-of-val best-epoch guide, and shipped autoplay plus scrubber with controlled and uncontrolled state pairs (Radix pattern).