'use client'; import { useEffect, useMemo, useState } from 'react'; import { t } from '@/i18n'; import { clientExplore } from '@/lib/client-api-explore'; import { cn } from '@/lib/cn'; import type { FormatSpec } from '@/lib/types'; import type { TrendResponse } from '@/lib/types-explore'; import { LineChart, type LineSeries } from '@/components/charts/line-chart'; import type { SeriesPoint } from '@/components/charts/scales'; import { EmptyState } from '@/components/data/empty-state'; import type { ProvenancePayload } from '@/components/data/provenance-context'; export interface TrendGroupOption { id: string; label: string; } /** Fixed group choices: World, organisations, World Bank regions, income groups (ids = group slugs). */ export const TREND_GROUPS: TrendGroupOption[] = [ { id: 'world', label: 'World' }, { id: 'oecd', label: 'OECD' }, { id: 'european-union', label: 'European Union' }, { id: 'g7', label: 'G7' }, { id: 'g20', label: 'G20' }, { id: 'brics', label: 'BRICS' }, { id: 'north-america', label: 'North America' }, { id: 'latin-america-caribbean', label: 'Latin America & Caribbean' }, { id: 'europe-central-asia', label: 'Europe & Central Asia' }, { id: 'middle-east-north-africa', label: 'Middle East & North Africa' }, { id: 'south-asia', label: 'South Asia' }, { id: 'east-asia-pacific', label: 'East Asia & Pacific' }, { id: 'sub-saharan-africa', label: 'Sub-Saharan Africa' }, { id: 'high-income', label: 'High income' }, { id: 'upper-middle-income', label: 'Upper middle income' }, { id: 'lower-middle-income', label: 'Lower middle income' }, { id: 'low-income', label: 'Low income' }, ]; function pts(points: TrendResponse['points'], key: 'median' | 'mean' | 'weighted_mean' | 'sum'): SeriesPoint[] { return points.filter((p) => p[key] != null).map((p) => ({ period: `${p.year}-01-01`, year: p.year, value: p[key] })); } /** * Aggregate trend for a group: the API's `preferred` aggregate (sum / population-weighted mean / median) as * the first series, the median as a second series when it differs. Group selector fetches other groups * client-side (`/indicators/{slug}/trend?group=`) and caches them. */ export function IndicatorTrend({ slug, initial, spec, payload }: { slug: string; initial: TrendResponse | null; spec: FormatSpec; payload: ProvenancePayload | null }) { const [group, setGroup] = useState('world'); const [cache, setCache] = useState>({ world: initial }); const [loading, setLoading] = useState(false); useEffect(() => { if (cache[group] !== undefined) return; const ctrl = new AbortController(); setLoading(true); clientExplore .indicatorTrend(slug, group, ctrl.signal) .then((r) => setCache((c) => ({ ...c, [group]: r }))) .catch(() => setCache((c) => ({ ...c, [group]: null }))) .finally(() => { if (!ctrl.signal.aborted) setLoading(false); }); return () => ctrl.abort(); }, [group, slug, cache]); const data = cache[group]; const series: LineSeries[] = useMemo(() => { if (!data) return []; const pref = (data.preferred as 'sum' | 'weighted_mean' | 'median') ?? 'median'; const out: LineSeries[] = []; const prefPts = pts(data.points, pref); if (prefPts.length) out.push({ id: pref, name: t(`indicator.trend.${pref}` as 'indicator.trend.median'), points: prefPts, colorIndex: 0 }); if (pref !== 'median' && pref !== 'sum') { const med = pts(data.points, 'median'); if (med.length) out.push({ id: 'median', name: t('indicator.trend.median'), points: med, colorIndex: 1 }); } return out; }, [data]); const ns = data?.points.map((p) => p.n) ?? []; const groupLabel = TREND_GROUPS.find((g) => g.id === group)?.label ?? data?.group.name ?? group; return (
    {TREND_GROUPS.slice(0, 6).map((g) => (
  • ))}
{ns.length ? {t('indicator.trend.n', { min: Math.min(...ns), max: Math.max(...ns) })} : null}
{data === null || (data && series.length === 0) ? ( ) : data ? ( ) : (
{t('common.loading')}
)}
); }