- · {#if location.admin1}{location.admin1},
+ · {#if location.admin1}{location.admin1},
{/if}{location.country ?? ''}
{/if}
diff --git a/src/routes/weather/14-day/[location]/+page.svelte b/src/routes/weather/14-day/[location]/+page.svelte
index 873b0c4..88ac784 100644
--- a/src/routes/weather/14-day/[location]/+page.svelte
+++ b/src/routes/weather/14-day/[location]/+page.svelte
@@ -154,6 +154,11 @@
fetchedData ? fetchedData.timestamps.slice(0, validLength).map((t) => t / 1000) : []
);
+ // Surface a note when the chosen model's ensemble stops short of the request.
+ let fullHours = $derived.by(() => (fetchedData ? fetchedData.timestamps.length : 0));
+ let validDays = $derived(Math.max(0, Math.round(validLength / 24)));
+ let isTrimmed = $derived(fetchedData != null && validLength > 0 && validLength < fullHours - 1);
+
interface ChartDef {
title?: string;
subtitle?: string;
@@ -210,14 +215,6 @@
bandTo: varData.min,
format: (v) => `${v.toFixed(1)} ${unit}`
},
- {
- name: 'Min',
- type: 'line',
- color: BAND_COLOR,
- data: varData.min,
- width: 1,
- format: (v) => `${v.toFixed(1)} ${unit}`
- },
{
name: 'Mean',
type: isColumn ? 'bar' : 'line',
@@ -226,6 +223,14 @@
width: 3,
dashed: !isColumn,
format: (v) => `${v.toFixed(1)} ${unit}`
+ },
+ {
+ name: 'Min',
+ type: 'line',
+ color: BAND_COLOR,
+ data: varData.min,
+ width: 1,
+ format: (v) => `${v.toFixed(1)} ${unit}`
}
];
@@ -285,6 +290,30 @@
+{#if isTrimmed}
+
chart != null));
+ // ─── Zoom range controls (mirrors the 7-day meteograms) ─────────────────────
+
+ const SECONDS_PER_DAY = 24 * 3600;
+
+ function dayStartSec(day: Date): number | null {
+ if (!fetchedData) return null;
+ const tz = fetchedData.timezone;
+ const target = formatZoned(day, tz, 'yyyy-MM-dd');
+ const idx = fetchedData.timestamps.findIndex(
+ (t) => formatZoned(new Date(t), tz, 'yyyy-MM-dd') === target
+ );
+ return idx === -1 ? null : fetchedData.timestamps[idx] / 1000;
+ }
+
+ function setRangeDays(from: Date, days: number): void {
+ const start = dayStartSec(from);
+ if (start == null || liveCharts.length === 0) return;
+ // charts share the group, so setting the range on one syncs all of them
+ liveCharts[0].setRange(start, start + days * SECONDS_PER_DAY);
+ }
+
+ function resetZoom(): void {
+ liveCharts[0]?.resetRange();
+ }
+
+ const rangePresets = [
+ { label: 'Today', apply: () => setRangeDays(new Date(), 1) },
+ { label: '3 days', apply: () => setRangeDays(new Date(), 3) },
+ { label: '5 days', apply: () => setRangeDays(new Date(), 5) },
+ { label: 'All', apply: () => resetZoom() }
+ ];
+
+ let zoomActive = $derived(groupRange(CHART_GROUP) != null);
+
// ─── Data Fetching (only when params.hourly or params.models change) ───────
$effect(() => {
@@ -161,6 +198,24 @@
series: ChartSeries[];
}
+ // Human labels for the compared variables (API names → readable title)
+ const VAR_LABELS: Record
= {
+ temperature_2m: 'Temperature',
+ apparent_temperature: 'Feels like',
+ dew_point_2m: 'Dew point',
+ precipitation: 'Precipitation',
+ rain: 'Rain',
+ showers: 'Showers',
+ snowfall: 'Snowfall',
+ wind_speed_10m: 'Wind speed',
+ wind_gusts_10m: 'Wind gusts',
+ relative_humidity_2m: 'Relative humidity',
+ cloud_cover: 'Cloud cover',
+ pressure_msl: 'Pressure (MSL)'
+ };
+ const varLabel = (v: string): string =>
+ VAR_LABELS[v] ?? v.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
+
let chartDefs = $derived.by((): ChartDef[] => {
if (!fetchedData) return [];
@@ -209,10 +264,11 @@
const isLast = vi === variableCount - 1;
defs.push({
- title: isFirst ? 'Model Compare' : undefined,
+ // label every chart with its variable so each is identifiable
+ title: varLabel(variable),
subtitle: isFirst
- ? `Compare ${chartVariables.join(', ') || ''} in models: ${params.models?.join(', ') || ''}`
- : undefined,
+ ? `${params.models?.length ?? 0} models · dashed = average`
+ : `across ${params.models?.length ?? 0} models`,
unit,
showCredit: isLast,
series
@@ -223,6 +279,27 @@
});
+
+
+
+
.toLowerCase()}.svg)
+
+
+ {location.name}
+
+
+ {#if location.admin1}{location.admin1},
+ {/if}{location.country ?? ''}·Model comparison
+
+
+
+
{#if loadError}
@@ -233,6 +310,49 @@