@hsu-react/ui 2.5.4 → 2.5.5

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 (46) 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 +49 -19
  4. package/es/components/Chart/Bubble/index.js +15 -5
  5. package/es/components/Chart/Heatmap/index.js +15 -5
  6. package/es/components/Chart/Line/index.js +49 -19
  7. package/es/components/Chart/Pie/Pie3D/index.js +15 -5
  8. package/es/components/Chart/Pie/index.js +15 -5
  9. package/es/components/Chart/Sankey/index.js +11 -2
  10. package/es/components/Chart/Tree/index.js +11 -2
  11. package/es/components/Select/_hooks/useSelectComposition.js +6 -2
  12. package/es/components/Table/_hooks/useAutoScrolling.js +16 -7
  13. package/es/layout/Menu/_hooks/useOnlyLvOneMenu.js +9 -2
  14. package/lib/components/ChainGraph/_hooks/useChainGraphData.js +24 -7
  15. package/lib/components/ChainGraph/_hooks/useChainGraphLayout.js +12 -2
  16. package/lib/components/Chart/Bar/index.js +45 -18
  17. package/lib/components/Chart/Bubble/index.js +14 -5
  18. package/lib/components/Chart/Heatmap/index.js +14 -5
  19. package/lib/components/Chart/Line/index.js +45 -18
  20. package/lib/components/Chart/Pie/Pie3D/index.js +14 -5
  21. package/lib/components/Chart/Pie/index.js +14 -5
  22. package/lib/components/Chart/Sankey/index.js +10 -2
  23. package/lib/components/Chart/Tree/index.js +10 -2
  24. package/lib/components/Select/_hooks/useSelectComposition.js +6 -2
  25. package/lib/components/Table/_hooks/useAutoScrolling.js +16 -7
  26. package/lib/layout/Menu/_hooks/useOnlyLvOneMenu.js +8 -2
  27. package/package.json +1 -1
  28. package/src/__tests__/callbackDepsGuard.test.ts +119 -0
  29. package/src/components/ChainGraph/_hooks/callbacks.test.tsx +155 -0
  30. package/src/components/ChainGraph/_hooks/useChainGraphData.ts +38 -10
  31. package/src/components/ChainGraph/_hooks/useChainGraphLayout.ts +16 -3
  32. package/src/components/Chart/Bar/index.tsx +59 -21
  33. package/src/components/Chart/Bubble/index.tsx +15 -5
  34. package/src/components/Chart/Heatmap/index.tsx +19 -5
  35. package/src/components/Chart/Line/index.tsx +59 -21
  36. package/src/components/Chart/Pie/Pie3D/index.tsx +15 -5
  37. package/src/components/Chart/Pie/index.tsx +15 -5
  38. package/src/components/Chart/Sankey/index.tsx +10 -4
  39. package/src/components/Chart/Tree/index.tsx +10 -4
  40. package/src/components/Chart/callbacks.test.tsx +315 -0
  41. package/src/components/Select/_hooks/useSelectComposition.test.ts +47 -0
  42. package/src/components/Select/_hooks/useSelectComposition.ts +6 -2
  43. package/src/components/Table/_hooks/useAutoScrolling.test.tsx +95 -0
  44. package/src/components/Table/_hooks/useAutoScrolling.ts +16 -7
  45. package/src/layout/Menu/_hooks/useOnlyLvOneMenu.test.tsx +56 -0
  46. 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,7 @@ 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";
19
20
  import { jsx as _jsx } from "react/jsx-runtime";
20
21
  var ChartBar = function ChartBar(props) {
21
22
  var className = props.className,
@@ -62,6 +63,30 @@ var ChartBar = function ChartBar(props) {
62
63
  return resolveChartChrome(isDark);
63
64
  }, [isDark]);
64
65
 
