build generic weather week
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import { storedLocation } from '$lib/stores/settings';
|
||||
import { storedEnsembleModel, storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import { ChartContainer, ChartToolbar } from '$lib/components/charts';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
@@ -14,7 +15,8 @@
|
||||
fetchEnsembleForecast
|
||||
} from '$lib/services/weather';
|
||||
|
||||
import { defaultParameters } from '../../options';
|
||||
import { defaultParameters, ensembleModelGroups } from '../../options';
|
||||
import ModelSelector from '../../week/[location]/ModelSelector.svelte';
|
||||
|
||||
import type { PageData } from './$types';
|
||||
|
||||
@@ -22,7 +24,7 @@
|
||||
|
||||
// ─── Display State (does NOT trigger data re-fetch) ─────────────────────────
|
||||
|
||||
let showLegend = $state(false);
|
||||
let showLegend = $state(true);
|
||||
|
||||
// ─── Data Fetch State ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -45,7 +47,7 @@
|
||||
let params = $state({
|
||||
...defaultParameters,
|
||||
hourly: ['temperature_2m'],
|
||||
models: ['gfs_seamless']
|
||||
models: ['ncep_gefs_seamless']
|
||||
});
|
||||
|
||||
// ─── Cached API Response ────────────────────────────────────────────────────
|
||||
@@ -62,6 +64,8 @@
|
||||
// ─── Lifecycle ──────────────────────────────────────────────────────────────
|
||||
|
||||
onMount(() => {
|
||||
// preselect the persisted ensemble model (client-only, keeps SSR stable)
|
||||
params.models = [get(storedEnsembleModel)];
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
@@ -128,6 +132,8 @@
|
||||
series: ChartSeries[];
|
||||
}
|
||||
|
||||
const BAND_COLOR = 'rgba(115, 192, 222, 0.9)';
|
||||
|
||||
let chartDefs = $derived.by((): ChartDef[] => {
|
||||
if (!fetchedData) return [];
|
||||
|
||||
@@ -142,37 +148,47 @@
|
||||
|
||||
const unit = varData.unit;
|
||||
const isColumn = isColumnUnit(unit);
|
||||
const series: ChartSeries[] = [];
|
||||
const memberCount = varData.members.length;
|
||||
|
||||
// Individual ensemble members: thin, low-alpha lines
|
||||
for (let mi = 0; mi < varData.members.length; mi++) {
|
||||
series.push({
|
||||
name: `${variable}_member${String(mi).padStart(2, '0')}`,
|
||||
// Min/max spread band + mean, instead of every individual member
|
||||
const series: ChartSeries[] = [
|
||||
{
|
||||
name: 'Max',
|
||||
type: 'line',
|
||||
color: CHART_COLORS.memberLine,
|
||||
data: varData.members[mi],
|
||||
color: BAND_COLOR,
|
||||
data: varData.max,
|
||||
width: 1,
|
||||
showInLegend: false
|
||||
});
|
||||
}
|
||||
|
||||
// Ensemble mean: bold dashed line on top
|
||||
series.push({
|
||||
name: variable + '_average',
|
||||
type: isColumn ? 'bar' : 'line',
|
||||
color: CHART_COLORS.average,
|
||||
data: varData.average,
|
||||
width: 4,
|
||||
dashed: !isColumn
|
||||
});
|
||||
fill: true,
|
||||
fillOpacity: 0.25,
|
||||
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',
|
||||
color: CHART_COLORS.average,
|
||||
data: varData.average,
|
||||
width: 3,
|
||||
dashed: !isColumn,
|
||||
format: (v) => `${v.toFixed(1)} ${unit}`
|
||||
}
|
||||
];
|
||||
|
||||
const isFirst = vi === 0;
|
||||
const isLast = vi === variables.length - 1;
|
||||
|
||||
defs.push({
|
||||
title: isFirst ? 'Model Spread' : undefined,
|
||||
title: isFirst ? 'Ensemble Spread' : undefined,
|
||||
subtitle: isFirst
|
||||
? `Compare ${params.hourly?.join(', ') || ''} in models: ${params.models?.join(', ') || ''}`
|
||||
? `${variable} min/mean/max across ${memberCount} members (${params.models?.[0] ?? ''})`
|
||||
: undefined,
|
||||
unit,
|
||||
showCredit: isLast,
|
||||
@@ -184,6 +200,39 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ─── Page hero: location + ensemble model selection ─────────────────────── -->
|
||||
|
||||
<div class="mb-5 flex flex-wrap items-center justify-between gap-x-6 gap-y-3">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<img
|
||||
class="h-10 w-10 shrink-0 rounded-full shadow-sm ring-2 ring-border"
|
||||
src="/images/country-flags/{(location.country_code || 'united_nations').toLowerCase()}.svg"
|
||||
alt={location.country ?? ''}
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<h1 class="truncate text-2xl leading-tight font-bold tracking-tight md:text-3xl">
|
||||
{location.name}
|
||||
</h1>
|
||||
<p class="truncate text-sm text-muted-foreground">
|
||||
{#if location.admin1}{location.admin1},
|
||||
{/if}{location.country ?? ''}
|
||||
<span class="mx-1 opacity-50">·</span>
|
||||
14-day ensemble forecast
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModelSelector
|
||||
selectedModel={params.models?.[0] ?? 'ncep_gefs_seamless'}
|
||||
groups={ensembleModelGroups}
|
||||
label="Ensemble model"
|
||||
onModelChange={(model) => {
|
||||
params.models = [model];
|
||||
storedEnsembleModel.set(model);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ─── Chart Area ─────────────────────────────────────────────────────────── -->
|
||||
|
||||
{#if loadError}
|
||||
|
||||
Reference in New Issue
Block a user