Eval Leaderboard
A model × benchmark leaderboard: every column normalised in its own direction, cells shaded with the raw score printed, per-column bests marked, rank with movement arrows against the previous snapshot, and a weighted index whose whole-percent weights always total 100.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-eval-leaderboard.jsonPrompt
Build a React + TypeScript + Tailwind "ChartEvalLeaderboard" component — a model ×
benchmark score matrix that ranks — in hand-rolled markup on a real <table>, with zod.
Contract
- A zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready"; title: string; caption?: string;
previousLabel?: string;
benchmarks: { id, label, higherIsBetter: boolean, range?: [number, number],
weight?: number, unit?: string, decimals?: number }[];
models: { id, label, scores: Record<string, number>, previousRank?: number,
note?: string }[] }.
- Component props = z.infer of the schema plus indexLabel (default "Index"),
locale (default "en-US"), onRetry?: () => void and className; forwardRef to the
root div and spread the remaining div props there. No parallel interface.
- Export the pure maths separately from the component: apportionPercent(weights,
total), normaliseScore(raw, column) and buildLeaderboardModel(models,
benchmarks, excluded) — everything derived is computed there, the renderer only
paints it.
Behavior
- Per-column normalisation, not one global scale. Each column maps its raw score
to 0–1 where 1 is always the good end: with `range` the declared bounds are used
(values outside are clamped, never extrapolated); without one the column fits to
the board's own min/max. `higherIsBetter: false` flips the result, so a latency
or price column shades, marks and scores in the direction that makes a smaller
number better. A column where every model landed on the same value returns 0.5
for all of them — flat, not "everyone is best".
- A missing key in `scores` means NOT RUN, never 0: the cell renders a hatch and
the words "not run", it stays out of the column's fitted domain and out of the
"best" comparison, and the model's index is computed over the benchmarks it did
report with the weights rescaled among them. Those rows print a "†" and are
named in a footnote with their coverage (k of n).
- Column best: the extreme of the *reported* values in the metric's own direction
(never the end of a declared range — nobody scored 100 on SWE-bench). Marked
with an inset outline plus bold, and an sr-only ", best in column"; ties all
mark, because they are ties.
- Aggregate index: weights are apportioned to WHOLE PERCENTS by largest remainder
across whichever columns are currently in the index, and those printed integers
are the weights actually used — so the stated weighting and the number can never
drift apart, and every composition totals exactly 100 (never 99 or 101). Index =
Σ(percent × normalised) ÷ Σ(percent present) × 100, printed 0–100 to one
decimal and shaded on the same ramp as the cells.
- Rank is by index and does NOT follow the sort: sorting reorders rows, the rank
column keeps saying where each model sits overall. Standard competition ranking
(1, 2, 2, 4) on the PRINTED index, so two rows showing the same number are never
ranked apart over a difference no reader can see. The movement mark is
previousRank − rank: a coloured triangle plus the magnitude, an en dash for no
change, a "new" pill when previousRank is absent.
- Composition control: a row of toggle chips, one per benchmark, showing its
percent (or "off"). Toggling re-apportions the weights, re-computes the index
and re-ranks. The last remaining chip is disabled — an empty index has no
ranking. While the composition differs from the published one the movement marks
are HIDDEN and the footer says why, because the arrow would then be mixing a
real move with a change of yardstick.
- Sorting: every header is a button. First click on a column sorts it best-first
(which is ascending on a lower-is-better column); clicking the active one flips.
Rows with no value sink to the bottom in BOTH directions. A sort key naming a
benchmark that no longer exists falls back to rank during derivation, with no
effect and no stale "sorted by" claim.
- Four first-class branches on one card: loading (a table-shaped pulse skeleton,
aria-hidden, plus an sr-only role="status"), empty (also entered when ready
arrives with no rows or no columns; the copy names what *is* declared), error
(message plus a "Try again" button only when onRetry exists), ready.
- Every sentence the card prints is derived: the leader and its margin, how many
models hold how many column bests, which columns fit to the board, the movement
tally, the weight total. None of it is written by hand.
Rendering & styling
- Semantic tokens only. Cell fill is color-mix(in oklab, var(--chart-1) N%,
var(--card)) with N ramped 10%→72% — mixing into the surface, never a low-alpha
wash, so the ramp travels away from the card in both themes and the low end does
not vanish on a near-black one; the ceiling keeps --foreground above 4.5:1.
Not-run cells get a repeating-linear-gradient hatch of --muted-foreground.
Movement uses var(--chart-2) up / var(--chart-5) down with the triangle carrying
the meaning in greyscale. Everything else is bg-card, border, bg-muted,
text-muted-foreground and ring-ring — the error branch stays neutral rather
than shouting in destructive.
- Layout: a horizontally scrollable pane (role="region", tabIndex 0, aria-label)
around a table-fixed table with a colgroup; the rank and model columns are
position: sticky with bg-card so they stay readable while the benchmark columns
scroll. border-separate + border-spacing-0, because sticky cells and
border-collapse do not get along. A gradient fade appears on the right edge only
while content is actually hidden there, measured by a ResizeObserver watching
BOTH the scroll box and the table inside it — adding or removing a benchmark
moves the table's width while the box keeps its own — plus a scroll listener,
all torn down on unmount and on node change.
- Accessibility: the card root is a role="group" named by its own heading, real
<th scope="col"> headers carrying aria-sort, <th scope="row"> for the model
name, an sr-only <caption> repeating the whole derived summary, aria-pressed on
the weight chips, focus-visible rings on every control, and tabular-nums
everywhere a number sits in a column. The rank cell carries an sr-only readout
of the move — "new entry" / "unchanged" / "up N from M" — which for a row with
no index at all says it is not ranked in this snapshot instead of inventing a
zero move out of a missing delta. Decorative glyphs are aria-hidden;
transitions carry motion-reduce:transition-none and the skeleton
motion-reduce:animate-none.
- cn() merges className; numbers are formatted through Intl.NumberFormat built
once per column from its `decimals`, with an explicit locale so SSR and the
client agree.
Customization levers
- Column widths: RANK_WIDTH / MODEL_WIDTH / SCORE_WIDTH / INDEX_WIDTH drive both
the colgroup and the sticky offsets and the table's min-width — widen
SCORE_WIDTH for long benchmark names, or drop the note line to shrink
MODEL_WIDTH.
- Ramp: MIN_MIX / MAX_MIX set how much of the card the shading eats; swap
var(--chart-1) for another chart token, or key the token off the column to give
each benchmark family its own hue.
- Index policy: replace the weighted mean with a rank-mean (average of per-column
ranks) or a z-score mean by changing buildLeaderboardModel alone — the chips,
the footer sentence and the ranking all read from it.
- Precision: round1 is the board's working precision; raise it to two decimals and
competition ties break apart accordingly.
- Sub-blocks: the composition chip row, the legend, and any of the footer
sentences can be dropped independently; nothing else reads them.
- Density: BODY_CELL padding and the text-xs / text-[10px] pair set row height —
tighten for a 30-model board, loosen for a five-model comparison.Concepts
- Per-column normalisation with direction — each benchmark is scaled inside its own column and
higherIsBetterdecides which end is good, so latency and price sit in the same grid as accuracy without anyone having to remember that smaller is better there. It is a property of the metric, not a display toggle: it flips the shading, the "best" mark and the index contribution together. - Declared range vs fitted spread — a column with a
rangemeans the same thing on every snapshot; a column without one is normalised against whoever happens to be on this board, which makes the fastest model normalise to 100 by construction however close the field is. The card names which columns are which instead of leaving the reader to guess why the tail is at zero. - Largest-remainder weights — the aggregate's weights are apportioned to whole percents and the printed integers are the weights used, so the stated weighting always totals exactly 100 and always reproduces the index. Naive per-share rounding prints 99 or 101 the moment three columns split evenly, and a leaderboard that cannot add up its own weighting has nothing left to argue with.
- Rank does not follow the sort — sorting reorders rows; rank keeps reporting position by the index. That is what lets the board answer "who wins on coding, and where do they actually sit?" in one glance, and it is why the movement arrow stays meaningful while you look at a single column.
- Movement needs a fixed yardstick —
previousRankwas published against one composition, so as soon as a benchmark is dropped from the index the arrows are hidden rather than silently mixing a real climb with a change of ruler. - Not run ≠ zero — a missing score gets a hatch, leaves the column's domain and the best comparison alone, and rescales that row's weights among the benchmarks it did report, with the row flagged partial. Coercing it to zero would hand a latency column its best possible score for never being measured.
Embedding Projection
A four-state 2-D embedding projection in plain SVG: clusters wrapped in trimmed convex-hull blobs with numbered centroid badges, a nearest-mark hover readout that prints the item's own text, a cross-tab that always adds to 100, and a caveat naming the groups whose distances mean nothing.
Training Runs
A four-state training-runs card: loss against optimiser step for several concurrent experiments, each a debiased EMA over its own faint raw trace, with divergence and restart markers, an optional learning-rate schedule on a secondary axis, and a scan that reads every run at the same step and says whether the lead survives the noise.