66
+ // 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
67
+ // 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
68
+ // 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
69
+ // 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
70
+ // 这样「从无到有传入回调」仍会重新注册,不会漏。
71
+ var onClickRef = useLatestRef(onClick);
72
+ var onLegendSelectChangedRef = useLatestRef(onLegendSelectChanged);
73
+ var onDataZoomWindowChangedRef = useLatestRef(onDataZoomWindowChanged);
74
+ var hasOnClick = !!onClick;
75
+ var hasOnLegendSelectChanged = !!onLegendSelectChanged;
76
+ var hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
77
+ // 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
78
+ // 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
79
+ var notifiedZoomWindowRef = useRef(null);
80
+ var emitZoomWindow = useCallback(function (next) {
81
+ var _onDataZoomWindowChan;
82
+ var prev = notifiedZoomWindowRef.current;
83
+ if (prev && prev.startIndex === next.startIndex && prev.endIndex === next.endIndex) {
84
+ return;
85
+ }
86
+ notifiedZoomWindowRef.current = next;
87
+ (_onDataZoomWindowChan = onDataZoomWindowChangedRef.current) === null || _onDataZoomWindowChan === void 0 || _onDataZoomWindowChan.call(onDataZoomWindowChangedRef, next);
88
+ }, [onDataZoomWindowChangedRef]);
89
+
65
90
  // Cache the chart option with useMemo
