remove echarts, use canvas

This commit is contained in:
Vincent van der Wal
2026-07-19 15:25:04 +02:00
parent 6d81af8df5
commit e031716ce6
37 changed files with 1483 additions and 2510 deletions
+83 -105
View File
@@ -1,32 +1,24 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { onMount } from 'svelte';
import { storedLocation } from '$lib/stores/settings';
import {
buildAverageSeries,
buildCurrentTimeSeries,
buildDaylightSeries,
buildSpreadSeries,
composeChartOption,
getThemeColors
} from '$lib/utils/echarts';
import { ChartContainer, ChartToolbar, EChart } from '$lib/components/charts';
import '$lib/components/charts/echarts.css';
import { ChartContainer, ChartToolbar } from '$lib/components/charts';
import { Label } from '$lib/components/ui/label';
import { Switch } from '$lib/components/ui/switch';
import { CHART_COLORS, CanvasChart, type ChartSeries, isColumnUnit } from '$lib/charts';
import {
type DaylightBand,
type EnsembleForecastResult,
type MarkArea,
fetchEnsembleForecast
} from '$lib/services/weather';
import { defaultParameters } from '../../options';
import type { PageData } from './$types';
import type * as echarts from 'echarts';
const CHART_GROUP = '14-day-ensemble';
// ─── Display State (does NOT trigger data re-fetch) ─────────────────────────
@@ -34,9 +26,7 @@
// ─── Data Fetch State ───────────────────────────────────────────────────────
let chartComponents: EChart[] = $state([]);
let chartInstances: echarts.ECharts[] = $state([]);
let chartOptions: Array<Record<string, unknown>> = $state([]);
let chartComponents: CanvasChart[] = $state([]);
let mounted = $state(false);
let loading = $state(true);
let loadError = $state<string | null>(null);
@@ -64,7 +54,7 @@
ensembleResult: EnsembleForecastResult;
timestamps: number[];
timezone: string;
markAreas: MarkArea[];
daylightBands: DaylightBand[];
}
let fetchedData: FetchedData | null = $state(null);
@@ -75,22 +65,8 @@
mounted = true;
});
onDestroy(() => {
chartInstances = [];
chartOptions = [];
chartComponents = [];
});
// ─── Chart Instance Tracking ────────────────────────────────────────────────
function handleChartReady(chart: echarts.ECharts): void {
chartInstances = [...chartInstances, chart];
}
// chart components persist across refetches (options update in place), so
// the registry is never reset; only instances disposed because the chart
// count shrank are filtered out
let liveCharts = $derived(chartInstances.filter((chart) => !chart.isDisposed()));
// components persist across refetches; entries are null while unmounted
let liveCharts = $derived(chartComponents.filter((chart) => chart != null));
// ─── Data Fetching (only when params.hourly or params.models change) ───────
@@ -125,7 +101,7 @@
ensembleResult: result,
timestamps: result.timestamps,
timezone: result.timezone,
markAreas: result.markAreas
daylightBands: result.daylightBands
};
loading = false;
@@ -137,77 +113,74 @@
});
});
// ─── Chart Option Building (runs when fetchedData OR display toggles change) ─
// ─── Chart Building (runs when fetchedData or the variable list changes) ────
$effect(() => {
if (!fetchedData) return;
// Timestamps from the service are in milliseconds; CanvasChart uses seconds.
let timestampsSec = $derived.by(() =>
fetchedData ? fetchedData.timestamps.map((t) => t / 1000) : []
);
const { ensembleResult, timestamps, timezone, markAreas } = fetchedData;
const _showLegend = showLegend;
interface ChartDef {
title?: string;
subtitle?: string;
unit: string;
showCredit: boolean;
series: ChartSeries[];
}
const colors = getThemeColors();
const variableCount = params.hourly?.length || 0;
const newOptions: Array<Record<string, unknown>> = [];
let chartDefs = $derived.by((): ChartDef[] => {
if (!fetchedData) return [];
for (let vi = 0; vi < variableCount; vi++) {
const variable = params.hourly![vi];
const { ensembleResult } = fetchedData;
const variables = params.hourly || [];
const defs: ChartDef[] = [];
for (let vi = 0; vi < variables.length; vi++) {
const variable = variables[vi];
const varData = ensembleResult.variables[variable];
if (!varData) continue;
const unit = varData.unit;
const { average, min: minValues, max: maxValues } = varData;
const isColumn = isColumnUnit(unit);
const series: ChartSeries[] = [];
const series: Array<Record<string, unknown>> = [];
const spreadData: Array<[number, number, number]> = minValues.map(
(minVal, index) =>
[timestamps[index], minVal ?? 0, maxValues[index] ?? 0] as [number, number, number]
);
series.push(...buildSpreadSeries({ variable, spreadData }));
const averageData = average.map((val, idx) => [timestamps[idx], val] as [number, number]);
series.push(buildAverageSeries({ variable, data: averageData, unit }));
series.push(buildCurrentTimeSeries());
const daylightSeries = buildDaylightSeries({ markAreas });
if (daylightSeries) {
series.push(daylightSeries);
// Individual ensemble members: thin, low-alpha lines
for (let mi = 0; mi < varData.members.length; mi++) {
series.push({
name: `${variable}_member${String(mi).padStart(2, '0')}`,
type: 'line',
color: CHART_COLORS.memberLine,
data: varData.members[mi],
width: 1,
showInLegend: false
});
}
const isFirst = vi === 0;
const isLast = vi === variableCount - 1;
const option = composeChartOption({
title: isFirst
? {
text: 'Model Spread',
subtext: `Compare ${params.hourly?.join(', ') || ''} in models: ${params.models?.join(', ') || ''}`
}
: null,
tooltip: { unit, timezone },
legend: {
show: _showLegend,
data: [variable + '_average']
},
grid: {
hasTitle: isFirst,
hasSubtitle: isFirst,
showLegend: _showLegend
},
yAxis: { unit },
series,
toolbox: false,
showCredit: isLast,
colors,
timezone
// Ensemble mean: bold dashed line on top
series.push({
name: variable + '_average',
type: isColumn ? 'bar' : 'line',
color: CHART_COLORS.average,
data: varData.average,
width: 4,
dashed: !isColumn
});
newOptions.push(option);
const isFirst = vi === 0;
const isLast = vi === variables.length - 1;
defs.push({
title: isFirst ? 'Model Spread' : undefined,
subtitle: isFirst
? `Compare ${params.hourly?.join(', ') || ''} in models: ${params.models?.join(', ') || ''}`
: undefined,
unit,
showCredit: isLast,
series
});
}
chartOptions = newOptions;
return defs;
});
</script>
@@ -221,20 +194,25 @@
</div>
{/if}
<ChartContainer
{loading}
chartCount={params.hourly?.length || 0}
chartHeight={showLegend ? 400 : 300}
>
{#each chartOptions as option, i (i)}
<EChart
{option}
notMerge
height={showLegend ? '400px' : '300px'}
onChartReady={handleChartReady}
bind:this={chartComponents[i]}
/>
{/each}
<ChartContainer {loading} chartCount={params.hourly?.length || 1} chartHeight={300}>
{#if fetchedData}
{#each chartDefs as def, i (i)}
<CanvasChart
bind:this={chartComponents[i]}
timestamps={timestampsSec}
timezone={fetchedData.timezone}
series={def.series}
bands={fetchedData.daylightBands}
unit={def.unit}
title={def.title}
subtitle={def.subtitle}
showCredit={def.showCredit}
{showLegend}
height={300}
group={CHART_GROUP}
/>
{/each}
{/if}
</ChartContainer>
<!-- ─── Toolbar: Controls + Download ───────────────────────────────────────── -->