Spaces:
Running
Running
| "use client" | |
| import { useMemo, useState } from "react" | |
| import type { | |
| CSSProperties, | |
| FocusEvent as ReactFocusEvent, | |
| MouseEvent as ReactMouseEvent, | |
| } from "react" | |
| import { | |
| feedbackConditionDescription, | |
| type ComputeMark, | |
| type FeedbackCondition, | |
| type ProtocolSeries, | |
| } from "@/lib/collections" | |
| interface ScoreSeries { | |
| /** Stable key — used by the metric dropdown to switch series. */ | |
| key: string | |
| /** Short label shown in the dropdown and as the panel sub-title. */ | |
| label: string | |
| /** Optional longer description shown next to the label. */ | |
| caption?: string | |
| values: number[] | |
| unit?: string | |
| lowerIsBetter?: boolean | |
| /** | |
| * Per-model rows for the optional frontier-plot view. When provided | |
| * (and at least one row carries a parseable releaseDate), the panel | |
| * exposes a chip toggle that swaps the density curve for a | |
| * release-date frontier (cumulative best score over time). | |
| */ | |
| points?: Array<{ | |
| score: number | |
| releaseDate?: string | null | |
| modelName?: string | null | |
| }> | |
| } | |
| interface ScoreDistributionProps { | |
| /** Single-series shorthand. Either pass `values` (single) or `series` (multi). */ | |
| values?: number[] | |
| label?: string | |
| unit?: string | |
| lowerIsBetter?: boolean | |
| /** Multi-series — when provided, a dropdown picker swaps between them. */ | |
| series?: ScoreSeries[] | |
| /** Initial selected key when multi-series. Defaults to first. */ | |
| initialKey?: string | |
| /** Compact variant — shorter, used for matrix per-column distributions. */ | |
| compact?: boolean | |
| /** Initial view when the active series supports both modes. Defaults | |
| * to "distribution". The /embed/.../frontier route passes "frontier" | |
| * to open directly on the Pareto-frontier view. */ | |
| defaultView?: "distribution" | "frontier" | |
| /** When false, the Distribution/Frontier chip toggle is hidden so the | |
| * caller can lock the panel to a single view (e.g. inside an embed | |
| * iframe that the user explicitly chose to embed as Distribution | |
| * *or* Frontier). The metric chips above still appear when the panel | |
| * carries more than one series. Defaults to true. */ | |
| showViewToggle?: boolean | |
| /** Optional Compute view. Only the eval-detail single-metric | |
| * call site passes this — matrix/embed sites must not (their row | |
| * shapes carry no protocol fields). */ | |
| protocol?: ProtocolSeries | |
| } | |
| interface SummaryStats { | |
| n: number | |
| min: number | |
| max: number | |
| mean: number | |
| median: number | |
| q1: number | |
| q3: number | |
| } | |
| function parseReleaseDate(value: string | null | undefined): number | null { | |
| if (!value) return null | |
| const raw = String(value).trim() | |
| if (!raw) return null | |
| // Numeric epoch — treat seconds-since-epoch values as such, ms otherwise. | |
| const numeric = Number(raw) | |
| if (!Number.isNaN(numeric) && !raw.includes("-")) { | |
| const ms = numeric > 1_000_000_000_000 ? numeric : numeric * 1000 | |
| return Number.isFinite(ms) ? ms : null | |
| } | |
| const parsed = new Date(raw).getTime() | |
| return Number.isFinite(parsed) ? parsed : null | |
| } | |
| const MONTH_LABELS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"] | |
| function formatMonthYear(ms: number): string { | |
| const d = new Date(ms) | |
| if (Number.isNaN(d.getTime())) return "" | |
| return `${MONTH_LABELS[d.getUTCMonth()]} ${d.getUTCFullYear()}` | |
| } | |
| function computeStats(values: number[]): SummaryStats | null { | |
| const sorted = values.filter((v) => Number.isFinite(v)).slice().sort((a, b) => a - b) | |
| const n = sorted.length | |
| if (n === 0) return null | |
| const min = sorted[0] | |
| const max = sorted[n - 1] | |
| const mean = sorted.reduce((acc, v) => acc + v, 0) / n | |
| const quantile = (p: number) => { | |
| if (n === 1) return sorted[0] | |
| const pos = (n - 1) * p | |
| const base = Math.floor(pos) | |
| const rest = pos - base | |
| return sorted[base + 1] != null | |
| ? sorted[base] + rest * (sorted[base + 1] - sorted[base]) | |
| : sorted[base] | |
| } | |
| return { | |
| n, | |
| min, | |
| max, | |
| mean, | |
| median: quantile(0.5), | |
| q1: quantile(0.25), | |
| q3: quantile(0.75), | |
| } | |
| } | |
| function formatValue(v: number, unit?: string) { | |
| const abs = Math.abs(v) | |
| let formatted: string | |
| if (abs >= 100) formatted = v.toFixed(1) | |
| else if (abs >= 10) formatted = v.toFixed(2) | |
| else formatted = v.toFixed(3).replace(/0+$/g, "").replace(/\.$/, "") | |
| return unit ? `${formatted} ${unit}` : formatted | |
| } | |
| /** | |
| * Continuous-density distribution plot. | |
| * | |
| * Builds a smoothed kernel density estimate (KDE) from the raw values rather | |
| * than a binned histogram, which reads as a continuous probability-weight | |
| * curve in the paper's hairline style. Median and mean are rendered as | |
| * vertical rules on top of the curve; IQR is a bracket along the baseline. | |
| * | |
| * Multi-series mode shows a small dropdown inside the panel header so a | |
| * caller (e.g. a multi-metric leaderboard) can stack metrics into one | |
| * visualization the user swaps between, instead of rendering N panels. | |
| */ | |
| export function ScoreDistribution({ | |
| values, | |
| label, | |
| unit, | |
| lowerIsBetter, | |
| series, | |
| initialKey, | |
| compact = false, | |
| defaultView, | |
| showViewToggle = true, | |
| protocol, | |
| }: ScoreDistributionProps) { | |
| // Normalize: either we got a single series (via values) or many. | |
| const seriesList: ScoreSeries[] = useMemo(() => { | |
| if (series && series.length > 0) return series | |
| if (values && values.length > 0) { | |
| return [{ key: "__single", label: label ?? "Score", values, unit, lowerIsBetter }] | |
| } | |
| return [] | |
| }, [series, values, label, unit, lowerIsBetter]) | |
| const [activeKey, setActiveKey] = useState<string>( | |
| () => initialKey ?? series?.[0]?.key ?? "__single", | |
| ) | |
| const active = | |
| seriesList.find((s) => s.key === activeKey) ?? seriesList[0] | |
| const stats = useMemo(() => (active ? computeStats(active.values) : null), [active]) | |
| // Frontier-plot data: parse release dates, sort by time, then walk the | |
| // sequence emitting an event whenever a model improves on the best | |
| // score seen so far. Honours lowerIsBetter so e.g. "Mean Response | |
| // Time · ms" shows the frontier descending instead of climbing. | |
| const frontier = useMemo(() => { | |
| if (!active?.points || active.points.length === 0) return null | |
| const lowerIsBetter = active.lowerIsBetter ?? false | |
| const parsed = active.points | |
| .map((p) => { | |
| const t = parseReleaseDate(p.releaseDate) | |
| if (t == null) return null | |
| if (!Number.isFinite(p.score)) return null | |
| return { time: t, score: p.score, name: p.modelName ?? "" } | |
| }) | |
| .filter((p): p is { time: number; score: number; name: string } => p !== null) | |
| .sort((a, b) => a.time - b.time) | |
| if (parsed.length < 2) return null | |
| let best = lowerIsBetter ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY | |
| const events: typeof parsed = [] | |
| for (const p of parsed) { | |
| const better = lowerIsBetter ? p.score < best : p.score > best | |
| if (better) { | |
| best = p.score | |
| events.push(p) | |
| } | |
| } | |
| if (events.length < 2) return null | |
| return { events, samples: parsed } | |
| }, [active]) | |
| const canShowFrontier = frontier != null | |
| const canShowCompute = (protocol?.marks.length ?? 0) > 0 | |
| const [view, setView] = useState<"distribution" | "frontier" | "compute">( | |
| defaultView ?? "distribution", | |
| ) | |
| // If the active series doesn't support the selected view (e.g. user | |
| // switched to a metric whose models don't carry release_date), fall | |
| // back to the distribution view rather than rendering an empty panel. | |
| const effectiveView = | |
| view === "frontier" && canShowFrontier | |
| ? "frontier" | |
| : view === "compute" && canShowCompute | |
| ? "compute" | |
| : "distribution" | |
| // When the caller hides the toggle (embed locks to one view), force the | |
| // panel to whatever defaultView/view it was created with — the user | |
| // can't switch, so any "frontier" inference must come from props. | |
| const renderViewToggle = showViewToggle && (canShowFrontier || canShowCompute) | |
| const availableViews = [ | |
| "distribution" as const, | |
| ...(canShowFrontier ? ["frontier" as const] : []), | |
| ...(canShowCompute ? ["compute" as const] : []), | |
| ] | |
| const density = useMemo(() => { | |
| if (!active || !stats) return null | |
| if (stats.max === stats.min) { | |
| return { points: [{ x: stats.min, y: 1 }], maxY: 1 } | |
| } | |
| const sorted = active.values | |
| .filter((v) => Number.isFinite(v)) | |
| .slice() | |
| .sort((a, b) => a - b) | |
| const n = sorted.length | |
| if (n === 0) return null | |
| // Silverman's rule of thumb for bandwidth. | |
| const variance = | |
| sorted.reduce((acc, v) => acc + (v - stats.mean) ** 2, 0) / n | |
| const stdDev = Math.sqrt(variance) | |
| const iqr = stats.q3 - stats.q1 | |
| const sigma = iqr > 0 ? Math.min(stdDev, iqr / 1.34) : stdDev || (stats.max - stats.min) / 6 | |
| const bandwidth = Math.max( | |
| 1.06 * sigma * Math.pow(n, -0.2), | |
| (stats.max - stats.min) / 80, | |
| ) | |
| const sampleCount = compact ? 80 : 140 | |
| const range = stats.max - stats.min | |
| const xs: number[] = [] | |
| for (let i = 0; i < sampleCount; i++) { | |
| xs.push(stats.min + (range * i) / (sampleCount - 1)) | |
| } | |
| const ys = xs.map((x) => { | |
| let sum = 0 | |
| for (const v of sorted) { | |
| const u = (x - v) / bandwidth | |
| sum += Math.exp(-0.5 * u * u) | |
| } | |
| return sum / (n * bandwidth * Math.sqrt(2 * Math.PI)) | |
| }) | |
| const maxY = Math.max(...ys, 1e-9) | |
| const points = xs.map((x, i) => ({ x, y: ys[i] })) | |
| return { points, maxY } | |
| }, [active, stats, compact]) | |
| if (!active || !stats || !density) return null | |
| const width = 100 | |
| const plotHeight = compact ? 28 : 56 | |
| const fullRange = stats.max - stats.min || 1 | |
| const markerX = (v: number) => ((v - stats.min) / fullRange) * width | |
| const markerY = (y: number) => plotHeight - (y / density.maxY) * (plotHeight - 4) | |
| const path = density.points | |
| .map((p, i) => { | |
| const x = markerX(p.x) | |
| const y = markerY(p.y) | |
| return `${i === 0 ? "M" : "L"}${x.toFixed(3)},${y.toFixed(3)}` | |
| }) | |
| .join(" ") | |
| const fillPath = `${path} L${width.toFixed(3)},${plotHeight.toFixed(3)} L0,${plotHeight.toFixed(3)} Z` | |
| const captionItems: Array<{ label: string; value: string; key: string }> = [ | |
| { key: "n", label: "n", value: stats.n.toString() }, | |
| { key: "min", label: "min", value: formatValue(stats.min, active.unit) }, | |
| { key: "q1", label: "q1", value: formatValue(stats.q1, active.unit) }, | |
| { key: "median", label: "median", value: formatValue(stats.median, active.unit) }, | |
| { key: "mean", label: "mean", value: formatValue(stats.mean, active.unit) }, | |
| { key: "q3", label: "q3", value: formatValue(stats.q3, active.unit) }, | |
| { key: "max", label: "max", value: formatValue(stats.max, active.unit) }, | |
| ] | |
| const directionHint = active.lowerIsBetter ? "lower is better ←" : "higher is better →" | |
| const showPicker = seriesList.length > 1 | |
| return ( | |
| <div | |
| style={{ | |
| padding: compact ? "10px 12px" : "16px 20px", | |
| border: "1px solid var(--border-soft)", | |
| background: "var(--bg)", | |
| }} | |
| > | |
| {!compact && ( | |
| <div className="mb-3 space-y-2"> | |
| {/* Row 1 — View toggle (left) + direction hint (right). The | |
| kicker label makes it clear that these chips switch the | |
| chart type, distinguishing them from the metric chips | |
| below. */} | |
| <div className="flex items-center justify-between gap-3 flex-wrap"> | |
| <div className="flex items-center gap-2 min-w-0"> | |
| <span | |
| className="font-mono uppercase shrink-0" | |
| style={{ fontSize: 10, letterSpacing: "0.14em", color: "var(--fg-subtle)" }} | |
| > | |
| {renderViewToggle ? "View" : "Score distribution"} | |
| </span> | |
| {renderViewToggle && ( | |
| <div | |
| role="tablist" | |
| aria-label="Chart view" | |
| className="inline-flex items-center gap-1" | |
| > | |
| {availableViews.map((view) => { | |
| const on = effectiveView === view | |
| const label = | |
| view === "distribution" | |
| ? "Distribution" | |
| : view === "frontier" | |
| ? "Frontier" | |
| : "Compute" | |
| return ( | |
| <button | |
| key={view} | |
| type="button" | |
| role="tab" | |
| aria-selected={on} | |
| onClick={() => setView(view)} | |
| title={ | |
| view === "frontier" | |
| ? "Frontier score over model release dates (cumulative best)." | |
| : view === "compute" | |
| ? `Per-run scores across the study's ${protocol?.axisLabel ?? "compute axis"} settings.` | |
| : "Kernel-density distribution of model scores." | |
| } | |
| className={`ec-pill${on ? " on" : ""}`} | |
| > | |
| {label} | |
| </button> | |
| ) | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| <div | |
| className="font-mono uppercase shrink-0" | |
| style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }} | |
| > | |
| {directionHint} | |
| </div> | |
| </div> | |
| {/* Row 2 — Metric chips. Only shown when there's more than | |
| one series; otherwise the active label gets a quiet inline | |
| caption next to the view kicker. */} | |
| {showPicker ? ( | |
| <div className="flex items-baseline gap-2 flex-wrap"> | |
| <span | |
| className="font-mono uppercase shrink-0" | |
| style={{ fontSize: 10, letterSpacing: "0.14em", color: "var(--fg-subtle)" }} | |
| > | |
| Metric | |
| </span> | |
| <div className="flex flex-wrap items-center gap-1.5"> | |
| {seriesList.map((s) => { | |
| const on = s.key === active.key | |
| return ( | |
| <button | |
| key={s.key} | |
| type="button" | |
| className={`ec-pill${on ? " on" : ""}`} | |
| onClick={() => setActiveKey(s.key)} | |
| title={s.caption ? `${s.label} · ${s.caption}` : s.label} | |
| > | |
| {s.label} | |
| {s.caption ? ( | |
| <span | |
| className="ml-1.5" | |
| style={{ color: on ? undefined : "var(--fg-subtle)" }} | |
| > | |
| · {s.caption} | |
| </span> | |
| ) : null} | |
| </button> | |
| ) | |
| })} | |
| </div> | |
| </div> | |
| ) : ( | |
| <div | |
| className="font-mono uppercase truncate" | |
| style={{ fontSize: 11, letterSpacing: "0.08em", color: "var(--fg-muted)" }} | |
| title={active.label} | |
| > | |
| {active.label} | |
| {active.unit ? <span style={{ color: "var(--fg-subtle)" }}>{" · " + active.unit}</span> : null} | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {compact && ( | |
| <div | |
| className="font-mono uppercase mb-1.5 truncate" | |
| style={{ fontSize: 9.5, letterSpacing: "0.1em", color: "var(--fg-subtle)" }} | |
| title={active.label} | |
| > | |
| {active.label} | |
| </div> | |
| )} | |
| {effectiveView === "compute" && protocol ? ( | |
| <ComputePlot protocol={protocol} unit={active.unit} label={active.label} /> | |
| ) : effectiveView === "frontier" && frontier ? ( | |
| <FrontierPlot | |
| events={frontier.events} | |
| samples={frontier.samples} | |
| unit={active.unit} | |
| lowerIsBetter={active.lowerIsBetter ?? false} | |
| label={active.label} | |
| /> | |
| ) : ( | |
| <svg | |
| viewBox={`0 0 ${width} ${plotHeight + 8}`} | |
| preserveAspectRatio="none" | |
| style={{ width: "100%", height: compact ? 38 : 72, display: "block" }} | |
| role="img" | |
| aria-label={`${active.label} distribution: ${stats.n} models`} | |
| > | |
| {/* Filled density area */} | |
| <path | |
| d={fillPath} | |
| fill="var(--bg-surface)" | |
| stroke="none" | |
| opacity={0.85} | |
| /> | |
| {/* Density curve */} | |
| <path | |
| d={path} | |
| fill="none" | |
| stroke="var(--fg-muted)" | |
| strokeWidth={0.7} | |
| vectorEffect="non-scaling-stroke" | |
| strokeLinejoin="round" | |
| strokeLinecap="round" | |
| /> | |
| {/* Baseline */} | |
| <line | |
| x1={0} | |
| x2={width} | |
| y1={plotHeight} | |
| y2={plotHeight} | |
| stroke="var(--border-strong)" | |
| strokeWidth={0.4} | |
| vectorEffect="non-scaling-stroke" | |
| /> | |
| {/* IQR bracket along baseline */} | |
| <line | |
| x1={markerX(stats.q1)} | |
| x2={markerX(stats.q3)} | |
| y1={plotHeight + 3} | |
| y2={plotHeight + 3} | |
| stroke="var(--fg-muted)" | |
| strokeWidth={0.7} | |
| vectorEffect="non-scaling-stroke" | |
| /> | |
| <line | |
| x1={markerX(stats.q1)} | |
| x2={markerX(stats.q1)} | |
| y1={plotHeight + 1.5} | |
| y2={plotHeight + 4.5} | |
| stroke="var(--fg-muted)" | |
| strokeWidth={0.7} | |
| vectorEffect="non-scaling-stroke" | |
| /> | |
| <line | |
| x1={markerX(stats.q3)} | |
| x2={markerX(stats.q3)} | |
| y1={plotHeight + 1.5} | |
| y2={plotHeight + 4.5} | |
| stroke="var(--fg-muted)" | |
| strokeWidth={0.7} | |
| vectorEffect="non-scaling-stroke" | |
| /> | |
| {/* Median vertical rule (accent) */} | |
| <line | |
| x1={markerX(stats.median)} | |
| x2={markerX(stats.median)} | |
| y1={2} | |
| y2={plotHeight} | |
| stroke="var(--accent)" | |
| strokeWidth={0.9} | |
| vectorEffect="non-scaling-stroke" | |
| /> | |
| {/* Mean tick (dashed) */} | |
| <line | |
| x1={markerX(stats.mean)} | |
| x2={markerX(stats.mean)} | |
| y1={2} | |
| y2={plotHeight} | |
| stroke="var(--fg)" | |
| strokeWidth={0.6} | |
| strokeDasharray="2 2" | |
| vectorEffect="non-scaling-stroke" | |
| /> | |
| </svg> | |
| )} | |
| {/* Caption row — distribution view only; the frontier and compute | |
| panels render their own captions. */} | |
| {effectiveView === "distribution" && ( | |
| <div | |
| className="mt-2 flex flex-wrap items-baseline font-mono" | |
| style={{ | |
| fontSize: compact ? 9.5 : 10.5, | |
| letterSpacing: "0.04em", | |
| color: "var(--fg-muted)", | |
| gap: compact ? "6px 10px" : "4px 14px", | |
| }} | |
| > | |
| {captionItems.map((item, i) => ( | |
| <span key={item.key} className="inline-flex items-baseline gap-1"> | |
| {i > 0 && <span style={{ color: "var(--fg-subtle)" }}>·</span>} | |
| <span | |
| className="uppercase" | |
| style={{ | |
| color: item.key === "median" | |
| ? "var(--accent)" | |
| : item.key === "mean" | |
| ? "var(--fg)" | |
| : "var(--fg-subtle)", | |
| fontSize: compact ? 9 : 9.5, | |
| letterSpacing: "0.12em", | |
| }} | |
| > | |
| {item.label} | |
| </span> | |
| <span | |
| className="tabular-nums" | |
| style={{ | |
| color: item.key === "median" ? "var(--accent)" : "var(--fg)", | |
| fontWeight: item.key === "median" ? 600 : 500, | |
| }} | |
| > | |
| {item.value} | |
| </span> | |
| </span> | |
| ))} | |
| </div> | |
| )} | |
| {!compact && effectiveView === "distribution" && ( | |
| <div | |
| className="mt-1 flex items-center gap-3 font-mono" | |
| style={{ fontSize: 9, letterSpacing: "0.06em", color: "var(--fg-subtle)" }} | |
| > | |
| <span className="inline-flex items-center gap-1"> | |
| <span | |
| aria-hidden | |
| style={{ | |
| display: "inline-block", | |
| width: 10, | |
| height: 1, | |
| background: "var(--accent)", | |
| }} | |
| /> | |
| median | |
| </span> | |
| <span className="inline-flex items-center gap-1"> | |
| <span | |
| aria-hidden | |
| style={{ | |
| display: "inline-block", | |
| width: 10, | |
| height: 1, | |
| borderTop: "1px dashed var(--fg)", | |
| }} | |
| /> | |
| mean | |
| </span> | |
| <span className="inline-flex items-center gap-1"> | |
| <span | |
| aria-hidden | |
| style={{ | |
| display: "inline-block", | |
| width: 10, | |
| height: 1, | |
| background: "var(--fg-muted)", | |
| }} | |
| /> | |
| IQR (q1–q3) | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |
| interface FrontierPlotProps { | |
| /** Strictly-improving subset of the input — each entry pushes the | |
| * cumulative best score further. Already sorted ascending by time. */ | |
| events: Array<{ time: number; score: number; name: string }> | |
| /** Every dated sample (improving or not), used as background dots. */ | |
| samples: Array<{ time: number; score: number; name: string }> | |
| unit?: string | |
| lowerIsBetter: boolean | |
| label: string | |
| } | |
| function FrontierPlot({ events, samples, unit, lowerIsBetter, label }: FrontierPlotProps) { | |
| const PLOT_HEIGHT = 180 | |
| const PAD_T = 8 | |
| const PAD_B = 22 // room for year labels under the axis | |
| const PAD_L_PCT = 1 | |
| const PAD_R_PCT = 1 | |
| const tMin = Math.min(...samples.map((s) => s.time)) | |
| const tMaxData = Math.max(...samples.map((s) => s.time)) | |
| // Always extend the rightmost edge to "now" so the user sees how | |
| // long the current frontier holder has been on top. | |
| const tMax = Math.max(tMaxData, Date.now()) | |
| const tRange = tMax - tMin || 1 | |
| const sValues = samples.map((s) => s.score) | |
| const sMin = Math.min(...sValues) | |
| const sMax = Math.max(...sValues) | |
| const sRange = sMax - sMin || Math.abs(sMax) || 1 | |
| // Pad y so dots don't kiss the borders. | |
| const yLo = sMin - sRange * 0.05 | |
| const yHi = sMax + sRange * 0.05 | |
| const yRange = yHi - yLo || 1 | |
| // Percent helpers — used for both HTML overlay positioning and the | |
| // SVG path (which uses a 0-100 viewBox so the line scales with the | |
| // container without distorting other glyphs). | |
| const xPct = (t: number) => | |
| PAD_L_PCT + ((t - tMin) / tRange) * (100 - PAD_L_PCT - PAD_R_PCT) | |
| const yPct = (s: number) => | |
| 100 - ((s - yLo) / yRange) * 100 // 0 at top, 100 at bottom | |
| // Pixel helpers for the step-line SVG. Keep its viewBox at 100x100 | |
| // so it overlays the container 1:1, while strokeWidth uses | |
| // vectorEffect=non-scaling-stroke so the line stays crisp. | |
| let d = "" | |
| for (let i = 0; i < events.length; i++) { | |
| const e = events[i] | |
| const x = xPct(e.time) | |
| const y = yPct(e.score) | |
| if (i === 0) { | |
| d += `M${x.toFixed(3)},${y.toFixed(3)} ` | |
| } else { | |
| const prev = events[i - 1] | |
| const yPrev = yPct(prev.score) | |
| d += `L${x.toFixed(3)},${yPrev.toFixed(3)} L${x.toFixed(3)},${y.toFixed(3)} ` | |
| } | |
| } | |
| if (events.length > 0) { | |
| const last = events[events.length - 1] | |
| d += `L${xPct(tMax).toFixed(3)},${yPct(last.score).toFixed(3)}` | |
| } | |
| // Year tick marks along the x-axis. Keep at most 6 to avoid label | |
| // collisions on narrow viewports. | |
| const startYear = new Date(tMin).getUTCFullYear() | |
| const endYear = new Date(tMax).getUTCFullYear() | |
| const yearSpan = endYear - startYear | |
| const tickStep = yearSpan <= 6 ? 1 : Math.ceil(yearSpan / 6) | |
| const yearTicks: number[] = [] | |
| for (let y = startYear; y <= endYear; y += tickStep) yearTicks.push(y) | |
| // Pre-bucket samples that *aren't* on the frontier so we don't | |
| // double-render them (the frontier dots are emphasised separately). | |
| const eventTimes = new Set(events.map((e) => `${e.time}|${e.score}`)) | |
| const bgSamples = samples.filter((s) => !eventTimes.has(`${s.time}|${s.score}`)) | |
| // Local hover state so we can render a richer label than the native | |
| // `title=` tooltip — keeps the dot and the popup nameplate in sync | |
| // even when the cursor sits right between two dots. | |
| const [hover, setHover] = useState<{ | |
| x: number | |
| y: number | |
| name: string | |
| when: string | |
| score: string | |
| onFrontier: boolean | |
| } | null>(null) | |
| return ( | |
| <div> | |
| <div | |
| style={{ | |
| position: "relative", | |
| width: "100%", | |
| height: PLOT_HEIGHT, | |
| paddingTop: PAD_T, | |
| paddingBottom: PAD_B, | |
| boxSizing: "border-box", | |
| }} | |
| onMouseLeave={() => setHover(null)} | |
| > | |
| {/* Inner plot canvas (the area minus axis padding). */} | |
| <div | |
| style={{ | |
| position: "absolute", | |
| top: PAD_T, | |
| bottom: PAD_B, | |
| left: 0, | |
| right: 0, | |
| }} | |
| > | |
| {/* Step line. The SVG uses a 0-100 viewBox so its path lines | |
| up with HTML overlays positioned via the same xPct/yPct | |
| helpers; non-scaling-stroke keeps the stroke crisp. */} | |
| <svg | |
| viewBox="0 0 100 100" | |
| preserveAspectRatio="none" | |
| style={{ | |
| position: "absolute", | |
| inset: 0, | |
| width: "100%", | |
| height: "100%", | |
| pointerEvents: "none", | |
| }} | |
| aria-hidden | |
| > | |
| <path | |
| d={d} | |
| fill="none" | |
| stroke="var(--accent)" | |
| strokeWidth={1.5} | |
| vectorEffect="non-scaling-stroke" | |
| strokeLinejoin="round" | |
| strokeLinecap="round" | |
| /> | |
| </svg> | |
| {/* Background sample dots — every dated model that's NOT on | |
| the frontier. Rendered as HTML so they're crisp circles | |
| and individually clickable / focusable. */} | |
| {bgSamples.map((s, i) => ( | |
| <button | |
| key={`s-${i}`} | |
| type="button" | |
| aria-label={`${s.name || "Model"} · ${formatMonthYear(s.time)} · ${formatValue(s.score, unit)}`} | |
| onMouseEnter={(event) => { | |
| const rect = event.currentTarget.parentElement!.getBoundingClientRect() | |
| const dot = event.currentTarget.getBoundingClientRect() | |
| setHover({ | |
| x: dot.left + dot.width / 2 - rect.left, | |
| y: dot.top + dot.height / 2 - rect.top, | |
| name: s.name || "Model", | |
| when: formatMonthYear(s.time), | |
| score: formatValue(s.score, unit), | |
| onFrontier: false, | |
| }) | |
| }} | |
| onFocus={(event) => { | |
| const rect = event.currentTarget.parentElement!.getBoundingClientRect() | |
| const dot = event.currentTarget.getBoundingClientRect() | |
| setHover({ | |
| x: dot.left + dot.width / 2 - rect.left, | |
| y: dot.top + dot.height / 2 - rect.top, | |
| name: s.name || "Model", | |
| when: formatMonthYear(s.time), | |
| score: formatValue(s.score, unit), | |
| onFrontier: false, | |
| }) | |
| }} | |
| style={{ | |
| position: "absolute", | |
| left: `${xPct(s.time)}%`, | |
| top: `${yPct(s.score)}%`, | |
| transform: "translate(-50%, -50%)", | |
| width: 7, | |
| height: 7, | |
| borderRadius: "50%", | |
| background: "var(--fg-subtle)", | |
| opacity: 0.4, | |
| border: "none", | |
| padding: 0, | |
| cursor: "pointer", | |
| }} | |
| /> | |
| ))} | |
| {/* Frontier-crossing dots, foregrounded. */} | |
| {events.map((e, i) => ( | |
| <button | |
| key={`e-${i}`} | |
| type="button" | |
| aria-label={`Frontier: ${e.name || "Model"} · ${formatMonthYear(e.time)} · ${formatValue(e.score, unit)}`} | |
| onMouseEnter={(event) => { | |
| const rect = event.currentTarget.parentElement!.getBoundingClientRect() | |
| const dot = event.currentTarget.getBoundingClientRect() | |
| setHover({ | |
| x: dot.left + dot.width / 2 - rect.left, | |
| y: dot.top + dot.height / 2 - rect.top, | |
| name: e.name || "Model", | |
| when: formatMonthYear(e.time), | |
| score: formatValue(e.score, unit), | |
| onFrontier: true, | |
| }) | |
| }} | |
| onFocus={(event) => { | |
| const rect = event.currentTarget.parentElement!.getBoundingClientRect() | |
| const dot = event.currentTarget.getBoundingClientRect() | |
| setHover({ | |
| x: dot.left + dot.width / 2 - rect.left, | |
| y: dot.top + dot.height / 2 - rect.top, | |
| name: e.name || "Model", | |
| when: formatMonthYear(e.time), | |
| score: formatValue(e.score, unit), | |
| onFrontier: true, | |
| }) | |
| }} | |
| style={{ | |
| position: "absolute", | |
| left: `${xPct(e.time)}%`, | |
| top: `${yPct(e.score)}%`, | |
| transform: "translate(-50%, -50%)", | |
| width: 11, | |
| height: 11, | |
| borderRadius: "50%", | |
| background: "var(--accent)", | |
| border: "1.5px solid var(--bg)", | |
| padding: 0, | |
| cursor: "pointer", | |
| boxShadow: "0 0 0 0.5px var(--accent)", | |
| }} | |
| /> | |
| ))} | |
| {/* Hover nameplate */} | |
| {hover && ( | |
| <div | |
| role="status" | |
| style={{ | |
| position: "absolute", | |
| left: hover.x, | |
| top: hover.y - 14, | |
| transform: "translate(-50%, -100%)", | |
| pointerEvents: "none", | |
| background: "var(--fg)", | |
| color: "var(--bg)", | |
| padding: "5px 9px", | |
| fontSize: 11, | |
| lineHeight: 1.3, | |
| whiteSpace: "nowrap", | |
| fontFamily: "var(--font-sans, inherit)", | |
| boxShadow: "var(--shadow-card, 0 2px 6px rgba(0,0,0,0.18))", | |
| zIndex: 2, | |
| }} | |
| > | |
| <div style={{ fontWeight: 600 }}>{hover.name}</div> | |
| <div | |
| className="font-mono" | |
| style={{ | |
| fontSize: 10, | |
| letterSpacing: "0.04em", | |
| opacity: 0.8, | |
| marginTop: 1, | |
| }} | |
| > | |
| {hover.when} · {hover.score} | |
| {hover.onFrontier ? " · frontier" : ""} | |
| </div> | |
| </div> | |
| )} | |
| {/* Baseline */} | |
| <div | |
| aria-hidden | |
| style={{ | |
| position: "absolute", | |
| left: 0, | |
| right: 0, | |
| bottom: 0, | |
| height: 1, | |
| background: "var(--border-strong)", | |
| }} | |
| /> | |
| </div> | |
| {/* Year ticks under the baseline */} | |
| {yearTicks.map((y) => { | |
| const t = Date.UTC(y, 0, 1) | |
| if (t < tMin || t > tMax) return null | |
| return ( | |
| <div | |
| key={y} | |
| aria-hidden | |
| style={{ | |
| position: "absolute", | |
| left: `${xPct(t)}%`, | |
| bottom: 4, | |
| transform: "translateX(-50%)", | |
| fontFamily: "var(--font-mono)", | |
| fontSize: 10, | |
| color: "var(--fg-subtle)", | |
| letterSpacing: "0.06em", | |
| }} | |
| > | |
| {y} | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| <div | |
| className="mt-1 flex flex-wrap items-baseline font-mono" | |
| style={{ | |
| fontSize: 10.5, | |
| letterSpacing: "0.04em", | |
| color: "var(--fg-muted)", | |
| gap: "4px 14px", | |
| }} | |
| > | |
| <span className="inline-flex items-baseline gap-1"> | |
| <span | |
| className="uppercase" | |
| style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }} | |
| > | |
| frontier | |
| </span> | |
| <span className="tabular-nums" style={{ color: "var(--fg)", fontWeight: 600 }}> | |
| {events.length} step{events.length === 1 ? "" : "s"} | |
| </span> | |
| </span> | |
| <span className="inline-flex items-baseline gap-1"> | |
| <span style={{ color: "var(--fg-subtle)" }}>·</span> | |
| <span | |
| className="uppercase" | |
| style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }} | |
| > | |
| best | |
| </span> | |
| <span className="tabular-nums" style={{ color: "var(--fg)" }}> | |
| {formatValue(events[events.length - 1]?.score, unit)} | |
| </span> | |
| <span style={{ color: "var(--fg-subtle)" }}>by</span> | |
| <span style={{ color: "var(--fg)" }}>{events[events.length - 1]?.name || "—"}</span> | |
| </span> | |
| <span className="inline-flex items-baseline gap-1"> | |
| <span style={{ color: "var(--fg-subtle)" }}>·</span> | |
| <span | |
| className="uppercase" | |
| style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }} | |
| > | |
| since | |
| </span> | |
| <span className="tabular-nums" style={{ color: "var(--fg)" }}> | |
| {formatMonthYear(events[0].time)} | |
| </span> | |
| </span> | |
| <span className="inline-flex items-baseline gap-1" style={{ color: "var(--fg-subtle)" }}> | |
| <span>·</span> | |
| <span style={{ fontSize: 9 }}> | |
| {lowerIsBetter ? "frontier descends: lower is better" : "frontier ascends: higher is better"} | |
| </span> | |
| </span> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Compute view: per-run scores over | |
| // the study's nominal budget axis. Honest-estimator rules: no trend | |
| // lines (2–3 nominal levels per axis, conditions at unmatched budgets — a line | |
| // would assert a compute trend the study says must be compared at | |
| // matched budgets); highlight scopes to one (model, condition), never across | |
| // conditions; the condition legend is always visible. | |
| // --------------------------------------------------------------------------- | |
| const CONDITION_LEGEND: Record<FeedbackCondition, string> = { | |
| none: "no feedback", | |
| unknown: "condition unknown", | |
| answer_feedback: "oracle score feedback (assisted)", | |
| } | |
| function formatTokenTick(v: number): string { | |
| const trim = (n: number) => { | |
| const s = n.toFixed(n >= 10 ? 0 : 1) | |
| return s.replace(/\.0$/, "") | |
| } | |
| if (v >= 1_000_000) return `${trim(v / 1_000_000)}M` | |
| if (v >= 1_000) return `${trim(v / 1_000)}k` | |
| return String(v) | |
| } | |
| function ConditionGlyph({ | |
| condition, | |
| highlighted, | |
| }: { | |
| condition: FeedbackCondition | |
| highlighted?: boolean | |
| }) { | |
| const ink = highlighted ? "var(--accent)" : "var(--fg-muted)" | |
| const base: CSSProperties = { display: "inline-block", boxSizing: "border-box" } | |
| if (condition === "none") { | |
| return ( | |
| <span | |
| aria-hidden | |
| style={{ ...base, width: 9, height: 9, borderRadius: "50%", background: ink }} | |
| /> | |
| ) | |
| } | |
| if (condition === "unknown") { | |
| return ( | |
| <span | |
| aria-hidden | |
| style={{ | |
| ...base, | |
| width: 8, | |
| height: 8, | |
| border: `1.5px solid ${ink}`, | |
| transform: "rotate(45deg)", | |
| background: "transparent", | |
| }} | |
| /> | |
| ) | |
| } | |
| return ( | |
| <span | |
| aria-hidden | |
| style={{ | |
| ...base, | |
| width: 9, | |
| height: 9, | |
| borderRadius: "50%", | |
| border: `1.5px solid ${ink}`, | |
| background: "transparent", | |
| opacity: highlighted ? 1 : 0.55, | |
| }} | |
| /> | |
| ) | |
| } | |
| export function ComputePlot({ | |
| protocol, | |
| unit, | |
| label, | |
| }: { | |
| protocol: ProtocolSeries | |
| unit?: string | |
| label: string | |
| }) { | |
| const { marks, axisLabel, omitted, mismatchedConditionBudgets, researcherMode } = protocol | |
| const PLOT_HEIGHT = 190 | |
| const PAD_T = 8 | |
| const PAD_B = 24 | |
| const logs = marks.map((m) => Math.log10(m.x)) | |
| let xLo = Math.min(...logs) | |
| let xHi = Math.max(...logs) | |
| if (xHi - xLo < 1e-9) { | |
| xLo -= 0.5 | |
| xHi += 0.5 | |
| } | |
| const xPad = (xHi - xLo) * 0.06 | |
| xLo -= xPad | |
| xHi += xPad | |
| const scores = marks.map((m) => m.score) | |
| const sMin = Math.min(...scores) | |
| const sMax = Math.max(...scores) | |
| const sRange = sMax - sMin || Math.abs(sMax) || 1 | |
| const yLo = sMin - sRange * 0.05 | |
| const yHi = sMax + sRange * 0.05 | |
| const yRange = yHi - yLo || 1 | |
| const xPct = (v: number) => ((Math.log10(v) - xLo) / (xHi - xLo)) * 98 + 1 | |
| const yPct = (s: number) => 100 - ((s - yLo) / yRange) * 100 | |
| // Ticks at the distinct NOMINAL levels actually present (2–6 values). | |
| const ticks = Array.from(new Set(marks.map((m) => m.x))).sort((a, b) => a - b) | |
| const conditionsPresent = (["none", "unknown", "answer_feedback"] as FeedbackCondition[]).filter( | |
| (condition) => marks.some((m) => m.condition === condition), | |
| ) | |
| const [highlightKey, setHighlightKey] = useState<string | null>(null) | |
| const [hover, setHover] = useState<{ x: number; y: number; mark: ComputeMark } | null>(null) | |
| const markKey = (mark: ComputeMark) => `${mark.modelName}|${mark.condition}` | |
| const hoverDetail = (mark: ComputeMark): string => { | |
| if (researcherMode) { | |
| return Object.entries(mark.protocolFields) | |
| .filter(([, v]) => v != null) | |
| .map(([k, v]) => `${k}=${String(v)}`) | |
| .join(" · ") | |
| } | |
| return feedbackConditionDescription(mark.condition) | |
| } | |
| return ( | |
| <div> | |
| <div | |
| role="img" | |
| aria-label={`${label}: ${marks.length} runs over ${axisLabel}`} | |
| style={{ | |
| position: "relative", | |
| width: "100%", | |
| height: PLOT_HEIGHT, | |
| paddingTop: PAD_T, | |
| paddingBottom: PAD_B, | |
| boxSizing: "border-box", | |
| }} | |
| onMouseLeave={() => { | |
| setHover(null) | |
| setHighlightKey(null) | |
| }} | |
| > | |
| <div style={{ position: "absolute", top: PAD_T, bottom: PAD_B, left: 0, right: 0 }}> | |
| {/* Tick rules at the nominal levels */} | |
| {ticks.map((t) => ( | |
| <div | |
| key={t} | |
| aria-hidden | |
| style={{ | |
| position: "absolute", | |
| left: `${xPct(t)}%`, | |
| top: 0, | |
| bottom: 0, | |
| width: 1, | |
| background: "var(--border-soft)", | |
| }} | |
| /> | |
| ))} | |
| {marks.map((mark, i) => { | |
| const highlighted = highlightKey === markKey(mark) | |
| const dimmed = highlightKey != null && !highlighted | |
| const enter = ( | |
| event: ReactMouseEvent<HTMLButtonElement> | ReactFocusEvent<HTMLButtonElement>, | |
| ) => { | |
| const rect = event.currentTarget.parentElement!.getBoundingClientRect() | |
| const dot = event.currentTarget.getBoundingClientRect() | |
| setHover({ | |
| x: dot.left + dot.width / 2 - rect.left, | |
| y: dot.top + dot.height / 2 - rect.top, | |
| mark, | |
| }) | |
| // Highlight every mark of this (model, condition) pair — | |
| // never across conditions, which would imply a | |
| // cross-condition per-model trend. | |
| setHighlightKey(markKey(mark)) | |
| } | |
| return ( | |
| <button | |
| key={`m-${i}`} | |
| type="button" | |
| aria-label={`${mark.modelName} · ${CONDITION_LEGEND[mark.condition]} · ${formatValue(mark.score, unit)} at ${formatTokenTick(mark.x)} ${axisLabel}`} | |
| onMouseEnter={enter} | |
| onFocus={enter} | |
| style={{ | |
| position: "absolute", | |
| left: `${xPct(mark.x)}%`, | |
| top: `${yPct(mark.score)}%`, | |
| transform: "translate(-50%, -50%)", | |
| padding: 0, | |
| border: "none", | |
| background: "transparent", | |
| cursor: "pointer", | |
| opacity: dimmed ? 0.3 : 1, | |
| zIndex: highlighted ? 1 : undefined, | |
| }} | |
| > | |
| <ConditionGlyph condition={mark.condition} highlighted={highlighted} /> | |
| </button> | |
| ) | |
| })} | |
| {hover && ( | |
| <div | |
| role="status" | |
| style={{ | |
| position: "absolute", | |
| left: hover.x, | |
| top: hover.y - 14, | |
| transform: "translate(-50%, -100%)", | |
| pointerEvents: "none", | |
| background: "var(--fg)", | |
| color: "var(--bg)", | |
| padding: "5px 9px", | |
| fontSize: 11, | |
| lineHeight: 1.3, | |
| whiteSpace: "nowrap", | |
| maxWidth: 420, | |
| boxShadow: "var(--shadow-card, 0 2px 6px rgba(0,0,0,0.18))", | |
| zIndex: 2, | |
| }} | |
| > | |
| <div style={{ fontWeight: 600 }}>{hover.mark.modelName}</div> | |
| <div | |
| className="font-mono" | |
| style={{ fontSize: 10, letterSpacing: "0.04em", opacity: 0.8, marginTop: 1 }} | |
| > | |
| {formatValue(hover.mark.score, unit)} · {formatTokenTick(hover.mark.x)}{" "} | |
| {axisLabel} | |
| </div> | |
| <div style={{ fontSize: 10, opacity: 0.8, marginTop: 1, whiteSpace: "normal" }}> | |
| {hoverDetail(hover.mark)} | |
| </div> | |
| </div> | |
| )} | |
| {/* Baseline */} | |
| <div | |
| aria-hidden | |
| style={{ | |
| position: "absolute", | |
| left: 0, | |
| right: 0, | |
| bottom: 0, | |
| height: 1, | |
| background: "var(--border-strong)", | |
| }} | |
| /> | |
| </div> | |
| {/* Nominal-level labels under the baseline (log-positioned) */} | |
| {ticks.map((t) => ( | |
| <div | |
| key={`t-${t}`} | |
| aria-hidden | |
| style={{ | |
| position: "absolute", | |
| left: `${xPct(t)}%`, | |
| bottom: 4, | |
| transform: "translateX(-50%)", | |
| fontFamily: "var(--font-mono)", | |
| fontSize: 10, | |
| color: "var(--fg-subtle)", | |
| letterSpacing: "0.06em", | |
| }} | |
| > | |
| {formatTokenTick(t)} | |
| </div> | |
| ))} | |
| </div> | |
| {/* Condition legend — ALWAYS visible: the study's own figures use | |
| solid/hollow marks with a different meaning, so arriving | |
| readers need the encoding spelled out. */} | |
| <div | |
| className="mt-1 flex flex-wrap items-center gap-x-4 gap-y-1 font-mono" | |
| style={{ fontSize: 9.5, letterSpacing: "0.06em", color: "var(--fg-subtle)" }} | |
| > | |
| {conditionsPresent.map((condition) => ( | |
| <span key={condition} className="inline-flex items-center gap-1.5"> | |
| <ConditionGlyph condition={condition} /> | |
| {CONDITION_LEGEND[condition]} | |
| </span> | |
| ))} | |
| </div> | |
| <div | |
| className="mt-1.5 space-y-0.5 font-mono" | |
| style={{ fontSize: 10, letterSpacing: "0.04em", color: "var(--fg-muted)" }} | |
| > | |
| <div> | |
| {marks.length} runs · x: {axisLabel} (log scale) · y: {label} | |
| </div> | |
| {omitted > 0 && ( | |
| <div style={{ color: "var(--fg-subtle)" }}> | |
| {omitted} {omitted === 1 ? "run has" : "runs have"} no recorded {axisLabel} and{" "} | |
| {omitted === 1 ? "is" : "are"} left out | |
| </div> | |
| )} | |
| {mismatchedConditionBudgets && ( | |
| <div style={{ color: "var(--fg-subtle)" }}> | |
| feedback conditions ran at different budgets; compare them only where budgets match | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| } | |