small bug fixes

This commit is contained in:
Vincent van der Wal
2026-08-01 17:24:00 +02:00
parent 49e011c20b
commit b2c8108c8c
9 changed files with 161 additions and 22 deletions
+34
View File
@@ -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;
}
+68 -11
View File
@@ -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<FriendlyWeatherError | null>(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
</button>
{#if suggestedCity}
<a
class="cursor-pointer rounded-md border border-border bg-background px-3 py-1 text-xs font-semibold text-foreground transition-colors hover:bg-muted"
href={href('/weather/week/[location]', { location: suggestedCity.slug })}
>
{m.no_data_try_city({ city: suggestedCity.label })}
</a>
{/if}
{#if params.models?.[0] !== 'best_match'}
<button
class="cursor-pointer rounded-md border border-border bg-background px-3 py-1 text-xs font-semibold text-foreground transition-colors hover:bg-muted"
onclick={resetToBestMatch}
>
Switch to Best match
{m.no_data_best_match()}
</button>
{/if}
</div>
@@ -362,18 +410,27 @@
/>
</svg>
<div class="min-w-0 flex-1">
<p class="font-semibold">No forecast data for this model here</p>
<p class="font-semibold">{m.no_data_title()}</p>
<p class="text-[13px] opacity-90">
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 ?? '' })}
</p>
</div>
<button
class="cursor-pointer rounded-md border border-amber-500/50 bg-background/60 px-3 py-1 text-xs font-semibold transition-colors hover:bg-background"
onclick={resetToBestMatch}
>
Switch to Best match
</button>
<div class="flex flex-wrap items-center gap-2">
{#if suggestedCity}
<a
class="cursor-pointer rounded-md border border-amber-500/50 bg-background/60 px-3 py-1 text-xs font-semibold transition-colors hover:bg-background"
href={href('/weather/week/[location]', { location: suggestedCity.slug })}
>
{m.no_data_try_city({ city: suggestedCity.label })}
</a>
{/if}
<button
class="cursor-pointer rounded-md border border-amber-500/50 bg-background/60 px-3 py-1 text-xs font-semibold transition-colors hover:bg-background"
onclick={resetToBestMatch}
>
{m.no_data_best_match()}
</button>
</div>
</div>
{/if}