diff --git a/src/lib/energy/EnergyChart.svelte b/src/lib/energy/EnergyChart.svelte new file mode 100644 index 0000000..35b4639 --- /dev/null +++ b/src/lib/energy/EnergyChart.svelte @@ -0,0 +1,104 @@ + + + + +
+ + diff --git a/src/lib/energy/data.test.ts b/src/lib/energy/data.test.ts new file mode 100644 index 0000000..f2086d2 --- /dev/null +++ b/src/lib/energy/data.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { fillGaps, gaps, latestSnapshot, seasonalTime, toBuckets, type EnergyRow } from './data'; + +const hour = 3_600_000; +const rows: EnergyRow[] = [ + { t: 0, type: 'ElectricityMix', value: 100, ef: 0.2 }, + { t: 0, type: 'ElectricityLoad', value: 90 }, + { t: 0, type: 'Wind', value: 60, pct: 0.5 }, + { t: 0, type: 'FossilGasPower', value: 30, pct: 0.1 }, + { t: 0, type: 'Nuclear', value: 10, pct: 1 }, + { t: hour, type: 'ElectricityMix', value: 100 }, + { t: hour, type: 'Wind', value: 20 } +]; + +describe('energy buckets', () => { + it('flags buckets whose sources do not add up to the total', () => { + const buckets = toBuckets(rows); + expect(buckets.map((bucket) => bucket.incomplete)).toEqual([false, true]); + expect(buckets[0].sum).toBe(100); + expect(buckets[0].load).toBe(90); + }); + + it('summarises the latest complete bucket', () => { + const snapshot = latestSnapshot(toBuckets(rows))!; + expect(snapshot.t).toBe(0); + expect(snapshot.renewableShare).toBeCloseTo(0.6); + expect(snapshot.lowCarbonShare).toBeCloseTo(0.7); + expect(snapshot.fossilShare).toBeCloseTo(0.3); + expect(snapshot.sources[0]).toMatchObject({ type: 'Wind', share: 0.6, utilization: 0.5 }); + }); + + it('finds and fills missing hours', () => { + expect(gaps([0, hour, 4 * hour])).toEqual([{ from: 2 * hour, to: 4 * hour, hours: 2 }]); + const filled = fillGaps(toBuckets([...rows, { t: 3 * hour, type: 'Wind', value: 1 }]), hour); + expect(filled.map((bucket) => 'missing' in bucket)).toEqual([false, false, true, false]); + }); + + it('maps dates onto one seasonal year', () => { + expect(seasonalTime('2023-02-28')).toBe(Date.UTC(2000, 1, 28)); + expect(seasonalTime('2024-12-31')).toBe(Date.UTC(2000, 11, 31)); + }); +}); diff --git a/src/lib/energy/data.ts b/src/lib/energy/data.ts new file mode 100644 index 0000000..ca94dd1 --- /dev/null +++ b/src/lib/energy/data.ts @@ -0,0 +1,195 @@ +// Pure helpers for the energy tracker. Inputs come from the ned-collector tables in the +// `energy` schema (NED.nl electricity data and AGSI/GIE gas storage). + +export type SourceType = + | 'Nuclear' + | 'WindOffshoreC' + | 'Wind' + | 'Solar' + | 'BiomassPower' + | 'WastePower' + | 'WKK Total' + | 'FossilGasPower' + | 'FossilHardCoal' + | 'OtherPower'; + +export type Source = { + type: SourceType; + label: string; + color: string; + renewable: boolean; + fossil: boolean; +}; + +// Stacking order, bottom to top: steady baseload first, weather-driven next, fossil on top. +export const SOURCES: readonly Source[] = [ + { type: 'Nuclear', label: 'Nuclear', color: '#a78bfa', renewable: false, fossil: false }, + { + type: 'WindOffshoreC', + label: 'Wind offshore', + color: '#0284c7', + renewable: true, + fossil: false + }, + { type: 'Wind', label: 'Wind onshore', color: '#38bdf8', renewable: true, fossil: false }, + { type: 'Solar', label: 'Solar', color: '#facc15', renewable: true, fossil: false }, + { type: 'BiomassPower', label: 'Biomass', color: '#65a30d', renewable: true, fossil: false }, + { type: 'WastePower', label: 'Waste', color: '#a3a3a3', renewable: false, fossil: false }, + { type: 'WKK Total', label: 'CHP (WKK)', color: '#fdba74', renewable: false, fossil: true }, + { type: 'FossilGasPower', label: 'Fossil gas', color: '#f97316', renewable: false, fossil: true }, + { type: 'FossilHardCoal', label: 'Hard coal', color: '#57534e', renewable: false, fossil: true }, + { type: 'OtherPower', label: 'Other', color: '#94a3b8', renewable: false, fossil: false } +]; + +export const MIX = 'ElectricityMix'; +export const LOAD = 'ElectricityLoad'; +const sourceTypes = new Setgrid archive / netherlands
++ Where Dutch electricity comes from hour by hour, how hard each source is running, and how + full the gas storages are. Data from NED.nl, collected hourly. +
++ NED's load factor per source. Nuclear and "other" are reported near 100% almost + constantly, so treat those as estimates. +
++ Stacked: generation by source. Dashed: consumption (gap above it is export). Red: grid CO₂ + intensity. * = incomplete (still running or hours missing). + {#if range === 'month' && daily.length < 28} + Hourly collection started {formatDate.format(energy.coverage.firstHour ?? now.t)}, so this + fills up over the coming weeks. + {/if} +
++ NED has no gas consumption series here, so this compares how much electricity is made from + gas with how full storage is. +
+