SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
4.8 KB · 74 lines tsx
Raw Blame History
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