spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1'use client';2import Link from 'next/link';3import { useMemo } from 'react';4import { t } from '@/i18n';5import { formatValue, grouped } from '@/lib/format';6import { routes } from '@/lib/site';7import type { FormatSpec } from '@/lib/types';8import type { PointCountry } from '@/lib/types-analytics';9import { Histogram } from '@/components/charts/histogram';10import { LineChart, type LineSeries } from '@/components/charts/line-chart';11import { RankedBars, type RankedBarRow } from '@/components/charts/ranked-bars';12import { histogramOf, median, wantsLog } from './geo';1314export interface YearRow {15 country: PointCountry;16 value: number;17 rank: number;18}1920/** Rank view: the top N of the current year, plus the selected country pinned when outside the top. */21export function RankView({ rows, year, spec, selectedId, indicatorSlug, top = 25 }: { rows: YearRow[]; year: number; spec: FormatSpec; selectedId: string | null; indicatorSlug: string; top?: number }) {22 const shown = rows.slice(0, top);23 const sel = selectedId ? rows.find((r) => r.country.id === selectedId) : null;24 if (sel && !shown.some((r) => r.country.id === sel.country.id)) shown.push(sel);25 const bars: RankedBarRow[] = shown.map((r) => ({ id: r.country.id, label: r.country.name ?? r.country.id, flag: r.country.flag, href: r.country.slug ? routes.country(r.country.slug) : null, value: r.value, rank: r.rank }));26 if (!rows.length) return <p className="py-8 text-center text-sm text-ink-3">{t('explorer.noDataYear', { indicator: spec.name ?? '', year })}</p>;27 return (28 <div className="mx-auto max-w-3xl px-4 py-4">29 <div className="mb-2 flex flex-wrap items-baseline justify-between gap-2 text-xs text-ink-3">30 <span>{t('explorer.rank.sub', { n: grouped(rows.length), year })}</span>31 <Link href={routes.ranking(indicatorSlug, { year })} className="text-accent hover:underline">32 {t('explorer.rank.full')} →33 </Link>34 </div>35 <RankedBars rows={bars} spec={spec} highlightId={selectedId} />36 </div>37 );38}3940/** Trend view: world median (and the country count), plus the selected country's own series. */41export function TrendView({ years, values, spec, selected, subject }: { years: number[]; values: Record<string, Array<number | null>>; spec: FormatSpec; selected: { country: PointCountry; series: Array<number | null> } | null; subject: string }) {42 const series: LineSeries[] = useMemo(() => {43 const med = years.map((y, i) => ({ period: `${y}-01-01`, year: y, value: median(Object.values(values).map((arr) => arr[i])) }));44 const out: LineSeries[] = [{ id: 'median', name: t('explorer.trend.median'), points: med.filter((p) => p.value != null), colorIndex: 0 }];45 if (selected) out.push({ id: selected.country.id, name: selected.country.name ?? selected.country.id, points: years.map((y, i) => ({ period: `${y}-01-01`, year: y, value: selected.series[i] ?? null })).filter((p) => p.value != null), colorIndex: 1 });46 return out;47 }, [years, values, selected]);48 const counts = years.map((_, i) => Object.values(values).filter((arr) => arr[i] != null).length);49 return (50 <div className="mx-auto max-w-4xl px-4 py-4">51 <LineChart series={series} spec={spec} subject={subject} height={320} title={t('explorer.trend.title', { name: spec.name ?? '' })} subtitle={t('explorer.trend.sub', { min: Math.min(...counts), max: Math.max(...counts) })} defaultWidth={860} endLabels />52 <p className="mt-2 text-2xs text-ink-3">{t('explorer.trend.note')}</p>53 </div>54 );55}5657/** Distribution view: histogram of the current year with world median and selected country markers. */58export function DistributionView({ rows, year, spec, selected }: { rows: YearRow[]; year: number; spec: FormatSpec; selected: { country: PointCountry; value: number | null } | null }) {59 const vals = rows.map((r) => r.value);60 const log = wantsLog(spec, vals);61 const h = useMemo(() => histogramOf(vals, 20, log), [vals, log]);62 const med = median(vals);63 const markers = [64 ...(med != null ? [{ id: 'median', label: t('explorer.trend.median'), value: med, tone: 'ink' as const }] : []),65 ...(selected && selected.value != null ? [{ id: selected.country.id, label: selected.country.name ?? selected.country.id, value: selected.value, tone: 'accent' as const }] : []),66 ];67 if (!rows.length) return <p className="py-8 text-center text-sm text-ink-3">{t('explorer.noDataYear', { indicator: spec.name ?? '', year })}</p>;68 return (69 <div className="mx-auto max-w-4xl px-4 py-4">70 <Histogram edges={h.edges} counts={h.counts} log={h.log} markers={markers} spec={spec} height={300} title={t('explorer.dist.title', { name: spec.name ?? '', year })} subtitle={t('explorer.dist.sub', { n: grouped(rows.length), median: formatValue(med, spec) })} defaultWidth={860} unitLabel={spec.unit ?? undefined} />71 </div>72 );73}74