@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.
Files changed (66) hide show
  1. package/es/components/ChainGraph/_hooks/useChainGraphData.js +29 -8
  2. package/es/components/ChainGraph/_hooks/useChainGraphLayout.js +14 -3
  3. package/es/components/Chart/Bar/index.js +55 -20
  4. package/es/components/Chart/Bubble/index.js +21 -6
  5. package/es/components/Chart/Common/index.js +6 -1
  6. package/es/components/Chart/Gauge/index.js +6 -1
  7. package/es/components/Chart/Heatmap/index.js +24 -7
  8. package/es/components/Chart/Line/index.js +55 -20
  9. package/es/components/Chart/Pie/Pie3D/index.js +30 -12
  10. package/es/components/Chart/Pie/index.js +24 -7
  11. package/es/components/Chart/Polar/index.js +9 -2
  12. package/es/components/Chart/Radar/index.js +6 -1
  13. package/es/components/Chart/Sankey/index.js +11 -2
  14. package/es/components/Chart/Tree/index.js +11 -2
  15. package/es/components/Select/_hooks/useSelectComposition.js +6 -2
  16. package/es/components/Table/_hooks/useAutoScrolling.js +16 -7
  17. package/es/hooks/useShallowStable.d.ts +32 -0
  18. package/es/hooks/useShallowStable.js +54 -0
  19. package/es/layout/Menu/_hooks/useOnlyLvOneMenu.js +9 -2
  20. package/lib/components/ChainGraph/_hooks/useChainGraphData.js +24 -7
  21. package/lib/components/ChainGraph/_hooks/useChainGraphLayout.js +12 -2
  22. package/lib/components/Chart/Bar/index.js +51 -19
  23. package/lib/components/Chart/Bubble/index.js +20 -6
  24. package/lib/components/Chart/Common/index.js +6 -1
  25. package/lib/components/Chart/Gauge/index.js +6 -1
  26. package/lib/components/Chart/Heatmap/index.js +23 -7
  27. package/lib/components/Chart/Line/index.js +51 -19
  28. package/lib/components/Chart/Pie/Pie3D/index.js +29 -12
  29. package/lib/components/Chart/Pie/index.js +23 -7
  30. package/lib/components/Chart/Polar/index.js +9 -2
  31. package/lib/components/Chart/Radar/index.js +6 -1
  32. package/lib/components/Chart/Sankey/index.js +10 -2
  33. package/lib/components/Chart/Tree/index.js +10 -2
  34. package/lib/components/Select/_hooks/useSelectComposition.js +6 -2
  35. package/lib/components/Table/_hooks/useAutoScrolling.js +16 -7
  36. package/lib/hooks/useShallowStable.d.ts +32 -0
  37. package/lib/hooks/useShallowStable.js +59 -0
  38. package/lib/layout/Menu/_hooks/useOnlyLvOneMenu.js +8 -2
  39. package/package.json +1 -1
  40. package/src/__tests__/callbackDepsGuard.test.ts +72 -0
  41. package/src/__tests__/depsScan.ts +158 -0
  42. package/src/__tests__/freshObjectDepsGuard.test.ts +144 -0
  43. package/src/components/ChainGraph/_hooks/callbacks.test.tsx +155 -0
  44. package/src/components/ChainGraph/_hooks/useChainGraphData.ts +38 -10
  45. package/src/components/ChainGraph/_hooks/useChainGraphLayout.ts +16 -3
  46. package/src/components/Chart/Bar/index.tsx +65 -22
  47. package/src/components/Chart/Bubble/index.tsx +21 -6
  48. package/src/components/Chart/Common/index.tsx +6 -1
  49. package/src/components/Chart/Gauge/index.tsx +6 -1
  50. package/src/components/Chart/Heatmap/index.tsx +39 -17
  51. package/src/components/Chart/Line/index.tsx +65 -22
  52. package/src/components/Chart/Pie/Pie3D/index.tsx +27 -8
  53. package/src/components/Chart/Pie/index.tsx +25 -7
  54. package/src/components/Chart/Polar/index.tsx +10 -2
  55. package/src/components/Chart/Radar/index.tsx +6 -1
  56. package/src/components/Chart/Sankey/index.tsx +10 -4
  57. package/src/components/Chart/Tree/index.tsx +10 -4
  58. package/src/components/Chart/callbacks.test.tsx +315 -0
  59. package/src/components/Chart/optionMemo.test.tsx +232 -0
  60. package/src/components/Select/_hooks/useSelectComposition.test.ts +47 -0
  61. package/src/components/Select/_hooks/useSelectComposition.ts +6 -2
  62. package/src/components/Table/_hooks/useAutoScrolling.test.tsx +95 -0
  63. package/src/components/Table/_hooks/useAutoScrolling.ts +16 -7
  64. package/src/hooks/useShallowStable.ts +65 -0
  65. package/src/layout/Menu/_hooks/useOnlyLvOneMenu.test.tsx +56 -0
  66. package/src/layout/Menu/_hooks/useOnlyLvOneMenu.ts +8 -2
@@ -4,8 +4,9 @@ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o =
4
4
  function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
5
5
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
6
6
  function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
7
- import { useEffect, useState } from "react";
7
+ import { useCallback, useEffect, useState } from "react";
8
8
  import { Equal } from "hsu-utils";
