wip improvements but still problematic

This commit is contained in:
terraputix
2026-01-09 21:18:19 +01:00
parent 1e434e0a84
commit 298c8ef597
41 changed files with 572 additions and 423 deletions
-55
View File
@@ -15,57 +15,16 @@
let location = $state(get(storedLocation));
let mounted = $state(false);
let currentWeather = $state(null);
// Subscribe to location changes
storedLocation.subscribe((value) => {
location = value;
if (mounted) {
loadCurrentWeather();
}
});
onMount(() => {
mounted = true;
loadCurrentWeather();
});
const loadCurrentWeather = async () => {
if (!location?.latitude) return;
try {
const response = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&current=temperature_2m,weather_code&forecast_days=1`
);
const data = await response.json();
currentWeather = data;
} catch (error) {
console.error('Failed to load current weather:', error);
}
};
const getWeatherIcon = (code) => {
const iconMap = {
0: '☀️',
1: '🌤️',
2: '⛅',
3: '☁️',
45: '🌫️',
48: '🌫️',
51: '🌦️',
53: '🌦️',
55: '🌦️',
61: '🌧️',
63: '🌧️',
65: '🌧️',
71: '🌨️',
73: '🌨️',
75: '❄️',
95: '⛈️'
};
return iconMap[code] || '☁️';
};
const getPageTitle = () => {
const path = $page.url.pathname;
if (path.includes('/compare')) return 'Model Comparison';
@@ -129,20 +88,6 @@
</p>
</div>
</div>
<!-- Current Weather -->
{#if currentWeather}
<div class="flex items-center space-x-3" in:fade={{ delay: 800 }}>
<div class="text-center">
<div class="mb-1 text-3xl">
{getWeatherIcon(currentWeather.current.weather_code)}
</div>
<div class="text-2xl font-bold text-gray-900 dark:text-white">
{Math.round(currentWeather.current.temperature_2m)}°C
</div>
</div>
</div>
{/if}
</div>
{/if}
+7 -4
View File
@@ -1,10 +1,13 @@
import type { ConfigInterface } from '../config';
export default (
ctx: CanvasRenderingContext2D | null | undefined,
config: ConfigInterface,
series: Float32Array | null | undefined,
canvasElement: HTMLCanvasElement
series: Float32Array | null | undefined
): void => {
if (ctx && series) {
const fillColor = `hsla(${config.styles.mutedForeground}, 0.5)`;
ctx.beginPath();
ctx.moveTo(0, 35 + (series[0] ** 1.5 / 1000) * 30);
for (const [index, value] of series.entries()) {
@@ -71,8 +74,8 @@ export default (
);
ctx.closePath();
//to fill the space in the shape
ctx.fillStyle = `hsla(${getComputedStyle(canvasElement).getPropertyValue('--muted-foreground').split(' ').join(',')}, 0.5)`;
// USE PRE-CALC STYLE
ctx.fillStyle = fillColor;
ctx.fill();
}
};
+2
View File
@@ -1,3 +1,5 @@
import type { ConfigInterface } from '../config';
export default (
ctx: CanvasRenderingContext2D | null | undefined,
config: ConfigInterface,
+6 -3
View File
@@ -1,15 +1,18 @@
import type { ConfigInterface } from '../config';
export default (
ctx: CanvasRenderingContext2D | null | undefined,
config: ConfigInterface,
series: Float32Array | null | undefined,
canvasElement: HTMLCanvasElement
series: Float32Array | null | undefined
): void => {
if (ctx && series) {
const strokeStyle = `hsla(${config.styles.primary}, 1)`;
for (const [index, value] of series.entries()) {
ctx.beginPath();
ctx.moveTo(index * config.deltaX + 0.5 * config.deltaX, config.maxY);
ctx.strokeStyle = `hsla(${getComputedStyle(canvasElement).getPropertyValue('--primary').split(' ').join(',')}, 1)`;
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = 12;
ctx.lineTo(index * config.deltaX + 0.5 * config.deltaX, config.maxY - value ** 0.55 * 45);
+2 -1
View File
@@ -1,3 +1,5 @@
import type { ConfigInterface } from '../config';
export default (
ctx: CanvasRenderingContext2D | null | undefined,
config: ConfigInterface,
@@ -18,7 +20,6 @@ export default (
series[index].getHours() === today.getHours()
) {
// fill now line
// TODO: update this line every minute
ctx.stroke();
ctx.closePath();
ctx.beginPath();
+5 -4
View File
@@ -1,3 +1,4 @@
import type { ConfigInterface } from '../config';
import { getColor } from '../utils/colors';
export default (
@@ -8,10 +9,10 @@ export default (
): void => {
if (ctx && series) {
const tempGradientFill = ctx.createLinearGradient(0, 0, 0, config.maxY);
tempGradientFill.addColorStop(0, getColor(config.maxTemp.toFixed(0), unit) + '5c');
tempGradientFill.addColorStop(0.25, getColor(config.maxTemp.toFixed(0), unit) + '5c');
tempGradientFill.addColorStop(0.85, getColor(config.minTemp.toFixed(0), unit) + '06');
tempGradientFill.addColorStop(1, getColor(config.minTemp.toFixed(0), unit) + '00');
tempGradientFill.addColorStop(0, getColor(config.maxTemp, unit) + '5c');
tempGradientFill.addColorStop(0.25, getColor(config.maxTemp, unit) + '5c');
tempGradientFill.addColorStop(0.85, getColor(config.minTemp, unit) + '06');
tempGradientFill.addColorStop(1, getColor(config.minTemp, unit) + '00');
ctx.beginPath();
ctx.moveTo(
-8
View File
@@ -1,8 +0,0 @@
interface ConfigInterface {
maxX: number;
maxY: number;
deltaX: number;
minTemp: number;
maxTemp: number;
diffTemp: number;
}
+13
View File
@@ -0,0 +1,13 @@
export interface ConfigInterface {
maxX: number;
maxY: number;
deltaX: number;
minTemp: number;
maxTemp: number;
diffTemp: number;
styles: {
mutedForeground: string;
primary: string;
border: string;
};
}
+34 -6
View File
@@ -25,19 +25,18 @@ export function rgbToHex(rgb: string) {
return hex;
}
export const getColor = (tempString: string, unit = 'celsius'): string => {
export const getColor = (value: number, unit = 'celsius'): string => {
let index = 0;
const temp = Number(tempString);
if (unit === 'celsius') {
if (temp <= -40) {
if (value <= -40) {
index = 0;
} else if (temp >= 60) {
} else if (value >= 60) {
index = colorScaleHex.length - 1;
} else {
index = temp + 40;
index = value + 40;
}
} else {
const tempInCelsius = Math.round(((temp - 32) * 5) / 9);
const tempInCelsius = Math.round(((value - 32) * 5) / 9);
if (tempInCelsius <= -40) {
index = 0;
} else if (tempInCelsius >= 60) {
@@ -47,5 +46,34 @@ export const getColor = (tempString: string, unit = 'celsius'): string => {
}
}
index = Math.floor(index);
return colorScaleHex[index];
};
export const textWhite = (hex: string): boolean => {
const cleaned = (hex || '').replace('#', '').trim().toLowerCase();
if (!cleaned) {
return true;
}
let r = 0,
g = 0,
b = 0;
if (cleaned.length === 6) {
r = parseInt(cleaned.slice(0, 2), 16);
g = parseInt(cleaned.slice(2, 4), 16);
b = parseInt(cleaned.slice(4, 6), 16);
} else {
throw new Error('Invalid color format');
}
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
return true;
}
// Perceived brightness (YIQ / luma). If brightness is low, use white text.
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness < 128;
};
+2 -1
View File
@@ -1,4 +1,4 @@
export default {
const map: Record<number, string> = {
0: 'clear',
1: 'clear',
2: 'cloudy',
@@ -79,3 +79,4 @@ export default {
96: 'thunderstorm',
99: 'tornado'
};
export default map;
+323 -289
View File
@@ -1,10 +1,11 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onMount, tick } from 'svelte';
import { fade } from 'svelte/transition';
import { fetchWeatherApi } from 'openmeteo';
import { type GeoLocation, storedLocation } from '$lib/stores/settings';
import { storedLocation } from '$lib/stores/settings';
import type { WeatherApiResponse } from '@openmeteo/sdk/weather-api-response';
import type { VariableWithValues } from '@openmeteo/sdk/variable-with-values';
import { urlHashStore } from '$lib/stores/url-hash-store';
import { Label } from '$lib/components/ui/label';
@@ -15,12 +16,52 @@
import precip from '../../canvas/precip';
import raster from '../../canvas/raster';
import tempGradient from '../../canvas/temp-gradient';
import type { ConfigInterface } from '../../config';
import { defaultParameters, models } from '../../options';
import { getColor } from '../../utils/colors';
import { getColor, textWhite } from '../../utils/colors';
import weatherCodes from '../../utils/weather-codes';
import { SvelteDate } from 'svelte/reactivity';
import { pad } from '$lib/utils';
// --- Interfaces ---
interface WeatherEntry {
id: number;
name: string;
title: string;
values: string[] | undefined;
}
interface WeatherHourly {
entries: WeatherEntry[];
entriesLength: number;
hourlyTime: Date[];
windDirections: Float32Array | undefined;
indexes: number[];
raw: {
hourlyTemps: Float32Array | undefined;
hourlyCloudCover: Float32Array | undefined;
hourlyPrecip: Float32Array | undefined;
minTemp: number;
};
}
interface WeatherDaily {
time: Date[];
weather_code: VariableWithValues;
temperature_2m_max: VariableWithValues;
temperature_2m_min: VariableWithValues;
sunrise: VariableWithValues;
sunset: VariableWithValues;
sunshine_duration: VariableWithValues;
precipitation_sum: VariableWithValues;
windspeed_10m_max: VariableWithValues;
windgusts_10m_max: VariableWithValues;
winddirection_10m_dominant: VariableWithValues;
}
// --- State Setup ---
const params = urlHashStore({
latitude: [$storedLocation.latitude],
longitude: [$storedLocation.longitude],
@@ -28,266 +69,279 @@
...defaultParameters
});
let location = $state($storedLocation);
storedLocation.subscribe((value) => {
location = value;
});
let location = $derived($storedLocation);
let diffTemp: number | undefined = $state();
let maxTemp: number | undefined = $state();
let weatherCodesHourly: Float32Array | null | undefined = $state();
let canvasElement: HTMLCanvasElement | null | undefined = $state();
const today = new Date();
// UI State
let selectedDay = $state(new Date());
let selectedDayIndex = $state(1);
let scrollDiv: HTMLElement | undefined = $state();
let canvasElement: HTMLCanvasElement | undefined = $state();
let entries = $state(0);
// Data State
let weather: WeatherHourly | null = $state(null);
let weatherDaily: WeatherDaily | null = $state(null);
let diffTemp: number = $state(0);
let maxTemp: number = $state(0);
let weatherCodesHourly: Float32Array | undefined = $state();
let weather = $derived(
(async (location: GeoLocation) => {
const reqParams = {
latitude: location.latitude,
longitude: location.longitude,
elevation: location.elevation,
// timezone: location.timezone, ???
models: [$params.models],
hourly: [
'precipitation',
'precipitation_probability',
'temperature_2m',
'weather_code',
'windspeed_10m',
'winddirection_10m',
'cloud_cover',
'relative_humidity_2m'
].join(','),
forecast_days: 6,
past_days: 1,
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);
const response = responses[0];
const utcOffsetSeconds = response.utcOffsetSeconds();
const hourly = response.hourly()!;
// Constants
const today = new Date();
const entries = 6;
const winddir = true;
weatherCodesHourly = hourly.variables(3)?.valuesArray();
let modelSelected = $derived(models.find((mo) => String(mo.value) === $params.models));
let hourlyTime = [
...Array((Number(hourly.timeEnd()) - Number(hourly.time())) / hourly.interval())
].map(
(_, i) =>
new Date((Number(hourly.time()) + i * hourly.interval() + utcOffsetSeconds) * 1000)
);
const hourlyTemps = hourly.variables(2)?.valuesArray();
const hourlyCloudCover = hourly.variables(6)?.valuesArray();
const hourlyPrecip = hourly.variables(0)?.valuesArray();
const indexes = [];
if (hourlyTemps) {
for (const index of hourlyTemps.keys()) {
indexes.push(index);
}
}
// --- Logic ---
const maxX = 10000;
const maxY = 500;
const deltaX = 10000 / hourly.variables(0)?.valuesArray()?.length;
async function loadWeatherData() {
if (!location) return;
const ctx = canvasElement?.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, maxX, maxY);
const commonParams = {
latitude: location.latitude,
longitude: location.longitude,
elevation: location.elevation,
models: [$params.models],
forecast_days: 6,
past_days: 1,
temperature_unit: $params.temperature_unit,
wind_speed_unit: $params.wind_speed_unit,
precipitation_unit: $params.precipitation_unit
};
const minTemp = Math.min(
...(hourly.variables(2)?.valuesArray() ?? []).filter((t) => !isNaN(t))
);
maxTemp = Math.max(...(hourly.variables(2)?.valuesArray() ?? []).filter((t) => !isNaN(t)));
diffTemp = maxTemp - minTemp;
const url = 'https://api.open-meteo.com/v1/forecast';
const config: ConfigInterface = {
maxX: maxX,
maxY: maxY,
deltaX: deltaX,
minTemp: minTemp,
maxTemp: maxTemp,
diffTemp: diffTemp
};
try {
const [hourlyRes, dailyRes] = await Promise.all([
fetchWeatherApi(url, {
...commonParams,
hourly:
'precipitation,precipitation_probability,temperature_2m,weather_code,windspeed_10m,winddirection_10m,cloud_cover,relative_humidity_2m'
}),
fetchWeatherApi(url, {
...commonParams,
daily:
'weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset,sunshine_duration,precipitation_sum,windspeed_10m_max,windgusts_10m_max,winddirection_10m_dominant'
})
]);
// 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);
}
processHourly(hourlyRes[0]);
processDaily(dailyRes[0]);
} catch (e) {
console.error('Weather fetch error:', e);
}
}
return {
entries: [
{
id: 0,
name: 'temperature_2m',
title: 'Temperature',
values: hourly
.variables(2)
?.valuesArray()
?.map((t) => t.toFixed(1))
},
{
id: 1,
name: 'precipitation',
title: 'Precipitation',
values: hourly
.variables(0)
?.valuesArray()
?.map((p) => p.toFixed(1))
},
{
id: 2,
name: 'precipitation_probability',
title: 'Precip Prob.',
values: hourly
.variables(1)
?.valuesArray()
?.map((p) => p.toFixed(0))
},
{
id: 3,
name: 'windspeed_10m',
title: 'Wind',
values: hourly
.variables(4)
?.valuesArray()
?.map((p) => p.toFixed(0))
},
{
id: 4,
name: 'relative_humidity_2m',
title: 'Rel. Hum.',
values: hourly
.variables(7)
?.valuesArray()
?.map((p) => p.toFixed(0))
}
],
entriesLength: hourly.variables(0)?.valuesArray()?.length,
hourlyTime: hourlyTime,
windDirections: hourly.variables(5)?.valuesArray(),
indexes: indexes
};
})(location)
);
function processHourly(response: WeatherApiResponse) {
const utcOffsetSeconds = response.utcOffsetSeconds();
const hourly = response.hourly()!;
let weatherDaily = $derived(
(async (location: GeoLocation) => {
const reqParams = {
latitude: location.latitude,
longitude: location.longitude,
elevation: location.elevation,
// timezone: location.timezone, ???
models: [$params.models],
daily: [
'weather_code',
'temperature_2m_max',
'temperature_2m_min',
'sunrise',
'sunset',
'sunshine_duration',
'precipitation_sum',
'windspeed_10m_max',
'windgusts_10m_max',
'winddirection_10m_dominant'
].join(','),
forecast_days: 6,
past_days: 1,
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);
const response = responses[0];
const utcOffsetSeconds = response.utcOffsetSeconds();
const daily = response.daily()!;
weatherCodesHourly = hourly.variables(3)?.valuesArray() ?? undefined;
return {
daily: {
time: [...Array((Number(daily.timeEnd()) - Number(daily.time())) / daily.interval())].map(
(_, i) =>
new Date((Number(daily.time()) + i * daily.interval() + utcOffsetSeconds) * 1000)
),
weather_code: daily.variables(0)!,
temperature_2m_max: daily.variables(1)!,
temperature_2m_min: daily.variables(2)!,
sunrise: daily.variables(3)!,
sunset: daily.variables(4)!,
sunshine_duration: daily.variables(5)!,
precipitation_sum: daily.variables(6)!,
windspeed_10m_max: daily.variables(7)!,
windgusts_10m_max: daily.variables(8)!,
winddirection_10m_dominant: daily.variables(9)!
}
};
})(location)
);
const hourlyTime = [
...Array((Number(hourly.timeEnd()) - Number(hourly.time())) / hourly.interval())
].map(
(_, i) => new Date((Number(hourly.time()) + i * hourly.interval() + utcOffsetSeconds) * 1000)
);
let winddir = true;
entries = 6;
// Note: The SDK returns Float32Array | null. We use ?? undefined for safer Svelte prop passing
const hourlyTemps = hourly.variables(2)?.valuesArray() ?? undefined;
const hourlyCloudCover = hourly.variables(6)?.valuesArray() ?? undefined;
const hourlyPrecip = hourly.variables(0)?.valuesArray() ?? undefined;
const hourlyPrecipProb = hourly.variables(1)?.valuesArray() ?? undefined;
const hourlyWindSpeed = hourly.variables(4)?.valuesArray() ?? undefined;
const hourlyHumidity = hourly.variables(7)?.valuesArray() ?? undefined;
const hourlyWindDir = hourly.variables(5)?.valuesArray() ?? undefined;
let scrollDiv: HTMLElement = $state();
let tableCells;
const switchDay = (date: SvelteDate, 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' });
break;
const indexes: number[] = [];
if (hourlyTemps) {
for (let i = 0; i < hourlyTemps.length; i++) {
indexes.push(i);
}
}
selectedDayIndex = index;
// Calculate Min/Max
// Convert Float32Array to regular array for Math operations, filter NaNs
const tempArray = hourlyTemps ? Array.from(hourlyTemps) : [];
const validTemps = tempArray.filter((t) => !isNaN(t));
const minT = validTemps.length ? Math.min(...validTemps) : 0;
const maxT = validTemps.length ? Math.max(...validTemps) : 0;
maxTemp = maxT;
diffTemp = maxT - minT;
// Map helper
const formatValues = (arr: Float32Array | undefined, digits: number): string[] => {
if (!arr) return [];
return Array.from(arr).map((v) => v.toFixed(digits));
};
weather = {
entries: [
{
id: 0,
name: 'temperature_2m',
title: 'Temperature',
values: formatValues(hourlyTemps, 1)
},
{
id: 1,
name: 'precipitation',
title: 'Precipitation',
values: formatValues(hourlyPrecip, 1)
},
{
id: 2,
name: 'precipitation_probability',
title: 'Precip Prob.',
values: formatValues(hourlyPrecipProb, 0)
},
{
id: 3,
name: 'windspeed_10m',
title: 'Wind',
values: formatValues(hourlyWindSpeed, 0)
},
{
id: 4,
name: 'relative_humidity_2m',
title: 'Rel. Hum.',
values: formatValues(hourlyHumidity, 0)
}
],
entriesLength: hourly.variables(0)?.valuesArray()?.length ?? 0,
hourlyTime: hourlyTime,
windDirections: hourlyWindDir,
indexes: indexes,
raw: {
hourlyTemps,
hourlyCloudCover,
hourlyPrecip,
minTemp: minT
}
};
}
function processDaily(response: WeatherApiResponse) {
const utcOffsetSeconds = response.utcOffsetSeconds();
const daily = response.daily()!;
weatherDaily = {
time: [...Array((Number(daily.timeEnd()) - Number(daily.time())) / daily.interval())].map(
(_, i) => new Date((Number(daily.time()) + i * daily.interval() + utcOffsetSeconds) * 1000)
),
weather_code: daily.variables(0)!,
temperature_2m_max: daily.variables(1)!,
temperature_2m_min: daily.variables(2)!,
sunrise: daily.variables(3)!,
sunset: daily.variables(4)!,
sunshine_duration: daily.variables(5)!,
precipitation_sum: daily.variables(6)!,
windspeed_10m_max: daily.variables(7)!,
windgusts_10m_max: daily.variables(8)!,
winddirection_10m_dominant: daily.variables(9)!
};
}
// --- Effects ---
$effect(() => {
if (location && $params) {
loadWeatherData();
}
});
$effect(() => {
if (!weather || !canvasElement) return;
const ctx = canvasElement.getContext('2d');
if (ctx) {
const maxX = 10000;
const maxY = 500;
const deltaX = maxX / (weather.entriesLength || 1); // prevent division by zero
const computedStyle = getComputedStyle(canvasElement);
const styleConfig = {
mutedForeground: computedStyle.getPropertyValue('--muted-foreground').split(' ').join(','),
primary: computedStyle.getPropertyValue('--primary').split(' ').join(','),
border: computedStyle.getPropertyValue('--border').split(' ').join(',')
};
ctx.clearRect(0, 0, maxX, maxY);
const config: ConfigInterface = {
maxX,
maxY,
deltaX,
minTemp: weather.raw.minTemp,
maxTemp: maxTemp,
diffTemp: diffTemp || 1, // prevent division by zero in canvas
styles: styleConfig
};
daylight(ctx, config, weather.hourlyTime);
raster(ctx, config, weather.hourlyTime, today, canvasElement);
tempGradient(ctx, config, weather.raw.hourlyTemps, $params.temperature_unit);
cloudCover(ctx, config, weather.raw.hourlyCloudCover);
precip(ctx, config, weather.raw.hourlyPrecip);
}
});
const switchDay = async (date: Date, index?: number) => {
selectedDay = date;
if (index !== undefined) selectedDayIndex = index;
await tick();
if (!scrollDiv) return;
const tableCells = document.querySelectorAll('td.time');
for (const tableCell of tableCells) {
if (Number((tableCell as HTMLElement).dataset['date']) === selectedDay.getDate()) {
scrollDiv.scrollTo({
left: (tableCell as HTMLElement).offsetLeft - 110,
behavior: 'smooth'
});
break;
}
}
};
onMount(() => {
setTimeout(() => {
tableCells = document.querySelectorAll('td.time');
for (let tableCell of tableCells) {
if (Number(tableCell.dataset['date']) === selectedDay.getDate()) {
scrollDiv.scrollTo({ left: tableCell.offsetLeft - 110 });
if (!scrollDiv) return;
const tableCells = document.querySelectorAll('td.time');
for (const tableCell of tableCells) {
if (Number((tableCell as HTMLElement).dataset['date']) === selectedDay.getDate()) {
scrollDiv.scrollTo({ left: (tableCell as HTMLElement).offsetLeft - 110 });
break;
}
}
}, 150);
document.onkeydown = (e) => {
if (!scrollDiv === document.activeElement || !scrollDiv.contains(document.activeElement)) {
if (e.key === 'ArrowLeft') {
if (selectedDay.getDate() >= today.getDate()) {
let newDate = new SvelteDate();
newDate.setDate(selectedDay.getDate() - 1);
switchDay(newDate);
}
if (
!scrollDiv ||
scrollDiv === document.activeElement ||
scrollDiv.contains(document.activeElement)
)
return;
if (e.key === 'ArrowLeft') {
if (selectedDay.getDate() >= today.getDate()) {
let newDate = new SvelteDate(selectedDay);
newDate.setDate(selectedDay.getDate() - 1);
switchDay(newDate);
}
if (e.key === 'ArrowRight') {
if (selectedDay.getDate() <= today.getDate() + 4) {
let newDate = new SvelteDate();
newDate.setDate(selectedDay.getDate() + 1);
switchDay(newDate);
}
}
if (e.key === 'ArrowRight') {
if (selectedDay.getDate() <= today.getDate() + 4) {
let newDate = new SvelteDate(selectedDay);
newDate.setDate(selectedDay.getDate() + 1);
switchDay(newDate);
}
}
};
});
let modelSelected = $derived(models.find((mo) => String(mo.value) === $params.models));
// let modelSelectedValue = $derived($params.models[0]);
//
</script>
<svelte:head>
@@ -304,10 +358,10 @@
style="min-height: 256px"
class="weather-week gap-md-2 mb-4 flex flex-col md:flex-row"
>
{#await weatherDaily then wd}
{#each wd.daily.time as time, index (index)}
{#if weatherDaily}
{#each weatherDaily.time as time, index (index)}
{@const selected = time.getDate() === selectedDay.getDate()}
{#if !isNaN(wd.daily.temperature_2m_max.values(index).toFixed(1))}
{#if !isNaN(weatherDaily.temperature_2m_max.values(index)!)}
<button
style="transition: 300ms; min-width: 13%; {selected ? 'transform: scale(1.025)' : ''}"
class="cursor-pointer"
@@ -337,23 +391,23 @@
<svg class="fill-foreground" width="60px" height="60px">
<use
xlink:href="/images/weather-icons/wi-day-{weatherCodes[
wd.daily.weather_code.values(index)
weatherDaily.weather_code.values(index)!
]}.svg#Layer_1"
></use>
</svg>
</div>
<div
class="weather-temp-max flex min-w-[65px] justify-center rounded-t p-1 text-sm"
style={`background-color: ${getColor(wd.daily.temperature_2m_max.values(index).toFixed(0), $params.temperature_unit)}; color: ${wd.daily.temperature_2m_min.values(index) < ($params.temperature_unit === 'celsius' ? 4 : 7) || wd.daily.temperature_2m_min.values(index) >= ($params.temperature_unit === 'celsius' ? 30 : 104) ? 'white' : 'black'}`}
class="weather-temp-max flex min-w-16.25 justify-center rounded-t p-1 text-sm"
style={`background-color: ${getColor(weatherDaily.temperature_2m_max.values(index)!, $params.temperature_unit)}; color: ${weatherDaily.temperature_2m_min.values(index)! < ($params.temperature_unit === 'celsius' ? 4 : 7) || weatherDaily.temperature_2m_min.values(index)! >= ($params.temperature_unit === 'celsius' ? 30 : 104) ? 'white' : 'black'}`}
>
{wd.daily.temperature_2m_max.values(index).toFixed(1)}
{weatherDaily.temperature_2m_max.values(index)!.toFixed(1)}
{$params.temperature_unit === 'celsius' ? '°C' : '°F'}
</div>
<div
class="weather-temp-min flex min-w-[65px] justify-center rounded-b p-1 text-sm"
style={`background: ${getColor(wd.daily.temperature_2m_min.values(index).toFixed(0), $params.temperature_unit)}; color: ${wd.daily.temperature_2m_min.values(index) < ($params.temperature_unit === 'celsius' ? 4 : 7) || wd.daily.temperature_2m_min.values(index) >= ($params.temperature_unit === 'celsius' ? 30 : 104) ? 'white' : 'black'}`}
class="weather-temp-min flex min-w-16.25 justify-center rounded-b p-1 text-sm"
style={`background: ${getColor(weatherDaily.temperature_2m_min.values(index)!, $params.temperature_unit)}; color: ${weatherDaily.temperature_2m_min.values(index)! < ($params.temperature_unit === 'celsius' ? 4 : 7) || weatherDaily.temperature_2m_min.values(index)! >= ($params.temperature_unit === 'celsius' ? 30 : 104) ? 'white' : 'black'}`}
>
{wd.daily.temperature_2m_min.values(index).toFixed(1)}
{weatherDaily.temperature_2m_min.values(index)!.toFixed(1)}
{$params.temperature_unit === 'celsius' ? '°C' : '°F'}
</div>
<div class="mt-2 flex items-center justify-center gap-1">
@@ -368,7 +422,7 @@
</div>
</div>
{Number(wd.daily.sunshine_duration.values(index) / 3600).toFixed(0)}h
{Number(weatherDaily.sunshine_duration.values(index)! / 3600).toFixed(0)}h
</div>
<div class="mt-1 flex items-center justify-center">
<div class="relative flex h-6 w-6 items-center justify-center">
@@ -381,7 +435,7 @@
</svg>
</div>
</div>
{Number(wd.daily.precipitation_sum.values(index)).toFixed(
{Number(weatherDaily.precipitation_sum.values(index)).toFixed(
1
)}{$params.precipitation_unit === 'mm' ? 'mm' : "'"}
</div>
@@ -389,9 +443,9 @@
</button>
{/if}
{/each}
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
{:else}
<p>Loading...</p>
{/if}
</div>
<div class="ml-22 md:ml-0">
<h3 class="text-xl font-bold">
@@ -410,7 +464,7 @@
<div
bind:this={scrollDiv}
style=" height: {218 + entries * 27.5}px; "
class="w-ful relative -mx-5 overflow-x-scroll overflow-y-hidden md:-ml-[110px]"
class="w-ful relative -mx-5 overflow-x-scroll overflow-y-hidden md:-ml-27.5"
>
<canvas
bind:this={canvasElement}
@@ -423,7 +477,7 @@
<table in:fade class="absolute bottom-0 border-b border-border">
<caption style="display:none"> Weather Week {location.name} </caption>
<tbody>
{#await weather then weather}
{#if weather && weather.entries[0] && weather.entries[0].values}
<tr>
<th
scope="row"
@@ -460,13 +514,13 @@
{@const now =
weather.hourlyTime[index].getDate() === today.getDate() &&
weather.hourlyTime[index].getHours() === today.getHours()}
<!-- Safe access to values -->
{@const tempVal = weather.entries[0].values?.[index] ?? '0'}
<td
style="position: absolute; bottom: {27.5 * entries -
24 +
0.8 * 200 -
0.54 *
200 *
((maxTemp - weather.entries[0].values[index]) / diffTemp)}px; left:{116 +
0.54 * 200 * ((maxTemp - Number(tempVal)) / diffTemp)}px; left:{116 +
(5000 / weather.entriesLength) * index}px; min-width: {5000 /
weather.entriesLength}px; max-width: {5000 / weather.entriesLength}px;"
><svg class="fill-foreground {now ? 'scale-125' : ''}" width="20px" height="20px">
@@ -475,7 +529,7 @@
xlink:href="/images/weather-icons/wi-{weather.hourlyTime[index].getHours() >
6 && weather.hourlyTime[index].getHours() < 21
? 'day'
: 'night'}-{weatherCodes[weatherCodesHourly[index]]}.svg#Layer_1"
: 'night'}-{weatherCodes[weatherCodesHourly?.[index] ?? 0]}.svg#Layer_1"
></use>
</svg></td
>
@@ -490,7 +544,7 @@
>Temp graph</th
>
{#each weather.indexes as index, j (j)}
{@const temp = weather.entries[0].values[index]}
{@const temp = Number(weather.entries[0].values?.[index])}
{#if !isNaN(temp)}
<td
@@ -519,7 +573,11 @@
>
{#each weather.indexes as index, j (j)}
{#if !isNaN(entry.values[index])}
{@const val = entry.values?.[index]}
{@const valNum = Number(val)}
{@const bgColor = getColor(valNum, $params.temperature_unit)}
{#if val !== undefined && !isNaN(valNum)}
<!-- Note: We use values?.[index] access here. -->
<td
class="border-r border-border {weather.hourlyTime[index].getDate() ===
today.getDate() && weather.hourlyTime[index].getHours() === today.getHours()
@@ -527,41 +585,17 @@
: ''}"
style="min-width: {5000 / weather.entriesLength}px; max-width: {5000 /
weather.entriesLength}px;
{entry.name === 'temperature_2m'
? 'background: ' +
getColor(
weather.entries[0].values[index].toFixed(0),
$params.temperature_unit
)
: ''};
{entry.name === 'temperature_2m'
? 'color: ' +
(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 === 'temperature_2m' ? 'background: ' + bgColor : ''};
{entry.name === 'temperature_2m' ? ('color: ' + textWhite(bgColor) ? 'white' : 'black') : ''};
{entry.name === 'precipitation_probability'
? 'background: rgba(0, 0, 230,' + valNum / 120 + ')'
: ''};
{entry.name === 'precipitation_probability'
? 'background: rgba(0, 0, 230,' +
weather.entries[2].values[index] / 120 +
')'
: ''};
{entry.name === 'precipitation_probability'
? 'color: ' +
(weather.entries[2].values[index] > 50
? 'white'
: 'hsl(var(--foreground)')
? 'color: ' + (valNum > 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 +
')'
: ''};"
>{entry.name === 'precipitation' || entry.name === 'temperature_2m'
? entry.values[index].toFixed(1)
: entry.values[index]}</td
? 'background: rgba(0, 240, 240,' + valNum ** 3.8 / 10 ** 8.2 + ')'
: ''};">{val}</td
>
{/if}
{/each}
@@ -577,7 +611,7 @@
>Wind Dir.</th
>
{#each weather.indexes as index, j (j)}
{#if !isNaN(weather.windDirections[index])}
{#if weather.windDirections && !isNaN(weather.windDirections[index])}
<td
class="border-r border-border {weather.hourlyTime[index].getDate() ===
today.getDate() && weather.hourlyTime[index].getHours() === today.getHours()
@@ -595,14 +629,14 @@
{/each}
</tr>
{/if}
{/await}
{/if}
</tbody>
</table>
</div>
</div>
{#await weatherDaily then wd}
{@const sunrise = new Date(Number(wd.daily.sunrise.valuesInt64(selectedDayIndex)) * 1000)}
{@const sunset = new Date(Number(wd.daily.sunset.valuesInt64(selectedDayIndex)) * 1000)}
{#if weatherDaily}
{@const sunrise = new Date(Number(weatherDaily.sunrise.valuesInt64(selectedDayIndex)) * 1000)}
{@const sunset = new Date(Number(weatherDaily.sunset.valuesInt64(selectedDayIndex)) * 1000)}
<div class="mt-6">
<div class="flex items-center gap-1">
<svg class="fill-foreground" width="28px" height="28px">
@@ -616,7 +650,7 @@
Sunset: {pad(sunset.getHours())}:{pad(sunset.getMinutes())}
</div>
</div>
{/await}
{/if}
<div>
<div class="mt-6 flex gap-6 md:mt-12">
<div class="relative w-1/2">