Files
website/src/lib/energy/EnergyChart.svelte
Stan Runge 0cf5d44062 Add energy tracker (/projects/energy)
Public, read-only page over the ned-collector energy schema: latest-hour
mix and per-source utilization, generation vs consumption with CO2
intensity (day/week/month/12 months/all years), NED forecast, gas storage
by year and per site, and gas-fired power share vs storage fill.
Incomplete hours (sources not summing to the NED total) are left out and
partial days/months/years are marked.
2026-10-23 23:11:27 +00:00

105 lines
2.5 KiB
Svelte

<script lang="ts" module>
export type ChartTheme = {
text: string;
muted: string;
border: string;
surface: string;
dark: boolean;
};
</script>
<script lang="ts">
import { onMount } from 'svelte';
import type { ECharts, EChartsCoreOption } from 'echarts/core';
// Generic ECharts wrapper for the energy tracker: same lazy-loading and theme handling as
// the CS price chart, but the caller builds the option from the current theme colours.
let {
option,
label,
height = 'min(52vh, 440px)'
}: {
option: (theme: ChartTheme) => EChartsCoreOption;
label: string;
height?: string;
} = $props();
let element: HTMLDivElement;
let chart: ECharts | undefined;
let ready = $state(false);
function update() {
if (!chart) return;
const style = getComputedStyle(document.documentElement);
const read = (name: string) => style.getPropertyValue(name).trim();
chart.setOption(
option({
text: read('--site-text'),
muted: read('--site-muted'),
border: read('--site-border'),
surface: read('--site-surface'),
dark: read('--site-bg') === '#000'
}),
true
);
}
onMount(() => {
let disposed = false;
let resize: ResizeObserver | undefined;
let theme: MutationObserver | undefined;
import('echarts/core').then(async (echarts) => {
const [
{ LineChart, BarChart, PieChart },
{ GridComponent, TooltipComponent, LegendComponent, DataZoomComponent, MarkAreaComponent },
{ CanvasRenderer }
] = await Promise.all([
import('echarts/charts'),
import('echarts/components'),
import('echarts/renderers')
]);
if (disposed) return;
echarts.use([
LineChart,
BarChart,
PieChart,
GridComponent,
TooltipComponent,
LegendComponent,
DataZoomComponent,
MarkAreaComponent,
CanvasRenderer
]);
chart = echarts.init(element, undefined, { renderer: 'canvas' });
ready = true;
update();
resize = new ResizeObserver(() => chart?.resize());
resize.observe(element);
theme = new MutationObserver(update);
theme.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme']
});
});
return () => {
disposed = true;
resize?.disconnect();
theme?.disconnect();
chart?.dispose();
};
});
$effect(() => {
// Re-render when the option builder (and therefore its inputs) changes.
void option;
if (ready) update();
});
</script>
<div class="chart" style:height bind:this={element} role="img" aria-label={label}></div>
<style>
.chart {
width: 100%;
min-height: 260px;
}
</style>