SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
4.2 KB · 82 lines tsx
Raw Blame History
1'use client';2import { useMemo, useState } from 'react';3import { InteractiveLineChart, Legend, type Series } from '@/components/charts';4import { cn } from '@/lib/cn';5import { fmtInt, fmtUsdPerM, num } from '@/lib/format';6import type { PriceIndexPointIntel } from '@/lib/types';78/*9  AI Price Index chart: five daily-median series with a linear/log toggle (mirrored into `?scale=` with replaceState) and a tooltip10  header that carries the sample sizes of the hovered day (offers · models · frontier · open · embedding). Days with a null median11  are simply absent from that series — a gap, never an interpolation.12*/13const SERIES: { key: keyof PriceIndexPointIntel; name: string; color: string }[] = [14  { key: 'median_input', name: 'Median input', color: 'var(--series-1)' },15  { key: 'median_output', name: 'Median output', color: 'var(--series-2)' },16  { key: 'median_frontier_output', name: 'Median frontier output', color: 'var(--series-3)' },17  { key: 'median_open_output', name: 'Median open-weight output', color: 'var(--series-4)' },18  { key: 'median_embedding_input', name: 'Median embedding input', color: 'var(--series-5)' },19];2021export function PriceIndexChart({ series, initialScale = 'linear', className }: { series: PriceIndexPointIntel[]; initialScale?: 'linear' | 'log'; className?: string }) {22  const [scale, setScale] = useState<'linear' | 'log'>(initialScale);23  const chart = useMemo<Series[]>(24    () =>25      SERIES.map((s) => ({26        name: s.name,27        color: s.color,28        points: series.flatMap((p) => {29          const v = num(p[s.key]);30          return v === null ? [] : [{ x: new Date(`${p.day}T00:00:00Z`), y: v }];31        }),32      })).filter((s) => s.points.length > 0),33    [series],34  );35  const byDay = useMemo(() => new Map(series.map((p) => [new Date(`${p.day}T00:00:00Z`).getTime(), p])), [series]);36  const populatedDays = useMemo(() => series.filter((p) => SERIES.some((s) => num(p[s.key]) !== null)).length, [series]);37  const set = (s: 'linear' | 'log') => {38    setScale(s);39    try {40      const url = new URL(window.location.href);41      if (s === 'log') url.searchParams.set('scale', 'log');42      else url.searchParams.delete('scale');43      window.history.replaceState(null, '', url.toString());44    } catch {45      /* ignore */46    }47  };48  const xFormat = (x: number) => {49    const p = byDay.get(x);50    const day = new Date(x).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' });51    if (!p) return day;52    const s = p.sample ?? {};53    const bits = [`${fmtInt(p.offers ?? s.offers)} offers`, `${fmtInt(p.models ?? s.models)} models`];54    if (num(s.frontier_offers) !== null) bits.push(`${fmtInt(s.frontier_offers)} frontier`);55    if (num(s.open_models) !== null) bits.push(`${fmtInt(s.open_models)} open`);56    if (num(s.embedding_models) !== null) bits.push(`${fmtInt(s.embedding_models)} embedding`);57    return `${day} · ${bits.join(' · ')}`;58  };59  const btn = (s: 'linear' | 'log', label: string) => (60    <button type="button" onClick={() => set(s)} aria-pressed={scale === s} className={cn('h-9 border px-3 text-xs font-medium transition-colors', scale === s ? 'border-ink bg-ink text-canvas' : 'border-rule text-ink-2 hover:border-rule-strong hover:text-ink')}>61      {label}62    </button>63  );64  if (populatedDays < 2) {65    return (66      <p className={cn('border border-dashed border-rule-strong px-4 py-6 text-center text-sm text-ink-3', className)} data-price-index-note>67        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&apos;s.68      </p>69    );70  }71  return (72    <div className={className} data-price-index-chart>73      <div className="mb-2 flex items-center gap-1" role="group" aria-label="Y axis scale">74        {btn('linear', 'Linear')}75        {btn('log', 'Log scale')}76      </div>77      <InteractiveLineChart series={chart} height={260} yFormat={(v) => fmtUsdPerM(v)} yLabel={`USD per 1M tokens${scale === 'log' ? ' (log scale)' : ''}`} yScale={scale} showDots xFormat={xFormat} />78      <Legend series={chart} className="mt-2" />79    </div>80  );81}82