9
+ import { useLatestRef } from "../../../hooks/useLatestRef";
9
10
  export function useChainGraphData(props) {
10
11
  var graph = props.graph,
11
12
  data = props.data,
@@ -22,6 +23,29 @@ export function useChainGraphData(props) {
22
23
  _useState4 = _slicedToArray(_useState3, 2),
23
24
  isLayouting = _useState4[0],
24
25
  setIsLayouting = _useState4[1];
26
+
27
+ // 这三个回调都是「交给图实例、由它在后续事件里回调」的出口,不是数据本身。
28
+ // 它们进依赖数组是条死依赖:effect 体被 ObjEqual(lastData, data) 挡住,
29
+ // 引用变了也不会重新 setData —— 图里存的还是首次 setData 时那个闭包,
30
+ // 换了引用永远调不到(stale closure)。改成引用恒定的转发函数:
31
+ // 依赖数组只留真正的输入(graph/data/level/rootLevel),调用时总是取到最新的回调。
32
+ var getImageRef = useLatestRef(getImage);
33
+ var labelRenderRef = useLatestRef(labelRender);
34
+ var onLayoutingChangeRef = useLatestRef(onLayoutingChange);
35
+ var emitImage = useCallback(function (img) {
36
+ var _getImageRef$current;
37
+ (_getImageRef$current = getImageRef.current) === null || _getImageRef$current === void 0 || _getImageRef$current.call(getImageRef, img);
38
+ }, [getImageRef]);
39
+ // labelRender 在 ChainGraphServices.setData 里是「传了才改写 label」的真值判断,
40
+ // 所以这里保留「有没有传」的语义:没传就传 undefined,传了才给转发函数。
41
+ var renderLabel = useCallback(function (label) {
42
+ return labelRenderRef.current(label);
43
+ }, [labelRenderRef]);
44
+ var emitLayouting = useCallback(function (value) {
45
+ var _onLayoutingChangeRef;
46
+ setIsLayouting(value);
47
+ (_onLayoutingChangeRef = onLayoutingChangeRef.current) === null || _onLayoutingChangeRef === void 0 || _onLayoutingChangeRef.call(onLayoutingChangeRef, value);
48
+ }, [onLayoutingChangeRef]);
25
49
  useEffect(function () {
26
50
  if (graph && !Equal.ObjEqual(lastData, data)) {
27
51
  setLastData(data);
@@ -29,15 +53,12 @@ export function useChainGraphData(props) {
29
53
  data: data,
30
54
  level: level,
31
55
  rootLevel: rootLevel,
32
- getImage: getImage,
33
- isLayouting: function isLayouting(value) {
34
- setIsLayouting(value);
35
- onLayoutingChange === null || onLayoutingChange === void 0 || onLayoutingChange(value);
36
- },
37
- labelRender: labelRender
56
+ getImage: emitImage,
57
+ isLayouting: emitLayouting,
58
+ labelRender: labelRenderRef.current ? renderLabel : undefined
38
59
  });
39
60
  }
40
- }, [graph, data, level, rootLevel, getImage, labelRender, onLayoutingChange, lastData]);
61
+ }, [graph, data, level, rootLevel, lastData, emitImage, emitLayouting, renderLabel, labelRenderRef]);
41
62
  return {
42
63
  isLayouting: isLayouting
43
64
  };
@@ -1,12 +1,23 @@
1
- import { useEffect } from "react";
1
+ import { useCallback, useEffect } from "react";
2
+ import { useLatestRef } from "../../../hooks/useLatestRef";
2
3
  export function useChainGraphLayout(props) {
3
4
  var graph = props.graph,
4
5
  octopus = props.octopus,
5
6
  rootLevel = props.rootLevel,
6
7
  getImage = props.getImage;
8
+
9
+ // getImage 只是「布局完成后把截图 dataURL 交出去」的出口,不是布局的输入。
10
+ // 它进依赖数组会让消费方每传一次内联箭头就重新 changeLayout 一次 —— 整张图重排;
11
+ // 消费方再把截图存进 state,就变成「重排 → 出图 → setState → 新引用 → 再重排」的死循环。
12
+ // 用引用恒定的转发函数注册,调用时再取最新的 getImage。
13
+ var getImageRef = useLatestRef(getImage);
14
+ var emitImage = useCallback(function (img) {
15
+ var _getImageRef$current;
16
+ (_getImageRef$current = getImageRef.current) === null || _getImageRef$current === void 0 || _getImageRef$current.call(getImageRef, img);
17
+ }, [getImageRef]);
7
18
  useEffect(function () {
8
19
  if (graph && octopus !== undefined) {
9
- graph.changeLayout(octopus, rootLevel, getImage);
20
+ graph.changeLayout(octopus, rootLevel, emitImage);
10
21
  }
11
- }, [graph, octopus, rootLevel, getImage]);
22
+ }, [graph, octopus, rootLevel, emitImage]);
12
23
  }
@@ -16,6 +16,8 @@ import { autoScrollByItem } from "../_utils/autoSmooth";
16
16
  import { buildCartesianSeriesOptions, createDefaultCategoryXAxis, createDefaultValueYAxis, getSliderShow, percentWindowToIndexWindow, resolveScrollConfig } from "../_utils/cartesian";
