@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.
- 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 +49 -19
- package/es/components/Chart/Bubble/index.js +15 -5
- package/es/components/Chart/Heatmap/index.js +15 -5
- package/es/components/Chart/Line/index.js +49 -19
- package/es/components/Chart/Pie/Pie3D/index.js +15 -5
- package/es/components/Chart/Pie/index.js +15 -5
- 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/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 +45 -18
- package/lib/components/Chart/Bubble/index.js +14 -5
- package/lib/components/Chart/Heatmap/index.js +14 -5
- package/lib/components/Chart/Line/index.js +45 -18
- package/lib/components/Chart/Pie/Pie3D/index.js +14 -5
- package/lib/components/Chart/Pie/index.js +14 -5
- 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/layout/Menu/_hooks/useOnlyLvOneMenu.js +8 -2
- package/package.json +1 -1
- package/src/__tests__/callbackDepsGuard.test.ts +119 -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 +59 -21
- package/src/components/Chart/Bubble/index.tsx +15 -5
- package/src/components/Chart/Heatmap/index.tsx +19 -5
- package/src/components/Chart/Line/index.tsx +59 -21
- package/src/components/Chart/Pie/Pie3D/index.tsx +15 -5
- package/src/components/Chart/Pie/index.tsx +15 -5
- 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/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/layout/Menu/_hooks/useOnlyLvOneMenu.test.tsx +56 -0
- package/src/layout/Menu/_hooks/useOnlyLvOneMenu.ts +8 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
|
2
2
|
import ChainGraphServices from "../ChainGraphServices";
|
|
3
|
+
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
3
4
|
|
|
4
5
|
interface UseChainGraphLayoutProps {
|
|
5
6
|
graph: ChainGraphServices | null;
|
|
@@ -11,9 +12,21 @@ interface UseChainGraphLayoutProps {
|
|
|
11
12
|
export function useChainGraphLayout(props: UseChainGraphLayoutProps) {
|
|
12
13
|
const { graph, octopus, rootLevel, getImage } = props;
|
|
13
14
|
|
|
15
|
+
// getImage 只是「布局完成后把截图 dataURL 交出去」的出口,不是布局的输入。
|
|
16
|
+
// 它进依赖数组会让消费方每传一次内联箭头就重新 changeLayout 一次 —— 整张图重排;
|
|
17
|
+
// 消费方再把截图存进 state,就变成「重排 → 出图 → setState → 新引用 → 再重排」的死循环。
|
|
18
|
+
// 用引用恒定的转发函数注册,调用时再取最新的 getImage。
|
|
19
|
+
const getImageRef = useLatestRef(getImage);
|
|
20
|
+
const emitImage = useCallback(
|
|
21
|
+
(img: string) => {
|
|
22
|
+
getImageRef.current?.(img);
|
|
23
|
+
},
|
|
24
|
+
[getImageRef],
|
|
25
|
+
);
|
|
26
|
+
|
|
14
27
|
useEffect(() => {
|
|
15
28
|
if (graph && octopus !== undefined) {
|
|
16
|
-
graph.changeLayout(octopus, rootLevel,
|
|
29
|
+
graph.changeLayout(octopus, rootLevel, emitImage);
|
|
17
30
|
}
|
|
18
|
-
}, [graph, octopus, rootLevel,
|
|
31
|
+
}, [graph, octopus, rootLevel, emitImage]);
|
|
19
32
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "../_utils/cartesian";
|
|
25
25
|
import { resolveChartChrome } from "../_utils/chartTheme";
|
|
26
26
|
import useIsDark from "../../../hooks/useIsDark";
|
|
27
|
+
import useLatestRef from "../../../hooks/useLatestRef";
|
|
27
28
|
|
|
28
29
|
export interface ChartBarProps extends ChartCommonProps {
|
|
29
30
|
chartTitle?: string;
|
|
@@ -89,6 +90,36 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
89
90
|
const isDark = useIsDark();
|
|
90
91
|
const chrome = useMemo(() => resolveChartChrome(isDark), [isDark]);
|
|
91
92
|
|
|
93
|
+
// 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
|
|
94
|
+
// 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
|
|
95
|
+
// 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
|
|
96
|
+
// 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
|
|
97
|
+
// 这样「从无到有传入回调」仍会重新注册,不会漏。
|
|
98
|
+
const onClickRef = useLatestRef(onClick);
|
|
99
|
+
const onLegendSelectChangedRef = useLatestRef(onLegendSelectChanged);
|
|
100
|
+
const onDataZoomWindowChangedRef = useLatestRef(onDataZoomWindowChanged);
|
|
101
|
+
const hasOnClick = !!onClick;
|
|
102
|
+
const hasOnLegendSelectChanged = !!onLegendSelectChanged;
|
|
103
|
+
const hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
|
|
104
|
+
// 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
|
|
105
|
+
// 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
|
|
106
|
+
const notifiedZoomWindowRef = useRef<DataZoomIndexWindow | null>(null);
|
|
107
|
+
const emitZoomWindow = useCallback(
|
|
108
|
+
(next: DataZoomIndexWindow) => {
|
|
109
|
+
const prev = notifiedZoomWindowRef.current;
|
|
110
|
+
if (
|
|
111
|
+
prev &&
|
|
112
|
+
prev.startIndex === next.startIndex &&
|
|
113
|
+
prev.endIndex === next.endIndex
|
|
114
|
+
) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
notifiedZoomWindowRef.current = next;
|
|
118
|
+
onDataZoomWindowChangedRef.current?.(next);
|
|
119
|
+
},
|
|
120
|
+
[onDataZoomWindowChangedRef],
|
|
121
|
+
);
|
|
122
|
+
|
|
92
123
|
// Cache the chart option with useMemo
|
|
93
124
|
const chartOption = useMemo(() => {
|
|
94
125
|
const def_xAxis = createDefaultCategoryXAxis(xAxisData, chrome);
|
|
@@ -328,18 +359,18 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
328
359
|
}
|
|
329
360
|
|
|
330
361
|
// Sync the current visible window to the caller (on initial render and after restore), used to recalculate axes based on the displayed portion
|
|
331
|
-
if (
|
|
362
|
+
if (hasOnDataZoomWindowChanged) {
|
|
332
363
|
const zooms = (chart.getOption() as ChartsOption | undefined)
|
|
333
364
|
?.dataZoom as Array<{ start?: number; end?: number }> | undefined;
|
|
334
365
|
const zoom = zooms?.[0];
|
|
335
366
|
const totalLen = xAxisData?.length ?? 0;
|
|
336
367
|
if (typeof zoom?.start === "number" && typeof zoom?.end === "number") {
|
|
337
|
-
|
|
368
|
+
emitZoomWindow(
|
|
338
369
|
percentWindowToIndexWindow(zoom.start, zoom.end, totalLen),
|
|
339
370
|
);
|
|
340
371
|
} else {
|
|
341
372
|
// 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
|
|
342
|
-
|
|
373
|
+
emitZoomWindow({
|
|
343
374
|
startIndex: 0,
|
|
344
375
|
endIndex: Math.max(0, totalLen - 1),
|
|
345
376
|
});
|
|
@@ -355,18 +386,21 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
355
386
|
resizeObserverRef.current.observe(chartRef.current);
|
|
356
387
|
}
|
|
357
388
|
|
|
358
|
-
// Add click event
|
|
359
|
-
|
|
360
|
-
|
|
389
|
+
// Add click event(注册与否看布尔量,实际调用取最新引用)
|
|
390
|
+
const handleClick = (event: echarts.ECElementEvent) => {
|
|
391
|
+
onClickRef.current?.(event);
|
|
392
|
+
};
|
|
393
|
+
if (hasOnClick) {
|
|
394
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
361
395
|
}
|
|
362
396
|
|
|
363
397
|
// Legend selection event
|
|
364
398
|
const handleLegendSelectChanged = (params: unknown) => {
|
|
365
|
-
|
|
399
|
+
onLegendSelectChangedRef.current?.(
|
|
366
400
|
(params as { selected: Record<string, boolean> }).selected,
|
|
367
401
|
);
|
|
368
402
|
};
|
|
369
|
-
if (
|
|
403
|
+
if (hasOnLegendSelectChanged) {
|
|
370
404
|
chartInstanceRef.current?.on(
|
|
371
405
|
"legendselectchanged",
|
|
372
406
|
handleLegendSelectChanged,
|
|
@@ -392,7 +426,7 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
392
426
|
typeof info?.startValue === "number" &&
|
|
393
427
|
typeof info?.endValue === "number"
|
|
394
428
|
) {
|
|
395
|
-
|
|
429
|
+
emitZoomWindow({
|
|
396
430
|
startIndex: Math.max(0, Math.round(info.startValue)),
|
|
397
431
|
endIndex: Math.max(0, Math.round(info.endValue)),
|
|
398
432
|
});
|
|
@@ -400,7 +434,7 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
400
434
|
typeof info?.start === "number" &&
|
|
401
435
|
typeof info?.end === "number"
|
|
402
436
|
) {
|
|
403
|
-
|
|
437
|
+
emitZoomWindow(
|
|
404
438
|
percentWindowToIndexWindow(
|
|
405
439
|
info.start,
|
|
406
440
|
info.end,
|
|
@@ -409,7 +443,7 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
409
443
|
);
|
|
410
444
|
}
|
|
411
445
|
};
|
|
412
|
-
if (
|
|
446
|
+
if (hasOnDataZoomWindowChanged) {
|
|
413
447
|
chartInstanceRef.current?.on("datazoom", handleDataZoom);
|
|
414
448
|
}
|
|
415
449
|
|
|
@@ -445,18 +479,18 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
445
479
|
return () => {
|
|
446
480
|
window.removeEventListener("resize", handleResize);
|
|
447
481
|
|
|
448
|
-
if (
|
|
449
|
-
chartInstanceRef.current?.off("click",
|
|
482
|
+
if (hasOnClick) {
|
|
483
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
450
484
|
}
|
|
451
485
|
|
|
452
|
-
if (
|
|
486
|
+
if (hasOnLegendSelectChanged) {
|
|
453
487
|
chartInstanceRef.current?.off(
|
|
454
488
|
"legendselectchanged",
|
|
455
489
|
handleLegendSelectChanged,
|
|
456
490
|
);
|
|
457
491
|
}
|
|
458
492
|
|
|
459
|
-
if (
|
|
493
|
+
if (hasOnDataZoomWindowChanged) {
|
|
460
494
|
chartInstanceRef.current?.off("datazoom", handleDataZoom);
|
|
461
495
|
}
|
|
462
496
|
|
|
@@ -468,9 +502,12 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
468
502
|
}, [
|
|
469
503
|
chartOption,
|
|
470
504
|
handleResize,
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
505
|
+
onClickRef,
|
|
506
|
+
onLegendSelectChangedRef,
|
|
507
|
+
hasOnClick,
|
|
508
|
+
hasOnLegendSelectChanged,
|
|
509
|
+
hasOnDataZoomWindowChanged,
|
|
510
|
+
emitZoomWindow,
|
|
474
511
|
xAxisData,
|
|
475
512
|
enableLegendAutoScroll,
|
|
476
513
|
legendVisibleCount,
|
|
@@ -507,9 +544,9 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
507
544
|
startIndex: normalizedScroll.startIndex,
|
|
508
545
|
autoPlay: normalizedScroll.autoScroll,
|
|
509
546
|
enableWheelScroll,
|
|
510
|
-
onWindowChange:
|
|
547
|
+
onWindowChange: hasOnDataZoomWindowChanged
|
|
511
548
|
? (startPercent, endPercent) =>
|
|
512
|
-
|
|
549
|
+
emitZoomWindow(
|
|
513
550
|
percentWindowToIndexWindow(
|
|
514
551
|
startPercent,
|
|
515
552
|
endPercent,
|
|
@@ -525,7 +562,8 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
525
562
|
normalizedScroll,
|
|
526
563
|
sliderDataZoom,
|
|
527
564
|
xAxisData,
|
|
528
|
-
|
|
565
|
+
hasOnDataZoomWindowChanged,
|
|
566
|
+
emitZoomWindow,
|
|
529
567
|
]);
|
|
530
568
|
|
|
531
569
|
// Clean up resources when the component unmounts
|
|
@@ -373,6 +373,12 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
373
373
|
|
|
374
374
|
const onChartRef = useLatestRef(onChart);
|
|
375
375
|
|
|
376
|
+
// onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
|
|
377
|
+
// 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
|
|
378
|
+
// 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
|
|
379
|
+
const onClickRef = useLatestRef(onClick);
|
|
380
|
+
const hasOnClick = !!onClick;
|
|
381
|
+
|
|
376
382
|
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
377
383
|
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
378
384
|
// Initialize the chart
|
|
@@ -398,16 +404,19 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
398
404
|
window.addEventListener("resize", handleResize);
|
|
399
405
|
|
|
400
406
|
// Add click event
|
|
401
|
-
|
|
402
|
-
|
|
407
|
+
const handleClick = (event: echarts.ECElementEvent) => {
|
|
408
|
+
onClickRef.current?.(event);
|
|
409
|
+
};
|
|
410
|
+
if (hasOnClick) {
|
|
411
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
403
412
|
}
|
|
404
413
|
|
|
405
414
|
// Cleanup function
|
|
406
415
|
return () => {
|
|
407
416
|
window.removeEventListener("resize", handleResize);
|
|
408
417
|
|
|
409
|
-
if (
|
|
410
|
-
chartInstanceRef.current?.off("click",
|
|
418
|
+
if (hasOnClick) {
|
|
419
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
411
420
|
}
|
|
412
421
|
};
|
|
413
422
|
}, [
|
|
@@ -416,7 +425,8 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
416
425
|
containerSize.height,
|
|
417
426
|
handleResize,
|
|
418
427
|
onChartRef,
|
|
419
|
-
|
|
428
|
+
onClickRef,
|
|
429
|
+
hasOnClick,
|
|
420
430
|
]);
|
|
421
431
|
|
|
422
432
|
// Dispose the chart instance when the component unmounts
|
|
@@ -269,6 +269,12 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
|
269
269
|
|
|
270
270
|
const onChartRef = useLatestRef(onChart);
|
|
271
271
|
|
|
272
|
+
// onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
|
|
273
|
+
// 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
|
|
274
|
+
// 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
|
|
275
|
+
const onClickRef = useLatestRef(onClick);
|
|
276
|
+
const hasOnClick = !!onClick;
|
|
277
|
+
|
|
272
278
|
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
273
279
|
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
274
280
|
// Initialize the chart
|
|
@@ -297,19 +303,27 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
|
297
303
|
}
|
|
298
304
|
|
|
299
305
|
// Add click event
|
|
300
|
-
|
|
301
|
-
|
|
306
|
+
const handleClick = (event: echarts.ECElementEvent) => {
|
|
307
|
+
onClickRef.current?.(event);
|
|
308
|
+
};
|
|
309
|
+
if (hasOnClick) {
|
|
310
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
302
311
|
}
|
|
303
312
|
|
|
304
313
|
// Cleanup function
|
|
305
314
|
return () => {
|
|
306
315
|
window.removeEventListener("resize", handleResize);
|
|
307
316
|
|
|
308
|
-
if (
|
|
309
|
-
chartInstanceRef.current?.off("click",
|
|
317
|
+
if (hasOnClick) {
|
|
318
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
310
319
|
}
|
|
311
320
|
};
|
|
312
|
-
}, [
|
|
321
|
+
}, [
|
|
322
|
+
chartOption,
|
|
323
|
+
handleResize,
|
|
324
|
+
onChartRef,
|
|
325
|
+
onClickRef,
|
|
326
|
+
hasOnClick,
|
|
313
327
|
containerReady,
|
|
314
328
|
]);
|
|
315
329
|
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "../_utils/cartesian";
|
|
25
25
|
import { resolveChartChrome } from "../_utils/chartTheme";
|
|
26
26
|
import useIsDark from "../../../hooks/useIsDark";
|
|
27
|
+
import useLatestRef from "../../../hooks/useLatestRef";
|
|
27
28
|
|
|
28
29
|
export interface ChartLineProps extends ChartCommonProps {
|
|
29
30
|
chartTitle?: string;
|
|
@@ -90,6 +91,36 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
90
91
|
const isDark = useIsDark();
|
|
91
92
|
const chrome = useMemo(() => resolveChartChrome(isDark), [isDark]);
|
|
92
93
|
|
|
94
|
+
// 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
|
|
95
|
+
// 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
|
|
96
|
+
// 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
|
|
97
|
+
// 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
|
|
98
|
+
// 这样「从无到有传入回调」仍会重新注册,不会漏。
|
|
99
|
+
const onClickRef = useLatestRef(onClick);
|
|
100
|
+
const onLegendSelectChangedRef = useLatestRef(onLegendSelectChanged);
|
|
101
|
+
const onDataZoomWindowChangedRef = useLatestRef(onDataZoomWindowChanged);
|
|
102
|
+
const hasOnClick = !!onClick;
|
|
103
|
+
const hasOnLegendSelectChanged = !!onLegendSelectChanged;
|
|
104
|
+
const hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
|
|
105
|
+
// 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
|
|
106
|
+
// 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
|
|
107
|
+
const notifiedZoomWindowRef = useRef<DataZoomIndexWindow | null>(null);
|
|
108
|
+
const emitZoomWindow = useCallback(
|
|
109
|
+
(next: DataZoomIndexWindow) => {
|
|
110
|
+
const prev = notifiedZoomWindowRef.current;
|
|
111
|
+
if (
|
|
112
|
+
prev &&
|
|
113
|
+
prev.startIndex === next.startIndex &&
|
|
114
|
+
prev.endIndex === next.endIndex
|
|
115
|
+
) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
notifiedZoomWindowRef.current = next;
|
|
119
|
+
onDataZoomWindowChangedRef.current?.(next);
|
|
120
|
+
},
|
|
121
|
+
[onDataZoomWindowChangedRef],
|
|
122
|
+
);
|
|
123
|
+
|
|
93
124
|
// Cache the chart option with useMemo
|
|
94
125
|
const chartOption = useMemo(() => {
|
|
95
126
|
const def_xAxis = createDefaultCategoryXAxis(xAxisData, chrome);
|
|
@@ -326,18 +357,18 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
326
357
|
}
|
|
327
358
|
|
|
328
359
|
// Sync the current visible window to the caller (on initial render and after restore), used to recalculate axes based on the displayed portion
|
|
329
|
-
if (
|
|
360
|
+
if (hasOnDataZoomWindowChanged) {
|
|
330
361
|
const zooms = (chart.getOption() as ChartsOption | undefined)
|
|
331
362
|
?.dataZoom as Array<{ start?: number; end?: number }> | undefined;
|
|
332
363
|
const zoom = zooms?.[0];
|
|
333
364
|
const totalLen = xAxisData?.length ?? 0;
|
|
334
365
|
if (typeof zoom?.start === "number" && typeof zoom?.end === "number") {
|
|
335
|
-
|
|
366
|
+
emitZoomWindow(
|
|
336
367
|
percentWindowToIndexWindow(zoom.start, zoom.end, totalLen),
|
|
337
368
|
);
|
|
338
369
|
} else {
|
|
339
370
|
// 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
|
|
340
|
-
|
|
371
|
+
emitZoomWindow({
|
|
341
372
|
startIndex: 0,
|
|
342
373
|
endIndex: Math.max(0, totalLen - 1),
|
|
343
374
|
});
|
|
@@ -353,18 +384,21 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
353
384
|
resizeObserverRef.current.observe(chartRef.current);
|
|
354
385
|
}
|
|
355
386
|
|
|
356
|
-
// Add click event
|
|
357
|
-
|
|
358
|
-
|
|
387
|
+
// Add click event(注册与否看布尔量,实际调用取最新引用)
|
|
388
|
+
const handleClick = (event: echarts.ECElementEvent) => {
|
|
389
|
+
onClickRef.current?.(event);
|
|
390
|
+
};
|
|
391
|
+
if (hasOnClick) {
|
|
392
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
359
393
|
}
|
|
360
394
|
|
|
361
395
|
// Legend selection event
|
|
362
396
|
const handleLegendSelectChanged = (params: unknown) => {
|
|
363
|
-
|
|
397
|
+
onLegendSelectChangedRef.current?.(
|
|
364
398
|
(params as { selected: Record<string, boolean> }).selected,
|
|
365
399
|
);
|
|
366
400
|
};
|
|
367
|
-
if (
|
|
401
|
+
if (hasOnLegendSelectChanged) {
|
|
368
402
|
chartInstanceRef.current?.on(
|
|
369
403
|
"legendselectchanged",
|
|
370
404
|
handleLegendSelectChanged,
|
|
@@ -390,7 +424,7 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
390
424
|
typeof info?.startValue === "number" &&
|
|
391
425
|
typeof info?.endValue === "number"
|
|
392
426
|
) {
|
|
393
|
-
|
|
427
|
+
emitZoomWindow({
|
|
394
428
|
startIndex: Math.max(0, Math.round(info.startValue)),
|
|
395
429
|
endIndex: Math.max(0, Math.round(info.endValue)),
|
|
396
430
|
});
|
|
@@ -398,7 +432,7 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
398
432
|
typeof info?.start === "number" &&
|
|
399
433
|
typeof info?.end === "number"
|
|
400
434
|
) {
|
|
401
|
-
|
|
435
|
+
emitZoomWindow(
|
|
402
436
|
percentWindowToIndexWindow(
|
|
403
437
|
info.start,
|
|
404
438
|
info.end,
|
|
@@ -407,7 +441,7 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
407
441
|
);
|
|
408
442
|
}
|
|
409
443
|
};
|
|
410
|
-
if (
|
|
444
|
+
if (hasOnDataZoomWindowChanged) {
|
|
411
445
|
chartInstanceRef.current?.on("datazoom", handleDataZoom);
|
|
412
446
|
}
|
|
413
447
|
|
|
@@ -443,18 +477,18 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
443
477
|
return () => {
|
|
444
478
|
window.removeEventListener("resize", handleResize);
|
|
445
479
|
|
|
446
|
-
if (
|
|
447
|
-
chartInstanceRef.current?.off("click",
|
|
480
|
+
if (hasOnClick) {
|
|
481
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
448
482
|
}
|
|
449
483
|
|
|
450
|
-
if (
|
|
484
|
+
if (hasOnLegendSelectChanged) {
|
|
451
485
|
chartInstanceRef.current?.off(
|
|
452
486
|
"legendselectchanged",
|
|
453
487
|
handleLegendSelectChanged,
|
|
454
488
|
);
|
|
455
489
|
}
|
|
456
490
|
|
|
457
|
-
if (
|
|
491
|
+
if (hasOnDataZoomWindowChanged) {
|
|
458
492
|
chartInstanceRef.current?.off("datazoom", handleDataZoom);
|
|
459
493
|
}
|
|
460
494
|
|
|
@@ -466,9 +500,12 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
466
500
|
}, [
|
|
467
501
|
chartOption,
|
|
468
502
|
handleResize,
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
503
|
+
onClickRef,
|
|
504
|
+
onLegendSelectChangedRef,
|
|
505
|
+
hasOnClick,
|
|
506
|
+
hasOnLegendSelectChanged,
|
|
507
|
+
hasOnDataZoomWindowChanged,
|
|
508
|
+
emitZoomWindow,
|
|
472
509
|
xAxisData,
|
|
473
510
|
enableLegendAutoScroll,
|
|
474
511
|
legendVisibleCount,
|
|
@@ -505,9 +542,9 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
505
542
|
startIndex: normalizedScroll.startIndex,
|
|
506
543
|
autoPlay: normalizedScroll.autoScroll,
|
|
507
544
|
enableWheelScroll,
|
|
508
|
-
onWindowChange:
|
|
545
|
+
onWindowChange: hasOnDataZoomWindowChanged
|
|
509
546
|
? (startPercent, endPercent) =>
|
|
510
|
-
|
|
547
|
+
emitZoomWindow(
|
|
511
548
|
percentWindowToIndexWindow(
|
|
512
549
|
startPercent,
|
|
513
550
|
endPercent,
|
|
@@ -523,7 +560,8 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
523
560
|
normalizedScroll,
|
|
524
561
|
sliderDataZoom,
|
|
525
562
|
xAxisData,
|
|
526
|
-
|
|
563
|
+
hasOnDataZoomWindowChanged,
|
|
564
|
+
emitZoomWindow,
|
|
527
565
|
]);
|
|
528
566
|
|
|
529
567
|
// Clean up resources when the component unmounts
|
|
@@ -173,6 +173,12 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
|
|
|
173
173
|
|
|
174
174
|
const onChartRef = useLatestRef(onChart);
|
|
175
175
|
|
|
176
|
+
// onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑会重建
|
|
177
|
+
// 3D 饼的 option 并重播动画。注册与否看布尔量(保证「从无到有传入」仍会注册),
|
|
178
|
+
// 实际调用取 ref 里的最新引用。
|
|
179
|
+
const onClickRef = useLatestRef(onClick);
|
|
180
|
+
const hasOnClick = !!onClick;
|
|
181
|
+
|
|
176
182
|
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
177
183
|
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
178
184
|
useEffect(() => {
|
|
@@ -197,8 +203,11 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
|
|
|
197
203
|
|
|
198
204
|
chart.on("mouseover", handleMouseOver);
|
|
199
205
|
chart.on("globalout", handleGlobalOut);
|
|
200
|
-
|
|
201
|
-
|
|
206
|
+
const handleClick = (event: echarts.ECElementEvent) => {
|
|
207
|
+
onClickRef.current?.(event);
|
|
208
|
+
};
|
|
209
|
+
if (hasOnClick) {
|
|
210
|
+
chart.on("click", handleClick);
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
return () => {
|
|
@@ -206,8 +215,8 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
|
|
|
206
215
|
if (chart) {
|
|
207
216
|
chart.off("mouseover", handleMouseOver);
|
|
208
217
|
chart.off("globalout", handleGlobalOut);
|
|
209
|
-
if (
|
|
210
|
-
chart.off("click",
|
|
218
|
+
if (hasOnClick) {
|
|
219
|
+
chart.off("click", handleClick);
|
|
211
220
|
}
|
|
212
221
|
}
|
|
213
222
|
};
|
|
@@ -217,7 +226,8 @@ const ChartPie3D: React.FC<ChartPie3DProps> = (props) => {
|
|
|
217
226
|
handleMouseOver,
|
|
218
227
|
handleGlobalOut,
|
|
219
228
|
onChartRef,
|
|
220
|
-
|
|
229
|
+
onClickRef,
|
|
230
|
+
hasOnClick,
|
|
221
231
|
containerReady,
|
|
222
232
|
]);
|
|
223
233
|
|
|
@@ -255,6 +255,12 @@ const ChartPie: ChartPieFC = (props) => {
|
|
|
255
255
|
|
|
256
256
|
const onChartRef = useLatestRef(onChart);
|
|
257
257
|
|
|
258
|
+
// onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
|
|
259
|
+
// 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
|
|
260
|
+
// 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
|
|
261
|
+
const onClickRef = useLatestRef(onClick);
|
|
262
|
+
const hasOnClick = !!onClick;
|
|
263
|
+
|
|
258
264
|
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
259
265
|
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
260
266
|
// Initialize the chart
|
|
@@ -283,8 +289,11 @@ const ChartPie: ChartPieFC = (props) => {
|
|
|
283
289
|
}
|
|
284
290
|
|
|
285
291
|
// Add click event
|
|
286
|
-
|
|
287
|
-
|
|
292
|
+
const handleClick = (event: echarts.ECElementEvent) => {
|
|
293
|
+
onClickRef.current?.(event);
|
|
294
|
+
};
|
|
295
|
+
if (hasOnClick) {
|
|
296
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
288
297
|
}
|
|
289
298
|
|
|
290
299
|
// Legend auto-scroll
|
|
@@ -307,8 +316,8 @@ const ChartPie: ChartPieFC = (props) => {
|
|
|
307
316
|
return () => {
|
|
308
317
|
window.removeEventListener("resize", handleResize);
|
|
309
318
|
|
|
310
|
-
if (
|
|
311
|
-
chartInstanceRef.current?.off("click",
|
|
319
|
+
if (hasOnClick) {
|
|
320
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
312
321
|
}
|
|
313
322
|
|
|
314
323
|
// Clean up legend scrolling
|
|
@@ -321,7 +330,8 @@ const ChartPie: ChartPieFC = (props) => {
|
|
|
321
330
|
chartOption,
|
|
322
331
|
handleResize,
|
|
323
332
|
onChartRef,
|
|
324
|
-
|
|
333
|
+
onClickRef,
|
|
334
|
+
hasOnClick,
|
|
325
335
|
enableLegendAutoScroll,
|
|
326
336
|
seriesData,
|
|
327
337
|
legendVisibleCount,
|
|
@@ -3,6 +3,7 @@ import useContainerReady from "../_hooks/useContainerReady";
|
|
|
3
3
|
import styles from "../index.module.scss";
|
|
4
4
|
import { ChartCommonProps, ChartOptionType, ChartsOption } from "..";
|
|
5
5
|
import * as echarts from "echarts";
|
|
6
|
+
import useLatestRef from "../../../hooks/useLatestRef";
|
|
6
7
|
|
|
7
8
|
type SankeyNodeItemOption = echarts.SankeySeriesOption["data"];
|
|
8
9
|
type SankeyEdgeItemOption = echarts.SankeySeriesOption["links"];
|
|
@@ -101,6 +102,13 @@ const Sankey: React.FC<ChartSankeyProps> = (props) => {
|
|
|
101
102
|
return option;
|
|
102
103
|
}, [seriesData, seriesLinks, series]);
|
|
103
104
|
|
|
105
|
+
// getImage 只是「出图后把 dataURL 交出去」的出口,既不决定图怎么画,
|
|
106
|
+
// 也不该决定要不要重画。它进依赖数组会让 effect 跟着重跑 → setOption(notMerge)
|
|
107
|
+
// → 图重绘 → 再触发一次 finished → 再回调;消费方把图存进 state 就是死循环。
|
|
108
|
+
// 而且 finished 监听只在首次 init 时注册,闭包里锁死的是首帧的 getImage,
|
|
109
|
+
// 后续换了引用永远调不到 —— 依赖数组写了它也没用,是条死依赖。走 ref 两个问题一起解决。
|
|
110
|
+
const getImageRef = useLatestRef(getImage);
|
|
111
|
+
|
|
104
112
|
// Callback handling chart resize
|
|
105
113
|
const handleResize = useCallback(() => {
|
|
106
114
|
// A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
|
|
@@ -123,7 +131,7 @@ const Sankey: React.FC<ChartSankeyProps> = (props) => {
|
|
|
123
131
|
|
|
124
132
|
// Attach the finished event listener on first initialization
|
|
125
133
|
chart.on("finished", () => {
|
|
126
|
-
|
|
134
|
+
getImageRef.current?.(
|
|
127
135
|
chart!.getDataURL({
|
|
128
136
|
type: "png",
|
|
129
137
|
pixelRatio: 1,
|
|
@@ -149,9 +157,7 @@ const Sankey: React.FC<ChartSankeyProps> = (props) => {
|
|
|
149
157
|
return () => {
|
|
150
158
|
window.removeEventListener("resize", handleResize);
|
|
151
159
|
};
|
|
152
|
-
}, [chartOption, handleResize,
|
|
153
|
-
containerReady,
|
|
154
|
-
]);
|
|
160
|
+
}, [chartOption, handleResize, getImageRef, containerReady]);
|
|
155
161
|
|
|
156
162
|
// Clean up resources on unmount
|
|
157
163
|
useEffect(() => {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
SeriesData,
|
|
9
9
|
} from "..";
|
|
10
10
|
import * as echarts from "echarts";
|
|
11
|
+
import useLatestRef from "../../../hooks/useLatestRef";
|
|
11
12
|
import { deepCopy } from "hsu-utils";
|
|
12
13
|
import { handleTreeData } from "../_utils/tree";
|
|
13
14
|
|
|
@@ -41,6 +42,13 @@ const Tree: React.FC<ChartTreeProps> = (props) => {
|
|
|
41
42
|
return option;
|
|
42
43
|
}, [seriesData, series]);
|
|
43
44
|
|
|
45
|
+
// getImage 只是「出图后把 dataURL 交出去」的出口,既不决定图怎么画,
|
|
46
|
+
// 也不该决定要不要重画。它进依赖数组会让 effect 跟着重跑 → setOption(notMerge)
|
|
47
|
+
// → 图重绘 → 再触发一次 finished → 再回调;消费方把图存进 state 就是死循环。
|
|
48
|
+
// 而且 finished 监听只在首次 init 时注册,闭包里锁死的是首帧的 getImage,
|
|
49
|
+
// 后续换了引用永远调不到 —— 依赖数组写了它也没用,是条死依赖。走 ref 两个问题一起解决。
|
|
50
|
+
const getImageRef = useLatestRef(getImage);
|
|
51
|
+
|
|
44
52
|
// Callback handling chart resize
|
|
45
53
|
const handleResize = useCallback(() => {
|
|
46
54
|
// A keep-alive tab losing focus fires the observer with a 0×0 box; resizing to that throws
|
|
@@ -63,7 +71,7 @@ const Tree: React.FC<ChartTreeProps> = (props) => {
|
|
|
63
71
|
|
|
64
72
|
// Attach the finished event listener on first initialization
|
|
65
73
|
chart.on("finished", () => {
|
|
66
|
-
|
|
74
|
+
getImageRef.current?.(
|
|
67
75
|
chart!.getDataURL({
|
|
68
76
|
type: "png",
|
|
69
77
|
pixelRatio: 1,
|
|
@@ -89,9 +97,7 @@ const Tree: React.FC<ChartTreeProps> = (props) => {
|
|
|
89
97
|
return () => {
|
|
90
98
|
window.removeEventListener("resize", handleResize);
|
|
91
99
|
};
|
|
92
|
-
}, [chartOption, handleResize,
|
|
93
|
-
containerReady,
|
|
94
|
-
]);
|
|
100
|
+
}, [chartOption, handleResize, getImageRef, containerReady]);
|
|
95
101
|
|
|
96
102
|
// Clean up resources on unmount
|
|
97
103
|
useEffect(() => {
|