@hsu-react/ui 2.4.0 → 2.4.2

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 (57) hide show
  1. package/es/components/Chart/Bar/index.js +21 -7
  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 +21 -6
  5. package/es/components/Chart/Line/index.js +21 -6
  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/components/Chart/_utils/cartesian.d.ts +3 -2
  15. package/es/components/Chart/_utils/cartesian.js +9 -10
  16. package/es/components/Chart/_utils/chartTheme.d.ts +22 -0
  17. package/es/components/Chart/_utils/chartTheme.js +23 -0
  18. package/es/components/Chart/_utils/heatmap.d.ts +3 -2
  19. package/es/components/Chart/_utils/heatmap.js +7 -10
  20. package/es/components/Panel/ListPanel/ListModalPanel/index.module.scss +10 -0
  21. package/lib/components/Chart/Bar/index.js +19 -7
  22. package/lib/components/Chart/Common/index.js +9 -2
  23. package/lib/components/Chart/Gauge/index.js +9 -2
  24. package/lib/components/Chart/Heatmap/index.js +19 -6
  25. package/lib/components/Chart/Line/index.js +19 -6
  26. package/lib/components/Chart/Pie/Pie3D/index.js +9 -2
  27. package/lib/components/Chart/Pie/index.js +9 -2
  28. package/lib/components/Chart/Polar/index.js +9 -2
  29. package/lib/components/Chart/Radar/index.js +9 -2
  30. package/lib/components/Chart/Sankey/index.js +9 -2
  31. package/lib/components/Chart/Tree/index.js +9 -2
  32. package/lib/components/Chart/_hooks/useContainerReady.d.ts +18 -0
  33. package/lib/components/Chart/_hooks/useContainerReady.js +51 -0
  34. package/lib/components/Chart/_utils/cartesian.d.ts +3 -2
  35. package/lib/components/Chart/_utils/cartesian.js +9 -11
  36. package/lib/components/Chart/_utils/chartTheme.d.ts +22 -0
  37. package/lib/components/Chart/_utils/chartTheme.js +29 -0
  38. package/lib/components/Chart/_utils/heatmap.d.ts +3 -2
  39. package/lib/components/Chart/_utils/heatmap.js +7 -10
  40. package/lib/components/Panel/ListPanel/ListModalPanel/index.module.scss +10 -0
  41. package/package.json +1 -1
  42. package/src/components/Chart/Bar/index.tsx +20 -5
  43. package/src/components/Chart/Common/index.tsx +12 -2
  44. package/src/components/Chart/Gauge/index.tsx +12 -2
  45. package/src/components/Chart/Heatmap/index.tsx +22 -5
  46. package/src/components/Chart/Line/index.tsx +20 -4
  47. package/src/components/Chart/Pie/Pie3D/index.tsx +10 -1
  48. package/src/components/Chart/Pie/index.tsx +10 -1
  49. package/src/components/Chart/Polar/index.tsx +12 -2
  50. package/src/components/Chart/Radar/index.tsx +10 -1
  51. package/src/components/Chart/Sankey/index.tsx +12 -2
  52. package/src/components/Chart/Tree/index.tsx +12 -2
  53. package/src/components/Chart/_hooks/useContainerReady.ts +50 -0
  54. package/src/components/Chart/_utils/cartesian.ts +11 -10
  55. package/src/components/Chart/_utils/chartTheme.ts +34 -0
  56. package/src/components/Chart/_utils/heatmap.ts +10 -10
  57. package/src/components/Panel/ListPanel/ListModalPanel/index.module.scss +10 -0
@@ -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";
@@ -6,6 +7,8 @@ import {
6
7
  createDefaultHeatmapXAxis,
7
8
  createDefaultHeatmapYAxis,
8
9
  } from "../_utils/heatmap";
10
+ import { resolveChartChrome } from "../_utils/chartTheme";
11
+ import useIsDark from "../../../hooks/useIsDark";
9
12
 
10
13
  export interface HeatmapDataItem {
11
14
  /** X-axis index or name */
@@ -72,7 +75,13 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
72
75
  } = props;
73
76
  const chartRef = useRef<HTMLDivElement>(null);
74
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);
75
80
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
81
+ // echarts options are plain JS and cannot read the `--vita-*` variables, so the axis and legend
82
+ // colours have to be resolved against the active appearance here
83
+ const isDark = useIsDark();
84
+ const chrome = useMemo(() => resolveChartChrome(isDark), [isDark]);
76
85
 
77
86
  // Process the data and generate the chart option
