summaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/Chart.svelte112
1 files changed, 61 insertions, 51 deletions
diff --git a/frontend/src/components/Chart.svelte b/frontend/src/components/Chart.svelte
index ca8f4f8..0854f3a 100644
--- a/frontend/src/components/Chart.svelte
+++ b/frontend/src/components/Chart.svelte
@@ -1,67 +1,77 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
- import type * as echartsNS from 'echarts';
+ import { LineChart } from 'layerchart';
+ export interface ChartSeries {
+ key: string;
+ label?: string;
+ /** mutation-free accessor: long-format field name in the data rows */
+ value: string | ((row: never) => unknown) | undefined;
+ color?: string;
+ dashed?: boolean;
+ area?: boolean;
+ opacity?: number;
+ }
interface Props {
- option: echartsNS.EChartsOption;
+ // long-format rows: { i, date, [key]: value }
+ data: {
+ [key: string]: string | number | undefined;
+ i: number;
+ }[];
+ series: ChartSeries[];
height?: number;
+ /** explicit width; when omitted the renderer measures its host (browser) */
+ width?: number;
ariaLabel?: string;
+ legend?: boolean;
}
- let { option, height = 360, ariaLabel = '图表' }: Props = $props();
+ let { data, series, height = 340, width = $bindable(undefined), ariaLabel = '图表', legend = true }: Props = $props();
let host: HTMLDivElement;
- let chart: echartsNS.ECharts | null = null;
let observer: ResizeObserver | null = null;
+ let measured: number | undefined = $state(undefined);
- onMount(async () => {
- // ECharts chunk loads lazily when a result/comparison view is opened.
- const echarts = (await import('echarts')) as typeof echartsNS;
- if (!host) return;
- // Pass explicit sizes when the host has no layout yet (jsdom regression
- // tests report 0×0 and ECharts warns "Can't get DOM width or height").
- // In a real browser clientWidth/clientHeight are non-zero, so the measured
- // size is used exactly as before; responsive resizing still works through
- // the ResizeObserver below.
- chart = echarts.init(host, undefined, {
- renderer: 'svg',
- width: host.clientWidth || 640,
- height: host.clientHeight || height
- });
- observer = new ResizeObserver(() => {
- if (!chart) return;
- // re-measure on EVERY resize callback and pass the measured size
- // explicitly: echarts keeps the width given at init/resize, so a
- // desktop->mobile viewport change would otherwise leave the SVG at the
- // old width and overflow the page (real, re-verified live bug).
- const w = host.clientWidth || undefined;
- const h = host.clientHeight || undefined;
- if (w === undefined && h === undefined) return;
- chart.resize({ width: w, height: h });
- });
- observer.observe(host);
- chart?.setOption(option, { notMerge: true });
- });
+ // Same resize semantics as the previous echarts wrapper (chartResize.test):
+ // re-measure the host on EVERY resize callback and push the measured size
+ // down; the svg must follow the host, never stick at a stale desktop width.
+ function applyMeasure() {
+ const w = host?.clientWidth;
+ if (w && w > 0) measured = w;
+ }
- onDestroy(() => {
- observer?.disconnect();
- chart?.dispose();
- chart = null;
+ onMount(() => {
+ applyMeasure();
+ if (typeof ResizeObserver !== 'undefined') {
+ observer = new ResizeObserver(applyMeasure);
+ observer.observe(host);
+ }
});
+ onDestroy(() => observer?.disconnect());
- $effect(() => {
- chart?.setOption(option, { notMerge: true });
- });
-</script>
+ const lcSeries = $derived(
+ series.map((s) => ({
+ key: s.key,
+ label: s.label ?? s.key,
+ value: s.value,
+ color: s.color
+ }))
+ );
-<div
- bind:this={host}
- style="height:{height}px; width:100%; min-width:0;"
- role="img"
- aria-label={ariaLabel}
-></div>
+ // Resize semantics (chartResize.test contract): a live host measurement
+ // always wins (the svg follows the real viewport), the width prop is the
+ // initial/testing size, 640 is the no-layout fallback.
+ const resolvedWidth = $derived(measured ?? width ?? 640);
+</script>
-<style>
- div {
- display: block;
- }
-</style>
+<div bind:this={host} style="height:{height}px; width:100%; min-width:0;" role="img" aria-label={ariaLabel}>
+ <LineChart
+ data={data as never[]}
+ x="i"
+ series={lcSeries as never}
+ width={resolvedWidth}
+ {height}
+ {legend}
+ axis
+ clip
+ />
+</div>