'use client'; import { useMemo, useState } from 'react'; import { InteractiveLineChart, Legend, type Series } from '@/components/charts'; import { cn } from '@/lib/cn'; import { fmtInt, fmtUsdPerM, num } from '@/lib/format'; import type { PriceIndexPointIntel } from '@/lib/types'; /* AI Price Index chart: five daily-median series with a linear/log toggle (mirrored into `?scale=` with replaceState) and a tooltip header that carries the sample sizes of the hovered day (offers · models · frontier · open · embedding). Days with a null median are simply absent from that series — a gap, never an interpolation. */ const SERIES: { key: keyof PriceIndexPointIntel; name: string; color: string }[] = [ { key: 'median_input', name: 'Median input', color: 'var(--series-1)' }, { key: 'median_output', name: 'Median output', color: 'var(--series-2)' }, { key: 'median_frontier_output', name: 'Median frontier output', color: 'var(--series-3)' }, { key: 'median_open_output', name: 'Median open-weight output', color: 'var(--series-4)' }, { key: 'median_embedding_input', name: 'Median embedding input', color: 'var(--series-5)' }, ]; export function PriceIndexChart({ series, initialScale = 'linear', className }: { series: PriceIndexPointIntel[]; initialScale?: 'linear' | 'log'; className?: string }) { const [scale, setScale] = useState<'linear' | 'log'>(initialScale); const chart = useMemo( () => SERIES.map((s) => ({ name: s.name, color: s.color, points: series.flatMap((p) => { const v = num(p[s.key]); return v === null ? [] : [{ x: new Date(`${p.day}T00:00:00Z`), y: v }]; }), })).filter((s) => s.points.length > 0), [series], ); const byDay = useMemo(() => new Map(series.map((p) => [new Date(`${p.day}T00:00:00Z`).getTime(), p])), [series]); const populatedDays = useMemo(() => series.filter((p) => SERIES.some((s) => num(p[s.key]) !== null)).length, [series]); const set = (s: 'linear' | 'log') => { setScale(s); try { const url = new URL(window.location.href); if (s === 'log') url.searchParams.set('scale', 'log'); else url.searchParams.delete('scale'); window.history.replaceState(null, '', url.toString()); } catch { /* ignore */ } }; const xFormat = (x: number) => { const p = byDay.get(x); const day = new Date(x).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' }); if (!p) return day; const s = p.sample ?? {}; const bits = [`${fmtInt(p.offers ?? s.offers)} offers`, `${fmtInt(p.models ?? s.models)} models`]; if (num(s.frontier_offers) !== null) bits.push(`${fmtInt(s.frontier_offers)} frontier`); if (num(s.open_models) !== null) bits.push(`${fmtInt(s.open_models)} open`); if (num(s.embedding_models) !== null) bits.push(`${fmtInt(s.embedding_models)} embedding`); return `${day} · ${bits.join(' · ')}`; }; const btn = (s: 'linear' | 'log', label: string) => ( ); if (populatedDays < 2) { return (

The index has {populatedDays} day{populatedDays === 1 ? '' : 's'} of observations so far — a line needs at least two daily snapshots. The chart fills in as the daily crawl accumulates; medians below are today's.

); } return (
{btn('linear', 'Linear')} {btn('log', 'Log scale')}
fmtUsdPerM(v)} yLabel={`USD per 1M tokens${scale === 'log' ? ' (log scale)' : ''}`} yScale={scale} showDots xFormat={xFormat} />
); }