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

@@ -0,0 +1,88 @@
// EP-Online energy labels on /stats/energy and /stats/gouda (collector: scripts/stats/eponline.ts).
import type { ChartTheme, StatPoint } from './chart';
import type { EChartsCoreOption } from 'echarts/core';
/** Best to worst (NTA 8800 scale). */
export const EP_LABELS = ['A++++', 'A+++', 'A++', 'A+', 'A', 'B', 'C', 'D', 'E', 'F', 'G'];
/** Dark green → red, like the label sticker. */
export const EP_COLORS = [
'#00632f',
'#00733a',
'#008a44',
'#00a24f',
'#22b14c',
'#8cc63e',
'#fff200',
'#fdb913',
'#f37021',
'#ed1c24',
'#b5121b'
];
export const GOUDA_PC4 = Array.from({ length: 10 }, (_, i) => String(2800 + i));
export const epShareId = (label: string, area: string) => `eponline:share:${label}:${area}`;
export const epHomesId = (area: string) => `eponline:homes:${area}`;
export const epSeriesIds = (areas: string[]) =>
areas.flatMap((a) => [epHomesId(a), ...EP_LABELS.map((l) => epShareId(l, a))]);
/** Share of homes with label A or better per snapshot. */
export function aOrBetter(get: (id: string) => StatPoint[], area: string): StatPoint[] {
const sum = new Map<string, number>();
for (const l of EP_LABELS.slice(0, 5))
for (const [p, v] of get(epShareId(l, area))) sum.set(p, (sum.get(p) ?? 0) + v);
return [...sum].sort((a, b) => a[0].localeCompare(b[0]));
}
/** Horizontal 100% stacked bars: one row per area, one segment per label (latest snapshot). */
export function labelStackOption(
theme: ChartTheme,
rows: { name: string; shares: number[] }[]
): EChartsCoreOption {
const pct = new Intl.NumberFormat('en-GB', { maximumFractionDigits: 1 });
return {
animation: false,
grid: { left: 8, right: 16, top: 36, bottom: 8, containLabel: true },
legend: {
top: 0,
itemWidth: 12,
itemHeight: 10,
textStyle: { color: theme.muted, fontSize: 11 }
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
backgroundColor: theme.surface,
borderColor: theme.border,
textStyle: { color: theme.text, fontSize: 12 },
valueFormatter: (v: number) => `${pct.format(v)}%`
},
xAxis: {
type: 'value',
max: 100,
axisLabel: { color: theme.muted, fontSize: 11, formatter: '{value}%' },
splitLine: { lineStyle: { color: theme.border, opacity: 0.6 } }
},
yAxis: {
type: 'category',
inverse: true,
data: rows.map((r) => r.name),
axisLabel: { color: theme.text, fontSize: 11 },
axisLine: { show: false },
axisTick: { show: false }
},
series: EP_LABELS.map((label, i) => ({
name: label,
type: 'bar',
stack: 'labels',
barMaxWidth: 22,
itemStyle: { color: EP_COLORS[i] },
data: rows.map((r) => r.shares[i] ?? 0),
label: {
show: true,
color: i >= 6 && i <= 8 ? '#111' : '#fff',
fontSize: 10,
formatter: (p: { value: number }) => (p.value >= 6 ? `${pct.format(p.value)}` : '')
}
}))
};
}