remove echarts, use canvas
This commit is contained in:
@@ -1,28 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import {
|
||||
buildAverageSeries,
|
||||
buildCurrentTimeSeries,
|
||||
buildDaylightSeries,
|
||||
buildModelSeries,
|
||||
calculateAverage,
|
||||
composeChartOption,
|
||||
findUnit,
|
||||
getThemeColors
|
||||
} from '$lib/utils/echarts';
|
||||
|
||||
import { ChartContainer, ChartToolbar, EChart } from '$lib/components/charts';
|
||||
import '$lib/components/charts/echarts.css';
|
||||
import { ChartContainer, ChartToolbar } from '$lib/components/charts';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
|
||||
import {
|
||||
type MarkArea,
|
||||
CHART_COLORS,
|
||||
CanvasChart,
|
||||
type ChartSeries,
|
||||
SERIES_COLORS,
|
||||
calculateAverage,
|
||||
findUnit,
|
||||
isColumnUnit
|
||||
} from '$lib/charts';
|
||||
import {
|
||||
type DaylightBand,
|
||||
type ModelCompareResult,
|
||||
fetchModelComparison
|
||||
} from '$lib/services/weather';
|
||||
@@ -32,19 +29,18 @@
|
||||
import ModelPictogramTimeline from './ModelPictogramTimeline.svelte';
|
||||
|
||||
import type { PageData } from './$types';
|
||||
import type * as echarts from 'echarts';
|
||||
|
||||
const models = [modelsFlat];
|
||||
|
||||
const CHART_GROUP = 'model-compare';
|
||||
|
||||
// ─── Display State (does NOT trigger data re-fetch) ─────────────────────────
|
||||
|
||||
let showLegend = $state(false);
|
||||
|
||||
// ─── Data Fetch State ───────────────────────────────────────────────────────
|
||||
|
||||
let chartComponents: EChart[] = $state([]);
|
||||
let chartInstances: echarts.ECharts[] = $state([]);
|
||||
let chartOptions: Array<Record<string, unknown>> = $state([]);
|
||||
let chartComponents: CanvasChart[] = $state([]);
|
||||
let mounted = $state(false);
|
||||
let loading = $state(true);
|
||||
let loadError = $state<string | null>(null);
|
||||
@@ -86,7 +82,7 @@
|
||||
hourly: Record<string, unknown>;
|
||||
hourly_units: Record<string, string>;
|
||||
timezone: string;
|
||||
markAreas: MarkArea[];
|
||||
daylightBands: DaylightBand[];
|
||||
timestamps: number[];
|
||||
sunrise: number[];
|
||||
sunset: number[];
|
||||
@@ -100,22 +96,8 @@
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
chartInstances = [];
|
||||
chartOptions = [];
|
||||
chartComponents = [];
|
||||
});
|
||||
|
||||
// ─── Chart Instance Tracking ────────────────────────────────────────────────
|
||||
|
||||
function handleChartReady(chart: echarts.ECharts): void {
|
||||
chartInstances = [...chartInstances, chart];
|
||||
}
|
||||
|
||||
// chart components persist across refetches (options update in place), so
|
||||
// the registry is never reset; only instances disposed because the chart
|
||||
// count shrank are filtered out
|
||||
let liveCharts = $derived(chartInstances.filter((chart) => !chart.isDisposed()));
|
||||
// components persist across refetches; entries are null while unmounted
|
||||
let liveCharts = $derived(chartComponents.filter((chart) => chart != null));
|
||||
|
||||
// ─── Data Fetching (only when params.hourly or params.models change) ───────
|
||||
|
||||
@@ -149,7 +131,7 @@
|
||||
hourly: result.hourlyFlat,
|
||||
hourly_units: result.hourlyUnitsFlat,
|
||||
timezone: result.timezone,
|
||||
markAreas: result.markAreas,
|
||||
daylightBands: result.daylightBands,
|
||||
timestamps: result.timestamps,
|
||||
sunrise: result.sunrise,
|
||||
sunset: result.sunset
|
||||
@@ -164,83 +146,77 @@
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Chart Option Building (runs when fetchedData OR display toggles change) ─
|
||||
// ─── Chart Building (runs when fetchedData or the variable list changes) ────
|
||||
|
||||
$effect(() => {
|
||||
if (!fetchedData) return;
|
||||
// Timestamps from the service are in milliseconds; CanvasChart uses seconds.
|
||||
let timestampsSec = $derived.by(() =>
|
||||
fetchedData ? fetchedData.timestamps.map((t) => t / 1000) : []
|
||||
);
|
||||
|
||||
const { hourly: hourlyData, hourly_units, timezone, markAreas, timestamps } = fetchedData;
|
||||
const _showLegend = showLegend;
|
||||
interface ChartDef {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
unit: string;
|
||||
showCredit: boolean;
|
||||
series: ChartSeries[];
|
||||
}
|
||||
|
||||
const colors = getThemeColors();
|
||||
let chartDefs = $derived.by((): ChartDef[] => {
|
||||
if (!fetchedData) return [];
|
||||
|
||||
const { hourly: hourlyData, hourly_units, timestamps } = fetchedData;
|
||||
const chartVariables = params.hourly?.filter((v) => v !== 'weather_code') || [];
|
||||
const variableCount = chartVariables.length;
|
||||
const timeLength = timestamps.length;
|
||||
const newOptions: Array<Record<string, unknown>> = [];
|
||||
const defs: ChartDef[] = [];
|
||||
|
||||
for (let vi = 0; vi < variableCount; vi++) {
|
||||
const variable = chartVariables[vi];
|
||||
const unit = findUnit(hourly_units, hourlyData, variable);
|
||||
const isColumn = isColumnUnit(unit);
|
||||
|
||||
const series: Array<Record<string, unknown>> = [];
|
||||
const series: ChartSeries[] = [];
|
||||
|
||||
let modelIndex = 0;
|
||||
for (const [model, values] of Object.entries(hourlyData)) {
|
||||
if (model === 'time') continue;
|
||||
if (!model.startsWith(variable)) continue;
|
||||
|
||||
const seriesData = (values as (number | null)[]).map(
|
||||
(val, idx) => [timestamps[idx], val] as [number, number | null]
|
||||
);
|
||||
|
||||
series.push(
|
||||
buildModelSeries({
|
||||
name: model,
|
||||
data: seriesData,
|
||||
unit
|
||||
})
|
||||
);
|
||||
series.push({
|
||||
name: model,
|
||||
type: isColumn ? 'bar' : 'line',
|
||||
color: SERIES_COLORS[modelIndex % SERIES_COLORS.length],
|
||||
data: values as (number | null)[],
|
||||
width: 2
|
||||
});
|
||||
modelIndex++;
|
||||
}
|
||||
|
||||
const { average } = calculateAverage(hourlyData, variable, timeLength);
|
||||
const averageData = average.map((val, idx) => [timestamps[idx], val] as [number, number]);
|
||||
series.push(buildAverageSeries({ variable, data: averageData, unit }));
|
||||
|
||||
series.push(buildCurrentTimeSeries());
|
||||
|
||||
const daylightSeries = buildDaylightSeries({ markAreas });
|
||||
if (daylightSeries) {
|
||||
series.push(daylightSeries);
|
||||
}
|
||||
series.push({
|
||||
name: variable + '_average',
|
||||
type: isColumn ? 'bar' : 'line',
|
||||
color: CHART_COLORS.average,
|
||||
data: average,
|
||||
width: 4,
|
||||
dashed: !isColumn
|
||||
});
|
||||
|
||||
const isFirst = vi === 0;
|
||||
const isLast = vi === variableCount - 1;
|
||||
|
||||
const option = composeChartOption({
|
||||
title: isFirst
|
||||
? {
|
||||
text: 'Model Compare',
|
||||
subtext: `Compare ${chartVariables.join(', ') || ''} in models: ${params.models?.join(', ') || ''}`
|
||||
}
|
||||
: null,
|
||||
tooltip: { unit, timezone },
|
||||
legend: { show: _showLegend },
|
||||
grid: {
|
||||
hasTitle: isFirst,
|
||||
hasSubtitle: isFirst,
|
||||
showLegend: _showLegend
|
||||
},
|
||||
yAxis: { unit },
|
||||
series,
|
||||
toolbox: false,
|
||||
defs.push({
|
||||
title: isFirst ? 'Model Compare' : undefined,
|
||||
subtitle: isFirst
|
||||
? `Compare ${chartVariables.join(', ') || ''} in models: ${params.models?.join(', ') || ''}`
|
||||
: undefined,
|
||||
unit,
|
||||
showCredit: isLast,
|
||||
colors,
|
||||
timezone
|
||||
series
|
||||
});
|
||||
|
||||
newOptions.push(option);
|
||||
}
|
||||
|
||||
chartOptions = newOptions;
|
||||
return defs;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -254,16 +230,25 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ChartContainer {loading} chartCount={chartOptions.length} chartHeight={showLegend ? 400 : 300}>
|
||||
{#each chartOptions as option, i (i)}
|
||||
<EChart
|
||||
{option}
|
||||
notMerge
|
||||
height={showLegend ? '400px' : '300px'}
|
||||
onChartReady={handleChartReady}
|
||||
bind:this={chartComponents[i]}
|
||||
/>
|
||||
{/each}
|
||||
<ChartContainer {loading} chartCount={chartDefs.length || 1} chartHeight={300}>
|
||||
{#if fetchedData}
|
||||
{#each chartDefs as def, i (i)}
|
||||
<CanvasChart
|
||||
bind:this={chartComponents[i]}
|
||||
timestamps={timestampsSec}
|
||||
timezone={fetchedData.timezone}
|
||||
series={def.series}
|
||||
bands={fetchedData.daylightBands}
|
||||
unit={def.unit}
|
||||
title={def.title}
|
||||
subtitle={def.subtitle}
|
||||
showCredit={def.showCredit}
|
||||
{showLegend}
|
||||
height={300}
|
||||
group={CHART_GROUP}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</ChartContainer>
|
||||
|
||||
{#if fetchedData && !loading}
|
||||
|
||||
Reference in New Issue
Block a user