wip improvements but still problematic
This commit is contained in:
+1
-1
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"utils": "$lib/utils/ui",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"userWords": [
|
||||
"ConfigInterface"
|
||||
]
|
||||
}
|
||||
Generated
+1
@@ -8,6 +8,7 @@
|
||||
"name": "open-meteo-weather",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@openmeteo/sdk": "^1.23.0",
|
||||
"highcharts": "^12.4.0",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"openmeteo": "^1.2.3"
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"vitest-browser-svelte": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openmeteo/sdk": "^1.23.0",
|
||||
"highcharts": "^12.4.0",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"openmeteo": "^1.2.3"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '$lib/utils/ui.js';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Checkbox as CheckboxPrimitive } from 'bits-ui';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import type { Snippet } from 'svelte';
|
||||
import * as Dialog from './index.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils/ui.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '$lib/utils/ui.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '$lib/utils/ui.js';
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import SelectPortal from './select-portal.svelte';
|
||||
import SelectScrollUpButton from './select-scroll-up-button.svelte';
|
||||
import SelectScrollDownButton from './select-scroll-down-button.svelte';
|
||||
import { cn, type WithoutChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChild } from '$lib/utils/ui.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import type { WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import type { WithoutChildrenOrChild } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '$lib/utils/ui.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChild } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '$lib/utils/ui.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Separator as SeparatorPrimitive } from 'bits-ui';
|
||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import { cn, type WithoutChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChild } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Switch as SwitchPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils/ui.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,24 +1,127 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { type Writable, 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<string, unknown>) {
|
||||
const { subscribe, set, update } = writable(initialValue);
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
|
||||
// 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
|
||||
import { debounce, isNumeric } from '$lib/utils';
|
||||
|
||||
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 }))
|
||||
};
|
||||
import type { Parameters } from '$lib/docs';
|
||||
|
||||
export type UrlHashStore = Writable<Parameters>;
|
||||
|
||||
export const urlHashStore = (initialValues: Parameters): UrlHashStore => {
|
||||
const urlHashes: Writable<Parameters> = writable({});
|
||||
|
||||
const defaultValues = JSON.parse(JSON.stringify(initialValues));
|
||||
urlHashes.set(JSON.parse(JSON.stringify(defaultValues)));
|
||||
|
||||
function updateURL() {
|
||||
const searchParams = page.url.searchParams.toString().replaceAll('%2C', ',');
|
||||
const link = `?${searchParams}${page.url.hash ?? ''}`;
|
||||
if (page.url.search !== window.location.search) {
|
||||
goto(link, {
|
||||
noScroll: true,
|
||||
keepFocus: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const processURLParamsUpdate = debounce(() => updateURL());
|
||||
|
||||
const updateURLParams = (values: Parameters) => {
|
||||
if (browser) {
|
||||
let changedParams = false;
|
||||
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
let defaultValue = defaultValues[key];
|
||||
|
||||
// params key is array
|
||||
if (defaultValue && Array === defaultValue.constructor) {
|
||||
if (JSON.stringify(value) === JSON.stringify(defaultValue)) {
|
||||
if (page.url.searchParams.has(key) && page.url.searchParams.get(key) !== value) {
|
||||
page.url.searchParams.delete(key);
|
||||
changedParams = true;
|
||||
}
|
||||
} else {
|
||||
let array = value;
|
||||
// remove empty string when array has more then 1 values
|
||||
if (array.length > 1 && array.includes('')) {
|
||||
array = value.filter((e: string) => e !== '');
|
||||
}
|
||||
page.url.searchParams.set(key, array.join(','));
|
||||
changedParams = true;
|
||||
}
|
||||
} else {
|
||||
let val: number | string = value;
|
||||
if (isNumeric(defaultValue)) {
|
||||
defaultValue = Number(defaultValue);
|
||||
}
|
||||
if (isNumeric(value)) {
|
||||
val = Number(value);
|
||||
}
|
||||
|
||||
if (val != defaultValue) {
|
||||
page.url.searchParams.set(key, String(val));
|
||||
changedParams = true;
|
||||
} else {
|
||||
if (page.url.searchParams.has(key) && page.url.searchParams.get(key) !== val) {
|
||||
page.url.searchParams.delete(key);
|
||||
changedParams = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (page.url.searchParams.has(key) && page.url.searchParams.get(key) === '') {
|
||||
if (
|
||||
defaultValue === undefined ||
|
||||
(defaultValue && Array === defaultValue.constructor && defaultValue.length === 0) ||
|
||||
defaultValue === '0'
|
||||
) {
|
||||
page.url.searchParams.delete(key);
|
||||
changedParams = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changedParams) {
|
||||
processURLParamsUpdate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// check if urlParams overrides any default values OR stored values
|
||||
if (browser && page.url.search) {
|
||||
for (const [key, value] of page.url.searchParams.entries()) {
|
||||
let defaultValue = defaultValues[key];
|
||||
|
||||
if (defaultValue && defaultValue.constructor === Array) {
|
||||
if (JSON.stringify(defaultValue) !== JSON.stringify(value)) {
|
||||
urlHashes.update((urlValues) => {
|
||||
urlValues[key] = value.split(/,|%2C/);
|
||||
return urlValues;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let val: number | string = value;
|
||||
if (isNumeric(defaultValue)) {
|
||||
defaultValue = Number(defaultValue);
|
||||
}
|
||||
if (isNumeric(value)) {
|
||||
val = Number(value);
|
||||
}
|
||||
if (defaultValue !== val) {
|
||||
urlHashes.update((urlValues) => {
|
||||
urlValues[key] = val;
|
||||
return urlValues;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
urlHashes.subscribe((values) => {
|
||||
updateURLParams(values);
|
||||
});
|
||||
|
||||
return urlHashes;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
export const isNumeric = (num: string | number) =>
|
||||
(typeof num === 'number' || (typeof num === 'string' && num.trim() !== '')) &&
|
||||
!isNaN(num as number);
|
||||
|
||||
export const pad = (n: string | number) => {
|
||||
if (n === null || n === undefined) {
|
||||
return '';
|
||||
}
|
||||
return ('0' + n).slice(-2);
|
||||
};
|
||||
|
||||
export function debounce<F extends (...args: unknown[]) => unknown>(
|
||||
func: F,
|
||||
timeout = 100
|
||||
): (this: ThisParameterType<F>, ...args: Parameters<F>) => void {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
return function (this: ThisParameterType<F>, ...args: Parameters<F>) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
}, timeout);
|
||||
};
|
||||
}
|
||||
@@ -11,10 +11,3 @@ export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
|
||||
export const pad = (n: string | number) => {
|
||||
if (n === null || n === undefined) {
|
||||
return '';
|
||||
}
|
||||
return ('0' + n).slice(-2);
|
||||
};
|
||||
@@ -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}¤t=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}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ConfigInterface } from '../config';
|
||||
|
||||
export default (
|
||||
ctx: CanvasRenderingContext2D | null | undefined,
|
||||
config: ConfigInterface,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
Vendored
-8
@@ -1,8 +0,0 @@
|
||||
interface ConfigInterface {
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
deltaX: number;
|
||||
minTemp: number;
|
||||
maxTemp: number;
|
||||
diffTemp: number;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,195 +69,166 @@
|
||||
...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 = {
|
||||
// Constants
|
||||
const today = new Date();
|
||||
const entries = 6;
|
||||
const winddir = true;
|
||||
|
||||
let modelSelected = $derived(models.find((mo) => String(mo.value) === $params.models));
|
||||
|
||||
// --- Logic ---
|
||||
|
||||
async function loadWeatherData() {
|
||||
if (!location) return;
|
||||
|
||||
const commonParams = {
|
||||
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];
|
||||
|
||||
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'
|
||||
})
|
||||
]);
|
||||
|
||||
processHourly(hourlyRes[0]);
|
||||
processDaily(dailyRes[0]);
|
||||
} catch (e) {
|
||||
console.error('Weather fetch error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function processHourly(response: WeatherApiResponse) {
|
||||
const utcOffsetSeconds = response.utcOffsetSeconds();
|
||||
const hourly = response.hourly()!;
|
||||
|
||||
weatherCodesHourly = hourly.variables(3)?.valuesArray();
|
||||
weatherCodesHourly = hourly.variables(3)?.valuesArray() ?? undefined;
|
||||
|
||||
let hourlyTime = [
|
||||
const hourlyTime = [
|
||||
...Array((Number(hourly.timeEnd()) - Number(hourly.time())) / hourly.interval())
|
||||
].map(
|
||||
(_, i) =>
|
||||
new Date((Number(hourly.time()) + i * hourly.interval() + utcOffsetSeconds) * 1000)
|
||||
(_, 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 = [];
|
||||
|
||||
// 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;
|
||||
|
||||
const indexes: number[] = [];
|
||||
if (hourlyTemps) {
|
||||
for (const index of hourlyTemps.keys()) {
|
||||
indexes.push(index);
|
||||
for (let i = 0; i < hourlyTemps.length; i++) {
|
||||
indexes.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
const maxX = 10000;
|
||||
const maxY = 500;
|
||||
const deltaX = 10000 / hourly.variables(0)?.valuesArray()?.length;
|
||||
// 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 ctx = canvasElement?.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, maxX, maxY);
|
||||
const minT = validTemps.length ? Math.min(...validTemps) : 0;
|
||||
const maxT = validTemps.length ? Math.max(...validTemps) : 0;
|
||||
|
||||
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;
|
||||
maxTemp = maxT;
|
||||
diffTemp = maxT - minT;
|
||||
|
||||
const config: ConfigInterface = {
|
||||
maxX: maxX,
|
||||
maxY: maxY,
|
||||
deltaX: deltaX,
|
||||
minTemp: minTemp,
|
||||
maxTemp: maxTemp,
|
||||
diffTemp: diffTemp
|
||||
// Map helper
|
||||
const formatValues = (arr: Float32Array | undefined, digits: number): string[] => {
|
||||
if (!arr) return [];
|
||||
return Array.from(arr).map((v) => v.toFixed(digits));
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
return {
|
||||
weather = {
|
||||
entries: [
|
||||
{
|
||||
id: 0,
|
||||
name: 'temperature_2m',
|
||||
title: 'Temperature',
|
||||
values: hourly
|
||||
.variables(2)
|
||||
?.valuesArray()
|
||||
?.map((t) => t.toFixed(1))
|
||||
values: formatValues(hourlyTemps, 1)
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'precipitation',
|
||||
title: 'Precipitation',
|
||||
values: hourly
|
||||
.variables(0)
|
||||
?.valuesArray()
|
||||
?.map((p) => p.toFixed(1))
|
||||
values: formatValues(hourlyPrecip, 1)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'precipitation_probability',
|
||||
title: 'Precip Prob.',
|
||||
values: hourly
|
||||
.variables(1)
|
||||
?.valuesArray()
|
||||
?.map((p) => p.toFixed(0))
|
||||
values: formatValues(hourlyPrecipProb, 0)
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'windspeed_10m',
|
||||
title: 'Wind',
|
||||
values: hourly
|
||||
.variables(4)
|
||||
?.valuesArray()
|
||||
?.map((p) => p.toFixed(0))
|
||||
values: formatValues(hourlyWindSpeed, 0)
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'relative_humidity_2m',
|
||||
title: 'Rel. Hum.',
|
||||
values: hourly
|
||||
.variables(7)
|
||||
?.valuesArray()
|
||||
?.map((p) => p.toFixed(0))
|
||||
values: formatValues(hourlyHumidity, 0)
|
||||
}
|
||||
],
|
||||
entriesLength: hourly.variables(0)?.valuesArray()?.length,
|
||||
entriesLength: hourly.variables(0)?.valuesArray()?.length ?? 0,
|
||||
hourlyTime: hourlyTime,
|
||||
windDirections: hourly.variables(5)?.valuesArray(),
|
||||
indexes: indexes
|
||||
windDirections: hourlyWindDir,
|
||||
indexes: indexes,
|
||||
raw: {
|
||||
hourlyTemps,
|
||||
hourlyCloudCover,
|
||||
hourlyPrecip,
|
||||
minTemp: minT
|
||||
}
|
||||
};
|
||||
})(location)
|
||||
);
|
||||
}
|
||||
|
||||
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];
|
||||
function processDaily(response: WeatherApiResponse) {
|
||||
const utcOffsetSeconds = response.utcOffsetSeconds();
|
||||
const daily = response.daily()!;
|
||||
|
||||
return {
|
||||
daily: {
|
||||
weatherDaily = {
|
||||
time: [...Array((Number(daily.timeEnd()) - Number(daily.time())) / daily.interval())].map(
|
||||
(_, i) =>
|
||||
new Date((Number(daily.time()) + i * daily.interval() + utcOffsetSeconds) * 1000)
|
||||
(_, i) => new Date((Number(daily.time()) + i * daily.interval() + utcOffsetSeconds) * 1000)
|
||||
),
|
||||
weather_code: daily.variables(0)!,
|
||||
temperature_2m_max: daily.variables(1)!,
|
||||
@@ -228,66 +240,108 @@
|
||||
windspeed_10m_max: daily.variables(7)!,
|
||||
windgusts_10m_max: daily.variables(8)!,
|
||||
winddirection_10m_dominant: daily.variables(9)!
|
||||
}
|
||||
};
|
||||
})(location)
|
||||
);
|
||||
}
|
||||
|
||||
let winddir = true;
|
||||
entries = 6;
|
||||
// --- Effects ---
|
||||
|
||||
let scrollDiv: HTMLElement = $state();
|
||||
let tableCells;
|
||||
$effect(() => {
|
||||
if (location && $params) {
|
||||
loadWeatherData();
|
||||
}
|
||||
});
|
||||
|
||||
const switchDay = (date: SvelteDate, index: number) => {
|
||||
$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;
|
||||
|
||||
tableCells = document.querySelectorAll('td.time');
|
||||
await tick();
|
||||
|
||||
for (let tableCell of tableCells) {
|
||||
if (Number(tableCell.dataset['date']) === selectedDay.getDate()) {
|
||||
scrollDiv.scrollTo({ left: tableCell.offsetLeft - 110, behavior: 'smooth' });
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
selectedDayIndex = index;
|
||||
};
|
||||
|
||||
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 (
|
||||
!scrollDiv ||
|
||||
scrollDiv === document.activeElement ||
|
||||
scrollDiv.contains(document.activeElement)
|
||||
)
|
||||
return;
|
||||
|
||||
if (e.key === 'ArrowLeft') {
|
||||
if (selectedDay.getDate() >= today.getDate()) {
|
||||
let newDate = new SvelteDate();
|
||||
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();
|
||||
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">
|
||||
|
||||
Reference in New Issue
Block a user