@hsu-react/ui 2.5.4 → 2.5.6
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/ChainGraph/_hooks/useChainGraphData.js +29 -8
- package/es/components/ChainGraph/_hooks/useChainGraphLayout.js +14 -3
- package/es/components/Chart/Bar/index.js +55 -20
- package/es/components/Chart/Bubble/index.js +21 -6
- package/es/components/Chart/Common/index.js +6 -1
- package/es/components/Chart/Gauge/index.js +6 -1
- package/es/components/Chart/Heatmap/index.js +24 -7
- package/es/components/Chart/Line/index.js +55 -20
- package/es/components/Chart/Pie/Pie3D/index.js +30 -12
- package/es/components/Chart/Pie/index.js +24 -7
- package/es/components/Chart/Polar/index.js +9 -2
- package/es/components/Chart/Radar/index.js +6 -1
- package/es/components/Chart/Sankey/index.js +11 -2
- package/es/components/Chart/Tree/index.js +11 -2
- package/es/components/Select/_hooks/useSelectComposition.js +6 -2
- package/es/components/Table/_hooks/useAutoScrolling.js +16 -7
- package/es/hooks/useShallowStable.d.ts +32 -0
- package/es/hooks/useShallowStable.js +54 -0
- package/es/layout/Menu/_hooks/useOnlyLvOneMenu.js +9 -2
- package/lib/components/ChainGraph/_hooks/useChainGraphData.js +24 -7
- package/lib/components/ChainGraph/_hooks/useChainGraphLayout.js +12 -2
- package/lib/components/Chart/Bar/index.js +51 -19
- package/lib/components/Chart/Bubble/index.js +20 -6
- package/lib/components/Chart/Common/index.js +6 -1
- package/lib/components/Chart/Gauge/index.js +6 -1
- package/lib/components/Chart/Heatmap/index.js +23 -7
- package/lib/components/Chart/Line/index.js +51 -19
- package/lib/components/Chart/Pie/Pie3D/index.js +29 -12
- package/lib/components/Chart/Pie/index.js +23 -7
- package/lib/components/Chart/Polar/index.js +9 -2
- package/lib/components/Chart/Radar/index.js +6 -1
- package/lib/components/Chart/Sankey/index.js +10 -2
- package/lib/components/Chart/Tree/index.js +10 -2
- package/lib/components/Select/_hooks/useSelectComposition.js +6 -2
- package/lib/components/Table/_hooks/useAutoScrolling.js +16 -7
- package/lib/hooks/useShallowStable.d.ts +32 -0
- package/lib/hooks/useShallowStable.js +59 -0
- package/lib/layout/Menu/_hooks/useOnlyLvOneMenu.js +8 -2
- package/package.json +1 -1
- package/src/__tests__/callbackDepsGuard.test.ts +72 -0
- package/src/__tests__/depsScan.ts +158 -0
- package/src/__tests__/freshObjectDepsGuard.test.ts +144 -0
- package/src/components/ChainGraph/_hooks/callbacks.test.tsx +155 -0
- package/src/components/ChainGraph/_hooks/useChainGraphData.ts +38 -10
- package/src/components/ChainGraph/_hooks/useChainGraphLayout.ts +16 -3
- package/src/components/Chart/Bar/index.tsx +65 -22
- package/src/components/Chart/Bubble/index.tsx +21 -6
- package/src/components/Chart/Common/index.tsx +6 -1
- package/src/components/Chart/Gauge/index.tsx +6 -1
- package/src/components/Chart/Heatmap/index.tsx +39 -17
- package/src/components/Chart/Line/index.tsx +65 -22
- package/src/components/Chart/Pie/Pie3D/index.tsx +27 -8
- package/src/components/Chart/Pie/index.tsx +25 -7
- package/src/components/Chart/Polar/index.tsx +10 -2
- package/src/components/Chart/Radar/index.tsx +6 -1
- package/src/components/Chart/Sankey/index.tsx +10 -4
- package/src/components/Chart/Tree/index.tsx +10 -4
- package/src/components/Chart/callbacks.test.tsx +315 -0
- package/src/components/Chart/optionMemo.test.tsx +232 -0
- package/src/components/Select/_hooks/useSelectComposition.test.ts +47 -0
- package/src/components/Select/_hooks/useSelectComposition.ts +6 -2
- package/src/components/Table/_hooks/useAutoScrolling.test.tsx +95 -0
- package/src/components/Table/_hooks/useAutoScrolling.ts +16 -7
- package/src/hooks/useShallowStable.ts +65 -0
- package/src/layout/Menu/_hooks/useOnlyLvOneMenu.test.tsx +56 -0
- package/src/layout/Menu/_hooks/useOnlyLvOneMenu.ts +8 -2
|
@@ -13,6 +13,8 @@ var _autoSmooth = require("../_utils/autoSmooth");
|
|
|
13
13
|
var _cartesian = require("../_utils/cartesian");
|
|
14
14
|
var _chartTheme = require("../_utils/chartTheme");
|
|
15
15
|
var _useIsDark = _interopRequireDefault(require("../../../hooks/useIsDark"));
|
|
16
|
+
var _useLatestRef = _interopRequireDefault(require("../../../hooks/useLatestRef"));
|
|
17
|
+
var _useShallowStable = _interopRequireDefault(require("../../../hooks/useShallowStable"));
|
|
16
18
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
17
19
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
18
20
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
@@ -42,8 +44,12 @@ const ChartBar = props => {
|
|
|
42
44
|
enableLegendAutoScroll = false,
|
|
43
45
|
legendVisibleCount = 8,
|
|
44
46
|
legendScrollInterval = 1500,
|
|
45
|
-
...
|
|
47
|
+
...restOption
|
|
46
48
|
} = props;
|
|
49
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
50
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
51
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
52
|
+
const coreOption = (0, _useShallowStable.default)(restOption);
|
|
47
53
|
const chartRef = (0, _react.useRef)(null);
|
|
48
54
|
const chartInstanceRef = (0, _react.useRef)(null);
|
|
49
55
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -57,6 +63,29 @@ const ChartBar = props => {
|
|
|
57
63
|
const isDark = (0, _useIsDark.default)();
|
|
58
64
|
const chrome = (0, _react.useMemo)(() => (0, _chartTheme.resolveChartChrome)(isDark), [isDark]);
|
|
59
65
|
|
|
66
|
+
// 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
|
|
67
|
+
// 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
|
|
68
|
+
// 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
|
|
69
|
+
// 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
|
|
70
|
+
// 这样「从无到有传入回调」仍会重新注册,不会漏。
|
|
71
|
+
const onClickRef = (0, _useLatestRef.default)(onClick);
|
|
72
|
+
const onLegendSelectChangedRef = (0, _useLatestRef.default)(onLegendSelectChanged);
|
|
73
|
+
const onDataZoomWindowChangedRef = (0, _useLatestRef.default)(onDataZoomWindowChanged);
|
|
74
|
+
const hasOnClick = !!onClick;
|
|
75
|
+
const hasOnLegendSelectChanged = !!onLegendSelectChanged;
|
|
76
|
+
const hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
|
|
77
|
+
// 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
|
|
78
|
+
// 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
|
|
79
|
+
const notifiedZoomWindowRef = (0, _react.useRef)(null);
|
|
80
|
+
const emitZoomWindow = (0, _react.useCallback)(next => {
|
|
81
|
+
const prev = notifiedZoomWindowRef.current;
|
|
82
|
+
if (prev && prev.startIndex === next.startIndex && prev.endIndex === next.endIndex) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
notifiedZoomWindowRef.current = next;
|
|
86
|
+
onDataZoomWindowChangedRef.current?.(next);
|
|
87
|
+
}, [onDataZoomWindowChangedRef]);
|
|
88
|
+
|
|
60
89
|
// Cache the chart option with useMemo
|
|
61
90
|
const chartOption = (0, _react.useMemo)(() => {
|
|
62
91
|
const def_xAxis = (0, _cartesian.createDefaultCategoryXAxis)(xAxisData, chrome);
|
|
@@ -241,15 +270,15 @@ const ChartBar = props => {
|
|
|
241
270
|
}
|
|
242
271
|
|
|
243
272
|
// Sync the current visible window to the caller (on initial render and after restore), used to recalculate axes based on the displayed portion
|
|
244
|
-
if (
|
|
273
|
+
if (hasOnDataZoomWindowChanged) {
|
|
245
274
|
const zooms = chart.getOption()?.dataZoom;
|
|
246
275
|
const zoom = zooms?.[0];
|
|
247
276
|
const totalLen = xAxisData?.length ?? 0;
|
|
248
277
|
if (typeof zoom?.start === "number" && typeof zoom?.end === "number") {
|
|
249
|
-
|
|
278
|
+
emitZoomWindow((0, _cartesian.percentWindowToIndexWindow)(zoom.start, zoom.end, totalLen));
|
|
250
279
|
} else {
|
|
251
280
|
// Without dataZoom (e.g. scrolling not enabled due to insufficient data) the window is the full range; sync once to avoid a stale old window
|
|
252
|
-
|
|
281
|
+
emitZoomWindow({
|
|
253
282
|
startIndex: 0,
|
|
254
283
|
endIndex: Math.max(0, totalLen - 1)
|
|
255
284
|
});
|
|
@@ -265,16 +294,19 @@ const ChartBar = props => {
|
|
|
265
294
|
resizeObserverRef.current.observe(chartRef.current);
|
|
266
295
|
}
|
|
267
296
|
|
|
268
|
-
// Add click event
|
|
269
|
-
|
|
270
|
-
|
|
297
|
+
// Add click event(注册与否看布尔量,实际调用取最新引用)
|
|
298
|
+
const handleClick = event => {
|
|
299
|
+
onClickRef.current?.(event);
|
|
300
|
+
};
|
|
301
|
+
if (hasOnClick) {
|
|
302
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
271
303
|
}
|
|
272
304
|
|
|
273
305
|
// Legend selection event
|
|
274
306
|
const handleLegendSelectChanged = params => {
|
|
275
|
-
|
|
307
|
+
onLegendSelectChangedRef.current?.(params.selected);
|
|
276
308
|
};
|
|
277
|
-
if (
|
|
309
|
+
if (hasOnLegendSelectChanged) {
|
|
278
310
|
chartInstanceRef.current?.on("legendselectchanged", handleLegendSelectChanged);
|
|
279
311
|
}
|
|
280
312
|
|
|
@@ -283,15 +315,15 @@ const ChartBar = props => {
|
|
|
283
315
|
const raw = params;
|
|
284
316
|
const info = raw?.batch?.[0] ?? raw;
|
|
285
317
|
if (typeof info?.startValue === "number" && typeof info?.endValue === "number") {
|
|
286
|
-
|
|
318
|
+
emitZoomWindow({
|
|
287
319
|
startIndex: Math.max(0, Math.round(info.startValue)),
|
|
288
320
|
endIndex: Math.max(0, Math.round(info.endValue))
|
|
289
321
|
});
|
|
290
322
|
} else if (typeof info?.start === "number" && typeof info?.end === "number") {
|
|
291
|
-
|
|
323
|
+
emitZoomWindow((0, _cartesian.percentWindowToIndexWindow)(info.start, info.end, xAxisData?.length ?? 0));
|
|
292
324
|
}
|
|
293
325
|
};
|
|
294
|
-
if (
|
|
326
|
+
if (hasOnDataZoomWindowChanged) {
|
|
295
327
|
chartInstanceRef.current?.on("datazoom", handleDataZoom);
|
|
296
328
|
}
|
|
297
329
|
|
|
@@ -318,13 +350,13 @@ const ChartBar = props => {
|
|
|
318
350
|
// Cleanup function
|
|
319
351
|
return () => {
|
|
320
352
|
window.removeEventListener("resize", handleResize);
|
|
321
|
-
if (
|
|
322
|
-
chartInstanceRef.current?.off("click",
|
|
353
|
+
if (hasOnClick) {
|
|
354
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
323
355
|
}
|
|
324
|
-
if (
|
|
356
|
+
if (hasOnLegendSelectChanged) {
|
|
325
357
|
chartInstanceRef.current?.off("legendselectchanged", handleLegendSelectChanged);
|
|
326
358
|
}
|
|
327
|
-
if (
|
|
359
|
+
if (hasOnDataZoomWindowChanged) {
|
|
328
360
|
chartInstanceRef.current?.off("datazoom", handleDataZoom);
|
|
329
361
|
}
|
|
330
362
|
if (legendScrollRef.current) {
|
|
@@ -332,7 +364,7 @@ const ChartBar = props => {
|
|
|
332
364
|
legendScrollRef.current = null;
|
|
333
365
|
}
|
|
334
366
|
};
|
|
335
|
-
}, [chartOption, handleResize,
|
|
367
|
+
}, [chartOption, handleResize, onClickRef, onLegendSelectChangedRef, hasOnClick, hasOnLegendSelectChanged, hasOnDataZoomWindowChanged, emitZoomWindow, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
|
|
336
368
|
// 图例是否接管滚轮由它推导,漏了会拿到过期的滚动配置
|
|
337
369
|
normalizedScroll, containerReady]);
|
|
338
370
|
|
|
@@ -351,10 +383,10 @@ const ChartBar = props => {
|
|
|
351
383
|
startIndex: normalizedScroll.startIndex,
|
|
352
384
|
autoPlay: normalizedScroll.autoScroll,
|
|
353
385
|
enableWheelScroll,
|
|
354
|
-
onWindowChange:
|
|
386
|
+
onWindowChange: hasOnDataZoomWindowChanged ? (startPercent, endPercent) => emitZoomWindow((0, _cartesian.percentWindowToIndexWindow)(startPercent, endPercent, xAxisData.length)) : undefined
|
|
355
387
|
});
|
|
356
388
|
return dispose;
|
|
357
|
-
}, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData,
|
|
389
|
+
}, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, hasOnDataZoomWindowChanged, emitZoomWindow]);
|
|
358
390
|
|
|
359
391
|
// Clean up resources when the component unmounts
|
|
360
392
|
(0, _react.useEffect)(() => {
|
|
@@ -9,6 +9,7 @@ var _indexModule = _interopRequireDefault(require("../index.module.scss"));
|
|
|
9
9
|
var echarts = _interopRequireWildcard(require("echarts"));
|
|
10
10
|
var _bubble = require("../_utils/bubble");
|
|
11
11
|
var _useLatestRef = require("../../../hooks/useLatestRef");
|
|
12
|
+
var _useShallowStable = _interopRequireDefault(require("../../../hooks/useShallowStable"));
|
|
12
13
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
14
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
14
15
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
@@ -28,8 +29,12 @@ const Bubble = props => {
|
|
|
28
29
|
title,
|
|
29
30
|
onChart,
|
|
30
31
|
onClick,
|
|
31
|
-
...
|
|
32
|
+
...restOption
|
|
32
33
|
} = props;
|
|
34
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
35
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
36
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
37
|
+
const coreOption = (0, _useShallowStable.default)(restOption);
|
|
33
38
|
const chartRef = (0, _react.useRef)(null);
|
|
34
39
|
const chartInstanceRef = (0, _react.useRef)(null);
|
|
35
40
|
const resizeObserverRef = (0, _react.useRef)(null);
|
|
@@ -257,6 +262,12 @@ const Bubble = props => {
|
|
|
257
262
|
}, [chartOption, data]);
|
|
258
263
|
const onChartRef = (0, _useLatestRef.useLatestRef)(onChart);
|
|
259
264
|
|
|
265
|
+
// onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
|
|
266
|
+
// 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
|
|
267
|
+
// 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
|
|
268
|
+
const onClickRef = (0, _useLatestRef.useLatestRef)(onClick);
|
|
269
|
+
const hasOnClick = !!onClick;
|
|
270
|
+
|
|
260
271
|
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
261
272
|
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
262
273
|
// Initialize the chart
|
|
@@ -282,18 +293,21 @@ const Bubble = props => {
|
|
|
282
293
|
window.addEventListener("resize", handleResize);
|
|
283
294
|
|
|
284
295
|
// Add click event
|
|
285
|
-
|
|
286
|
-
|
|
296
|
+
const handleClick = event => {
|
|
297
|
+
onClickRef.current?.(event);
|
|
298
|
+
};
|
|
299
|
+
if (hasOnClick) {
|
|
300
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
287
301
|
}
|
|
288
302
|
|
|
289
303
|
// Cleanup function
|
|
290
304
|
return () => {
|
|
291
305
|
window.removeEventListener("resize", handleResize);
|
|
292
|
-
if (
|
|
293
|
-
chartInstanceRef.current?.off("click",
|
|
306
|
+
if (hasOnClick) {
|
|
307
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
294
308
|
}
|
|
295
309
|
};
|
|
296
|
-
}, [chartOption, containerSize.width, containerSize.height, handleResize, onChartRef,
|
|
310
|
+
}, [chartOption, containerSize.width, containerSize.height, handleResize, onChartRef, onClickRef, hasOnClick]);
|
|
297
311
|
|
|
298
312
|
// Dispose the chart instance when the component unmounts
|
|
299
313
|
(0, _react.useEffect)(() => {
|
|
@@ -9,6 +9,7 @@ var _useContainerReady = _interopRequireDefault(require("../_hooks/useContainerR
|
|
|
9
9
|
var _indexModule = _interopRequireDefault(require("../index.module.scss"));
|
|
10
10
|
var echarts = _interopRequireWildcard(require("echarts"));
|
|
11
11
|
var _useLatestRef = require("../../../hooks/useLatestRef");
|
|
12
|
+
var _useShallowStable = _interopRequireDefault(require("../../../hooks/useShallowStable"));
|
|
12
13
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
14
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
14
15
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
@@ -18,8 +19,12 @@ const Common = props => {
|
|
|
18
19
|
className,
|
|
19
20
|
style,
|
|
20
21
|
onChart,
|
|
21
|
-
...
|
|
22
|
+
...restOption
|
|
22
23
|
} = props;
|
|
24
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
25
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
26
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
27
|
+
const coreOption = (0, _useShallowStable.default)(restOption);
|
|
23
28
|
const chartRef = (0, _react.useRef)(null);
|
|
24
29
|
const chartInstanceRef = (0, _react.useRef)(null);
|
|
25
30
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -8,6 +8,7 @@ var echarts = _interopRequireWildcard(require("echarts"));
|
|
|
8
8
|
var _useContainerReady = _interopRequireDefault(require("../_hooks/useContainerReady"));
|
|
9
9
|
var _react = _interopRequireWildcard(require("react"));
|
|
10
10
|
var _indexModule = _interopRequireDefault(require("../index.module.scss"));
|
|
11
|
+
var _useShallowStable = _interopRequireDefault(require("../../../hooks/useShallowStable"));
|
|
11
12
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
12
13
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
13
14
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
@@ -29,8 +30,12 @@ const Gauge = props => {
|
|
|
29
30
|
axisLabel,
|
|
30
31
|
title,
|
|
31
32
|
color = "#1675FB",
|
|
32
|
-
...
|
|
33
|
+
...restSeries
|
|
33
34
|
} = series;
|
|
35
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
36
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
37
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
38
|
+
const coreSeries = (0, _useShallowStable.default)(restSeries);
|
|
34
39
|
const chartRef = (0, _react.useRef)(null);
|
|
35
40
|
const chartInstanceRef = (0, _react.useRef)(null);
|
|
36
41
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -12,10 +12,13 @@ var _heatmap = require("../_utils/heatmap");
|
|
|
12
12
|
var _chartTheme = require("../_utils/chartTheme");
|
|
13
13
|
var _useIsDark = _interopRequireDefault(require("../../../hooks/useIsDark"));
|
|
14
14
|
var _useLatestRef = require("../../../hooks/useLatestRef");
|
|
15
|
+
var _useShallowStable = _interopRequireDefault(require("../../../hooks/useShallowStable"));
|
|
15
16
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
16
17
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
17
18
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
18
19
|
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
20
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
21
|
+
const DEFAULT_IN_RANGE_COLOR = ["#8BCCFB", "#75C3FB", "#5AB5F6", "#42AEFA", "#2DA1F5", "#0F89E1", "#056FBB", "#025794", "#0A4089"];
|
|
19
22
|
const Heatmap = props => {
|
|
20
23
|
const {
|
|
21
24
|
className,
|
|
@@ -34,9 +37,13 @@ const Heatmap = props => {
|
|
|
34
37
|
onChart,
|
|
35
38
|
onClick,
|
|
36
39
|
zeroColor,
|
|
37
|
-
inRangeColor =
|
|
38
|
-
...
|
|
40
|
+
inRangeColor = DEFAULT_IN_RANGE_COLOR,
|
|
41
|
+
...restOption
|
|
39
42
|
} = props;
|
|
43
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
44
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
45
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
46
|
+
const coreOption = (0, _useShallowStable.default)(restOption);
|
|
40
47
|
const chartRef = (0, _react.useRef)(null);
|
|
41
48
|
const chartInstanceRef = (0, _react.useRef)(null);
|
|
42
49
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -194,6 +201,12 @@ const Heatmap = props => {
|
|
|
194
201
|
}, []);
|
|
195
202
|
const onChartRef = (0, _useLatestRef.useLatestRef)(onChart);
|
|
196
203
|
|
|
204
|
+
// onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
|
|
205
|
+
// 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
|
|
206
|
+
// 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
|
|
207
|
+
const onClickRef = (0, _useLatestRef.useLatestRef)(onClick);
|
|
208
|
+
const hasOnClick = !!onClick;
|
|
209
|
+
|
|
197
210
|
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
198
211
|
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
199
212
|
// Initialize the chart
|
|
@@ -222,18 +235,21 @@ const Heatmap = props => {
|
|
|
222
235
|
}
|
|
223
236
|
|
|
224
237
|
// Add click event
|
|
225
|
-
|
|
226
|
-
|
|
238
|
+
const handleClick = event => {
|
|
239
|
+
onClickRef.current?.(event);
|
|
240
|
+
};
|
|
241
|
+
if (hasOnClick) {
|
|
242
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
227
243
|
}
|
|
228
244
|
|
|
229
245
|
// Cleanup function
|
|
230
246
|
return () => {
|
|
231
247
|
window.removeEventListener("resize", handleResize);
|
|
232
|
-
if (
|
|
233
|
-
chartInstanceRef.current?.off("click",
|
|
248
|
+
if (hasOnClick) {
|
|
249
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
234
250
|
}
|
|
235
251
|
};
|
|
236
|
-
}, [chartOption, handleResize, onChartRef,
|
|
252
|
+
}, [chartOption, handleResize, onChartRef, onClickRef, hasOnClick, containerReady]);
|
|
237
253
|
|
|
238
254
|
// Clean up resources when the component unmounts
|
|
239
255
|
(0, _react.useEffect)(() => {
|
|
@@ -13,6 +13,8 @@ var _autoSmooth = require("../_utils/autoSmooth");
|
|
|
13
13
|
var _cartesian = require("../_utils/cartesian");
|
|
14
14
|
var _chartTheme = require("../_utils/chartTheme");
|
|
15
15
|
var _useIsDark = _interopRequireDefault(require("../../../hooks/useIsDark"));
|
|
16
|
+
var _useLatestRef = _interopRequireDefault(require("../../../hooks/useLatestRef"));
|
|
17
|
+
var _useShallowStable = _interopRequireDefault(require("../../../hooks/useShallowStable"));
|
|
16
18
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
17
19
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
18
20
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
@@ -42,8 +44,12 @@ const ChartLine = props => {
|
|
|
42
44
|
enableLegendAutoScroll = false,
|
|
43
45
|
legendVisibleCount = 8,
|
|
44
46
|
legendScrollInterval = 1500,
|
|
45
|
-
...
|
|
47
|
+
...restOption
|
|
46
48
|
} = props;
|
|
49
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
50
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
51
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
52
|
+
const coreOption = (0, _useShallowStable.default)(restOption);
|
|
47
53
|
const chartRef = (0, _react.useRef)(null);
|
|
48
54
|
const chartInstanceRef = (0, _react.useRef)(null);
|
|
49
55
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -57,6 +63,29 @@ const ChartLine = props => {
|
|
|
57
63
|
const isDark = (0, _useIsDark.default)();
|
|
58
64
|
const chrome = (0, _react.useMemo)(() => (0, _chartTheme.resolveChartChrome)(isDark), [isDark]);
|
|
59
65
|
|
|
66
|
+
// 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
|
|
67
|
+
// 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
|
|
68
|
+
// 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
|
|
69
|
+
// 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
|
|
70
|
+
// 这样「从无到有传入回调」仍会重新注册,不会漏。
|
|
71
|
+
const onClickRef = (0, _useLatestRef.default)(onClick);
|
|
72
|
+
const onLegendSelectChangedRef = (0, _useLatestRef.default)(onLegendSelectChanged);
|
|
73
|
+
const onDataZoomWindowChangedRef = (0, _useLatestRef.default)(onDataZoomWindowChanged);
|
|
74
|
+
const hasOnClick = !!onClick;
|
|
75
|
+
const hasOnLegendSelectChanged = !!onLegendSelectChanged;
|
|
76
|
+
const hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
|
|
77
|
+
// 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
|
|
78
|
+
// 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
|
|
79
|
+
const notifiedZoomWindowRef = (0, _react.useRef)(null);
|
|
80
|
+
const emitZoomWindow = (0, _react.useCallback)(next => {
|
|
81
|
+
const prev = notifiedZoomWindowRef.current;
|
|
82
|
+
if (prev && prev.startIndex === next.startIndex && prev.endIndex === next.endIndex) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
notifiedZoomWindowRef.current = next;
|
|
86
|
+
onDataZoomWindowChangedRef.current?.(next);
|
|
87
|
+
}, [onDataZoomWindowChangedRef]);
|
|
88
|
+
|
|
60
89
|
// Cache the chart option with useMemo
|
|
61
90
|
const chartOption = (0, _react.useMemo)(() => {
|
|
62
91
|
const def_xAxis = (0, _cartesian.createDefaultCategoryXAxis)(xAxisData, chrome);
|
|
@@ -238,15 +267,15 @@ const ChartLine = props => {
|
|
|
238
267
|
}
|
|
239
268
|
|
|
240
269
|
// Sync the current visible window to the caller (on initial render and after restore), used to recalculate axes based on the displayed portion
|
|
241
|
-
if (
|
|
270
|
+
if (hasOnDataZoomWindowChanged) {
|
|
242
271
|
const zooms = chart.getOption()?.dataZoom;
|
|
243
272
|
const zoom = zooms?.[0];
|
|
244
273
|
const totalLen = xAxisData?.length ?? 0;
|
|
245
274
|
if (typeof zoom?.start === "number" && typeof zoom?.end === "number") {
|
|
246
|
-
|
|
275
|
+
emitZoomWindow((0, _cartesian.percentWindowToIndexWindow)(zoom.start, zoom.end, totalLen));
|
|
247
276
|
} else {
|
|
248
277
|
// Without dataZoom (e.g. scrolling not enabled due to insufficient data) the window is the full range; sync once to avoid a stale old window
|
|
249
|
-
|
|
278
|
+
emitZoomWindow({
|
|
250
279
|
startIndex: 0,
|
|
251
280
|
endIndex: Math.max(0, totalLen - 1)
|
|
252
281
|
});
|
|
@@ -262,16 +291,19 @@ const ChartLine = props => {
|
|
|
262
291
|
resizeObserverRef.current.observe(chartRef.current);
|
|
263
292
|
}
|
|
264
293
|
|
|
265
|
-
// Add click event
|
|
266
|
-
|
|
267
|
-
|
|
294
|
+
// Add click event(注册与否看布尔量,实际调用取最新引用)
|
|
295
|
+
const handleClick = event => {
|
|
296
|
+
onClickRef.current?.(event);
|
|
297
|
+
};
|
|
298
|
+
if (hasOnClick) {
|
|
299
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
268
300
|
}
|
|
269
301
|
|
|
270
302
|
// Legend selection event
|
|
271
303
|
const handleLegendSelectChanged = params => {
|
|
272
|
-
|
|
304
|
+
onLegendSelectChangedRef.current?.(params.selected);
|
|
273
305
|
};
|
|
274
|
-
if (
|
|
306
|
+
if (hasOnLegendSelectChanged) {
|
|
275
307
|
chartInstanceRef.current?.on("legendselectchanged", handleLegendSelectChanged);
|
|
276
308
|
}
|
|
277
309
|
|
|
@@ -280,15 +312,15 @@ const ChartLine = props => {
|
|
|
280
312
|
const raw = params;
|
|
281
313
|
const info = raw?.batch?.[0] ?? raw;
|
|
282
314
|
if (typeof info?.startValue === "number" && typeof info?.endValue === "number") {
|
|
283
|
-
|
|
315
|
+
emitZoomWindow({
|
|
284
316
|
startIndex: Math.max(0, Math.round(info.startValue)),
|
|
285
317
|
endIndex: Math.max(0, Math.round(info.endValue))
|
|
286
318
|
});
|
|
287
319
|
} else if (typeof info?.start === "number" && typeof info?.end === "number") {
|
|
288
|
-
|
|
320
|
+
emitZoomWindow((0, _cartesian.percentWindowToIndexWindow)(info.start, info.end, xAxisData?.length ?? 0));
|
|
289
321
|
}
|
|
290
322
|
};
|
|
291
|
-
if (
|
|
323
|
+
if (hasOnDataZoomWindowChanged) {
|
|
292
324
|
chartInstanceRef.current?.on("datazoom", handleDataZoom);
|
|
293
325
|
}
|
|
294
326
|
|
|
@@ -315,13 +347,13 @@ const ChartLine = props => {
|
|
|
315
347
|
// Cleanup function
|
|
316
348
|
return () => {
|
|
317
349
|
window.removeEventListener("resize", handleResize);
|
|
318
|
-
if (
|
|
319
|
-
chartInstanceRef.current?.off("click",
|
|
350
|
+
if (hasOnClick) {
|
|
351
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
320
352
|
}
|
|
321
|
-
if (
|
|
353
|
+
if (hasOnLegendSelectChanged) {
|
|
322
354
|
chartInstanceRef.current?.off("legendselectchanged", handleLegendSelectChanged);
|
|
323
355
|
}
|
|
324
|
-
if (
|
|
356
|
+
if (hasOnDataZoomWindowChanged) {
|
|
325
357
|
chartInstanceRef.current?.off("datazoom", handleDataZoom);
|
|
326
358
|
}
|
|
327
359
|
if (legendScrollRef.current) {
|
|
@@ -329,7 +361,7 @@ const ChartLine = props => {
|
|
|
329
361
|
legendScrollRef.current = null;
|
|
330
362
|
}
|
|
331
363
|
};
|
|
332
|
-
}, [chartOption, handleResize,
|
|
364
|
+
}, [chartOption, handleResize, onClickRef, onLegendSelectChangedRef, hasOnClick, hasOnLegendSelectChanged, hasOnDataZoomWindowChanged, emitZoomWindow, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
|
|
333
365
|
// 图例是否接管滚轮由它推导,漏了会拿到过期的滚动配置
|
|
334
366
|
normalizedScroll, containerReady]);
|
|
335
367
|
|
|
@@ -348,10 +380,10 @@ const ChartLine = props => {
|
|
|
348
380
|
startIndex: normalizedScroll.startIndex,
|
|
349
381
|
autoPlay: normalizedScroll.autoScroll,
|
|
350
382
|
enableWheelScroll,
|
|
351
|
-
onWindowChange:
|
|
383
|
+
onWindowChange: hasOnDataZoomWindowChanged ? (startPercent, endPercent) => emitZoomWindow((0, _cartesian.percentWindowToIndexWindow)(startPercent, endPercent, xAxisData.length)) : undefined
|
|
352
384
|
});
|
|
353
385
|
return dispose;
|
|
354
|
-
}, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData,
|
|
386
|
+
}, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, hasOnDataZoomWindowChanged, emitZoomWindow]);
|
|
355
387
|
|
|
356
388
|
// Clean up resources when the component unmounts
|
|
357
389
|
(0, _react.useEffect)(() => {
|
|
@@ -12,10 +12,18 @@ var _hooks = require("./_hooks");
|
|
|
12
12
|
var _option = require("./_utils/option");
|
|
13
13
|
var _indexModule = _interopRequireDefault(require("../../index.module.scss"));
|
|
14
14
|
var _useLatestRef = require("../../../../hooks/useLatestRef");
|
|
15
|
+
var _useShallowStable = _interopRequireDefault(require("../../../../hooks/useShallowStable"));
|
|
15
16
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
16
17
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
17
18
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
18
19
|
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
20
|
+
// 解构默认值写成字面量会每次渲染新建一个对象,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
21
|
+
const DEFAULT_TOOLTIP = {
|
|
22
|
+
show: false
|
|
23
|
+
};
|
|
24
|
+
const DEFAULT_LABEL = {
|
|
25
|
+
show: true
|
|
26
|
+
};
|
|
19
27
|
const ChartPie3D = props => {
|
|
20
28
|
const {
|
|
21
29
|
className,
|
|
@@ -30,17 +38,17 @@ const ChartPie3D = props => {
|
|
|
30
38
|
minHeight = 1,
|
|
31
39
|
maxHeight = 10,
|
|
32
40
|
yOffset = -0.2,
|
|
33
|
-
tooltip =
|
|
34
|
-
|
|
35
|
-
},
|
|
36
|
-
label = {
|
|
37
|
-
show: true
|
|
38
|
-
},
|
|
41
|
+
tooltip = DEFAULT_TOOLTIP,
|
|
42
|
+
label = DEFAULT_LABEL,
|
|
39
43
|
enableMouseControl = false,
|
|
40
44
|
onChart,
|
|
41
45
|
onClick,
|
|
42
|
-
...
|
|
46
|
+
...restOption
|
|
43
47
|
} = props;
|
|
48
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
49
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
50
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
51
|
+
const coreOption = (0, _useShallowStable.default)(restOption);
|
|
44
52
|
const chartRef = (0, _react.useRef)(null);
|
|
45
53
|
const chartInstanceRef = (0, _react.useRef)(null);
|
|
46
54
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -87,6 +95,12 @@ const ChartPie3D = props => {
|
|
|
87
95
|
});
|
|
88
96
|
const onChartRef = (0, _useLatestRef.useLatestRef)(onChart);
|
|
89
97
|
|
|
98
|
+
// onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑会重建
|
|
99
|
+
// 3D 饼的 option 并重播动画。注册与否看布尔量(保证「从无到有传入」仍会注册),
|
|
100
|
+
// 实际调用取 ref 里的最新引用。
|
|
101
|
+
const onClickRef = (0, _useLatestRef.useLatestRef)(onClick);
|
|
102
|
+
const hasOnClick = !!onClick;
|
|
103
|
+
|
|
90
104
|
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
91
105
|
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
92
106
|
(0, _react.useEffect)(() => {
|
|
@@ -106,20 +120,23 @@ const ChartPie3D = props => {
|
|
|
106
120
|
}
|
|
107
121
|
chart.on("mouseover", handleMouseOver);
|
|
108
122
|
chart.on("globalout", handleGlobalOut);
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
const handleClick = event => {
|
|
124
|
+
onClickRef.current?.(event);
|
|
125
|
+
};
|
|
126
|
+
if (hasOnClick) {
|
|
127
|
+
chart.on("click", handleClick);
|
|
111
128
|
}
|
|
112
129
|
return () => {
|
|
113
130
|
window.removeEventListener("resize", handleResize);
|
|
114
131
|
if (chart) {
|
|
115
132
|
chart.off("mouseover", handleMouseOver);
|
|
116
133
|
chart.off("globalout", handleGlobalOut);
|
|
117
|
-
if (
|
|
118
|
-
chart.off("click",
|
|
134
|
+
if (hasOnClick) {
|
|
135
|
+
chart.off("click", handleClick);
|
|
119
136
|
}
|
|
120
137
|
}
|
|
121
138
|
};
|
|
122
|
-
}, [chartOption, handleResize, handleMouseOver, handleGlobalOut, onChartRef,
|
|
139
|
+
}, [chartOption, handleResize, handleMouseOver, handleGlobalOut, onChartRef, onClickRef, hasOnClick, containerReady]);
|
|
123
140
|
(0, _react.useEffect)(() => {
|
|
124
141
|
return () => {
|
|
125
142
|
if (resizeObserverRef.current) {
|