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.
This commit is contained in:
104
src/lib/energy/EnergyChart.svelte
Normal file
104
src/lib/energy/EnergyChart.svelte
Normal file
@@ -0,0 +1,104 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user