Cost-Quality Frontier
A four-state price/quality scatter for a model shortlist — Pareto frontier drawn through the non-dominated models, dominated ones greyed and named their beater, and a movable iso-value line that names the best buy at the reader's own quality-per-dollar rate.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-cost-quality-frontier.jsonPrompt
Build a React + TypeScript + Tailwind "ChartCostQualityFrontier" card — a model
price/quality plane in hand-rolled SVG (no chart library), typed by zod.
Contract
- One zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready"; title: string;
metric: { label, unit?, digits? };
higherIsBetter: boolean;
costLabel?: string; priceBasis?: string;
models: { id, label, cost > 0, quality, family? }[] }
`ready` is refined to require at least one model.
- Props = z.infer of that schema plus height? (plot px, clamped 180-420),
defaultRate?, onRetry?, and the div's own props spread on the root, with
forwardRef. No hand-written parallel interface.
- `cost` is a blended price in dollars per million tokens and must be strictly
positive, because the price axis is logarithmic and a log axis has nowhere to
put a zero. `higherIsBetter` is the only place the metric's direction lives.
Behavior
- Pure model first (exported, testable, no pixels). Normalise once:
gain = (higherIsBetter ? 1 : -1) * quality, so bigger is always better and
every comparison downstream reads gain, never quality.
- Dominance, tested pairwise: A is dominated when some B is no dearer and no
worse, strictly better on at least one axis. Record *which* B — the strongest,
then the cheapest — so the table can say who to buy instead. A dozen rows
make the quadratic version cheaper to read than a sweep that has to
special-case equal prices and equal quality.
- The non-dominated set is the frontier. Its upper convex hull is the sharper
answer: only a hull vertex is ever the argmax of gain - rate * cost, so a
frontier model under the chord between two others is non-dominated yet never
the pick at any exchange rate. Say so by name, and say that a routing split
across the two chord ends beats it everywhere.
- Slopes between consecutive hull vertices are the breakpoints: the exchange
rates at which the best buy changes hands, strictly descending by
construction. Vertex i owns the band [breaks[i], breaks[i-1]).
- One control: an exchange rate — quality units gained (or removed, for a
lower-is-better metric) per extra $1 / M tokens. It is a hand-rolled
role="slider" on a logarithmic track from a third of the lowest breakpoint to
three times the highest, driven by pointer capture (never window listeners)
plus arrows / PageUp / PageDown / Home / End, with aria-valuetext naming the
current rate and the model it crowns. Breakpoints are ticked on the track;
under it, one shortcut button per hull vertex jumps the rate to the geometric
centre of that vertex's band. The live one is aria-current, not aria-pressed —
pressing it again changes nothing.
- The card opens at the geometric centre of the breakpoint range, which on a log
track is dead centre; a single breakpoint would *be* a tie, so that case opens
half an octave above it. Uncontrolled: the derived rate follows the data until
the reader moves the line, then the reader owns it — no effect, no sync.
- Four first-class branches in one bg-card panel: loading (skeleton in the ready
silhouette: two tiles, a scatter with a rising frontier, a track), empty
(outlined axes plus what will fill them), error (message plus a Try again
button only when onRetry exists), ready. A ready payload whose rows are all
unusable degrades to empty and counts them.
- No hover state and no tooltip: every number is printed — in the plot, in the
two tiles, in the caption or in the screen-reader table — so the card reads the
same on a phone as under a mouse.
Rendering & styling
- Price axis logarithmic with 1 / 2 / 5 ticks per decade, thinning to 1 / 5 and
then to bare decades as the plot narrows. Quality axis linear on the smallest
1 / 2 / 2.5 / 5 x 10^n step that fits ~6 gridlines, with decimals derived from
the observed spread (0 for a 240-point Elo range, 2 for a half-nat loss range).
- Three marks, one rule each: frontier models are filled with their family's
chart token, dominated models are hollow rings in muted-foreground, and the
current best buy wears a primary ring. Every mark is placed by the same cost
and quality that decide its ink and print in its row — nothing is positioned by
a rank.
- The frontier is a solid polyline through the non-dominated models; its segments
join measured models and claim nothing between them. The iso-value line is the
support line through the current best buy, sampled and clipped to the plot: on
a log price axis it bends, because the rate is constant and the axis is not.
Nothing sits on the good side of it, by construction.
- Labels: greedy placement per mark — beside, then above or below, then stepped
away with a leader line — rejecting any slot that leaves the plot or touches
another label or mark, in importance order (the pick, then hull vertices, then
the rest of the frontier, then the also-rans). Text over paint gets a
card-coloured halo (paintOrder="stroke", stroke="var(--card)"). Anything that
still cannot fit is counted out loud rather than dropped in silence.
- Family shares are apportioned to whole percents by largest remainder — ties to
the bigger count, then the earlier family — so three equal families read
34 / 33 / 33 instead of the 99 that per-row rounding prints.
- Only semantic tokens: bg-card, border, muted, muted-foreground, primary,
destructive, ring and var(--chart-1..5). cn() merges className. Plot width comes
from a ResizeObserver disconnected on unmount and on node change; skeletons use
animate-pulse with motion-reduce:animate-none.
- The svg is role="img" with a sentence-long aria-label; the full model table is
real DOM in an sr-only wrapper (never sr-only on the table itself), ranked by
cost-adjusted quality at the live rate.
Customization levers
- Metric direction: higherIsBetter is the whole story — Elo, accuracy and win
rate one way, loss, error rate and perplexity the other. Nothing else changes.
- Rate units: the exchange rate is quality per dollar because that is the plot's
own slope. Invert it to dollars per quality point if your readers budget that
way, and invert the breakpoints with it.
- Slider range: a third of the lowest breakpoint to three times the highest.
Widen it to expose more of the two open-ended bands, or pin it to a band your
procurement policy already fixes.
- Density: `height` for the plot; drop the ladder of shortcut buttons on narrow
cards, or drop the second tile and keep the winner alone.
- Axis: swap the log price projection for a linear one when your catalogue spans
less than a decade — the iso-value line then draws straight, and the sampling
loop can collapse to two points.
- Palette: family colours cycle five chart tokens; map a fixed token per lab
instead, keeping dominated marks on muted-foreground so grey never means a
vendor.
- Grouping: `family` colours and gets a legend share; re-point it at a weight
class, a serving region or a licence type without touching the geometry.Concepts
- Dominance before opinion — a model beaten on price and quality at once is out before any preference is expressed, so the greyed marks are the one part of the card no reader can argue with. Each of them is told which model beat it, because "dominated" without a replacement is a verdict with no next step.
- The hull is the sharper question — the frontier answers "is anything strictly better?"; the hull answers "could I ever want this?". A frontier model sitting under the chord between two others loses to a routing split across that pair at every exchange rate, which is why the card names it rather than leaving it looking like a live option.
- Exchange rate as the reader's own axis — one number, quality gained per extra dollar per million tokens, converts the whole plane into a ranking. Tilting it re-asks the question rather than filtering the data, so the card serves the reader who will pay anything for quality and the one who wants the cheapest thing that works, without shipping two charts.
- Breakpoints, not opinions — the slopes between hull vertices are exactly the rates at which the answer changes hands, so the ladder under the slider is derived from the catalogue rather than chosen. A band that spans 9.53 to 22.9 is telling you how much your own estimate can be wrong before it stops mattering.
- The support line and the number it prints — the dashed line is the iso-value line through the current pick, so nothing sits on its good side. Where it would meet a price of zero is the cost-adjusted score in the tile — one value, drawn as a ring, printed as a figure and sorted on in the table, so the mark, the label and the ranking cannot drift apart.
- Log price, bent line — frontier catalogues run two decades from a mini tier to a flagship, so the price axis has to be logarithmic or two thirds of the field piles onto the left edge. A constant exchange rate then bends across the plot: the rate is constant, the axis is not, and the card says so instead of letting the curve read as a second series.
Context Window
A four-state context-window budget bar: the assembled prompt packed part by part, the reply's reserve subtracted from the window instead of stacked on top of it, the overflow framed against the budget that actually applies, and the tokens the eviction order takes first cut out of the part that owns them.
Elo Ratings
An arena leaderboard over time — one line per model with a confidence band derived from its battle count, releases annotated with the move they produced, and a pairwise win-rate matrix that doubles as the card's control surface.