From bed6e3aa53ebf059f37989203cb902a5934a3243 Mon Sep 17 00:00:00 2001 From: terraputix Date: Mon, 16 Feb 2026 11:46:53 +0100 Subject: [PATCH] feat: model comparison picto timeline --- src/lib/services/weather.ts | 10 +- .../weather/compare/[location]/+page.svelte | 33 ++-- .../[location]/ModelPictogramTimeline.svelte | 152 ++++++++++++++++++ src/routes/weather/utils/weather-codes.ts | 6 + .../week/[location]/HourlyTable.svelte | 8 +- 5 files changed, 190 insertions(+), 19 deletions(-) create mode 100644 src/routes/weather/compare/[location]/ModelPictogramTimeline.svelte diff --git a/src/lib/services/weather.ts b/src/lib/services/weather.ts index 7641305..b4577e6 100644 --- a/src/lib/services/weather.ts +++ b/src/lib/services/weather.ts @@ -222,6 +222,8 @@ export interface ModelCompareResult { utcOffsetSeconds: number; timezone: string; markAreas: MarkArea[]; + sunrise: number[]; + sunset: number[]; units: Record; /** Flat record compatible with the existing chart utilities (keys like "temperature_2m_icon_seamless") */ hourlyFlat: Record; @@ -408,12 +410,14 @@ export async function fetchModelComparison( // Extract sunrise/sunset from the first response's daily block let markAreas: MarkArea[] = []; + let sunrise: number[] = []; + let sunset: number[] = []; const dailyBlock = firstResponse.daily(); if (dailyBlock) { const sunriseVar = dailyBlock.variables(0)!; const sunsetVar = dailyBlock.variables(1)!; - const sunrise = getInt64Values(sunriseVar); - const sunset = getInt64Values(sunsetVar); + sunrise = getInt64Values(sunriseVar); + sunset = getInt64Values(sunsetVar); markAreas = buildDaylightMarkAreas(sunrise, sunset); } @@ -471,6 +475,8 @@ export async function fetchModelComparison( utcOffsetSeconds, timezone, markAreas, + sunrise, + sunset, units, hourlyFlat, hourlyUnitsFlat diff --git a/src/routes/weather/compare/[location]/+page.svelte b/src/routes/weather/compare/[location]/+page.svelte index 342b963..b2675fe 100644 --- a/src/routes/weather/compare/[location]/+page.svelte +++ b/src/routes/weather/compare/[location]/+page.svelte @@ -29,6 +29,7 @@ import { hourly, models as modelsFlat } from '../../options'; import { defaultParameters } from '../../options'; + import ModelPictogramTimeline from './ModelPictogramTimeline.svelte'; import type * as echarts from 'echarts'; @@ -79,6 +80,8 @@ timezone: string; markAreas: MarkArea[]; timestamps: number[]; + sunrise: number[]; + sunset: number[]; } let fetchedData: FetchedData | null = $state(null); @@ -119,7 +122,7 @@ const result: ModelCompareResult = await fetchModelComparison({ latitude: loc.latitude!, longitude: loc.longitude!, - hourlyVariables: hourlyVars, + hourlyVariables: [...new Set([...hourlyVars, 'weather_code'])], models: modelList, temperature_unit: params.temperature_unit as 'celsius' | 'fahrenheit', wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn', @@ -132,7 +135,9 @@ hourly_units: result.hourlyUnitsFlat, timezone: result.timezone, markAreas: result.markAreas, - timestamps: result.timestamps + timestamps: result.timestamps, + sunrise: result.sunrise, + sunset: result.sunset }; loading = false; @@ -150,12 +155,13 @@ const _showLegend = showLegend; const colors = getThemeColors(); - const variableCount = params.hourly?.length || 0; + const chartVariables = params.hourly?.filter((v) => v !== 'weather_code') || []; + const variableCount = chartVariables.length; const timeLength = timestamps.length; const newOptions: Array> = []; for (let vi = 0; vi < variableCount; vi++) { - const variable = params.hourly![vi]; + const variable = chartVariables[vi]; const unit = findUnit(hourly_units, hourlyData, variable); const series: Array> = []; @@ -195,7 +201,7 @@ title: isFirst ? { text: 'Model Compare', - subtext: `Compare ${params.hourly?.join(', ') || ''} in models: ${params.models?.join(', ') || ''}` + subtext: `Compare ${chartVariables.join(', ') || ''} in models: ${params.models?.join(', ') || ''}` } : null, tooltip: { unit, timezone }, @@ -222,11 +228,7 @@ - + {#each chartOptions as option, i (i)} +{#if fetchedData && !loading} + } + models={params.models || []} + sunrise={fetchedData.sunrise} + sunset={fetchedData.sunset} + timezone={fetchedData.timezone} + /> +{/if} +
diff --git a/src/routes/weather/compare/[location]/ModelPictogramTimeline.svelte b/src/routes/weather/compare/[location]/ModelPictogramTimeline.svelte new file mode 100644 index 0000000..22567c5 --- /dev/null +++ b/src/routes/weather/compare/[location]/ModelPictogramTimeline.svelte @@ -0,0 +1,152 @@ + + +{#snippet weatherIcon(name: string, size: number = 24)} + + + +{/snippet} + +{#if displayModels.length > 0} +
+
+

Model Comparison Timeline

+
+ 3h + + 1h +
+
+
+ + + + + {#each filteredIndices as idx, i} + {@const ts = timestamps[idx]} + {@const isNewDay = checkNewDay(i, ts)} + + {/each} + + + + {#each displayModels as model} + + + {#each filteredIndices as idx, i} + {@const ts = timestamps[idx]} + {@const codes = hourlyFlat[`weather_code_${model}`]} + {@const code = codes ? codes[idx] : 0} + {@const day = allDaytimeFlags[idx]} + {@const isNewDay = checkNewDay(i, ts)} + + {/each} + + {/each} + +
+ Model + +
{formatZoned(new Date(ts), timezone, 'HH')}
+
+ {isNewDay ? formatZoned(new Date(ts), timezone, 'EEE d') : ''} +
+
+ {model.replace(/_/g, ' ')} + + {@render weatherIcon(getWeatherIconName(code, day))} +
+
+
+{/if} + + diff --git a/src/routes/weather/utils/weather-codes.ts b/src/routes/weather/utils/weather-codes.ts index 9e7b191..e45726f 100644 --- a/src/routes/weather/utils/weather-codes.ts +++ b/src/routes/weather/utils/weather-codes.ts @@ -80,4 +80,10 @@ const weatherCodes: Record = { 99: 'tornado' }; +export function getWeatherIconName(code: number, daytime: boolean): string { + const prefix = daytime ? 'day' : 'night'; + const name = weatherCodes[code as keyof typeof weatherCodes] ?? 'clear'; + return `wi-${prefix}-${name}`; +} + export default weatherCodes; diff --git a/src/routes/weather/week/[location]/HourlyTable.svelte b/src/routes/weather/week/[location]/HourlyTable.svelte index d57bada..a7f90b1 100644 --- a/src/routes/weather/week/[location]/HourlyTable.svelte +++ b/src/routes/weather/week/[location]/HourlyTable.svelte @@ -2,7 +2,7 @@ import { formatUtcOffset, formatZoned, getZonedHour, isSameDayInZone } from '$lib/utils/date'; import { getTempStyle } from '../../utils/colors'; - import weatherCodes from '../../utils/weather-codes'; + import { getWeatherIconName } from '../../utils/weather-codes'; import { type FetchedDaily, type FetchedHourly, @@ -159,12 +159,6 @@ let sunsetPercent = $derived( sunTimes && cellData.length > 0 ? timeToFraction(sunTimes.sunset) * 100 : null ); - - function getWeatherIconName(code: number, daytime: boolean): string { - const prefix = daytime ? 'day' : 'night'; - const name = weatherCodes[code as keyof typeof weatherCodes] ?? 'clear'; - return `wi-${prefix}-${name}`; - } {#snippet weatherIcon(name: string, size: number = 16)}