spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
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