spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1/**2 * URL contract of `/rankings/[indicator]`:3 * ?year=2020 ranking year (default: latest; the API falls back to the nearest available year)4 * &group=oecd group slug (world · regions · continents · income groups · organisations); default world5 * &income=high-income income-group slug applied client-side on top of the group (hic/umc/lmc/lic slugs)6 * &minpop=1000000 minimum population (client-side; 0/absent = none)7 * &mincov=2 minimum coverage: rows whose year ≥ ranking year − N (client-side)8 * &view=table|bars|map presentation (default table)9 * &sort=asc|desc default = the API's ("asc" when lower is better, else "desc")10 * &highlight=canada country slug pinned at the top of the list11 * &q=fra text filter within the ranking12 * &history=CAN,USA,FRA countries of the rank-over-time panel (≤ 5 ISO3)13 * Canonical URL = path + `group` (when not world); everything else is volatile.14 */15export type RankingView = 'table' | 'bars' | 'map';16export const RANKING_VIEWS: readonly RankingView[] = ['table', 'bars', 'map'];17export const MIN_POP_OPTIONS = [0, 1_000_000, 5_000_000, 10_000_000, 50_000_000] as const;18export const MIN_COV_OPTIONS = [0, 1, 2, 5] as const;1920export interface RankingState {21 year: number | null;22 group: string;23 income: string | null;24 minpop: number | null;25 mincov: number | null;26 view: RankingView;27 sort: 'asc' | 'desc' | null;28 highlight: string | null;29 q: string;30 history: string[];31}3233type ParamsLike = { get(name: string): string | null } | Record<string, string | string[] | undefined>;34function read(params: ParamsLike, key: string): string | null {35 if (typeof (params as { get?: unknown }).get === 'function') return (params as { get(name: string): string | null }).get(key);36 const v = (params as Record<string, string | string[] | undefined>)[key];37 return Array.isArray(v) ? (v[0] ?? null) : (v ?? null);38}3940export function parseRankingState(params: ParamsLike): RankingState {41 const y = Number(read(params, 'year'));42 const sort = read(params, 'sort');43 const group = (read(params, 'group') ?? 'world').toLowerCase().replace(/[^a-z0-9-]/g, '') || 'world';44 const income = (read(params, 'income') ?? '').toLowerCase().replace(/[^a-z0-9-]/g, '') || null;45 const minpop = Number(read(params, 'minpop'));46 const mincov = Number(read(params, 'mincov'));47 const viewRaw = read(params, 'view');48 const highlight = (read(params, 'highlight') ?? '').toLowerCase().replace(/[^a-z0-9-]/g, '') || null;49 const history = (read(params, 'history') ?? '')50 .split(',')51 .map((s) => s.trim().toUpperCase())52 .filter((s) => /^[A-Z]{3}$/.test(s))53 .slice(0, 5);54 return {55 year: Number.isInteger(y) && y >= 1800 && y <= 2100 ? y : null,56 group,57 income,58 minpop: Number.isFinite(minpop) && minpop > 0 ? minpop : null,59 mincov: Number.isFinite(mincov) && mincov > 0 ? mincov : null,60 view: (RANKING_VIEWS as readonly string[]).includes(viewRaw ?? '') ? (viewRaw as RankingView) : 'table',61 sort: sort === 'asc' || sort === 'desc' ? sort : null,62 highlight,63 q: (read(params, 'q') ?? '').slice(0, 60),64 history,65 };66}6768export function rankingQuery(state: Partial<RankingState>): string {69 const p = new URLSearchParams();70 if (state.year != null) p.set('year', String(state.year));71 if (state.group && state.group !== 'world') p.set('group', state.group);72 if (state.income) p.set('income', state.income);73 if (state.minpop) p.set('minpop', String(state.minpop));74 if (state.mincov) p.set('mincov', String(state.mincov));75 if (state.view && state.view !== 'table') p.set('view', state.view);76 if (state.sort) p.set('sort', state.sort);77 if (state.highlight) p.set('highlight', state.highlight);78 if (state.q) p.set('q', state.q);79 if (state.history?.length) p.set('history', state.history.join(','));80 const s = p.toString();81 return s ? `?${s}` : '';82}83