@hsu-react/ui 2.4.1 → 2.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/es/components/Chart/Bar/index.js +9 -2
  2. package/es/components/Chart/Common/index.js +9 -2
  3. package/es/components/Chart/Gauge/index.js +9 -2
  4. package/es/components/Chart/Heatmap/index.js +9 -2
  5. package/es/components/Chart/Line/index.js +9 -2
  6. package/es/components/Chart/Pie/Pie3D/index.js +9 -2
  7. package/es/components/Chart/Pie/index.js +9 -2
  8. package/es/components/Chart/Polar/index.js +9 -2
  9. package/es/components/Chart/Radar/index.js +9 -2
  10. package/es/components/Chart/Sankey/index.js +9 -2
  11. package/es/components/Chart/Tree/index.js +9 -2
  12. package/es/components/Chart/_hooks/useContainerReady.d.ts +18 -0
  13. package/es/components/Chart/_hooks/useContainerReady.js +56 -0
  14. package/es/layout/NavTabBar/_hooks/useTabPath.js +45 -8
  15. package/lib/components/Chart/Bar/index.js +9 -2
  16. package/lib/components/Chart/Common/index.js +9 -2
  17. package/lib/components/Chart/Gauge/index.js +9 -2
  18. package/lib/components/Chart/Heatmap/index.js +9 -2
  19. package/lib/components/Chart/Line/index.js +9 -2
  20. package/lib/components/Chart/Pie/Pie3D/index.js +9 -2
  21. package/lib/components/Chart/Pie/index.js +9 -2
  22. package/lib/components/Chart/Polar/index.js +9 -2
  23. package/lib/components/Chart/Radar/index.js +9 -2
  24. package/lib/components/Chart/Sankey/index.js +9 -2
  25. package/lib/components/Chart/Tree/index.js +9 -2
  26. package/lib/components/Chart/_hooks/useContainerReady.d.ts +18 -0
  27. package/lib/components/Chart/_hooks/useContainerReady.js +51 -0
  28. package/lib/layout/NavTabBar/_hooks/useTabPath.js +42 -7
  29. package/package.json +1 -1
  30. package/src/components/Chart/Bar/index.tsx +10 -1
  31. package/src/components/Chart/Common/index.tsx +12 -2
  32. package/src/components/Chart/Gauge/index.tsx +12 -2
  33. package/src/components/Chart/Heatmap/index.tsx +12 -2
  34. package/src/components/Chart/Line/index.tsx +10 -1
  35. package/src/components/Chart/Pie/Pie3D/index.tsx +10 -1
  36. package/src/components/Chart/Pie/index.tsx +10 -1
  37. package/src/components/Chart/Polar/index.tsx +12 -2
  38. package/src/components/Chart/Radar/index.tsx +10 -1
  39. package/src/components/Chart/Sankey/index.tsx +12 -2
  40. package/src/components/Chart/Tree/index.tsx +12 -2
  41. package/src/components/Chart/_hooks/useContainerReady.ts +50 -0
  42. package/src/layout/NavTabBar/_hooks/useTabPath.ts +43 -10
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = require("react");
8
+ /**
9
+ * Whether the referenced element has a non-zero box yet.
10
+ *
11
+ * `echarts.init` on a 0×0 element logs `Can't get DOM width or height` and builds a canvas with no
12
+ * size, recovering only on the next `resize()`. A chart hits that path whenever it mounts before
13
+ * layout has given it space — inside a keep-alive tab that is not the active one, a collapsed
14
+ * panel, a modal that has not opened yet — so consuming apps see the warning for charts that end
15
+ * up rendering perfectly well.
16
+ *
17
+ * Gating `init` on this flag defers it to the first frame the element actually occupies space.
18
+ *
19
+ * It **latches**: once measured, the element going back to 0×0 (a keep-alive tab losing focus)
20
+ * must not read as "not ready" again, or the init effect would tear the chart down and re-create
21
+ * it on every tab switch.
22
+ */
23
+ const useContainerReady = ref => {
24
+ const [ready, setReady] = (0, _react.useState)(false);
25
+ (0, _react.useEffect)(() => {
26
+ if (ready) return;
27
+ const el = ref.current;
28
+ if (!el) return;
29
+ const measure = () => {
30
+ const {
31
+ width,
32
+ height
33
+ } = el.getBoundingClientRect();
34
+ if (width > 0 && height > 0) {
35
+ setReady(true);
36
+ return true;
37
+ }
38
+ return false;
39
+ };
40
+
41
+ // Already laid out on mount — the common case, and it must not cost an extra frame
42
+ if (measure()) return;
43
+ const observer = new ResizeObserver(() => {
44
+ if (measure()) observer.disconnect();
45
+ });
46
+ observer.observe(el);
47
+ return () => observer.disconnect();
48
+ }, [ready, ref]);
49
+ return ready;
50
+ };
51
+ var _default = exports.default = useContainerReady;
@@ -16,6 +16,17 @@ const useTabPath = ({
16
16
  affixRouter
17
17
  }) => {
18
18
  const location = (0, _reactRouter.useLocation)();
19
+
20
+ /**
21
+ * Keyed on the *contents* of `affixRouter`, not on the array itself.
22
+ *
23
+ * Callers pass an inline literal (`affixRouter={[HOME]}`), and omitting the prop hands the
24
+ * component a fresh `[]` default on every render — either way the array identity changes each
25
+ * time. Depending on it directly re-runs the effect below on every render, and that effect
26
+ * calls setState, so the whole thing becomes a render loop.
27
+ */
28
+ const affixKey = affixRouter.join("\u0001");
29
+ const affixSet = (0, _react.useMemo)(() => new Set(affixKey ? affixKey.split("\u0001") : []), [affixKey]);
19
30
  const [tabKey, setTabKey] = (0, _react.useState)("");
20
31
  const [openKeys, setOpenkeys] = (0, _react.useState)([]);
21
32
 
@@ -28,7 +39,7 @@ const useTabPath = ({
28
39
  _checkAffix(item.children);
29
40
  } else {
30
41
  // Check whether it is an affixed route
31
- const isAffix = affixRouter.includes(item.key) || item.affix;
42
+ const isAffix = affixSet.has(item.key) || item.affix;
32
43
  if (isAffix) {
33
44
  setOpenkeys(prev => {
34
45
  const find = prev.find(i => i.key === item.key);
@@ -40,7 +51,7 @@ const useTabPath = ({
40
51
  }
41
52
  }
42
53
  });
43
- }, [affixRouter]);
54
+ }, [affixSet]);
44
55
  const _checkPath = (0, _react.useCallback)((items, parents) => {
45
56
  const pathname = decodeURI(location.pathname);
46
57
  const search = location.search;
@@ -58,32 +69,44 @@ const useTabPath = ({
58
69
  // Handle param routes
59
70
  setTabKey(`${pathname}${search}`);
60
71
  setOpenkeys(prev => {
72
+ const nextKey = `${pathname}${search}`;
61
73
  const find = prev.find(i => i.key.split("?")[0] === pathname);
62
74
  if (find) {
75
+ // Already on this key: return `prev` untouched. `map` builds a new array
76
+ // every time, and React treats a new reference as a state change — which is
77
+ // the other half of the render loop above.
78
+ if (find.key === nextKey) {
79
+ return prev;
80
+ }
63
81
  return prev.map(i => i.key.split("?")[0] === pathname ? {
64
82
  ...i,
65
- key: `${pathname}${search}`
83
+ key: nextKey
66
84
  } : i);
67
85
  }
68
86
  return [...prev, {
69
87
  ...item,
70
- key: `${pathname}${search}`
88
+ key: nextKey
71
89
  }];
72
90
  });
73
91
  } else {
74
92
  // Handle plain routes
75
93
  setTabKey(`${item.key}${search}`);
76
94
  setOpenkeys(prev => {
95
+ const nextKey = `${item.key}${search}`;
77
96
  const find = prev.find(i => i.key.split("?")[0] === item.key);
78
97
  if (find) {
98
+ // Same as above: an unchanged tab must not produce a new reference
99
+ if (find.key === nextKey) {
100
+ return prev;
101
+ }
79
102
  return prev.map(i => i.key.split("?")[0] === item.key ? {
80
103
  ...i,
81
- key: `${item.key}${search}`
104
+ key: nextKey
82
105
  } : i);
83
106
  }
84
107
  return [...prev, {
85
108
  ...item,
86
- key: `${item.key}${search}`
109
+ key: nextKey
87
110
  }];
88
111
  });
89
112
  }
@@ -91,12 +114,24 @@ const useTabPath = ({
91
114
  }
92
115
  });
93
116
  }, [location.pathname, location.search]);
