remove echarts, use canvas
This commit is contained in:
@@ -1,32 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { storedLocation } from '$lib/stores/settings';
|
||||
|
||||
import {
|
||||
buildAverageSeries,
|
||||
buildCurrentTimeSeries,
|
||||
buildDaylightSeries,
|
||||
buildSpreadSeries,
|
||||
composeChartOption,
|
||||
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 { Label } from '$lib/components/ui/label';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
|
||||
import { CHART_COLORS, CanvasChart, type ChartSeries, isColumnUnit } from '$lib/charts';
|
||||
import {
|
||||
type DaylightBand,
|
||||
type EnsembleForecastResult,
|
||||
type MarkArea,
|
||||
fetchEnsembleForecast
|
||||
} from '$lib/services/weather';
|
||||
|
||||
import { defaultParameters } from '../../options';
|
||||
|
||||
import type { PageData } from './$types';
|
||||
import type * as echarts from 'echarts';
|
||||
|
||||
const CHART_GROUP = '14-day-ensemble';
|
||||
|
||||
// ─── Display State (does NOT trigger data re-fetch) ─────────────────────────
|
||||
|
||||
@@ -34,9 +26,7 @@
|
||||
|
||||
// ─── 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);
|
||||
@@ -64,7 +54,7 @@
|
||||
ensembleResult: EnsembleForecastResult;
|
||||
timestamps: number[];
|
||||
timezone: string;
|
||||
markAreas: MarkArea[];
|
||||
daylightBands: DaylightBand[];
|
||||
}
|
||||
|
||||
let fetchedData: FetchedData | null = $state(null);
|
||||
@@ -75,22 +65,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) ───────
|
||||
|
||||
@@ -125,7 +101,7 @@
|
||||
ensembleResult: result,
|
||||
timestamps: result.timestamps,
|
||||
timezone: result.timezone,
|
||||
markAreas: result.markAreas
|
||||
daylightBands: result.daylightBands
|
||||
};
|
||||
|
||||
loading = false;
|
||||
@@ -137,77 +113,74 @@
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 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 { ensembleResult, timestamps, timezone, markAreas } = fetchedData;
|
||||
const _showLegend = showLegend;
|
||||
interface ChartDef {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
unit: string;
|
||||
showCredit: boolean;
|
||||
series: ChartSeries[];
|
||||
}
|
||||
|
||||
const colors = getThemeColors();
|
||||
const variableCount = params.hourly?.length || 0;
|
||||
const newOptions: Array<Record<string, unknown>> = [];
|
||||
let chartDefs = $derived.by((): ChartDef[] => {
|
||||
if (!fetchedData) return [];
|
||||
|
||||
for (let vi = 0; vi < variableCount; vi++) {
|
||||
const variable = params.hourly![vi];
|
||||
const { ensembleResult } = fetchedData;
|
||||
const variables = params.hourly || [];
|
||||
const defs: ChartDef[] = [];
|
||||
|
||||
for (let vi = 0; vi < variables.length; vi++) {
|
||||
const variable = variables[vi];
|
||||
const varData = ensembleResult.variables[variable];
|
||||
if (!varData) continue;
|
||||
|
||||
const unit = varData.unit;
|
||||
const { average, min: minValues, max: maxValues } = varData;
|
||||
const isColumn = isColumnUnit(unit);
|
||||
const series: ChartSeries[] = [];
|
||||
|
||||
const series: Array<Record<string, unknown>> = [];
|
||||
|
||||
const spreadData: Array<[number, number, number]> = minValues.map(
|
||||
(minVal, index) =>
|
||||
[timestamps[index], minVal ?? 0, maxValues[index] ?? 0] as [number, number, number]
|
||||
);
|
||||
|
||||
series.push(...buildSpreadSeries({ variable, spreadData }));
|
||||
|
||||
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);
|
||||
// Individual ensemble members: thin, low-alpha lines
|
||||
for (let mi = 0; mi < varData.members.length; mi++) {
|
||||
series.push({
|
||||
name: `${variable}_member${String(mi).padStart(2, '0')}`,
|
||||
type: 'line',
|
||||
color: CHART_COLORS.memberLine,
|
||||
data: varData.members[mi],
|
||||
width: 1,
|
||||
showInLegend: false
|
||||
});
|
||||
}
|
||||
|
||||
const isFirst = vi === 0;
|
||||
const isLast = vi === variableCount - 1;
|
||||
|
||||
const option = composeChartOption({
|
||||
title: isFirst
|
||||
? {
|
||||
text: 'Model Spread',
|
||||
subtext: `Compare ${params.hourly?.join(', ') || ''} in models: ${params.models?.join(', ') || ''}`
|
||||
}
|
||||
: null,
|
||||
tooltip: { unit, timezone },
|
||||
legend: {
|
||||
show: _showLegend,
|
||||
data: [variable + '_average']
|
||||
},
|
||||
grid: {
|
||||
hasTitle: isFirst,
|
||||
hasSubtitle: isFirst,
|
||||
showLegend: _showLegend
|
||||
},
|
||||
yAxis: { unit },
|
||||
series,
|
||||
toolbox: false,
|
||||
showCredit: isLast,
|
||||
colors,
|
||||
timezone
|
||||
// Ensemble mean: bold dashed line on top
|
||||
series.push({
|
||||
name: variable + '_average',
|
||||
type: isColumn ? 'bar' : 'line',
|
||||
color: CHART_COLORS.average,
|
||||
data: varData.average,
|
||||
width: 4,
|
||||
dashed: !isColumn
|
||||
});
|
||||
|
||||
newOptions.push(option);
|
||||
const isFirst = vi === 0;
|
||||
const isLast = vi === variables.length - 1;
|
||||
|
||||
defs.push({
|
||||
title: isFirst ? 'Model Spread' : undefined,
|
||||
subtitle: isFirst
|
||||
? `Compare ${params.hourly?.join(', ') || ''} in models: ${params.models?.join(', ') || ''}`
|
||||
: undefined,
|
||||
unit,
|
||||
showCredit: isLast,
|
||||
series
|
||||
});
|
||||
}
|
||||
|
||||
chartOptions = newOptions;
|
||||
return defs;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -221,20 +194,25 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ChartContainer
|
||||
{loading}
|
||||
chartCount={params.hourly?.length || 0}
|
||||
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={params.hourly?.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>
|
||||
|
||||
<!-- ─── Toolbar: Controls + Download ───────────────────────────────────────── -->
|
||||
|
||||
Reference in New Issue
Block a user