78
87
  const chartOption = useMemo(() => {
@@ -120,7 +129,7 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
120
129
  left: "right",
121
130
  top: "center",
122
131
  textStyle: {
123
- color: "#373D48",
132
+ color: chrome.text,
124
133
  },
125
134
  inRange: {
126
135
  color: inRangeColor,
@@ -128,7 +137,7 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
128
137
  };
129
138
 
130
139
  // Process xAxis config
131
- const def_xAxis = createDefaultHeatmapXAxis(xAxisData);
140
+ const def_xAxis = createDefaultHeatmapXAxis(xAxisData, chrome);
132
141
  const processedXAxis = Array.isArray(xAxis)
133
142
  ? xAxis?.map((item) => ({
134
143
  ...def_xAxis,
@@ -142,7 +151,7 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
142
151
  };
143
152
 
144
153
  // Process yAxis config
145
- const def_yAxis = createDefaultHeatmapYAxis(yAxisData);
154
+ const def_yAxis = createDefaultHeatmapYAxis(yAxisData, chrome);
146
155
  const processedYAxis = Array.isArray(yAxis)
147
156
  ? yAxis?.map((item) => ({
148
157
  ...def_yAxis,
@@ -244,16 +253,22 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
244
253
  series,
245
254
  coreOption,
246
255
  zeroColor,
256
+ chrome,
247
257
  ]);
248
258
 
249
259
  // Callback for handling chart resize
250
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
+
251
266
  chartInstanceRef.current?.resize();
252
267
  }, []);
253
268
 
254
269
  // Initialize the chart
255
270
  useEffect(() => {
256
- if (!chartRef.current) return;
271
+ if (!chartRef.current || !containerReady) return;
257
272
 
258
273
  // Initialize or reuse the existing instance
259
274
  let chart = chartInstanceRef.current;
@@ -289,7 +304,9 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
289
304
  chartInstanceRef.current?.off("click", onClick);
290
305
  }
291
306
  };
292
- }, [chartOption, handleResize, onChart, onClick]);
307
+ }, [chartOption, handleResize, onChart, onClick,
308
+ containerReady,
309
+ ]);
293
310
 
294
311
  // Clean up resources when the component unmounts
295
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,
@@ -21,6 +22,8 @@ import {
21
22
  percentWindowToIndexWindow,
22
23
  resolveScrollConfig,
23
24
  } from "../_utils/cartesian";
25
+ import { resolveChartChrome } from "../_utils/chartTheme";
26
+ import useIsDark from "../../../hooks/useIsDark";
24
27
 
25
28
  export interface ChartLineProps extends ChartCommonProps {
26
29
  chartTitle?: string;
@@ -71,6 +74,8 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
71
74
  } = props;
72
75
  const chartRef = useRef<HTMLDivElement>(null);
73
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);
74
79
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
75
80
  const prevXAxisKeyRef = useRef<string | null>(null);
76
81
  const legendScrollRef = useRef<ReturnType<typeof autoScrollLegend> | null>(
@@ -80,11 +85,15 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
80
85
  () => resolveScrollConfig(scrollConfig),
81
86
  [scrollConfig],
82
87
  );
88
+ // echarts options are plain JS and cannot read the `--vita-*` variables, so the axis and
89
+ // legend colours have to be resolved against the active appearance here
90
+ const isDark = useIsDark();
91
+ const chrome = useMemo(() => resolveChartChrome(isDark), [isDark]);
83
92
 
84
93
  // Cache the chart option with useMemo
85
94
  const chartOption = useMemo(() => {
86
- const def_xAxis = createDefaultCategoryXAxis(xAxisData);
87
- const def_yAxis = createDefaultValueYAxis("line");
95
+ const def_xAxis = createDefaultCategoryXAxis(xAxisData, chrome);
96
+ const def_yAxis = createDefaultValueYAxis("line", chrome);
88
97
 
89
98
  // Process xAxis config
90
99
  const processedXAxis = Array.isArray(xAxis)
@@ -206,7 +215,7 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
206
215
  top: "5%",
207
216
  icon: "circle",
208
217
  textStyle: {
209
- color: "#373D48",
218
+ color: chrome.text,
210
219
  },
211
220
  itemWidth: 8,
212
221
  itemHeight: 8,
@@ -256,16 +265,22 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
256
265
  series,
257
266
  enableLegendAutoScroll,
258
267
  coreOption,
268
+ chrome,
259
269
  ]);
260
270
 
