diff --git a/src/lib/projects.ts b/src/lib/projects.ts index de3db94..7b4c461 100644 --- a/src/lib/projects.ts +++ b/src/lib/projects.ts @@ -34,70 +34,66 @@ export const projects = [ } ] as const; -// The public /stats section: open data on the economy, energy and Gouda (src/routes/stats). +// The public /stats section (src/routes/stats): a place × theme matrix; these are the shortcuts. export const statsPages = [ { name: 'economy', icon: 'stats-economy', subtitle: 'rates, inflation, bond spreads, GDP and debt', - href: '/stats/economy', + href: '/stats/core/economy', + external: false + }, + { + name: 'jobs & cost of living', + icon: 'stats-jobs', + subtitle: 'unemployment, vacancies, graduate outcomes, wages, rents and price levels', + href: '/stats/core/jobs', external: false }, { name: 'energy', icon: 'stats-energy', subtitle: 'live grid mix and gas storage, tariffs, oil and gas, renewables, heat pumps and EVs', - href: '/stats/energy', + href: '/stats/nld/energy', external: false }, { name: 'gouda', icon: 'stats-gouda', subtitle: 'population, housing and neighbourhoods in Gouda', - href: '/stats/gouda', + href: '/stats/gouda/overview', external: false }, { name: 'countries', icon: 'stats-countries', subtitle: 'the Netherlands, Curaçao, the US, Korea, China and more side by side', - href: '/stats/countries', + href: '/stats/core/overview', external: false }, { name: 'elections', icon: 'stats-elections', subtitle: 'Dutch elections since 1918, Gouda per polling station, US presidential', - href: '/stats/elections', + href: '/stats/nld/politics', external: false }, { name: 'history', icon: 'stats-history', subtitle: 'the long run, a sourced timeline, politics and culture of five places', - href: '/stats/history', + href: '/stats/world/history', external: false }, { name: 'opinions', icon: 'stats-opinions', subtitle: 'polls, Eurobarometer, happiness, party positions and world opinions', - href: '/stats/opinions', + href: '/stats/core/politics', external: false } ] as const; -/** Sub-tabs of a /stats section (second row under the stats nav), keyed by the section path. */ -export const statsSubPages: Record< - string, - readonly { name: string; href: string; icon: string }[] -> = { - '/stats/opinions': [ - { name: 'polls & surveys', href: '/stats/opinions', icon: 'stats-polls' }, - { name: 'world', href: '/stats/opinions/world', icon: 'stats-world' } - ] -}; - type Tracker = { name: string; subtitle: string; diff --git a/src/lib/seo.ts b/src/lib/seo.ts index 854ae75..665aba2 100644 --- a/src/lib/seo.ts +++ b/src/lib/seo.ts @@ -122,7 +122,7 @@ const publicPages: Record = { description: 'Learn Dutch, Korean and English from beginner to advanced, with lessons and endless daily challenges.' }, - '/stats/opinions/world': { + '/stats/world/politics': { title: 'World Opinions', description: 'How political words, parties, values and national histories differ across the world.' diff --git a/src/lib/server/db/schema/stats.ts b/src/lib/server/db/schema/stats.ts index 8af5873..ac0efe8 100644 --- a/src/lib/server/db/schema/stats.ts +++ b/src/lib/server/db/schema/stats.ts @@ -523,7 +523,7 @@ export const statsElecTurnout = statsSchema.table( ); // ── Step 3: world opinions (§4, A15) ───────────────────────────────────────────────────── -// The /stats/opinions/world guide reads these. Curated rows (seeded from the former +// The /stats/world/politics guide reads these. Curated rows (seeded from the former // src/lib/world-opinions/data.ts) keep Stan's text exactly; collectors add CHES / V-Party data. export const statsWoCountry = statsSchema.table('wo_country', { diff --git a/src/lib/server/stats-history.ts b/src/lib/server/stats-history.ts index 2561de4..c71d237 100644 --- a/src/lib/server/stats-history.ts +++ b/src/lib/server/stats-history.ts @@ -70,7 +70,7 @@ export function loadHeritage(place: HeritageCode): Promise { }); } -/** Long-run comparison series for /stats/history. */ +/** Long-run comparison series for /stats/world/history. */ export function loadHistoryOverview() { return cached('stats:history:overview', async () => { const codes = ['NLD', 'USA', 'KOR', 'CUW', 'WLD']; diff --git a/src/lib/server/stats-matrix.ts b/src/lib/server/stats-matrix.ts new file mode 100644 index 0000000..6954f3a --- /dev/null +++ b/src/lib/server/stats-matrix.ts @@ -0,0 +1,111 @@ +// Server side of the /stats matrix: resolves a theme's metrics for the selected places to stored +// series (first candidate with data wins) and loads the rich views that fit the combination. +import { cached, loadSeries, type StatPoint } from '#lib/server/stats.js'; +import { + candidates, + PLACE_BY_CODE, + type Metric, + type Selection, + type Theme, + type ViewSpec +} from '#lib/stats/matrix.js'; +import * as views from '#lib/server/stats-views.js'; +import type { CountryCode } from '#lib/stats/countries.js'; +import type { HeritageCode } from '#lib/stats/heritage.js'; + +export type MetricLine = { + code: string; + seriesId: string; + sourceId: string; + freq: string; + unit: string; + points: StatPoint[]; +}; +export type MetricResult = { + metric: Metric; + lines: MetricLine[]; + /** Selected places without any series for this metric. */ + missing: string[]; +}; + +export async function loadMetrics(sel: Selection, theme: Theme): Promise { + const places = sel.codes.map((c) => PLACE_BY_CODE.get(c)!).filter(Boolean); + const reqs = new Map(); + for (const m of theme.metrics) + for (const p of places) + for (const id of candidates(m, p)) + reqs.set(id, { + id, + ...(m.from ? { from: m.from } : {}), + ...(m.resample ? { resample: m.resample } : {}) + }); + const key = `stats:matrix:${theme.slug}:${sel.codes.join(',')}`; + const series = await cached(key, () => loadSeries([...reqs.values()])); + const today = new Date().toISOString().slice(0, 10); + return theme.metrics.map((m) => { + const lines: MetricLine[] = []; + const missing: string[] = []; + for (const p of places) { + const id = candidates(m, p).find((i) => series[i]?.points.length); + if (!id) { + missing.push(p.code); + continue; + } + const s = series[id]; + // Drop forecasts (IMF WEO runs years ahead); the charts show what has happened. + const points = s.points.filter(([period]) => period <= today); + if (!points.length) { + missing.push(p.code); + continue; + } + lines.push({ + code: p.code, + seriesId: id, + sourceId: s.sourceId, + freq: s.freq, + unit: s.unit, + points + }); + } + return { metric: m, lines, missing }; + }); +} + +export type LoadedView = + | { kind: 'economy'; data: views.EconomyViewData } + | { kind: 'energy'; data: views.EnergyViewData } + | { kind: 'gouda'; data: views.GoudaViewData } + | { kind: 'countries'; data: views.CountriesViewData } + | { kind: 'country'; data: views.CountryViewData } + | { kind: 'elections'; focus: 'all' | 'nld' | 'gouda' | 'usa'; data: views.ElectionsViewData } + | { kind: 'opinions'; data: views.OpinionsViewData } + | { kind: 'worldOpinions'; data: views.WorldOpinionsViewData } + | { kind: 'history'; data: views.HistoryViewData } + | { kind: 'heritage'; data: Awaited> }; + +export function loadView(v: ViewSpec): Promise { + switch (v.kind) { + case 'economy': + return views.loadEconomyView().then((data) => ({ kind: v.kind, data })); + case 'energy': + return views.loadEnergyView().then((data) => ({ kind: v.kind, data })); + case 'gouda': + return views.loadGoudaView().then((data) => ({ kind: v.kind, data })); + case 'countries': + return views.loadCountriesView().then((data) => ({ kind: v.kind, data })); + case 'country': + return views.loadCountryView(v.code as CountryCode).then((data) => ({ kind: v.kind, data })); + case 'elections': + return views.loadElectionsView().then((data) => ({ kind: v.kind, focus: v.focus, data })); + case 'opinions': + return views.loadOpinionsView().then((data) => ({ kind: v.kind, data })); + case 'worldOpinions': + return views.loadWorldOpinionsView().then((data) => ({ kind: v.kind, data })); + case 'history': + return views.loadHistoryView().then((data) => ({ kind: v.kind, data })); + case 'heritage': + return views + .loadHeritageView(v.code as HeritageCode) + .then((data) => ({ kind: v.kind, data })); + } +} diff --git a/src/lib/server/stats-views.ts b/src/lib/server/stats-views.ts new file mode 100644 index 0000000..7df0451 --- /dev/null +++ b/src/lib/server/stats-views.ts @@ -0,0 +1,433 @@ +// Loaders for the rich per-place views of the /stats matrix (the former /stats/
pages). +// Each returns plain data for one view component in #lib/stats/views; the matrix page decides +// which views a place × theme combination shows (see viewsFor in #lib/stats/matrix.ts). +import { cached, loadCbsCross, loadLatest, loadSeries } from '#lib/server/stats.js'; +import { client } from '#lib/server/db/index.js'; +import { loadEnergy } from '#lib/server/energy.js'; +import { loadHeritage, loadHistoryOverview } from '#lib/server/stats-history.js'; +import { loadWorldOpinions } from '#lib/server/world-opinions.js'; +import { + ANNUAL, + CPI_BY_CATEGORY, + CPI_TOTAL, + ECON_COUNTRIES, + POLICY_RATE +} from '#lib/stats/econ.js'; +import { epSeriesIds, GOUDA_PC4 } from '#lib/stats/energylabels.js'; +import { + COMPARE_COLUMNS, + COUNTRY_PAGES, + PROFILE_SERIES, + seriesIds, + type CountryCode +} from '#lib/stats/countries.js'; +import { CW_SERIES } from '#lib/stats/curacao.js'; +import { isHeritage, type HeritageCode } from '#lib/stats/heritage.js'; +import { ebLabel, latestPolls, pollAverages, type Poll } from '#lib/stats/opinions.js'; + +// economy (core countries) + +const ECO_EU = ['NL', 'DE', 'FR', 'BE', 'IT', 'ES', 'PL', 'SE', 'DK', 'AT', 'EU27_2020', 'EA20']; +const ECO_YIELDS = ['NL', 'DE', 'FR', 'IT', 'ES', 'BE', 'AT', 'EA']; +const ECO_REGIONS = [...ECON_COUNTRIES, 'EU27', 'EA']; + +export const loadEconomyView = async () => ({ + series: await cached('stats:economy', () => + loadSeries([ + ...[...new Set(Object.values(POLICY_RATE))].map((a) => ({ + id: `bis:M:${a}`, + from: '1990-01-01' + })), + ...ECO_EU.map((g) => `eurostat:prc_hicp_minr:RCH_A.TOTAL.${g}`), + // CPI index levels (all items, then the categories) + ...[ + ...new Set([ + ...Object.values(CPI_TOTAL).map((c) => c.id), + ...Object.values(CPI_BY_CATEGORY).flatMap((c) => Object.values(c.ids)) + ]) + ].map((id) => ({ id, from: '1960-01-01' })), + ...ECO_REGIONS.map((c) => ({ id: `oecd:irlt:${c}`, from: '1980-01-01' })), + ...ECO_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' }, + ...['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' }, + ...Object.values(ANNUAL).flatMap((ind) => + ECO_REGIONS.flatMap((c) => [ + ...(ind.imf ? [{ id: `imf:${ind.imf}:${c}`, from: '1980-01-01' }] : []), + { id: `wb:${ind.wb}:${c}`, from: '1980-01-01' } + ]) + ), + ...Object.values(ANNUAL).flatMap((ind) => (ind.cw ? [ind.cw] : [])), + 'fred:GDPC1', + 'fred:UNRATE', + ...ECO_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}` + ]) + ]) + ) +}); + +// energy (the Netherlands, with Europe and the US) + +const ENE_EU = ['NL', 'DE', 'FR', 'BE', 'IT', 'ES', 'PL', 'SE', 'DK', 'AT', 'EU27_2020']; +const ENE_RENEW = ['total', 'wind-onshore', 'wind-offshore', 'solar', 'biomass', 'hydro']; + +export const loadEnergyView = async () => ({ + // Live NED grid + gas storage (read-only, ned-collector's `energy` schema). + energy: await cached('stats:energy:live', loadEnergy), + 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', + ...ENE_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_pc_204:E7000.KWH3500.KWH.I_TAX.EUR.${g}`, + `eurostat:nrg_pc_202:G3000.GJ83P70.GJ_GCV.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', + ...ENE_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', + ...epSeriesIds(['NL', 'GOUDA']) + ]) + ) +}); + +// Gouda overview + +// Gouda (GM0513) and its neighbours; NL01 is the national total. +const GOU_RING = ['GM0513', 'GM0627', 'GM1901', 'GM1892', 'GM1931', 'GM0484', 'GM0632']; +const GOU_KWB_TABLE = '85984NED'; // Kerncijfers wijken en buurten 2024 +const GOU_KWB_MEASURES = [ + 'T001036', + 'M000100', + 'M001642', + '1014850_2', + 'A047047', + 'M000224', + 'M008349', + 'M000221_2', + 'M000219_2', + 'M008295', + 'M008297' +]; + +export const loadGoudaView = async () => { + const [series, wijken, heritage] = await Promise.all([ + cached('stats:gouda', () => + loadSeries([ + ...['NL01', ...GOU_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}`) + ]), + ...GOU_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', + ...epSeriesIds(['NL', 'GOUDA', ...GOUDA_PC4]) + ]) + ), + cached('stats:gouda:kwb', () => + loadCbsCross(GOU_KWB_TABLE, GOU_KWB_MEASURES, 'WijkenEnBuurten', ['WK0513', 'GM0513', 'NL00']) + ), + loadHeritage('GM0513') + ]); + return { series, wijken, kwbYear: 2024, heritage }; +}; + +// countries side by side + +export const loadCountriesView = async () => + cached('stats:countries', async () => { + const ids = COUNTRY_PAGES.flatMap((c) => + COMPARE_COLUMNS.flatMap((col) => seriesIds(c, col.ids)) + ); + const [latest, series] = await Promise.all([ + loadLatest(ids), + loadSeries( + COUNTRY_PAGES.flatMap((c) => [ + { id: `wb:NY.GDP.PCAP.PP.KD:${c}` }, + { id: `wb:SP.DYN.LE00.IN:${c}`, from: '1960-01-01' } + ]) + ) + ]); + const rows = COUNTRY_PAGES.map((c) => ({ + code: c, + values: Object.fromEntries( + COMPARE_COLUMNS.map((col) => { + const id = seriesIds(c, col.ids).find((i) => latest[i]); + return [col.key, id ? { ...latest[id], source: id.split(':')[0] } : null]; + }) + ) + })); + return { rows, series }; + }); + +// elections (NL, Gouda, US) + +export type ElecRow = { + election: string; + date: string; + party: string; + votes: number | null; + seats: number | null; +}; + +const ELE_rows = (r: readonly Record[]): ElecRow[] => + r.map((x) => ({ + election: x.election as string, + date: x.date as string, + party: x.party as string, + votes: x.votes as number | null, + seats: (x.seats as number | null) ?? null + })); + +export const loadElectionsView = async () => + cached('stats:elections', async () => { + const [elections, nl, turnout, gouda, stations, us, usStates] = await Promise.all([ + client`select election_id, country, date::text as date, name, total_seats from stats.elec_election order by date`, + client`select r.election_id as election, e.date::text as date, r.party, r.votes::float8 as votes, r.seats + from stats.elec_result r join stats.elec_election e using (election_id) + where e.country = 'NLD' and r.level = 'national'`, + client`select t.election_id as election, e.date::text as date, t.electorate::float8 as electorate, + t.ballots::float8 as ballots, t.valid::float8 as valid + from stats.elec_turnout t join stats.elec_election e using (election_id) + where t.level = 'national' order by e.date`, + client`select r.election_id as election, e.date::text as date, r.party, r.votes::float8 as votes + from stats.elec_result r join stats.elec_election e using (election_id) + where r.level = 'muni' and r.area_code = 'GM0513'`, + client`select r.area_code, r.area_name, r.party, r.votes::float8 as votes + from stats.elec_result r + where r.level = 'station' and r.election_id = ( + select max(election_id) from stats.elec_result where level = 'station')`, + client`select r.election_id as election, e.date::text as date, r.party, r.votes::float8 as votes + from stats.elec_result r join stats.elec_election e using (election_id) + where e.country = 'USA' and r.level = 'national'`, + client`select r.area_code, r.area_name, r.party, r.votes::float8 as votes + from stats.elec_result r + where r.level = 'state' and r.election_id = ( + select max(election_id) from stats.elec_election where country = 'USA')` + ]); + const stationElection = ( + await client`select max(election_id) as id from stats.elec_result where level = 'station'` + )[0]?.id as string | null; + return { + elections: elections.map((e) => ({ + id: e.election_id as string, + country: e.country as string, + date: e.date as string, + name: e.name as string, + seats: e.total_seats as number | null + })), + nl: ELE_rows(nl), + turnout: turnout.map((t) => ({ + election: t.election as string, + date: t.date as string, + electorate: t.electorate as number | null, + ballots: t.ballots as number | null, + valid: t.valid as number | null + })), + gouda: ELE_rows(gouda), + stationElection, + stations: stations.map((s) => ({ + code: s.area_code as string, + name: s.area_name as string, + party: s.party as string, + votes: s.votes as number | null + })), + us: ELE_rows(us), + usStates: usStates.map((s) => ({ + code: s.area_code as string, + name: s.area_name as string, + party: s.party as string, + votes: s.votes as number | null + })) + }; + }); + +// opinions + +// Averages are computed here so the page gets a few hundred points per series, not every poll row. +function pollData(polls: Poll[]) { + const noRaw = (s: ReturnType) => s.map((o) => ({ ...o, raw: [] })); + return { + nl: pollAverages(polls, 'nl-tk-seats', 5), + nlLatest: latestPolls(polls, 'nl-tk-seats', 8), + us: pollAverages(polls, 'us-trump-approval', 10), + usAgg: latestPolls(polls, 'us-trump-approval-aggregate', 20), + kr: noRaw(pollAverages(polls, 'kr-lee-approval', 10)), + krParty: noRaw(pollAverages(polls, 'kr-party', 10)) + }; +} + +export const loadOpinionsView = async () => + cached('stats:opinions', async () => { + const [polls, eb, whr, trends, gdelt, ches, vparty] = await Promise.all([ + client`select series, poll_key, fieldwork_end::text as date, pollster, sample, option, value + from stats.op_poll where value is not null order by fieldwork_end`, + client`select wave, fieldwork_end::text as date, topic, item, country, share + from stats.eb_item where country in ('NL', 'EU27') and share is not null`, + client`select year, country, iso3, rank, ladder, gdp, social, health, freedom, generosity, corruption + from stats.whr_score order by year`, + client`select geo, date::text as date, rank, query, approx_traffic + from stats.gt_trending t where date = (select max(date) from stats.gt_trending where geo = t.geo) + order by geo, rank`, + client`select date::text as date, country, theme, vol_pct from stats.gdelt_theme_vol + where date > now() - interval '45 days' order by date`, + client`select p.party_id, coalesce(p.abbrev, p.name) as label, p.name, p.family, pp.year, pp.dim, + pp.value, pp.vote_share + from stats.wo_party_position pp join stats.wo_party p using (party_id) + where p.iso3 = 'NLD' and pp.source = 'ches' + and pp.year = (select max(year) from stats.wo_party_position where source = 'ches')`, + client`select p.party_id, coalesce(p.abbrev, p.name) as label, pp.year, pp.value, pp.vote_share + from stats.wo_party_position pp join stats.wo_party p using (party_id) + where p.iso3 = 'NLD' and pp.source = 'vparty' and pp.dim = 'v2xpa_popul' and pp.year >= 1946 + order by pp.year` + ]); + return { + ...pollData( + polls.map((p): Poll => ({ + series: p.series as string, + key: p.poll_key as string, + date: p.date as string, + pollster: p.pollster as string, + sample: p.sample as number | null, + option: p.option as string, + value: Number(p.value) + })) + ), + eb: eb.map((r) => ({ + wave: r.wave as string, + date: r.date as string, + topic: r.topic as string, + item: ebLabel(r.topic as string, r.item as string), + country: r.country as string, + share: Math.round(Number(r.share) * 1000) / 10 + })), + whr: whr.map((r) => ({ + year: r.year as number, + country: r.country as string, + iso3: r.iso3 as string | null, + rank: r.rank as number | null, + ladder: r.ladder == null ? null : Number(r.ladder) + })), + trends: trends.map((r) => ({ + geo: r.geo as string, + date: r.date as string, + rank: r.rank as number, + query: r.query as string, + traffic: r.approx_traffic as number | null + })), + gdelt: gdelt.map((r) => ({ + date: r.date as string, + country: (r.country as string).trim(), + theme: r.theme as string, + vol: Number(r.vol_pct) + })), + ches: ches.map((r) => ({ + id: r.party_id as string, + label: r.label as string, + name: r.name as string | null, + family: r.family as string | null, + year: r.year as number, + dim: r.dim as string, + value: Number(r.value), + share: r.vote_share == null ? null : Number(r.vote_share) + })), + vparty: vparty.map((r) => ({ + id: r.party_id as string, + label: r.label as string, + year: r.year as number, + value: Number(r.value), + share: r.vote_share == null ? null : Number(r.vote_share) + })) + }; + }); + +// one country or region profile +export const loadCountryView = async (code: CountryCode) => { + const heritage = isHeritage(code) ? loadHeritage(code) : null; + const series = await cached(`stats:country:${code}`, () => + loadSeries([ + ...PROFILE_SERIES.map((id) => `${id}:${code}`), + // Benchmarks for the "next to the Netherlands and the world" lines. + ...['NLD', 'WLD'].flatMap((c) => [ + `wb:NY.GDP.PCAP.PP.KD:${c}`, + `wb:SP.DYN.LE00.IN:${c}`, + `wb:FP.CPI.TOTL.ZG:${c}` + ]), + // §8: Curaçao detail from CBS Curaçao and the international APIs (ids carry no code suffix). + ...(code === 'CUW' ? CW_SERIES : []) + ]) + ); + return { code, series, heritage: heritage ? await heritage : null }; +}; + +export const loadWorldOpinionsView = async () => ({ wo: await loadWorldOpinions() }); +export const loadHistoryView = () => loadHistoryOverview(); +export const loadHeritageView = async (code: HeritageCode) => ({ + heritage: await loadHeritage(code) +}); + +export type EconomyViewData = Awaited>; +export type EnergyViewData = Awaited>; +export type GoudaViewData = Awaited>; +export type CountriesViewData = Awaited>; +export type ElectionsViewData = Awaited>; +export type OpinionsViewData = Awaited>; +export type CountryViewData = Awaited>; +export type WorldOpinionsViewData = Awaited>; +export type HistoryViewData = Awaited>; diff --git a/src/lib/server/world-opinions.ts b/src/lib/server/world-opinions.ts index a82388b..f3bcc18 100644 --- a/src/lib/server/world-opinions.ts +++ b/src/lib/server/world-opinions.ts @@ -12,7 +12,7 @@ import type { WorldOpinionsData } from '#lib/world-opinions/data.js'; -// /stats/opinions/world content from stats.wo_* (seeded from the former data.ts). Rebuilds +// /stats/world/politics content from stats.wo_* (seeded from the former data.ts). Rebuilds // the exact shapes the components used, leaving optional keys out when the column is null. export function loadWorldOpinions(): Promise { return cached('world-opinions', async () => { diff --git a/src/lib/stats/CountryPicker.svelte b/src/lib/stats/CountryPicker.svelte index febb921..654f0ba 100644 --- a/src/lib/stats/CountryPicker.svelte +++ b/src/lib/stats/CountryPicker.svelte @@ -1,5 +1,5 @@ + +{#if shown.length && (multi || shown.length > 2)} +
+
+ + + + + {#each codes as c (c)} + + {/each} + + + + {#each shown as m (m.metric.key)} + {@const b = best(m)} + + + {#each codes as c (c)} + {@const v = lastOf(m, c)} + + {/each} + + {/each} + +
Latest {short(c)}
{m.metric.title} {m.metric.unit} + {#if v}{fmt(v.value, m.metric)}{v.period}{:else}–{/if} +
+
+
+{/if} + +{#each groups as g (g.name)} + {#if g.name}

{g.name}

{/if} +
+ {#each g.items as m (m.metric.key)} + l.sourceId)} + {attributions} + note={note(m)} + height={m.metric.bars ? `${Math.max(140, m.lines.length * 34 + 30)}px` : '300px'} + option={(t) => + m.metric.bars + ? barRankOption( + t, + m.lines.map((l) => ({ label: placeName(l.code), value: l.points.at(-1)![1] })), + { decimals: m.metric.decimals ?? 1, unit: m.metric.unit } + ) + : lineOption(t, lines(m), { + unit: m.metric.unit, + decimals: m.metric.decimals ?? 1, + zoom: m.lines.some((l) => l.points.length > 60) + })} + /> + {/each} +
+{/each} + +{#if !shown.length} +

No series in this theme for {codes.map(placeName).join(', ')} yet.

+{:else if empty.length} +

+ Not available for {codes.map(placeName).join(', ')}: {empty + .map((m) => m.metric.title) + .join(', ')}. +

+{/if} + + diff --git a/src/lib/stats/MatrixSelector.svelte b/src/lib/stats/MatrixSelector.svelte new file mode 100644 index 0000000..97fed28 --- /dev/null +++ b/src/lib/stats/MatrixSelector.svelte @@ -0,0 +1,160 @@ + + +
+
+ + {#each main as p (p.slug)} + {p.short}{#if compare && p.slug !== 'core'}{/if} + {/each} + + {#each more as p (p.slug)} + {p.short}{#if compare}{/if} + {/each} +
+ +
+ + diff --git a/src/lib/stats/PlaceHeritage.svelte b/src/lib/stats/PlaceHeritage.svelte index 8a0079a..66dfcb9 100644 --- a/src/lib/stats/PlaceHeritage.svelte +++ b/src/lib/stats/PlaceHeritage.svelte @@ -1,5 +1,5 @@ - - countries · stats · Stan Runge - - -
stats / countries

Countries

@@ -79,7 +73,9 @@ {#each data.rows as r (r.code)} - {FLAG[r.code]}{name(r.code)} diff --git a/src/routes/stats/countries/[iso3]/+page.svelte b/src/lib/stats/views/CountryView.svelte similarity index 98% rename from src/routes/stats/countries/[iso3]/+page.svelte rename to src/lib/stats/views/CountryView.svelte index d74c37f..a5d6f87 100644 --- a/src/routes/stats/countries/[iso3]/+page.svelte +++ b/src/lib/stats/views/CountryView.svelte @@ -1,4 +1,6 @@ - - {name} · countries · stats · Stan Runge - - -
- countries stats / countries / {c} diff --git a/src/routes/stats/economy/+page.svelte b/src/lib/stats/views/EconomyView.svelte similarity index 97% rename from src/routes/stats/economy/+page.svelte rename to src/lib/stats/views/EconomyView.svelte index 35d0279..783c2a3 100644 --- a/src/routes/stats/economy/+page.svelte +++ b/src/lib/stats/views/EconomyView.svelte @@ -1,4 +1,6 @@ - - economy · stats · Stan Runge - - -
stats / economy

Economy

diff --git a/src/routes/stats/elections/+page.svelte b/src/lib/stats/views/ElectionsView.svelte similarity index 53% rename from src/routes/stats/elections/+page.svelte rename to src/lib/stats/views/ElectionsView.svelte index 69548cb..09b2b48 100644 --- a/src/routes/stats/elections/+page.svelte +++ b/src/lib/stats/views/ElectionsView.svelte @@ -1,11 +1,17 @@ - - Elections · stats · Stan Runge - - -
stats / elections

Elections

@@ -213,211 +211,229 @@
- - - - usLatest.dem - ? `R +${num(usLatest.rep - usLatest.dem, 1)}` - : `D +${num(usLatest.dem - usLatest.rep, 1)}` - : '–'} - sub={`R ${num(usLatest?.rep, 1)}% · D ${num(usLatest?.dem, 1)}%`} - color="#e11d48" - /> -
- -

The Netherlands

-
- lineOption(t, familyLines, { category: true, unit: '%', decimals: 1, min: 0 })} - /> - - lineOption(t, [{ name: 'Turnout', points: turnout, color: '#3b82f6', kind: 'area' }], { - category: true, - unit: '%', - decimals: 1, - legend: false, - min: 50 - })} - /> - - barRankOption( - t, - (latestNl?.parties ?? []) - .filter((p) => p.seats) - .map((p) => ({ label: p.party, value: p.seats ?? 0 })), - { decimals: 0, color: '#06b6d4' } + {#if show('nld')} + + {/if} + {#if show('nld')} + + unit="%" + sub={`${compact(latestTurnout?.ballots)} ballots · ${latestTurnout ? year(latestTurnout.date) : ''}`} + color="#3b82f6" + /> + {/if} + {#if show('gouda')} + + {/if} + {#if show('usa')} + usLatest.dem + ? `R +${num(usLatest.rep - usLatest.dem, 1)}` + : `D +${num(usLatest.dem - usLatest.rep, 1)}` + : '–'} + sub={`R ${num(usLatest?.rep, 1)}% · D ${num(usLatest?.dem, 1)}%`} + color="#e11d48" + /> + {/if}
-
- - - - - {#each recent as e (e.id)}{/each} - - - - {#each seatParties as p (p)} - - - {#each recent as e (e.id)}{/each} - - {/each} - -
Seats{year(e.date)}
- {p} - {seatsOf(e, p) ?? ''}
-
- -

Gouda

-{#if goudaLatest} +{#if show('nld')} +

The Netherlands

lineOption(t, goudaVsNl, { category: true, unit: '%', decimals: 1 })} + wide + height="420px" + note="Christian democrats: ARP, CHU, RKSP/KVP and from 1977 the CDA. Labour & green left includes the joint GL-PvdA list (2023, 2025). Populist & radical right: CD, LPF, PVV, FvD, JA21 and BBB. CBS publishes shares to one decimal for 1918–2006; the Kiesraad counts are used from 2010." + option={(t) => lineOption(t, familyLines, { category: true, unit: '%', decimals: 1, min: 0 })} /> + lineOption(t, [{ name: 'Turnout', points: turnout, color: '#3b82f6', kind: 'area' }], { + category: true, + unit: '%', + decimals: 1, + legend: false, + min: 50 + })} + /> + lineOption(t, goudaTrend, { category: true, unit: '%', decimals: 1 })} + option={(t) => + barRankOption( + t, + (latestNl?.parties ?? []) + .filter((p) => p.seats) + .map((p) => ({ label: p.party, value: p.seats ?? 0 })), + { decimals: 0, color: '#06b6d4' } + )} /> - {#each stationCols.slice(0, 2) as p (p)} - - barRankOption(t, stationBars(p), { - decimals: 1, - unit: '%', - color: PARTY_COLORS[p] ?? '#3b82f6' - })} - /> - {/each}
+
- - - - {#each stationCols as p (p)}{/each} + + {#each recent as e (e.id)}{/each} - {#each stationRows as s (s.code)} + {#each seatParties as p (p)} - - - - {#each stationCols as p (p)}{/each} + + {#each recent as e (e.id)}{/each} {/each}
Polling station, TK {year(goudaLatest.date)}VotesLargest{p} %Seats{year(e.date)}
{s.name}{num(s.total, 0)}{s.top}{num(share(s.shares.get(p) ?? 0, s.total), 1)} + {p} + {seatsOf(e, p) ?? ''}
-

- {a.kiesraad ?? ''} · Postal and mobile stations have few votes; votes are counted where they are cast, - not where voters live. -

-{:else} -

Gouda's results are not loaded yet.

{/if} -

United States

-
- - lineOption( - t, - [ - { - name: 'Democrat', - color: '#2563eb', - points: usBy.map((e) => [year(e.date), e.dem] as StatPoint) - }, - { - name: 'Republican', - color: '#dc2626', - points: usBy.map((e) => [year(e.date), e.rep] as StatPoint) - } - ], - { category: true, unit: '%', decimals: 1, min: 30 } - )} - /> - - barRankOption( - t, - states.map((s) => ({ label: s.name, value: s.margin, highlight: Math.abs(s.margin) < 3 })), - { decimals: 1, unit: 'pp', color: '#64748b' } - )} - /> -
+{#if show('gouda')} +

Gouda

+ {#if goudaLatest} +
+ lineOption(t, goudaVsNl, { category: true, unit: '%', decimals: 1 })} + /> + lineOption(t, goudaTrend, { category: true, unit: '%', decimals: 1 })} + /> + {#each stationCols.slice(0, 2) as p (p)} + + barRankOption(t, stationBars(p), { + decimals: 1, + unit: '%', + color: PARTY_COLORS[p] ?? '#3b82f6' + })} + /> + {/each} +
+
+ + + + + + + {#each stationCols as p (p)}{/each} + + + + {#each stationRows as s (s.code)} + + + + + {#each stationCols as p (p)}{/each} + + {/each} + +
Polling station, TK {year(goudaLatest.date)}VotesLargest{p} %
{s.name}{num(s.total, 0)}{s.top}{num(share(s.shares.get(p) ?? 0, s.total), 1)}
+
+

+ {a.kiesraad ?? ''} · Postal and mobile stations have few votes; votes are counted where they are + cast, not where voters live. +

+ {:else} +

Gouda's results are not loaded yet.

+ {/if} +{/if} + +{#if show('usa')} +

United States

+
+ + lineOption( + t, + [ + { + name: 'Democrat', + color: '#2563eb', + points: usBy.map((e) => [year(e.date), e.dem] as StatPoint) + }, + { + name: 'Republican', + color: '#dc2626', + points: usBy.map((e) => [year(e.date), e.rep] as StatPoint) + } + ], + { category: true, unit: '%', decimals: 1, min: 30 } + )} + /> + + barRankOption( + t, + states.map((s) => ({ + label: s.name, + value: s.margin, + highlight: Math.abs(s.margin) < 3 + })), + { decimals: 1, unit: 'pp', color: '#64748b' } + )} + /> +
+{/if} diff --git a/src/routes/stats/countries/+page.server.ts b/src/routes/stats/countries/+page.server.ts deleted file mode 100644 index 829f1c4..0000000 --- a/src/routes/stats/countries/+page.server.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { cached, loadLatest, loadSeries } from '#lib/server/stats.js'; -import { COMPARE_COLUMNS, COUNTRY_PAGES, seriesIds } from '#lib/stats/countries.js'; -import type { PageServerLoad } from './$types'; - -export const load: PageServerLoad = async () => - cached('stats:countries', async () => { - const ids = COUNTRY_PAGES.flatMap((c) => - COMPARE_COLUMNS.flatMap((col) => seriesIds(c, col.ids)) - ); - const [latest, series] = await Promise.all([ - loadLatest(ids), - loadSeries( - COUNTRY_PAGES.flatMap((c) => [ - { id: `wb:NY.GDP.PCAP.PP.KD:${c}` }, - { id: `wb:SP.DYN.LE00.IN:${c}`, from: '1960-01-01' } - ]) - ) - ]); - const rows = COUNTRY_PAGES.map((c) => ({ - code: c, - values: Object.fromEntries( - COMPARE_COLUMNS.map((col) => { - const id = seriesIds(c, col.ids).find((i) => latest[i]); - return [col.key, id ? { ...latest[id], source: id.split(':')[0] } : null]; - }) - ) - })); - return { rows, series }; - }); diff --git a/src/routes/stats/countries/[iso3]/+page.server.ts b/src/routes/stats/countries/[iso3]/+page.server.ts deleted file mode 100644 index ce7bd16..0000000 --- a/src/routes/stats/countries/[iso3]/+page.server.ts +++ /dev/null @@ -1,29 +0,0 @@ -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 }) => { - const code = params.iso3.toUpperCase(); - 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}`), - // Benchmarks for the "next to the Netherlands and the world" lines. - ...['NLD', 'WLD'].flatMap((c) => [ - `wb:NY.GDP.PCAP.PP.KD:${c}`, - `wb:SP.DYN.LE00.IN:${c}`, - `wb:FP.CPI.TOTL.ZG:${c}` - ]), - // §8: Curaçao detail from CBS Curaçao and the international APIs (ids carry no code suffix). - ...(code === 'CUW' ? CW_SERIES : []) - ]) - ); - return { code, series, heritage: heritage ? await heritage : null }; -}; diff --git a/src/routes/stats/economy/+page.server.ts b/src/routes/stats/economy/+page.server.ts deleted file mode 100644 index 6a879f3..0000000 --- a/src/routes/stats/economy/+page.server.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { cached, loadSeries } from '#lib/server/stats.js'; -import { - ANNUAL, - CPI_BY_CATEGORY, - CPI_TOTAL, - ECON_COUNTRIES, - POLICY_RATE -} from '#lib/stats/econ.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']; -const REGIONS = [...ECON_COUNTRIES, 'EU27', 'EA']; - -export const load: PageServerLoad = async () => ({ - series: await cached('stats:economy', () => - loadSeries([ - ...[...new Set(Object.values(POLICY_RATE))].map((a) => ({ - id: `bis:M:${a}`, - from: '1990-01-01' - })), - ...EU.map((g) => `eurostat:prc_hicp_minr:RCH_A.TOTAL.${g}`), - // CPI index levels (all items, then the categories) - ...[ - ...new Set([ - ...Object.values(CPI_TOTAL).map((c) => c.id), - ...Object.values(CPI_BY_CATEGORY).flatMap((c) => Object.values(c.ids)) - ]) - ].map((id) => ({ id, from: '1960-01-01' })), - ...REGIONS.map((c) => ({ id: `oecd:irlt:${c}`, from: '1980-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' }, - ...['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' }, - ...Object.values(ANNUAL).flatMap((ind) => - REGIONS.flatMap((c) => [ - ...(ind.imf ? [{ id: `imf:${ind.imf}:${c}`, from: '1980-01-01' }] : []), - { id: `wb:${ind.wb}:${c}`, from: '1980-01-01' } - ]) - ), - ...Object.values(ANNUAL).flatMap((ind) => (ind.cw ? [ind.cw] : [])), - '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/elections/+page.server.ts b/src/routes/stats/elections/+page.server.ts deleted file mode 100644 index 6065879..0000000 --- a/src/routes/stats/elections/+page.server.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { cached } from '#lib/server/stats.js'; -import { client } from '#lib/server/db/index.js'; -import type { PageServerLoad } from './$types'; - -export type ElecRow = { - election: string; - date: string; - party: string; - votes: number | null; - seats: number | null; -}; - -const rows = (r: readonly Record[]): ElecRow[] => - r.map((x) => ({ - election: x.election as string, - date: x.date as string, - party: x.party as string, - votes: x.votes as number | null, - seats: (x.seats as number | null) ?? null - })); - -export const load: PageServerLoad = async () => - cached('stats:elections', async () => { - const [elections, nl, turnout, gouda, stations, us, usStates] = await Promise.all([ - client`select election_id, country, date::text as date, name, total_seats from stats.elec_election order by date`, - client`select r.election_id as election, e.date::text as date, r.party, r.votes::float8 as votes, r.seats - from stats.elec_result r join stats.elec_election e using (election_id) - where e.country = 'NLD' and r.level = 'national'`, - client`select t.election_id as election, e.date::text as date, t.electorate::float8 as electorate, - t.ballots::float8 as ballots, t.valid::float8 as valid - from stats.elec_turnout t join stats.elec_election e using (election_id) - where t.level = 'national' order by e.date`, - client`select r.election_id as election, e.date::text as date, r.party, r.votes::float8 as votes - from stats.elec_result r join stats.elec_election e using (election_id) - where r.level = 'muni' and r.area_code = 'GM0513'`, - client`select r.area_code, r.area_name, r.party, r.votes::float8 as votes - from stats.elec_result r - where r.level = 'station' and r.election_id = ( - select max(election_id) from stats.elec_result where level = 'station')`, - client`select r.election_id as election, e.date::text as date, r.party, r.votes::float8 as votes - from stats.elec_result r join stats.elec_election e using (election_id) - where e.country = 'USA' and r.level = 'national'`, - client`select r.area_code, r.area_name, r.party, r.votes::float8 as votes - from stats.elec_result r - where r.level = 'state' and r.election_id = ( - select max(election_id) from stats.elec_election where country = 'USA')` - ]); - const stationElection = ( - await client`select max(election_id) as id from stats.elec_result where level = 'station'` - )[0]?.id as string | null; - return { - elections: elections.map((e) => ({ - id: e.election_id as string, - country: e.country as string, - date: e.date as string, - name: e.name as string, - seats: e.total_seats as number | null - })), - nl: rows(nl), - turnout: turnout.map((t) => ({ - election: t.election as string, - date: t.date as string, - electorate: t.electorate as number | null, - ballots: t.ballots as number | null, - valid: t.valid as number | null - })), - gouda: rows(gouda), - stationElection, - stations: stations.map((s) => ({ - code: s.area_code as string, - name: s.area_name as string, - party: s.party as string, - votes: s.votes as number | null - })), - us: rows(us), - usStates: usStates.map((s) => ({ - code: s.area_code as string, - name: s.area_name as string, - party: s.party as string, - votes: s.votes as number | null - })) - }; - }); diff --git a/src/routes/stats/energy/+page.server.ts b/src/routes/stats/energy/+page.server.ts deleted file mode 100644 index 42ae5f3..0000000 --- a/src/routes/stats/energy/+page.server.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { cached, loadSeries } from '#lib/server/stats.js'; -import { loadEnergy } from '#lib/server/energy.js'; -import { epSeriesIds } from '#lib/stats/energylabels.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 () => ({ - // Live NED grid + gas storage (read-only, ned-collector's `energy` schema). - energy: await cached('stats:energy:live', loadEnergy), - 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_pc_204:E7000.KWH3500.KWH.I_TAX.EUR.${g}`, - `eurostat:nrg_pc_202:G3000.GJ83P70.GJ_GCV.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', - ...epSeriesIds(['NL', 'GOUDA']) - ]) - ) -}); diff --git a/src/routes/stats/gouda/+page.server.ts b/src/routes/stats/gouda/+page.server.ts deleted file mode 100644 index 01338db..0000000 --- a/src/routes/stats/gouda/+page.server.ts +++ /dev/null @@ -1,48 +0,0 @@ -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. -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, heritage] = 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', - ...epSeriesIds(['NL', 'GOUDA', ...GOUDA_PC4]) - ]) - ), - cached('stats:gouda:kwb', () => - loadCbsCross(KWB_TABLE, KWB_MEASURES, 'WijkenEnBuurten', ['WK0513', 'GM0513', 'NL00']) - ), - loadHeritage('GM0513') - ]); - return { series, wijken, kwbYear: 2024, heritage }; -}; diff --git a/src/routes/stats/history/+page.server.ts b/src/routes/stats/history/+page.server.ts deleted file mode 100644 index ebe20f5..0000000 --- a/src/routes/stats/history/+page.server.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { loadHistoryOverview } from '#lib/server/stats-history.js'; -import type { PageServerLoad } from './$types'; - -export const load: PageServerLoad = async () => loadHistoryOverview(); diff --git a/src/routes/stats/opinions/+page.server.ts b/src/routes/stats/opinions/+page.server.ts deleted file mode 100644 index a9c8d67..0000000 --- a/src/routes/stats/opinions/+page.server.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { cached } from '#lib/server/stats.js'; -import { client } from '#lib/server/db/index.js'; -import { ebLabel, latestPolls, pollAverages, type Poll } from '#lib/stats/opinions.js'; - -// Averages are computed here so the page gets a few hundred points per series, not every poll row. -function pollData(polls: Poll[]) { - const noRaw = (s: ReturnType) => s.map((o) => ({ ...o, raw: [] })); - return { - nl: pollAverages(polls, 'nl-tk-seats', 5), - nlLatest: latestPolls(polls, 'nl-tk-seats', 8), - us: pollAverages(polls, 'us-trump-approval', 10), - usAgg: latestPolls(polls, 'us-trump-approval-aggregate', 20), - kr: noRaw(pollAverages(polls, 'kr-lee-approval', 10)), - krParty: noRaw(pollAverages(polls, 'kr-party', 10)) - }; -} -import type { PageServerLoad } from './$types'; - -export const load: PageServerLoad = async () => - cached('stats:opinions', async () => { - const [polls, eb, whr, trends, gdelt, ches, vparty] = await Promise.all([ - client`select series, poll_key, fieldwork_end::text as date, pollster, sample, option, value - from stats.op_poll where value is not null order by fieldwork_end`, - client`select wave, fieldwork_end::text as date, topic, item, country, share - from stats.eb_item where country in ('NL', 'EU27') and share is not null`, - client`select year, country, iso3, rank, ladder, gdp, social, health, freedom, generosity, corruption - from stats.whr_score order by year`, - client`select geo, date::text as date, rank, query, approx_traffic - from stats.gt_trending t where date = (select max(date) from stats.gt_trending where geo = t.geo) - order by geo, rank`, - client`select date::text as date, country, theme, vol_pct from stats.gdelt_theme_vol - where date > now() - interval '45 days' order by date`, - client`select p.party_id, coalesce(p.abbrev, p.name) as label, p.name, p.family, pp.year, pp.dim, - pp.value, pp.vote_share - from stats.wo_party_position pp join stats.wo_party p using (party_id) - where p.iso3 = 'NLD' and pp.source = 'ches' - and pp.year = (select max(year) from stats.wo_party_position where source = 'ches')`, - client`select p.party_id, coalesce(p.abbrev, p.name) as label, pp.year, pp.value, pp.vote_share - from stats.wo_party_position pp join stats.wo_party p using (party_id) - where p.iso3 = 'NLD' and pp.source = 'vparty' and pp.dim = 'v2xpa_popul' and pp.year >= 1946 - order by pp.year` - ]); - return { - ...pollData( - polls.map((p): Poll => ({ - series: p.series as string, - key: p.poll_key as string, - date: p.date as string, - pollster: p.pollster as string, - sample: p.sample as number | null, - option: p.option as string, - value: Number(p.value) - })) - ), - eb: eb.map((r) => ({ - wave: r.wave as string, - date: r.date as string, - topic: r.topic as string, - item: ebLabel(r.topic as string, r.item as string), - country: r.country as string, - share: Math.round(Number(r.share) * 1000) / 10 - })), - whr: whr.map((r) => ({ - year: r.year as number, - country: r.country as string, - iso3: r.iso3 as string | null, - rank: r.rank as number | null, - ladder: r.ladder == null ? null : Number(r.ladder) - })), - trends: trends.map((r) => ({ - geo: r.geo as string, - date: r.date as string, - rank: r.rank as number, - query: r.query as string, - traffic: r.approx_traffic as number | null - })), - gdelt: gdelt.map((r) => ({ - date: r.date as string, - country: (r.country as string).trim(), - theme: r.theme as string, - vol: Number(r.vol_pct) - })), - ches: ches.map((r) => ({ - id: r.party_id as string, - label: r.label as string, - name: r.name as string | null, - family: r.family as string | null, - year: r.year as number, - dim: r.dim as string, - value: Number(r.value), - share: r.vote_share == null ? null : Number(r.vote_share) - })), - vparty: vparty.map((r) => ({ - id: r.party_id as string, - label: r.label as string, - year: r.year as number, - value: Number(r.value), - share: r.vote_share == null ? null : Number(r.vote_share) - })) - }; - }); diff --git a/src/routes/stats/opinions/world/+page.server.ts b/src/routes/stats/opinions/world/+page.server.ts deleted file mode 100644 index c84e05d..0000000 --- a/src/routes/stats/opinions/world/+page.server.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { loadWorldOpinions } from '#lib/server/world-opinions.js'; -import type { PageServerLoad } from './$types'; - -export const load: PageServerLoad = async () => ({ wo: await loadWorldOpinions() });