17
17
  import { resolveChartChrome } from "../_utils/chartTheme";
18
18
  import useIsDark from "../../../hooks/useIsDark";
19
+ import useLatestRef from "../../../hooks/useLatestRef";
20
+ import useShallowStable from "../../../hooks/useShallowStable";
19
21
  import { jsx as _jsx } from "react/jsx-runtime";
20
22
  var ChartBar = function ChartBar(props) {
21
23
  var className = props.className,
@@ -44,7 +46,11 @@ var ChartBar = function ChartBar(props) {
44
46
  legendVisibleCount = _props$legendVisibleC === void 0 ? 8 : _props$legendVisibleC,
45
47
  _props$legendScrollIn = props.legendScrollInterval,
46
48
  legendScrollInterval = _props$legendScrollIn === void 0 ? 1500 : _props$legendScrollIn,
47
- coreOption = _objectWithoutProperties(props, _excluded);
49
+ restOption = _objectWithoutProperties(props, _excluded);
50
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
51
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
52
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
53
+ var coreOption = useShallowStable(restOption);
48
54
  var chartRef = useRef(null);
49
55
  var chartInstanceRef = useRef(null);
50
56
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -62,6 +68,30 @@ var ChartBar = function ChartBar(props) {
62
68
  return resolveChartChrome(isDark);
63
69
  }, [isDark]);
64
70
 
71
+ // 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
72
+ // 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
73
+ // 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
74
+ // 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
75
+ // 这样「从无到有传入回调」仍会重新注册,不会漏。
76
+ var onClickRef = useLatestRef(onClick);
77
+ var onLegendSelectChangedRef = useLatestRef(onLegendSelectChanged);
78
+ var onDataZoomWindowChangedRef = useLatestRef(onDataZoomWindowChanged);
79
+ var hasOnClick = !!onClick;
80
+ var hasOnLegendSelectChanged = !!onLegendSelectChanged;
81
+ var hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
82
+ // 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
83
+ // 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
84
+ var notifiedZoomWindowRef = useRef(null);
85
+ var emitZoomWindow = useCallback(function (next) {
86
+ var _onDataZoomWindowChan;
87
+ var prev = notifiedZoomWindowRef.current;
88
+ if (prev && prev.startIndex === next.startIndex && prev.endIndex === next.endIndex) {
89
+ return;
90
+ }
91
+ notifiedZoomWindowRef.current = next;
92
+ (_onDataZoomWindowChan = onDataZoomWindowChangedRef.current) === null || _onDataZoomWindowChan === void 0 || _onDataZoomWindowChan.call(onDataZoomWindowChangedRef, next);
93
+ }, [onDataZoomWindowChangedRef]);
94
+
65
95
  // Cache the chart option with useMemo
66
96
  var chartOption = useMemo(function () {
67
97
  var _xAxisData$length, _ref, _ref2;
@@ -239,16 +269,16 @@ var ChartBar = function ChartBar(props) {
239
269
  }
240
270
 
241
271
  // Sync the current visible window to the caller (on initial render and after restore), used to recalculate axes based on the displayed portion
242
- if (onDataZoomWindowChanged) {
272
+ if (hasOnDataZoomWindowChanged) {
243
273
  var _chart$getOption3, _xAxisData$length2;
244
274
  var zooms = (_chart$getOption3 = chart.getOption()) === null || _chart$getOption3 === void 0 ? void 0 : _chart$getOption3.dataZoom;
245
275
  var zoom = zooms === null || zooms === void 0 ? void 0 : zooms[0];
246
276
  var totalLen = (_xAxisData$length2 = xAxisData === null || xAxisData === void 0 ? void 0 : xAxisData.length) !== null && _xAxisData$length2 !== void 0 ? _xAxisData$length2 : 0;
247
277
  if (typeof (zoom === null || zoom === void 0 ? void 0 : zoom.start) === "number" && typeof (zoom === null || zoom === void 0 ? void 0 : zoom.end) === "number") {
248
- onDataZoomWindowChanged(percentWindowToIndexWindow(zoom.start, zoom.end, totalLen));
278
+ emitZoomWindow(percentWindowToIndexWindow(zoom.start, zoom.end, totalLen));
249
279
  } else {
250
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
251
- onDataZoomWindowChanged({
281
+ emitZoomWindow({
252
282
  startIndex: 0,
253
283
  endIndex: Math.max(0, totalLen - 1)
254
284
  });
@@ -264,17 +294,22 @@ var ChartBar = function ChartBar(props) {
264
294
  resizeObserverRef.current.observe(chartRef.current);
265
295
  }
266
296
 
267
- // Add click event
268
- if (onClick) {
297
+ // Add click event(注册与否看布尔量,实际调用取最新引用)
298
+ var handleClick = function handleClick(event) {
299
+ var _onClickRef$current;
300
+ (_onClickRef$current = onClickRef.current) === null || _onClickRef$current === void 0 || _onClickRef$current.call(onClickRef, event);
301
+ };
302
+ if (hasOnClick) {
269
303
  var _chartInstanceRef$cur2;
270
- (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", onClick);
304
+ (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", handleClick);
271
305
  }
272
306
 
273
307
  // Legend selection event
274
308
  var handleLegendSelectChanged = function handleLegendSelectChanged(params) {
275
- onLegendSelectChanged === null || onLegendSelectChanged === void 0 || onLegendSelectChanged(params.selected);
309
+ var _onLegendSelectChange;
310
+ (_onLegendSelectChange = onLegendSelectChangedRef.current) === null || _onLegendSelectChange === void 0 || _onLegendSelectChange.call(onLegendSelectChangedRef, params.selected);
276
311
  };
277
- if (onLegendSelectChanged) {
312
+ if (hasOnLegendSelectChanged) {
278
313
  var _chartInstanceRef$cur3;
279
314
  (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.on("legendselectchanged", handleLegendSelectChanged);
280
315
  }
@@ -285,16 +320,16 @@ var ChartBar = function ChartBar(props) {
285
320
  var raw = params;
286
321
  var info = (_raw$batch$ = raw === null || raw === void 0 || (_raw$batch = raw.batch) === null || _raw$batch === void 0 ? void 0 : _raw$batch[0]) !== null && _raw$batch$ !== void 0 ? _raw$batch$ : raw;
287
322
  if (typeof (info === null || info === void 0 ? void 0 : info.startValue) === "number" && typeof (info === null || info === void 0 ? void 0 : info.endValue) === "number") {
288
- onDataZoomWindowChanged === null || onDataZoomWindowChanged === void 0 || onDataZoomWindowChanged({
323
+ emitZoomWindow({
289
324
  startIndex: Math.max(0, Math.round(info.startValue)),
290
325
  endIndex: Math.max(0, Math.round(info.endValue))
291
326
  });
292
327
  } else if (typeof (info === null || info === void 0 ? void 0 : info.start) === "number" && typeof (info === null || info === void 0 ? void 0 : info.end) === "number") {
293
328
  var _xAxisData$length3;
294
- onDataZoomWindowChanged === null || onDataZoomWindowChanged === void 0 || onDataZoomWindowChanged(percentWindowToIndexWindow(info.start, info.end, (_xAxisData$length3 = xAxisData === null || xAxisData === void 0 ? void 0 : xAxisData.length) !== null && _xAxisData$length3 !== void 0 ? _xAxisData$length3 : 0));
329
+ emitZoomWindow(percentWindowToIndexWindow(info.start, info.end, (_xAxisData$length3 = xAxisData === null || xAxisData === void 0 ? void 0 : xAxisData.length) !== null && _xAxisData$length3 !== void 0 ? _xAxisData$length3 : 0));
295
330
  }
296
331
  };
297
- if (onDataZoomWindowChanged) {
332
+ if (hasOnDataZoomWindowChanged) {
298
333
  var _chartInstanceRef$cur4;
299
334
  (_chartInstanceRef$cur4 = chartInstanceRef.current) === null || _chartInstanceRef$cur4 === void 0 || _chartInstanceRef$cur4.on("datazoom", handleDataZoom);
300
335
  }
@@ -322,15 +357,15 @@ var ChartBar = function ChartBar(props) {
322
357
  // Cleanup function
323
358
  return function () {
324
359
  window.removeEventListener("resize", handleResize);
325
- if (onClick) {
360
+ if (hasOnClick) {
326
361
  var _chartInstanceRef$cur5;
327
- (_chartInstanceRef$cur5 = chartInstanceRef.current) === null || _chartInstanceRef$cur5 === void 0 || _chartInstanceRef$cur5.off("click", onClick);
362
+ (_chartInstanceRef$cur5 = chartInstanceRef.current) === null || _chartInstanceRef$cur5 === void 0 || _chartInstanceRef$cur5.off("click", handleClick);
328
363
  }
329
- if (onLegendSelectChanged) {
364
+ if (hasOnLegendSelectChanged) {
330
365
  var _chartInstanceRef$cur6;
331
366
  (_chartInstanceRef$cur6 = chartInstanceRef.current) === null || _chartInstanceRef$cur6 === void 0 || _chartInstanceRef$cur6.off("legendselectchanged", handleLegendSelectChanged);
332
367
  }
333
- if (onDataZoomWindowChanged) {
368
+ if (hasOnDataZoomWindowChanged) {
334
369
  var _chartInstanceRef$cur7;
335
370
  (_chartInstanceRef$cur7 = chartInstanceRef.current) === null || _chartInstanceRef$cur7 === void 0 || _chartInstanceRef$cur7.off("datazoom", handleDataZoom);
336
371
  }
@@ -339,7 +374,7 @@ var ChartBar = function ChartBar(props) {
339
374
  legendScrollRef.current = null;
340
375
  }
341
376
  };
342
- }, [chartOption, handleResize, onClick, onLegendSelectChanged, onDataZoomWindowChanged, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
377
+ }, [chartOption, handleResize, onClickRef, onLegendSelectChangedRef, hasOnClick, hasOnLegendSelectChanged, hasOnDataZoomWindowChanged, emitZoomWindow, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
343
378
  // 图例是否接管滚轮由它推导,漏了会拿到过期的滚动配置
344
379
  normalizedScroll, containerReady]);
345
380
 
@@ -359,13 +394,13 @@ var ChartBar = function ChartBar(props) {
359
394
  startIndex: normalizedScroll.startIndex,
360
395
  autoPlay: normalizedScroll.autoScroll,
361
396
  enableWheelScroll: enableWheelScroll,
362
- onWindowChange: onDataZoomWindowChanged ? function (startPercent, endPercent) {
363
- return onDataZoomWindowChanged(percentWindowToIndexWindow(startPercent, endPercent, xAxisData.length));
397
+ onWindowChange: hasOnDataZoomWindowChanged ? function (startPercent, endPercent) {
398
+ return emitZoomWindow(percentWindowToIndexWindow(startPercent, endPercent, xAxisData.length));
364
399
  } : undefined
365
400
  }),
366
401
  dispose = _autoScrollByItem.dispose;
367
402
  return dispose;
368
- }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, onDataZoomWindowChanged]);
403
+ }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, hasOnDataZoomWindowChanged, emitZoomWindow]);
369
404
 
370
405
  // Clean up resources when the component unmounts
371
406
  useEffect(function () {
@@ -22,6 +22,7 @@ import styles from "../index.module.scss";
22
22
  import * as echarts from "echarts";
23
23
  import { defaultBubbleColorList, drawCircles } from "../_utils/bubble";
24
24
  import { useLatestRef } from "../../../hooks/useLatestRef";
25
+ import useShallowStable from "../../../hooks/useShallowStable";
25
26
 
26
27
  // Extended data item type containing all properties the chart needs
27
28
  import { jsx as _jsx } from "react/jsx-runtime";
@@ -38,7 +39,11 @@ var Bubble = function Bubble(props) {
38
39
  title = props.title,
39
40
  onChart = props.onChart,
40
41
  onClick = props.onClick,
41
- coreOption = _objectWithoutProperties(props, _excluded);
42
+ restOption = _objectWithoutProperties(props, _excluded);
43
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
44
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
45
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
46
+ var coreOption = useShallowStable(restOption);
42
47
  var chartRef = useRef(null);
43
48
  var chartInstanceRef = useRef(null);
44
49
  var resizeObserverRef = useRef(null);
@@ -279,6 +284,12 @@ var Bubble = function Bubble(props) {
279
284
  }, [chartOption, data]);
280
285
  var onChartRef = useLatestRef(onChart);
281
286
 
287
+ // onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
288
+ // 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
289
+ // 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
290
+ var onClickRef = useLatestRef(onClick);
291
+ var hasOnClick = !!onClick;
292
+
282
293
  // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
283
294
  // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
284
295
  // Initialize the chart
@@ -305,20 +316,24 @@ var Bubble = function Bubble(props) {
305
316
  window.addEventListener("resize", handleResize);
306
317
 
307
318
  // Add click event
308
- if (onClick) {
319
+ var handleClick = function handleClick(event) {
320
+ var _onClickRef$current;
321
+ (_onClickRef$current = onClickRef.current) === null || _onClickRef$current === void 0 || _onClickRef$current.call(onClickRef, event);
322
+ };
323
+ if (hasOnClick) {
309
324
  var _chartInstanceRef$cur2;
310
- (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", onClick);
325
+ (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", handleClick);
311
326
  }
312
327
 
313
328
  // Cleanup function
314
329
  return function () {
315
330
  window.removeEventListener("resize", handleResize);
316
- if (onClick) {
331
+ if (hasOnClick) {
317
332
  var _chartInstanceRef$cur3;
318
- (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.off("click", onClick);
333
+ (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.off("click", handleClick);
319
334
  }
320
335
  };
321
- }, [chartOption, containerSize.width, containerSize.height, handleResize, onChartRef, onClick]);
336
+ }, [chartOption, containerSize.width, containerSize.height, handleResize, onChartRef, onClickRef, hasOnClick]);
322
337
 
323
338
  // Dispose the chart instance when the component unmounts
324
339
  useEffect(function () {
@@ -12,12 +12,17 @@ import useContainerReady from "../_hooks/useContainerReady";
12
12
  import styles from "../index.module.scss";
13
13
  import * as echarts from "echarts";
14
14
  import { useLatestRef } from "../../../hooks/useLatestRef";
15
+ import useShallowStable from "../../../hooks/useShallowStable";
15
16
  import { jsx as _jsx } from "react/jsx-runtime";
16
17
  var Common = function Common(props) {
17
18
  var className = props.className,
18
19
  style = props.style,
19
20
  onChart = props.onChart,
20
- coreOption = _objectWithoutProperties(props, _excluded);
21
+ restOption = _objectWithoutProperties(props, _excluded);
22
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
23
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
24
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
25
+ var coreOption = useShallowStable(restOption);
21
26
  var chartRef = useRef(null);
22
27
  var chartInstanceRef = useRef(null);
23
28
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -11,6 +11,7 @@ import * as echarts from "echarts";
11
11
  import useContainerReady from "../_hooks/useContainerReady";
12
12
  import React, { useCallback, useEffect, useMemo, useRef } from "react";
13
13
  import styles from "../index.module.scss";
14
+ import useShallowStable from "../../../hooks/useShallowStable";
14
15
  import { jsx as _jsx } from "react/jsx-runtime";
15
16
  var Gauge = function Gauge(props) {
16
17
  var className = props.className,
@@ -29,7 +30,11 @@ var Gauge = function Gauge(props) {
29
30
  title = _ref.title,
30
31
  _ref$color = _ref.color,
31
32
  color = _ref$color === void 0 ? "#1675FB" : _ref$color,
32
- coreSeries = _objectWithoutProperties(_ref, _excluded);
33
+ restSeries = _objectWithoutProperties(_ref, _excluded);
34
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
35
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
36
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
37
+ var coreSeries = useShallowStable(restSeries);
33
38
  var chartRef = useRef(null);
34
39
  var chartInstanceRef = useRef(null);
35
40
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -21,7 +21,10 @@ import { createDefaultHeatmapXAxis, createDefaultHeatmapYAxis } from "../_utils/
21
21
  import { resolveChartChrome } from "../_utils/chartTheme";
22
22
  import useIsDark from "../../../hooks/useIsDark";
23
23
  import { useLatestRef } from "../../../hooks/useLatestRef";
24
+ import useShallowStable from "../../../hooks/useShallowStable";
24
25
  import { jsx as _jsx } from "react/jsx-runtime";
26
+ // 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
27
+ var DEFAULT_IN_RANGE_COLOR = ["#8BCCFB", "#75C3FB", "#5AB5F6", "#42AEFA", "#2DA1F5", "#0F89E1", "#056FBB", "#025794", "#0A4089"];
25
28
  var Heatmap = function Heatmap(props) {
26
29
  var className = props.className,
27
30
  style = props.style,
@@ -40,8 +43,12 @@ var Heatmap = function Heatmap(props) {
40
43
  onClick = props.onClick,
41
44
  zeroColor = props.zeroColor,
42
45
  _props$inRangeColor = props.inRangeColor,
43
- inRangeColor = _props$inRangeColor === void 0 ? ["#8BCCFB", "#75C3FB", "#5AB5F6", "#42AEFA", "#2DA1F5", "#0F89E1", "#056FBB", "#025794", "#0A4089"] : _props$inRangeColor,
44
- coreOption = _objectWithoutProperties(props, _excluded);
46
+ inRangeColor = _props$inRangeColor === void 0 ? DEFAULT_IN_RANGE_COLOR : _props$inRangeColor,
47
+ restOption = _objectWithoutProperties(props, _excluded);
48
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
49
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
50
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
51
+ var coreOption = useShallowStable(restOption);
45
52
  var chartRef = useRef(null);
46
53
  var chartInstanceRef = useRef(null);
47
54
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -195,6 +202,12 @@ var Heatmap = function Heatmap(props) {
195
202
  }, []);
196
203
  var onChartRef = useLatestRef(onChart);
197
204
 
205
+ // onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
206
+ // 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
207
+ // 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
208
+ var onClickRef = useLatestRef(onClick);
209
+ var hasOnClick = !!onClick;
210
+
198
211
  // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
199
212
  // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
200
213
  // Initialize the chart
@@ -224,20 +237,24 @@ var Heatmap = function Heatmap(props) {
224
237
  }
225
238
 
226
239
  // Add click event
227
- if (onClick) {
240
+ var handleClick = function handleClick(event) {
241
+ var _onClickRef$current;
242
+ (_onClickRef$current = onClickRef.current) === null || _onClickRef$current === void 0 || _onClickRef$current.call(onClickRef, event);
243
+ };
244
+ if (hasOnClick) {
228
245
  var _chartInstanceRef$cur2;
229
- (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", onClick);
246
+ (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", handleClick);
230
247
  }
231
248
 
232
249
  // Cleanup function
233
250
  return function () {
234
251
  window.removeEventListener("resize", handleResize);
235
- if (onClick) {
252
+ if (hasOnClick) {
236
253
  var _chartInstanceRef$cur3;
237
- (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.off("click", onClick);
254
+ (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.off("click", handleClick);
238
255
  }
239
256
  };
240
- }, [chartOption, handleResize, onChartRef, onClick, containerReady]);
257
+ }, [chartOption, handleResize, onChartRef, onClickRef, hasOnClick, containerReady]);
241
258
 
242
259
  // Clean up resources when the component unmounts
243
260
  useEffect(function () {
@@ -16,6 +16,8 @@ import { autoScrollByItem } from "../_utils/autoSmooth";
16
16
  import { buildCartesianSeriesOptions, createDefaultCategoryXAxis, createDefaultValueYAxis, getSliderShow, percentWindowToIndexWindow, resolveScrollConfig } from "../_utils/cartesian";
17
17
  import { resolveChartChrome } from "../_utils/chartTheme";
18
18
  import useIsDark from "../../../hooks/useIsDark";
19
+ import useLatestRef from "../../../hooks/useLatestRef";
20
+ import useShallowStable from "../../../hooks/useShallowStable";
19
21
  import { jsx as _jsx } from "react/jsx-runtime";
20
22
  var ChartLine = function ChartLine(props) {
21
23
  var className = props.className,
@@ -44,7 +46,11 @@ var ChartLine = function ChartLine(props) {
44
46
  legendVisibleCount = _props$legendVisibleC === void 0 ? 8 : _props$legendVisibleC,
45
47
  _props$legendScrollIn = props.legendScrollInterval,
46
48
  legendScrollInterval = _props$legendScrollIn === void 0 ? 1500 : _props$legendScrollIn,
47
- coreOption = _objectWithoutProperties(props, _excluded);
49
+ restOption = _objectWithoutProperties(props, _excluded);
50
+ // rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
51
+ // 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
52
+ // useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
53
+ var coreOption = useShallowStable(restOption);
48
54
  var chartRef = useRef(null);
49
55
  var chartInstanceRef = useRef(null);
50
56
  // Defers `echarts.init` until the container has a box — see the hook for why
@@ -62,6 +68,30 @@ var ChartLine = function ChartLine(props) {
62
68
  return resolveChartChrome(isDark);
63
69
  }, [isDark]);
64
70
 
71
+ // 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
72
+ // 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
73
+ // 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
74
+ // 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
75
+ // 这样「从无到有传入回调」仍会重新注册,不会漏。
76
+ var onClickRef = useLatestRef(onClick);
77
+ var onLegendSelectChangedRef = useLatestRef(onLegendSelectChanged);
78
+ var onDataZoomWindowChangedRef = useLatestRef(onDataZoomWindowChanged);
79
+ var hasOnClick = !!onClick;
80
+ var hasOnLegendSelectChanged = !!onLegendSelectChanged;
81
+ var hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
82
+ // 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
83
+ // 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
84
+ var notifiedZoomWindowRef = useRef(null);
85
+ var emitZoomWindow = useCallback(function (next) {
86
+ var _onDataZoomWindowChan;
87
+ var prev = notifiedZoomWindowRef.current;
88
+ if (prev && prev.startIndex === next.startIndex && prev.endIndex === next.endIndex) {
89
+ return;
90
+ }
91
+ notifiedZoomWindowRef.current = next;
92
+ (_onDataZoomWindowChan = onDataZoomWindowChangedRef.current) === null || _onDataZoomWindowChan === void 0 || _onDataZoomWindowChan.call(onDataZoomWindowChangedRef, next);
93
+ }, [onDataZoomWindowChangedRef]);
94
+
65
95
  // Cache the chart option with useMemo
66
96
  var chartOption = useMemo(function () {
67
97
  var _xAxisData$length, _ref, _ref2;
@@ -236,16 +266,16 @@ var ChartLine = function ChartLine(props) {
236
266
  }
237
267
 
238
268
  // Sync the current visible window to the caller (on initial render and after restore), used to recalculate axes based on the displayed portion
239
- if (onDataZoomWindowChanged) {
269
+ if (hasOnDataZoomWindowChanged) {
240
270
  var _chart$getOption3, _xAxisData$length2;
241
271
  var zooms = (_chart$getOption3 = chart.getOption()) === null || _chart$getOption3 === void 0 ? void 0 : _chart$getOption3.dataZoom;
242
272
  var zoom = zooms === null || zooms === void 0 ? void 0 : zooms[0];
243
273
  var totalLen = (_xAxisData$length2 = xAxisData === null || xAxisData === void 0 ? void 0 : xAxisData.length) !== null && _xAxisData$length2 !== void 0 ? _xAxisData$length2 : 0;
244
274
  if (typeof (zoom === null || zoom === void 0 ? void 0 : zoom.start) === "number" && typeof (zoom === null || zoom === void 0 ? void 0 : zoom.end) === "number") {
245
- onDataZoomWindowChanged(percentWindowToIndexWindow(zoom.start, zoom.end, totalLen));
275
+ emitZoomWindow(percentWindowToIndexWindow(zoom.start, zoom.end, totalLen));
246
276
  } else {
247
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
248
- onDataZoomWindowChanged({
278
+ emitZoomWindow({
249
279
  startIndex: 0,
250
280
  endIndex: Math.max(0, totalLen - 1)
251
281
  });
@@ -261,17 +291,22 @@ var ChartLine = function ChartLine(props) {
261
291
  resizeObserverRef.current.observe(chartRef.current);
262
292
  }
263
293
 
264
- // Add click event
265
- if (onClick) {
294
+ // Add click event(注册与否看布尔量,实际调用取最新引用)
295
+ var handleClick = function handleClick(event) {
296
+ var _onClickRef$current;
297
+ (_onClickRef$current = onClickRef.current) === null || _onClickRef$current === void 0 || _onClickRef$current.call(onClickRef, event);
298
+ };
299
+ if (hasOnClick) {
266
300
  var _chartInstanceRef$cur2;
267
- (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", onClick);
301
+ (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", handleClick);
268
302
  }
269
303
 
270
304
  // Legend selection event
271
305
  var handleLegendSelectChanged = function handleLegendSelectChanged(params) {
272
- onLegendSelectChanged === null || onLegendSelectChanged === void 0 || onLegendSelectChanged(params.selected);
306
+ var _onLegendSelectChange;
307
+ (_onLegendSelectChange = onLegendSelectChangedRef.current) === null || _onLegendSelectChange === void 0 || _onLegendSelectChange.call(onLegendSelectChangedRef, params.selected);
273
308
  };
274
- if (onLegendSelectChanged) {
309
+ if (hasOnLegendSelectChanged) {
275
310
  var _chartInstanceRef$cur3;
276
311
  (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.on("legendselectchanged", handleLegendSelectChanged);
277
312
  }
@@ -282,16 +317,16 @@ var ChartLine = function ChartLine(props) {
282
317
  var raw = params;
283
318
  var info = (_raw$batch$ = raw === null || raw === void 0 || (_raw$batch = raw.batch) === null || _raw$batch === void 0 ? void 0 : _raw$batch[0]) !== null && _raw$batch$ !== void 0 ? _raw$batch$ : raw;
284
319
  if (typeof (info === null || info === void 0 ? void 0 : info.startValue) === "number" && typeof (info === null || info === void 0 ? void 0 : info.endValue) === "number") {
285
- onDataZoomWindowChanged === null || onDataZoomWindowChanged === void 0 || onDataZoomWindowChanged({
320
+ emitZoomWindow({
286
321
  startIndex: Math.max(0, Math.round(info.startValue)),
287
322
  endIndex: Math.max(0, Math.round(info.endValue))
288
323
  });
289
324
  } else if (typeof (info === null || info === void 0 ? void 0 : info.start) === "number" && typeof (info === null || info === void 0 ? void 0 : info.end) === "number") {
290
325
  var _xAxisData$length3;
291
- onDataZoomWindowChanged === null || onDataZoomWindowChanged === void 0 || onDataZoomWindowChanged(percentWindowToIndexWindow(info.start, info.end, (_xAxisData$length3 = xAxisData === null || xAxisData === void 0 ? void 0 : xAxisData.length) !== null && _xAxisData$length3 !== void 0 ? _xAxisData$length3 : 0));
326
+ emitZoomWindow(percentWindowToIndexWindow(info.start, info.end, (_xAxisData$length3 = xAxisData === null || xAxisData === void 0 ? void 0 : xAxisData.length) !== null && _xAxisData$length3 !== void 0 ? _xAxisData$length3 : 0));
292
327
  }
293
328
  };
294
- if (onDataZoomWindowChanged) {
329
+ if (hasOnDataZoomWindowChanged) {
295
330
  var _chartInstanceRef$cur4;
296
331
  (_chartInstanceRef$cur4 = chartInstanceRef.current) === null || _chartInstanceRef$cur4 === void 0 || _chartInstanceRef$cur4.on("datazoom", handleDataZoom);
297
332
  }
@@ -319,15 +354,15 @@ var ChartLine = function ChartLine(props) {
319
354
  // Cleanup function
320
355
  return function () {
321
356
  window.removeEventListener("resize", handleResize);
322
- if (onClick) {
357
+ if (hasOnClick) {
323
358
  var _chartInstanceRef$cur5;
324
- (_chartInstanceRef$cur5 = chartInstanceRef.current) === null || _chartInstanceRef$cur5 === void 0 || _chartInstanceRef$cur5.off("click", onClick);
359
+ (_chartInstanceRef$cur5 = chartInstanceRef.current) === null || _chartInstanceRef$cur5 === void 0 || _chartInstanceRef$cur5.off("click", handleClick);
325
360
  }
326
- if (onLegendSelectChanged) {
361
+ if (hasOnLegendSelectChanged) {
327
362
  var _chartInstanceRef$cur6;
328
363
  (_chartInstanceRef$cur6 = chartInstanceRef.current) === null || _chartInstanceRef$cur6 === void 0 || _chartInstanceRef$cur6.off("legendselectchanged", handleLegendSelectChanged);
329
364
  }
330
- if (onDataZoomWindowChanged) {
365
+ if (hasOnDataZoomWindowChanged) {
331
366
  var _chartInstanceRef$cur7;
332
367
  (_chartInstanceRef$cur7 = chartInstanceRef.current) === null || _chartInstanceRef$cur7 === void 0 || _chartInstanceRef$cur7.off("datazoom", handleDataZoom);
333
368
  }
@@ -336,7 +371,7 @@ var ChartLine = function ChartLine(props) {
336
371
  legendScrollRef.current = null;
337
372
  }
338
373
  };
339
- }, [chartOption, handleResize, onClick, onLegendSelectChanged, onDataZoomWindowChanged, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
374
+ }, [chartOption, handleResize, onClickRef, onLegendSelectChangedRef, hasOnClick, hasOnLegendSelectChanged, hasOnDataZoomWindowChanged, emitZoomWindow, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
340
375
  // 图例是否接管滚轮由它推导,漏了会拿到过期的滚动配置
341
376
  normalizedScroll, containerReady]);
342
377
 
@@ -356,13 +391,13 @@ var ChartLine = function ChartLine(props) {
356
391
  startIndex: normalizedScroll.startIndex,
357
392
  autoPlay: normalizedScroll.autoScroll,
358
393
  enableWheelScroll: enableWheelScroll,
359
- onWindowChange: onDataZoomWindowChanged ? function (startPercent, endPercent) {
360
- return onDataZoomWindowChanged(percentWindowToIndexWindow(startPercent, endPercent, xAxisData.length));
394
+ onWindowChange: hasOnDataZoomWindowChanged ? function (startPercent, endPercent) {
395
+ return emitZoomWindow(percentWindowToIndexWindow(startPercent, endPercent, xAxisData.length));
361
396
  } : undefined
362
397
  }),
363
398
  dispose = _autoScrollByItem.dispose;
364
399
  return dispose;
365
- }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, onDataZoomWindowChanged]);
400
+ }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, hasOnDataZoomWindowChanged, emitZoomWindow]);
366
401
 
367
402
  // Clean up resources when the component unmounts
368
403
  useEffect(function () {