@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.
- package/es/components/Chart/Bar/index.js +21 -7
- package/es/components/Chart/Common/index.js +9 -2
- package/es/components/Chart/Gauge/index.js +9 -2
- package/es/components/Chart/Heatmap/index.js +21 -6
- package/es/components/Chart/Line/index.js +21 -6
- package/es/components/Chart/Pie/Pie3D/index.js +9 -2
- package/es/components/Chart/Pie/index.js +9 -2
- package/es/components/Chart/Polar/index.js +9 -2
- package/es/components/Chart/Radar/index.js +9 -2
- package/es/components/Chart/Sankey/index.js +9 -2
- package/es/components/Chart/Tree/index.js +9 -2
- package/es/components/Chart/_hooks/useContainerReady.d.ts +18 -0
- package/es/components/Chart/_hooks/useContainerReady.js +56 -0
- package/es/components/Chart/_utils/cartesian.d.ts +3 -2
- package/es/components/Chart/_utils/cartesian.js +9 -10
- package/es/components/Chart/_utils/chartTheme.d.ts +22 -0
- package/es/components/Chart/_utils/chartTheme.js +23 -0
- package/es/components/Chart/_utils/heatmap.d.ts +3 -2
- package/es/components/Chart/_utils/heatmap.js +7 -10
- package/es/components/Panel/ListPanel/ListModalPanel/index.module.scss +10 -0
- package/lib/components/Chart/Bar/index.js +19 -7
- package/lib/components/Chart/Common/index.js +9 -2
- package/lib/components/Chart/Gauge/index.js +9 -2
- package/lib/components/Chart/Heatmap/index.js +19 -6
- package/lib/components/Chart/Line/index.js +19 -6
- package/lib/components/Chart/Pie/Pie3D/index.js +9 -2
- package/lib/components/Chart/Pie/index.js +9 -2
- package/lib/components/Chart/Polar/index.js +9 -2
- package/lib/components/Chart/Radar/index.js +9 -2
- package/lib/components/Chart/Sankey/index.js +9 -2
- package/lib/components/Chart/Tree/index.js +9 -2
- package/lib/components/Chart/_hooks/useContainerReady.d.ts +18 -0
- package/lib/components/Chart/_hooks/useContainerReady.js +51 -0
- package/lib/components/Chart/_utils/cartesian.d.ts +3 -2
- package/lib/components/Chart/_utils/cartesian.js +9 -11
- package/lib/components/Chart/_utils/chartTheme.d.ts +22 -0
- package/lib/components/Chart/_utils/chartTheme.js +29 -0
- package/lib/components/Chart/_utils/heatmap.d.ts +3 -2
- package/lib/components/Chart/_utils/heatmap.js +7 -10
- package/lib/components/Panel/ListPanel/ListModalPanel/index.module.scss +10 -0
- package/package.json +1 -1
- package/src/components/Chart/Bar/index.tsx +20 -5
- package/src/components/Chart/Common/index.tsx +12 -2
- package/src/components/Chart/Gauge/index.tsx +12 -2
- package/src/components/Chart/Heatmap/index.tsx +22 -5
- package/src/components/Chart/Line/index.tsx +20 -4
- package/src/components/Chart/Pie/Pie3D/index.tsx +10 -1
- package/src/components/Chart/Pie/index.tsx +10 -1
- package/src/components/Chart/Polar/index.tsx +12 -2
- package/src/components/Chart/Radar/index.tsx +10 -1
- package/src/components/Chart/Sankey/index.tsx +12 -2
- package/src/components/Chart/Tree/index.tsx +12 -2
- package/src/components/Chart/_hooks/useContainerReady.ts +50 -0
- package/src/components/Chart/_utils/cartesian.ts +11 -10
- package/src/components/Chart/_utils/chartTheme.ts +34 -0
- package/src/components/Chart/_utils/heatmap.ts +10 -10
- 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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
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 {
|