From 0a59a1c845d6eeeb79ecc5b4a8087b8bb3af2b58 Mon Sep 17 00:00:00 2001 From: terraputix Date: Sun, 15 Feb 2026 16:35:54 +0100 Subject: [PATCH] migrate to apache echarts --- README.md | 2 +- package-lock.json | 39 +- package.json | 2 +- src/routes/+layout.server.ts | 1 + src/routes/+page.svelte | 2 +- src/routes/weather/+page.ts | 2 - src/routes/weather/14-day/+page.svelte | 445 ++++--- src/routes/weather/compare/+page.svelte | 423 ++++--- src/routes/weather/compare/echarts.css | 65 + src/routes/weather/compare/highcharts.css | 1172 ------------------- src/routes/weather/week/+page.ts | 2 - src/routes/weather/week/[location]/+page.ts | 2 - 12 files changed, 652 insertions(+), 1505 deletions(-) create mode 100644 src/routes/+layout.server.ts create mode 100644 src/routes/weather/compare/echarts.css delete mode 100644 src/routes/weather/compare/highcharts.css diff --git a/README.md b/README.md index 806e050..c8993b0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Our objective is to provide a comprehensive, user-friendly weather platform for - **Framework**: [SvelteKit](https://kit.svelte.dev/) - **Language**: [TypeScript](https://www.typescriptlang.org/) - **Data Source**: [Open-Meteo API](https://open-meteo.com/) -- **Visualization**: [Highcharts](https://www.highcharts.com/) (Current, transitioning to a more flexible charting library in the future) +- **Visualization**: [Apache Echarts](https://echarts.apache.org) ## Developing diff --git a/package-lock.json b/package-lock.json index 0f088c0..f48e1cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "open-meteo-weather", "version": "0.0.1", "dependencies": { - "highcharts": "^12.4.0", + "echarts": "^6.0.0", "mode-watcher": "^1.1.0", "openmeteo": "^1.2.3" }, @@ -2594,6 +2594,22 @@ "integrity": "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==", "license": "MIT" }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/enhanced-resolve": { "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -3063,12 +3079,6 @@ "node": ">=8" } }, - "node_modules/highcharts": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-12.4.0.tgz", - "integrity": "sha512-o6UxxfChSUrvrZUbWrAuqL1HO/+exhAUPcZY6nnqLsadZQlnP16d082sg7DnXKZCk1gtfkyfkp6g3qkIZ9miZg==", - "license": "https://www.highcharts.com/license" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4989,6 +4999,21 @@ "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", "license": "MIT" + }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" } } } diff --git a/package.json b/package.json index ee3d27d..f6dff6e 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "vitest-browser-svelte": "^2.0.1" }, "dependencies": { - "highcharts": "^12.4.0", + "echarts": "^6.0.0", "mode-watcher": "^1.1.0", "openmeteo": "^1.2.3" } diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..189f71e --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1 @@ +export const prerender = true; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 57e6aad..af09426 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -209,7 +209,7 @@
- {#each [{ title: 'Multiple Models', desc: 'Compare ECMWF, GFS, and more', icon: 'layers' }, { title: 'Extended Forecasts', desc: 'Up to 14 days ahead', icon: 'clock' }, { title: 'Interactive Charts', desc: 'Highcharts visualization', icon: 'bar-chart' }, { title: 'Real-time Data', desc: 'Always up-to-date', icon: 'refresh' }] as feature, index (feature.title)} + {#each [{ title: 'Multiple Models', desc: 'Compare ECMWF, GFS, and more', icon: 'layers' }, { title: 'Extended Forecasts', desc: 'Up to 14 days ahead', icon: 'clock' }, { title: 'Interactive Charts', desc: 'Apache ECharts visualization', icon: 'bar-chart' }, { title: 'Real-time Data', desc: 'Always up-to-date', icon: 'refresh' }] as feature, index (feature.title)}
{ throw redirect(303, '/weather/week/'); }) satisfies PageLoad; diff --git a/src/routes/weather/14-day/+page.svelte b/src/routes/weather/14-day/+page.svelte index 85227ab..a245b7e 100644 --- a/src/routes/weather/14-day/+page.svelte +++ b/src/routes/weather/14-day/+page.svelte @@ -3,19 +3,20 @@ import { get } from 'svelte/store'; import { fade } from 'svelte/transition'; - import { dev } from '$app/environment'; + import * as echarts from 'echarts'; import { storedLocation } from '$lib/stores/settings'; import { Label } from '$lib/components/ui/label'; import { Switch } from '$lib/components/ui/switch'; - import '../compare/highcharts.css'; + import '../compare/echarts.css'; import { defaultParameters } from './options'; let node: HTMLElement; - let chart: any; - let Highcharts = $state(null); + let charts: echarts.ECharts[] = []; + let resizeObservers: ResizeObserver[] = []; + let mounted = $state(false); let showLegend = $state(false); let averageOnly = $state(false); @@ -32,32 +33,48 @@ }); let count = $state(0); - onMount(async () => { - /// Highcharts needs to be loaded in `onMount` to work with prerendered SSG - Highcharts = (await import('highcharts')).default; - const more = (await import('highcharts/highcharts-more')).default; - // more(Highcharts); - if (dev) { - // const HighchartsDebugger = await import('highcharts/modules/debugger'); - // HighchartsDebugger.default(Highcharts); - const Debugger = ( - await import('highcharts/es-modules/Extensions/Debugger/Debugger.js' as any) - ).default; - const ErrorMessages = ( - await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js' as any) - ).default; - if (Highcharts) { - (Highcharts as any).errorMessages = ErrorMessages; - Debugger.compose(Highcharts.Chart); - } - } + function isDarkMode(): boolean { + if (typeof document === 'undefined') return false; + return ( + document.documentElement.classList.contains('dark') || + document.documentElement.getAttribute('data-theme') === 'dark' || + (window.matchMedia && + window.matchMedia('(prefers-color-scheme: dark)').matches && + document.documentElement.getAttribute('data-theme') !== 'light') + ); + } + + function getTextColor(): string { + return isDarkMode() ? '#e5e7eb' : '#374151'; + } + + function getAxisLineColor(): string { + return isDarkMode() ? 'rgba(229, 231, 235, 0.3)' : 'rgba(55, 65, 81, 0.3)'; + } + + function getSplitLineColor(): string { + return isDarkMode() ? 'rgba(229, 231, 235, 0.1)' : 'rgba(55, 65, 81, 0.1)'; + } + + onMount(() => { + mounted = true; }); $effect(() => { const loadData = async () => { count = 0; - if (Highcharts) { + if (mounted) { + // Dispose existing charts and observers + resizeObservers.forEach((ro) => ro.disconnect()); + resizeObservers = []; + charts.forEach((chart) => { + if (chart) { + chart.dispose(); + } + }); + charts = []; + // eslint-disable-next-line svelte/no-dom-manipulating node.replaceChildren(); const dataDaily = await fetch( @@ -70,41 +87,60 @@ ); const data = await dataReq.json(); - let plotBands: any = []; + // Create day/night plot bands as markArea data + let markAreas: Array<[{ xAxis: number; itemStyle: { color: string } }, { xAxis: number }]> = + []; if ('daily' in wd && 'sunrise' in wd.daily && 'sunset' in wd.daily) { let rise = wd.daily.sunrise; let set = wd.daily.sunset; - plotBands = rise.map(function (r: any, i: number) { - return { - color: 'rgba(255, 255, 194, 0.5)', - from: (r + data.utc_offset_seconds) * 1000, - to: (set[i] + data.utc_offset_seconds) * 1000 - }; + markAreas = rise.map(function (r: number, i: number) { + return [ + { + xAxis: (r + data.utc_offset_seconds) * 1000, + itemStyle: { + color: 'rgba(255, 255, 194, 0.3)' + } + }, + { + xAxis: (set[i] + data.utc_offset_seconds) * 1000 + } + ]; }); } let minValues = new Array(data.hourly.time.length).fill(undefined); let maxValues = new Array(data.hourly.time.length).fill(undefined); + const textColor = getTextColor(); + const axisLineColor = getAxisLineColor(); + const splitLineColor = getSplitLineColor(); + for (let variable of params.hourly || []) { const chartDiv = document.createElement('div'); + chartDiv.style.width = '100%'; + chartDiv.style.height = showLegend ? '400px' : '300px'; - let unit; + // Append to DOM BEFORE echarts.init so it can measure dimensions + // eslint-disable-next-line svelte/no-dom-manipulating + node.appendChild(chartDiv); - let hourly_starttime = (data.hourly.time[0] + data.utc_offset_seconds) * 1000; - let pointInterval = (data.hourly.time[1] - data.hourly.time[0]) * 1000; + let unit: string = ''; - const series = []; + const series: Array> = []; let average = new Array(data.hourly.time.length).fill(0); let averageCount = new Array(data.hourly.time.length).fill(0); + const timestamps = data.hourly.time.map( + (t: number) => (t + data.utc_offset_seconds) * 1000 + ); + for (let [model, values] of Object.entries(data.hourly)) { if (model === 'time') { continue; } if (model.startsWith(variable)) { - for (let [index, val] of (values as any[]).entries()) { - if (val) { + for (let [index, val] of (values as number[]).entries()) { + if (val !== null && val !== undefined) { let avVal = average[index]; average[index] = avVal + val; averageCount[index]++; @@ -122,145 +158,231 @@ } } + // Calculate average for (let [index, val] of average.entries()) { average[index] = Math.round((val / averageCount[index]) * 10) / 10; } - const minMax = []; - for (let [index, min] of minValues.entries()) { - minMax.push([min, maxValues[index]]); - } + // Create min-max area data + const spreadData: Array<[number, number, number]> = minValues.map( + (min: number, index: number) => [timestamps[index], min, maxValues[index]] + ); + const isColumn = unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²'; + + // Add spread series (lower bound) series.push({ - name: 'temperature_2m_spread', - data: minMax, - type: 'arearange', - tooltip: { - valueSuffix: ' ' + unit + name: variable + '_spread', + type: 'line', + data: spreadData.map((d) => [d[0], d[1]]), + areaStyle: { + color: 'rgba(173, 216, 230, 0.3)', + origin: 'auto' }, - pointStart: hourly_starttime, - pointInterval: pointInterval, - className: 'highcharts-spread-series' + lineStyle: { + width: 0 + }, + showSymbol: false, + stack: 'spread', + smooth: true, + z: 1 }); + // Add spread series (upper bound delta) + series.push({ + name: variable + '_spread_max', + type: 'line', + data: spreadData.map((d) => [d[0], d[2] - d[1]]), + areaStyle: { + color: 'rgba(173, 216, 230, 0.3)', + origin: 'auto' + }, + lineStyle: { + width: 0 + }, + showSymbol: false, + stack: 'spread', + smooth: true, + z: 1 + }); + + // Add average line + const averageData = average.map((val: number, idx: number) => [timestamps[idx], val]); + 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 + type: isColumn ? 'bar' : 'line', + data: averageData, + smooth: !isColumn, + showSymbol: false, + lineStyle: { + type: 'dashed', + width: 4, + color: '#5e5e5e' }, - lineWidth: 4, - states: { - hover: { - lineWidth: 6 + itemStyle: { + color: '#5e5e5e' + }, + emphasis: { + lineStyle: { + width: 6 } }, - pointStart: hourly_starttime, - pointInterval: pointInterval, - className: 'highcharts-average-series' + barMaxWidth: 5, + z: 10 }); - new Highcharts!.Chart({ - chart: { - renderTo: chartDiv, - height: showLegend ? '400px' : '300px', - styledMode: true, - marginLeft: 50, - marginRight: 0 - }, - - credits: { - text: count === (params.hourly?.length || 0) - 1 ? 'Open-Meteo.com' : '', - href: 'http://open-meteo.com' - }, - - lang: { - locale: 'en-GB' - }, - - title: { - text: count === 0 ? 'Model Spread' : '', - align: 'left' - }, - - subtitle: { - text: - count === 0 - ? `Compare ${params.hourly?.join(', ') || ''} in models: ` + - (params.models?.join(', ') || '') + - '' - : '', - align: 'left' - }, - - yAxis: { - title: { - text: unit - } - }, - - xAxis: { - type: 'datetime', - plotLines: [ + // Add current time markLine via a helper series + series.push({ + name: 'Current Time', + type: 'line', + data: [], + markLine: { + silent: true, + symbol: 'none', + data: [ { - 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: { - pointWidth: 5 - } - }, - - legend: { - enabled: showLegend, - layout: 'horizontal', - align: 'center', - verticalAlign: 'bottom' - }, - - series: series as any, - - responsive: { - rules: [ - { - condition: { - maxWidth: 800 + xAxis: Date.now() + data.utc_offset_seconds * 1000, + lineStyle: { + color: 'red', + width: 2 + }, + label: { + show: false } } ] - }, - - tooltip: { - shared: true, - animation: false } }); + // Add day/night bands via markArea + if (markAreas.length > 0) { + series.push({ + name: 'Daylight', + type: 'line', + data: [], + markArea: { + silent: true, + data: markAreas + } + }); + } + + const option: Record = { + title: { + text: count === 0 ? 'Model Spread' : '', + left: 'left', + textStyle: { + fontWeight: 'normal', + color: textColor + }, + ...(count === 0 + ? { + subtext: `Compare ${params.hourly?.join(', ') || ''} in models: ${params.models?.join(', ') || ''}`, + subtextStyle: { + fontWeight: 'normal', + color: textColor + } + } + : {}) + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + animation: false + }, + valueFormatter: (value: number) => { + if (value === null || value === undefined) return '-'; + return value.toFixed(1) + ' ' + unit; + } + }, + legend: { + show: showLegend, + bottom: 0, + type: 'scroll', + data: [variable + '_average'], + textStyle: { + color: textColor + } + }, + grid: { + left: 60, + right: 10, + top: count === 0 ? 80 : 40, + bottom: showLegend ? 60 : 40 + }, + xAxis: { + type: 'time', + splitLine: { + show: false + }, + axisLine: { + lineStyle: { + color: axisLineColor + } + }, + axisLabel: { + color: textColor + } + }, + yAxis: { + type: 'value', + name: unit, + nameTextStyle: { + color: textColor + }, + axisLine: { + show: false + }, + axisLabel: { + color: textColor + }, + splitLine: { + lineStyle: { + color: splitLineColor + } + } + }, + series: series, + textStyle: { + color: textColor + } + }; + + // Add credits for last chart + if (count === (params.hourly?.length || 0) - 1) { + option.graphic = [ + { + type: 'text', + right: 10, + bottom: 5, + style: { + text: 'Open-Meteo.com', + fontSize: 10, + fill: textColor, + opacity: 0.5 + }, + onclick: function () { + window.open('https://open-meteo.com', '_blank'); + }, + cursor: 'pointer' + } + ]; + } + + const chart = echarts.init(chartDiv, null, { renderer: 'canvas' }); + charts.push(chart); + chart.setOption(option); + + // Handle responsive resize + const resizeObserver = new ResizeObserver(() => { + chart.resize(); + }); + resizeObserver.observe(chartDiv); + resizeObservers.push(resizeObserver); + count++; - node.appendChild(chartDiv); } } }; @@ -268,22 +390,25 @@ }); onDestroy(() => { - if (chart) { - chart.destroy(); - } + resizeObservers.forEach((ro) => ro.disconnect()); + resizeObservers = []; + charts.forEach((chart) => { + chart.dispose(); + }); + charts = []; });
- +
- +
diff --git a/src/routes/weather/compare/+page.svelte b/src/routes/weather/compare/+page.svelte index 25406ca..a8ea7cd 100644 --- a/src/routes/weather/compare/+page.svelte +++ b/src/routes/weather/compare/+page.svelte @@ -3,7 +3,7 @@ import { get } from 'svelte/store'; import { fade } from 'svelte/transition'; - import { dev } from '$app/environment'; + import * as echarts from 'echarts'; import { storedLocation } from '$lib/stores/settings'; @@ -12,15 +12,16 @@ import { Switch } from '$lib/components/ui/switch'; import { hourly, models as modelsFlat } from '../options'; - import './highcharts.css'; + import './echarts.css'; import { defaultParameters } from './options'; // Wrap models in array to match template expectation of nested arrays like hourly const models = [modelsFlat]; let node: HTMLElement; - let chart: any; - let Highcharts = $state(null); + let charts: echarts.ECharts[] = []; + let resizeObservers: ResizeObserver[] = []; + let mounted = $state(false); let showLegend = $state(false); let averageOnly = $state(false); @@ -42,30 +43,48 @@ }); let count = $state(0); - onMount(async () => { - /// Highcharts needs to be loaded in `onMount` to work with prerendered SSG - Highcharts = (await import('highcharts')).default; - if (dev) { - // const HighchartsDebugger = await import('highcharts/modules/debugger'); - // HighchartsDebugger.default(Highcharts); - const Debugger = ( - await import('highcharts/es-modules/Extensions/Debugger/Debugger.js' as any) - ).default; - const ErrorMessages = ( - await import('highcharts/es-modules/Extensions/Debugger/ErrorMessages.js' as any) - ).default; - if (Highcharts) { - (Highcharts as any).errorMessages = ErrorMessages; - Debugger.compose(Highcharts.Chart); - } - } + function isDarkMode(): boolean { + if (typeof document === 'undefined') return false; + return ( + document.documentElement.classList.contains('dark') || + document.documentElement.getAttribute('data-theme') === 'dark' || + (window.matchMedia && + window.matchMedia('(prefers-color-scheme: dark)').matches && + document.documentElement.getAttribute('data-theme') !== 'light') + ); + } + + function getTextColor(): string { + return isDarkMode() ? '#e5e7eb' : '#374151'; + } + + function getAxisLineColor(): string { + return isDarkMode() ? 'rgba(229, 231, 235, 0.3)' : 'rgba(55, 65, 81, 0.3)'; + } + + function getSplitLineColor(): string { + return isDarkMode() ? 'rgba(229, 231, 235, 0.1)' : 'rgba(55, 65, 81, 0.1)'; + } + + onMount(() => { + mounted = true; }); $effect(() => { const loadData = async () => { count = 0; - if (Highcharts) { + if (mounted) { + // Dispose existing charts and observers + resizeObservers.forEach((ro) => ro.disconnect()); + resizeObservers = []; + charts.forEach((chart) => { + if (chart) { + chart.dispose(); + } + }); + charts = []; + // eslint-disable-next-line svelte/no-dom-manipulating node.replaceChildren(); const dataReq = await fetch( @@ -77,7 +96,9 @@ dailyFirstModelKey.shift(); dailyFirstModelKey = dailyFirstModelKey.join('_'); - let plotBands: any = []; + // Create day/night plot bands as markArea data + let markAreas: Array<[{ xAxis: number; itemStyle: { color: string } }, { xAxis: number }]> = + []; if ( 'daily' in data && 'sunrise_' + dailyFirstModelKey in data.daily && @@ -85,34 +106,51 @@ ) { let rise = data.daily['sunrise_' + dailyFirstModelKey]; let set = data.daily['sunset_' + dailyFirstModelKey]; - plotBands = rise.map(function (r: any, i: number) { - return { - color: 'rgba(255, 255, 194, 0.5)', - from: (r + data.utc_offset_seconds) * 1000, - to: (set[i] + data.utc_offset_seconds) * 1000 - }; + markAreas = rise.map(function (r: number, i: number) { + return [ + { + xAxis: (r + data.utc_offset_seconds) * 1000, + itemStyle: { + color: 'rgba(255, 255, 194, 0.3)' + } + }, + { + xAxis: (set[i] + data.utc_offset_seconds) * 1000 + } + ]; }); } + const textColor = getTextColor(); + const axisLineColor = getAxisLineColor(); + const splitLineColor = getSplitLineColor(); + for (let variable of params.hourly || []) { const chartDiv = document.createElement('div'); + chartDiv.style.width = '100%'; + chartDiv.style.height = showLegend ? '400px' : '300px'; - let unit; + // Append to DOM BEFORE echarts.init so it can measure dimensions + // eslint-disable-next-line svelte/no-dom-manipulating + node.appendChild(chartDiv); - let hourly_starttime = (data.hourly.time[0] + data.utc_offset_seconds) * 1000; - let pointInterval = (data.hourly.time[1] - data.hourly.time[0]) * 1000; + let unit: string = ''; - const series = []; + const series: Array> = []; let average = new Array(data.hourly.time.length).fill(0); let averageCount = new Array(data.hourly.time.length).fill(0); + const timestamps = data.hourly.time.map( + (t: number) => (t + data.utc_offset_seconds) * 1000 + ); + for (let [model, values] of Object.entries(data.hourly)) { if (model === 'time') { continue; } if (model.startsWith(variable)) { - for (let [index, val] of (values as any[]).entries()) { - if (val) { + for (let [index, val] of (values as number[]).entries()) { + if (val !== null && val !== undefined) { let avVal = average[index]; average[index] = avVal + val; averageCount[index]++; @@ -122,145 +160,213 @@ unit = data.hourly_units[model]; if (!averageOnly) { + const isColumn = unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²'; + + const seriesData = (values as (number | null)[]).map( + (val: number | null, idx: number) => [timestamps[idx], val] + ); + series.push({ name: model, - data: values, - type: - unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²' - ? 'column' - : 'spline', - tooltip: { - valueSuffix: ' ' + unit + type: isColumn ? 'bar' : 'line', + data: seriesData, + smooth: !isColumn, + showSymbol: false, + lineStyle: { + width: 2 }, - pointStart: hourly_starttime, - pointInterval: pointInterval + emphasis: { + lineStyle: { + width: 3 + } + }, + barMaxWidth: 5 }); } } } + // Calculate average for (let [index, val] of average.entries()) { average[index] = Math.round((val / averageCount[index]) * 10) / 10; } + const isColumn = unit == 'mm' || unit == 'cm' || unit == 'inch' || unit == 'MJ/m²'; + const averageData = average.map((val: number, idx: number) => [timestamps[idx], val]); + 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 + type: isColumn ? 'bar' : 'line', + data: averageData, + smooth: !isColumn, + showSymbol: false, + lineStyle: { + type: 'dashed', + width: 4, + color: '#5e5e5e' }, - lineWidth: 4, - states: { - hover: { - lineWidth: 6 + itemStyle: { + color: '#5e5e5e' + }, + emphasis: { + lineStyle: { + width: 6 } }, - pointStart: hourly_starttime, - pointInterval: pointInterval, - className: 'highcharts-average-series' + barMaxWidth: 5, + z: 10 }); - new Highcharts!.Chart({ - chart: { - renderTo: chartDiv, - height: showLegend ? '400px' : '300px', - styledMode: true, - marginLeft: 50, - marginRight: 0 - }, - - credits: { - text: count === (params.hourly?.length || 0) - 1 ? 'Open-Meteo.com' : '', - href: 'http://open-meteo.com' - }, - - lang: { - locale: 'en-GB' - }, - - title: { - text: count === 0 ? 'Model Compare' : '', - align: 'left' - }, - - subtitle: { - text: - count === 0 - ? `Compare ${params.hourly?.join(', ') || ''} in models: ` + - (params.models?.join(', ') || '') + - '' - : '', - align: 'left' - }, - - yAxis: { - title: { - text: unit - } - }, - - xAxis: { - type: 'datetime', - plotLines: [ + // Add current time markLine via a helper series + series.push({ + name: 'Current Time', + type: 'line', + data: [], + markLine: { + silent: true, + symbol: 'none', + data: [ { - 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: { - pointWidth: 5 - } - }, - - legend: { - enabled: showLegend, - layout: 'horizontal', - align: 'center', - verticalAlign: 'bottom' - }, - - series: series as any, - - responsive: { - rules: [ - { - condition: { - maxWidth: 800 + xAxis: Date.now() + data.utc_offset_seconds * 1000, + lineStyle: { + color: 'red', + width: 2 + }, + label: { + show: false } } ] - }, - - tooltip: { - shared: true, - animation: false } }); + // Add day/night bands via markArea + if (markAreas.length > 0) { + series.push({ + name: 'Daylight', + type: 'line', + data: [], + markArea: { + silent: true, + data: markAreas + } + }); + } + + const option: Record = { + title: { + text: count === 0 ? 'Model Compare' : '', + left: 'left', + textStyle: { + fontWeight: 'normal', + color: textColor + }, + ...(count === 0 + ? { + subtext: `Compare ${params.hourly?.join(', ') || ''} in models: ${params.models?.join(', ') || ''}`, + subtextStyle: { + fontWeight: 'normal', + color: textColor + } + } + : {}) + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + animation: false + }, + valueFormatter: (value: number) => { + if (value === null || value === undefined) return '-'; + return value.toFixed(1) + ' ' + unit; + } + }, + legend: { + show: showLegend, + bottom: 0, + type: 'scroll', + textStyle: { + color: textColor + } + }, + grid: { + left: 60, + right: 10, + top: count === 0 ? 80 : 40, + bottom: showLegend ? 60 : 40 + }, + xAxis: { + type: 'time', + splitLine: { + show: false + }, + axisLine: { + lineStyle: { + color: axisLineColor + } + }, + axisLabel: { + color: textColor + } + }, + yAxis: { + type: 'value', + name: unit, + nameTextStyle: { + color: textColor + }, + axisLine: { + show: false + }, + axisLabel: { + color: textColor + }, + splitLine: { + lineStyle: { + color: splitLineColor + } + } + }, + series: series, + textStyle: { + color: textColor + } + }; + + // Add credits for last chart + if (count === (params.hourly?.length || 0) - 1) { + option.graphic = [ + { + type: 'text', + right: 10, + bottom: 5, + style: { + text: 'Open-Meteo.com', + fontSize: 10, + fill: textColor, + opacity: 0.5 + }, + onclick: function () { + window.open('https://open-meteo.com', '_blank'); + }, + cursor: 'pointer' + } + ]; + } + + const chart = echarts.init(chartDiv, null, { renderer: 'canvas' }); + charts.push(chart); + chart.setOption(option); + + // Handle responsive resize + const resizeObserver = new ResizeObserver(() => { + chart.resize(); + }); + resizeObserver.observe(chartDiv); + resizeObservers.push(resizeObserver); + count++; - node.appendChild(chartDiv); } } }; @@ -268,22 +374,25 @@ }); onDestroy(() => { - if (chart) { - chart.destroy(); - } + resizeObservers.forEach((ro) => ro.disconnect()); + resizeObservers = []; + charts.forEach((chart) => { + chart.dispose(); + }); + charts = []; });
- +
- +
@@ -334,7 +443,7 @@

Models

{#if params.models && params.models.length > 0} -
+
@@ -387,7 +496,7 @@ {#if params.hourly && params.hourly.length > 0} -
+
diff --git a/src/routes/weather/compare/echarts.css b/src/routes/weather/compare/echarts.css new file mode 100644 index 0000000..cc685c0 --- /dev/null +++ b/src/routes/weather/compare/echarts.css @@ -0,0 +1,65 @@ +/* ECharts theme integration for Open-Meteo Weather */ + +/* Container styling */ +.echarts-container { + width: 100%; + height: 100%; + min-height: 300px; +} + +/* Ensure ECharts respects current color scheme */ +[data-theme='light'] .echarts-container, +:root:not([data-theme='dark']) .echarts-container { + color: hsl(var(--foreground)); +} + +/* Tooltip styling to match application theme */ +.echarts-tooltip { + background: hsl(var(--popover)) !important; + border: 1px solid hsl(var(--border)) !important; + border-radius: var(--radius) !important; + box-shadow: + 0 4px 6px -1px rgb(0 0 0 / 0.1), + 0 2px 4px -2px rgb(0 0 0 / 0.1) !important; + padding: 0.75rem !important; +} + +.echarts-tooltip-content { + color: hsl(var(--popover-foreground)) !important; +} + +/* Ensure text is readable in both themes */ +.echarts-container text { + fill: currentColor !important; +} + +/* Chart background */ +.echarts-container canvas { + background: transparent !important; +} + +/* Loading state */ +.echarts-loading-mask { + background: hsl(var(--background) / 0.8) !important; +} + +/* Color palette for series */ +:root { + --echarts-color-0: #5470c6; + --echarts-color-1: #91cc75; + --echarts-color-2: #fac858; + --echarts-color-3: #ee6666; + --echarts-color-4: #73c0de; + --echarts-color-5: #3ba272; + --echarts-color-6: #fc8452; + --echarts-color-7: #9a60b4; + --echarts-color-8: #ea7ccc; + --echarts-color-9: #5470c6; +} + +/* Responsive sizing */ +@media (max-width: 768px) { + .echarts-container { + min-height: 250px; + } +} diff --git a/src/routes/weather/compare/highcharts.css b/src/routes/weather/compare/highcharts.css deleted file mode 100644 index 3463cb2..0000000 --- a/src/routes/weather/compare/highcharts.css +++ /dev/null @@ -1,1172 +0,0 @@ -:root, -.highcharts-light { - --highcharts-color-0: #2caffe; - --highcharts-color-1: #544fc5; - --highcharts-color-2: #00e272; - --highcharts-color-3: #fe6a35; - --highcharts-color-4: #6b8abc; - --highcharts-color-5: #d568fb; - --highcharts-color-6: #2ee0ca; - --highcharts-color-7: #fa4b42; - --highcharts-color-8: #feb56a; - --highcharts-color-9: #91e8e1; - --highcharts-background-color: #ffffff; - --highcharts-tooltip-color: #f6f6f6; - --highcharts-neutral-color-100: #000000; - --highcharts-neutral-color-80: #333333; - --highcharts-neutral-color-60: #666666; - --highcharts-neutral-color-40: #999999; - --highcharts-neutral-color-20: #cccccc; - --highcharts-neutral-color-10: #e6e6e6; - --highcharts-neutral-color-5: #f2f2f2; - --highcharts-neutral-color-3: #f7f7f7; - --highcharts-highlight-color-100: #0022ff; - --highcharts-highlight-color-80: #334eff; - --highcharts-highlight-color-60: #667aff; - --highcharts-highlight-color-20: #ccd3ff; - --highcharts-highlight-color-10: #e6e9ff; - --highcharts-positive-color: #06b535; - --highcharts-negative-color: #f21313; -} - -@media (prefers-color-scheme: dark) { - :root { - --highcharts-background-color: rgb(2, 8, 23); - --highcharts-neutral-color-100: rgb(255, 255, 255); - --highcharts-neutral-color-80: rgb(248, 250, 252); - --highcharts-neutral-color-60: rgb(173, 173, 173); - --highcharts-neutral-color-40: rgb(133, 133, 133); - --highcharts-neutral-color-20: rgb(92, 92, 92); - --highcharts-neutral-color-10: rgb(71, 71, 71); - --highcharts-neutral-color-5: rgb(61, 61, 61); - --highcharts-neutral-color-3: rgb(57, 57, 57); - --highcharts-highlight-color-100: rgb(122, 167, 255); - --highcharts-highlight-color-80: rgb(108, 144, 214); - --highcharts-highlight-color-60: rgb(94, 121, 173); - --highcharts-highlight-color-20: rgb(65, 74, 92); - --highcharts-highlight-color-10: rgb(58, 63, 71); - } -} - -.highcharts-dark { - /*--highcharts-background-color: rgb(48, 48, 48);*/ - --highcharts-background-color: rgb(2, 8, 23); - --highcharts-tooltip-color: rgb(57, 64, 70); - --highcharts-neutral-color-100: rgb(255, 255, 255); - --highcharts-neutral-color-80: rgb(214, 214, 214); - --highcharts-neutral-color-60: rgb(173, 173, 173); - --highcharts-neutral-color-40: rgb(133, 133, 133); - --highcharts-neutral-color-20: rgb(92, 92, 92); - --highcharts-neutral-color-10: rgb(71, 71, 71); - --highcharts-neutral-color-5: rgb(61, 61, 61); - --highcharts-neutral-color-3: rgb(57, 57, 57); - --highcharts-highlight-color-100: rgb(122, 167, 255); - --highcharts-highlight-color-80: rgb(108, 144, 214); - --highcharts-highlight-color-60: rgb(94, 121, 173); - --highcharts-highlight-color-20: rgb(65, 74, 92); - --highcharts-highlight-color-10: rgb(58, 63, 71); -} - -.highcharts-container { - position: relative; - overflow: hidden; - width: 100%; - height: 100%; - text-align: left; - line-height: normal; - z-index: 0; - -webkit-tap-highlight-color: transparent; - font-family: Helvetica, Arial, sans-serif; - font-size: 1rem; - user-select: none; - touch-action: manipulation; - outline: none; -} - -.highcharts-root { - display: block; -} - -.highcharts-root text { - stroke-width: 0; -} - -.highcharts-strong { - font-weight: 700; -} - -.highcharts-emphasized { - font-style: italic; -} - -.highcharts-anchor { - cursor: pointer; -} - -.highcharts-background { - fill: var(--highcharts-background-color); -} - -.highcharts-plot-border, -.highcharts-plot-background { - fill: none; -} - -.highcharts-label-box { - fill: none; -} - -.highcharts-label text { - fill: var(--highcharts-neutral-color-80); - font-size: 0.8em; -} - -.highcharts-button-box { - fill: inherit; -} - -.highcharts-tracker-line { - stroke-linejoin: round; - stroke: rgba(192, 192, 192, 0.0001); - stroke-width: 22; - fill: none; -} - -.highcharts-tracker-area { - fill: rgba(192, 192, 192, 0.0001); - stroke-width: 0; -} - -.highcharts-title { - fill: var(--highcharts-neutral-color-80); - font-size: 1.2em; - font-weight: 700; -} - -.highcharts-subtitle { - fill: var(--highcharts-neutral-color-60); - font-size: 0.8em; -} - -.highcharts-axis-line { - fill: none; - stroke: var(--highcharts-neutral-color-80); -} - -.highcharts-yaxis .highcharts-axis-line { - stroke-width: 0; -} - -.highcharts-axis-title { - fill: var(--highcharts-neutral-color-60); - font-size: 0.8em; -} - -.highcharts-axis-labels { - fill: var(--highcharts-neutral-color-80); - cursor: default; - font-size: 0.65em; -} - -.highcharts-grid-line { - fill: none; - stroke: var(--highcharts-neutral-color-10); -} - -.highcharts-xaxis-grid .highcharts-grid-line { - stroke-width: 0; -} - -.highcharts-tick { - stroke: var(--highcharts-neutral-color-80); -} - -.highcharts-yaxis .highcharts-tick { - stroke-width: 0; -} - -.highcharts-minor-grid-line { - stroke: var(--highcharts-neutral-color-5); -} - -.highcharts-crosshair-thin { - stroke-width: 1px; - stroke: var(--highcharts-neutral-color-20); -} - -.highcharts-crosshair-category { - stroke: var(--highcharts-highlight-color-20); - stroke-opacity: 0.25; -} - -.highcharts-credits { - cursor: pointer; - fill: var(--highcharts-neutral-color-40); - font-size: 0.6em; - transition: - fill 250ms, - font-size 250ms; -} - -.highcharts-credits:hover { - fill: var(--highcharts-neutral-color-100); - font-size: 0.7em; -} - -.highcharts-tooltip { - cursor: default; - pointer-events: none; - white-space: nowrap; - transition: stroke 150ms; -} - -.highcharts-tooltip text { - fill: var(--highcharts-neutral-color-80); - font-size: 0.8em; -} - -.highcharts-tooltip .highcharts-header { - font-size: 0.8em; -} - -.highcharts-tooltip-box { - stroke-width: 1px; - fill: var(--highcharts-tooltip-color); -} - -.highcharts-tooltip-box { - stroke-width: 0; - fill: var(--highcharts-tooltip-color); -} - -.highcharts-tooltip-box .highcharts-label-box { - fill: var(--highcharts-tooltip-color); -} - -div.highcharts-tooltip { - filter: none; - font-size: 0.8em; -} - -.highcharts-selection-marker { - fill: var(--highcharts-highlight-color-80); - fill-opacity: 0.25; -} - -.highcharts-graph { - fill: none; - stroke-width: 1.5px; - stroke-linecap: round; - stroke-linejoin: round; -} - -.highcharts-empty-series { - stroke-width: 1px; - fill: none; - stroke: var(--highcharts-neutral-color-20); -} - -.highcharts-state-hover .highcharts-graph { - stroke-width: 3; -} - -.highcharts-point-inactive { - opacity: 0.2; - transition: opacity 50ms; -} - -.highcharts-series-inactive { - opacity: 0.2; - transition: opacity 50ms; -} - -.highcharts-state-hover path { - transition: stroke-width 50ms; -} - -.highcharts-state-normal path { - transition: stroke-width 250ms; -} - -g.highcharts-series, -.highcharts-point, -.highcharts-markers, -.highcharts-data-labels { - transition: opacity 250ms; -} - -.highcharts-legend-series-active g.highcharts-series:not(.highcharts-series-hover), -.highcharts-legend-point-active .highcharts-point:not(.highcharts-point-hover), -.highcharts-legend-series-active .highcharts-markers:not(.highcharts-series-hover), -.highcharts-legend-series-active .highcharts-data-labels:not(.highcharts-series-hover) { - opacity: 0.2; -} - -.highcharts-color-0 { - fill: var(--highcharts-color-0); - stroke: var(--highcharts-color-0); -} - -.highcharts-color-1 { - fill: var(--highcharts-color-1); - stroke: var(--highcharts-color-1); -} - -.highcharts-color-2 { - fill: var(--highcharts-color-2); - stroke: var(--highcharts-color-2); -} - -.highcharts-color-3 { - fill: var(--highcharts-color-3); - stroke: var(--highcharts-color-3); -} - -.highcharts-color-4 { - fill: var(--highcharts-color-4); - stroke: var(--highcharts-color-4); -} - -.highcharts-color-5 { - fill: var(--highcharts-color-5); - stroke: var(--highcharts-color-5); -} - -.highcharts-color-6 { - fill: var(--highcharts-color-6); - stroke: var(--highcharts-color-6); -} - -.highcharts-color-7 { - fill: var(--highcharts-color-7); - stroke: var(--highcharts-color-7); -} - -.highcharts-color-8 { - fill: var(--highcharts-color-8); - stroke: var(--highcharts-color-8); -} - -.highcharts-color-9 { - fill: var(--highcharts-color-9); - stroke: var(--highcharts-color-9); -} - -.highcharts-area { - fill-opacity: 0.75; - stroke-width: 0; -} - -.highcharts-markers { - stroke-width: 1px; - stroke: var(--highcharts-background-color); -} - -.highcharts-a11y-markers-hidden - .highcharts-point:not(.highcharts-point-hover, .highcharts-a11y-marker-visible), -.highcharts-a11y-marker-hidden { - opacity: 0; -} - -.highcharts-point { - stroke-width: 1px; -} - -.highcharts-dense-data .highcharts-point { - stroke-width: 0; -} - -.highcharts-data-label text, -text.highcharts-data-label { - font-size: 0.7em; - font-weight: 700; -} - -.highcharts-data-label-box { - fill: none; - stroke-width: 0; -} - -.highcharts-data-label text, -text.highcharts-data-label { - fill: var(--highcharts-neutral-color-80); -} - -.highcharts-data-label-connector { - fill: none; -} - -.highcharts-data-label-hidden { - pointer-events: none; -} - -.highcharts-halo { - fill-opacity: 0.25; - stroke-width: 0; -} - -.highcharts-series-label text { - fill: inherit; - font-weight: 700; -} - -.highcharts-series:not(.highcharts-pie-series) .highcharts-point-select, -.highcharts-markers .highcharts-point-select { - fill: var(--highcharts-neutral-color-20); - stroke: var(--highcharts-neutral-color-100); -} - -.highcharts-column-series rect.highcharts-point { - stroke: var(--highcharts-background-color); -} - -.highcharts-column-series .highcharts-point { - transition: fill-opacity 250ms; -} - -.highcharts-column-series .highcharts-point-hover { - fill-opacity: 0.75; - transition: fill-opacity 50ms; -} - -.highcharts-pie-series .highcharts-point { - stroke-linejoin: round; - stroke: var(--highcharts-background-color); -} - -.highcharts-pie-series .highcharts-point-hover { - fill-opacity: 0.75; - transition: fill-opacity 50ms; -} - -.highcharts-funnel-series .highcharts-point { - stroke-linejoin: round; - stroke: var(--highcharts-background-color); -} - -.highcharts-funnel-series .highcharts-point-hover { - fill-opacity: 0.75; - transition: fill-opacity 50ms; -} - -.highcharts-funnel-series .highcharts-point-select { - fill: inherit; - stroke: inherit; -} - -.highcharts-pyramid-series .highcharts-point { - stroke-linejoin: round; - stroke: var(--highcharts-background-color); -} - -.highcharts-pyramid-series .highcharts-point-hover { - fill-opacity: 0.75; - transition: fill-opacity 50ms; -} - -.highcharts-pyramid-series .highcharts-point-select { - fill: inherit; - stroke: inherit; -} - -.highcharts-solidgauge-series .highcharts-point { - stroke-width: 0; -} - -.highcharts-treemap-series .highcharts-point { - stroke-width: 1px; - stroke: var(--highcharts-neutral-color-10); - transition: - stroke 250ms, - fill 250ms, - fill-opacity 250ms; -} - -.highcharts-treemap-series .highcharts-point-hover { - stroke: var(--highcharts-neutral-color-40); - transition: - stroke 25ms, - fill 25ms, - fill-opacity 25ms; -} - -.highcharts-treemap-series .highcharts-above-level { - display: none; -} - -.highcharts-treemap-series .highcharts-internal-node { - fill: none; -} - -.highcharts-treemap-series .highcharts-internal-node-interactive { - fill-opacity: 0.15; - cursor: pointer; -} - -.highcharts-treemap-series .highcharts-internal-node-interactive:hover { - fill-opacity: 0.75; -} - -.highcharts-vector-series .highcharts-point { - fill: none; - stroke-width: 2px; -} - -.highcharts-windbarb-series .highcharts-point { - fill: none; - stroke-width: 2px; -} - -.highcharts-lollipop-stem { - stroke: var(--highcharts-neutral-color-100); -} - -.highcharts-focus-border { - fill: none; - stroke-width: 2px; -} - -.highcharts-legend-item-hidden .highcharts-focus-border { - fill: none !important; -} - -.highcharts-legend-box { - fill: none; - stroke-width: 0; -} - -.highcharts-legend-item > text { - fill: var(--highcharts-neutral-color-80); - font-weight: 700; - font-size: 0.8em; - cursor: pointer; - stroke-width: 0; -} - -.highcharts-legend-item:hover text { - fill: var(--highcharts-neutral-color-100); -} - -.highcharts-legend-item-hidden * { - fill: var(--highcharts-neutral-color-60) !important; - stroke: var(--highcharts-neutral-color-60) !important; - transition: fill 250ms; - text-decoration: line-through; -} - -.highcharts-legend-nav-active { - fill: var(--highcharts-highlight-color-100); - cursor: pointer; -} - -.highcharts-legend-nav-inactive { - fill: var(--highcharts-neutral-color-20); -} - -circle.highcharts-legend-nav-active, -circle.highcharts-legend-nav-inactive { - fill: rgba(192, 192, 192, 0.0001); -} - -.highcharts-legend-title-box { - fill: none; - stroke-width: 0; -} - -.highcharts-bubble-legend-symbol { - stroke-width: 2; - fill-opacity: 0.5; -} - -.highcharts-bubble-legend-connectors { - stroke-width: 1; -} - -.highcharts-bubble-legend-labels { - fill: var(--highcharts-neutral-color-80); - font-size: 0.7em; -} - -.highcharts-loading { - position: absolute; - background-color: var(--highcharts-background-color); - opacity: 0.5; - text-align: center; - z-index: 10; - transition: opacity 250ms; -} - -.highcharts-loading-hidden { - height: 0 !important; - opacity: 0; - overflow: hidden; - transition: - opacity 250ms, - height 250ms step-end; -} - -.highcharts-loading-inner { - font-weight: 700; - position: relative; - top: 45%; -} - -.highcharts-plot-band, -.highcharts-pane { - fill: rgba(255, 255, 194, 0.5); -} - -.highcharts-dark .highcharts-plot-band, -.highcharts-dark .highcharts-pane { - fill: rgba(255, 255, 194, 0.1); -} - -.highcharts-plot-line { - fill: none; - stroke: var(--highcharts-neutral-color-40); - stroke-width: 1px; -} - -.highcharts-plot-line-label { - font-size: 0.8em; -} - -.highcharts-boxplot-box { - fill: var(--highcharts-background-color); -} - -.highcharts-boxplot-median { - stroke-width: 2px; -} - -.highcharts-bubble-series .highcharts-point { - fill-opacity: 0.5; -} - -.highcharts-errorbar-series .highcharts-point { - stroke: var(--highcharts-neutral-color-100); -} - -.highcharts-gauge-series .highcharts-data-label-box { - stroke: var(--highcharts-neutral-color-20); - stroke-width: 1px; -} - -.highcharts-gauge-series .highcharts-dial { - fill: var(--highcharts-neutral-color-100); - stroke-width: 0; -} - -.highcharts-polygon-series .highcharts-graph { - fill: inherit; - stroke-width: 0; -} - -.highcharts-waterfall-series .highcharts-graph { - stroke: var(--highcharts-neutral-color-80); - stroke-dasharray: 1, 3; -} - -.highcharts-sankey-series .highcharts-point { - stroke-width: 0; -} - -.highcharts-sankey-series .highcharts-link { - transition: - fill 250ms, - fill-opacity 250ms; - fill-opacity: 0.5; -} - -.highcharts-sankey-series .highcharts-point-hover.highcharts-link { - transition: - fill 50ms, - fill-opacity 50ms; - fill-opacity: 1; -} - -.highcharts-venn-series .highcharts-point { - fill-opacity: 0.75; - stroke: var(--highcharts-neutral-color-20); - transition: - stroke 250ms, - fill-opacity 250ms; -} - -.highcharts-venn-series .highcharts-point-hover { - fill-opacity: 1; - stroke: var(--highcharts-neutral-color-20); -} - -.highcharts-timeline-series .highcharts-graph { - stroke: var(--highcharts-neutral-color-20); -} - -.highcharts-navigator-mask-outside { - fill-opacity: 0; -} - -.highcharts-navigator-mask-inside { - fill: var(--highcharts-highlight-color-60); - fill-opacity: 0.25; - cursor: ew-resize; -} - -.highcharts-navigator-outline { - stroke: var(--highcharts-neutral-color-40); - fill: none; -} - -.highcharts-navigator-handle { - stroke: var(--highcharts-neutral-color-40); - fill: var(--highcharts-neutral-color-5); - cursor: ew-resize; -} - -.highcharts-navigator-series { - fill: var(--highcharts-highlight-color-80); - stroke: var(--highcharts-highlight-color-80); -} - -.highcharts-navigator-series .highcharts-graph { - stroke-width: 1px; -} - -.highcharts-navigator-series .highcharts-area { - fill-opacity: 0.05; -} - -.highcharts-navigator-xaxis .highcharts-axis-line { - stroke-width: 0; -} - -.highcharts-navigator-xaxis .highcharts-grid-line { - stroke-width: 1px; - stroke: var(--highcharts-neutral-color-10); -} - -.highcharts-navigator-xaxis.highcharts-axis-labels { - fill: var(--highcharts-neutral-color-100); - font-size: 0.7em; - opacity: 0.6; -} - -.highcharts-navigator-yaxis .highcharts-grid-line { - stroke-width: 0; -} - -.highcharts-scrollbar-thumb { - fill: var(--highcharts-neutral-color-20); - stroke: var(--highcharts-neutral-color-20); - stroke-width: 0; -} - -.highcharts-scrollbar-button { - fill: var(--highcharts-neutral-color-10); - stroke: var(--highcharts-neutral-color-20); - stroke-width: 1px; -} - -.highcharts-scrollbar-arrow { - fill: var(--highcharts-neutral-color-60); -} - -.highcharts-scrollbar-rifles { - stroke: none; - stroke-width: 1px; -} - -.highcharts-scrollbar-track { - fill: rgba(255, 255, 255, 0.001); - stroke: var(--highcharts-neutral-color-20); - stroke-width: 1px; -} - -.highcharts-button { - fill: var(--highcharts-neutral-color-3); - stroke: var(--highcharts-neutral-color-20); - cursor: default; - stroke-width: 1px; - transition: fill 250ms; -} - -.highcharts-button text { - fill: var(--highcharts-neutral-color-80); - font-size: 0.8em; -} - -.highcharts-button-hover { - transition: fill 0ms; - fill: var(--highcharts-neutral-color-10); - stroke: var(--highcharts-neutral-color-20); -} - -.highcharts-button-hover text { - fill: var(--highcharts-neutral-color-80); -} - -.highcharts-button-pressed { - font-weight: 700; - fill: var(--highcharts-highlight-color-10); - stroke: var(--highcharts-neutral-color-20); -} - -.highcharts-button-pressed text { - fill: var(--highcharts-neutral-color-80); - font-weight: 700; -} - -.highcharts-button-disabled text { - fill: var(--highcharts-neutral-color-80); -} - -.highcharts-range-selector-buttons .highcharts-button { - stroke-width: 0; -} - -.highcharts-range-label rect { - fill: none; -} - -.highcharts-range-label text { - fill: var(--highcharts-neutral-color-60); -} - -.highcharts-range-input rect { - fill: none; -} - -.highcharts-range-input text { - fill: var(--highcharts-neutral-color-80); - font-size: 0.8em; -} - -.highcharts-range-input { - stroke-width: 1px; - stroke: var(--highcharts-neutral-color-20); -} - -input.highcharts-range-selector { - position: absolute; - border: 0; - width: 1px; - height: 1px; - padding: 0; - text-align: center; - left: -9em; -} - -.highcharts-crosshair-label text { - fill: var(--highcharts-background-color); - font-size: 1.7em; -} - -.highcharts-crosshair-label .highcharts-label-box { - fill: inherit; -} - -.highcharts-candlestick-series .highcharts-point { - stroke: var(--highcharts-neutral-color-100); - stroke-width: 1px; -} - -.highcharts-candlestick-series .highcharts-point-up { - fill: var(--highcharts-background-color); -} - -.highcharts-hollowcandlestick-series .highcharts-point-down { - fill: var(--highcharts-negative-color); - stroke: var(--highcharts-negative-color); -} - -.highcharts-hollowcandlestick-series .highcharts-point-down-bearish-up { - fill: var(--highcharts-positive-color); - stroke: var(--highcharts-positive-color); -} - -.highcharts-hollowcandlestick-series .highcharts-point-up { - fill: transparent; - stroke: var(--highcharts-positive-color); -} - -.highcharts-ohlc-series .highcharts-point-hover { - stroke-width: 3px; -} - -.highcharts-flags-series .highcharts-point .highcharts-label-box { - stroke: var(--highcharts-neutral-color-40); - fill: var(--highcharts-background-color); - transition: fill 250ms; -} - -.highcharts-flags-series .highcharts-point-hover .highcharts-label-box { - stroke: var(--highcharts-neutral-color-100); - fill: var(--highcharts-highlight-color-20); -} - -.highcharts-flags-series .highcharts-point text { - fill: var(--highcharts-neutral-color-100); - font-size: 0.9em; - font-weight: 700; -} - -.highcharts-map-series .highcharts-point { - transition: - fill 500ms, - fill-opacity 500ms, - stroke-width 250ms; - stroke: var(--highcharts-neutral-color-20); - stroke-width: inherit; -} - -.highcharts-map-series .highcharts-point-hover { - transition: - fill 0ms, - fill-opacity 0ms; - fill-opacity: 0.5; -} - -.highcharts-mapline-series .highcharts-point { - fill: none; -} - -.highcharts-heatmap-series .highcharts-point { - stroke-width: 0; -} - -.highcharts-map-navigation { - font-size: 1.3em; - font-weight: 700; - text-align: center; -} - -.highcharts-map-navigation.highcharts-button { - fill: var(--highcharts-background-color); - stroke: var(--highcharts-neutral-color-10); -} - -.highcharts-map-navigation.highcharts-button:hover { - fill: var(--highcharts-neutral-color-10); -} - -.highcharts-map-navigation.highcharts-button .highcharts-button-symbol { - stroke-width: 2px; -} - -.highcharts-mapview-inset-border { - stroke: var(--highcharts-neutral-color-20); - stroke-width: 1px; - fill: none; -} - -.highcharts-coloraxis { - stroke-width: 0; -} - -.highcharts-coloraxis-marker { - fill: var(--highcharts-neutral-color-40); -} - -.highcharts-null-point { - fill: var(--highcharts-neutral-color-3); -} - -.highcharts-3d-frame { - fill: transparent; -} - -.highcharts-contextbutton { - fill: var(--highcharts-background-color); - stroke: none; - stroke-linecap: round; -} - -.highcharts-contextbutton:hover { - fill: var(--highcharts-neutral-color-10); - stroke: var(--highcharts-neutral-color-10); -} - -.highcharts-button-symbol { - stroke: var(--highcharts-neutral-color-60); - stroke-width: 3px; -} - -.highcharts-menu { - border: none; - background: var(--highcharts-background-color); - border-radius: 3px; - padding: 0.5em; - box-shadow: 3px 3px 10px #888; -} - -.highcharts-menu-item { - background: 0 0; - border-radius: 3px; - color: var(--highcharts-neutral-color-80); - cursor: pointer; - font-size: 0.8em; - list-style-type: none; - padding: 0.5em; - transition: - background 250ms, - color 250ms; -} - -.highcharts-menu-item:hover { - background: var(--highcharts-neutral-color-5); -} - -.highcharts-breadcrumbs-button { - fill: none; - stroke-width: 0; - cursor: pointer; -} - -.highcharts-breadcrumbs-separator { - fill: var(--highcharts-neutral-color-60); -} - -.highcharts-drilldown-point { - cursor: pointer; -} - -.highcharts-drilldown-data-label text, -text.highcharts-drilldown-data-label, -.highcharts-drilldown-axis-label { - cursor: pointer; - fill: var(--highcharts-highlight-color-100); - font-weight: 700; - text-decoration: underline; -} - -.highcharts-no-data text { - font-weight: 700; - font-size: 0.8em; - fill: var(--highcharts-neutral-color-60); -} - -.highcharts-axis-resizer { - cursor: ns-resize; - stroke: var(--highcharts-neutral-color-100); - stroke-width: 2px; -} - -.highcharts-bullet-target { - stroke-width: 0; -} - -.highcharts-lineargauge-target { - stroke-width: 1px; - stroke: var(--highcharts-neutral-color-80); -} - -.highcharts-lineargauge-target-line { - stroke-width: 1px; - stroke: var(--highcharts-neutral-color-80); -} - -.highcharts-annotation-label-box { - stroke-width: 1px; - stroke: var(--highcharts-neutral-color-100); - fill: var(--highcharts-neutral-color-100); - fill-opacity: 0.75; -} - -.highcharts-annotation-label text { - fill: var(--highcharts-neutral-color-10); - font-size: 0.8em; -} - -.highcharts-a11y-proxy-button { - border-width: 0; - background-color: transparent; - cursor: pointer; - outline: none; - opacity: 0.001; - z-index: 999; - overflow: hidden; - padding: 0; - margin: 0; - display: block; - position: absolute; -} - -.highcharts-a11y-proxy-group li { - list-style: none; -} - -.highcharts-visually-hidden { - position: absolute; - width: 1px; - height: 1px; - overflow: hidden; - white-space: nowrap; - clip: rect(1px, 1px, 1px, 1px); - margin-top: -3px; - opacity: 0.01; -} - -.highcharts-a11y-invisible { - visibility: hidden; -} - -.highcharts-a11y-proxy-container, -.highcharts-a11y-proxy-container-before, -.highcharts-a11y-proxy-container-after { - position: absolute; - white-space: nowrap; -} - -g.highcharts-series, -.highcharts-markers, -.highcharts-point { - outline: none; -} - -.highcharts-treegrid-node-collapsed, -.highcharts-treegrid-node-expanded { - cursor: pointer; -} - -.highcharts-point-connecting-path { - fill: none; -} - -.highcharts-grid-axis .highcharts-tick { - stroke-width: 1px; -} - -.highcharts-grid-axis .highcharts-axis-line { - stroke-width: 1px; -} - -.highcharts-average-series { - fill: rgb(110, 110, 110); - stroke: rgb(110, 110, 110); -} - -.highcharts-average-series .highcharts-graph { - stroke-width: 4; - stroke-dasharray: 3, 8, 1, 8, 3; - stroke-linecap: square; - stroke-dashoffset:; -} - -.highcharts-dark .highcharts-average-series { - fill: rgb(200, 200, 200); - stroke: rgb(200, 200, 200); -} - -.highcharts-spread-series { - fill: rgb(236, 190, 41); - stroke: rgb(236, 190, 41); -} - -.highcharts-spread-series .highcharts-graph { - stroke-width: 2; -} - -.highcharts-dark .highcharts-spread-series { - fill: rgb(201, 162, 35); - stroke: rgb(201, 162, 35); -} diff --git a/src/routes/weather/week/+page.ts b/src/routes/weather/week/+page.ts index 0963c36..c891c68 100644 --- a/src/routes/weather/week/+page.ts +++ b/src/routes/weather/week/+page.ts @@ -8,8 +8,6 @@ import { geoLocationNameToRoute } from '$lib/utils/meteo'; import type { PageLoad } from './$types'; -export const prerender = true; - export const load = (async () => { const location = get(storedLocation); const locationRoute = geoLocationNameToRoute(location.name); diff --git a/src/routes/weather/week/[location]/+page.ts b/src/routes/weather/week/[location]/+page.ts index e321db1..efabba4 100644 --- a/src/routes/weather/week/[location]/+page.ts +++ b/src/routes/weather/week/[location]/+page.ts @@ -6,8 +6,6 @@ import { geoLocationNameToRoute } from '$lib/utils/meteo'; import type { PageLoad } from './$types'; -export const prerender = true; - export const load: PageLoad = async (event) => { const urlLocation = event.params.location; let urlLocationSplit, urlLocationName, urlLocationId;