stats B11: EP-Online energy labels (monthly total file → homes per label for NL, Gouda, Gouda PC4s; aggregates only), stats.ep_label_agg, monthly timer with key from ~/.config/stats/ep-online.env, charts on /stats/energy and /stats/gouda
Some checks failed
Deploy production / deploy (push) Has been cancelled

This commit is contained in:
2026-10-25 21:28:37 +00:00
parent 2a4466cb82
commit fa58e5c661
15 changed files with 33573 additions and 6 deletions

View File

@@ -1,4 +1,5 @@
import { cached, loadSeries } from '#lib/server/stats.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'];
@@ -54,7 +55,8 @@ export const load: PageServerLoad = async () => ({
'cbs:cars:petrol',
'cbs:cars:diesel',
'eurostat:road_eqr_carpda:NR.ELC.NL',
'eurostat:road_eqr_carpda:NR.TOTAL.NL'
'eurostat:road_eqr_carpda:NR.TOTAL.NL',
...epSeriesIds(['NL', 'GOUDA'])
])
)
});

View File

@@ -5,6 +5,13 @@
import { barRankOption, latest, lineOption, type StatPoint } from '#lib/stats/chart.js';
import { compact, num, period } from '#lib/stats/format.js';
import { COUNTRY } from '#lib/stats/regions.js';
import {
aOrBetter,
epHomesId,
epShareId,
EP_LABELS,
labelStackOption
} from '#lib/stats/energylabels.js';
import Zap from 'lucide-svelte/icons/zap';
import ArrowRight from 'lucide-svelte/icons/arrow-right';
@@ -13,6 +20,11 @@
const a = $derived(data.attributions);
const pts = (id: string): StatPoint[] => s[id]?.points ?? [];
const last = (id: string) => latest(s[id]?.points);
// EP-Online: share of homes per energy label, latest monthly snapshot.
const epShares = (area: string) => EP_LABELS.map((l) => last(epShareId(l, area))?.[1] ?? 0);
const epHomes = $derived(last(epHomesId('NL')));
const epAorBetter = $derived(aOrBetter(pts, 'NL'));
const epGoudaAorBetter = $derived(aOrBetter(pts, 'GOUDA'));
const EU = ['NL', 'DE', 'FR', 'BE', 'IT', 'ES', 'PL', 'SE', 'DK', 'AT', 'EU27_2020'];
/** a ÷ b × k on shared dates. */
@@ -592,6 +604,43 @@
/>
</div>
{#if epHomes}
<h2 class="stats-section-title">Energy labels of homes</h2>
<div class="stats-grid">
<StatChart
title="Homes by energy label"
subtitle={`% of homes with a registered label · ${num(epHomes[1], 0)} homes in NL · ${period(epHomes[0], 'M')}`}
sources={['eponline']}
attributions={a}
wide
height="220px"
note="Latest registered label per home (BAG dwelling) in EP-Online's monthly total file; labels on building-permit designs are left out. Gouda = postcodes 2800–2809."
option={(t) =>
labelStackOption(t, [
{ name: 'Netherlands', shares: epShares('NL') },
{ name: 'Gouda', shares: epShares('GOUDA') }
])}
/>
{#if epAorBetter.length > 1}
<StatChart
title="Homes labelled A or better"
subtitle="% of homes with a registered label, monthly"
sources={['eponline']}
attributions={a}
option={(t) =>
lineOption(
t,
[
{ name: 'Netherlands', points: epAorBetter, color: '#0f172a', dashed: true },
{ name: 'Gouda', points: epGoudaAorBetter, color: '#f97316' }
],
{ unit: '%', decimals: 1 }
)}
/>
{/if}
</div>
{/if}
<style>
.live-link {
display: flex;

View File

@@ -1,4 +1,5 @@
import { cached, loadCbsCross, loadSeries } from '#lib/server/stats.js';
import { epSeriesIds, GOUDA_PC4 } from '#lib/stats/energylabels.js';
import type { PageServerLoad } from './$types';
// Gouda (GM0513) and its neighbours; NL01 is the national total.
@@ -33,7 +34,8 @@ export const load: PageServerLoad = async () => {
(k) => `cbs:pop:${k}:GM0513`
),
'cbs:house:hpi:cr28',
'cbs:house:hpi:nl'
'cbs:house:hpi:nl',
...epSeriesIds(['NL', 'GOUDA', ...GOUDA_PC4])
])
),
cached('stats:gouda:kwb', () =>

View File

@@ -3,12 +3,24 @@
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';
import { epShareId, EP_LABELS, GOUDA_PC4, labelStackOption } from '#lib/stats/energylabels.js';
let { data } = $props();
const s = $derived(data.series);
const a = $derived(data.attributions);
const pts = (id: string): StatPoint[] => s[id]?.points ?? [];
const last = (id: string) => latest(s[id]?.points);
// EP-Online energy labels per Gouda postcode (PC4), latest monthly snapshot.
const epShares = (area: string) => EP_LABELS.map((l) => last(epShareId(l, area))?.[1] ?? 0);
const epPeriod = $derived(last(epShareId('A', 'GOUDA'))?.[0]);
const epRows = $derived([
{ name: 'Netherlands', shares: epShares('NL') },
{ name: 'Gouda (2800–2809)', shares: epShares('GOUDA') },
...GOUDA_PC4.filter((p) => s[epShareId('A', p)]?.points.length).map((p) => ({
name: p,
shares: epShares(p)
}))
]);
const NAMES: Record<string, string> = {
NL01: 'Netherlands',
@@ -336,6 +348,18 @@
{ unit: '€', decimals: 0 }
)}
/>
{#if epPeriod}
<StatChart
title="Energy labels by postcode"
subtitle={`% of homes with a registered label · ${period(epPeriod, 'M')}`}
sources={['eponline']}
attributions={a}
wide
height="440px"
note="Latest registered label per home in EP-Online's monthly total file (permit-stage labels left out). 2809 is mostly new-build (Westergouwe)."
option={(t) => labelStackOption(t, epRows)}
/>
{/if}
</div>
<h2 class="stats-section-title">Neighbourhoods ({data.kwbYear})</h2>