Charts

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import type {
  ChartEvalLeaderboardBenchmark,
  ChartEvalLeaderboardData,
  ChartEvalLeaderboardModel,
} from "./chart-eval-leaderboard.contract"

export interface ChartEvalLeaderboardProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">,
    ChartEvalLeaderboardData {

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/chart-eval-leaderboard.json

Prompt

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 higherIsBetter decides 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 range means 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 yardstickpreviousRank was 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.

On This Page