This commit is contained in:
Vincent van der Wal
2026-08-01 17:58:03 +02:00
parent b2c8108c8c
commit 01cc6225aa
430 changed files with 3150 additions and 441 deletions
@@ -1,10 +1,12 @@
<script lang="ts">
import { onMount } from 'svelte';
import { SvelteDate } from 'svelte/reactivity';
import { get } from 'svelte/store';
import { fade } from 'svelte/transition';
import { reportPageReady } from '$lib/stores/page-transition.svelte';
import {
storedArchiveModel,
storedChartLayout,
storedLocation,
storedUnits,
@@ -13,6 +15,7 @@
import { ChartContainer } from '$lib/components/charts';
import * as m from '$lib/paraglide/messages';
import PaywallGate from '$lib/paywall/PaywallGate.svelte';
import { isSupporter } from '$lib/paywall/supporter';
import {
@@ -22,8 +25,10 @@
fetchHistoricalWeather
} from '$lib/services/weather';
import { defaultParameters } from '../../options';
import { useHeroActions } from '../../hero.svelte';
import { archiveModelGroups, defaultParameters } from '../../options';
import HourlyTable from '../../week/[location]/HourlyTable.svelte';
import ModelSelector from '../../week/[location]/ModelSelector.svelte';
import { neededHourlyApiVars } from '../../week/[location]/variables';
import DateRangeControls from './DateRangeControls.svelte';
import HistoricalDaily from './HistoricalDaily.svelte';
@@ -37,6 +42,8 @@
// the page cross-fade waits for this before revealing the new page
reportPageReady(() => result != null);
useHeroActions(heroActions);
let location = $derived(data.location);
$effect(() => {
storedLocation.set(data.location);
@@ -79,6 +86,11 @@
mounted = true;
});
let archiveModel = $state('best_match');
onMount(() => {
archiveModel = get(storedArchiveModel);
});
function onRangeChange(s: string, e: string) {
startDate = s;
endDate = e;
@@ -101,6 +113,8 @@
const s = startDate;
const e = endDate;
const vars = hourlyVars;
const model = archiveModel;
void model;
if (!mounted || !$isSupporter || !loc || !s || !e) return;
const version = ++requestVersion;
@@ -203,12 +217,27 @@
}
</script>
<!-- the reanalysis picker rides in the layout's location row -->
{#snippet heroActions()}
<div class="lg:absolute lg:right-0 lg:top-0 flex w-full min-w-0 items-center gap-3 sm:w-auto">
<ModelSelector
selectedModel={archiveModel}
groups={archiveModelGroups}
label={m.model_archive()}
onModelChange={(model) => {
archiveModel = model;
storedArchiveModel.set(model);
}}
/>
</div>
{/snippet}
<svelte:head>
<title>Drizz.li | Historical weather</title>
<title>Drizz.li | {m.page_historical_subtitle()}</title>
<meta name="description" content="Past weather and climate-normal comparisons for any location" />
</svelte:head>
<PaywallGate feature="Historical weather">
<PaywallGate feature={m.page_historical_subtitle()}>
<DateRangeControls
start={startDate}
end={endDate}
@@ -1,4 +1,6 @@
<script lang="ts">
import * as m from '$lib/paraglide/messages';
interface Props {
start: string;
end: string;
@@ -34,10 +36,10 @@
}
const presets = [
{ label: '7 days', apply: () => applyLastDays(7) },
{ label: '30 days', apply: () => applyLastDays(30) },
{ label: '90 days', apply: () => applyLastDays(90) },
{ label: 'Month, last year', apply: applyThisMonthLastYear }
{ label: () => m.historical_last_days({ days: 7 }), apply: () => applyLastDays(7) },
{ label: () => m.historical_last_days({ days: 30 }), apply: () => applyLastDays(30) },
{ label: () => m.historical_last_days({ days: 90 }), apply: () => applyLastDays(90) },
{ label: () => m.historical_month_last_year(), apply: applyThisMonthLastYear }
];
// ─── Manual inputs ────────────────────────────────────────────────────────────
@@ -65,7 +67,9 @@
>
<div class="flex flex-wrap items-end gap-3">
<div class="grid gap-1">
<label for="hist-start" class="text-xs font-semibold text-muted-foreground">From</label>
<label for="hist-start" class="text-xs font-semibold text-muted-foreground"
>{m.historical_from()}</label
>
<input
id="hist-start"
type="date"
@@ -77,7 +81,9 @@
/>
</div>
<div class="grid gap-1">
<label for="hist-end" class="text-xs font-semibold text-muted-foreground">To</label>
<label for="hist-end" class="text-xs font-semibold text-muted-foreground"
>{m.historical_to()}</label
>
<input
id="hist-end"
type="date"
@@ -93,15 +99,15 @@
<div
class="inline-flex flex-wrap items-center gap-0.5 rounded-lg bg-muted p-0.5 text-xs font-semibold"
role="group"
aria-label="Quick ranges"
aria-label={m.historical_quick_ranges()}
>
{#each presets as preset (preset.label)}
{#each presets as preset (preset.label())}
<button
type="button"
class="cursor-pointer rounded-md px-2.5 py-1.5 whitespace-nowrap text-muted-foreground transition-colors hover:bg-background hover:text-foreground hover:shadow-sm"
onclick={preset.apply}
>
{preset.label}
{preset.label()}
</button>
{/each}
</div>
@@ -1,6 +1,7 @@
<script lang="ts">
import { formatZoned, isSameDayInZone } from '$lib/utils/date';
import * as m from '$lib/paraglide/messages';
import {
type ClimateNormals,
type HistoricalDailyData,
@@ -104,7 +105,7 @@
<!-- ─── KPI tiles ────────────────────────────────────────────────────────────── -->
<div class="grid grid-cols-2 gap-2.5 lg:grid-cols-4">
<div class="rounded-xl border border-border/70 bg-card p-3.5 shadow-sm">
<p class="text-xs font-medium text-muted-foreground">Average temperature</p>
<p class="text-xs font-medium text-muted-foreground">{m.stat_avg_temperature()}</p>
<p class="mt-1 text-2xl font-bold tabular-nums">
{fmtTemp(stats.avg)}<span class="text-base font-semibold text-muted-foreground"
>{tempUnit.replace('°', '')}</span
@@ -118,15 +119,15 @@
class:dark:text-red-400={stats.tempAnomaly >= 0}
class:dark:text-blue-400={stats.tempAnomaly < 0}
>
{fmtSigned(stats.tempAnomaly)} vs normal
{m.anomaly_vs_normal({ value: fmtSigned(stats.tempAnomaly) })}
</p>
{:else}
<p class="mt-0.5 text-xs text-muted-foreground">19912020 normal loading…</p>
<p class="mt-0.5 text-xs text-muted-foreground">{m.normals_loading_period()}</p>
{/if}
</div>
<div class="rounded-xl border border-border/70 bg-card p-3.5 shadow-sm">
<p class="text-xs font-medium text-muted-foreground">Total precipitation</p>
<p class="text-xs font-medium text-muted-foreground">{m.stat_total_precipitation()}</p>
<p class="mt-1 text-2xl font-bold tabular-nums">{fmtPrecip(stats.totalPrecip)}</p>
{#if stats.normalPrecip != null}
<p class="mt-0.5 text-xs font-semibold text-muted-foreground">
@@ -140,7 +141,7 @@
</div>
<div class="rounded-xl border border-border/70 bg-card p-3.5 shadow-sm">
<p class="text-xs font-medium text-muted-foreground">Warmest day</p>
<p class="text-xs font-medium text-muted-foreground">{m.stat_warmest_day()}</p>
<p class="mt-1 text-2xl font-bold tabular-nums">{fmtTemp(stats.warm.t)}</p>
{#if stats.warm.i >= 0}
<p class="mt-0.5 text-xs font-semibold text-muted-foreground">
@@ -150,7 +151,7 @@
</div>
<div class="rounded-xl border border-border/70 bg-card p-3.5 shadow-sm">
<p class="text-xs font-medium text-muted-foreground">Coldest day</p>
<p class="text-xs font-medium text-muted-foreground">{m.stat_coldest_day()}</p>
<p class="mt-1 text-2xl font-bold tabular-nums">{fmtTemp(stats.cold.t)}</p>
{#if stats.cold.i >= 0}
<p class="mt-0.5 text-xs font-semibold text-muted-foreground">
@@ -166,12 +167,12 @@
>
<div class="flex items-center justify-between border-b border-border/70 bg-muted/40 px-4 py-2.5">
<h3 class="text-base font-bold">
Daily <span class="font-semibold text-muted-foreground"> select a day for hourly detail</span
>
{m.historical_daily_heading()}
<span class="font-semibold text-muted-foreground">{m.historical_daily_hint()}</span>
</h3>
{#if normals}
<span class="hidden text-xs text-muted-foreground sm:inline">
normal band = 19912020 mean
{m.normal_band_legend()}
</span>
{/if}
</div>
@@ -242,7 +243,7 @@
<span
class="mt-0.5 inline-block h-1.5 w-6 rounded-full"
style="background:{anomalyColor(anomaly)}"
title="{fmtSigned(anomaly)} vs normal"
title={m.anomaly_vs_normal({ value: fmtSigned(anomaly) })}
></span>
{/if}
@@ -8,6 +8,7 @@
import { ChartContainer, downloadChartsPng } from '$lib/components/charts';
import { CanvasChart, groupRange } from '$lib/charts';
import * as m from '$lib/paraglide/messages';
import { getWeatherIconName } from '../../utils/weather-codes';
import { type FetchedHourly, type WeatherUnits } from '../../week/[location]/types';
@@ -121,22 +122,23 @@
<section class="mt-8" transition:fade={{ duration: 200 }}>
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
<h3 class="text-lg font-bold">
Meteograms <span class="font-semibold text-muted-foreground"> full range</span>
{m.meteograms_heading()}
<span class="font-semibold text-muted-foreground">{m.historical_full_range()}</span>
</h3>
<div class="flex flex-wrap items-center gap-3">
<span class="hidden text-xs text-muted-foreground md:inline">
drag or
{m.meteograms_zoom_hint()}
<kbd class="rounded border border-border bg-muted px-1 py-0.5 font-sans text-[10px]"
>Ctrl</kbd
>
+ scroll to zoom
{m.meteograms_zoom_hint_end()}
</span>
<button
type="button"
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-border bg-card px-3 py-1.5 text-xs font-semibold text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
disabled={liveCharts.length === 0 || downloadingPng}
onclick={downloadPng}
title="Download meteogram as PNG image"
title={m.chart_download()}
>
{#if downloadingPng}Rendering…{:else}PNG{/if}
</button>
@@ -148,7 +150,7 @@
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-primary/50 bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary transition-colors hover:bg-primary/15"
onclick={resetZoom}
>
Reset zoom
{m.reset_zoom()}
</button>
{/if}
</div>
@@ -158,7 +160,7 @@
<div
class="rounded-xl border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground"
>
No meteograms configured. Add variables from the 7-day forecast page.
{m.meteograms_none_historical()}
</div>
{:else}
<div class="-mx-3 border-y border-border/70 bg-card shadow-sm lg:mx-0 lg:rounded-2xl lg:border">