117
+
118
+ /**
119
+ * Closing the last tab navigates to `basePath` — but when that tab *was* `basePath`, the
120
+ * location never changes, so path matching would not re-run and the bar would be left empty
121
+ * while its page is still on screen. Going empty has to re-trigger the match on its own.
122
+ *
123
+ * This settles rather than looping: re-matching adds the tab back, which flips `isEmpty` and
124
+ * runs the effect once more, and that pass finds the tab already present and returns the same
125
+ * state object — so React stops there. (Only true because the setters below bail out when
126
+ * nothing changed; without that this would spin.)
127
+ */
128
+ const isEmpty = openKeys.length === 0;
94
129
  (0, _react.useEffect)(() => {
95
130
  // Handle affixed routes first to populate openKeys
96
131
  _checkAffix(items);
97
132
  // Then run path matching
98
133
  _checkPath(items);
99
- }, [_checkAffix, _checkPath, items]);
134
+ }, [_checkAffix, _checkPath, items, isEmpty]);
100
135
  return {
101
136
  tabKey,
102
137
  setTabKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hsu-react/ui",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
4
4
  "description": "一套基于 React + Ant Design 的中后台业务组件库",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,4 +1,5 @@
1
1
  import * as echarts from "echarts";
2
+ import useContainerReady from "../_hooks/useContainerReady";
2
3
 
3
4
  import {
4
5
  ChartCommonProps,
@@ -72,6 +73,8 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
72
73
  } = props;
73
74
  const chartRef = useRef<HTMLDivElement>(null);
74
75
  const chartInstanceRef = useRef<echarts.ECharts | null>(null);
76
+ // Defers `echarts.init` until the container has a box — see the hook for why
77
+ const containerReady = useContainerReady(chartRef);
75
78
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
76
79
  const prevXAxisKeyRef = useRef<string | null>(null);
77
80
  const legendScrollRef = useRef<ReturnType<typeof autoScrollLegend> | null>(
@@ -269,12 +272,17 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
269
272
 
270
273
  // Callback for handling chart resize
271
274
  const handleResize = useCallback(() => {
275
+ // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
276
+ // the laid-out canvas away and it has to be rebuilt when the tab comes back
277
+ const el = chartRef.current;
278
+ if (!el || el.clientWidth === 0 || el.clientHeight === 0) return;
279
+
272
280
  chartInstanceRef.current?.resize();
273
281
  }, []);
274
282
 
275
283
  // Initialize the chart
276
284
  useEffect(() => {
277
- if (!chartRef.current) return;
285
+ if (!chartRef.current || !containerReady) return;
278
286
 
279
287
  // Initialize or reuse the existing instance
280
288
  let chart = chartInstanceRef.current;
@@ -469,6 +477,7 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
469
477
  legendScrollInterval,
470
478
  // 图例是否接管滚轮由它推导,漏了会拿到过期的滚动配置
471
479
  normalizedScroll,
480
+ containerReady,
472
481
  ]);
473
482
 
474
483
  // Handle auto play
@@ -1,4 +1,5 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import useContainerReady from "../_hooks/useContainerReady";
2
3
  import styles from "../index.module.scss";
3
4
  import { ChartCommonProps, ChartOptionType, ChartsOption } from "..";
4
5
  import * as echarts from "echarts";
@@ -7,6 +8,8 @@ const Common: React.FC<ChartCommonProps> = (props) => {
7
8
  const { className, style, onChart, ...coreOption } = props;
8
9
  const chartRef = useRef<HTMLDivElement>(null);
9
10
  const chartInstanceRef = useRef<echarts.ECharts | null>(null);
11
+ // Defers `echarts.init` until the container has a box — see the hook for why
12
+ const containerReady = useContainerReady(chartRef);
10
13
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
11
14
 
12
15
  // Cache the chart configuration with useMemo
@@ -20,12 +23,17 @@ const Common: React.FC<ChartCommonProps> = (props) => {
20
23
 
21
24
  // Callback that handles chart resize
22
25
  const handleResize = useCallback(() => {
26
+ // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
27
+ // the laid-out canvas away and it has to be rebuilt when the tab comes back
28
+ const el = chartRef.current;
29
+ if (!el || el.clientWidth === 0 || el.clientHeight === 0) return;
30
+
23
31
  chartInstanceRef.current?.resize();
24
32
  }, []);
25
33
 
26
34
  // Initialize the chart
27
35
  useEffect(() => {
28
- if (!chartRef.current) return;
36
+ if (!chartRef.current || !containerReady) return;
29
37
 
30
38
  // Initialize or reuse an existing instance
31
39
  let chart = chartInstanceRef.current;
@@ -53,7 +61,9 @@ const Common: React.FC<ChartCommonProps> = (props) => {
53
61
  return () => {
54
62
  window.removeEventListener("resize", handleResize);
55
63
  };
56
- }, [chartOption, handleResize, onChart]);
64
+ }, [chartOption, handleResize, onChart,
65
+ containerReady,
66
+ ]);
57
67
 
58
68
  // Clean up resources on component unmount
59
69
  useEffect(() => {
@@ -1,4 +1,5 @@
1
1
  import * as echarts from "echarts";
2
+ import useContainerReady from "../_hooks/useContainerReady";
2
3
 
3
4
  import {
4
5
  ChartCommonProps,
@@ -31,6 +32,8 @@ const Gauge: React.FC<ChartGaugeProps> = (props) => {
31
32
  } = series as echarts.GaugeSeriesOption;
32
33
  const chartRef = useRef<HTMLDivElement>(null);
33
34
  const chartInstanceRef = useRef<echarts.ECharts | null>(null);
35
+ // Defers `echarts.init` until the container has a box — see the hook for why
36
+ const containerReady = useContainerReady(chartRef);
34
37
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
35
38
 
36
39
  // Cache the chart option with useMemo
@@ -104,12 +107,17 @@ const Gauge: React.FC<ChartGaugeProps> = (props) => {
104
107
 
105
108
  // Callback for handling chart resize
106
109
  const handleResize = useCallback(() => {
110
+ // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
111
+ // the laid-out canvas away and it has to be rebuilt when the tab comes back
112
+ const el = chartRef.current;
113
+ if (!el || el.clientWidth === 0 || el.clientHeight === 0) return;
114
+
107
115
  chartInstanceRef.current?.resize();
108
116
  }, []);
109
117
 
110
118
  // Initialize the chart
111
119
  useEffect(() => {
112
- if (!chartRef.current) return;
120
+ if (!chartRef.current || !containerReady) return;
113
121
 
114
122
  // Initialize or reuse the existing instance
115
123
  let chart = chartInstanceRef.current;
@@ -134,7 +142,9 @@ const Gauge: React.FC<ChartGaugeProps> = (props) => {
134
142
  return () => {
135
143
  window.removeEventListener("resize", handleResize);
136
144
  };
137
- }, [chartOption, handleResize]);
145
+ }, [chartOption, handleResize,
146
+ containerReady,
147
+ ]);
138
148
 
139
149
  // Clean up resources when the component unmounts
140
150
  useEffect(() => {
@@ -1,4 +1,5 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import useContainerReady from "../_hooks/useContainerReady";
2
3
  import styles from "../index.module.scss";
3
4
  import { ChartCommonProps, ChartOptionType, ChartsOption } from "..";
4
5
  import * as echarts from "echarts";
@@ -74,6 +75,8 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
74
75
  } = props;
75
76
  const chartRef = useRef<HTMLDivElement>(null);
76
77
  const chartInstanceRef = useRef<echarts.ECharts | null>(null);
78
+ // Defers `echarts.init` until the container has a box — see the hook for why
79
+ const containerReady = useContainerReady(chartRef);
77
80
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
78
81
  // echarts options are plain JS and cannot read the `--vita-*` variables, so the axis and legend
79
82
  // colours have to be resolved against the active appearance here
@@ -255,12 +258,17 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
255
258
 
256
259
  // Callback for handling chart resize
257
260
  const handleResize = useCallback(() => {
261
+ // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
262
+ // the laid-out canvas away and it has to be rebuilt when the tab comes back
263
+ const el = chartRef.current;
264
+ if (!el || el.clientWidth === 0 || el.clientHeight === 0) return;
265
+
258
266
  chartInstanceRef.current?.resize();
259
267
  }, []);
260
268
 
261
269
  // Initialize the chart
262
270
  useEffect(() => {
263
- if (!chartRef.current) return;
271
+ if (!chartRef.current || !containerReady) return;
264
272
 
265
273
  // Initialize or reuse the existing instance
266
274
  let chart = chartInstanceRef.current;
@@ -296,7 +304,9 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
296
304
  chartInstanceRef.current?.off("click", onClick);
297
305
  }
298
306
  };
299
- }, [chartOption, handleResize, onChart, onClick]);
307
+ }, [chartOption, handleResize, onChart, onClick,
308
+ containerReady,
309
+ ]);
300
310
 
301
311
  // Clean up resources when the component unmounts
302
312
  useEffect(() => {
@@ -1,4 +1,5 @@
1
1
  import * as echarts from "echarts";
2
+ import useContainerReady from "../_hooks/useContainerReady";
2
3
 
3
4
  import {
4
5
  ChartCommonProps,
@@ -73,6 +74,8 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
73
74
  } = props;
74
75
  const chartRef = useRef<HTMLDivElement>(null);
75
76
  const chartInstanceRef = useRef<echarts.ECharts | null>(null);
77
+ // Defers `echarts.init` until the container has a box — see the hook for why
78
+ const containerReady = useContainerReady(chartRef);
76
79
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
77
80
  const prevXAxisKeyRef = useRef<string | null>(null);
78
81
  const legendScrollRef = useRef<ReturnType<typeof autoScrollLegend> | null>(
@@ -267,12 +270,17 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
267
270
 
268
271
  // Callback for handling chart resize
269
272
  const handleResize = useCallback(() => {
273
+ // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
274
+ // the laid-out canvas away and it has to be rebuilt when the tab comes back
275
+ const el = chartRef.current;
276
+ if (!el || el.clientWidth === 0 || el.clientHeight === 0) return;
277
+
270
278
  chartInstanceRef.current?.resize();
271
279
  }, []);
272
280
 
273
281
  // Initialize the chart
274
282
  useEffect(() => {
275
- if (!chartRef.current) return;
283
+ if (!chartRef.current || !containerReady) return;
276
284
 
277
285
  // Initialize or reuse the existing instance
278
286
  let chart = chartInstanceRef.current;
@@ -467,6 +475,7 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
467
475
  legendScrollInterval,
468
476
  // 图例是否接管滚轮由它推导,漏了会拿到过期的滚动配置
469
477
  normalizedScroll,
478
+ containerReady,
470
479
  ]);
471
480
 
472
481
  // Handle auto play
@@ -1,4 +1,5 @@
1
1
  import * as echarts from "echarts";
2
+ import useContainerReady from "../../_hooks/useContainerReady";
2
3
  import "echarts-gl";
3
4
  import React, { useCallback, useEffect, useMemo, useRef } from "react";
4
5
 
@@ -108,6 +109,8 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
108
109
 
109
110
  const chartRef = useRef<HTMLDivElement>(null);
110
111
  const chartInstanceRef = useRef<echarts.ECharts | null>(null);
112
+ // Defers `echarts.init` until the container has a box — see the hook for why
113
+ const containerReady = useContainerReady(chartRef);
111
114
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
112
115
  const hoveredIndexRef = useRef<number | "">("");
113
116
  const optionRef = useRef<ChartsOption | null>(null);
@@ -145,6 +148,11 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
145
148
  ]);
146
149
 
147
150
  const handleResize = useCallback(() => {
151
+ // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
152
+ // the laid-out canvas away and it has to be rebuilt when the tab comes back
153
+ const el = chartRef.current;
154
+ if (!el || el.clientWidth === 0 || el.clientHeight === 0) return;
155
+
148
156
  chartInstanceRef.current?.resize();
149
157
  }, []);
150
158
 
@@ -163,7 +171,7 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
163
171
  );
164
172
 
165
173
  useEffect(() => {
166
- if (!chartRef.current) return;
174
+ if (!chartRef.current || !containerReady) return;
167
175
 
168
176
  let chart = chartInstanceRef.current;
169
177
  if (!chart) {
@@ -205,6 +213,7 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
205
213
  handleGlobalOut,
206
214
  onChart,
207
215
  onClick,
216
+ containerReady,
208
217
  ]);
209
218
 
210
219
  useEffect(() => {
@@ -1,4 +1,5 @@
1
1
  import * as echarts from "echarts";
2
+ import useContainerReady from "../_hooks/useContainerReady";
2
3
 
3
4
  import {
4
5
  ChartCommonProps,
@@ -66,6 +67,8 @@ const ChartPie: ChartPieFC = (props) => {
66
67
  } = props;
67
68
  const chartRef = useRef<HTMLDivElement>(null);
68
69
  const chartInstanceRef = useRef<echarts.ECharts | null>(null);
70
+ // Defers `echarts.init` until the container has a box — see the hook for why
71
+ const containerReady = useContainerReady(chartRef);
69
72
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
70
73
  const legendScrollRef = useRef<ReturnType<typeof autoScrollLegend> | null>(
71
74
  null
@@ -241,12 +244,17 @@ const ChartPie: ChartPieFC = (props) => {
241
244
 
242
245
  // Callback for handling chart resize
243
246
  const handleResize = useCallback(() => {
247
+ // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
248
+ // the laid-out canvas away and it has to be rebuilt when the tab comes back
249
+ const el = chartRef.current;
250
+ if (!el || el.clientWidth === 0 || el.clientHeight === 0) return;
251
+
244
252
  chartInstanceRef.current?.resize();
245
253
  }, []);
246
254
 
247
255
  // Initialize the chart
248
256
  useEffect(() => {
249
- if (!chartRef.current) return;
257
+ if (!chartRef.current || !containerReady) return;
250
258
 
251
259
  // Initialize or reuse the existing instance
252
260
  let chart = chartInstanceRef.current;
@@ -313,6 +321,7 @@ const ChartPie: ChartPieFC = (props) => {
313
321
  seriesData,
314
322
  legendVisibleCount,
315
323
  legendScrollInterval,
324
+ containerReady,
316
325
  ]);
317
326
 
318
327
  // Clean up resources when the component unmounts
@@ -1,4 +1,5 @@
1
1
  import * as echarts from "echarts";
2
+ import useContainerReady from "../_hooks/useContainerReady";
2
3
  import {
3
4
  ChartCommonProps,
4
5
  ChartOptionType,
@@ -36,6 +37,8 @@ const Polar: React.FC<ChartPolarProps> = (props) => {
36
37
  } = props;
37
38
  const chartRef = useRef<HTMLDivElement>(null);
38
39
  const chartInstanceRef = useRef<echarts.ECharts | null>(null);
40
+ // Defers `echarts.init` until the container has a box — see the hook for why
41
+ const containerReady = useContainerReady(chartRef);
39
42
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
40
43
 
41
44
  // Cache the chart option with useMemo
@@ -170,12 +173,17 @@ const Polar: React.FC<ChartPolarProps> = (props) => {
170
173
 
171
174
  // Callback handling chart resize
172
175
  const handleResize = useCallback(() => {
176
+ // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
177
+ // the laid-out canvas away and it has to be rebuilt when the tab comes back
178
+ const el = chartRef.current;
179
+ if (!el || el.clientWidth === 0 || el.clientHeight === 0) return;
180
+
173
181
  chartInstanceRef.current?.resize();
174
182
  }, []);
175
183
 
176
184
  // Initialize the chart
177
185
  useEffect(() => {
178
- if (!chartRef.current) return;
186
+ if (!chartRef.current || !containerReady) return;
179
187
 
180
188
  // Initialize a new instance or reuse the existing one
181
189
  let chart = chartInstanceRef.current;
@@ -200,7 +208,9 @@ const Polar: React.FC<ChartPolarProps> = (props) => {
200
208
  return () => {
201
209
  window.removeEventListener("resize", handleResize);
202
210
  };
203
- }, [chartOption, handleResize]);
211
+ }, [chartOption, handleResize,
212
+ containerReady,
213
+ ]);
204
214
 
205
215
  // Clean up resources on unmount
206
216
  useEffect(() => {
@@ -1,4 +1,5 @@
1
1
  import * as echarts from "echarts";
2
+ import useContainerReady from "../_hooks/useContainerReady";
2
3
  import { ChartCommonProps, ChartOptionType, ChartsOption } from "..";
3
4
  import React, { useCallback, useEffect, useMemo, useRef } from "react";
4
5
  import styles from "../index.module.scss";
@@ -55,6 +56,8 @@ const Radar: React.FC<ChartRadarProps> = (props) => {
55
56
 
56
57
  const chartRef = useRef<HTMLDivElement>(null);
57
58
  const chartInstanceRef = useRef<echarts.ECharts | null>(null);
59
+ // Defers `echarts.init` until the container has a box — see the hook for why
60
+ const containerReady = useContainerReady(chartRef);
58
61
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
59
62
  const hoveredIndicatorRef = useRef<number>(-1);
60
63
 
@@ -188,11 +191,16 @@ const Radar: React.FC<ChartRadarProps> = (props) => {
188
191
  ]);
189
192
 
190
193
  const handleResize = useCallback(() => {
194
+ // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
195
+ // the laid-out canvas away and it has to be rebuilt when the tab comes back
196
+ const el = chartRef.current;
197
+ if (!el || el.clientWidth === 0 || el.clientHeight === 0) return;
198
+
191
199
  chartInstanceRef.current?.resize();
192
200
  }, []);
193
201
 
194
202
  useEffect(() => {
195
- if (!chartRef.current) return;
203
+ if (!chartRef.current || !containerReady) return;
196
204
 
197
205
  let chart = chartInstanceRef.current;
198
206
  if (!chart) {
@@ -343,6 +351,7 @@ const Radar: React.FC<ChartRadarProps> = (props) => {
343
351
  singleIndicatorHighlight,
344
352
  singleIndicatorHoverFill,
345
353
  color,
354
+ containerReady,
346
355
  ]);
347
356
 
348
357
  useEffect(() => {
@@ -1,4 +1,5 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import useContainerReady from "../_hooks/useContainerReady";
2
3
  import styles from "../index.module.scss";
3
4
  import { ChartCommonProps, ChartOptionType, ChartsOption } from "..";
4
5
  import * as echarts from "echarts";
@@ -16,6 +17,8 @@ const Sankey: React.FC<ChartSankeyProps> = (props) => {
16
17
  const { className, style, seriesData, seriesLinks, series, getImage } = props;
17
18
  const chartRef = useRef<HTMLDivElement>(null);
18
19
  const chartInstanceRef = useRef<echarts.ECharts | null>(null);
20
+ // Defers `echarts.init` until the container has a box — see the hook for why
21
+ const containerReady = useContainerReady(chartRef);
19
22
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
20
23
 
21
24
  // Cache the chart option with useMemo
@@ -100,12 +103,17 @@ const Sankey: React.FC<ChartSankeyProps> = (props) => {
100
103
 
101
104
  // Callback handling chart resize
102
105
  const handleResize = useCallback(() => {
106
+ // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
107
+ // the laid-out canvas away and it has to be rebuilt when the tab comes back
108
+ const el = chartRef.current;
109
+ if (!el || el.clientWidth === 0 || el.clientHeight === 0) return;
110
+
103
111
  chartInstanceRef.current?.resize();
104
112
  }, []);
105
113
 
106
114
  // Initialize the chart
107
115
  useEffect(() => {
108
- if (!chartRef.current) return;
116
+ if (!chartRef.current || !containerReady) return;
109
117
 
110
118
  // Initialize a new instance or reuse the existing one
111
119
  let chart = chartInstanceRef.current;
@@ -141,7 +149,9 @@ const Sankey: React.FC<ChartSankeyProps> = (props) => {
141
149
  return () => {
142
150
  window.removeEventListener("resize", handleResize);
143
151
  };
144
- }, [chartOption, handleResize, getImage]);
152
+ }, [chartOption, handleResize, getImage,
153
+ containerReady,
154
+ ]);
145
155
 
146
156
  // Clean up resources on unmount
147
157
  useEffect(() => {
@@ -1,4 +1,5 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import useContainerReady from "../_hooks/useContainerReady";
2
3
  import styles from "../index.module.scss";
3
4
  import {
4
5
  ChartCommonProps,
@@ -19,6 +20,8 @@ const Tree: React.FC<ChartTreeProps> = (props) => {
19
20
  const { className, style, seriesData, series, getImage } = props;
20
21
  const chartRef = useRef<HTMLDivElement>(null);
21
22
  const chartInstanceRef = useRef<echarts.ECharts | null>(null);
23
+ // Defers `echarts.init` until the container has a box — see the hook for why
24
+ const containerReady = useContainerReady(chartRef);
22
25
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
23
26
 
24
27
  // Cache the chart option with useMemo
@@ -40,12 +43,17 @@ const Tree: React.FC<ChartTreeProps> = (props) => {
40
43
 
41
44
  // Callback handling chart resize
42
45
  const handleResize = useCallback(() => {
46
+ // A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
47
+ // the laid-out canvas away and it has to be rebuilt when the tab comes back
48
+ const el = chartRef.current;
49
+ if (!el || el.clientWidth === 0 || el.clientHeight === 0) return;
50
+
43
51
  chartInstanceRef.current?.resize();
44
52
  }, []);
45
53
 
46
54
  // Initialize the chart
47
55
  useEffect(() => {
48
- if (!chartRef.current) return;
56
+ if (!chartRef.current || !containerReady) return;
49
57
 
50
58
  // Initialize a new instance or reuse the existing one
51
59
  let chart = chartInstanceRef.current;
@@ -81,7 +89,9 @@ const Tree: React.FC<ChartTreeProps> = (props) => {
81
89
  return () => {
82
90
  window.removeEventListener("resize", handleResize);
83
91
  };
84
- }, [chartOption, handleResize, getImage]);
92
+ }, [chartOption, handleResize, getImage,
93
+ containerReady,
94
+ ]);
85
95
 
86
96
  // Clean up resources on unmount
87
97
  useEffect(() => {