66
91
  var chartOption = useMemo(function () {
67
92
  var _xAxisData$length, _ref, _ref2;
@@ -239,16 +264,16 @@ var ChartBar = function ChartBar(props) {
239
264
  }
240
265
 
241
266
  // 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) {
267
+ if (hasOnDataZoomWindowChanged) {
243
268
  var _chart$getOption3, _xAxisData$length2;
244
269
  var zooms = (_chart$getOption3 = chart.getOption()) === null || _chart$getOption3 === void 0 ? void 0 : _chart$getOption3.dataZoom;
245
270
  var zoom = zooms === null || zooms === void 0 ? void 0 : zooms[0];
246
271
  var totalLen = (_xAxisData$length2 = xAxisData === null || xAxisData === void 0 ? void 0 : xAxisData.length) !== null && _xAxisData$length2 !== void 0 ? _xAxisData$length2 : 0;
247
272
  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));
273
+ emitZoomWindow(percentWindowToIndexWindow(zoom.start, zoom.end, totalLen));
249
274
  } else {
250
275
  // 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({
276
+ emitZoomWindow({
252
277
  startIndex: 0,
253
278
  endIndex: Math.max(0, totalLen - 1)
254
279
  });
@@ -264,17 +289,22 @@ var ChartBar = function ChartBar(props) {
264
289
  resizeObserverRef.current.observe(chartRef.current);
265
290
  }
266
291
 
267
- // Add click event
268
- if (onClick) {
292
+ // Add click event(注册与否看布尔量,实际调用取最新引用)
293
+ var handleClick = function handleClick(event) {
294
+ var _onClickRef$current;
295
+ (_onClickRef$current = onClickRef.current) === null || _onClickRef$current === void 0 || _onClickRef$current.call(onClickRef, event);
296
+ };
297
+ if (hasOnClick) {
269
298
  var _chartInstanceRef$cur2;
270
- (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", onClick);
299
+ (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", handleClick);
271
300
  }
272
301
 
273
302
  // Legend selection event
274
303
  var handleLegendSelectChanged = function handleLegendSelectChanged(params) {
275
- onLegendSelectChanged === null || onLegendSelectChanged === void 0 || onLegendSelectChanged(params.selected);
304
+ var _onLegendSelectChange;
305
+ (_onLegendSelectChange = onLegendSelectChangedRef.current) === null || _onLegendSelectChange === void 0 || _onLegendSelectChange.call(onLegendSelectChangedRef, params.selected);
276
306
  };
277
- if (onLegendSelectChanged) {
307
+ if (hasOnLegendSelectChanged) {
278
308
  var _chartInstanceRef$cur3;
279
309
  (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.on("legendselectchanged", handleLegendSelectChanged);
280
310
  }
@@ -285,16 +315,16 @@ var ChartBar = function ChartBar(props) {
285
315
  var raw = params;
286
316
  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
317
  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({
318
+ emitZoomWindow({
289
319
  startIndex: Math.max(0, Math.round(info.startValue)),
290
320
  endIndex: Math.max(0, Math.round(info.endValue))
291
321
  });
292
322
  } 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
323
  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));
324
+ 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
325
  }
296
326
  };
297
- if (onDataZoomWindowChanged) {
327
+ if (hasOnDataZoomWindowChanged) {
298
328
  var _chartInstanceRef$cur4;
299
329
  (_chartInstanceRef$cur4 = chartInstanceRef.current) === null || _chartInstanceRef$cur4 === void 0 || _chartInstanceRef$cur4.on("datazoom", handleDataZoom);
300
330
  }
@@ -322,15 +352,15 @@ var ChartBar = function ChartBar(props) {
322
352
  // Cleanup function
323
353
  return function () {
324
354
  window.removeEventListener("resize", handleResize);
325
- if (onClick) {
355
+ if (hasOnClick) {
326
356
  var _chartInstanceRef$cur5;
327
- (_chartInstanceRef$cur5 = chartInstanceRef.current) === null || _chartInstanceRef$cur5 === void 0 || _chartInstanceRef$cur5.off("click", onClick);
357
+ (_chartInstanceRef$cur5 = chartInstanceRef.current) === null || _chartInstanceRef$cur5 === void 0 || _chartInstanceRef$cur5.off("click", handleClick);
328
358
  }
329
- if (onLegendSelectChanged) {
359
+ if (hasOnLegendSelectChanged) {
330
360
  var _chartInstanceRef$cur6;
331
361
  (_chartInstanceRef$cur6 = chartInstanceRef.current) === null || _chartInstanceRef$cur6 === void 0 || _chartInstanceRef$cur6.off("legendselectchanged", handleLegendSelectChanged);
332
362
  }
333
- if (onDataZoomWindowChanged) {
363
+ if (hasOnDataZoomWindowChanged) {
334
364
  var _chartInstanceRef$cur7;
335
365
  (_chartInstanceRef$cur7 = chartInstanceRef.current) === null || _chartInstanceRef$cur7 === void 0 || _chartInstanceRef$cur7.off("datazoom", handleDataZoom);
336
366
  }
@@ -339,7 +369,7 @@ var ChartBar = function ChartBar(props) {
339
369
  legendScrollRef.current = null;
340
370
  }
341
371
  };
342
- }, [chartOption, handleResize, onClick, onLegendSelectChanged, onDataZoomWindowChanged, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
372
+ }, [chartOption, handleResize, onClickRef, onLegendSelectChangedRef, hasOnClick, hasOnLegendSelectChanged, hasOnDataZoomWindowChanged, emitZoomWindow, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
343
373
  // 图例是否接管滚轮由它推导,漏了会拿到过期的滚动配置
344
374
  normalizedScroll, containerReady]);
345
375
 
@@ -359,13 +389,13 @@ var ChartBar = function ChartBar(props) {
359
389
  startIndex: normalizedScroll.startIndex,
360
390
  autoPlay: normalizedScroll.autoScroll,
361
391
  enableWheelScroll: enableWheelScroll,
362
- onWindowChange: onDataZoomWindowChanged ? function (startPercent, endPercent) {
363
- return onDataZoomWindowChanged(percentWindowToIndexWindow(startPercent, endPercent, xAxisData.length));
392
+ onWindowChange: hasOnDataZoomWindowChanged ? function (startPercent, endPercent) {
393
+ return emitZoomWindow(percentWindowToIndexWindow(startPercent, endPercent, xAxisData.length));
364
394
  } : undefined
365
395
  }),
366
396
  dispose = _autoScrollByItem.dispose;
367
397
  return dispose;
368
- }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, onDataZoomWindowChanged]);
398
+ }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, hasOnDataZoomWindowChanged, emitZoomWindow]);
369
399
 
370
400
  // Clean up resources when the component unmounts
371
401
  useEffect(function () {
@@ -279,6 +279,12 @@ var Bubble = function Bubble(props) {
279
279
  }, [chartOption, data]);
280
280
  var onChartRef = useLatestRef(onChart);
281
281
 
282
+ // onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
283
+ // 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
284
+ // 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
285
+ var onClickRef = useLatestRef(onClick);
286
+ var hasOnClick = !!onClick;
287
+
282
288
  // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
283
289
  // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
284
290
  // Initialize the chart
@@ -305,20 +311,24 @@ var Bubble = function Bubble(props) {
305
311
  window.addEventListener("resize", handleResize);
306
312
 
307
313
  // Add click event
308
- if (onClick) {
314
+ var handleClick = function handleClick(event) {
315
+ var _onClickRef$current;
316
+ (_onClickRef$current = onClickRef.current) === null || _onClickRef$current === void 0 || _onClickRef$current.call(onClickRef, event);
317
+ };
318
+ if (hasOnClick) {
309
319
  var _chartInstanceRef$cur2;
310
- (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", onClick);
320
+ (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", handleClick);
311
321
  }
312
322
 
313
323
  // Cleanup function
314
324
  return function () {
315
325
  window.removeEventListener("resize", handleResize);
316
- if (onClick) {
326
+ if (hasOnClick) {
317
327
  var _chartInstanceRef$cur3;
318
- (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.off("click", onClick);
328
+ (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.off("click", handleClick);
319
329
  }
320
330
  };
321
- }, [chartOption, containerSize.width, containerSize.height, handleResize, onChartRef, onClick]);
331
+ }, [chartOption, containerSize.width, containerSize.height, handleResize, onChartRef, onClickRef, hasOnClick]);
322
332
 
323
333
  // Dispose the chart instance when the component unmounts
324
334
  useEffect(function () {
@@ -195,6 +195,12 @@ var Heatmap = function Heatmap(props) {
195
195
  }, []);
196
196
  var onChartRef = useLatestRef(onChart);
197
197
 
198
+ // onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
199
+ // 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
200
+ // 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
201
+ var onClickRef = useLatestRef(onClick);
202
+ var hasOnClick = !!onClick;
203
+
198
204
  // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
199
205
  // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
200
206
  // Initialize the chart
@@ -224,20 +230,24 @@ var Heatmap = function Heatmap(props) {
224
230
  }
225
231
 
226
232
  // Add click event
227
- if (onClick) {
233
+ var handleClick = function handleClick(event) {
234
+ var _onClickRef$current;
235
+ (_onClickRef$current = onClickRef.current) === null || _onClickRef$current === void 0 || _onClickRef$current.call(onClickRef, event);
236
+ };
237
+ if (hasOnClick) {
228
238
  var _chartInstanceRef$cur2;
229
- (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", onClick);
239
+ (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", handleClick);
230
240
  }
231
241
 
232
242
  // Cleanup function
233
243
  return function () {
234
244
  window.removeEventListener("resize", handleResize);
235
- if (onClick) {
245
+ if (hasOnClick) {
236
246
  var _chartInstanceRef$cur3;
237
- (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.off("click", onClick);
247
+ (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.off("click", handleClick);
238
248
  }
239
249
  };
240
- }, [chartOption, handleResize, onChartRef, onClick, containerReady]);
250
+ }, [chartOption, handleResize, onChartRef, onClickRef, hasOnClick, containerReady]);
241
251
 
242
252
  // Clean up resources when the component unmounts
243
253
  useEffect(function () {
@@ -16,6 +16,7 @@ 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";
19
20
  import { jsx as _jsx } from "react/jsx-runtime";
20
21
  var ChartLine = function ChartLine(props) {
21
22
  var className = props.className,
@@ -62,6 +63,30 @@ var ChartLine = function ChartLine(props) {
62
63
  return resolveChartChrome(isDark);
63
64
  }, [isDark]);
64
65
 
66
+ // 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
67
+ // 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
68
+ // 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
69
+ // 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
70
+ // 这样「从无到有传入回调」仍会重新注册,不会漏。
71
+ var onClickRef = useLatestRef(onClick);
72
+ var onLegendSelectChangedRef = useLatestRef(onLegendSelectChanged);
73
+ var onDataZoomWindowChangedRef = useLatestRef(onDataZoomWindowChanged);
74
+ var hasOnClick = !!onClick;
75
+ var hasOnLegendSelectChanged = !!onLegendSelectChanged;
76
+ var hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
77
+ // 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
78
+ // 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
79
+ var notifiedZoomWindowRef = useRef(null);
80
+ var emitZoomWindow = useCallback(function (next) {
81
+ var _onDataZoomWindowChan;
82
+ var prev = notifiedZoomWindowRef.current;
83
+ if (prev && prev.startIndex === next.startIndex && prev.endIndex === next.endIndex) {
84
+ return;
85
+ }
86
+ notifiedZoomWindowRef.current = next;
87
+ (_onDataZoomWindowChan = onDataZoomWindowChangedRef.current) === null || _onDataZoomWindowChan === void 0 || _onDataZoomWindowChan.call(onDataZoomWindowChangedRef, next);
88
+ }, [onDataZoomWindowChangedRef]);
89
+
65
90
  // Cache the chart option with useMemo
66
91
  var chartOption = useMemo(function () {
67
92
  var _xAxisData$length, _ref, _ref2;
@@ -236,16 +261,16 @@ var ChartLine = function ChartLine(props) {
236
261
  }
237
262
 
238
263
  // 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) {
264
+ if (hasOnDataZoomWindowChanged) {
240
265
  var _chart$getOption3, _xAxisData$length2;
241
266
  var zooms = (_chart$getOption3 = chart.getOption()) === null || _chart$getOption3 === void 0 ? void 0 : _chart$getOption3.dataZoom;
242
267
  var zoom = zooms === null || zooms === void 0 ? void 0 : zooms[0];
243
268
  var totalLen = (_xAxisData$length2 = xAxisData === null || xAxisData === void 0 ? void 0 : xAxisData.length) !== null && _xAxisData$length2 !== void 0 ? _xAxisData$length2 : 0;
244
269
  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));
270
+ emitZoomWindow(percentWindowToIndexWindow(zoom.start, zoom.end, totalLen));
246
271
  } else {
247
272
  // 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({
273
+ emitZoomWindow({
249
274
  startIndex: 0,
250
275
  endIndex: Math.max(0, totalLen - 1)
251
276
  });
@@ -261,17 +286,22 @@ var ChartLine = function ChartLine(props) {
261
286
  resizeObserverRef.current.observe(chartRef.current);
262
287
  }
263
288
 
264
- // Add click event
265
- if (onClick) {
289
+ // Add click event(注册与否看布尔量,实际调用取最新引用)
290
+ var handleClick = function handleClick(event) {
291
+ var _onClickRef$current;
292
+ (_onClickRef$current = onClickRef.current) === null || _onClickRef$current === void 0 || _onClickRef$current.call(onClickRef, event);
293
+ };
294
+ if (hasOnClick) {
266
295
  var _chartInstanceRef$cur2;
267
- (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", onClick);
296
+ (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", handleClick);
268
297
  }
269
298
 
270
299
  // Legend selection event
271
300
  var handleLegendSelectChanged = function handleLegendSelectChanged(params) {
272
- onLegendSelectChanged === null || onLegendSelectChanged === void 0 || onLegendSelectChanged(params.selected);
301
+ var _onLegendSelectChange;
302
+ (_onLegendSelectChange = onLegendSelectChangedRef.current) === null || _onLegendSelectChange === void 0 || _onLegendSelectChange.call(onLegendSelectChangedRef, params.selected);
273
303
  };
274
- if (onLegendSelectChanged) {
304
+ if (hasOnLegendSelectChanged) {
275
305
  var _chartInstanceRef$cur3;
276
306
  (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.on("legendselectchanged", handleLegendSelectChanged);
277
307
  }
@@ -282,16 +312,16 @@ var ChartLine = function ChartLine(props) {
282
312
  var raw = params;
283
313
  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
314
  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({
315
+ emitZoomWindow({
286
316
  startIndex: Math.max(0, Math.round(info.startValue)),
287
317
  endIndex: Math.max(0, Math.round(info.endValue))
288
318
  });
289
319
  } 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
320
  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));
321
+ 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
322
  }
293
323
  };
294
- if (onDataZoomWindowChanged) {
324
+ if (hasOnDataZoomWindowChanged) {
295
325
  var _chartInstanceRef$cur4;
296
326
  (_chartInstanceRef$cur4 = chartInstanceRef.current) === null || _chartInstanceRef$cur4 === void 0 || _chartInstanceRef$cur4.on("datazoom", handleDataZoom);
297
327
  }
@@ -319,15 +349,15 @@ var ChartLine = function ChartLine(props) {
319
349
  // Cleanup function
320
350
  return function () {
321
351
  window.removeEventListener("resize", handleResize);
322
- if (onClick) {
352
+ if (hasOnClick) {
323
353
  var _chartInstanceRef$cur5;
324
- (_chartInstanceRef$cur5 = chartInstanceRef.current) === null || _chartInstanceRef$cur5 === void 0 || _chartInstanceRef$cur5.off("click", onClick);
354
+ (_chartInstanceRef$cur5 = chartInstanceRef.current) === null || _chartInstanceRef$cur5 === void 0 || _chartInstanceRef$cur5.off("click", handleClick);
325
355
  }
326
- if (onLegendSelectChanged) {
356
+ if (hasOnLegendSelectChanged) {
327
357
  var _chartInstanceRef$cur6;
328
358
  (_chartInstanceRef$cur6 = chartInstanceRef.current) === null || _chartInstanceRef$cur6 === void 0 || _chartInstanceRef$cur6.off("legendselectchanged", handleLegendSelectChanged);
329
359
  }
330
- if (onDataZoomWindowChanged) {
360
+ if (hasOnDataZoomWindowChanged) {
331
361
  var _chartInstanceRef$cur7;
332
362
  (_chartInstanceRef$cur7 = chartInstanceRef.current) === null || _chartInstanceRef$cur7 === void 0 || _chartInstanceRef$cur7.off("datazoom", handleDataZoom);
333
363
  }
@@ -336,7 +366,7 @@ var ChartLine = function ChartLine(props) {
336
366
  legendScrollRef.current = null;
337
367
  }
338
368
  };
339
- }, [chartOption, handleResize, onClick, onLegendSelectChanged, onDataZoomWindowChanged, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
369
+ }, [chartOption, handleResize, onClickRef, onLegendSelectChangedRef, hasOnClick, hasOnLegendSelectChanged, hasOnDataZoomWindowChanged, emitZoomWindow, xAxisData, enableLegendAutoScroll, legendVisibleCount, legendScrollInterval,
340
370
  // 图例是否接管滚轮由它推导,漏了会拿到过期的滚动配置
341
371
  normalizedScroll, containerReady]);
342
372
 
@@ -356,13 +386,13 @@ var ChartLine = function ChartLine(props) {
356
386
  startIndex: normalizedScroll.startIndex,
357
387
  autoPlay: normalizedScroll.autoScroll,
358
388
  enableWheelScroll: enableWheelScroll,
359
- onWindowChange: onDataZoomWindowChanged ? function (startPercent, endPercent) {
360
- return onDataZoomWindowChanged(percentWindowToIndexWindow(startPercent, endPercent, xAxisData.length));
389
+ onWindowChange: hasOnDataZoomWindowChanged ? function (startPercent, endPercent) {
390
+ return emitZoomWindow(percentWindowToIndexWindow(startPercent, endPercent, xAxisData.length));
361
391
  } : undefined
362
392
  }),
363
393
  dispose = _autoScrollByItem.dispose;
364
394
  return dispose;
365
- }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, onDataZoomWindowChanged]);
395
+ }, [dataZoom, normalizedScroll, sliderDataZoom, xAxisData, hasOnDataZoomWindowChanged, emitZoomWindow]);
366
396
 
367
397
  // Clean up resources when the component unmounts
368
398
  useEffect(function () {
@@ -92,6 +92,12 @@ var ChartPie3D = function ChartPie3D(props) {
92
92
  });
93
93
  var onChartRef = useLatestRef(onChart);
94
94
 
95
+ // onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑会重建
96
+ // 3D 饼的 option 并重播动画。注册与否看布尔量(保证「从无到有传入」仍会注册),
97
+ // 实际调用取 ref 里的最新引用。
98
+ var onClickRef = useLatestRef(onClick);
99
+ var hasOnClick = !!onClick;
100
+
95
101
  // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
96
102
  // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
97
103
  useEffect(function () {
@@ -112,20 +118,24 @@ var ChartPie3D = function ChartPie3D(props) {
112
118
  }
113
119
  chart.on("mouseover", handleMouseOver);
114
120
  chart.on("globalout", handleGlobalOut);
115
- if (onClick) {
116
- chart.on("click", onClick);
121
+ var handleClick = function handleClick(event) {
122
+ var _onClickRef$current;
123
+ (_onClickRef$current = onClickRef.current) === null || _onClickRef$current === void 0 || _onClickRef$current.call(onClickRef, event);
124
+ };
125
+ if (hasOnClick) {
126
+ chart.on("click", handleClick);
117
127
  }
118
128
  return function () {
119
129
  window.removeEventListener("resize", handleResize);
120
130
  if (chart) {
121
131
  chart.off("mouseover", handleMouseOver);
122
132
  chart.off("globalout", handleGlobalOut);
123
- if (onClick) {
124
- chart.off("click", onClick);
133
+ if (hasOnClick) {
134
+ chart.off("click", handleClick);
125
135
  }
126
136
  }
127
137
  };
128
- }, [chartOption, handleResize, handleMouseOver, handleGlobalOut, onChartRef, onClick, containerReady]);
138
+ }, [chartOption, handleResize, handleMouseOver, handleGlobalOut, onChartRef, onClickRef, hasOnClick, containerReady]);
129
139
  useEffect(function () {
130
140
  return function () {
131
141
  if (resizeObserverRef.current) {
@@ -210,6 +210,12 @@ var ChartPie = function ChartPie(props) {
210
210
  }, []);
211
211
  var onChartRef = useLatestRef(onChart);
212
212
 
213
+ // onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
214
+ // 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
215
+ // 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
216
+ var onClickRef = useLatestRef(onClick);
217
+ var hasOnClick = !!onClick;
218
+
213
219
  // 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
214
220
  // 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
215
221
  // Initialize the chart
@@ -239,9 +245,13 @@ var ChartPie = function ChartPie(props) {
239
245
  }
240
246
 
241
247
  // Add click event
242
- if (onClick) {
248
+ var handleClick = function handleClick(event) {
249
+ var _onClickRef$current;
250
+ (_onClickRef$current = onClickRef.current) === null || _onClickRef$current === void 0 || _onClickRef$current.call(onClickRef, event);
251
+ };
252
+ if (hasOnClick) {
243
253
  var _chartInstanceRef$cur2;
244
- (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", onClick);
254
+ (_chartInstanceRef$cur2 = chartInstanceRef.current) === null || _chartInstanceRef$cur2 === void 0 || _chartInstanceRef$cur2.on("click", handleClick);
245
255
  }
246
256
 
247
257
  // Legend auto-scroll
@@ -262,9 +272,9 @@ var ChartPie = function ChartPie(props) {
262
272
  // Cleanup function
263
273
  return function () {
264
274
  window.removeEventListener("resize", handleResize);
265
- if (onClick) {
275
+ if (hasOnClick) {
266
276
  var _chartInstanceRef$cur3;
267
- (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.off("click", onClick);
277
+ (_chartInstanceRef$cur3 = chartInstanceRef.current) === null || _chartInstanceRef$cur3 === void 0 || _chartInstanceRef$cur3.off("click", handleClick);
268
278
  }
269
279
 
270
280
  // Clean up legend scrolling
@@ -273,7 +283,7 @@ var ChartPie = function ChartPie(props) {
273
283
  legendScrollRef.current = null;
274
284
  }
275
285
  };
276
- }, [chartOption, handleResize, onChartRef, onClick, enableLegendAutoScroll, seriesData, legendVisibleCount, legendScrollInterval, containerReady]);
286
+ }, [chartOption, handleResize, onChartRef, onClickRef, hasOnClick, enableLegendAutoScroll, seriesData, legendVisibleCount, legendScrollInterval, containerReady]);
277
287
 
278
288
  // Clean up resources when the component unmounts
279
289
  useEffect(function () {
@@ -14,6 +14,7 @@ import React, { useCallback, useEffect, useMemo, useRef } from "react";
14
14
  import useContainerReady from "../_hooks/useContainerReady";
15
15
  import styles from "../index.module.scss";
16
16
  import * as echarts from "echarts";
17
+ import useLatestRef from "../../../hooks/useLatestRef";
17
18
  import { jsx as _jsx } from "react/jsx-runtime";
18
19
  var Sankey = function Sankey(props) {
19
20
  var className = props.className,
@@ -103,6 +104,13 @@ var Sankey = function Sankey(props) {
103
104
  return option;
104
105
  }, [seriesData, seriesLinks, series]);
105
106
 
107
+ // getImage 只是「出图后把 dataURL 交出去」的出口,既不决定图怎么画,
108
+ // 也不该决定要不要重画。它进依赖数组会让 effect 跟着重跑 → setOption(notMerge)
109
+ // → 图重绘 → 再触发一次 finished → 再回调;消费方把图存进 state 就是死循环。
110
+ // 而且 finished 监听只在首次 init 时注册,闭包里锁死的是首帧的 getImage,
111
+ // 后续换了引用永远调不到 —— 依赖数组写了它也没用,是条死依赖。走 ref 两个问题一起解决。
112
+ var getImageRef = useLatestRef(getImage);
113
+
106
114
  // Callback handling chart resize
107
115
  var handleResize = useCallback(function () {
108
116
  var _chartInstanceRef$cur;
@@ -125,7 +133,8 @@ var Sankey = function Sankey(props) {
125
133
 
126
134
  // Attach the finished event listener on first initialization
127
135
  chart.on("finished", function () {
128
- getImage === null || getImage === void 0 || getImage(chart.getDataURL({
136
+ var _getImageRef$current;
137
+ (_getImageRef$current = getImageRef.current) === null || _getImageRef$current === void 0 || _getImageRef$current.call(getImageRef, chart.getDataURL({
129
138
  type: "png",
130
139
  pixelRatio: 1,
131
140
  backgroundColor: "#fff"
@@ -149,7 +158,7 @@ var Sankey = function Sankey(props) {
149
158
  return function () {
150
159
  window.removeEventListener("resize", handleResize);
151
160
  };
152
- }, [chartOption, handleResize, getImage, containerReady]);
161
+ }, [chartOption, handleResize, getImageRef, containerReady]);
153
162
 
154
163
  // Clean up resources on unmount
155
164
  useEffect(function () {