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%
3.9 KB · 83 lines typescript
Raw Blame History
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