gemini going wild on this
This commit is contained in:
+1
-3
@@ -1,5 +1,3 @@
|
|||||||
{
|
{
|
||||||
"userWords": [
|
"userWords": ["ConfigInterface"]
|
||||||
"ConfigInterface"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -240,8 +240,8 @@
|
|||||||
{location.country || ''}
|
{location.country || ''}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{location.latitude.toFixed(2)}°N {location.longitude.toFixed(2)}°E
|
{location.latitude?.toFixed(2)}°N {location.longitude?.toFixed(2)}°E
|
||||||
{#if location.elevation}• {location.elevation.toFixed(0)}m{/if}
|
{#if location.elevation}• {location.elevation?.toFixed(0)}m{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils/index.js';
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-action"
|
data-slot="card-action"
|
||||||
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
class={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils/index.js';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
@@ -10,6 +10,6 @@
|
|||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
|
<div bind:this={ref} data-slot="card-content" class={cn('px-6', className)} {...restProps}>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils/index.js';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<p
|
<p
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-description"
|
data-slot="card-description"
|
||||||
class={cn("text-muted-foreground text-sm", className)}
|
class={cn('text-sm text-muted-foreground', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils/index.js';
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-footer"
|
data-slot="card-footer"
|
||||||
class={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
class={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils/index.js';
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-header"
|
data-slot="card-header"
|
||||||
class={cn(
|
class={cn(
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils/index.js';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card-title"
|
data-slot="card-title"
|
||||||
class={cn("leading-none font-semibold", className)}
|
class={cn('leading-none font-semibold', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from '$lib/utils/index.js';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
'flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import Root from "./card.svelte";
|
import Root from './card.svelte';
|
||||||
import Content from "./card-content.svelte";
|
import Content from './card-content.svelte';
|
||||||
import Description from "./card-description.svelte";
|
import Description from './card-description.svelte';
|
||||||
import Footer from "./card-footer.svelte";
|
import Footer from './card-footer.svelte';
|
||||||
import Header from "./card-header.svelte";
|
import Header from './card-header.svelte';
|
||||||
import Title from "./card-title.svelte";
|
import Title from './card-title.svelte';
|
||||||
import Action from "./card-action.svelte";
|
import Action from './card-action.svelte';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
@@ -21,5 +21,5 @@ export {
|
|||||||
Footer as CardFooter,
|
Footer as CardFooter,
|
||||||
Header as CardHeader,
|
Header as CardHeader,
|
||||||
Title as CardTitle,
|
Title as CardTitle,
|
||||||
Action as CardAction,
|
Action as CardAction
|
||||||
};
|
};
|
||||||
|
|||||||
+18
-18
@@ -1,24 +1,24 @@
|
|||||||
import { persisted } from 'svelte-persisted-store';
|
import { persisted } from 'svelte-persisted-store';
|
||||||
|
|
||||||
export interface GeoLocation {
|
export interface GeoLocation {
|
||||||
id: number;
|
id?: number;
|
||||||
name: string;
|
name?: string;
|
||||||
latitude: number;
|
latitude?: number;
|
||||||
longitude: number;
|
longitude?: number;
|
||||||
elevation: number;
|
elevation?: number;
|
||||||
feature_code: string;
|
feature_code?: string;
|
||||||
country_code: string | undefined;
|
country_code?: string;
|
||||||
admin1_id: number | undefined;
|
admin1_id?: number;
|
||||||
admin3_id?: number | undefined;
|
admin3_id?: number;
|
||||||
admin4_id?: number | undefined;
|
admin4_id?: number;
|
||||||
timezone: string;
|
timezone?: string;
|
||||||
population: number | undefined;
|
population?: number;
|
||||||
postcodes: string[] | undefined;
|
postcodes?: string[];
|
||||||
country_id: number | undefined;
|
country_id?: number;
|
||||||
country: string | undefined;
|
country?: string;
|
||||||
admin1: string | undefined;
|
admin1?: string;
|
||||||
admin3?: string | undefined;
|
admin3?: string;
|
||||||
admin4?: string | undefined;
|
admin4?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultLocation: GeoLocation = {
|
export const defaultLocation: GeoLocation = {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { page } from '$app/state';
|
|||||||
|
|
||||||
import { debounce, isNumeric } from '$lib/utils';
|
import { debounce, isNumeric } from '$lib/utils';
|
||||||
|
|
||||||
import type { Parameters } from '$lib/docs';
|
import type { Parameters } from '$lib/types';
|
||||||
|
|
||||||
export type UrlHashStore = Writable<Parameters>;
|
export type UrlHashStore = Writable<Parameters>;
|
||||||
|
|
||||||
@@ -44,20 +44,20 @@ export const urlHashStore = (initialValues: Parameters): UrlHashStore => {
|
|||||||
changedParams = true;
|
changedParams = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let array = value;
|
let array = value as any[];
|
||||||
// remove empty string when array has more then 1 values
|
// remove empty string when array has more then 1 values
|
||||||
if (array.length > 1 && array.includes('')) {
|
if (array.length > 1 && array.includes('')) {
|
||||||
array = value.filter((e: string) => e !== '');
|
array = array.filter((e: string) => e !== '');
|
||||||
}
|
}
|
||||||
page.url.searchParams.set(key, array.join(','));
|
page.url.searchParams.set(key, array.join(','));
|
||||||
changedParams = true;
|
changedParams = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let val: number | string = value;
|
let val: number | string = value as number | string;
|
||||||
if (isNumeric(defaultValue)) {
|
if (isNumeric(defaultValue)) {
|
||||||
defaultValue = Number(defaultValue);
|
defaultValue = Number(defaultValue);
|
||||||
}
|
}
|
||||||
if (isNumeric(value)) {
|
if (isNumeric(value as number | string)) {
|
||||||
val = Number(value);
|
val = Number(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
export interface Parameters {
|
||||||
|
latitude?: number | number[];
|
||||||
|
longitude?: number | number[];
|
||||||
|
hourly?: string[];
|
||||||
|
models?: string[];
|
||||||
|
daily?: string[];
|
||||||
|
current?: string[];
|
||||||
|
minutely_15?: string[];
|
||||||
|
timezone?: string;
|
||||||
|
location_mode?: string;
|
||||||
|
csv_coordinates?: string;
|
||||||
|
time_mode?: string;
|
||||||
|
past_days?: string;
|
||||||
|
forecast_days?: string;
|
||||||
|
end_date?: string;
|
||||||
|
start_date?: string;
|
||||||
|
past_hours?: string;
|
||||||
|
cell_selection?: string;
|
||||||
|
forecast_hours?: string;
|
||||||
|
past_minutely_15?: string;
|
||||||
|
temporal_resolution?: string;
|
||||||
|
forecast_minutely_15?: string;
|
||||||
|
tilt?: string;
|
||||||
|
azimuth?: string;
|
||||||
|
timeformat?: string;
|
||||||
|
wind_speed_unit?: string;
|
||||||
|
temperature_unit?: string;
|
||||||
|
precipitation_unit?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
export * from './ui.js';
|
||||||
|
export * from './meteo.js';
|
||||||
|
|
||||||
export const isNumeric = (num: string | number) =>
|
export const isNumeric = (num: string | number) =>
|
||||||
(typeof num === 'number' || (typeof num === 'string' && num.trim() !== '')) &&
|
(typeof num === 'number' || (typeof num === 'string' && num.trim() !== '')) &&
|
||||||
!isNaN(num as number);
|
!isNaN(num as number);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const prerender = true;
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
let mounted = $state(false);
|
let mounted = $state(false);
|
||||||
let location = get(storedLocation);
|
let location = $derived($storedLocation);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
mounted = true;
|
mounted = true;
|
||||||
@@ -180,7 +180,9 @@
|
|||||||
Current Location: {location.name}
|
Current Location: {location.name}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription class="text-center">
|
<CardDescription class="text-center">
|
||||||
{location.country} • {location.latitude.toFixed(2)}°, {location.longitude.toFixed(2)}°
|
{location.country} • {location.latitude?.toFixed(2)}°, {location.longitude?.toFixed(
|
||||||
|
2
|
||||||
|
)}°
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="text-center">
|
<CardContent class="text-center">
|
||||||
|
|||||||
@@ -83,8 +83,8 @@
|
|||||||
{/if}{location.country}
|
{/if}{location.country}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{location.latitude.toFixed(2)}°N, {location.longitude.toFixed(2)}°E
|
{location.latitude?.toFixed(2)}°N, {location.longitude?.toFixed(2)}°E
|
||||||
{#if location.elevation}• {location.elevation.toFixed(0)}m{/if}
|
{#if location.elevation}• {location.elevation?.toFixed(0)}m{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { get } from 'svelte/store';
|
|||||||
|
|
||||||
import { storedLocation } from '$lib/stores/settings';
|
import { storedLocation } from '$lib/stores/settings';
|
||||||
|
|
||||||
import type { LayoutLoad } from './$types';
|
import type { LayoutLoad } from '././$types';
|
||||||
|
|
||||||
const location = get(storedLocation);
|
const location = get(storedLocation);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import type { PageLoad } from '$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
|
|
||||||
export const load = (async () => {
|
export const load: PageLoad = async () => {
|
||||||
throw redirect(303, '/weather/week/');
|
throw redirect(303, '/weather/week/');
|
||||||
}) satisfies PageLoad;
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { get } from 'svelte/store';
|
|||||||
|
|
||||||
import { storedLocation } from '$lib/stores/settings';
|
import { storedLocation } from '$lib/stores/settings';
|
||||||
|
|
||||||
import type { LayoutLoad } from './$types';
|
import type { LayoutLoad } from '././$types';
|
||||||
|
|
||||||
const location = get(storedLocation);
|
const location = get(storedLocation);
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,9 @@
|
|||||||
import { defaultParameters } from './options';
|
import { defaultParameters } from './options';
|
||||||
|
|
||||||
let node: HTMLElement;
|
let node: HTMLElement;
|
||||||
let chart: any;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let Highcharts = $state(null);
|
let charts: any[] = [];
|
||||||
|
let Highcharts = $state<any>(null);
|
||||||
|
|
||||||
let showLegend = $state(false);
|
let showLegend = $state(false);
|
||||||
let averageOnly = $state(false);
|
let averageOnly = $state(false);
|
||||||
@@ -36,237 +37,251 @@
|
|||||||
/// Highcharts needs to be loaded in `onMount` to work with prerendered SSG
|
/// Highcharts needs to be loaded in `onMount` to work with prerendered SSG
|
||||||
Highcharts = (await import('highcharts')).default;
|
Highcharts = (await import('highcharts')).default;
|
||||||
const more = (await import('highcharts/highcharts-more')).default;
|
const more = (await import('highcharts/highcharts-more')).default;
|
||||||
// more(Highcharts);
|
(more as any)(Highcharts);
|
||||||
|
|
||||||
if (dev) {
|
if (dev) {
|
||||||
// const HighchartsDebugger = await import('highcharts/modules/debugger');
|
// const HighchartsDebugger = await import('highcharts/modules/debugger');
|
||||||
// HighchartsDebugger.default(Highcharts);
|
// HighchartsDebugger.default(Highcharts);
|
||||||
const Debugger = (await import('highcharts/es-modules/Extensions/Debugger/Debugger.js'))
|
// @ts-ignore
|
||||||
.default;
|
const Debugger = (
|
||||||
|
(await import('highcharts/es-modules/Extensions/Debugger/Debugger.js')) as any
|
||||||
|
).default;
|
||||||
|
// @ts-ignore
|
||||||
const ErrorMessages = (
|
const ErrorMessages = (
|
||||||
await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js')
|
(await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js')) as any
|
||||||
).default;
|
).default;
|
||||||
Highcharts.errorMessages = ErrorMessages;
|
Highcharts.errorMessages = ErrorMessages;
|
||||||
Debugger.compose(Highcharts.Chart);
|
Debugger.compose(Highcharts.Chart);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(async () => {
|
$effect(() => {
|
||||||
count = 0;
|
count = 0;
|
||||||
if (Highcharts) {
|
if (Highcharts) {
|
||||||
node.replaceChildren([]);
|
charts.forEach((c) => c.destroy());
|
||||||
|
charts = [];
|
||||||
|
// eslint-disable-next-line svelte/no-dom-manipulating
|
||||||
|
(node as any).replaceChildren();
|
||||||
|
|
||||||
const dataDaily = await fetch(
|
(async () => {
|
||||||
`https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&timeformat=unixtime&daily=sunset,sunrise&forecast_days=14`
|
const dataDaily = await fetch(
|
||||||
);
|
`https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&timeformat=unixtime&daily=sunset,sunrise&forecast_days=14`
|
||||||
const wd = await dataDaily.json();
|
);
|
||||||
|
const wd = await dataDaily.json();
|
||||||
|
|
||||||
const dataReq = await fetch(
|
const dataReq = await fetch(
|
||||||
`https://ensemble-api.open-meteo.com/v1/ensemble?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${$params.hourly.join(',')}&models=${$params.models.join(',')}&timeformat=unixtime&forecast_days=14`
|
`https://ensemble-api.open-meteo.com/v1/ensemble?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${$params.hourly?.join(',')}&models=${$params.models?.join(',')}&timeformat=unixtime&forecast_days=14`
|
||||||
);
|
);
|
||||||
const data = await dataReq.json();
|
const data = await dataReq.json();
|
||||||
|
|
||||||
let plotBands: any = [];
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
if ('daily' in wd && 'sunrise' in wd.daily && 'sunset' in wd.daily) {
|
let plotBands: any = [];
|
||||||
let rise = wd.daily.sunrise;
|
if ('daily' in wd && 'sunrise' in wd.daily && 'sunset' in wd.daily) {
|
||||||
let set = wd.daily.sunset;
|
let rise = wd.daily.sunrise;
|
||||||
plotBands = rise.map(function (r, i) {
|
let set = wd.daily.sunset;
|
||||||
return {
|
plotBands = rise.map(function (r: any, i: number) {
|
||||||
color: 'rgba(255, 255, 194, 0.5)',
|
return {
|
||||||
from: (r + data.utc_offset_seconds) * 1000,
|
color: 'rgba(255, 255, 194, 0.5)',
|
||||||
to: (set[i] + data.utc_offset_seconds) * 1000
|
from: (r + data.utc_offset_seconds) * 1000,
|
||||||
};
|
to: (set[i] + data.utc_offset_seconds) * 1000
|
||||||
});
|
};
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let minValues = new Array(data.hourly.time.length).fill(undefined);
|
let minValues = new Array(data.hourly.time.length).fill(undefined);
|
||||||
let maxValues = new Array(data.hourly.time.length).fill(undefined);
|
let maxValues = new Array(data.hourly.time.length).fill(undefined);
|
||||||
|
|
||||||
for (let variable of $params.hourly) {
|
for (let variable of $params.hourly || []) {
|
||||||
const chartDiv = document.createElement('div');
|
const chartDiv = document.createElement('div');
|
||||||
|
|
||||||
let unit;
|
let unit;
|
||||||
|
|
||||||
let hourly_starttime = (data.hourly.time[0] + data.utc_offset_seconds) * 1000;
|
let hourly_starttime = (data.hourly.time[0] + data.utc_offset_seconds) * 1000;
|
||||||
let pointInterval = (data.hourly.time[1] - data.hourly.time[0]) * 1000;
|
let pointInterval = (data.hourly.time[1] - data.hourly.time[0]) * 1000;
|
||||||
|
|
||||||
const series = [];
|
const series = [];
|
||||||
let average = new Array(data.hourly.time.length).fill(0);
|
let average = new Array(data.hourly.time.length).fill(0);
|
||||||
let averageCount = new Array(data.hourly.time.length).fill(0);
|
let averageCount = new Array(data.hourly.time.length).fill(0);
|
||||||
|
|
||||||
for (let [model, values] of Object.entries(data.hourly)) {
|
for (let [model, values] of Object.entries(data.hourly)) {
|
||||||
if (model === 'time') {
|
if (model === 'time') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (model.startsWith(variable)) {
|
if (model.startsWith(variable)) {
|
||||||
for (let [index, val] of values.entries()) {
|
for (let [index, val] of (values as any[]).entries()) {
|
||||||
if (val) {
|
if (val) {
|
||||||
let avVal = average[index];
|
let avVal = average[index];
|
||||||
average[index] = avVal + val;
|
average[index] = avVal + val;
|
||||||
averageCount[index]++;
|
averageCount[index]++;
|
||||||
|
|
||||||
if (minValues[index] > val || minValues[index] === undefined) {
|
if (minValues[index] > val || minValues[index] === undefined) {
|
||||||
minValues[index] = val;
|
minValues[index] = val;
|
||||||
}
|
}
|
||||||
if (maxValues[index] < val || maxValues[index] === undefined) {
|
if (maxValues[index] < val || maxValues[index] === undefined) {
|
||||||
maxValues[index] = val;
|
maxValues[index] = val;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
unit = data.hourly_units[model];
|
unit = data.hourly_units[model];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (let [index, val] of average.entries()) {
|
for (let [index, val] of average.entries()) {
|
||||||
average[index] = Math.round((val / averageCount[index]) * 10) / 10;
|
average[index] = Math.round((val / averageCount[index]) * 10) / 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
const minMax = [];
|
const minMax = [];
|
||||||
for (let [index, min] of minValues.entries()) {
|
for (let [index, min] of minValues.entries()) {
|
||||||
minMax.push([min, maxValues[index]]);
|
minMax.push([min, maxValues[index]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
series.push({
|
series.push({
|
||||||
name: 'temperature_2m_spread',
|
name: 'temperature_2m_spread',
|
||||||
data: minMax,
|
data: minMax,
|
||||||
type: 'arearange',
|
type: 'arearange',
|
||||||
tooltip: {
|
tooltip: {
|
||||||
valueSuffix: ' ' + unit
|
valueSuffix: ' ' + unit
|
||||||
},
|
},
|
||||||
pointStart: hourly_starttime,
|
pointStart: hourly_starttime,
|
||||||
pointInterval: pointInterval,
|
pointInterval: pointInterval,
|
||||||
className: 'highcharts-spread-series'
|
className: 'highcharts-spread-series'
|
||||||
});
|
});
|
||||||
|
|
||||||
series.push({
|
series.push({
|
||||||
name: variable + '_average',
|
name: variable + '_average',
|
||||||
data: average,
|
data: average,
|
||||||
dashStyle: 'ShortDashDot',
|
dashStyle: 'ShortDashDot',
|
||||||
color: '#5e5e5e',
|
color: '#5e5e5e',
|
||||||
type:
|
type:
|
||||||
unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²' ? 'column' : 'spline',
|
unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²'
|
||||||
tooltip: {
|
? 'column'
|
||||||
valueSuffix: ' ' + unit
|
: 'spline',
|
||||||
},
|
tooltip: {
|
||||||
lineWidth: 4,
|
valueSuffix: ' ' + unit
|
||||||
states: {
|
},
|
||||||
hover: {
|
lineWidth: 4,
|
||||||
lineWidth: 6
|
states: {
|
||||||
}
|
hover: {
|
||||||
},
|
lineWidth: 6
|
||||||
pointStart: hourly_starttime,
|
|
||||||
pointInterval: pointInterval,
|
|
||||||
className: 'highcharts-average-series'
|
|
||||||
});
|
|
||||||
|
|
||||||
new Highcharts.Chart(chartDiv, {
|
|
||||||
credits: {
|
|
||||||
text: count === $params.hourly.length - 1 ? 'Open-Meteo.com' : '',
|
|
||||||
href: 'http://open-meteo.com'
|
|
||||||
},
|
|
||||||
|
|
||||||
chart: {
|
|
||||||
height: showLegend ? '400px' : '300px',
|
|
||||||
styledMode: true,
|
|
||||||
marginLeft: '50',
|
|
||||||
marginRight: 0
|
|
||||||
},
|
|
||||||
|
|
||||||
lang: {
|
|
||||||
locale: 'en-GB'
|
|
||||||
},
|
|
||||||
|
|
||||||
title: {
|
|
||||||
text: count === 0 ? 'Model Spread' : '',
|
|
||||||
align: 'left'
|
|
||||||
},
|
|
||||||
|
|
||||||
subtitle: {
|
|
||||||
text:
|
|
||||||
count === 0
|
|
||||||
? `Compare <span class="font-bold">${$params.hourly.join(', ')}</span> in models: <span class="font-bold">` +
|
|
||||||
$params.models.join(', ') +
|
|
||||||
'</span>'
|
|
||||||
: '',
|
|
||||||
align: 'left'
|
|
||||||
},
|
|
||||||
|
|
||||||
yAxis: {
|
|
||||||
title: {
|
|
||||||
text: unit
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
xAxis: {
|
|
||||||
type: 'datetime',
|
|
||||||
plotLines: [
|
|
||||||
{
|
|
||||||
value: Date.now() + data.utc_offset_seconds * 1000,
|
|
||||||
color: 'red',
|
|
||||||
width: 2
|
|
||||||
}
|
|
||||||
],
|
|
||||||
plotBands: plotBands
|
|
||||||
},
|
|
||||||
|
|
||||||
plotOptions: {
|
|
||||||
spline: {
|
|
||||||
lineWidth: 2,
|
|
||||||
states: {
|
|
||||||
hover: {
|
|
||||||
lineWidth: 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
marker: {
|
|
||||||
enabled: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
column: {
|
pointStart: hourly_starttime,
|
||||||
pointWidth: 5
|
pointInterval: pointInterval,
|
||||||
}
|
className: 'highcharts-average-series'
|
||||||
},
|
});
|
||||||
|
|
||||||
legend: {
|
const chart = new Highcharts.Chart(chartDiv, {
|
||||||
enabled: showLegend,
|
credits: {
|
||||||
layout: 'horizontal',
|
text: count === ($params.hourly?.length || 0) - 1 ? 'Open-Meteo.com' : '',
|
||||||
align: 'center',
|
href: 'http://open-meteo.com'
|
||||||
verticalAlign: 'bottom'
|
},
|
||||||
},
|
|
||||||
|
|
||||||
series: series,
|
chart: {
|
||||||
|
height: showLegend ? '400px' : '300px',
|
||||||
|
styledMode: true,
|
||||||
|
marginLeft: '50',
|
||||||
|
marginRight: 0
|
||||||
|
},
|
||||||
|
|
||||||
responsive: {
|
lang: {
|
||||||
rules: [
|
locale: 'en-GB'
|
||||||
{
|
},
|
||||||
condition: {
|
|
||||||
maxWidth: 800
|
title: {
|
||||||
}
|
text: count === 0 ? 'Model Spread' : '',
|
||||||
|
align: 'left'
|
||||||
|
},
|
||||||
|
|
||||||
|
subtitle: {
|
||||||
|
text:
|
||||||
|
count === 0
|
||||||
|
? `Compare <span class="font-bold">${$params.hourly?.join(', ')}</span> in models: <span class="font-bold">` +
|
||||||
|
$params.models?.join(', ') +
|
||||||
|
'</span>'
|
||||||
|
: '',
|
||||||
|
align: 'left'
|
||||||
|
},
|
||||||
|
|
||||||
|
yAxis: {
|
||||||
|
title: {
|
||||||
|
text: unit
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
},
|
|
||||||
|
|
||||||
tooltip: {
|
xAxis: {
|
||||||
shared: true,
|
type: 'datetime',
|
||||||
animation: false
|
plotLines: [
|
||||||
}
|
{
|
||||||
});
|
value: Date.now() + data.utc_offset_seconds * 1000,
|
||||||
|
color: 'red',
|
||||||
|
width: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plotBands: plotBands
|
||||||
|
},
|
||||||
|
|
||||||
count++;
|
plotOptions: {
|
||||||
node.appendChild(chartDiv);
|
spline: {
|
||||||
}
|
lineWidth: 2,
|
||||||
|
states: {
|
||||||
|
hover: {
|
||||||
|
lineWidth: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
marker: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
pointWidth: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
legend: {
|
||||||
|
enabled: showLegend,
|
||||||
|
layout: 'horizontal',
|
||||||
|
align: 'center',
|
||||||
|
verticalAlign: 'bottom'
|
||||||
|
},
|
||||||
|
|
||||||
|
series: series,
|
||||||
|
|
||||||
|
responsive: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
condition: {
|
||||||
|
maxWidth: 800
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
tooltip: {
|
||||||
|
shared: true,
|
||||||
|
animation: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
count++;
|
||||||
|
charts.push(chart);
|
||||||
|
// eslint-disable-next-line svelte/no-dom-manipulating
|
||||||
|
node.appendChild(chartDiv);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (chart) {
|
charts.forEach((c) => c.destroy());
|
||||||
chart.destroy();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- min-h-[302px] min-h-[602px] min-h-[902px] min-h-[1202px] -->
|
<!-- min-h-[302px] min-h-[602px] min-h-[902px] min-h-[1202px] -->
|
||||||
<div class="container-wrapper relative -mx-6 md:mx-0 min-h-[{300 * $params.hourly.length + 2}px]">
|
<div
|
||||||
|
class="container-wrapper relative -mx-6 md:mx-0 min-h-[{300 * ($params.hourly?.length || 0) +
|
||||||
|
2}px]"
|
||||||
|
>
|
||||||
<div in:fade={{ duration: 300 }} out:fade={{ duration: 300 }} bind:this={node}></div>
|
<div in:fade={{ duration: 300 }} out:fade={{ duration: 300 }} bind:this={node}></div>
|
||||||
<div
|
<div
|
||||||
class="{count > 0
|
class="{count > 0
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { get } from 'svelte/store';
|
|||||||
|
|
||||||
import { storedLocation } from '$lib/stores/settings';
|
import { storedLocation } from '$lib/stores/settings';
|
||||||
|
|
||||||
import type { LayoutLoad } from './$types';
|
import type { LayoutLoad } from '././$types';
|
||||||
|
|
||||||
const location = get(storedLocation);
|
const location = get(storedLocation);
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,9 @@
|
|||||||
import { defaultParameters } from './options';
|
import { defaultParameters } from './options';
|
||||||
|
|
||||||
let node: HTMLElement;
|
let node: HTMLElement;
|
||||||
let chart: any;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let Highcharts = $state();
|
let charts: any[] = [];
|
||||||
|
let Highcharts = $state<any>();
|
||||||
|
|
||||||
let showLegend = $state(false);
|
let showLegend = $state(false);
|
||||||
let averageOnly = $state(false);
|
let averageOnly = $state(false);
|
||||||
@@ -47,224 +48,238 @@
|
|||||||
if (dev) {
|
if (dev) {
|
||||||
// const HighchartsDebugger = await import('highcharts/modules/debugger');
|
// const HighchartsDebugger = await import('highcharts/modules/debugger');
|
||||||
// HighchartsDebugger.default(Highcharts);
|
// HighchartsDebugger.default(Highcharts);
|
||||||
const Debugger = (await import('highcharts/es-modules/Extensions/Debugger/Debugger.js'))
|
// @ts-ignore
|
||||||
.default;
|
const Debugger = (
|
||||||
|
(await import('highcharts/es-modules/Extensions/Debugger/Debugger.js')) as any
|
||||||
|
).default;
|
||||||
|
// @ts-ignore
|
||||||
const ErrorMessages = (
|
const ErrorMessages = (
|
||||||
await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js')
|
(await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js')) as any
|
||||||
).default;
|
).default;
|
||||||
Highcharts.errorMessages = ErrorMessages;
|
Highcharts.errorMessages = ErrorMessages;
|
||||||
Debugger.compose(Highcharts.Chart);
|
Debugger.compose(Highcharts.Chart);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(async () => {
|
$effect(() => {
|
||||||
count = 0;
|
count = 0;
|
||||||
if (Highcharts) {
|
if (Highcharts) {
|
||||||
node.replaceChildren([]);
|
charts.forEach((c) => c.destroy());
|
||||||
|
charts = [];
|
||||||
|
// eslint-disable-next-line svelte/no-dom-manipulating
|
||||||
|
(node as any).replaceChildren();
|
||||||
|
|
||||||
const dataReq = await fetch(
|
(async () => {
|
||||||
`https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${$params.hourly.join(',')}&models=${$params.models.join(',')}&timeformat=unixtime&daily=sunset,sunrise`
|
const dataReq = await fetch(
|
||||||
);
|
`https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&hourly=${$params.hourly?.join(',')}&models=${$params.models?.join(',')}&timeformat=unixtime&daily=sunset,sunrise`
|
||||||
const data = await dataReq.json();
|
);
|
||||||
|
const data = await dataReq.json();
|
||||||
|
|
||||||
let dailyFirstModelKey = Object.keys(data.daily)[1].split('_');
|
let dailyFirstModelKeyParts = Object.keys(data.daily)[1].split('_');
|
||||||
dailyFirstModelKey.shift();
|
dailyFirstModelKeyParts.shift();
|
||||||
dailyFirstModelKey = dailyFirstModelKey.join('_');
|
const dailyFirstModelKey = dailyFirstModelKeyParts.join('_');
|
||||||
|
|
||||||
let plotBands: any = [];
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
if (
|
let plotBands: any = [];
|
||||||
'daily' in data &&
|
if (
|
||||||
'sunrise_' + dailyFirstModelKey in data.daily &&
|
'daily' in data &&
|
||||||
'sunset_' + dailyFirstModelKey in data.daily
|
'sunrise_' + dailyFirstModelKey in data.daily &&
|
||||||
) {
|
'sunset_' + dailyFirstModelKey in data.daily
|
||||||
let rise = data.daily['sunrise_' + dailyFirstModelKey];
|
) {
|
||||||
let set = data.daily['sunset_' + dailyFirstModelKey];
|
let rise = data.daily['sunrise_' + dailyFirstModelKey];
|
||||||
plotBands = rise.map(function (r, i) {
|
let set = data.daily['sunset_' + dailyFirstModelKey];
|
||||||
return {
|
plotBands = rise.map(function (r: any, i: number) {
|
||||||
color: 'rgba(255, 255, 194, 0.5)',
|
return {
|
||||||
from: (r + data.utc_offset_seconds) * 1000,
|
color: 'rgba(255, 255, 194, 0.5)',
|
||||||
to: (set[i] + data.utc_offset_seconds) * 1000
|
from: (r + data.utc_offset_seconds) * 1000,
|
||||||
};
|
to: (set[i] + data.utc_offset_seconds) * 1000
|
||||||
});
|
};
|
||||||
}
|
});
|
||||||
|
|
||||||
for (let variable of $params.hourly) {
|
|
||||||
const chartDiv = document.createElement('div');
|
|
||||||
|
|
||||||
let unit;
|
|
||||||
|
|
||||||
let hourly_starttime = (data.hourly.time[0] + data.utc_offset_seconds) * 1000;
|
|
||||||
let pointInterval = (data.hourly.time[1] - data.hourly.time[0]) * 1000;
|
|
||||||
|
|
||||||
const series = [];
|
|
||||||
let average = new Array(data.hourly.time.length).fill(0);
|
|
||||||
let averageCount = new Array(data.hourly.time.length).fill(0);
|
|
||||||
|
|
||||||
for (let [model, values] of Object.entries(data.hourly)) {
|
|
||||||
if (model === 'time') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (model.startsWith(variable)) {
|
|
||||||
for (let [index, val] of values.entries()) {
|
|
||||||
if (val) {
|
|
||||||
let avVal = average[index];
|
|
||||||
average[index] = avVal + val;
|
|
||||||
averageCount[index]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unit = data.hourly_units[model];
|
|
||||||
|
|
||||||
if (!averageOnly) {
|
|
||||||
series.push({
|
|
||||||
name: model,
|
|
||||||
data: values,
|
|
||||||
type:
|
|
||||||
unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²'
|
|
||||||
? 'column'
|
|
||||||
: 'spline',
|
|
||||||
tooltip: {
|
|
||||||
valueSuffix: ' ' + unit
|
|
||||||
},
|
|
||||||
pointStart: hourly_starttime,
|
|
||||||
pointInterval: pointInterval
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let [index, val] of average.entries()) {
|
for (let variable of $params.hourly || []) {
|
||||||
average[index] = Math.round((val / averageCount[index]) * 10) / 10;
|
const chartDiv = document.createElement('div');
|
||||||
}
|
|
||||||
|
|
||||||
series.push({
|
let unit;
|
||||||
name: variable + '_average',
|
|
||||||
data: average,
|
let hourly_starttime = (data.hourly.time[0] + data.utc_offset_seconds) * 1000;
|
||||||
dashStyle: 'ShortDashDot',
|
let pointInterval = (data.hourly.time[1] - data.hourly.time[0]) * 1000;
|
||||||
color: '#5e5e5e',
|
|
||||||
type:
|
const series = [];
|
||||||
unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²' ? 'column' : 'spline',
|
let average = new Array(data.hourly.time.length).fill(0);
|
||||||
tooltip: {
|
let averageCount = new Array(data.hourly.time.length).fill(0);
|
||||||
valueSuffix: ' ' + unit
|
|
||||||
},
|
for (let [model, values] of Object.entries(data.hourly)) {
|
||||||
lineWidth: 4,
|
if (model === 'time') {
|
||||||
states: {
|
continue;
|
||||||
hover: {
|
|
||||||
lineWidth: 6
|
|
||||||
}
|
}
|
||||||
},
|
if (model.startsWith(variable)) {
|
||||||
pointStart: hourly_starttime,
|
for (let [index, val] of (values as any[]).entries()) {
|
||||||
pointInterval: pointInterval,
|
if (val) {
|
||||||
className: 'highcharts-average-series'
|
let avVal = average[index];
|
||||||
});
|
average[index] = avVal + val;
|
||||||
|
averageCount[index]++;
|
||||||
new Highcharts.Chart(chartDiv, {
|
|
||||||
credits: {
|
|
||||||
text: count === $params.hourly.length - 1 ? 'Open-Meteo.com' : '',
|
|
||||||
href: 'http://open-meteo.com'
|
|
||||||
},
|
|
||||||
|
|
||||||
chart: {
|
|
||||||
height: showLegend ? '400px' : '300px',
|
|
||||||
styledMode: true,
|
|
||||||
marginLeft: '50',
|
|
||||||
marginRight: 0
|
|
||||||
},
|
|
||||||
|
|
||||||
lang: {
|
|
||||||
locale: 'en-GB'
|
|
||||||
},
|
|
||||||
|
|
||||||
title: {
|
|
||||||
text: count === 0 ? 'Model Compare' : '',
|
|
||||||
align: 'left'
|
|
||||||
},
|
|
||||||
|
|
||||||
subtitle: {
|
|
||||||
text:
|
|
||||||
count === 0
|
|
||||||
? `Compare <span class="font-bold">${$params.hourly.join(', ')}</span> in models: <span class="font-bold">` +
|
|
||||||
$params.models.join(', ') +
|
|
||||||
'</span>'
|
|
||||||
: '',
|
|
||||||
align: 'left'
|
|
||||||
},
|
|
||||||
|
|
||||||
yAxis: {
|
|
||||||
title: {
|
|
||||||
text: unit
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
xAxis: {
|
|
||||||
type: 'datetime',
|
|
||||||
plotLines: [
|
|
||||||
{
|
|
||||||
value: Date.now() + data.utc_offset_seconds * 1000,
|
|
||||||
color: 'red',
|
|
||||||
width: 2
|
|
||||||
}
|
|
||||||
],
|
|
||||||
plotBands: plotBands
|
|
||||||
},
|
|
||||||
|
|
||||||
plotOptions: {
|
|
||||||
spline: {
|
|
||||||
lineWidth: 2,
|
|
||||||
states: {
|
|
||||||
hover: {
|
|
||||||
lineWidth: 3
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
marker: {
|
|
||||||
enabled: false
|
unit = data.hourly_units[model];
|
||||||
|
|
||||||
|
if (!averageOnly) {
|
||||||
|
series.push({
|
||||||
|
name: model,
|
||||||
|
data: values,
|
||||||
|
type:
|
||||||
|
unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²'
|
||||||
|
? 'column'
|
||||||
|
: 'spline',
|
||||||
|
tooltip: {
|
||||||
|
valueSuffix: ' ' + unit
|
||||||
|
},
|
||||||
|
pointStart: hourly_starttime,
|
||||||
|
pointInterval: pointInterval
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let [index, val] of average.entries()) {
|
||||||
|
average[index] = Math.round((val / averageCount[index]) * 10) / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
series.push({
|
||||||
|
name: variable + '_average',
|
||||||
|
data: average,
|
||||||
|
dashStyle: 'ShortDashDot',
|
||||||
|
color: '#5e5e5e',
|
||||||
|
type:
|
||||||
|
unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²'
|
||||||
|
? 'column'
|
||||||
|
: 'spline',
|
||||||
|
tooltip: {
|
||||||
|
valueSuffix: ' ' + unit
|
||||||
|
},
|
||||||
|
lineWidth: 4,
|
||||||
|
states: {
|
||||||
|
hover: {
|
||||||
|
lineWidth: 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
column: {
|
pointStart: hourly_starttime,
|
||||||
pointWidth: 5
|
pointInterval: pointInterval,
|
||||||
}
|
className: 'highcharts-average-series'
|
||||||
},
|
});
|
||||||
|
|
||||||
legend: {
|
const chart = new Highcharts.Chart(chartDiv, {
|
||||||
enabled: showLegend,
|
credits: {
|
||||||
layout: 'horizontal',
|
text: count === ($params.hourly?.length || 0) - 1 ? 'Open-Meteo.com' : '',
|
||||||
align: 'center',
|
href: 'http://open-meteo.com'
|
||||||
verticalAlign: 'bottom'
|
},
|
||||||
},
|
|
||||||
|
|
||||||
series: series,
|
chart: {
|
||||||
|
height: showLegend ? '400px' : '300px',
|
||||||
|
styledMode: true,
|
||||||
|
marginLeft: '50',
|
||||||
|
marginRight: 0
|
||||||
|
},
|
||||||
|
|
||||||
responsive: {
|
lang: {
|
||||||
rules: [
|
locale: 'en-GB'
|
||||||
{
|
},
|
||||||
condition: {
|
|
||||||
maxWidth: 800
|
title: {
|
||||||
}
|
text: count === 0 ? 'Model Compare' : '',
|
||||||
|
align: 'left'
|
||||||
|
},
|
||||||
|
|
||||||
|
subtitle: {
|
||||||
|
text:
|
||||||
|
count === 0
|
||||||
|
? `Compare <span class="font-bold">${$params.hourly?.join(', ')}</span> in models: <span class="font-bold">` +
|
||||||
|
$params.models?.join(', ') +
|
||||||
|
'</span>'
|
||||||
|
: '',
|
||||||
|
align: 'left'
|
||||||
|
},
|
||||||
|
|
||||||
|
yAxis: {
|
||||||
|
title: {
|
||||||
|
text: unit
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
},
|
|
||||||
|
|
||||||
tooltip: {
|
xAxis: {
|
||||||
shared: true,
|
type: 'datetime',
|
||||||
animation: false
|
plotLines: [
|
||||||
}
|
{
|
||||||
});
|
value: Date.now() + data.utc_offset_seconds * 1000,
|
||||||
|
color: 'red',
|
||||||
|
width: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plotBands: plotBands
|
||||||
|
},
|
||||||
|
|
||||||
count++;
|
plotOptions: {
|
||||||
node.appendChild(chartDiv);
|
spline: {
|
||||||
}
|
lineWidth: 2,
|
||||||
|
states: {
|
||||||
|
hover: {
|
||||||
|
lineWidth: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
marker: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
pointWidth: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
legend: {
|
||||||
|
enabled: showLegend,
|
||||||
|
layout: 'horizontal',
|
||||||
|
align: 'center',
|
||||||
|
verticalAlign: 'bottom'
|
||||||
|
},
|
||||||
|
|
||||||
|
series: series,
|
||||||
|
|
||||||
|
responsive: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
condition: {
|
||||||
|
maxWidth: 800
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
tooltip: {
|
||||||
|
shared: true,
|
||||||
|
animation: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
count++;
|
||||||
|
charts.push(chart);
|
||||||
|
// eslint-disable-next-line svelte/no-dom-manipulating
|
||||||
|
node.appendChild(chartDiv);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (chart) {
|
charts.forEach((c) => c.destroy());
|
||||||
chart.destroy();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- min-h-[302px] min-h-[602px] min-h-[902px] min-h-[1202px] -->
|
<!-- min-h-[302px] min-h-[602px] min-h-[902px] min-h-[1202px] -->
|
||||||
<div class="container-wrapper relative -mx-6 md:mx-0 min-h-[{300 * $params.hourly.length + 2}px]">
|
<div
|
||||||
|
class="container-wrapper relative -mx-6 md:mx-0 min-h-[{300 * ($params.hourly?.length || 0) +
|
||||||
|
2}px]"
|
||||||
|
>
|
||||||
<div in:fade={{ duration: 300 }} out:fade={{ duration: 300 }} bind:this={node}></div>
|
<div in:fade={{ duration: 300 }} out:fade={{ duration: 300 }} bind:this={node}></div>
|
||||||
<div
|
<div
|
||||||
class="{count > 0
|
class="{count > 0
|
||||||
@@ -319,12 +334,12 @@
|
|||||||
<div class="mt-4 md:mt-8">
|
<div class="mt-4 md:mt-8">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<a href="#models"><h2 id="models" class="text-2xl md:text-3xl">Models</h2></a>
|
<a href="#models"><h2 id="models" class="text-2xl md:text-3xl">Models</h2></a>
|
||||||
{#if $params.models.length > 0}
|
{#if $params.models && $params.models.length > 0}
|
||||||
<div transition:fade={{ duration: 200 }} class="relative mt-[5px]">
|
<div transition:fade={{ duration: 200 }} class="relative mt-[5px]">
|
||||||
<div
|
<div
|
||||||
class="absolute -top-1 ml-2 rounded-full border-2 border-foreground/25 bg-secondary px-3 py-1 text-sm no-underline"
|
class="absolute -top-1 ml-2 rounded-full border-2 border-foreground/25 bg-secondary px-3 py-1 text-sm no-underline"
|
||||||
>
|
>
|
||||||
{$params.models.length} / {models.flat().length}
|
{$params.models?.length} / {models.flat().length}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -343,7 +358,7 @@
|
|||||||
aria-labelledby="{value}_label"
|
aria-labelledby="{value}_label"
|
||||||
onCheckedChange={() => {
|
onCheckedChange={() => {
|
||||||
if ($params.models?.includes(value)) {
|
if ($params.models?.includes(value)) {
|
||||||
$params.models = $params.models.filter((item) => {
|
$params.models = $params.models.filter((item: string) => {
|
||||||
return item !== value;
|
return item !== value;
|
||||||
});
|
});
|
||||||
} else if ($params.models) {
|
} else if ($params.models) {
|
||||||
@@ -371,12 +386,12 @@
|
|||||||
Hourly Weather Variables
|
Hourly Weather Variables
|
||||||
</h2></a
|
</h2></a
|
||||||
>
|
>
|
||||||
{#if $params.hourly.length > 0}
|
{#if $params.hourly && $params.hourly.length > 0}
|
||||||
<div transition:fade={{ duration: 200 }} class="relative mt-[5px]">
|
<div transition:fade={{ duration: 200 }} class="relative mt-[5px]">
|
||||||
<div
|
<div
|
||||||
class="absolute -top-1 ml-2 rounded-full border-2 border-foreground/25 bg-secondary px-3 py-1 text-sm no-underline"
|
class="absolute -top-1 ml-2 rounded-full border-2 border-foreground/25 bg-secondary px-3 py-1 text-sm no-underline"
|
||||||
>
|
>
|
||||||
{$params.hourly.length} / {hourly.flat().length}
|
{$params.hourly?.length} / {hourly.flat().length}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -397,7 +412,7 @@
|
|||||||
aria-labelledby="{value}_label"
|
aria-labelledby="{value}_label"
|
||||||
onCheckedChange={() => {
|
onCheckedChange={() => {
|
||||||
if ($params.hourly?.includes(value)) {
|
if ($params.hourly?.includes(value)) {
|
||||||
$params.hourly = $params.hourly.filter((item) => {
|
$params.hourly = $params.hourly.filter((item: string) => {
|
||||||
return item !== value;
|
return item !== value;
|
||||||
});
|
});
|
||||||
} else if ($params.hourly) {
|
} else if ($params.hourly) {
|
||||||
|
|||||||
@@ -6,18 +6,20 @@ export const defaultParameters = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const models = [
|
export const models = [
|
||||||
{ value: 'best_match', label: 'Best match' },
|
[{ value: 'best_match', label: 'Best match' }],
|
||||||
{ value: 'gfs_seamless', label: 'NCEP GFS Seamless' },
|
[
|
||||||
{ value: 'jma_seamless', label: 'JMA Seamless' },
|
{ value: 'gfs_seamless', label: 'NCEP GFS Seamless' },
|
||||||
{ value: 'kma_seamless', label: 'KMA Seamless' },
|
{ value: 'jma_seamless', label: 'JMA Seamless' },
|
||||||
{ value: 'icon_seamless', label: 'DWD ICON Seamless' },
|
{ value: 'kma_seamless', label: 'KMA Seamless' },
|
||||||
{ value: 'gem_seamless', label: 'GEM Seamless' },
|
{ value: 'icon_seamless', label: 'DWD ICON Seamless' },
|
||||||
{ value: 'meteofrance_seamless', label: 'Météo-France Seamless' },
|
{ value: 'gem_seamless', label: 'GEM Seamless' },
|
||||||
{ value: 'arpae_cosmo_seamless', label: 'ARPAE Seamless' },
|
{ value: 'meteofrance_seamless', label: 'Météo-France Seamless' },
|
||||||
{ value: 'metno_seamless', label: 'MET Norway Seamless (with ECMWF)' },
|
{ value: 'arpae_cosmo_seamless', label: 'ARPAE Seamless' },
|
||||||
{ value: 'knmi_seamless', label: 'KNMI Seamless (with ECMWF)' },
|
{ value: 'metno_seamless', label: 'MET Norway Seamless (with ECMWF)' },
|
||||||
{ value: 'dmi_seamless', label: 'DMI Seamless (with ECMWF)' },
|
{ value: 'knmi_seamless', label: 'KNMI Seamless (with ECMWF)' },
|
||||||
{ value: 'ukmo_seamless', label: 'UK Met Office Seamless' }
|
{ value: 'dmi_seamless', label: 'DMI Seamless (with ECMWF)' },
|
||||||
|
{ value: 'ukmo_seamless', label: 'UK Met Office Seamless' }
|
||||||
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
export const hourly = [
|
export const hourly = [
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { get } from 'svelte/store';
|
|||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import { storedLocation } from '$lib/stores/settings';
|
import { storedLocation } from '$lib/stores/settings';
|
||||||
import { geoLocationNameToRoute } from '$lib/utils/meteo';
|
import { geoLocationNameToRoute } from '$lib/utils/meteo';
|
||||||
import type { PageLoad } from '$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
|
|
||||||
export const load = (async () => {
|
export const load: PageLoad = async () => {
|
||||||
const location = get(storedLocation);
|
const location = get(storedLocation);
|
||||||
const locationRoute = geoLocationNameToRoute(location.name);
|
const locationRoute = geoLocationNameToRoute(location.name || '');
|
||||||
throw redirect(
|
throw redirect(
|
||||||
303,
|
303,
|
||||||
'/weather/week/' +
|
'/weather/week/' +
|
||||||
@@ -18,4 +18,4 @@ export const load = (async () => {
|
|||||||
: locationRoute + '_' + location.id
|
: locationRoute + '_' + location.id
|
||||||
: locationRoute + '_' + location.id)
|
: locationRoute + '_' + location.id)
|
||||||
);
|
);
|
||||||
}) satisfies PageLoad;
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
import { getColor, textWhite } from '../../utils/colors';
|
import { getColor, textWhite } from '../../utils/colors';
|
||||||
import weatherCodes from '../../utils/weather-codes';
|
import weatherCodes from '../../utils/weather-codes';
|
||||||
import { SvelteDate } from 'svelte/reactivity';
|
import { SvelteDate } from 'svelte/reactivity';
|
||||||
import { pad } from '$lib/utils';
|
import { pad } from '$lib/utils/index.js';
|
||||||
|
|
||||||
// --- Interfaces ---
|
// --- Interfaces ---
|
||||||
|
|
||||||
@@ -63,9 +63,9 @@
|
|||||||
// --- State Setup ---
|
// --- State Setup ---
|
||||||
|
|
||||||
const params = urlHashStore({
|
const params = urlHashStore({
|
||||||
latitude: [$storedLocation.latitude],
|
latitude: [$storedLocation.latitude || 0],
|
||||||
longitude: [$storedLocation.longitude],
|
longitude: [$storedLocation.longitude || 0],
|
||||||
models: 'best_match',
|
models: ['best_match'],
|
||||||
...defaultParameters
|
...defaultParameters
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,7 +89,9 @@
|
|||||||
const entries = 6;
|
const entries = 6;
|
||||||
const winddir = true;
|
const winddir = true;
|
||||||
|
|
||||||
let modelSelected = $derived(models.find((mo) => String(mo.value) === $params.models));
|
let modelSelected = $derived(
|
||||||
|
models.flat().find((mo) => $params.models?.includes(String(mo.value)))
|
||||||
|
);
|
||||||
|
|
||||||
// --- Logic ---
|
// --- Logic ---
|
||||||
|
|
||||||
@@ -654,13 +656,20 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="mt-6 flex gap-6 md:mt-12">
|
<div class="mt-6 flex gap-6 md:mt-12">
|
||||||
<div class="relative w-1/2">
|
<div class="relative w-1/2">
|
||||||
<Select.Root name="model_selection" type="single" bind:value={$params.models}>
|
<Select.Root
|
||||||
|
name="model_selection"
|
||||||
|
type="single"
|
||||||
|
value={$params.models?.[0]}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
$params.models = [v];
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Select.Trigger
|
<Select.Trigger
|
||||||
aria-label="Forecast days input"
|
aria-label="Forecast days input"
|
||||||
class="h-12 cursor-pointer pt-6 [&_svg]:mb-3">{modelSelected?.label}</Select.Trigger
|
class="h-12 cursor-pointer pt-6 [&_svg]:mb-3">{modelSelected?.label}</Select.Trigger
|
||||||
>
|
>
|
||||||
<Select.Content preventScroll={false} class="border-border">
|
<Select.Content preventScroll={false} class="border-border">
|
||||||
{#each models as mo (mo.value)}
|
{#each models.flat() as mo (mo.value)}
|
||||||
<Select.Item class="cursor-pointer" value={mo.value}>{mo.label}</Select.Item>
|
<Select.Item class="cursor-pointer" value={mo.value}>{mo.label}</Select.Item>
|
||||||
{/each}
|
{/each}
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { type GeoLocation, storedLocation } from '$lib/stores/settings';
|
|||||||
|
|
||||||
import { geoLocationNameToRoute } from '$lib/utils/meteo';
|
import { geoLocationNameToRoute } from '$lib/utils/meteo';
|
||||||
|
|
||||||
import type { PageLoad } from '$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
|
|
||||||
export const load = (async (event) => {
|
export const load: PageLoad = async (event) => {
|
||||||
const urlLocation = event.params.location;
|
const urlLocation = event.params.location;
|
||||||
let urlLocationSplit, urlLocationName, urlLocationId;
|
let urlLocationSplit, urlLocationName, urlLocationId;
|
||||||
|
|
||||||
@@ -31,8 +31,8 @@ export const load = (async (event) => {
|
|||||||
// lat, long coordinates
|
// lat, long coordinates
|
||||||
if (urlLocation.includes('N') && urlLocation.includes('E')) {
|
if (urlLocation.includes('N') && urlLocation.includes('E')) {
|
||||||
urlLocationSplit = urlLocation.split(/N|E/);
|
urlLocationSplit = urlLocation.split(/N|E/);
|
||||||
const latitude = urlLocationSplit[0];
|
const latitude = Number(urlLocationSplit[0]);
|
||||||
const longitude = urlLocationSplit[1];
|
const longitude = Number(urlLocationSplit[1]);
|
||||||
|
|
||||||
location = {
|
location = {
|
||||||
//id: undefined,
|
//id: undefined,
|
||||||
@@ -58,7 +58,7 @@ export const load = (async (event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const locationRoute = geoLocationNameToRoute(location.name);
|
const locationRoute = geoLocationNameToRoute(location.name || '');
|
||||||
|
|
||||||
if (location.population && location.population > 543000) {
|
if (location.population && location.population > 543000) {
|
||||||
// 1000 biggest cities
|
// 1000 biggest cities
|
||||||
@@ -74,4 +74,4 @@ export const load = (async (event) => {
|
|||||||
|
|
||||||
storedLocation.set(location);
|
storedLocation.set(location);
|
||||||
return { location: location };
|
return { location: location };
|
||||||
}) satisfies PageLoad;
|
};
|
||||||
|
|||||||
+5
-1
@@ -7,7 +7,11 @@ const config = {
|
|||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
kit: { adapter: adapter() }
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
fallback: '404.html'
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
Reference in New Issue
Block a user