'use client'; import Link from 'next/link'; import { useMemo, useState } from 'react'; import { t } from '@/i18n'; import { cn } from '@/lib/cn'; import { formatValue } from '@/lib/format'; import { routes } from '@/lib/site'; import type { FormatSpec } from '@/lib/types'; import type { RaceResponse } from '@/lib/types-analytics'; import { YearSlider } from '@/components/controls/year-slider'; import { regionColor } from './bubble-chart'; import { ChartFrame, type TableData } from './chart-frame'; const ROW_H = 36; /** * Bar chart race: the top N countries of a ranking, one horizontal bar per country keyed by id so bars slide * to their new rank (CSS transitions on transform + width) as the year slider plays. Colour = region, flags + * names + values, faded year behind. Data from `/rankings/{indicator}/race`. Table toggle lists the current frame. */ export function RankRace({ data, spec, top = 10, height, interval = 650, className, initialYear }: { data: RaceResponse; spec: FormatSpec; top?: number; height?: number; interval?: number; className?: string; initialYear?: number | null }) { const years = data.years; const [year, setYear] = useState(initialYear && years.includes(initialYear) ? initialYear : years[years.length - 1] ?? 0); const frame = useMemo(() => data.frames.find((f) => f.year === year) ?? data.frames[data.frames.length - 1], [data.frames, year]); const rows = useMemo(() => (frame ? [...frame.rows].sort((a, b) => a.rank - b.rank).slice(0, top) : []), [frame, top]); const max = rows.length ? Math.max(...rows.map((r) => Math.abs(r.value))) || 1 : 1; const n = Math.min(top, data.top); const h = height ?? n * ROW_H + 8; // Every country that ever appears keeps a DOM node so its bar can slide in/out. const ids = useMemo(() => Object.keys(data.countries), [data.countries]); const byId = useMemo(() => new Map(rows.map((r) => [r.id, r])), [rows]); const summary = t('chart.race.summary', { name: spec.name ?? '', y0: years[0] ?? '', y1: years[years.length - 1] ?? '', top: n }); const table: TableData = useMemo( () => ({ columns: [{ key: 'rank', label: t('common.rank'), numeric: true }, { key: 'country', label: t('common.country') }, { key: 'value', label: spec.name ?? t('common.value'), numeric: true }], rows: rows.map((r) => ({ rank: String(r.rank), country: data.countries[r.id]?.name ?? r.id, value: formatValue(r.value, spec) })) }), [rows, data.countries, spec], ); return (
{year} {ids.map((id) => { const r = byId.get(id); const c = data.countries[id]; const visible = !!r; const idx = r ? r.rank - 1 : n; return (
{r?.rank ?? ''}
{c?.flag} {c?.name ?? id}
{r ? formatValue(r.value, spec) : ''}
); })}
    {rows.map((r) => (
  • {r.rank}. {data.countries[r.id]?.name ?? r.id}: {formatValue(r.value, spec)} ({year})
  • ))}
{t('chart.race.openYear', { year })} →
); }