stats: /stats/history and History, Politics and Culture sections on NL, Gouda, Curaçao, US and KR pages (step 4)
All checks were successful
Deploy production / deploy (push) Successful in 1m13s

Long-run charts (Maddison, HYDE, UN via OWID; CBS and Pew religion), a filterable
sourced timeline, Wikidata office holders and turnout, Nager.Date holidays, UNESCO
sites, Wikidata people by sitelinks, curated food and cultural exports. Wikidata
labels now fall back to mul.
This commit is contained in:
2026-10-25 23:39:34 +00:00
parent e0bcd63ce3
commit 9b21220280
16 changed files with 1482 additions and 7 deletions

View File

@@ -22,6 +22,10 @@ const staticPaths = [
'/stats/countries/CUW',
'/stats/elections',
'/stats/opinions',
'/stats/history',
'/stats/countries/NLD',
'/stats/countries/USA',
'/stats/countries/KOR',
'/trackers/books',
'/trackers/spotting',
'/trackers/cars',

View File

@@ -9,6 +9,7 @@
import Globe from 'lucide-svelte/icons/globe';
import Vote from 'lucide-svelte/icons/vote';
import MessageCircle from 'lucide-svelte/icons/message-circle';
import ScrollText from 'lucide-svelte/icons/scroll-text';
let { children } = $props();
const icons = {
overview: ChartLine,
@@ -17,7 +18,8 @@
gouda: House,
countries: Globe,
elections: Vote,
opinions: MessageCircle
opinions: MessageCircle,
history: ScrollText
} as const;
const tabs = [
{ name: 'overview', href: '/stats' },
@@ -103,6 +105,9 @@
.tab-elections {
background: linear-gradient(135deg, #06b6d4, #3b82f6);
}
.tab-history {
background: linear-gradient(135deg, #b45309, #eab308);
}
.tab-opinions {
background: linear-gradient(135deg, #ec4899, #f97316);
}

View File

@@ -11,6 +11,7 @@
import Palmtree from 'lucide-svelte/icons/tree-palm';
import Vote from 'lucide-svelte/icons/vote';
import MessageCircle from 'lucide-svelte/icons/message-circle';
import ScrollText from 'lucide-svelte/icons/scroll-text';
import ArrowRight from 'lucide-svelte/icons/arrow-right';
let { data } = $props();
@@ -88,6 +89,13 @@
grad: 'linear-gradient(135deg,#06b6d4,#3b82f6)',
text: 'Every Dutch general election since 1918, Gouda down to the polling station, and the US presidential vote by state.'
},
{
href: '/stats/history',
title: 'History and culture',
icon: ScrollText,
grad: 'linear-gradient(135deg,#b45309,#eab308)',
text: 'The Netherlands, Gouda, Curaçao, the US and Korea over the long run, a sourced timeline, office holders, religion, holidays, UNESCO sites and famous people.'
},
{
href: '/stats/opinions',
title: 'Opinions',

View File

@@ -2,6 +2,8 @@ import { error, redirect } from '@sveltejs/kit';
import { cached, loadSeries } from '#lib/server/stats.js';
import { isCountryPage, PROFILE_SERIES } from '#lib/stats/countries.js';
import { CW_SERIES } from '#lib/stats/curacao.js';
import { isHeritage } from '#lib/stats/heritage.js';
import { loadHeritage } from '#lib/server/stats-history.js';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, url }) => {
@@ -9,6 +11,7 @@ export const load: PageServerLoad = async ({ params, url }) => {
if (code !== params.iso3 && isCountryPage(code))
redirect(308, `/stats/countries/${code}${url.search}`);
if (!isCountryPage(code)) error(404, 'Not Found');
const heritage = isHeritage(code) ? loadHeritage(code) : null;
const series = await cached(`stats:country:${code}`, () =>
loadSeries([
...PROFILE_SERIES.map((id) => `${id}:${code}`),
@@ -22,5 +25,5 @@ export const load: PageServerLoad = async ({ params, url }) => {
...(code === 'CUW' ? CW_SERIES : [])
])
);
return { code, series };
return { code, series, heritage: heritage ? await heritage : null };
};

View File

@@ -15,6 +15,7 @@
import { COUNTRY } from '#lib/stats/regions.js';
import { COUNTRY_COLOR, FLAG } from '#lib/stats/countries.js';
import ArrowLeft from 'lucide-svelte/icons/arrow-left';
import PlaceHeritage from '#lib/stats/PlaceHeritage.svelte';
let { data } = $props();
const c = $derived(data.code);
@@ -713,6 +714,10 @@
</div>
{/if}
{#if data.heritage}
<PlaceHeritage h={data.heritage} attributions={a} />
{/if}
<style>
.back {
display: inline-flex;

View File

@@ -1,5 +1,6 @@
import { cached, loadCbsCross, loadSeries } from '#lib/server/stats.js';
import { epSeriesIds, GOUDA_PC4 } from '#lib/stats/energylabels.js';
import { loadHeritage } from '#lib/server/stats-history.js';
import type { PageServerLoad } from './$types';
// Gouda (GM0513) and its neighbours; NL01 is the national total.
@@ -20,7 +21,7 @@ const KWB_MEASURES = [
];
export const load: PageServerLoad = async () => {
const [series, wijken] = await Promise.all([
const [series, wijken, heritage] = await Promise.all([
cached('stats:gouda', () =>
loadSeries([
...['NL01', ...RING].flatMap((r) => [
@@ -40,7 +41,8 @@ export const load: PageServerLoad = async () => {
),
cached('stats:gouda:kwb', () =>
loadCbsCross(KWB_TABLE, KWB_MEASURES, 'WijkenEnBuurten', ['WK0513', 'GM0513', 'NL00'])
)
),
loadHeritage('GM0513')
]);
return { series, wijken, kwbYear: 2024 };
return { series, wijken, kwbYear: 2024, heritage };
};

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import StatChart from '#lib/stats/StatChart.svelte';
import PlaceHeritage from '#lib/stats/PlaceHeritage.svelte';
import Kpi from '#lib/stats/Kpi.svelte';
import { barRankOption, change, latest, lineOption, type StatPoint } from '#lib/stats/chart.js';
import { compact, num, period } from '#lib/stats/format.js';
@@ -415,6 +416,8 @@
<p class="empty">Neighbourhood figures are not loaded yet.</p>
{/if}
<PlaceHeritage h={data.heritage} attributions={a} />
<style>
tr.total td {
font-weight: 600;

View File

@@ -0,0 +1,4 @@
import { loadHistoryOverview } from '#lib/server/stats-history.js';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => loadHistoryOverview();

View File

@@ -0,0 +1,203 @@
<script lang="ts">
import { resolve } from '$app/paths';
import StatChart from '#lib/stats/StatChart.svelte';
import EventTimeline from '#lib/stats/EventTimeline.svelte';
import { barRankOption, lineOption, type Line, type StatPoint } from '#lib/stats/chart.js';
import { HERITAGE, indexTo } from '#lib/stats/heritage.js';
import ArrowRight from 'lucide-svelte/icons/arrow-right';
let { data } = $props();
const s = $derived(data.series);
const a = $derived(data.attributions);
const pts = (id: string): StatPoint[] => s[id]?.points ?? [];
const COUNTRIES = ['NLD', 'USA', 'KOR'] as const;
const WORLD = { name: 'World', color: '#94a3b8', dashed: true };
const lines = (prefix: string, withCw = false, cwId = ''): Line[] => [
...COUNTRIES.map((c) => ({
name: HERITAGE[c].name,
points: pts(`${prefix}:${c}`),
color: HERITAGE[c].color
})),
...(withCw
? [{ name: 'Curaçao', points: pts(cwId || `${prefix}:CUW`), color: HERITAGE.CUW.color }]
: []),
{ ...WORLD, points: pts(`${prefix}:WLD`) }
];
const popIndex = $derived([
...COUNTRIES.map((c) => ({
name: HERITAGE[c].name,
points: indexTo(pts(`owid:hist:population:${c}`), '1950'),
color: HERITAGE[c].color
})),
{
name: 'Curaçao',
points: indexTo(pts('owid:cw:population'), '1950'),
color: HERITAGE.CUW.color
},
{
name: 'Gouda',
points: indexTo(pts('cbs:pop:jan1:GM0513'), '1950'),
color: HERITAGE.GM0513.color
},
{ ...WORLD, points: indexTo(pts('owid:hist:population:WLD'), '1950') }
]);
const unaffiliated = $derived(
(['NLD', 'USA', 'KOR', 'CUW', 'WLD'] as const)
.map((c) => ({
label: c === 'WLD' ? 'World' : HERITAGE[c].name,
value:
pts(`pew:religion:religiously_unaffiliated:${c}`).find(([d]) =>
d.startsWith('2020')
)?.[1] ?? NaN,
highlight: c === 'NLD'
}))
.filter((r) => Number.isFinite(r.value))
);
const places = Object.values(HERITAGE);
</script>
<svelte:head>
<title>History · stats · Stan Runge</title>
<meta
name="description"
content="The Netherlands, Gouda, Curaçao, the US and South Korea over the long run: population, income, life expectancy, urbanisation and religion, plus a sourced timeline of key events."
/>
</svelte:head>
<section class="stats-intro">
<span class="stats-eyebrow">stats / history</span>
<h1>History and culture</h1>
<p>
Five places over the long run: the Netherlands, Gouda, Curaçao, the United States and South
Korea. Long series come from the Maddison Project, HYDE and the UN via Our World in Data, CBS
and Pew; the timeline is curated, and every event links to its source. Each place's own page has
its office holders, holidays, UNESCO sites, well-known people and food.
</p>
</section>
<div class="places">
{#each places as p (p.code)}
<a class="place" href={`${resolve(p.href as '/stats')}#history`} style:--c={p.color}>
<strong>{p.name}</strong>
<span>history, politics and culture <ArrowRight size={13} /></span>
</a>
{/each}
</div>
<h2 class="stats-section-title">The long run</h2>
<div class="stats-grid">
<StatChart
title="GDP per person, long run"
subtitle="Maddison Project Database 2023 · international $ at 2011 prices"
sources={['owid']}
attributions={a}
note="Curaçao is not in the Maddison database."
option={(t) => lineOption(t, lines('owid:maddison'), { unit: '$', decimals: 0, zoom: true })}
/>
<StatChart
title="Life expectancy, long run"
subtitle="years at birth · Riley, Zijdeman et al., HMD and UN WPP via Our World in Data"
sources={['owid']}
attributions={a}
option={(t) =>
lineOption(t, lines('owid:hist:life-expectancy', true, 'owid:cw:life-expectancy'), {
unit: 'years',
decimals: 1
})}
/>
<StatChart
title="Urbanisation since 1500"
subtitle="% living in urban areas · HYDE 2023 via Our World in Data"
sources={['owid']}
attributions={a}
option={(t) => lineOption(t, lines('owid:hist:urban-long'), { unit: '%', decimals: 1 })}
/>
<StatChart
title="Population growth since 1950"
subtitle="index, 1950 = 100 · OWID (UN WPP), CBS for Gouda"
sources={['owid', 'cbs']}
attributions={a}
option={(t) => lineOption(t, popIndex, { decimals: 0 })}
/>
<StatChart
title="Population since 1500"
subtitle="inhabitants · HYDE, Gapminder and UN via Our World in Data"
sources={['owid']}
attributions={a}
option={(t) =>
lineOption(
t,
COUNTRIES.map((c) => ({
name: HERITAGE[c].name,
points: pts(`owid:hist:population:${c}`),
color: HERITAGE[c].color
})),
{ decimals: 0 }
)}
/>
<StatChart
title="No religion, 2020"
subtitle="% of the population religiously unaffiliated · Pew Research Center"
sources={['pew']}
attributions={a}
height="240px"
option={(t) => barRankOption(t, unaffiliated, { unit: '%', decimals: 1, color: '#64748b' })}
/>
<StatChart
title="The Netherlands loses its religion"
subtitle="% of adults without religious affiliation, and Catholics · CBS since 1849"
sources={['cbs']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name: 'No religion', points: pts('cbs:religion:37944:none:NL01'), color: '#64748b' },
{
name: 'Roman Catholic',
points: pts('cbs:religion:37944:catholic:NL01'),
color: '#e11d48'
},
{
name: 'Dutch Reformed',
points: pts('cbs:religion:37944:hervormd:NL01'),
color: '#f97316'
}
],
{ unit: '%', decimals: 1 }
)}
/>
</div>
<h2 class="stats-section-title">Timeline</h2>
<EventTimeline events={data.events} showPlaces />
<p class="stats-note">
{data.events.length} curated events, each summarised from and linked to its source (mostly Wikipedia,
CC BY-SA). Click an event for the summary and source; filter by theme or place.
</p>
<style>
.places {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 190px), 1fr));
gap: 0.75rem;
}
.place {
border: 1px solid var(--site-border);
border-left: 4px solid var(--c);
border-radius: 0.75rem;
padding: 0.7rem 0.9rem;
display: flex;
flex-direction: column;
gap: 0.15rem;
text-decoration: none;
color: inherit;
}
.place span {
color: var(--site-muted);
font-size: 0.78rem;
display: inline-flex;
align-items: center;
gap: 0.2rem;
}
</style>