summaryrefslogtreecommitdiff
path: root/frontend/src/components/Chart.svelte
blob: ca8f4f89cfe6628541b7bf8ef9fe39bedc4297f5 (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
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import type * as echartsNS from 'echarts';

  interface Props {
    option: echartsNS.EChartsOption;
    height?: number;
    ariaLabel?: string;
  }
  let { option, height = 360, ariaLabel = '图表' }: Props = $props();

  let host: HTMLDivElement;
  let chart: echartsNS.ECharts | null = null;
  let observer: ResizeObserver | null = null;

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

  onDestroy(() => {
    observer?.disconnect();
    chart?.dispose();
    chart = null;
  });

  $effect(() => {
    chart?.setOption(option, { notMerge: true });
  });
</script>

<div
  bind:this={host}
  style="height:{height}px; width:100%; min-width:0;"
  role="img"
  aria-label={ariaLabel}
></div>

<style>
  div {
    display: block;
  }
</style>