From 6b48dd6764da6addcd1baaf0da79fb0d82d7ebfa Mon Sep 17 00:00:00 2001 From: frederic Date: Sun, 15 Feb 2026 15:53:54 +0100 Subject: [PATCH] fix: type errors (#3) Co-authored-by: terraputix Reviewed-on: https://gitea.servert.ch/useweb/ombrella/pulls/3 --- src/lib/stores/url-hash-store.ts | 24 - src/routes/weather/+layout.svelte | 13 +- src/routes/weather/+page.ts | 2 +- src/routes/weather/14-day/+page.svelte | 392 ++++++++-------- src/routes/weather/14-day/options.ts | 28 +- src/routes/weather/compare/+page.svelte | 435 +++++++++--------- src/routes/weather/compare/options.ts | 28 +- src/routes/weather/utils/weather-codes.ts | 4 +- src/routes/weather/week/+page.ts | 2 +- .../weather/week/[location]/+page.svelte | 191 ++++---- src/routes/weather/week/[location]/+page.ts | 28 +- 11 files changed, 569 insertions(+), 578 deletions(-) delete mode 100644 src/lib/stores/url-hash-store.ts diff --git a/src/lib/stores/url-hash-store.ts b/src/lib/stores/url-hash-store.ts deleted file mode 100644 index 676ab4c..0000000 --- a/src/lib/stores/url-hash-store.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { writable } from 'svelte/store'; - -// Placeholder function for urlHashStore -// In a real application, this would handle URL hash parameters -// and return a Svelte store that reflects those parameters. -export function urlHashStore(initialValue: Record) { - const { subscribe, set, update } = writable(initialValue); - - // In a full implementation, you would add logic here to: - // 1. Read the URL hash on initialization - // 2. Parse the hash into an object - // 3. Update the store with these values - // 4. Listen for changes to the store and update the URL hash accordingly - // 5. Listen for URL hash changes (e.g., back/forward buttons) and update the store - - return { - subscribe, - set, - update, - // You might want to add methods to easily update specific hash parameters - updateParam: (key: string, value: unknown) => - update((current) => ({ ...current, [key]: value })) - }; -} diff --git a/src/routes/weather/+layout.svelte b/src/routes/weather/+layout.svelte index 7cc664c..c6723cb 100644 --- a/src/routes/weather/+layout.svelte +++ b/src/routes/weather/+layout.svelte @@ -16,9 +16,16 @@ let { children }: Props = $props(); + interface CurrentWeather { + current: { + temperature_2m: number; + weather_code: number; + }; + } + let location = $state(get(storedLocation)); let mounted = $state(false); - let currentWeather = $state(null); + let currentWeather = $state(null); // Subscribe to location changes storedLocation.subscribe((value) => { @@ -47,8 +54,8 @@ } }; - const getWeatherIcon = (code) => { - const iconMap = { + const getWeatherIcon = (code: number): string => { + const iconMap: Record = { 0: '☀️', 1: '🌤️', 2: '⛅', diff --git a/src/routes/weather/+page.ts b/src/routes/weather/+page.ts index e65096f..9c6e182 100644 --- a/src/routes/weather/+page.ts +++ b/src/routes/weather/+page.ts @@ -1,6 +1,6 @@ import { redirect } from '@sveltejs/kit'; -import type { PageLoad } from '$types'; +import type { PageLoad } from './$types'; export const prerender = true; diff --git a/src/routes/weather/14-day/+page.svelte b/src/routes/weather/14-day/+page.svelte index 2c91724..85227ab 100644 --- a/src/routes/weather/14-day/+page.svelte +++ b/src/routes/weather/14-day/+page.svelte @@ -6,7 +6,6 @@ import { dev } from '$app/environment'; import { storedLocation } from '$lib/stores/settings'; - import { urlHashStore } from '$lib/stores/url-hash-store'; import { Label } from '$lib/components/ui/label'; import { Switch } from '$lib/components/ui/switch'; @@ -16,14 +15,15 @@ let node: HTMLElement; let chart: any; - let Highcharts = $state(null); + let Highcharts = $state(null); let showLegend = $state(false); let averageOnly = $state(false); const location = get(storedLocation); - const params = urlHashStore({ + // Local component state for chart configuration + let params = $state({ latitude: [52.52], longitude: [13.41], ...defaultParameters, @@ -41,221 +41,230 @@ if (dev) { // const HighchartsDebugger = await import('highcharts/modules/debugger'); // HighchartsDebugger.default(Highcharts); - const Debugger = (await import('highcharts/es-modules/Extensions/Debugger/Debugger.js')) - .default; - const ErrorMessages = ( - await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js') + const Debugger = ( + await import('highcharts/es-modules/Extensions/Debugger/Debugger.js' as any) ).default; - Highcharts.errorMessages = ErrorMessages; - Debugger.compose(Highcharts.Chart); + const ErrorMessages = ( + await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js' as any) + ).default; + if (Highcharts) { + (Highcharts as any).errorMessages = ErrorMessages; + Debugger.compose(Highcharts.Chart); + } } }); - $effect(async () => { - count = 0; - if (Highcharts) { - node.replaceChildren([]); + $effect(() => { + const loadData = async () => { + count = 0; + if (Highcharts) { + node.replaceChildren(); - const dataDaily = await fetch( - `https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&timeformat=unixtime&daily=sunset,sunrise&forecast_days=14` - ); - const wd = await dataDaily.json(); + const dataDaily = await fetch( + `https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&timeformat=unixtime&daily=sunset,sunrise&forecast_days=14` + ); + const wd = await dataDaily.json(); - const dataReq = await fetch( - `https://ensemble-api.open-meteo.com/v1/ensemble?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${$params.hourly.join(',')}&models=${$params.models.join(',')}&timeformat=unixtime&forecast_days=14` - ); - const data = await dataReq.json(); + const dataReq = await fetch( + `https://ensemble-api.open-meteo.com/v1/ensemble?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${params.hourly?.join(',') || ''}&models=${params.models?.join(',') || ''}&timeformat=unixtime&forecast_days=14` + ); + const data = await dataReq.json(); - let plotBands: any = []; - if ('daily' in wd && 'sunrise' in wd.daily && 'sunset' in wd.daily) { - let rise = wd.daily.sunrise; - let set = wd.daily.sunset; - plotBands = rise.map(function (r, i) { - return { - color: 'rgba(255, 255, 194, 0.5)', - from: (r + data.utc_offset_seconds) * 1000, - to: (set[i] + data.utc_offset_seconds) * 1000 - }; - }); - } + let plotBands: any = []; + if ('daily' in wd && 'sunrise' in wd.daily && 'sunset' in wd.daily) { + let rise = wd.daily.sunrise; + let set = wd.daily.sunset; + plotBands = rise.map(function (r: any, i: number) { + return { + color: 'rgba(255, 255, 194, 0.5)', + from: (r + data.utc_offset_seconds) * 1000, + to: (set[i] + data.utc_offset_seconds) * 1000 + }; + }); + } - let minValues = new Array(data.hourly.time.length).fill(undefined); - let maxValues = new Array(data.hourly.time.length).fill(undefined); + let minValues = new Array(data.hourly.time.length).fill(undefined); + let maxValues = new Array(data.hourly.time.length).fill(undefined); - for (let variable of $params.hourly) { - const chartDiv = document.createElement('div'); + for (let variable of params.hourly || []) { + const chartDiv = document.createElement('div'); - let unit; + let unit; - let hourly_starttime = (data.hourly.time[0] + data.utc_offset_seconds) * 1000; - let pointInterval = (data.hourly.time[1] - data.hourly.time[0]) * 1000; + let hourly_starttime = (data.hourly.time[0] + data.utc_offset_seconds) * 1000; + let pointInterval = (data.hourly.time[1] - data.hourly.time[0]) * 1000; - const series = []; - let average = new Array(data.hourly.time.length).fill(0); - let averageCount = new Array(data.hourly.time.length).fill(0); + const series = []; + let average = new Array(data.hourly.time.length).fill(0); + let averageCount = new Array(data.hourly.time.length).fill(0); - for (let [model, values] of Object.entries(data.hourly)) { - if (model === 'time') { - continue; - } - if (model.startsWith(variable)) { - for (let [index, val] of values.entries()) { - if (val) { - let avVal = average[index]; - average[index] = avVal + val; - averageCount[index]++; + for (let [model, values] of Object.entries(data.hourly)) { + if (model === 'time') { + continue; + } + if (model.startsWith(variable)) { + for (let [index, val] of (values as any[]).entries()) { + if (val) { + let avVal = average[index]; + average[index] = avVal + val; + averageCount[index]++; - if (minValues[index] > val || minValues[index] === undefined) { - minValues[index] = val; - } - if (maxValues[index] < val || maxValues[index] === undefined) { - maxValues[index] = val; + if (minValues[index] > val || minValues[index] === undefined) { + minValues[index] = val; + } + if (maxValues[index] < val || maxValues[index] === undefined) { + maxValues[index] = val; + } } } - } - unit = data.hourly_units[model]; + unit = data.hourly_units[model]; + } } - } - for (let [index, val] of average.entries()) { - average[index] = Math.round((val / averageCount[index]) * 10) / 10; - } + for (let [index, val] of average.entries()) { + average[index] = Math.round((val / averageCount[index]) * 10) / 10; + } - const minMax = []; - for (let [index, min] of minValues.entries()) { - minMax.push([min, maxValues[index]]); - } + const minMax = []; + for (let [index, min] of minValues.entries()) { + minMax.push([min, maxValues[index]]); + } - series.push({ - name: 'temperature_2m_spread', - data: minMax, - type: 'arearange', - tooltip: { - valueSuffix: ' ' + unit - }, - pointStart: hourly_starttime, - pointInterval: pointInterval, - className: 'highcharts-spread-series' - }); + series.push({ + name: 'temperature_2m_spread', + data: minMax, + type: 'arearange', + tooltip: { + valueSuffix: ' ' + unit + }, + pointStart: hourly_starttime, + pointInterval: pointInterval, + className: 'highcharts-spread-series' + }); - series.push({ - name: variable + '_average', - data: average, - dashStyle: 'ShortDashDot', - color: '#5e5e5e', - type: - unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²' ? 'column' : 'spline', - tooltip: { - valueSuffix: ' ' + unit - }, - lineWidth: 4, - states: { - hover: { - lineWidth: 6 - } - }, - pointStart: hourly_starttime, - pointInterval: pointInterval, - className: 'highcharts-average-series' - }); - - new Highcharts.Chart(chartDiv, { - credits: { - text: count === $params.hourly.length - 1 ? 'Open-Meteo.com' : '', - href: 'http://open-meteo.com' - }, - - chart: { - height: showLegend ? '400px' : '300px', - styledMode: true, - marginLeft: '50', - marginRight: 0 - }, - - lang: { - locale: 'en-GB' - }, - - title: { - text: count === 0 ? 'Model Spread' : '', - align: 'left' - }, - - subtitle: { - text: - count === 0 - ? `Compare ${$params.hourly.join(', ')} in models: ` + - $params.models.join(', ') + - '' - : '', - align: 'left' - }, - - yAxis: { - title: { - text: unit - } - }, - - xAxis: { - type: 'datetime', - plotLines: [ - { - value: Date.now() + data.utc_offset_seconds * 1000, - color: 'red', - width: 2 - } - ], - plotBands: plotBands - }, - - plotOptions: { - spline: { - lineWidth: 2, - states: { - hover: { - lineWidth: 3 - } - }, - marker: { - enabled: false + series.push({ + name: variable + '_average', + data: average, + dashStyle: 'ShortDashDot', + color: '#5e5e5e', + type: + unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²' + ? 'column' + : 'spline', + tooltip: { + valueSuffix: ' ' + unit + }, + lineWidth: 4, + states: { + hover: { + lineWidth: 6 } }, - column: { - pointWidth: 5 - } - }, + pointStart: hourly_starttime, + pointInterval: pointInterval, + className: 'highcharts-average-series' + }); - legend: { - enabled: showLegend, - layout: 'horizontal', - align: 'center', - verticalAlign: 'bottom' - }, + new Highcharts!.Chart({ + chart: { + renderTo: chartDiv, + height: showLegend ? '400px' : '300px', + styledMode: true, + marginLeft: 50, + marginRight: 0 + }, - series: series, + credits: { + text: count === (params.hourly?.length || 0) - 1 ? 'Open-Meteo.com' : '', + href: 'http://open-meteo.com' + }, - responsive: { - rules: [ - { - condition: { - maxWidth: 800 - } + lang: { + locale: 'en-GB' + }, + + title: { + text: count === 0 ? 'Model Spread' : '', + align: 'left' + }, + + subtitle: { + text: + count === 0 + ? `Compare ${params.hourly?.join(', ') || ''} in models: ` + + (params.models?.join(', ') || '') + + '' + : '', + align: 'left' + }, + + yAxis: { + title: { + text: unit } - ] - }, + }, - tooltip: { - shared: true, - animation: false - } - }); + xAxis: { + type: 'datetime', + plotLines: [ + { + value: Date.now() + data.utc_offset_seconds * 1000, + color: 'red', + width: 2 + } + ], + plotBands: plotBands + }, - count++; - node.appendChild(chartDiv); + plotOptions: { + spline: { + lineWidth: 2, + states: { + hover: { + lineWidth: 3 + } + }, + marker: { + enabled: false + } + }, + column: { + pointWidth: 5 + } + }, + + legend: { + enabled: showLegend, + layout: 'horizontal', + align: 'center', + verticalAlign: 'bottom' + }, + + series: series as any, + + responsive: { + rules: [ + { + condition: { + maxWidth: 800 + } + } + ] + }, + + tooltip: { + shared: true, + animation: false + } + }); + + count++; + node.appendChild(chartDiv); + } } - } + }; + loadData(); }); onDestroy(() => { @@ -266,7 +275,10 @@ -
+
{ - $params.hourly = $params.hourly; + params.hourly = params.hourly; }} /> @@ -310,7 +322,7 @@ name="Average only" bind:checked={averageOnly} onCheckedChange={() => { - $params.hourly = $params.hourly; + params.hourly = params.hourly; }} /> diff --git a/src/routes/weather/14-day/options.ts b/src/routes/weather/14-day/options.ts index 8727075..970b98d 100644 --- a/src/routes/weather/14-day/options.ts +++ b/src/routes/weather/14-day/options.ts @@ -1,31 +1,5 @@ +// Default configuration for 14-day ensemble forecast charts export const defaultParameters = { - daily: [], - hourly: [], - models: [], - current: [], - minutely_15: [], - - timezone: 'UTC', - location_mode: 'location_search', - csv_coordinates: undefined, - - time_mode: 'forecast_days', - past_days: '0', - forecast_days: '14', - - end_date: undefined, - start_date: undefined, - - past_hours: undefined, - cell_selection: undefined, - forecast_hours: undefined, - past_minutely_15: undefined, - temporal_resolution: undefined, - forecast_minutely_15: undefined, - - tilt: '0', - azimuth: '0', - timeformat: 'iso8601', wind_speed_unit: 'kmh', temperature_unit: 'celsius', diff --git a/src/routes/weather/compare/+page.svelte b/src/routes/weather/compare/+page.svelte index 4d86ae4..25406ca 100644 --- a/src/routes/weather/compare/+page.svelte +++ b/src/routes/weather/compare/+page.svelte @@ -6,26 +6,28 @@ import { dev } from '$app/environment'; import { storedLocation } from '$lib/stores/settings'; - import { urlHashStore } from '$lib/stores/url-hash-store'; import { Checkbox } from '$lib/components/ui/checkbox'; import { Label } from '$lib/components/ui/label'; import { Switch } from '$lib/components/ui/switch'; - import { hourly, models } from '../options'; + import { hourly, models as modelsFlat } from '../options'; import './highcharts.css'; import { defaultParameters } from './options'; + // Wrap models in array to match template expectation of nested arrays like hourly + const models = [modelsFlat]; + let node: HTMLElement; let chart: any; - let Highcharts = $state(); + let Highcharts = $state(null); let showLegend = $state(false); let averageOnly = $state(false); const location = get(storedLocation); - const params = urlHashStore({ + let params = $state({ latitude: [52.52], longitude: [13.41], ...defaultParameters, @@ -47,213 +49,222 @@ if (dev) { // const HighchartsDebugger = await import('highcharts/modules/debugger'); // HighchartsDebugger.default(Highcharts); - const Debugger = (await import('highcharts/es-modules/Extensions/Debugger/Debugger.js')) - .default; - const ErrorMessages = ( - await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js') + const Debugger = ( + await import('highcharts/es-modules/Extensions/Debugger/Debugger.js' as any) ).default; - Highcharts.errorMessages = ErrorMessages; - Debugger.compose(Highcharts.Chart); + const ErrorMessages = ( + await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js' as any) + ).default; + if (Highcharts) { + (Highcharts as any).errorMessages = ErrorMessages; + Debugger.compose(Highcharts.Chart); + } } }); - $effect(async () => { - count = 0; - if (Highcharts) { - node.replaceChildren([]); + $effect(() => { + const loadData = async () => { + count = 0; + if (Highcharts) { + node.replaceChildren(); - const dataReq = await fetch( - `https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${$params.hourly.join(',')}&models=${$params.models.join(',')}&timeformat=unixtime&daily=sunset,sunrise` - ); - const data = await dataReq.json(); + const dataReq = await fetch( + `https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${params.hourly?.join(',') || ''}&models=${params.models?.join(',') || ''}&timeformat=unixtime&daily=sunset,sunrise` + ); + const data = await dataReq.json(); - let dailyFirstModelKey = Object.keys(data.daily)[1].split('_'); - dailyFirstModelKey.shift(); - dailyFirstModelKey = dailyFirstModelKey.join('_'); + let dailyFirstModelKey: string[] | string = Object.keys(data.daily)[1].split('_'); + dailyFirstModelKey.shift(); + dailyFirstModelKey = dailyFirstModelKey.join('_'); - let plotBands: any = []; - if ( - 'daily' in data && - 'sunrise_' + dailyFirstModelKey in data.daily && - 'sunset_' + dailyFirstModelKey in data.daily - ) { - let rise = data.daily['sunrise_' + dailyFirstModelKey]; - let set = data.daily['sunset_' + dailyFirstModelKey]; - plotBands = rise.map(function (r, i) { - return { - color: 'rgba(255, 255, 194, 0.5)', - from: (r + data.utc_offset_seconds) * 1000, - to: (set[i] + data.utc_offset_seconds) * 1000 - }; - }); - } - - for (let variable of $params.hourly) { - const chartDiv = document.createElement('div'); - - let unit; - - let hourly_starttime = (data.hourly.time[0] + data.utc_offset_seconds) * 1000; - let pointInterval = (data.hourly.time[1] - data.hourly.time[0]) * 1000; - - const series = []; - let average = new Array(data.hourly.time.length).fill(0); - let averageCount = new Array(data.hourly.time.length).fill(0); - - for (let [model, values] of Object.entries(data.hourly)) { - if (model === 'time') { - continue; - } - if (model.startsWith(variable)) { - for (let [index, val] of values.entries()) { - if (val) { - let avVal = average[index]; - average[index] = avVal + val; - averageCount[index]++; - } - } - - unit = data.hourly_units[model]; - - if (!averageOnly) { - series.push({ - name: model, - data: values, - type: - unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²' - ? 'column' - : 'spline', - tooltip: { - valueSuffix: ' ' + unit - }, - pointStart: hourly_starttime, - pointInterval: pointInterval - }); - } - } + let plotBands: any = []; + if ( + 'daily' in data && + 'sunrise_' + dailyFirstModelKey in data.daily && + 'sunset_' + dailyFirstModelKey in data.daily + ) { + let rise = data.daily['sunrise_' + dailyFirstModelKey]; + let set = data.daily['sunset_' + dailyFirstModelKey]; + plotBands = rise.map(function (r: any, i: number) { + return { + color: 'rgba(255, 255, 194, 0.5)', + from: (r + data.utc_offset_seconds) * 1000, + to: (set[i] + data.utc_offset_seconds) * 1000 + }; + }); } - for (let [index, val] of average.entries()) { - average[index] = Math.round((val / averageCount[index]) * 10) / 10; - } + for (let variable of params.hourly || []) { + const chartDiv = document.createElement('div'); - series.push({ - name: variable + '_average', - data: average, - dashStyle: 'ShortDashDot', - color: '#5e5e5e', - type: - unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²' ? 'column' : 'spline', - tooltip: { - valueSuffix: ' ' + unit - }, - lineWidth: 4, - states: { - hover: { - lineWidth: 6 + let unit; + + let hourly_starttime = (data.hourly.time[0] + data.utc_offset_seconds) * 1000; + let pointInterval = (data.hourly.time[1] - data.hourly.time[0]) * 1000; + + const series = []; + let average = new Array(data.hourly.time.length).fill(0); + let averageCount = new Array(data.hourly.time.length).fill(0); + + for (let [model, values] of Object.entries(data.hourly)) { + if (model === 'time') { + continue; } - }, - pointStart: hourly_starttime, - pointInterval: pointInterval, - className: 'highcharts-average-series' - }); - - new Highcharts.Chart(chartDiv, { - credits: { - text: count === $params.hourly.length - 1 ? 'Open-Meteo.com' : '', - href: 'http://open-meteo.com' - }, - - chart: { - height: showLegend ? '400px' : '300px', - styledMode: true, - marginLeft: '50', - marginRight: 0 - }, - - lang: { - locale: 'en-GB' - }, - - title: { - text: count === 0 ? 'Model Compare' : '', - align: 'left' - }, - - subtitle: { - text: - count === 0 - ? `Compare ${$params.hourly.join(', ')} in models: ` + - $params.models.join(', ') + - '' - : '', - align: 'left' - }, - - yAxis: { - title: { - text: unit - } - }, - - xAxis: { - type: 'datetime', - plotLines: [ - { - value: Date.now() + data.utc_offset_seconds * 1000, - color: 'red', - width: 2 - } - ], - plotBands: plotBands - }, - - plotOptions: { - spline: { - lineWidth: 2, - states: { - hover: { - lineWidth: 3 + if (model.startsWith(variable)) { + for (let [index, val] of (values as any[]).entries()) { + if (val) { + let avVal = average[index]; + average[index] = avVal + val; + averageCount[index]++; } - }, - marker: { - enabled: false + } + + unit = data.hourly_units[model]; + + if (!averageOnly) { + series.push({ + name: model, + data: values, + type: + unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²' + ? 'column' + : 'spline', + tooltip: { + valueSuffix: ' ' + unit + }, + pointStart: hourly_starttime, + pointInterval: pointInterval + }); + } + } + } + + for (let [index, val] of average.entries()) { + average[index] = Math.round((val / averageCount[index]) * 10) / 10; + } + + series.push({ + name: variable + '_average', + data: average, + dashStyle: 'ShortDashDot', + color: '#5e5e5e', + type: + unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²' + ? 'column' + : 'spline', + tooltip: { + valueSuffix: ' ' + unit + }, + lineWidth: 4, + states: { + hover: { + lineWidth: 6 } }, - column: { - pointWidth: 5 - } - }, + pointStart: hourly_starttime, + pointInterval: pointInterval, + className: 'highcharts-average-series' + }); - legend: { - enabled: showLegend, - layout: 'horizontal', - align: 'center', - verticalAlign: 'bottom' - }, + new Highcharts!.Chart({ + chart: { + renderTo: chartDiv, + height: showLegend ? '400px' : '300px', + styledMode: true, + marginLeft: 50, + marginRight: 0 + }, - series: series, + credits: { + text: count === (params.hourly?.length || 0) - 1 ? 'Open-Meteo.com' : '', + href: 'http://open-meteo.com' + }, - responsive: { - rules: [ - { - condition: { - maxWidth: 800 - } + lang: { + locale: 'en-GB' + }, + + title: { + text: count === 0 ? 'Model Compare' : '', + align: 'left' + }, + + subtitle: { + text: + count === 0 + ? `Compare ${params.hourly?.join(', ') || ''} in models: ` + + (params.models?.join(', ') || '') + + '' + : '', + align: 'left' + }, + + yAxis: { + title: { + text: unit } - ] - }, + }, - tooltip: { - shared: true, - animation: false - } - }); + xAxis: { + type: 'datetime', + plotLines: [ + { + value: Date.now() + data.utc_offset_seconds * 1000, + color: 'red', + width: 2 + } + ], + plotBands: plotBands + }, - count++; - node.appendChild(chartDiv); + plotOptions: { + spline: { + lineWidth: 2, + states: { + hover: { + lineWidth: 3 + } + }, + marker: { + enabled: false + } + }, + column: { + pointWidth: 5 + } + }, + + legend: { + enabled: showLegend, + layout: 'horizontal', + align: 'center', + verticalAlign: 'bottom' + }, + + series: series as any, + + responsive: { + rules: [ + { + condition: { + maxWidth: 800 + } + } + ] + }, + + tooltip: { + shared: true, + animation: false + } + }); + + count++; + node.appendChild(chartDiv); + } } - } + }; + loadData(); }); onDestroy(() => { @@ -264,7 +275,10 @@ -
+
{ - $params.hourly = $params.hourly; + params.hourly = params.hourly; }} /> @@ -308,7 +322,7 @@ name="Average only" bind:checked={averageOnly} onCheckedChange={() => { - $params.hourly = $params.hourly; + params.hourly = params.hourly; }} /> @@ -319,12 +333,12 @@

