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%
16.5 KB · 299 lines typescript
Raw Blame History
1/**2 * Data stories — declarative templates. A story is a slug, a title, a standfirst and an ordered list of blocks3 * bound to indicators. Blocks are rendered by `components/stories/*` from live API payloads; `text` blocks are4 * template sentences whose variables are computed server-side from the SAME payloads (see5 * components/stories/resolve.ts). A sentence whose variable cannot be computed is omitted — never guessed.6 *7 * Adding a story = adding an entry to STORIES. No code elsewhere.8 */910export type VarSpec =11  /** Aggregate trend value (world or a group) at the first / last year or a given year. */12  | { type: 'trend'; indicator: string; group?: string; year: 'first' | 'last' | number; stat?: 'preferred' | 'median' | 'mean' | 'weighted_mean' | 'sum'; format?: 'value' | 'year' | 'n' }13  /** Number of countries whose value in `year` (latest when omitted) satisfies op threshold. */14  | { type: 'mapCount'; indicator: string; year?: number; op: 'gte' | 'lte' | 'gt' | 'lt'; threshold: number; format?: 'n' | 'total' | 'pct' | 'year' }15  /** Top-ranked country (name or value) for an indicator in a year (latest when omitted). */16  | { type: 'rankTop'; indicator: string; year?: number; pos?: number; sort?: 'asc' | 'desc'; format?: 'name' | 'value' | 'year' }17  /** One country's value at the first / last year of its series (or a given year). */18  | { type: 'country'; country: string; indicator: string; year: 'first' | 'last' | number; format?: 'value' | 'year' | 'name' }19  /** Share (%) of a group aggregate in the world aggregate (sum indicators) at a year. */20  | { type: 'share'; indicator: string; group: string; year: 'first' | 'last' | number; format?: 'pct' | 'year' };2122export interface TextParagraph {23  /** `{var}` placeholders resolved from `vars`. */24  template: string;25  vars: Record<string, VarSpec>;26}2728export type StoryBlock =29  | { kind: 'text'; paragraphs: TextParagraph[] }30  | { kind: 'map'; indicator: string; years: number[]; title?: string; note?: string }31  | { kind: 'trend'; indicator: string; groups?: string[]; title?: string; log?: boolean; from?: number }32  | { kind: 'lines'; indicator: string; countries: string[]; title?: string; from?: number; log?: boolean }33  | { kind: 'ranked'; indicator: string; year?: number; top: number; sort?: 'asc' | 'desc'; title?: string }34  | { kind: 'shares'; indicator: string; groups: string[]; title?: string; from?: number };3536export interface Story {37  slug: string;38  title: string;39  standfirst: string;40  /** ISO date of first publication (stories are regenerated from data on every request). */41  published: string;42  topics: string[];43  blocks: StoryBlock[];44}4546const WB_REGIONS = ['east-asia-pacific', 'europe-central-asia', 'north-america', 'latin-america-caribbean', 'middle-east-north-africa', 'south-asia', 'sub-saharan-africa'];4748export const STORIES: readonly Story[] = [49  {50    slug: 'the-world-is-getting-older',51    title: 'The world is getting older',52    standfirst: 'Median age has climbed on every continent, the share of people over 65 has more than doubled since 1960, and fertility has fallen below replacement in most rich countries.',53    published: '2026-09-11',54    topics: ['population'],55    blocks: [56      {57        kind: 'text',58        paragraphs: [59          {60            template: 'In {y0}, the population-weighted median age of the world was {v0}. By {y1} it had risen to {v1}.',61            vars: {62              y0: { type: 'trend', indicator: 'median-age', year: 'first', format: 'year' },63              v0: { type: 'trend', indicator: 'median-age', year: 'first' },64              y1: { type: 'trend', indicator: 'median-age', year: 'last', format: 'year' },65              v1: { type: 'trend', indicator: 'median-age', year: 'last' },66            },67          },68          {69            template: 'The oldest population in {y} is {name}, with a median age of {v}.',70            vars: {71              y: { type: 'rankTop', indicator: 'median-age', format: 'year' },72              name: { type: 'rankTop', indicator: 'median-age', format: 'name' },73              v: { type: 'rankTop', indicator: 'median-age', format: 'value' },74            },75          },76        ],77      },78      { kind: 'map', indicator: 'median-age', years: [1960, 1980, 2000, 2023], title: 'Median age, decade by decade' },79      {80        kind: 'text',81        paragraphs: [82          {83            template: 'People aged 65 and over were {v0} of the world population in {y0}; in {y1} they were {v1}.',84            vars: {85              v0: { type: 'trend', indicator: 'population-65-plus-share', year: 'first' },86              y0: { type: 'trend', indicator: 'population-65-plus-share', year: 'first', format: 'year' },87              v1: { type: 'trend', indicator: 'population-65-plus-share', year: 'last' },88              y1: { type: 'trend', indicator: 'population-65-plus-share', year: 'last', format: 'year' },89            },90          },91        ],92      },93      { kind: 'trend', indicator: 'population-65-plus-share', groups: ['world', 'high-income', 'low-income'], title: 'Share of population aged 65+' },94      { kind: 'lines', indicator: 'fertility-rate', countries: ['JPN', 'ITA', 'KOR', 'CHN', 'IND', 'NGA'], title: 'Fertility rate, births per woman' },95      { kind: 'ranked', indicator: 'median-age', top: 10, title: 'Oldest populations' },96    ],97  },98  {99    slug: 'rise-of-renewable-electricity',100    title: 'The rise of renewable electricity',101    standfirst: 'Renewables were a hydro story for decades. Since 2010 solar and wind have changed the shape of electricity systems on every continent.',102    published: '2026-09-11',103    topics: ['energy'],104    blocks: [105      {106        kind: 'text',107        paragraphs: [108          {109            template: 'Weighted by population, renewables supplied {v0} of electricity in {y0} and {v1} in {y1}.',110            vars: {111              v0: { type: 'trend', indicator: 'renewable-electricity-share', year: 'first' },112              y0: { type: 'trend', indicator: 'renewable-electricity-share', year: 'first', format: 'year' },113              v1: { type: 'trend', indicator: 'renewable-electricity-share', year: 'last' },114              y1: { type: 'trend', indicator: 'renewable-electricity-share', year: 'last', format: 'year' },115            },116          },117          {118            template: '{n} of {total} countries with data generated more than half of their electricity from renewables in {y}.',119            vars: {120              n: { type: 'mapCount', indicator: 'renewable-electricity-share', op: 'gte', threshold: 50, format: 'n' },121              total: { type: 'mapCount', indicator: 'renewable-electricity-share', op: 'gte', threshold: 50, format: 'total' },122              y: { type: 'mapCount', indicator: 'renewable-electricity-share', op: 'gte', threshold: 50, format: 'year' },123            },124          },125        ],126      },127      { kind: 'map', indicator: 'renewable-electricity-share', years: [1990, 2005, 2015, 2025], title: 'Share of electricity from renewables' },128      { kind: 'trend', indicator: 'renewable-electricity-share', groups: ['world', 'european-union', 'east-asia-pacific'], title: 'Renewable share of electricity' },129      { kind: 'lines', indicator: 'solar-generation', countries: ['CHN', 'USA', 'IND', 'DEU', 'JPN', 'BRA'], from: 2000, title: 'Solar electricity generation' },130      { kind: 'lines', indicator: 'wind-generation', countries: ['CHN', 'USA', 'DEU', 'IND', 'BRA', 'GBR'], from: 2000, title: 'Wind electricity generation' },131      { kind: 'ranked', indicator: 'renewable-electricity-share', top: 12, title: 'Highest renewable shares' },132    ],133  },134  {135    slug: 'internet-adoption-since-1990',136    title: 'Internet adoption since 1990',137    standfirst: 'From a laboratory network to the default way most people read, pay and talk: three decades of one of the fastest diffusions of a technology ever measured.',138    published: '2026-09-11',139    topics: ['digital'],140    blocks: [141      {142        kind: 'text',143        paragraphs: [144          {145            template: 'In {y0}, {v0} of the world population used the Internet. In {y1} the population-weighted share was {v1}.',146            vars: {147              y0: { type: 'trend', indicator: 'internet-users', year: 2000, format: 'year' },148              v0: { type: 'trend', indicator: 'internet-users', year: 2000 },149              y1: { type: 'trend', indicator: 'internet-users', year: 2023, format: 'year' },150              v1: { type: 'trend', indicator: 'internet-users', year: 2023 },151            },152          },153          {154            template: '{n} of {total} countries with data had at least nine users in ten in {y}.',155            vars: {156              n: { type: 'mapCount', indicator: 'internet-users', year: 2023, op: 'gte', threshold: 90, format: 'n' },157              total: { type: 'mapCount', indicator: 'internet-users', year: 2023, op: 'gte', threshold: 90, format: 'total' },158              y: { type: 'mapCount', indicator: 'internet-users', year: 2023, op: 'gte', threshold: 90, format: 'year' },159            },160          },161        ],162      },163      { kind: 'map', indicator: 'internet-users', years: [1995, 2005, 2015, 2023], title: 'Internet users, % of population' },164      { kind: 'trend', indicator: 'internet-users', groups: ['world', 'high-income', 'lower-middle-income', 'low-income'], title: 'Internet users by income group' },165      { kind: 'lines', indicator: 'internet-users', countries: ['KOR', 'USA', 'BRA', 'CHN', 'IND', 'NGA'], title: 'Six adoption curves' },166      { kind: 'ranked', indicator: 'internet-users', year: 2023, top: 10, title: 'Most connected countries' },167    ],168  },169  {170    slug: 'shifting-centre-of-the-world-economy',171    title: 'The shifting centre of the world economy',172    standfirst: 'Summing the GDP of every country by region shows the world economy tilting toward East Asia and the Pacific since 1990, while North America holds and Europe recedes in share.',173    published: '2026-09-11',174    topics: ['economy'],175    blocks: [176      {177        kind: 'text',178        paragraphs: [179          {180            template: 'East Asia & Pacific produced {s0} of the world total in {y0} and {s1} in {y1}.',181            vars: {182              s0: { type: 'share', indicator: 'gdp', group: 'east-asia-pacific', year: 1990 },183              y0: { type: 'share', indicator: 'gdp', group: 'east-asia-pacific', year: 1990, format: 'year' },184              s1: { type: 'share', indicator: 'gdp', group: 'east-asia-pacific', year: 'last' },185              y1: { type: 'share', indicator: 'gdp', group: 'east-asia-pacific', year: 'last', format: 'year' },186            },187          },188          {189            template: 'Europe & Central Asia went from {s0} to {s1} over the same period; North America from {n0} to {n1}.',190            vars: {191              s0: { type: 'share', indicator: 'gdp', group: 'europe-central-asia', year: 1990 },192              s1: { type: 'share', indicator: 'gdp', group: 'europe-central-asia', year: 'last' },193              n0: { type: 'share', indicator: 'gdp', group: 'north-america', year: 1990 },194              n1: { type: 'share', indicator: 'gdp', group: 'north-america', year: 'last' },195            },196          },197        ],198      },199      { kind: 'shares', indicator: 'gdp', groups: WB_REGIONS, from: 1970, title: 'Share of world GDP by World Bank region' },200      { kind: 'trend', indicator: 'gdp', groups: ['east-asia-pacific', 'north-america', 'europe-central-asia', 'south-asia'], from: 1970, log: true, title: 'GDP by region, current US$' },201      { kind: 'map', indicator: 'gdp-per-capita-ppp', years: [1990, 2005, 2025], title: 'GDP per capita (PPP)' },202      { kind: 'ranked', indicator: 'gdp', top: 12, title: 'Largest economies' },203    ],204  },205  {206    slug: 'global-fertility-collapse',207    title: 'Global fertility collapse',208    standfirst: 'In 1960 the average woman had about five children. Today most of humanity lives in countries below the replacement rate of 2.1.',209    published: '2026-09-11',210    topics: ['population'],211    blocks: [212      {213        kind: 'text',214        paragraphs: [215          {216            template: 'The population-weighted fertility rate fell from {v0} births per woman in {y0} to {v1} in {y1}.',217            vars: {218              v0: { type: 'trend', indicator: 'fertility-rate', year: 'first' },219              y0: { type: 'trend', indicator: 'fertility-rate', year: 'first', format: 'year' },220              v1: { type: 'trend', indicator: 'fertility-rate', year: 'last' },221              y1: { type: 'trend', indicator: 'fertility-rate', year: 'last', format: 'year' },222            },223          },224          {225            template: '{n0} countries were below the 2.1 replacement rate in {y0}; {n1} of {total} were in {y1}.',226            vars: {227              n0: { type: 'mapCount', indicator: 'fertility-rate', year: 1990, op: 'lt', threshold: 2.1, format: 'n' },228              y0: { type: 'mapCount', indicator: 'fertility-rate', year: 1990, op: 'lt', threshold: 2.1, format: 'year' },229              n1: { type: 'mapCount', indicator: 'fertility-rate', op: 'lt', threshold: 2.1, format: 'n' },230              total: { type: 'mapCount', indicator: 'fertility-rate', op: 'lt', threshold: 2.1, format: 'total' },231              y1: { type: 'mapCount', indicator: 'fertility-rate', op: 'lt', threshold: 2.1, format: 'year' },232            },233          },234        ],235      },236      { kind: 'map', indicator: 'fertility-rate', years: [1960, 1980, 2000, 2024], title: 'Births per woman' },237      { kind: 'trend', indicator: 'fertility-rate', groups: ['world', 'sub-saharan-africa', 'east-asia-pacific', 'europe-central-asia'], title: 'Fertility by region' },238      { kind: 'lines', indicator: 'fertility-rate', countries: ['KOR', 'CHN', 'IRN', 'BRA', 'BGD', 'NER'], title: 'Six trajectories' },239      { kind: 'ranked', indicator: 'fertility-rate', top: 10, sort: 'asc', title: 'Lowest fertility rates' },240    ],241  },242  {243    slug: 'fifty-years-of-life-expectancy',244    title: '50 years of life expectancy',245    standfirst: 'Half a century of gains on every continent — interrupted by AIDS, transitions and a pandemic, but rarely reversed for long.',246    published: '2026-09-11',247    topics: ['health'],248    blocks: [249      {250        kind: 'text',251        paragraphs: [252          {253            template: 'World life expectancy at birth, weighted by population, was {v0} in {y0} and {v1} in {y1}.',254            vars: {255              v0: { type: 'trend', indicator: 'life-expectancy', year: 1974 },256              y0: { type: 'trend', indicator: 'life-expectancy', year: 1974, format: 'year' },257              v1: { type: 'trend', indicator: 'life-expectancy', year: 'last' },258              y1: { type: 'trend', indicator: 'life-expectancy', year: 'last', format: 'year' },259            },260          },261          {262            template: '{n} of {total} countries with data reached 80 years or more in {y}; the highest is {name} at {v}.',263            vars: {264              n: { type: 'mapCount', indicator: 'life-expectancy', op: 'gte', threshold: 80, format: 'n' },265              total: { type: 'mapCount', indicator: 'life-expectancy', op: 'gte', threshold: 80, format: 'total' },266              y: { type: 'mapCount', indicator: 'life-expectancy', op: 'gte', threshold: 80, format: 'year' },267              name: { type: 'rankTop', indicator: 'life-expectancy', format: 'name' },268              v: { type: 'rankTop', indicator: 'life-expectancy', format: 'value' },269            },270          },271        ],272      },273      { kind: 'map', indicator: 'life-expectancy', years: [1974, 1990, 2005, 2024], title: 'Life expectancy at birth' },274      { kind: 'trend', indicator: 'life-expectancy', groups: ['world', 'sub-saharan-africa', 'south-asia', 'high-income'], from: 1974, title: 'Life expectancy by group' },275      { kind: 'lines', indicator: 'life-expectancy', countries: ['JPN', 'KOR', 'CHN', 'IND', 'NGA', 'USA'], from: 1974, title: 'Six countries, fifty years' },276      { kind: 'ranked', indicator: 'life-expectancy', top: 10, title: 'Longest lives' },277    ],278  },279];280281export function storyBySlug(slug: string): Story | undefined {282  return STORIES.find((s) => s.slug === slug);283}284285/** Distinct indicator slugs a story draws on (for metadata, JSON-LD and the "indicators used" list). */286export function storyIndicators(story: Story): string[] {287  const out: string[] = [];288  for (const b of story.blocks) {289    if (b.kind === 'text') {290      for (const p of b.paragraphs) for (const v of Object.values(p.vars)) if (!out.includes(v.indicator)) out.push(v.indicator);291    } else if (!out.includes(b.indicator)) out.push(b.indicator);292  }293  return out;294}295296export function storyChartCount(story: Story): number {297  return story.blocks.filter((b) => b.kind !== 'text').length;298}299