261
271
  // Callback for handling chart resize
262
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
+
263
278
  chartInstanceRef.current?.resize();
264
279
  }, []);
265
280
 
266
281
  // Initialize the chart
267
282
  useEffect(() => {
268
- if (!chartRef.current) return;
283
+ if (!chartRef.current || !containerReady) return;
269
284
 
270
285
  // Initialize or reuse the existing instance
271
286
  let chart = chartInstanceRef.current;
@@ -460,6 +475,7 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
460
475
  legendScrollInterval,
461
476
  // 图例是否接管滚轮由它推导,漏了会拿到过期的滚动配置
462
477
  normalizedScroll,
478
+ containerReady,
463
479
  ]);
464
480
 
465
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(() => {
@@ -0,0 +1,50 @@
1
+ import { RefObject, useEffect, useState } from "react";
2
+
3
+ /**
4
+ * Whether the referenced element has a non-zero box yet.
5
+ *
6
+ * `echarts.init` on a 0×0 element logs `Can't get DOM width or height` and builds a canvas with no
7
+ * size, recovering only on the next `resize()`. A chart hits that path whenever it mounts before
8
+ * layout has given it space — inside a keep-alive tab that is not the active one, a collapsed
9
+ * panel, a modal that has not opened yet — so consuming apps see the warning for charts that end
10
+ * up rendering perfectly well.
11
+ *
12
+ * Gating `init` on this flag defers it to the first frame the element actually occupies space.
13
+ *
14
+ * It **latches**: once measured, the element going back to 0×0 (a keep-alive tab losing focus)
15
+ * must not read as "not ready" again, or the init effect would tear the chart down and re-create
16
+ * it on every tab switch.
17
+ */
18
+ const useContainerReady = (ref: RefObject<HTMLElement | null>): boolean => {
19
+ const [ready, setReady] = useState(false);
20
+
21
+ useEffect(() => {
22
+ if (ready) return;
23
+
24
+ const el = ref.current;
25
+ if (!el) return;
26
+
27
+ const measure = (): boolean => {
28
+ const { width, height } = el.getBoundingClientRect();
29
+ if (width > 0 && height > 0) {
30
+ setReady(true);
31
+ return true;
32
+ }
33
+ return false;
34
+ };
35
+
36
+ // Already laid out on mount — the common case, and it must not cost an extra frame
37
+ if (measure()) return;
38
+
39
+ const observer = new ResizeObserver(() => {
40
+ if (measure()) observer.disconnect();
41
+ });
42
+ observer.observe(el);
43
+
44
+ return () => observer.disconnect();
45
+ }, [ready, ref]);
46
+
47
+ return ready;
48
+ };
49
+
50
+ export default useContainerReady;
@@ -1,5 +1,6 @@
1
1
  import * as echarts from "echarts";
2
2
  import { ChartOptionType, SeriesData, SeriesDataType } from "..";
3
+ import { ChartChrome } from "./chartTheme";
3
4
 
4
5
  export interface ChartScrollConfig {
5
6
  enabled?: boolean;
@@ -50,7 +51,8 @@ export const percentWindowToIndexWindow = (
50
51
  };
51
52
 
52
53
  export const createDefaultCategoryXAxis = (
53
- xAxisData?: Array<string>,
54
+ xAxisData: Array<string> | undefined,
55
+ chrome: ChartChrome,
54
56
  ): ChartOptionType => ({
55
57
  type: "category",
56
58
  boundaryGap: true,
@@ -58,10 +60,10 @@ export const createDefaultCategoryXAxis = (
58
60
  axisLabel: {
59
61
  interval: 0,
60
62
  hideOverlap: true,
61
- textStyle: {
62
- fontSize: 14,
63
- color: "#373D48",
64
- },
63
+ // echarts removed the `textStyle` nesting under `axisLabel` in v4: keeping it means both
64
+ // properties are silently dropped *and* a deprecation warning is logged on every render.
65
+ fontSize: chrome.fontSize,
66
+ color: chrome.axis,
65
67
  },
66
68
  axisTick: {
67
69
  show: false,
@@ -70,19 +72,18 @@ export const createDefaultCategoryXAxis = (
70
72
 
71
73
  export const createDefaultValueYAxis = (
72
74
  mode: "bar" | "line" = "bar",
75
+ chrome: ChartChrome,
73
76
  ): ChartOptionType => ({
74
77
  type: "value",
75
78
  axisLabel: {
76
- textStyle: {
77
- fontSize: 14,
78
- color: "#373D48",
79
- },
79
+ fontSize: chrome.fontSize,
80
+ color: chrome.axis,
80
81
  },
81
82
  splitLine:
82
83
  mode === "line"
83
84
  ? {
84
85
  lineStyle: {
85
- color: "#C9CED6",
86
+ color: chrome.splitLine,
86
87
  type: "dashed",
87
88
  },
88
89
  }
@@ -0,0 +1,34 @@
1
+ import { darkTokens, fontTokens, lightTokens } from "../../../styles/tokens";
2
+
3
+ /**
4
+ * Colours for the chart *chrome* — axis ticks, split lines, legend labels.
5
+ *
6
+ * echarts options are plain JS, so they cannot read the `--vita-*` CSS variables the rest of the
7
+ * library styles itself with; the literal values have to be resolved here instead. These used to
8
+ * be hard-coded to a light-mode slate (`#373D48` / `#C9CED6`), which left every axis and legend
9
+ * nearly invisible once the page switched to the dark palette.
10
+ *
11
+ * The plotted data is deliberately *not* covered here: series colours come from echarts' own
12
+ * palette or from whatever the caller passes as `color`, and both read fine on either background.
13
+ */
14
+ export interface ChartChrome {
15
+ /** Legend labels — a real label, so it sits on the foreground ramp */
16
+ text: string;
17
+ /** Axis tick labels — secondary information, one step down */
18
+ axis: string;
19
+ /** Split lines behind the plot area */
20
+ splitLine: string;
21
+ /** Base font size, same scale the rest of the library uses */
22
+ fontSize: number;
23
+ }
24
+
25
+ export const resolveChartChrome = (dark: boolean): ChartChrome => {
26
+ const t = dark ? darkTokens : lightTokens;
27
+
28
+ return {
29
+ text: t.foreground,
30
+ axis: t.mutedForeground,
31
+ splitLine: t.border,
32
+ fontSize: fontTokens.size,
33
+ };
34
+ };
@@ -1,7 +1,9 @@
1
1
  import { ChartOptionType } from "..";
2
+ import { ChartChrome } from "./chartTheme";
2
3
 
3
4
  export const createDefaultHeatmapXAxis = (
4
- xAxisData?: string[],
5
+ xAxisData: string[] | undefined,
6
+ chrome: ChartChrome,
5
7
  ): ChartOptionType => ({
6
8
  type: "category",
7
9
  data: xAxisData,
@@ -11,10 +13,9 @@ export const createDefaultHeatmapXAxis = (
11
13
  axisLabel: {
12
14
  interval: 0,
13
15
  hideOverlap: true,
14
- textStyle: {
15
- fontSize: 14,
16
- color: "#373D48",
17
- },
16
+ // See the note in `cartesian.ts`: `axisLabel.textStyle` has been deprecated since echarts 4
17
+ fontSize: chrome.fontSize,
18
+ color: chrome.axis,
18
19
  },
19
20
  axisTick: {
20
21
  show: false,
@@ -22,7 +23,8 @@ export const createDefaultHeatmapXAxis = (
22
23
  });
23
24
 
24
25
  export const createDefaultHeatmapYAxis = (
25
- yAxisData?: string[],
26
+ yAxisData: string[] | undefined,
27
+ chrome: ChartChrome,
26
28
  ): ChartOptionType => ({
27
29
  type: "category",
28
30
  data: yAxisData,
@@ -30,10 +32,8 @@ export const createDefaultHeatmapYAxis = (
30
32
  show: true,
31
33
  },
32
34
  axisLabel: {
33
- textStyle: {
34
- fontSize: 14,
35
- color: "#373D48",
36
- },
35
+ fontSize: chrome.fontSize,
36
+ color: chrome.axis,
37
37
  },
38
38
  axisTick: {
39
39
  show: false,
@@ -21,8 +21,18 @@
21
21
 
22
22
  height: 100%;
23
23
 
24
+ // Inside a modal the Search must not keep its card chrome. On a page it is an outlined white
25
+ // card standing on the grey page background; in a modal the body is already `--vita-surface`,
26
+ // so the same treatment paints a white card on white and only the border survives — a stray
27
+ // outline around the search row. The padding was already being cleared here; the background and
28
+ // border are the other half of the same intent, and `.tableContainer` below is flush for
29
+ // exactly this reason.
24
30
  .search {
25
31
  padding: 0px;
32
+
33
+ background: transparent;
34
+
35
+ border: none;
26
36
  }
27
37
 
28
38
  .tableContainer {