Scaling Law
A four-state log-log scaling plot that fits L = E + A·C^(−α) to the runs it is given — one power law per model family, exponent, irreducible loss and R² printed, with the stretch past the largest run hatched and projected.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-scaling-law.jsonPrompt
Build a React + TypeScript + Tailwind "ChartScalingLaw" research card in plain
SVG with zod. Not recharts: both axes are logarithmic, the curve on them is
fitted by the component rather than supplied, and the mark that matters most —
a hatched region past the last real run — is a statement about evidence, not a
series.
Contract
- One zod schema is the source of truth:
{ status: "loading" | "empty" | "error" | "ready"; title: string;
caption?: string; computeLabel: string; lossLabel: string;
runs: { compute: number > 0; loss: number > 0; family?: string;
params?: number; tokens?: number; label?: string }[];
extrapolateTo?: number > 0 }.
- Component props = z.infer of that schema plus height?: number,
showFamilyFilter?: boolean, onRetry?: () => void and className. No
hand-written parallel interface.
- Refinements, because these are contract errors and not render-time surprises:
a ready payload needs at least four runs (L = E + A·C^(−α) has three free
parameters, so three points interpolate it exactly), and extrapolateTo must
lie beyond the largest run — the hatched band is an extrapolation, not a
redraw of the fitted range.
- compute is total training FLOPs (6 · params · tokens, counting active
parameters for a sparse model); loss is whatever lossLabel names. params and
tokens are reported in the tooltip and the data table, never fitted: this is
loss against compute, not the two-variable L(N, D) surface.
Behavior
- Fit, do not accept a fit. For each family: at a candidate floor E the law
linearises to ln(L − E) = ln A − α·ln C, so A and α come from closed-form
least squares; only E is searched — a grid over [0, 0.98 · min(loss)) and
then golden section inside the bracketing cell. The 0.98 ceiling is real:
as E approaches the best run's loss its residual collapses and the objective
starts describing the parameterisation instead of the data. Least squares
runs in log space, which weights a 1% miss at the bottom of the ladder like a
1% miss at the top. Report R² of that log-space fit, and NaN → "—" when the
residual is constant.
- Fit each family separately and never pool them: a mixture of model shapes has
no single exponent, and the whole reason to draw several on one pair of axes
is to compare their α. A family shorter than the four-run minimum is plotted
as points with no line and says why.
- Runs with no family collect into one implicit series; when NO run declares a
family, the series is "All runs" and the filter disappears — a filter with
one option is furniture.
- Extrapolation is drawn twice over, because two different things are being
admitted: the hatch starts at the largest run ON THE CARD, while each
family's line goes dashed at ITS OWN last run. A projection marker sits at
extrapolateTo as a hollow ring — a filled dot is a run somebody paid for.
Refuse to project a fit whose exponent came out ≤ 0 and say so: loss rising
with compute is a broken ladder, not a scaling law.
- Filter: one aria-pressed toggle per family in a role="group", all pressed to
start. Hiding the last visible family is refused rather than obeyed (that
button carries aria-disabled and an sr-only hint). Everything downstream
recomputes from the visible set — the frame, the hatch's left edge, the
compute shares and every printed verdict.
- Compute shares are apportioned by largest remainder over the visible
families, so they total exactly 100 in every subset; rounding each share on
its own is how three families print 97 under one filter state and 101 under
another.
- Derived readouts, all computed and none hard-coded: percentage of reducible
loss bought per 10× of compute (1 − 10^(−α)), the compute multiplier that
halves the remaining gap to the floor (2^(1/α)), the projected loss at the
target, the spread between projections, and a caveat for any family fitted
over less than one decade of compute.
- Four first-class branches: loading (skeleton with a falling ladder over its
own floor, aria-hidden, animate-pulse + motion-reduce:animate-none), empty
(names the four-run minimum instead of implying a failed fetch, and reports
runs dropped for a non-positive compute or loss), error (role="alert" plus a
Try again button only when onRetry exists), ready.
- A ResizeObserver measures the plot box and is disconnected on unmount and on
node change; it is the only subscription in the component.
Rendering & styling
- Axes: decade gridlines on x labelled "1e22", thinned by a stride so labels
never collide; 2× and 5× minor rules only while a decade is wide enough. On y
the ticks come from a mantissa ladder — the finest ladder whose ticks all
clear a minimum pixel gap wins, because a loss axis spans a fraction of a
decade and plain decade ticks would print two labels.
- The frame opens far enough to show a fitted floor sitting just under the runs
(that asymptote is the point of the chart) but not one half a decade below,
which would squash every run into a stripe.
- Colours come only from tokens: family i takes var(--chart-{(i % 5) + 1}) for
its points, its law, its floor rule, its projection ring and its legend
swatch — one formula, five consumers — plus a dash pattern per family so the
card survives greyscale. Panel is rounded-xl border bg-card; the hatch is
fill-muted with stroke-muted-foreground lines, since a low-alpha wash alone
disappears on a near-black card.
- role="img" on the plot with an aria-label and aria-describedby; the only
controls are the family pills above it, and every number is repeated in an
sr-only table of runs (rank-sampled to a cap) with the fitted value and the
residual beside each measured loss.
- cn() merges className, numbers are tabular-nums, and any expression followed
by a unit is separated with an explicit {" "} — JSX deletes a newline next to
an expression rather than folding it to a space.
Customization levers
- Plot density: height (200–520) and the y-axis mantissa ladders; drop the
minor 2×/5× rules for a cleaner frame, or add 3× and 7× for log-paper feel.
- Which blocks to keep: the family filter (showFamilyFilter), the legend's
projection row, the floor rules, the verdict paragraph — each is independent
of the plot.
- The fit: FLOOR_STEPS / GOLDEN_STEPS trade search cost for precision; pin the
floor to a known entropy estimate by skipping the search entirely; or swap
the objective for Huber in log space, which is what the Chinchilla paper
does, when a ladder carries a diverged run.
- Minimum ladder length: CHART_SCALING_LAW_MIN_RUNS is exported — raise it to
six if your reviewers refuse fits on four points.
- The projection: extrapolateTo drives the hatch, the dashed extensions and the
rings; omit it and the card draws only the measured range. Swap the "halve
the gap" multiplier for "compute needed to reach loss X" by inverting the law
— C = ((L − E) / A)^(−1/α).
- Palette: the colour formula cycles five chart tokens; map a fixed token per
family (brand colours for shipped architectures) by replacing inkFor.Concepts
- The fit is the deliverable — the card is handed
(compute, loss)pairs and nothing else, so the exponent it prints can be checked against the runs sitting next to it. A pre-fitted line passed in as props is a claim the reader has to take on faith; here the line, the exponent and the residual column all come out of the same four lines of algebra. - Three parameters, one of them awkward —
Aandαare linear in log space once the floorEis fixed, so onlyEneeds searching. That asymmetry is why the component gridsEfirst and refines by golden section: the objective has a wall atE → min(loss)where the best run's residual collapses, and any plain descent walks straight into it. - The irreducible term is the honest half —
Eis the entropy the data has and the model cannot remove. Without it a power law promises zero loss at infinite compute, and every conversation about "just scale it" quietly assumes that. DrawingEas a rule the runs approach and never cross turns the argument into a number. - Per-family fits, never pooled — different model shapes have different exponents; averaging them produces a law describing a mixture nobody trained. Each family gets its own colour, its own dash, its own α, and the comparison the axes were drawn for.
- Extrapolation is admitted twice — the hatched band says "past anything on this card", the dashed line says "past this family's own last run". They are different admissions, they start at different places, and a chart that draws only one of them is over-claiming for the family that stopped early.
- Every subset totals 100 — compute shares are apportioned by largest remainder over the visible families, so hiding one re-apportions the rest instead of leaving 97 or 101 on screen. Rounding per slice is a defect that survives review because it only shows up in the filter states nobody screenshots.
Cricket Manhattan
A four-state cricket Manhattan chart in hand-rolled SVG — runs off every over as a skyline, wickets as drops under the axis, a cumulative worm on a second scale, two innings on one shared over axis, and a real transport that plays the match forward over by over.
Token Usage
A four-state LLM spend card: stacked bars split input, cached input, output and reasoning tokens per period, with the cache-hit share called out and a cost line derived in-component from per-million rates.