+ )
+}
+
+const SCATTER_WIDTH = 640
+const SCATTER_HEIGHT = 320
+const SCATTER_PAD_LEFT = 48
+const SCATTER_PAD_BOTTOM = 32
+const SCATTER_PAD_TOP = 16
+const SCATTER_PAD_RIGHT = 16
+
+// Cost-vs-score is a scatter (an all-pairs form), which caps categorical
+// color at 3 series -- with up to 7+ models this would blow that cap, so
+// identity here comes from direct text labels + marker shape (per
+// benchmark), not from a per-model hue.
+export function CostVsScoreChart({ points }: { points: ProviderBenchmarkPoint[] }) {
+ const rows = points.filter((p) => p.score != null && p.cost_usd != null)
+ if (!rows.length) {
+ return (
+
+ No cost/score data yet.
+
+ )
+ }
+
+ const benchmarks = Array.from(new Set(rows.map((p) => p.benchmark))).sort()
+ // Cost typically spans multiple orders of magnitude (a cheap model at
+ // $0.01 next to an expensive one at $0.78), which crushes everything
+ // cheap into one unreadable cluster on a linear axis -- log scale spreads
+ // it out. Floor at a small epsilon so a $0 run still has a position.
+ const costs = rows.map((p) => Math.max(p.cost_usd ?? 0, 0.001))
+ const minLog = Math.log10(Math.min(...costs))
+ const maxLog = Math.log10(Math.max(...costs, 0.01))
+ const logSpan = Math.max(maxLog - minLog, 0.5)
+ const plotWidth = SCATTER_WIDTH - SCATTER_PAD_LEFT - SCATTER_PAD_RIGHT
+ const plotHeight = SCATTER_HEIGHT - SCATTER_PAD_TOP - SCATTER_PAD_BOTTOM
+
+ const xFor = (cost: number) => {
+ const frac = (Math.log10(Math.max(cost, 0.001)) - minLog) / logSpan
+ return SCATTER_PAD_LEFT + Math.max(0, Math.min(1, frac)) * plotWidth
+ }
+ const yFor = (score: number) => SCATTER_PAD_TOP + plotHeight * (1 - Math.max(0, Math.min(1, score)))
+
+ return (
+