diff --git a/src/routes/weather/options.ts b/src/routes/weather/options.ts index a58f330..7e52522 100644 --- a/src/routes/weather/options.ts +++ b/src/routes/weather/options.ts @@ -13,7 +13,6 @@ export const models = [ { value: 'icon_seamless', label: 'DWD ICON Seamless' }, { value: 'gem_seamless', label: 'GEM Seamless' }, { value: 'meteofrance_seamless', label: 'Météo-France Seamless' }, - { value: 'arpae_cosmo_seamless', label: 'ARPAE Seamless' }, { value: 'metno_seamless', label: 'MET Norway Seamless (with ECMWF)' }, { value: 'knmi_seamless', label: 'KNMI Seamless (with ECMWF)' }, { value: 'dmi_seamless', label: 'DMI Seamless (with ECMWF)' }, diff --git a/src/routes/weather/utils/colors.ts b/src/routes/weather/utils/colors.ts index 7ad1963..d10374e 100644 --- a/src/routes/weather/utils/colors.ts +++ b/src/routes/weather/utils/colors.ts @@ -1,11 +1,11 @@ import colorScaleHex from './color-scale-hex'; -function componentFromStr(numStr: string, percent: number) { +const componentFromStr = (numStr: string, percent: number) => { const num = Math.max(0, parseInt(numStr, 10)); return percent ? Math.floor((255 * Math.min(100, num)) / 100) : Math.min(255, num); -} +}; -export function rgbToHex(rgb: string) { +export const rgbToHex = (rgb: string): string => { const rgbRegex = /^rgb\(\s*(-?\d+)(%?)\s*,\s*(-?\d+)(%?)\s*,\s*(-?\d+)(%?)\s*\)$/; let result, r, @@ -23,28 +23,58 @@ export function rgbToHex(rgb: string) { return '355522'; } return hex; -} +}; + +export const hexToRgb = (hex: string): [number, number, number] => { + const h = hex.replace('#', ''); + return [ + parseInt(h.substring(0, 2), 16), + parseInt(h.substring(2, 4), 16), + parseInt(h.substring(4, 6), 16) + ]; +}; export const getColor = (temperature: number, unit = 'celsius'): string => { + if (unit !== 'celsius') { + temperature = Math.round(((temperature - 32) * 5) / 9); + } + let index = 0; - if (unit === 'celsius') { - if (temperature <= -40) { - index = 0; - } else if (temperature >= 60) { - index = colorScaleHex.length - 1; - } else { - index = temperature + 40; - } + if (temperature <= -40) { + index = 0; + } else if (temperature >= 60) { + index = colorScaleHex.length - 1; } else { - const tempInCelsius = Math.round(((temperature - 32) * 5) / 9); - if (tempInCelsius <= -40) { - index = 0; - } else if (tempInCelsius >= 60) { - index = colorScaleHex.length - 1; - } else { - index = tempInCelsius + 40; - } + index = Math.round(temperature) + 45; } return colorScaleHex[index]; }; + +export interface TempStyle { + bg: string; + fg: 'white' | 'black'; +} + +export const getTempStyle = (temp: number, unit: string): TempStyle => { + const bg = getColor(temp, unit); + const fg = textWhite(hexToRgb(bg)) ? 'white' : 'black'; + return { bg, fg }; +}; + +export const textWhite = ( + [r, g, b, a]: [number, number, number, number] | [number, number, number], + dark?: boolean, + globalOpacity?: number +): boolean => { + const alpha = ((a || 1) * (globalOpacity || 100)) / 100; + if (alpha < 0.65) { + if (dark) { + return true; + } else { + return false; + } + } + // check luminance + return r * 0.299 + g * 0.587 + b * 0.114 <= 150; +}; diff --git a/src/routes/weather/utils/colour-gradient.ipynb b/src/routes/weather/utils/colour-gradient.ipynb deleted file mode 100644 index 3f25f7d..0000000 --- a/src/routes/weather/utils/colour-gradient.ipynb +++ /dev/null @@ -1,191 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "8c554ac3-6dae-4561-b493-32943137a3ea", - "metadata": {}, - "source": [ - "## Generating colours" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "id": "ba97e928-85be-46bd-9429-e69f89c3ce13", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import json\n", - "from colour import Color\n", - "\n", - "def flatten(xss):\n", - " return [x for xs in xss for x in xs]\n", - "\n", - "deep_purple = Color(\"#4F00A3\")\n", - "dark_blue = Color(\"#0117DB\")\n", - "light_blue = Color(\"#01B1FF\")\n", - "light_green = Color(\"#00FFC2\")\n", - "dark_green = Color(\"#00D139\")\n", - "warm_green = Color(\"#FFFF00\")\n", - "light_orange = Color(\"#FFC600\")\n", - "middle_orange = Color(\"#FFA200\")\n", - "dark_orange = Color(\"#FF640A\")\n", - "deep_red = Color(\"#9E1500\")\n", - "\n", - "colors = [\n", - " list(deep_purple.range_to(dark_blue,15)),\n", - " list(dark_blue.range_to(light_blue,13)),\n", - " list(light_blue.range_to(light_green,13)),\n", - " list(light_green.range_to(dark_green,8)),\n", - " list(dark_green.range_to(warm_green,14)),\n", - " list(warm_green.range_to(light_orange,8)),\n", - " list(light_orange.range_to(middle_orange,9)),\n", - " list(middle_orange.range_to(dark_orange,10)),\n", - " list(dark_orange.range_to(deep_red,11)),\n", - "]\n", - "\n", - "colors = flatten(colors)\n", - "\n", - "color_list = []\n", - "rgb_list = []\n", - "hsl_list = []\n", - "hex_list = []\n", - "\n", - "temp_x= []\n", - "temp_height= []\n", - "\n", - "for [ind, color] in enumerate(colors):\n", - " x = -40 + ind\n", - " color_list.append(color.hex)\n", - " hex_list.append(color.hex)\n", - " rgb_list.append(color.rgb)\n", - " hsl_list.append(color.hsl)\n", - " temp_x.append(x)\n", - " temp_height.append(1)" - ] - }, - { - "cell_type": "markdown", - "id": "248a5151-772d-43e7-8fde-5d58511f30e8", - "metadata": {}, - "source": [ - "## Visualisation" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "id": "40b96c47-6025-41d1-a2cc-a46d9233efcf", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4bf8047db9cf4060b54ccfff7857fbcb", - "version_major": 2, - "version_minor": 0 - }, - "image/png": "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BADA2OgAAgAkIwACACQjAAIAJCMAAgAkIwACACQjAAIAJOMxMEXYORyVoeFqVwEAdUSPajz5dAEAkhEAAQCSMQIuQnlvxJ691a4CAOqIHtV48ukCACQjAAIAJGMEXISdRsAAUKzGahdQ13QAAQCSEQABAJIRAAEAkrEGsAg790bstgYQAKgNOoAAAMkIgAAAyRgBF+ENI2AAoHboAAIAJCMAAgAkYwRchJ1D0dA0VO0qAADGRAcQACCZugyAL7/8cixdujTmz58fkydPjsMOOyyuvPLK2L1796jznn322TjppJOiubk5Ojo64tprr61azQAA+0tdjoA3b94cw8PDceutt8b73//+2LRpUyxbtix27doV1113XURElMvlOP3006OrqyvWrVsXzz33XFxwwQUxY8aMuPDCC6v9KwAAjJuGSqVSqXYR+8N3v/vdWLt2bbz00ksREbF27dq44ooroq+vL5qamiIi4vLLL4/7778/Nm/ePKZrlsvlaG1tjfiv/zsaGqePa/0AkMnwT+aM27Xf/v4eGBiIlpaWcXuff2V1OQLel4GBgZg1a9bI697e3jj55JNHwl9ExOLFi2PLli3x2muvValKAIDxlyIAvvjii3HzzTfHl770pZF9fX190dbWNuq8t1/39fXt8zqDg4NRLpdHbQAAtaam1gBefvnlcc011/zDc37/+9/HggULRl5v27YtPvnJT8ZZZ50Vy5Yte0/vv3r16ujp6XnngTf2Rkzyl0AAgNpQU2sAd+zYEX/+85//4TmHHnroyFh3+/btccopp8QJJ5wQP/jBD2LChH9veH7xi1+Mcrkc999//8i+xx57LE477bT4y1/+EjNnznzHtQcHB2NwcHDkdblcjo6OjohT/xANk6wBBICiDD/cPm7XtgawxjqAc+bMiTlzxrYodNu2bXHqqafGwoULY/369aPCX0REZ2dnXHHFFbFnz55obGyMiIhHHnkkjjjiiH2Gv4iIUqkUpVKpgN8EAKB6aioAjtW2bdvilFNOiUMOOSSuu+662LFjx8ix9vb/9y+Ks88+O3p6emLp0qVx2WWXxaZNm+LGG2+MNWvWvPs3NAIGAGpIXQbARx55JF588cV48cUXY968eaOOvT3xbm1tjYcffji6u7tj4cKFccABB8SqVas8AxAAqHs1tQbwX83IcwCP+701gABQoOFf/adxu7Y1gEkeAwMAwL+ryxHwfvfXvRETrQEEAGqDDiAAQDICIABAMkbARdi1N2KCETAAUBt0AAEAkhEAAQCSMQIughEwAFBDdAABAJIRAAEAkhEAAQCSEQABAJIRAAEAkhEAAQCS8RiYIry5N6JhqNpVAACMiQ4gAEAyAiAAQDJGwEX465ARMABQM3QAAQCSEQABAJIxAi5A496haDACBgBqhA4gAEAyAiAAQDICIABAMtYAFqA5hqIhrAEEAGqDDiAAQDICIABAMkbABSjF3phgBAwA1AgdQACAZARAAIBkjIAL0BxDRsAAQM3QAQQASEYABABIRgAEAEjGGsACNMVQTKxYAwgA1AYdQACAZARAAIBkjIALUIqhmOgxMABAjdABBABIRgAEAEjGCLgARsAAQC3RAQQASEYABABIRgAEAEjGGsACNMbemGQNIABQI3QAAQCSEQABAJIxAi5AUwwZAQMANUMHEAAgGQEQACAZI+ACGAEDALVEBxAAIBkBEAAgGQEQACAZawALMCmGotEaQACgRugAAgAkU/cBcHBwMI455phoaGiIZ555ZtSxZ599Nk466aRobm6Ojo6OuPbaa6tWJwDA/lL3I+BLL7005s6dG7/73e9G7S+Xy3H66adHV1dXrFu3Lp577rm44IILYsaMGXHhhRe+q/dojL1GwABAzajrAPjggw/Gww8/HD/5yU/iwQcfHHXsrrvuit27d8cdd9wRTU1N8YEPfCCeeeaZuP766991AAQAqCV1OwLu7++PZcuWxY9+9KOYMmXKO4739vbGySefHE1NTSP7Fi9eHFu2bInXXnttn9ccHByMcrk8agMAqDV12QGsVCpx3nnnxZe//OVYtGhRvPzyy+84p6+vL+bPnz9qX1tb28ixmTNnvuNnVq9eHT09Pe/Y/z8HPhctLS2F/g4AAOOlpjqAl19+eTQ0NPzDbfPmzXHzzTfHzp07Y+XKlYW+/8qVK2NgYGBk27p1a6HXBwDYH2qqA/j1r389zjvvvH94zqGHHhqPPvpo9Pb2RqlUGnVs0aJFcc4558Sdd94Z7e3t0d/fP+r426/b29v3ee1SqfSOawIA1JqaCoBz5syJOXPm/Ifn3XTTTXHVVVeNvN6+fXssXrw47rnnnjj++OMjIqKzszOuuOKK2LNnTzQ2NkZExCOPPBJHHHHEPse/AAD1oqYC4FgdfPDBo15PmzYtIiIOO+ywmDdvXkREnH322dHT0xNLly6Nyy67LDZt2hQ33nhjrFmzpio1AwDsL3UZAMeitbU1Hn744eju7o6FCxfGAQccEKtWrfIIGACg7jVUKpVKtYuoVeVyOVpbW2NgYMBdwABQI3x/19hdwAAAvHcCIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyaf8UXBHe/iMq5XK52qUAAGP09vd25j+GJgC+Bzt37oyIiI6OjmqXAgC8Szt37ozW1tZql1EV/hbwezA8PBzbt2+P6dOnR0NDQ7XLAQDGoFKpxM6dO2Pu3LkxYULO1XACIABAMjljLwBAYgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAyAiAAQDICIABAMgIgAEAy/xdOvzqG8oXciwAAAABJRU5ErkJggg==", - "text/html": [ - "\n", - "
\n", - "
\n", - " Figure\n", - "
\n", - " \n", - "
\n", - " " - ], - "text/plain": [ - "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "%matplotlib ipympl\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "fig, ax = plt.subplots()\n", - "ax.axes.get_xaxis().set_visible(False)\n", - "ax.barh(temp_x, temp_height, color=list(color_list), align='edge', height=1)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "id": "5f0ba609-0885-42d3-9c6c-a1f3921aa5d7", - "metadata": {}, - "outputs": [], - "source": [ - "plt.close()" - ] - }, - { - "cell_type": "markdown", - "id": "7d242bca-18b1-4548-8118-c82110b56b57", - "metadata": {}, - "source": [ - "## Exporting" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "id": "9f338b99-a190-4025-8491-388094ced8f2", - "metadata": {}, - "outputs": [], - "source": [ - "# os.chdir(pwd)\n", - "\n", - "with open(\"color-scale-rgb.ts\", \"w+\") as f:\n", - " f.write(\"export default \")\n", - " f.write(json.dumps(hsl_list))\n", - "\n", - "with open(\"color-scale-hsl.ts\", \"w+\") as f:\n", - " f.write(\"export default \")\n", - " f.write(json.dumps(rgb_list))\n", - " \n", - "with open(\"color-scale-hex.ts\", \"w+\") as f:\n", - " f.write(\"export default \")\n", - " f.write(json.dumps(hex_list))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/src/routes/weather/week/[location]/+page.svelte b/src/routes/weather/week/[location]/+page.svelte index df8e566..18a1cca 100644 --- a/src/routes/weather/week/[location]/+page.svelte +++ b/src/routes/weather/week/[location]/+page.svelte @@ -11,7 +11,6 @@ import HourlyTable from './HourlyTable.svelte'; import MeteogramCharts from './MeteogramCharts.svelte'; import ModelSelector from './ModelSelector.svelte'; - import SunInfo from './SunInfo.svelte'; import type { GeoLocation } from '$lib/stores/settings'; import type { FetchedDaily, FetchedHourly } from './types'; @@ -116,17 +115,16 @@
- {#if fetchedHourly} + {#if fetchedHourly && fetchedDaily} {/if} - - {#if fetchedHourly} import { fade } from 'svelte/transition'; - import { getColor } from '../../utils/colors'; + import { getTempStyle } from '../../utils/colors'; import weatherCodes from '../../utils/weather-codes'; - import { - type FetchedDaily, - type WeatherUnits, - getDayLabel, - getTextColorForTemp, - getWindArrowRotation - } from './types'; + import { type FetchedDaily, type WeatherUnits, getDayLabel, getWindArrowRotation } from './types'; interface Props { daily: FetchedDaily | null; @@ -21,10 +15,32 @@ let { daily, selectedDay, units, onSelectDay }: Props = $props(); const today = new Date(); + + function getDaylightSeconds(index: number): number { + if (!daily) return 0; + const sunriseTs = daily.daily.sunrise[index]; + const sunsetTs = daily.daily.sunset[index]; + if (!sunriseTs || !sunsetTs) return 0; + return Math.max(0, sunsetTs - sunriseTs); + } + + function getSunshinePercent(sunshineSeconds: number | null, daylightSeconds: number): number { + if (!sunshineSeconds || daylightSeconds <= 0) return 0; + return Math.min(100, (sunshineSeconds / daylightSeconds) * 100); + } + + function getSunshineColor(sunshineSeconds: number | null, daylightSeconds: number): string { + if (daylightSeconds <= 0) return '#d1d5db'; + const ratio = (sunshineSeconds ?? 0) / daylightSeconds; + if (ratio >= 0.7) return '#f59e0b'; + if (ratio >= 0.45) return '#fbbf24'; + if (ratio >= 0.2) return '#fcd34d'; + return '#d1d5db'; + } -
-
+
+
{#if daily} {#each daily.dailyDates as time, index (index)} {@const selected = time.getDate() === selectedDay.getDate()} @@ -32,22 +48,38 @@ {@const tempMin = daily.daily.temperature_2m_min[index]} {@const wCode = daily.daily.weather_code[index]} {@const sunDuration = daily.daily.sunshine_duration[index]} + {@const daylightSec = getDaylightSeconds(index)} + {@const sunColor = getSunshineColor(sunDuration, daylightSec)} + {@const sunPct = getSunshinePercent(sunDuration, daylightSec)} {@const precipSum = daily.daily.precipitation_sum[index]} {@const windMax = daily.daily.windspeed_10m_max[index]} {@const gustMax = daily.daily.windgusts_10m_max[index]} {@const windDir = daily.daily.winddirection_10m_dominant[index]} + {@const unit = String(units.temperature_unit)} + {@const maxStyle = getTempStyle(tempMax, unit)} + {@const minStyle = getTempStyle(tempMin, unit)} {#if tempMax != null && !isNaN(tempMax)} {/if} {/each} @@ -115,118 +165,14 @@
diff --git a/src/routes/weather/week/[location]/HourlyTable.svelte b/src/routes/weather/week/[location]/HourlyTable.svelte index 4297c95..1a99ba2 100644 --- a/src/routes/weather/week/[location]/HourlyTable.svelte +++ b/src/routes/weather/week/[location]/HourlyTable.svelte @@ -1,28 +1,28 @@ -
-

+{#snippet weatherIcon(name: string, size: number = 16)} + + + +{/snippet} + +{#snippet rowHeader(iconName?: string, unit?: string, label?: string)} + +
+ {#if iconName} + {@render weatherIcon(iconName)} + {/if} + {#if label} + {label} + {/if} + {#if unit} + {unit} + {/if} +
+ +{/snippet} + + +
+

{selectedDay.toLocaleDateString('en-GB', { weekday: 'long' })} – Hourly + ({timezoneLabel})

-
- 3h +
+ 3h - 1h + 1h
-{#if numCols > 0} +{#if cellData.length > 0} {@const hourly = data.hourly} - {@const dates = data.hourlyDates} -
- + {@const iconPx = is3h ? 40 : 26} +
+
- - {#each dayIdx as _ (_.toString())} + + {#each cellData as _ (_.idx)} {/each} - - - - {#each dayIdx as idx (idx)} - {@const date = dates[idx]} - {@const now = isCurrentHour(date, today)} - - {/each} + + + + - - - {#each dayIdx as idx (idx)} - {@const wCode = hourly.weather_code[idx]} - {@const date = dates[idx]} - {@const daytime = isDaytimeHour(date.getHours())} - {@const now = isCurrentHour(date, today)} - + {@render rowHeader('wi-day-cloudy')} + {#each cellData as cell, i (cell.idx)} + {@const wCode = hourly.weather_code[cell.idx]} + {/each} - - {#each dayIdx as idx (idx)} - {@const temp = hourly.temperature_2m[idx]} - {@const date = dates[idx]} - {@const now = isCurrentHour(date, today)} - {@const bg = getColor(temp, String(units.temperature_unit))} - {@const fg = getTextColorForTemp(temp, String(units.temperature_unit))} + {@render rowHeader('wi-thermometer', tempUnit)} + {#each cellData as cell (cell.idx)} + {@const temp = hourly.temperature_2m[cell.idx]} + {@const style = getTempStyle(temp, String(units.temperature_unit))} {/each} - - {#each dayIdx as idx (idx)} - {@const temp = hourly.apparent_temperature[idx]} - {@const date = dates[idx]} - {@const now = isCurrentHour(date, today)} - {@const bg = getColor(temp, String(units.temperature_unit))} - {@const fg = getTextColorForTemp(temp ?? 0, String(units.temperature_unit))} + {@render rowHeader(undefined, tempUnit, 'Feels')} + {#each cellData as cell (cell.idx)} + {@const temp = hourly.apparent_temperature[cell.idx]} {/each} - + - - {#each dayIdx as idx (idx)} - {@const cloud = hourly.cloud_cover[idx]} - {@const date = dates[idx]} - {@const now = isCurrentHour(date, today)} - - {/each} - - - - - - {#each dayIdx as idx (idx)} - {@const precip = hourly.precipitation[idx]} - {@const prob = hourly.precipitation_probability[idx]} - {@const date = dates[idx]} - {@const now = isCurrentHour(date, today)} - - {/each} - - - - - - {#each dayIdx as idx (idx)} - {@const wind = hourly.windspeed_10m[idx]} - {@const windDir = hourly.winddirection_10m[idx]} - {@const date = dates[idx]} - {@const now = isCurrentHour(date, today)} - {/each} - - {#each dayIdx as idx (idx)} - {@const hum = hourly.relative_humidity_2m[idx]} - {@const date = dates[idx]} - {@const now = isCurrentHour(date, today)} - + {/each} + + + + + {@render rowHeader('wi-cloud', '%')} + {#each cellData as cell (cell.idx)} + {@const cloud = hourly.cloud_cover[cell.idx]} + + {/each} + + + + + {@render rowHeader('wi-raindrop', precipUnit)} + {#each cellData as cell (cell.idx)} + {@const precip = hourly.precipitation[cell.idx]} + {@const prob = hourly.precipitation_probability[cell.idx]} + {/each} @@ -316,94 +431,31 @@ {/if} diff --git a/src/routes/weather/week/[location]/MeteogramCharts.svelte b/src/routes/weather/week/[location]/MeteogramCharts.svelte index 0c87627..a5640f7 100644 --- a/src/routes/weather/week/[location]/MeteogramCharts.svelte +++ b/src/routes/weather/week/[location]/MeteogramCharts.svelte @@ -240,17 +240,15 @@ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, - color: getColor(Math.round(maxTemp), String(units.temperature_unit)) + '88' + color: getColor(maxTemp, String(units.temperature_unit)) + '88' }, { offset: 0.5, - color: - getColor(Math.round((maxTemp + minTemp) / 2), String(units.temperature_unit)) + - '44' + color: getColor((maxTemp + minTemp) / 2, String(units.temperature_unit)) + '44' }, { offset: 1, - color: getColor(Math.round(minTemp), String(units.temperature_unit)) + '08' + color: getColor(minTemp, String(units.temperature_unit)) + '08' } ]) }, diff --git a/src/routes/weather/week/[location]/types.ts b/src/routes/weather/week/[location]/types.ts index e4f4396..cc2be44 100644 --- a/src/routes/weather/week/[location]/types.ts +++ b/src/routes/weather/week/[location]/types.ts @@ -19,28 +19,23 @@ export interface FetchedDaily { dailyDates: Date[]; } -export function getTempUnit(units: WeatherUnits): string { +export const getTempUnit = (units: WeatherUnits): '°C' | '°F' => { return units.temperature_unit === 'celsius' ? '°C' : '°F'; -} +}; -export function getWindUnit(units: WeatherUnits): string { +export const getWindUnit = (units: WeatherUnits): string => { return units.wind_speed_unit === 'kmh' ? 'km/h' : units.wind_speed_unit; -} +}; -export function getPrecipUnit(units: WeatherUnits): string { +export const getPrecipUnit = (units: WeatherUnits): 'mm' | 'in' => { return units.precipitation_unit === 'mm' ? 'mm' : 'in'; -} +}; -export function getTextColorForTemp(temp: number, unit: string): string { - const threshold = unit === 'celsius' ? { low: -13, high: 40 } : { low: 7, high: 104 }; - return temp < threshold.low || temp >= threshold.high ? 'white' : 'black'; -} - -export function getWindArrowRotation(deg: number): string { +export const getWindArrowRotation = (deg: number): string => { return `rotate(${deg}deg)`; -} +}; -export function getWindDirectionLabel(deg: number): string { +export const getWindDirectionLabel = (deg: number): string => { const dirs = [ 'N', 'NNE', @@ -60,9 +55,9 @@ export function getWindDirectionLabel(deg: number): string { 'NNW' ]; return dirs[Math.round(deg / 22.5) % 16]; -} +}; -export function getDayLabel(date: Date, today: Date): string { +export const getDayLabel = (date: Date, today: Date): string => { const MS_PER_DAY = 24 * 3600 * 1000; const diff = Math.round( (new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - @@ -73,17 +68,13 @@ export function getDayLabel(date: Date, today: Date): string { if (diff === 1) return 'Tomorrow'; if (diff === -1) return 'Yesterday'; return `${date.getMonth() + 1}-${date.getDate()}`; -} +}; -export function isDaytimeHour(hour: number): boolean { - return hour >= 6 && hour < 21; -} - -export function isCurrentHour(date: Date, now: Date): boolean { +export const isCurrentHour = (date: Date, now: Date): boolean => { return ( date.getDate() === now.getDate() && date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear() && date.getHours() === now.getHours() ); -} +};
Hourly weather details for {locationName}
- {pad(date.getHours())}00 -
+ {timezoneLabel} + + + {#if sunTimes && sunrisePercent != null && sunsetPercent != null} +
+
+
+ +
+ + + {formatTime(sunTimes.sunrise)} + +
+ +
+ + + {formatTime(sunTimes.sunset)} + +
+ {/if} + + {#each cellData as cell, i (cell.idx)} + {@const leftPct = (i / cellData.length) * 100} + {@const widthPct = 100 / cellData.length} + + {#if is3h} + {pad(cell.date.getHours())} + {:else} + + {pad(cell.date.getHours())} + 00 + + {/if} + + {/each} +
- - - - - - - +
+ {@render weatherIcon(getWeatherIconName(wCode, cell.isDaytime), iconPx)}
-
- - - - {tempUnit} -
-
- {temp != null ? temp.toFixed(0) + '°' : '-'} + {formatTemp(temp)}
-
- Feels - {tempUnit} -
-
- {temp != null ? temp.toFixed(0) + '°' : '-'} + {formatTemp(temp)}
-
- - - - % -
-
- {cloud != null ? cloud.toFixed(0) : '-'} -
-
- - - - {precipUnit} -
-
-
- {#if precip > 0} -
- {/if} - - {#if precip > 0} - {precip.toFixed(1)} - {:else if prob != null && prob > 0} - {prob}% - {/if} - -
-
- - - - {windUnit} -
-
+ {@render rowHeader('wi-strong-wind', windUnit)} + {#each cellData as cell (cell.idx)} + {@const wind = hourly.windspeed_10m[cell.idx]} + {@const windDir = hourly.winddirection_10m[cell.idx]} + {#if windDir != null && !isNaN(windDir)} -
- - - -
+ + {@render weatherIcon('wi-direction-down', 24)} + {/if} - {wind?.toFixed(0) ?? '-'} + + {formatValue(wind)} +
-
- - - - % -
-
- {hum != null ? hum.toFixed(0) : '-'} + {@render rowHeader('wi-humidity', '%')} + {#each cellData as cell (cell.idx)} + {@const hum = hourly.relative_humidity_2m[cell.idx]} + + {formatValue(hum)} +
+ {formatValue(cloud)} +
+ {#if precip > 0} +
+ + {precip.toFixed(1)} + + {/if}