|
|
|
@@ -1,7 +1,7 @@
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
import { onDestroy, onMount } from 'svelte';
|
|
|
|
|
|
|
|
|
|
import { type GeoLocation, storedLocation } from '$lib/stores/settings';
|
|
|
|
|
import { storedLocation } from '$lib/stores/settings';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
buildAverageSeries,
|
|
|
|
@@ -25,6 +25,7 @@
|
|
|
|
|
|
|
|
|
|
import { defaultParameters } from '../../options';
|
|
|
|
|
|
|
|
|
|
import type { PageData } from './$types';
|
|
|
|
|
import type * as echarts from 'echarts';
|
|
|
|
|
|
|
|
|
|
// ─── Display State (does NOT trigger data re-fetch) ─────────────────────────
|
|
|
|
@@ -38,10 +39,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({
|
|
|
|
@@ -79,6 +87,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(() => {
|
|
|
|
@@ -89,35 +102,39 @@
|
|
|
|
|
|
|
|
|
|
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: EnsembleForecastResult = await fetchEnsembleForecast({
|
|
|
|
|
latitude: loc.latitude!,
|
|
|
|
|
longitude: loc.longitude!,
|
|
|
|
|
hourlyVariables: hourlyVars,
|
|
|
|
|
models: modelList,
|
|
|
|
|
forecast_days: 14,
|
|
|
|
|
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
|
|
|
|
|
fetchEnsembleForecast({
|
|
|
|
|
latitude: loc.latitude!,
|
|
|
|
|
longitude: loc.longitude!,
|
|
|
|
|
hourlyVariables: hourlyVars,
|
|
|
|
|
models: modelList,
|
|
|
|
|
forecast_days: 14,
|
|
|
|
|
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: EnsembleForecastResult) => {
|
|
|
|
|
if (version !== requestVersion) return;
|
|
|
|
|
|
|
|
|
|
fetchedData = {
|
|
|
|
|
ensembleResult: result,
|
|
|
|
|
timestamps: result.timestamps,
|
|
|
|
|
timezone: result.timezone,
|
|
|
|
|
markAreas: result.markAreas
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
loading = false;
|
|
|
|
|
})
|
|
|
|
|
.catch((err: unknown) => {
|
|
|
|
|
if (version !== requestVersion) return;
|
|
|
|
|
loadError = err instanceof Error ? err.message : String(err);
|
|
|
|
|
loading = false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fetchedData = {
|
|
|
|
|
ensembleResult: result,
|
|
|
|
|
timestamps: result.timestamps,
|
|
|
|
|
timezone: result.timezone,
|
|
|
|
|
markAreas: result.markAreas
|
|
|
|
|
};
|
|
|
|
|
console.log(fetchedData.timezone);
|
|
|
|
|
|
|
|
|
|
loading = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
loadData();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Chart Option Building (runs when fetchedData OR display toggles change) ─
|
|
|
|
@@ -196,6 +213,14 @@
|
|
|
|
|
|
|
|
|
|
<!-- ─── 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={params.hourly?.length || 0}
|
|
|
|
@@ -204,6 +229,7 @@
|
|
|
|
|
{#each chartOptions as option, i (i)}
|
|
|
|
|
<EChart
|
|
|
|
|
{option}
|
|
|
|
|
notMerge
|
|
|
|
|
height={showLegend ? '400px' : '300px'}
|
|
|
|
|
onChartReady={handleChartReady}
|
|
|
|
|
bind:this={chartComponents[i]}
|
|
|
|
@@ -214,7 +240,7 @@
|
|
|
|
|
<!-- ─── Toolbar: Controls + Download ───────────────────────────────────────── -->
|
|
|
|
|
|
|
|
|
|
<div class="mt-6 md:mt-10">
|
|
|
|
|
<ChartToolbar charts={chartInstances} fileName="14-day-forecast">
|
|
|
|
|
<ChartToolbar charts={liveCharts} fileName="14-day-forecast">
|
|
|
|
|
{#snippet controls()}
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<Switch id="show_legend" name="Show legend" bind:checked={showLegend} />
|
|
|
|
|