summaryrefslogtreecommitdiff
path: root/frontend/src/components/Chart.svelte
blob: 0854f3a0d2cf67fdbdea8aee6a89bcce36aa7226 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  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 {
    // 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 { data, series, height = 340, width = $bindable(undefined), ariaLabel = '图表', legend = true }: Props = $props();

  let host: HTMLDivElement;
  let observer: ResizeObserver | null = null;
  let measured: number | undefined = $state(undefined);

  // 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;
  }

  onMount(() => {
    applyMeasure();
    if (typeof ResizeObserver !== 'undefined') {
      observer = new ResizeObserver(applyMeasure);
      observer.observe(host);
    }
  });
  onDestroy(() => observer?.disconnect());

  const lcSeries = $derived(
    series.map((s) => ({
      key: s.key,
      label: s.label ?? s.key,
      value: s.value,
      color: s.color
    }))
  );

  // 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>

<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>