diff --git a/messages/de.json b/messages/de.json
index ac3f4f8..93e31f9 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -203,5 +203,9 @@
"var_visibility_short": "Sicht",
"var_cape": "CAPE",
"var_cape_short": "CAPE",
- "var_time": "Zeit"
+ "var_time": "Zeit",
+ "no_data_title": "Für dieses Modell gibt es hier keine Daten",
+ "no_data_body": "Das gewählte Wettermodell deckt {location} nicht ab - regionale Modelle liefern nur Daten in ihrem eigenen Gebiet.",
+ "no_data_try_city": "Ort auf {city} ändern",
+ "no_data_best_match": "Zu „Best match“ wechseln"
}
diff --git a/messages/en.json b/messages/en.json
index 9d9bfe3..3743a22 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -203,5 +203,9 @@
"var_visibility_short": "Vis",
"var_cape": "CAPE",
"var_cape_short": "CAPE",
- "var_time": "Time"
+ "var_time": "Time",
+ "no_data_title": "No forecast data for this model here",
+ "no_data_body": "The selected weather model doesn't cover {location} - regional models only provide data inside their own area.",
+ "no_data_try_city": "Change location to {city}",
+ "no_data_best_match": "Switch to Best match"
}
diff --git a/messages/es.json b/messages/es.json
index 0afb764..dc58924 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -203,5 +203,9 @@
"var_visibility_short": "Vis.",
"var_cape": "CAPE",
"var_cape_short": "CAPE",
- "var_time": "Hora"
+ "var_time": "Hora",
+ "no_data_title": "Este modelo no tiene datos aquí",
+ "no_data_body": "El modelo seleccionado no cubre {location}: los modelos regionales solo ofrecen datos dentro de su propia área.",
+ "no_data_try_city": "Cambiar la ubicación a {city}",
+ "no_data_best_match": "Cambiar a «Best match»"
}
diff --git a/messages/fr.json b/messages/fr.json
index 5d504a2..613f5a4 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -203,5 +203,9 @@
"var_visibility_short": "Vis.",
"var_cape": "CAPE",
"var_cape_short": "CAPE",
- "var_time": "Heure"
+ "var_time": "Heure",
+ "no_data_title": "Ce modèle n'a pas de données ici",
+ "no_data_body": "Le modèle sélectionné ne couvre pas {location} : les modèles régionaux ne fournissent des données que dans leur propre zone.",
+ "no_data_try_city": "Changer de lieu pour {city}",
+ "no_data_best_match": "Passer à « Best match »"
}
diff --git a/messages/it.json b/messages/it.json
index 1498c91..c285940 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -203,5 +203,9 @@
"var_visibility_short": "Vis.",
"var_cape": "CAPE",
"var_cape_short": "CAPE",
- "var_time": "Ora"
+ "var_time": "Ora",
+ "no_data_title": "Nessun dato per questo modello qui",
+ "no_data_body": "Il modello selezionato non copre {location}: i modelli regionali forniscono dati solo nella propria area.",
+ "no_data_try_city": "Cambia località in {city}",
+ "no_data_best_match": "Passa a «Best match»"
}
diff --git a/src/lib/components/navigation/weather-nav.svelte b/src/lib/components/navigation/weather-nav.svelte
index d09f374..e30633c 100644
--- a/src/lib/components/navigation/weather-nav.svelte
+++ b/src/lib/components/navigation/weather-nav.svelte
@@ -139,7 +139,35 @@
{/each}
-
+
+ {#if onMobileClose}
+
+ {/if}
{#if onToggle}
diff --git a/src/routes/layout.css b/src/routes/layout.css
index 24108f4..bf2d613 100644
--- a/src/routes/layout.css
+++ b/src/routes/layout.css
@@ -155,13 +155,13 @@
in the cross-fade. Everything else keeps its pixels: the `day-switch`
class cancels the root animation, so the strip, header and page chrome do
not so much as flicker while the table, summary and charts swap over. */
- .day-region-table {
+ :root.day-switch .day-region-table {
view-transition-name: day-table;
}
- .day-region-summary {
+ :root.day-switch .day-region-summary {
view-transition-name: day-summary;
}
- .day-region-charts {
+ :root.day-switch .day-region-charts {
view-transition-name: day-charts;
}
@@ -169,7 +169,7 @@
captured too: view-transition snapshots all paint in one layer above the
page, so without its own group (and a higher z-index in that layer) the
fading table and charts would slide over the top of it. */
- .daystrip {
+ :root.day-switch .daystrip {
view-transition-name: daystrip;
}
::view-transition-group(daystrip) {
@@ -178,7 +178,7 @@
/* Same for the topbar: a captured region's snapshot is painted at its layout
position, including the part normally scrolled up behind the chrome. */
- .topbar {
+ :root.day-switch .topbar {
view-transition-name: topbar;
}
::view-transition-group(topbar) {
diff --git a/src/routes/weather/options.ts b/src/routes/weather/options.ts
index 0197874..b384cfe 100644
--- a/src/routes/weather/options.ts
+++ b/src/routes/weather/options.ts
@@ -479,3 +479,37 @@ export const ensembleModels: WeatherModel[] = ensembleModelGroups.flatMap((group
export const findEnsembleModel = (value: string): WeatherModel | undefined =>
ensembleModels.find((model) => model.value === value);
+
+/**
+ * A city inside each regional model's domain, used to offer a way out when the
+ * chosen model has no data for the current place. Keyed by the model-id prefix
+ * so new variants of a domain are covered without another entry.
+ */
+const IN_DOMAIN_CITY: [string, { slug: string; label: string }][] = [
+ ['meteoswiss', { slug: 'zurich', label: 'Zürich' }],
+ ['italia_meteo', { slug: 'rome', label: 'Rome' }],
+ ['icon', { slug: 'berlin', label: 'Berlin' }],
+ ['dwd', { slug: 'berlin', label: 'Berlin' }],
+ ['meteofrance', { slug: 'paris', label: 'Paris' }],
+ ['arpege', { slug: 'paris', label: 'Paris' }],
+ ['arome', { slug: 'paris', label: 'Paris' }],
+ ['ukmo', { slug: 'london', label: 'London' }],
+ ['knmi', { slug: 'amsterdam', label: 'Amsterdam' }],
+ ['dmi', { slug: 'copenhagen', label: 'Copenhagen' }],
+ ['metno', { slug: 'oslo', label: 'Oslo' }],
+ ['ncep', { slug: 'new-york', label: 'New York' }],
+ ['gfs', { slug: 'new-york', label: 'New York' }],
+ ['hrrr', { slug: 'new-york', label: 'New York' }],
+ ['nbm', { slug: 'new-york', label: 'New York' }],
+ ['gem', { slug: 'toronto', label: 'Toronto' }],
+ ['jma', { slug: 'tokyo', label: 'Tokyo' }],
+ ['kma', { slug: 'seoul', label: 'Seoul' }],
+ ['cma', { slug: 'beijing', label: 'Beijing' }],
+ ['bom', { slug: 'sydney', label: 'Sydney' }]
+];
+
+/** Where this model definitely has data, or null for a global model. */
+export function inDomainCity(model: string): { slug: string; label: string } | null {
+ const hit = IN_DOMAIN_CITY.find(([prefix]) => model.startsWith(prefix));
+ return hit ? hit[1] : null;
+}
diff --git a/src/routes/weather/week/[location]/+page.svelte b/src/routes/weather/week/[location]/+page.svelte
index c1a4145..e48c041 100644
--- a/src/routes/weather/week/[location]/+page.svelte
+++ b/src/routes/weather/week/[location]/+page.svelte
@@ -22,6 +22,8 @@
import { ChartContainer } from '$lib/components/charts';
+ import { href } from '$lib/i18n';
+ import * as m from '$lib/paraglide/messages';
import {
type FriendlyWeatherError,
type WeekForecastResult,
@@ -30,7 +32,7 @@
} from '$lib/services/weather';
import { useHeroActions } from '../../hero.svelte';
- import { defaultParameters } from '../../options';
+ import { defaultParameters, inDomainCity } from '../../options';
import { computeDayNightWeatherCodes } from '../../utils/weather-codes';
import DailyStripSticky from './DailyStripSticky.svelte';
import DaySummary from './DaySummary.svelte';
@@ -114,6 +116,14 @@
// for the same place once their range is exhausted.
let locationRoute = $derived(buildLocationRoute(location));
+ // When a regional model has no data here, point at a place it does cover
+ // rather than only offering to abandon the model.
+ let suggestedCity = $derived.by(() => {
+ const city = inDomainCity(params.models?.[0] ?? '');
+ // pointless to offer the place we are already on
+ return city && city.slug !== locationRoute ? city : null;
+ });
+
let mounted = $state(false);
let loading = $state(true);
let loadError = $state(null);
@@ -157,6 +167,36 @@
return fd ? formatZoned(selectedDay, fd.timezone, 'yyyy-MM-dd') : '';
});
+ // A model swap can leave the open day outside what the new model covers (a
+ // short-range model after a 15-day one, say). Rather than showing an empty
+ // day, fall back to the nearest day that does have data - searching forward
+ // first, then back.
+ $effect(() => {
+ const fd = fetchedDaily;
+ if (!fd || wantedDay) return;
+ const days = fd.dailyDates;
+ if (days.length === 0) return;
+
+ const covered = (i: number) => {
+ const max = fd.daily.temperature_2m_max[i];
+ return max != null && Number.isFinite(max);
+ };
+ const current = days.findIndex(
+ (d) => formatZoned(d, fd.timezone, 'yyyy-MM-dd') === selectedDayKey
+ );
+ if (current >= 0 && covered(current)) return;
+
+ const from = current >= 0 ? current : 0;
+ for (let step = 0; step < days.length; step++) {
+ for (const i of [from + step, from - step]) {
+ if (i >= 0 && i < days.length && covered(i)) {
+ selectedDay.setTime(days[i].getTime());
+ return;
+ }
+ }
+ }
+ });
+
// Charts intentionally keep their current range: they show the full week
// unless the user narrows it via the range presets or Ctrl+scroll.
const switchDay = (date: Date) => {
@@ -324,12 +364,20 @@
>
Try again
+ {#if suggestedCity}
+
+ {m.no_data_try_city({ city: suggestedCity.label })}
+
+ {/if}
{#if params.models?.[0] !== 'best_match'}
{/if}
@@ -362,18 +410,27 @@
/>
-
No forecast data for this model here
+
{m.no_data_title()}
- The selected weather model doesn't cover {location.name} — regional models only provide data
- inside their own area.
+ {m.no_data_body({ location: location.name ?? '' })}