import Link from 'next/link'; import { t } from '@/i18n'; import { cn } from '@/lib/cn'; import { formatValue, isNum, ordinal } from '@/lib/format'; import { routes } from '@/lib/site'; import type { FormatSpec as Spec, Provenance } from '@/lib/types'; import { MARK } from './palette'; export interface RankedBarRow { id: string; label: string; flag?: string | null; href?: string | null; value: number | null; rank?: number | null; /** Secondary text shown right of the value (e.g. change); hidden on phones. */ hint?: string | null; /** Observation year — pass it only when it is older than the list's reference year (freshness honesty); always visible. */ year?: number | null; } /** * Horizontal ranked bars rendered as HTML (server component, fully responsive, no SVG text scaling). * One series → one colour (slot 1); `highlightId` marks the country of interest with the accent and bold label. * Bars ≤ 24 px thick, 4 px rounded data-end, square at the baseline; value labelled at the tip (text tokens). */ export function RankedBars({ rows, spec, highlightId, showRank = true, className, provenance, ariaLabel, linkRows = true, }: { rows: RankedBarRow[]; spec: Spec; highlightId?: string | null; showRank?: boolean; className?: string; provenance?: Provenance | null; ariaLabel?: string; linkRows?: boolean; }) { const values = rows.map((r) => r.value).filter(isNum); const max = values.length ? Math.max(...values.map(Math.abs)) : 0; const top = rows[0]; const summary = top ? t('chart.summary.bars', { top: top.label, value: formatValue(top.value, spec), n: rows.length }) : t('chart.noData'); if (rows.length === 0) return

{t('chart.noData')}

; return (
    {rows.map((r, i) => { const pct = isNum(r.value) && max > 0 ? Math.max(0, (Math.abs(r.value) / max) * 100) : 0; const hl = highlightId && r.id === highlightId; // Names wrap on phones (never clipped mid-word); they truncate only in the fixed 13 rem column on sm+. const label = ( {r.flag ? ( {r.flag} ) : null} {r.label} ); // Phones: [rank + name] [value] on line 1, bar on line 2. sm+: [rank + name] [bar] [value hint] on one line. return (
  1. {showRank ? {r.rank ?? i + 1} : null} {linkRows && r.href ? ( {label} ) : ( label )}
    {formatValue(r.value, spec)} {r.year != null ? {r.year} : null} {r.hint ? {r.hint} : null}
  2. ); })}
{provenance ? (

{t('common.source')}: {[provenance.source_name, provenance.dataset].filter(Boolean).join(' — ')} · {provenance.series_code}

) : null}
); } /** Convenience: a country row for RankedBars from a ranking row. */ export function rankedRowFromCountry(c: { id: string; slug: string | null; name: string | null; flag: string | null }, value: number | null, rank?: number | null, hint?: string | null, year?: number | null): RankedBarRow { return { id: c.id, label: c.name ?? c.id, flag: c.flag, href: c.slug ? routes.country(c.slug) : null, value, rank, hint, year }; } /** Year to display for a row (freshness honesty): only when ≥ 2 years older than the reference year. */ export function staleYear(year: number | null | undefined, reference: number | null | undefined): number | null { return year != null && reference != null && reference - year >= 2 ? year : null; } export { ordinal };