This commit is contained in:
Vincent van der Wal
2026-07-19 14:57:26 +02:00
parent 9e1944396e
commit 6d81af8df5
26 changed files with 408 additions and 466 deletions
@@ -2,7 +2,7 @@
import { onDestroy, onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { type GeoLocation, storedLocation } from '$lib/stores/settings';
import { storedLocation } from '$lib/stores/settings';
import {
buildAverageSeries,
@@ -31,6 +31,7 @@
import { defaultParameters } from '../../options';
import ModelPictogramTimeline from './ModelPictogramTimeline.svelte';
import type { PageData } from './$types';
import type * as echarts from 'echarts';
const models = [modelsFlat];
@@ -46,10 +47,17 @@
let chartOptions: Array<Record<string, unknown>> = $state([]);
let mounted = $state(false);
let loading = $state(true);
let loadError = $state<string | null>(null);
let requestVersion = 0;
let location = $state<GeoLocation>($storedLocation);
storedLocation.subscribe((value) => {
location = value;
let { data }: { data: PageData } = $props();
// the URL is the source of truth: location comes from the load function,
// which is also correct on hydrated prerendered pages. The persisted store
// only mirrors it so the header and bare /weather/* redirects follow along.
let location = $derived(data.location);
$effect(() => {
storedLocation.set(data.location);
});
let params = $state({
@@ -104,6 +112,11 @@
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()));
// ─── Data Fetching (only when params.hourly or params.models change) ───────
$effect(() => {
@@ -114,36 +127,41 @@
const loc = location;
const loadData = async () => {
loading = true;
chartInstances = [];
chartComponents = [];
// versioned so a slow stale response can never overwrite a newer one
const version = ++requestVersion;
loading = true;
loadError = null;
const result: ModelCompareResult = await fetchModelComparison({
latitude: loc.latitude!,
longitude: loc.longitude!,
hourlyVariables: [...new Set([...hourlyVars, 'weather_code'])],
models: modelList,
temperature_unit: params.temperature_unit as 'celsius' | 'fahrenheit',
wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn',
precipitation_unit: params.precipitation_unit as 'mm' | 'inch',
timezone: loc.timezone
fetchModelComparison({
latitude: loc.latitude!,
longitude: loc.longitude!,
hourlyVariables: [...new Set([...hourlyVars, 'weather_code'])],
models: modelList,
temperature_unit: params.temperature_unit as 'celsius' | 'fahrenheit',
wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn',
precipitation_unit: params.precipitation_unit as 'mm' | 'inch',
timezone: loc.timezone
})
.then((result: ModelCompareResult) => {
if (version !== requestVersion) return;
fetchedData = {
hourly: result.hourlyFlat,
hourly_units: result.hourlyUnitsFlat,
timezone: result.timezone,
markAreas: result.markAreas,
timestamps: result.timestamps,
sunrise: result.sunrise,
sunset: result.sunset
};
loading = false;
})
.catch((err: unknown) => {
if (version !== requestVersion) return;
loadError = err instanceof Error ? err.message : String(err);
loading = false;
});
fetchedData = {
hourly: result.hourlyFlat,
hourly_units: result.hourlyUnitsFlat,
timezone: result.timezone,
markAreas: result.markAreas,
timestamps: result.timestamps,
sunrise: result.sunrise,
sunset: result.sunset
};
loading = false;
};
loadData();
});
// ─── Chart Option Building (runs when fetchedData OR display toggles change) ─
@@ -228,10 +246,19 @@
<!-- ─── Chart Area ─────────────────────────────────────────────────────────── -->
{#if loadError}
<div
class="mb-4 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive"
>
Failed to load weather data: {loadError}
</div>
{/if}
<ChartContainer {loading} chartCount={chartOptions.length} chartHeight={showLegend ? 400 : 300}>
{#each chartOptions as option, i (i)}
<EChart
{option}
notMerge
height={showLegend ? '400px' : '300px'}
onChartReady={handleChartReady}
bind:this={chartComponents[i]}
@@ -253,7 +280,7 @@
<!-- ─── Toolbar: Controls + Download ───────────────────────────────────────── -->
<div class="mt-6 md:mt-10">
<ChartToolbar charts={chartInstances} fileName="model-comparison">
<ChartToolbar charts={liveCharts} fileName="model-comparison">
{#snippet controls()}
<div class="flex gap-2">
<Switch id="show_legend" name="Show legend" bind:checked={showLegend} />