[t, ...(t.children ?? [])])
.map((t) => ({ name: t.name, href: t.href, kind: 'tracker' })),
- ...extraHits.map((t) => ({ ...t, kind: 'tracker' }))
+ ...extraHits.map((t) => ({ ...t, kind: 'tracker' })),
+ ...statsHits.map((t) => ({ ...t, kind: 'stats' }))
]);
const hits = $derived.by(() => {
const q = query.trim().toLowerCase();
@@ -559,6 +566,7 @@
>{servicesDown.length ? `${servicesDown.length} down` : 'all up'}
{/if}
+ stats
{#if data.weight}{data.weight.kg} kg {/if}
diff --git a/src/routes/stats/+layout.server.ts b/src/routes/stats/+layout.server.ts
new file mode 100644
index 0000000..3f2228e
--- /dev/null
+++ b/src/routes/stats/+layout.server.ts
@@ -0,0 +1,11 @@
+import { cached, loadSources } from '#lib/server/stats.js';
+import type { LayoutServerLoad } from './$types';
+
+export const load: LayoutServerLoad = async ({ setHeaders }) => {
+ setHeaders({ 'cache-control': 'public, max-age=300' });
+ const sources = await cached('stats:sources', loadSources);
+ return {
+ sources,
+ attributions: Object.fromEntries(Object.values(sources).map((s) => [s.id, s.attribution]))
+ };
+};
diff --git a/src/routes/stats/+layout.svelte b/src/routes/stats/+layout.svelte
new file mode 100644
index 0000000..44e431a
--- /dev/null
+++ b/src/routes/stats/+layout.svelte
@@ -0,0 +1,217 @@
+
+
+
+
+ {#each tabs as tab (tab.href)}
+ {@const Icon = icons[tab.name as keyof typeof icons] ?? ChartLine}
+ {tab.name}
+ {/each}
+
+ {@render children()}
+
+
+
diff --git a/src/routes/stats/+page.server.ts b/src/routes/stats/+page.server.ts
new file mode 100644
index 0000000..e2503d8
--- /dev/null
+++ b/src/routes/stats/+page.server.ts
@@ -0,0 +1,29 @@
+import { cached, loadFreshness, loadSeries } from '#lib/server/stats.js';
+import type { PageServerLoad } from './$types';
+
+export const load: PageServerLoad = async () => {
+ const [series, freshness] = await Promise.all([
+ cached('stats:overview', () =>
+ loadSeries([
+ { id: 'fred:DCOILBRENTEU', from: '2015-01-01', resample: 'W' },
+ { id: 'fred:PNGASEUUSDM', from: '2015-01-01' },
+ { id: 'bis:D:US', from: '2015-01-01', resample: 'W' },
+ { id: 'bis:D:XM', from: '2015-01-01', resample: 'W' },
+ { id: 'bis:D:KR', from: '2015-01-01', resample: 'W' },
+ { id: 'eurostat:prc_hicp_minr:RCH_A.TOTAL.NL', from: '2015-01-01' },
+ { id: 'eurostat:prc_hicp_minr:RCH_A.TOTAL.EA20', from: '2015-01-01' },
+ { id: 'cbs:cpi:yoy', from: '2015-01-01' },
+ { id: 'eurostat:irt_lt_mcby_m:MCBY.FR', from: '2015-01-01' },
+ { id: 'eurostat:irt_lt_mcby_m:MCBY.DE', from: '2015-01-01' },
+ { id: 'cbs:tariff:elec:variable', from: '2015-01-01' },
+ { id: 'cbs:tariff:elec:tax', from: '2015-01-01' },
+ 'cbs:renew:total:share',
+ { id: 'cbs:pop:monthly:GM0513', from: '2015-01-01' },
+ 'cbs:house:avgprice:GM0513',
+ 'cbs:house:avgprice:NL01'
+ ])
+ ),
+ cached('stats:freshness', loadFreshness)
+ ]);
+ return { series, freshness };
+};
diff --git a/src/routes/stats/+page.svelte b/src/routes/stats/+page.svelte
new file mode 100644
index 0000000..4072529
--- /dev/null
+++ b/src/routes/stats/+page.svelte
@@ -0,0 +1,323 @@
+
+
+
+ stats · Stan Runge
+
+
+
+
+ stats
+ Statistics
+
+ Numbers I like to keep an eye on, pulled straight from official open data (CBS, Eurostat, FRED,
+ the BIS) and refreshed on a schedule. Every chart names its source.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ lineOption(
+ t,
+ [
+ { name: 'Fed', points: pts('bis:D:US'), color: '#3b82f6', kind: 'step' },
+ { name: 'ECB', points: pts('bis:D:XM'), color: '#eab308', kind: 'step' },
+ { name: 'Bank of Korea', points: pts('bis:D:KR'), color: '#e11d48', kind: 'step' }
+ ],
+ { unit: '%', decimals: 2 }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Netherlands',
+ points: pts('eurostat:prc_hicp_minr:RCH_A.TOTAL.NL'),
+ color: '#f97316'
+ },
+ {
+ name: 'Euro area',
+ points: pts('eurostat:prc_hicp_minr:RCH_A.TOTAL.EA20'),
+ color: '#3b82f6'
+ }
+ ],
+ { unit: '%' }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ { name: 'Brent', points: pts('fred:DCOILBRENTEU'), color: '#64748b' },
+ { name: 'EU gas (TTF)', points: pts('fred:PNGASEUUSDM'), color: '#3b82f6', right: true }
+ ],
+ { unit: '$/bbl', rightUnit: '$/MMBtu' }
+ )}
+ />
+
+ lineOption(
+ t,
+ [{ name: 'Gouda', points: pts('cbs:pop:monthly:GM0513'), color: '#f97316', kind: 'area' }],
+ { decimals: 0, legend: false, min: 'dataMin' }
+ )}
+ />
+
+
+Sources and freshness
+
+
+
+ Source Series Observations Latest data Last refresh
+
+
+ {#each data.freshness.filter((f) => f.series > 0) as f (f.sourceId)}
+
+ {f.name}
+ {num(f.series, 0)}
+ {num(f.observations, 0)}
+ {period(f.latestPeriod, 'D')}
+
+ {ago(f.lastOk)}
+
+ {/each}
+
+
+
+
+ Data is fetched by scheduled jobs with a polite rate limit and stored as-is; charts are rendered
+ from the stored copy. Licences: CBS and Eurostat CC BY 4.0, FRED and BIS per their terms of use.
+
+
+
diff --git a/src/routes/stats/economy/+page.server.ts b/src/routes/stats/economy/+page.server.ts
new file mode 100644
index 0000000..c229122
--- /dev/null
+++ b/src/routes/stats/economy/+page.server.ts
@@ -0,0 +1,40 @@
+import { cached, loadSeries } from '#lib/server/stats.js';
+import type { PageServerLoad } from './$types';
+
+const EU = ['NL', 'DE', 'FR', 'BE', 'IT', 'ES', 'PL', 'SE', 'DK', 'AT', 'EU27_2020', 'EA20'];
+const YIELDS = ['NL', 'DE', 'FR', 'IT', 'ES', 'BE', 'AT', 'EA'];
+
+export const load: PageServerLoad = async () => ({
+ series: await cached('stats:economy', () =>
+ loadSeries([
+ ...['US', 'XM', 'KR', 'CN', 'GB', 'JP'].map((a) => ({
+ id: `bis:M:${a}`,
+ from: '1990-01-01'
+ })),
+ ...EU.map((g) => `eurostat:prc_hicp_minr:RCH_A.TOTAL.${g}`),
+ { id: 'fred:CPIAUCSL', from: '1989-01-01' },
+ ...YIELDS.map((g) => `eurostat:irt_lt_mcby_m:MCBY.${g}`),
+ { id: 'fred:DGS10', from: '1990-01-01', resample: 'M' },
+ { id: 'fred:DGS2', from: '1990-01-01', resample: 'M' },
+ 'fred:IRLTLT01KRM156N',
+ ...['NL', 'KR', 'US', 'CN'].map((c) => ({
+ id: `fred:SPASTT01${c}M661N`,
+ from: '1990-01-01'
+ })),
+ { id: 'fred:SP500', resample: 'W' },
+ { id: 'fred:NASDAQCOM', from: '1990-01-01', resample: 'W' },
+ ...EU.map((g) => `eurostat:nama_10_gdp:CLV_PCH_PRE.B1GQ.${g}`),
+ ...EU.map((g) => `eurostat:nama_10_gdp:CP_MEUR.B1GQ.${g}`),
+ 'fred:GDPC1',
+ 'fred:UNRATE',
+ ...EU.flatMap((g) => [
+ `eurostat:gov_10dd_edpt1:PC_GDP.S13.GD.${g}`,
+ `eurostat:gov_10dd_edpt1:PC_GDP.S13.B9.${g}`,
+ `eurostat:gov_10a_main:PC_GDP.S13.TE.${g}`,
+ `eurostat:gov_10a_main:PC_GDP.S13.TR.${g}`,
+ `eurostat:migr_imm1ctz:TOTAL.COMPLET.TOTAL.NR.T.${g}`,
+ `eurostat:demo_pjan:NR.TOTAL.T.${g}`
+ ])
+ ])
+ )
+});
diff --git a/src/routes/stats/economy/+page.svelte b/src/routes/stats/economy/+page.svelte
new file mode 100644
index 0000000..522ef05
--- /dev/null
+++ b/src/routes/stats/economy/+page.svelte
@@ -0,0 +1,406 @@
+
+
+
+ economy · stats · Stan Runge
+
+
+
+
+ stats / economy
+ Economy
+
+ Central bank rates, inflation, 10-year bond yields and spreads, stock markets, growth, public
+ finances and migration. The Netherlands next to Europe, the US and South Korea, refreshed daily
+ from open sources.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Rates and inflation
+
+
+ lineOption(
+ t,
+ RATES.map(([c, name, color]) => ({ name, color, kind: 'step', points: pts(`bis:M:${c}`) })),
+ { unit: '%', zoom: true }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ { name: 'Netherlands', points: hicp('NL'), color: '#f97316' },
+ { name: 'Euro area', points: hicp('EA20'), color: '#10b981' },
+ { name: 'Germany', points: hicp('DE'), color: '#64748b' },
+ { name: 'United States', points: usCpi, color: '#3b82f6' }
+ ],
+ { unit: '%', zoom: true }
+ )}
+ />
+
+
+Bond markets
+
+
+ lineOption(
+ t,
+ [
+ { name: 'Netherlands', points: y('NL'), color: '#f97316' },
+ { name: 'Germany', points: y('DE'), color: '#64748b' },
+ { name: 'France', points: y('FR'), color: '#3b82f6' },
+ { name: 'Italy', points: y('IT'), color: '#10b981' },
+ { name: 'Spain', points: y('ES'), color: '#eab308' },
+ { name: 'United States', points: us10, color: '#a855f7' },
+ { name: 'South Korea', points: pts('fred:IRLTLT01KRM156N'), color: '#e11d48' }
+ ],
+ { unit: '%', zoom: true }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ { name: 'France − Germany', points: frde, color: '#3b82f6', kind: 'area' },
+ { name: 'Italy − Germany', points: itde, color: '#10b981' },
+ { name: 'Netherlands − Germany', points: nlde, color: '#f97316' }
+ ],
+ { unit: 'pp', zoom: true }
+ )}
+ />
+
+ lineOption(t, [{ name: '10y − 2y', points: curve, color: '#a855f7', kind: 'area' }], {
+ unit: 'pp',
+ zoom: true,
+ legend: false
+ })}
+ />
+
+ lineOption(
+ t,
+ [
+ { name: 'Netherlands', points: pts('fred:SPASTT01NLM661N'), color: '#f97316' },
+ { name: 'United States', points: pts('fred:SPASTT01USM661N'), color: '#3b82f6' },
+ { name: 'South Korea', points: pts('fred:SPASTT01KRM661N'), color: '#e11d48' },
+ { name: 'China', points: pts('fred:SPASTT01CNM661N'), color: '#eab308' }
+ ],
+ { zoom: true }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ { name: 'S&P 500', points: pts('fred:SP500'), color: '#3b82f6' },
+ { name: 'NASDAQ Composite', points: pts('fred:NASDAQCOM'), color: '#10b981', right: true }
+ ],
+ { zoom: true, unit: 'S&P', rightUnit: 'NASDAQ' }
+ )}
+ />
+
+
+Growth and jobs
+
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Netherlands',
+ points: pts('eurostat:nama_10_gdp:CLV_PCH_PRE.B1GQ.NL'),
+ color: '#f97316',
+ kind: 'bar'
+ },
+ {
+ name: 'EU 27',
+ points: pts('eurostat:nama_10_gdp:CLV_PCH_PRE.B1GQ.EU27_2020'),
+ color: '#3b82f6'
+ },
+ {
+ name: 'Germany',
+ points: pts('eurostat:nama_10_gdp:CLV_PCH_PRE.B1GQ.DE'),
+ color: '#64748b'
+ }
+ ],
+ { unit: '%' }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ { name: 'Real GDP', points: pts('fred:GDPC1'), color: '#3b82f6', kind: 'area' },
+ { name: 'Unemployment rate', points: pts('fred:UNRATE'), color: '#e11d48', right: true }
+ ],
+ { unit: 'USD bn', rightUnit: '%', zoom: true }
+ )}
+ />
+
+
+Public finances and migration
+
+ barRankOption(t, debt, { unit: '% of GDP', color: '#a855f7' })}
+ />
+ barRankOption(t, deficit, { unit: '% of GDP', color: '#10b981' })}
+ />
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Spending',
+ points: pts('eurostat:gov_10a_main:PC_GDP.S13.TE.NL'),
+ color: '#e11d48'
+ },
+ {
+ name: 'Revenue',
+ points: pts('eurostat:gov_10a_main:PC_GDP.S13.TR.NL'),
+ color: '#10b981'
+ },
+ {
+ name: 'EU 27 spending',
+ points: pts('eurostat:gov_10a_main:PC_GDP.S13.TE.EU27_2020'),
+ color: '#e11d48',
+ dashed: true
+ },
+ {
+ name: 'EU 27 revenue',
+ points: pts('eurostat:gov_10a_main:PC_GDP.S13.TR.EU27_2020'),
+ color: '#10b981',
+ dashed: true
+ }
+ ],
+ { unit: '% of GDP', min: 'dataMin' }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Netherlands',
+ points: pts('eurostat:migr_imm1ctz:TOTAL.COMPLET.TOTAL.NR.T.NL'),
+ color: '#f97316',
+ kind: 'area'
+ },
+ {
+ name: 'Belgium',
+ points: pts('eurostat:migr_imm1ctz:TOTAL.COMPLET.TOTAL.NR.T.BE'),
+ color: '#eab308'
+ },
+ {
+ name: 'Sweden',
+ points: pts('eurostat:migr_imm1ctz:TOTAL.COMPLET.TOTAL.NR.T.SE'),
+ color: '#3b82f6'
+ },
+ {
+ name: 'Austria',
+ points: pts('eurostat:migr_imm1ctz:TOTAL.COMPLET.TOTAL.NR.T.AT'),
+ color: '#e11d48'
+ }
+ ],
+ { decimals: 0 }
+ )}
+ />
+ barRankOption(t, migrationPerK, { unit: 'per 1,000', color: '#06b6d4' })}
+ />
+
+
+ Policy rates: BIS central bank policy rates (month end). Yields for the euro countries are
+ Eurostat's EMU convergence-criterion series.
+
diff --git a/src/routes/stats/energy/+page.server.ts b/src/routes/stats/energy/+page.server.ts
new file mode 100644
index 0000000..4588582
--- /dev/null
+++ b/src/routes/stats/energy/+page.server.ts
@@ -0,0 +1,58 @@
+import { cached, loadSeries } from '#lib/server/stats.js';
+import type { PageServerLoad } from './$types';
+
+const EU = ['NL', 'DE', 'FR', 'BE', 'IT', 'ES', 'PL', 'SE', 'DK', 'AT', 'EU27_2020'];
+const RENEW = ['total', 'wind-onshore', 'wind-offshore', 'solar', 'biomass', 'hydro'];
+
+export const load: PageServerLoad = async () => ({
+ series: await cached('stats:energy', () =>
+ loadSeries([
+ ...['variable', 'tax', 'transport', 'fixed', 'taxcredit', 'dynamic'].map(
+ (k) => `cbs:tariff:elec:${k}`
+ ),
+ ...['variable', 'tax', 'transport', 'fixed'].map((k) => `cbs:tariff:gas:${k}`),
+ ...['variable', 'tax', 'ode'].flatMap((k) => [
+ `cbs:tariff1823:elec:${k}`,
+ `cbs:tariff1823:gas:${k}`
+ ]),
+ 'cbs:price0723:elec',
+ 'cbs:price0723:gas',
+ 'cbs:cpi:energy:yoy',
+ ...EU.flatMap((g) => [
+ `eurostat:nrg_pc_204:E7000.KWH2500-4999.KWH.I_TAX.EUR.${g}`,
+ `eurostat:nrg_pc_202:G3000.GJ20-199.KWH.I_TAX.EUR.${g}`,
+ `eurostat:nrg_ind_id:TOTAL.PC.${g}`
+ ]),
+ ...['X_TAX', 'X_VAT'].flatMap((t) => [
+ `eurostat:nrg_pc_204:E7000.KWH2500-4999.KWH.${t}.EUR.NL`,
+ `eurostat:nrg_pc_202:G3000.GJ20-199.KWH.${t}.EUR.NL`
+ ]),
+ { id: 'fred:APU000072610', from: '2007-01-01' },
+ { id: 'fred:DCOILBRENTEU', resample: 'W' },
+ 'fred:PNGASEUUSDM',
+ 'cbs:energy:tpes',
+ 'cbs:energy:netimport',
+ 'cbs:energy:production',
+ 'cbs:energy:gas:production',
+ 'cbs:energy:gas:netimport',
+ 'cbs:energy:gas:tpes',
+ ...RENEW.flatMap((k) => [`cbs:renew:${k}:share`, `cbs:renew:${k}:mw`]),
+ 'cbs:heatpumps:homes',
+ 'cbs:heatpumps:homes:air',
+ 'cbs:heatpumps:homes:ground',
+ 'cbs:heatpumps:new',
+ ...['NL01', 'GM0513'].flatMap((r) => [
+ `cbs:solar:homes:kwp:${r}`,
+ `cbs:solar:homes:n:${r}`,
+ `cbs:solar:kwp:${r}`
+ ]),
+ 'cbs:pop:jan1:GM0513',
+ 'cbs:cars:total',
+ 'cbs:cars:electric',
+ 'cbs:cars:petrol',
+ 'cbs:cars:diesel',
+ 'eurostat:road_eqr_carpda:NR.ELC.NL',
+ 'eurostat:road_eqr_carpda:NR.TOTAL.NL'
+ ])
+ )
+});
diff --git a/src/routes/stats/energy/+page.svelte b/src/routes/stats/energy/+page.svelte
new file mode 100644
index 0000000..14fc428
--- /dev/null
+++ b/src/routes/stats/energy/+page.svelte
@@ -0,0 +1,605 @@
+
+
+
+ energy · stats · Stan Runge
+
+
+
+
+ stats / energy
+ Energy
+
+ What Dutch households pay for electricity and gas, how that compares with Europe and the US, and
+ how fast the Netherlands is switching: renewables, heat pumps, solar panels and electric cars.
+
+
+
+
+
+ Live grid data · hourly Dutch production, consumption and the energy mix in the
+ energy tracker
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Household tariffs in the Netherlands
+
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Average price 2007–2022 (2.5–5 MWh)',
+ points: pts('cbs:price0723:elec'),
+ color: '#64748b'
+ },
+ { name: 'Variable + tax 2018–2022', points: elec1823, color: '#eab308', dashed: true },
+ { name: 'Variable + tax (new contracts)', points: elecAllIn, color: '#eab308' },
+ { name: 'Dynamic contract', points: pts('cbs:tariff:elec:dynamic'), color: '#10b981' }
+ ],
+ { unit: '€/kWh', decimals: 3, zoom: true }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ { name: 'Variable + tax 2018–2022', points: gas1823, color: '#f97316', dashed: true },
+ { name: 'Variable + tax (new contracts)', points: gasAllIn, color: '#f97316' },
+ {
+ name: 'of which energy tax',
+ points: pts('cbs:tariff:gas:tax'),
+ color: '#e11d48',
+ kind: 'area'
+ }
+ ],
+ { unit: '€/m³', decimals: 3, zoom: true }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Supply (variable)',
+ points: [
+ ...pts('cbs:tariff1823:elec:variable').filter(([d]) => d < '2021-01-01'),
+ ...pts('cbs:tariff:elec:variable')
+ ],
+ color: '#eab308',
+ kind: 'area',
+ stack: 'kwh'
+ },
+ {
+ name: 'Energy tax',
+ points: [
+ ...pts('cbs:tariff1823:elec:tax').filter(([d]) => d < '2021-01-01'),
+ ...pts('cbs:tariff:elec:tax')
+ ],
+ color: '#e11d48',
+ kind: 'area',
+ stack: 'kwh'
+ },
+ {
+ name: 'ODE surcharge',
+ points: pts('cbs:tariff1823:elec:ode'),
+ color: '#a855f7',
+ kind: 'area',
+ stack: 'kwh'
+ }
+ ],
+ { unit: '€/kWh', decimals: 3 }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Network (transport) tariff',
+ points: pts('cbs:tariff:elec:transport'),
+ color: '#3b82f6',
+ kind: 'step'
+ },
+ {
+ name: 'Fixed supply tariff',
+ points: pts('cbs:tariff:elec:fixed'),
+ color: '#eab308',
+ kind: 'step'
+ },
+ {
+ name: 'Energy tax reduction',
+ points: pts('cbs:tariff:elec:taxcredit'),
+ color: '#10b981',
+ kind: 'step'
+ },
+ {
+ name: 'Gas network tariff',
+ points: pts('cbs:tariff:gas:transport'),
+ color: '#f97316',
+ kind: 'step'
+ }
+ ],
+ { unit: '€/year', decimals: 0 }
+ )}
+ />
+
+
+Compared with Europe and the US
+
+
+ barRankOption(t, elecRank.rows, { decimals: 3, unit: '€/kWh', color: '#eab308' })}
+ />
+
+ lineOption(
+ t,
+ [
+ { name: 'Netherlands (€)', points: elecPrice('NL'), color: '#f97316', kind: 'step' },
+ { name: 'Germany (€)', points: elecPrice('DE'), color: '#64748b', kind: 'step' },
+ { name: 'EU 27 (€)', points: elecPrice('EU27_2020'), color: '#3b82f6', kind: 'step' },
+ { name: 'United States ($)', points: usElec, color: '#e11d48' }
+ ],
+ { unit: 'per kWh', decimals: 3 }
+ )}
+ />
+ barRankOption(t, gasRank.rows, { decimals: 3, unit: '€/kWh', color: '#f97316' })}
+ />
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Electricity excl. taxes',
+ points: pts('eurostat:nrg_pc_204:E7000.KWH2500-4999.KWH.X_TAX.EUR.NL'),
+ color: '#eab308',
+ kind: 'step',
+ dashed: true
+ },
+ {
+ name: 'Electricity incl. taxes',
+ points: elecPrice('NL'),
+ color: '#eab308',
+ kind: 'step'
+ },
+ {
+ name: 'Gas excl. taxes',
+ points: pts('eurostat:nrg_pc_202:G3000.GJ20-199.KWH.X_TAX.EUR.NL'),
+ color: '#f97316',
+ kind: 'step',
+ dashed: true
+ },
+ { name: 'Gas incl. taxes', points: gasPrice('NL'), color: '#f97316', kind: 'step' }
+ ],
+ { unit: '€/kWh', decimals: 3 }
+ )}
+ />
+
+
+Oil, gas and imports
+
+
+ lineOption(
+ t,
+ [
+ { name: 'Brent', points: pts('fred:DCOILBRENTEU'), color: '#64748b' },
+ { name: 'EU gas (TTF)', points: pts('fred:PNGASEUUSDM'), color: '#3b82f6', right: true }
+ ],
+ { unit: '$/bbl', rightUnit: '$/MMBtu', zoom: true }
+ )}
+ />
+
+ lineOption(t, [{ name: 'Netherlands', points: importDep, color: '#e11d48', kind: 'area' }], {
+ unit: '%',
+ legend: false
+ })}
+ />
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Production',
+ points: pts('cbs:energy:gas:production'),
+ color: '#10b981',
+ kind: 'area'
+ },
+ { name: 'Net imports', points: pts('cbs:energy:gas:netimport'), color: '#e11d48' },
+ { name: 'Supply', points: pts('cbs:energy:gas:tpes'), color: '#64748b', dashed: true }
+ ],
+ { unit: 'PJ', decimals: 0 }
+ )}
+ />
+ barRankOption(t, depRank.rows, { unit: '%', color: '#e11d48' })}
+ />
+
+
+The switch
+
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Onshore wind',
+ points: pts('cbs:renew:wind-onshore:share'),
+ color: '#06b6d4',
+ kind: 'area',
+ stack: 'r'
+ },
+ {
+ name: 'Offshore wind',
+ points: pts('cbs:renew:wind-offshore:share'),
+ color: '#3b82f6',
+ kind: 'area',
+ stack: 'r'
+ },
+ {
+ name: 'Solar',
+ points: pts('cbs:renew:solar:share'),
+ color: '#eab308',
+ kind: 'area',
+ stack: 'r'
+ },
+ {
+ name: 'Biomass',
+ points: pts('cbs:renew:biomass:share'),
+ color: '#84cc16',
+ kind: 'area',
+ stack: 'r'
+ },
+ {
+ name: 'Hydro',
+ points: pts('cbs:renew:hydro:share'),
+ color: '#a855f7',
+ kind: 'area',
+ stack: 'r'
+ }
+ ],
+ { unit: '%' }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Solar',
+ points: giga(pts('cbs:renew:solar:mw'), 1000),
+ color: '#eab308',
+ kind: 'bar'
+ },
+ {
+ name: 'Onshore wind',
+ points: giga(pts('cbs:renew:wind-onshore:mw'), 1000),
+ color: '#06b6d4',
+ kind: 'bar'
+ },
+ {
+ name: 'Offshore wind',
+ points: giga(pts('cbs:renew:wind-offshore:mw'), 1000),
+ color: '#3b82f6',
+ kind: 'bar'
+ }
+ ],
+ { unit: 'GW' }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Air source',
+ points: pts('cbs:heatpumps:homes:air'),
+ color: '#06b6d4',
+ kind: 'area',
+ stack: 'hp'
+ },
+ {
+ name: 'Ground / water source',
+ points: pts('cbs:heatpumps:homes:ground'),
+ color: '#10b981',
+ kind: 'area',
+ stack: 'hp'
+ },
+ {
+ name: 'New per year (all buildings)',
+ points: pts('cbs:heatpumps:new'),
+ color: '#e11d48',
+ kind: 'bar',
+ right: true
+ }
+ ],
+ { decimals: 0, rightUnit: 'new' }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Netherlands',
+ points: giga(pts('cbs:solar:homes:kwp:NL01'), 1000),
+ color: '#eab308',
+ kind: 'bar'
+ },
+ {
+ name: 'Gouda',
+ points: giga(pts('cbs:solar:homes:kwp:GM0513'), 1000),
+ color: '#f97316',
+ right: true
+ }
+ ],
+ { unit: 'NL MWp', rightUnit: 'Gouda MWp', decimals: 1 }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ { name: 'Share of all cars on the road', points: evShare, color: '#a855f7', kind: 'bar' },
+ { name: 'Share of new cars registered', points: evNewShare, color: '#10b981' }
+ ],
+ { unit: '%' }
+ )}
+ />
+
+
+
diff --git a/src/routes/stats/gouda/+page.server.ts b/src/routes/stats/gouda/+page.server.ts
new file mode 100644
index 0000000..698783c
--- /dev/null
+++ b/src/routes/stats/gouda/+page.server.ts
@@ -0,0 +1,44 @@
+import { cached, loadCbsCross, loadSeries } from '#lib/server/stats.js';
+import type { PageServerLoad } from './$types';
+
+// Gouda (GM0513) and its neighbours; NL01 is the national total.
+const RING = ['GM0513', 'GM0627', 'GM1901', 'GM1892', 'GM1931', 'GM0484', 'GM0632'];
+const KWB_TABLE = '85984NED'; // Kerncijfers wijken en buurten 2024
+const KWB_MEASURES = [
+ 'T001036',
+ 'M000100',
+ 'M001642',
+ '1014850_2',
+ 'A047047',
+ 'M000224',
+ 'M008349',
+ 'M000221_2',
+ 'M000219_2',
+ 'M008295',
+ 'M008297'
+];
+
+export const load: PageServerLoad = async () => {
+ const [series, wijken] = await Promise.all([
+ cached('stats:gouda', () =>
+ loadSeries([
+ ...['NL01', ...RING].flatMap((r) => [
+ `cbs:pop:monthly:${r}`,
+ `cbs:pop:jan1:${r}`,
+ `cbs:house:avgprice:${r}`,
+ ...['total', 'owner', 'social', 'private'].map((k) => `cbs:stock:${k}:${r}`)
+ ]),
+ ...RING.flatMap((r) => ['total', 'ozb', 'sewer', 'waste'].map((k) => `cbs:levy:${k}:${r}`)),
+ ...['births', 'deaths', 'immigration', 'emigration', 'in', 'out', 'growth'].map(
+ (k) => `cbs:pop:${k}:GM0513`
+ ),
+ 'cbs:house:hpi:cr28',
+ 'cbs:house:hpi:nl'
+ ])
+ ),
+ cached('stats:gouda:kwb', () =>
+ loadCbsCross(KWB_TABLE, KWB_MEASURES, 'WijkenEnBuurten', ['WK0513', 'GM0513', 'NL00'])
+ )
+ ]);
+ return { series, wijken, kwbYear: 2024 };
+};
diff --git a/src/routes/stats/gouda/+page.svelte b/src/routes/stats/gouda/+page.svelte
new file mode 100644
index 0000000..4d62731
--- /dev/null
+++ b/src/routes/stats/gouda/+page.svelte
@@ -0,0 +1,396 @@
+
+
+
+ Gouda · stats · Stan Runge
+
+
+
+
+ stats / gouda
+ Gouda
+
+ My home town in numbers, next to its neighbours: Waddinxveen, Bodegraven-Reeuwijk, Zuidplas,
+ Krimpenerwaard, Alphen aan den Rijn and Woerden.
+
+
+
+
+
+
+
+
+
+
+
+People
+
+
+ lineOption(
+ t,
+ [{ name: 'Gouda', points: pts('cbs:pop:jan1:GM0513'), color: '#f97316', kind: 'area' }],
+ { decimals: 0, legend: false }
+ )}
+ />
+
+ lineOption(
+ t,
+ ['NL01', ...RING].map((r) => ({
+ name: NAMES[r],
+ points: indexed(pts(`cbs:pop:monthly:${r}`)),
+ color: COLORS[r],
+ dashed: r === 'NL01'
+ })),
+ { decimals: 1, zoom: true }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ { name: 'Births', points: pts('cbs:pop:births:GM0513'), color: '#10b981' },
+ { name: 'Deaths', points: pts('cbs:pop:deaths:GM0513'), color: '#64748b' },
+ { name: 'Immigration', points: pts('cbs:pop:immigration:GM0513'), color: '#3b82f6' },
+ { name: 'Emigration', points: pts('cbs:pop:emigration:GM0513'), color: '#a855f7' },
+ { name: 'Moved in', points: pts('cbs:pop:in:GM0513'), color: '#eab308', dashed: true },
+ { name: 'Moved out', points: pts('cbs:pop:out:GM0513'), color: '#e11d48', dashed: true },
+ {
+ name: 'Net growth',
+ points: pts('cbs:pop:growth:GM0513'),
+ color: '#f97316',
+ kind: 'bar'
+ }
+ ],
+ { decimals: 0, zoom: true }
+ )}
+ />
+
+
+Housing
+
+
+ lineOption(
+ t,
+ ['NL01', ...RING].map((r) => ({
+ name: NAMES[r],
+ points: pts(`cbs:house:avgprice:${r}`),
+ color: COLORS[r],
+ dashed: r === 'NL01'
+ })),
+ { unit: '€', decimals: 0 }
+ )}
+ />
+
+ lineOption(
+ t,
+ [
+ { name: 'Oost-Zuid-Holland', points: pts('cbs:house:hpi:cr28'), color: '#f97316' },
+ { name: 'Netherlands', points: pts('cbs:house:hpi:nl'), color: '#0f172a', dashed: true }
+ ],
+ { decimals: 1, zoom: true }
+ )}
+ />
+ barRankOption(t, priceRank, { decimals: 0, unit: 'k€', color: '#64748b' })}
+ />
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Owner-occupied',
+ points: pts('cbs:stock:owner:GM0513'),
+ color: '#3b82f6',
+ kind: 'bar',
+ stack: 's'
+ },
+ {
+ name: 'Social rent (housing corporation)',
+ points: pts('cbs:stock:social:GM0513'),
+ color: '#f97316',
+ kind: 'bar',
+ stack: 's'
+ },
+ {
+ name: 'Private rent',
+ points: pts('cbs:stock:private:GM0513'),
+ color: '#a855f7',
+ kind: 'bar',
+ stack: 's'
+ }
+ ],
+ { decimals: 0 }
+ )}
+ />
+ barRankOption(t, socialShare, { unit: '%', color: '#64748b' })}
+ />
+ barRankOption(t, levyRank, { decimals: 0, unit: '€', color: '#64748b' })}
+ />
+
+ lineOption(
+ t,
+ [
+ {
+ name: 'Property tax (OZB)',
+ points: pts('cbs:levy:ozb:GM0513'),
+ color: '#3b82f6',
+ kind: 'bar',
+ stack: 'l'
+ },
+ {
+ name: 'Sewer charge',
+ points: pts('cbs:levy:sewer:GM0513'),
+ color: '#06b6d4',
+ kind: 'bar',
+ stack: 'l'
+ },
+ {
+ name: 'Waste charge',
+ points: pts('cbs:levy:waste:GM0513'),
+ color: '#10b981',
+ kind: 'bar',
+ stack: 'l'
+ }
+ ],
+ { unit: '€', decimals: 0 }
+ )}
+ />
+
+
+Neighbourhoods ({data.kwbYear})
+{#if wijken.length}
+
+
+ barRankOption(t, wijkBars('M001642'), { decimals: 0, unit: 'k€', color: '#e11d48' })}
+ />
+
+ barRankOption(t, wijkBars('M000224'), { decimals: 1, unit: 'k€', color: '#10b981' })}
+ />
+
+
+
+
+
+ Neighbourhood
+ {#each COLS as c (c.key)}{c.label}{c.unit ? ` (${c.unit})` : ''} {/each}
+
+
+
+ {#each wijken as w (w.code)}
+
+ {w.title}
+ {#each COLS as c (c.key)}{num(w.values[c.key], c.decimals)} {/each}
+
+ {/each}
+ {#each totals as w (w.code)}
+
+ {w.title}
+ {#each COLS as c (c.key)}{num(w.values[c.key], c.decimals)} {/each}
+
+ {/each}
+
+
+
+
+ {data.attributions.cbs ?? ''} · Kerncijfers wijken en buurten {data.kwbYear}. Blank cells are
+ suppressed by CBS for privacy.
+
+{:else}
+ Neighbourhood figures are not loaded yet.
+{/if}
+
+