Loss Curve Compare
A side-by-side chart for comparing pre-computed loss curves. Hand it N training-loss arrays and it plots them as polylines on a shared step axis with a configurable log or linear y, optional trailing-average smoothing, and an optional shared cursor. Use it to compare model A vs model B, different loss functions on the same task (MSE vs cross-entropy), or one hyperparameter swept across runs.
Loss curve comparison: Model A, Model B, Model C.Step 60 of 119. Model A: 0.104. Model B: 0.270. Model C: 0.668.
Loss curvesy log smooth 5 step 60/119
- Model A0.104
- Model B0.270
- Model C0.668
Customize
Axis
Smoothing
5
Cursor
60
Installation
npx shadcn@latest add https://craftbits.dev/r/loss-curve-compare.jsonUsage
import { LossCurveCompare } from "@craft-bits/core";
<LossCurveCompare
curves={[
{ id: "a", label: "Model A", points: lossA, tone: "accent" },
{ id: "b", label: "Model B", points: lossB, tone: "success" },
]}
/>Tame noisy minibatch losses with a trailing running average — the convergence trend reads through the wobble without distorting it:
<LossCurveCompare curves={runs} smoothingWindow={10} />Highlight a teaching beat with a shared cursor (controlled — wire scrubbing externally if you need it):
<LossCurveCompare curves={runs} currentStep={42} />Switch to a linear axis for converged-tail comparisons:
<LossCurveCompare curves={runs} yScale="linear" />Understanding the component
- Pure plotting primitive — no math beyond smoothing and the axis transform. You hand it
points: number[]per curve and it renders. Compute the curves however you like — analytic, recorded from a real training run, or synthesised for teaching. - Log y-axis by default. Losses span orders of magnitude (cross-entropy from
~1.0down to~1e-4), sologis the default — the spread reads cleanly across decades. Flip tolinearfor converged-tail comparisons where the absolute gap matters more than the order. smoothingWindowis a trailing running average.1(the default) is a no-op — the raw curve.>1averages the lastwsamples at each step. The window is causal — the smoothed value at steptonly depends on data ≤t, so the smoothed curve never "knows the future" of the training run.- The longest curve sets the x-axis. Shorter curves stop drawing at their last point; the cursor still spans the longest. Mix-and-match runs of different lengths without padding.
- Cursor is opt-in. Pass
currentStepto draw a shared vertical guide with a head dot on every curve. Omit it and the chart just shows the curves and the legend. No autoplay, no transport — wire scrubbing from outside if you need it. - Three tones, by intent.
accentis the protagonist (Model A, the new loss).successis the converged baseline.warningis the struggler (the curve that plateaus or diverges). - Two springs, two roles. The curves animate between data changes (e.g. when you bump
smoothingWindow) withSPRINGS.smooth. The cursor and head dots useSPRINGS.snap.prefers-reduced-motion: reducesnaps both instantly.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
curves | readonly LossCurve[] | — | Required. Pre-computed loss curves with id, label, points, optional tone. |
yScale | 'linear' | 'log' | 'log' | Y-axis transform. log for orders-of-magnitude spread, linear for converged tails. |
currentStep | number | — | Shared cursor step. Omit for no cursor. Out-of-range values clamp into [0, maxStep]. |
smoothingWindow | number | 1 | Trailing running-average window. 1 is a no-op. |
transition | Transition | SPRINGS.snap | Spring for the cursor and head dots. |
className | string | — | Merged onto the root <div> via cn(). |
The LossCurve shape:
| Field | Type | Description |
|---|---|---|
id | string | Unique key — React key + aria summary token. |
label | string | Legend label. |
points | readonly number[] | One loss value per step. Non-finite entries are skipped. |
tone | 'accent' | 'success' | 'warning' | Curve tone. Defaults to 'accent'. |
Accessibility
- The figure is
role="figure"with a labelled title that names every curve in the comparison. - An
aria-live="polite"summary above the chart announces the curve count, axis transform, and per-curve loss at the cursor (when one is set) — so the comparison is readable without color. - Each curve pairs a tone swatch with a textual label (and a numerical loss when the cursor is on) in the legend, so color is never the only signal.
prefers-reduced-motion: reducesnaps both the curves and the cursor with no spring.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/viz/LossCurveCompare.tsx). The source was a bespoke MSE-vs-cross-entropy lesson primitive (Explore / Predict mode strip, drag-the-pslider, six-question quiz withusePredictRounds, gradient-arrow inset, narration heuristics,SvgLabel/TogglePill/ChallengeBtnchrome). Reframed here into a general multi-line loss-curve viewer: stripped the modes, prediction quiz, narration, drag interaction, and gradient-arrow inset; replaced the hard-coded MSE / CE pair with arbitrarycurves: { id, label, points, tone? }[]; added alog/linearaxis toggle and a trailing-averagesmoothingWindow; kept the shared-cursor head-dot pattern but switched it to controlled-only (no internal drag state — pair it with your own scrubber if you need one).