Models

- {#if $params.models.length > 0} + {#if params.models && params.models.length > 0}
- {$params.models.length} / {models.flat().length} + {params.models?.length || 0} / {models.flat().length}
{/if} @@ -333,22 +347,23 @@
{#each models as group, i (i)}
- {#each group as { value, label } (value)} + {#each group as item (item.value)} + {@const { value, label } = item as { value: string; label: string }}
{ - if ($params.models?.includes(value)) { - $params.models = $params.models.filter((item) => { + if (params.models?.includes(value)) { + params.models = params.models.filter((item) => { return item !== value; }); - } else if ($params.models) { - $params.models.push(value); - $params.models = $params.models; + } else if (params.models) { + params.models.push(value); + params.models = params.models; } }} /> @@ -371,12 +386,12 @@ Hourly Weather Variables - {#if $params.hourly.length > 0} + {#if params.hourly && params.hourly.length > 0}
- {$params.hourly.length} / {hourly.flat().length} + {params.hourly?.length || 0} / {hourly.flat().length}
{/if} @@ -393,16 +408,16 @@ id="{value}_hourly" class="border-border-dark cursor-pointer bg-muted/50 duration-100 group-hover:border-[currentColor]" {value} - checked={$params.hourly?.includes(value)} + checked={params.hourly?.includes(value)} aria-labelledby="{value}_label" onCheckedChange={() => { - if ($params.hourly?.includes(value)) { - $params.hourly = $params.hourly.filter((item) => { + if (params.hourly?.includes(value)) { + params.hourly = params.hourly.filter((item) => { return item !== value; }); - } else if ($params.hourly) { - $params.hourly.push(value); - $params.hourly = $params.hourly; + } else if (params.hourly) { + params.hourly.push(value); + params.hourly = params.hourly; } }} /> diff --git a/src/routes/weather/compare/options.ts b/src/routes/weather/compare/options.ts index a0a4378..463bab5 100644 --- a/src/routes/weather/compare/options.ts +++ b/src/routes/weather/compare/options.ts @@ -1,31 +1,5 @@ +// Default configuration for weather comparison charts export const defaultParameters = { - daily: [], - hourly: [], - models: [], - current: [], - minutely_15: [], - - timezone: 'UTC', - location_mode: 'location_search', - csv_coordinates: undefined, - - time_mode: 'forecast_days', - past_days: '0', - forecast_days: '7', - - end_date: undefined, - start_date: undefined, - - past_hours: undefined, - cell_selection: undefined, - forecast_hours: undefined, - past_minutely_15: undefined, - temporal_resolution: undefined, - forecast_minutely_15: undefined, - - tilt: '0', - azimuth: '0', - timeformat: 'iso8601', wind_speed_unit: 'kmh', temperature_unit: 'celsius', diff --git a/src/routes/weather/utils/weather-codes.ts b/src/routes/weather/utils/weather-codes.ts index 0ad67eb..9e7b191 100644 --- a/src/routes/weather/utils/weather-codes.ts +++ b/src/routes/weather/utils/weather-codes.ts @@ -1,4 +1,4 @@ -export default { +const weatherCodes: Record = { 0: 'clear', 1: 'clear', 2: 'cloudy', @@ -79,3 +79,5 @@ export default { 96: 'thunderstorm', 99: 'tornado' }; + +export default weatherCodes; diff --git a/src/routes/weather/week/+page.ts b/src/routes/weather/week/+page.ts index 3bf39f3..0963c36 100644 --- a/src/routes/weather/week/+page.ts +++ b/src/routes/weather/week/+page.ts @@ -6,7 +6,7 @@ import { storedLocation } from '$lib/stores/settings'; import { geoLocationNameToRoute } from '$lib/utils/meteo'; -import type { PageLoad } from '$types'; +import type { PageLoad } from './$types'; export const prerender = true; diff --git a/src/routes/weather/week/[location]/+page.svelte b/src/routes/weather/week/[location]/+page.svelte index ad4f717..79799b0 100644 --- a/src/routes/weather/week/[location]/+page.svelte +++ b/src/routes/weather/week/[location]/+page.svelte @@ -6,7 +6,6 @@ import { fetchWeatherApi } from 'openmeteo'; import { type GeoLocation, storedLocation } from '$lib/stores/settings'; - import { urlHashStore } from '$lib/stores/url-hash-store'; import { pad } from '$lib/utils/index'; @@ -22,10 +21,10 @@ import { getColor } from '../../utils/colors'; import weatherCodes from '../../utils/weather-codes'; - const params = urlHashStore({ + let params = $state({ latitude: [$storedLocation.latitude], longitude: [$storedLocation.longitude], - models: 'best_match', + models: ['best_match'], ...defaultParameters }); @@ -53,7 +52,7 @@ longitude: location.longitude, elevation: location.elevation, // timezone: location.timezone, ??? - models: [$params.models], + models: [params.models], hourly: [ 'precipitation', 'precipitation_probability', @@ -66,9 +65,9 @@ ].join(','), forecast_days: 6, past_days: 1, - temperature_unit: $params.temperature_unit, - wind_speed_unit: $params.wind_speed_unit, - precipitation_unit: $params.precipitation_unit + temperature_unit: params.temperature_unit, + wind_speed_unit: params.wind_speed_unit, + precipitation_unit: params.precipitation_unit }; const url = 'https://api.open-meteo.com/v1/forecast'; const responses = await fetchWeatherApi(url, reqParams); @@ -96,7 +95,7 @@ const maxX = 10000; const maxY = 500; - const deltaX = 10000 / hourly.variables(0)?.valuesArray()?.length; + const deltaX = 10000 / (hourly.variables(0)?.valuesArray()?.length || 1); const ctx = canvasElement?.getContext('2d'); if (ctx) { @@ -119,10 +118,10 @@ // create canvas daylight(ctx, config, hourlyTime); - raster(ctx, config, hourlyTime, today, canvasElement); - tempGradient(ctx, config, hourlyTemps, $params.temperature_unit); - cloudCover(ctx, config, hourlyCloudCover, canvasElement); - precip(ctx, config, hourlyPrecip, canvasElement); + raster(ctx, config, hourlyTime, today, canvasElement!); + tempGradient(ctx, config, hourlyTemps, params.temperature_unit); + cloudCover(ctx, config, hourlyCloudCover, canvasElement!); + precip(ctx, config, hourlyPrecip, canvasElement!); } return { @@ -134,7 +133,7 @@ values: hourly .variables(2) ?.valuesArray() - ?.map((t) => t.toFixed(1)) + ?.map((t) => Number(t.toFixed(1))) }, { id: 1, @@ -143,7 +142,7 @@ values: hourly .variables(0) ?.valuesArray() - ?.map((p) => p.toFixed(1)) + ?.map((p) => Number(p.toFixed(1))) }, { id: 2, @@ -152,7 +151,7 @@ values: hourly .variables(1) ?.valuesArray() - ?.map((p) => p.toFixed(0)) + ?.map((p) => Number(p.toFixed(0))) }, { id: 3, @@ -161,7 +160,7 @@ values: hourly .variables(4) ?.valuesArray() - ?.map((p) => p.toFixed(0)) + ?.map((p) => Number(p.toFixed(0))) }, { id: 4, @@ -170,7 +169,7 @@ values: hourly .variables(7) ?.valuesArray() - ?.map((p) => p.toFixed(0)) + ?.map((p) => Number(p.toFixed(0))) } ], entriesLength: hourly.variables(0)?.valuesArray()?.length, @@ -188,7 +187,7 @@ longitude: location.longitude, elevation: location.elevation, // timezone: location.timezone, ??? - models: [$params.models], + models: [params.models], daily: [ 'weather_code', 'temperature_2m_max', @@ -203,9 +202,9 @@ ].join(','), forecast_days: 6, past_days: 1, - temperature_unit: $params.temperature_unit, - wind_speed_unit: $params.wind_speed_unit, - precipitation_unit: $params.precipitation_unit + temperature_unit: params.temperature_unit, + wind_speed_unit: params.wind_speed_unit, + precipitation_unit: params.precipitation_unit }; const url = 'https://api.open-meteo.com/v1/forecast'; const responses = await fetchWeatherApi(url, reqParams); @@ -237,17 +236,18 @@ let winddir = true; entries = 6; - let scrollDiv: HTMLElement = $state(); + let scrollDiv: HTMLElement | undefined = $state(); let tableCells; - const switchDay = (date: SvelteDate, index: number) => { + const switchDay = (date: Date, index: number) => { selectedDay = date; tableCells = document.querySelectorAll('td.time'); for (let tableCell of tableCells) { - if (Number(tableCell.dataset['date']) === selectedDay.getDate()) { - scrollDiv.scrollTo({ left: tableCell.offsetLeft - 110, behavior: 'smooth' }); + const htmlCell = tableCell as HTMLElement; + if (Number(htmlCell.dataset['date']) === selectedDay.getDate()) { + scrollDiv?.scrollTo({ left: htmlCell.offsetLeft - 110, behavior: 'smooth' }); break; } } @@ -259,35 +259,36 @@ setTimeout(() => { tableCells = document.querySelectorAll('td.time'); for (let tableCell of tableCells) { - if (Number(tableCell.dataset['date']) === selectedDay.getDate()) { - scrollDiv.scrollTo({ left: tableCell.offsetLeft - 110 }); + const htmlCell = tableCell as HTMLElement; + if (Number(htmlCell.dataset['date']) === selectedDay.getDate()) { + scrollDiv?.scrollTo({ left: htmlCell.offsetLeft - 110 }); break; } } }, 150); document.onkeydown = (e) => { - if (!scrollDiv === document.activeElement || !scrollDiv.contains(document.activeElement)) { + if (scrollDiv !== document.activeElement && !scrollDiv?.contains(document.activeElement)) { if (e.key === 'ArrowLeft') { if (selectedDay.getDate() >= today.getDate()) { - let newDate = new SvelteDate(); + let newDate = new Date(); newDate.setDate(selectedDay.getDate() - 1); - switchDay(newDate); + switchDay(newDate, selectedDayIndex - 1); } } if (e.key === 'ArrowRight') { if (selectedDay.getDate() <= today.getDate() + 4) { - let newDate = new SvelteDate(); + let newDate = new Date(); newDate.setDate(selectedDay.getDate() + 1); - switchDay(newDate); + switchDay(newDate, selectedDayIndex + 1); } } } }; }); - let modelSelected = $derived(models.find((mo) => String(mo.value) === $params.models)); - // let modelSelectedValue = $derived($params.models[0]); + let modelSelected = $derived(models.find((mo) => String(mo.value) === params.models?.[0])); + // let modelSelectedValue = $derived(params.models[0]); // @@ -308,7 +309,9 @@ {#await weatherDaily then wd} {#each wd.daily.time as time, index (index)} {@const selected = time.getDate() === selectedDay.getDate()} - {#if !isNaN(wd.daily.temperature_2m_max.values(index).toFixed(1))} + {#if wd.daily.temperature_2m_max.values(index) != null && !isNaN(Number(wd.daily.temperature_2m_max + .values(index)! + .toFixed(1)))}
= ($params.temperature_unit === 'celsius' ? 30 : 104) ? 'white' : 'black'}`} + style={`background-color: ${getColor((wd.daily.temperature_2m_max.values(index) ?? 0).toFixed(0), params.temperature_unit)}; color: ${(wd.daily.temperature_2m_min.values(index) ?? 0) < (params.temperature_unit === 'celsius' ? 4 : 7) || (wd.daily.temperature_2m_min.values(index) ?? 0) >= (params.temperature_unit === 'celsius' ? 30 : 104) ? 'white' : 'black'}`} > - {wd.daily.temperature_2m_max.values(index).toFixed(1)} - {$params.temperature_unit === 'celsius' ? '°C' : '°F'} + {wd.daily.temperature_2m_max.values(index)?.toFixed(1)} + {params.temperature_unit === 'celsius' ? '°C' : '°F'}
= ($params.temperature_unit === 'celsius' ? 30 : 104) ? 'white' : 'black'}`} + style={`background: ${getColor((wd.daily.temperature_2m_min.values(index) ?? 0).toFixed(0), params.temperature_unit)}; color: ${(wd.daily.temperature_2m_min.values(index) ?? 0) < (params.temperature_unit === 'celsius' ? 4 : 7) || (wd.daily.temperature_2m_min.values(index) ?? 0) >= (params.temperature_unit === 'celsius' ? 30 : 104) ? 'white' : 'black'}`} > - {wd.daily.temperature_2m_min.values(index).toFixed(1)} - {$params.temperature_unit === 'celsius' ? '°C' : '°F'} + {wd.daily.temperature_2m_min.values(index)?.toFixed(1)} + {params.temperature_unit === 'celsius' ? '°C' : '°F'}
@@ -369,7 +372,7 @@
- {Number(wd.daily.sunshine_duration.values(index) / 3600).toFixed(0)}h + {Number((wd.daily.sunshine_duration.values(index) ?? 0) / 3600).toFixed(0)}h
@@ -384,7 +387,7 @@
{Number(wd.daily.precipitation_sum.values(index)).toFixed( 1 - )}{$params.precipitation_unit === 'mm' ? 'mm' : "'"} + )}{params.precipitation_unit === 'mm' ? 'mm' : "'"}
@@ -442,8 +445,9 @@ data-time={weather.hourlyTime[index].getHours() + ':00'} style="font-size: 11px; position: absolute; bottom: {188 + 27 * entries}px; left:{111 + - (5000 / weather.entriesLength) * index}px; min-width: {5000 / - weather.entriesLength}px; max-width: {5000 / weather.entriesLength}px;" + (5000 / (weather.entriesLength || 1)) * index}px; min-width: {5000 / + (weather.entriesLength || 1)}px; max-width: {5000 / + (weather.entriesLength || 1)}px;" >{weather.hourlyTime[index].getHours() < 10 ? '0' : ''}{weather.hourlyTime[ index ].getHours()} @@ -491,9 +496,9 @@ >Temp graph {#each weather.indexes as index, j (j)} - {@const temp = weather.entries[0].values[index]} + {@const temp = weather.entries?.[0]?.values?.[index]} - {#if !isNaN(temp)} + {#if temp !== undefined && !isNaN(temp)} {temp.toFixed(0)}{temp?.toFixed(0)} {/if} {/each} @@ -520,49 +525,49 @@ > {#each weather.indexes as index, j (j)} - {#if !isNaN(entry.values[index])} + {#if entry.values && !isNaN(entry.values[index])} = - ($params.temperature_unit === 'celsius' ? 40 : 104) + (weather.entries[0].values![index] < + (params.temperature_unit === 'celsius' ? -13 : 7) || + weather.entries[0].values![index] >= + (params.temperature_unit === 'celsius' ? 40 : 104) ? 'white' : 'black') : ''}; {entry.name === 'precipitation_probability' ? 'background: rgba(0, 0, 230,' + - weather.entries[2].values[index] / 120 + + weather.entries[2].values![index] / 120 + ')' : ''}; {entry.name === 'precipitation_probability' ? 'color: ' + - (weather.entries[2].values[index] > 50 + (weather.entries[2].values![index] > 50 ? 'white' : 'hsl(var(--foreground)') : ''}; {entry.name === 'relative_humidity_2m' ? 'background: rgba(0, 240, 240,' + - weather.entries[4].values[index] ** 3.8 / 10 ** 8.2 + + weather.entries[4].values![index] ** 3.8 / 10 ** 8.2 + ')' : ''};" >{entry.name === 'precipitation' || entry.name === 'temperature_2m' - ? entry.values[index].toFixed(1) - : entry.values[index]} {/if} {/each} @@ -578,16 +583,16 @@ >Wind Dir. {#each weather.indexes as index, j (j)} - {#if !isNaN(weather.windDirections[index])} + {#if weather.windDirections && !isNaN(weather.windDirections[index])}
- - {modelSelected?.label} 0} + {@const modelValue = params.models[0]} + { + if (params.models && val) { + params.models = [val]; + } + }} > - - {#each models as mo (mo.value)} - {mo.label} - {/each} - - - + {modelSelected?.label} + + {#each models as mo (mo.value)} + {mo.label} + {/each} + + + + {/if}
diff --git a/src/routes/weather/week/[location]/+page.ts b/src/routes/weather/week/[location]/+page.ts index 1f26625..e321db1 100644 --- a/src/routes/weather/week/[location]/+page.ts +++ b/src/routes/weather/week/[location]/+page.ts @@ -4,11 +4,11 @@ import { type GeoLocation, storedLocation } from '$lib/stores/settings'; import { geoLocationNameToRoute } from '$lib/utils/meteo'; -import type { PageLoad } from '$types'; +import type { PageLoad } from './$types'; export const prerender = true; -export const load = (async (event) => { +export const load: PageLoad = async (event) => { const urlLocation = event.params.location; let urlLocationSplit, urlLocationName, urlLocationId; @@ -31,14 +31,28 @@ export const load = (async (event) => { // lat, long coordinates if (urlLocation.includes('N') && urlLocation.includes('E')) { urlLocationSplit = urlLocation.split(/N|E/); - const latitude = urlLocationSplit[0]; - const longitude = urlLocationSplit[1]; + const latitude = parseFloat(urlLocationSplit[0]); + const longitude = parseFloat(urlLocationSplit[1]); location = { - //id: undefined, + id: 0, name: `${latitude}N° ${longitude}E°`, latitude: latitude, - longitude: longitude + longitude: longitude, + elevation: 0, + feature_code: 'COORD', + country_code: undefined, + admin1_id: undefined, + admin3_id: undefined, + admin4_id: undefined, + timezone: 'UTC', + population: undefined, + postcodes: undefined, + country_id: undefined, + country: undefined, + admin1: undefined, + admin3: undefined, + admin4: undefined }; } else { if (urlLocationId) { @@ -74,4 +88,4 @@ export const load = (async (event) => { storedLocation.set(location); return { location: location }; -}) satisfies PageLoad; +};