@hsu-react/ui 2.5.4 → 2.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/components/ChainGraph/_hooks/useChainGraphData.js +29 -8
- package/es/components/ChainGraph/_hooks/useChainGraphLayout.js +14 -3
- package/es/components/Chart/Bar/index.js +55 -20
- package/es/components/Chart/Bubble/index.js +21 -6
- package/es/components/Chart/Common/index.js +6 -1
- package/es/components/Chart/Gauge/index.js +6 -1
- package/es/components/Chart/Heatmap/index.js +24 -7
- package/es/components/Chart/Line/index.js +55 -20
- package/es/components/Chart/Pie/Pie3D/index.js +30 -12
- package/es/components/Chart/Pie/index.js +24 -7
- package/es/components/Chart/Polar/index.js +9 -2
- package/es/components/Chart/Radar/index.js +6 -1
- package/es/components/Chart/Sankey/index.js +11 -2
- package/es/components/Chart/Tree/index.js +11 -2
- package/es/components/Select/_hooks/useSelectComposition.js +6 -2
- package/es/components/Table/_hooks/useAutoScrolling.js +16 -7
- package/es/hooks/useShallowStable.d.ts +32 -0
- package/es/hooks/useShallowStable.js +54 -0
- package/es/layout/Menu/_hooks/useOnlyLvOneMenu.js +9 -2
- package/lib/components/ChainGraph/_hooks/useChainGraphData.js +24 -7
- package/lib/components/ChainGraph/_hooks/useChainGraphLayout.js +12 -2
- package/lib/components/Chart/Bar/index.js +51 -19
- package/lib/components/Chart/Bubble/index.js +20 -6
- package/lib/components/Chart/Common/index.js +6 -1
- package/lib/components/Chart/Gauge/index.js +6 -1
- package/lib/components/Chart/Heatmap/index.js +23 -7
- package/lib/components/Chart/Line/index.js +51 -19
- package/lib/components/Chart/Pie/Pie3D/index.js +29 -12
- package/lib/components/Chart/Pie/index.js +23 -7
- package/lib/components/Chart/Polar/index.js +9 -2
- package/lib/components/Chart/Radar/index.js +6 -1
- package/lib/components/Chart/Sankey/index.js +10 -2
- package/lib/components/Chart/Tree/index.js +10 -2
- package/lib/components/Select/_hooks/useSelectComposition.js +6 -2
- package/lib/components/Table/_hooks/useAutoScrolling.js +16 -7
- package/lib/hooks/useShallowStable.d.ts +32 -0
- package/lib/hooks/useShallowStable.js +59 -0
- package/lib/layout/Menu/_hooks/useOnlyLvOneMenu.js +8 -2
- package/package.json +1 -1
- package/src/__tests__/callbackDepsGuard.test.ts +72 -0
- package/src/__tests__/depsScan.ts +158 -0
- package/src/__tests__/freshObjectDepsGuard.test.ts +144 -0
- package/src/components/ChainGraph/_hooks/callbacks.test.tsx +155 -0
- package/src/components/ChainGraph/_hooks/useChainGraphData.ts +38 -10
- package/src/components/ChainGraph/_hooks/useChainGraphLayout.ts +16 -3
- package/src/components/Chart/Bar/index.tsx +65 -22
- package/src/components/Chart/Bubble/index.tsx +21 -6
- package/src/components/Chart/Common/index.tsx +6 -1
- package/src/components/Chart/Gauge/index.tsx +6 -1
- package/src/components/Chart/Heatmap/index.tsx +39 -17
- package/src/components/Chart/Line/index.tsx +65 -22
- package/src/components/Chart/Pie/Pie3D/index.tsx +27 -8
- package/src/components/Chart/Pie/index.tsx +25 -7
- package/src/components/Chart/Polar/index.tsx +10 -2
- package/src/components/Chart/Radar/index.tsx +6 -1
- package/src/components/Chart/Sankey/index.tsx +10 -4
- package/src/components/Chart/Tree/index.tsx +10 -4
- package/src/components/Chart/callbacks.test.tsx +315 -0
- package/src/components/Chart/optionMemo.test.tsx +232 -0
- package/src/components/Select/_hooks/useSelectComposition.test.ts +47 -0
- package/src/components/Select/_hooks/useSelectComposition.ts +6 -2
- package/src/components/Table/_hooks/useAutoScrolling.test.tsx +95 -0
- package/src/components/Table/_hooks/useAutoScrolling.ts +16 -7
- package/src/hooks/useShallowStable.ts +65 -0
- package/src/layout/Menu/_hooks/useOnlyLvOneMenu.test.tsx +56 -0
- package/src/layout/Menu/_hooks/useOnlyLvOneMenu.ts +8 -2
|
@@ -24,6 +24,8 @@ 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";
|
|
28
|
+
import useShallowStable from "../../../hooks/useShallowStable";
|
|
27
29
|
|
|
28
30
|
export interface ChartBarProps extends ChartCommonProps {
|
|
29
31
|
chartTitle?: string;
|
|
@@ -69,8 +71,12 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
69
71
|
enableLegendAutoScroll = false,
|
|
70
72
|
legendVisibleCount = 8,
|
|
71
73
|
legendScrollInterval = 1500,
|
|
72
|
-
...
|
|
74
|
+
...restOption
|
|
73
75
|
} = props;
|
|
76
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
77
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
78
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
79
|
+
const coreOption = useShallowStable(restOption);
|
|
74
80
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
75
81
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
76
82
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -89,6 +95,36 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
89
95
|
const isDark = useIsDark();
|
|
90
96
|
const chrome = useMemo(() => resolveChartChrome(isDark), [isDark]);
|
|
91
97
|
|
|
98
|
+
// 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
|
|
99
|
+
// 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
|
|
100
|
+
// 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
|
|
101
|
+
// 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
|
|
102
|
+
// 这样「从无到有传入回调」仍会重新注册,不会漏。
|
|
103
|
+
const onClickRef = useLatestRef(onClick);
|
|
104
|
+
const onLegendSelectChangedRef = useLatestRef(onLegendSelectChanged);
|
|
105
|
+
const onDataZoomWindowChangedRef = useLatestRef(onDataZoomWindowChanged);
|
|
106
|
+
const hasOnClick = !!onClick;
|
|
107
|
+
const hasOnLegendSelectChanged = !!onLegendSelectChanged;
|
|
108
|
+
const hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
|
|
109
|
+
// 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
|
|
110
|
+
// 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
|
|
111
|
+
const notifiedZoomWindowRef = useRef<DataZoomIndexWindow | null>(null);
|
|
112
|
+
const emitZoomWindow = useCallback(
|
|
113
|
+
(next: DataZoomIndexWindow) => {
|
|
114
|
+
const prev = notifiedZoomWindowRef.current;
|
|
115
|
+
if (
|
|
116
|
+
prev &&
|
|
117
|
+
prev.startIndex === next.startIndex &&
|
|
118
|
+
prev.endIndex === next.endIndex
|
|
119
|
+
) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
notifiedZoomWindowRef.current = next;
|
|
123
|
+
onDataZoomWindowChangedRef.current?.(next);
|
|
124
|
+
},
|
|
125
|
+
[onDataZoomWindowChangedRef],
|
|
126
|
+
);
|
|
127
|
+
|
|
92
128
|
// Cache the chart option with useMemo
|
|
93
129
|
const chartOption = useMemo(() => {
|
|
94
130
|
const def_xAxis = createDefaultCategoryXAxis(xAxisData, chrome);
|
|
@@ -328,18 +364,18 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
328
364
|
}
|
|
329
365
|
|
|
330
366
|
// 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 (
|
|
367
|
+
if (hasOnDataZoomWindowChanged) {
|
|
332
368
|
const zooms = (chart.getOption() as ChartsOption | undefined)
|
|
333
369
|
?.dataZoom as Array<{ start?: number; end?: number }> | undefined;
|
|
334
370
|
const zoom = zooms?.[0];
|
|
335
371
|
const totalLen = xAxisData?.length ?? 0;
|
|
336
372
|
if (typeof zoom?.start === "number" && typeof zoom?.end === "number") {
|
|
337
|
-
|
|
373
|
+
emitZoomWindow(
|
|
338
374
|
percentWindowToIndexWindow(zoom.start, zoom.end, totalLen),
|
|
339
375
|
);
|
|
340
376
|
} else {
|
|
341
377
|
// 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
|
-
|
|
378
|
+
emitZoomWindow({
|
|
343
379
|
startIndex: 0,
|
|
344
380
|
endIndex: Math.max(0, totalLen - 1),
|
|
345
381
|
});
|
|
@@ -355,18 +391,21 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
355
391
|
resizeObserverRef.current.observe(chartRef.current);
|
|
356
392
|
}
|
|
357
393
|
|
|
358
|
-
// Add click event
|
|
359
|
-
|
|
360
|
-
|
|
394
|
+
// Add click event(注册与否看布尔量,实际调用取最新引用)
|
|
395
|
+
const handleClick = (event: echarts.ECElementEvent) => {
|
|
396
|
+
onClickRef.current?.(event);
|
|
397
|
+
};
|
|
398
|
+
if (hasOnClick) {
|
|
399
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
361
400
|
}
|
|
362
401
|
|
|
363
402
|
// Legend selection event
|
|
364
403
|
const handleLegendSelectChanged = (params: unknown) => {
|
|
365
|
-
|
|
404
|
+
onLegendSelectChangedRef.current?.(
|
|
366
405
|
(params as { selected: Record<string, boolean> }).selected,
|
|
367
406
|
);
|
|
368
407
|
};
|
|
369
|
-
if (
|
|
408
|
+
if (hasOnLegendSelectChanged) {
|
|
370
409
|
chartInstanceRef.current?.on(
|
|
371
410
|
"legendselectchanged",
|
|
372
411
|
handleLegendSelectChanged,
|
|
@@ -392,7 +431,7 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
392
431
|
typeof info?.startValue === "number" &&
|
|
393
432
|
typeof info?.endValue === "number"
|
|
394
433
|
) {
|
|
395
|
-
|
|
434
|
+
emitZoomWindow({
|
|
396
435
|
startIndex: Math.max(0, Math.round(info.startValue)),
|
|
397
436
|
endIndex: Math.max(0, Math.round(info.endValue)),
|
|
398
437
|
});
|
|
@@ -400,7 +439,7 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
400
439
|
typeof info?.start === "number" &&
|
|
401
440
|
typeof info?.end === "number"
|
|
402
441
|
) {
|
|
403
|
-
|
|
442
|
+
emitZoomWindow(
|
|
404
443
|
percentWindowToIndexWindow(
|
|
405
444
|
info.start,
|
|
406
445
|
info.end,
|
|
@@ -409,7 +448,7 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
409
448
|
);
|
|
410
449
|
}
|
|
411
450
|
};
|
|
412
|
-
if (
|
|
451
|
+
if (hasOnDataZoomWindowChanged) {
|
|
413
452
|
chartInstanceRef.current?.on("datazoom", handleDataZoom);
|
|
414
453
|
}
|
|
415
454
|
|
|
@@ -445,18 +484,18 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
445
484
|
return () => {
|
|
446
485
|
window.removeEventListener("resize", handleResize);
|
|
447
486
|
|
|
448
|
-
if (
|
|
449
|
-
chartInstanceRef.current?.off("click",
|
|
487
|
+
if (hasOnClick) {
|
|
488
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
450
489
|
}
|
|
451
490
|
|
|
452
|
-
if (
|
|
491
|
+
if (hasOnLegendSelectChanged) {
|
|
453
492
|
chartInstanceRef.current?.off(
|
|
454
493
|
"legendselectchanged",
|
|
455
494
|
handleLegendSelectChanged,
|
|
456
495
|
);
|
|
457
496
|
}
|
|
458
497
|
|
|
459
|
-
if (
|
|
498
|
+
if (hasOnDataZoomWindowChanged) {
|
|
460
499
|
chartInstanceRef.current?.off("datazoom", handleDataZoom);
|
|
461
500
|
}
|
|
462
501
|
|
|
@@ -468,9 +507,12 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
468
507
|
}, [
|
|
469
508
|
chartOption,
|
|
470
509
|
handleResize,
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
510
|
+
onClickRef,
|
|
511
|
+
onLegendSelectChangedRef,
|
|
512
|
+
hasOnClick,
|
|
513
|
+
hasOnLegendSelectChanged,
|
|
514
|
+
hasOnDataZoomWindowChanged,
|
|
515
|
+
emitZoomWindow,
|
|
474
516
|
xAxisData,
|
|
475
517
|
enableLegendAutoScroll,
|
|
476
518
|
legendVisibleCount,
|
|
@@ -507,9 +549,9 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
507
549
|
startIndex: normalizedScroll.startIndex,
|
|
508
550
|
autoPlay: normalizedScroll.autoScroll,
|
|
509
551
|
enableWheelScroll,
|
|
510
|
-
onWindowChange:
|
|
552
|
+
onWindowChange: hasOnDataZoomWindowChanged
|
|
511
553
|
? (startPercent, endPercent) =>
|
|
512
|
-
|
|
554
|
+
emitZoomWindow(
|
|
513
555
|
percentWindowToIndexWindow(
|
|
514
556
|
startPercent,
|
|
515
557
|
endPercent,
|
|
@@ -525,7 +567,8 @@ const ChartBar: React.FC<ChartBarProps> = (props) => {
|
|
|
525
567
|
normalizedScroll,
|
|
526
568
|
sliderDataZoom,
|
|
527
569
|
xAxisData,
|
|
528
|
-
|
|
570
|
+
hasOnDataZoomWindowChanged,
|
|
571
|
+
emitZoomWindow,
|
|
529
572
|
]);
|
|
530
573
|
|
|
531
574
|
// Clean up resources when the component unmounts
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
drawCircles,
|
|
14
14
|
} from "../_utils/bubble";
|
|
15
15
|
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
16
|
+
import useShallowStable from "../../../hooks/useShallowStable";
|
|
16
17
|
|
|
17
18
|
export interface BubbleDataItem {
|
|
18
19
|
name: string;
|
|
@@ -71,8 +72,12 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
71
72
|
title,
|
|
72
73
|
onChart,
|
|
73
74
|
onClick,
|
|
74
|
-
...
|
|
75
|
+
...restOption
|
|
75
76
|
} = props;
|
|
77
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
78
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
79
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
80
|
+
const coreOption = useShallowStable(restOption);
|
|
76
81
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
77
82
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
78
83
|
const resizeObserverRef = useRef<ResizeObserver | null>(null);
|
|
@@ -373,6 +378,12 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
373
378
|
|
|
374
379
|
const onChartRef = useLatestRef(onChart);
|
|
375
380
|
|
|
381
|
+
// onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
|
|
382
|
+
// 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
|
|
383
|
+
// 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
|
|
384
|
+
const onClickRef = useLatestRef(onClick);
|
|
385
|
+
const hasOnClick = !!onClick;
|
|
386
|
+
|
|
376
387
|
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
377
388
|
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
378
389
|
// Initialize the chart
|
|
@@ -398,16 +409,19 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
398
409
|
window.addEventListener("resize", handleResize);
|
|
399
410
|
|
|
400
411
|
// Add click event
|
|
401
|
-
|
|
402
|
-
|
|
412
|
+
const handleClick = (event: echarts.ECElementEvent) => {
|
|
413
|
+
onClickRef.current?.(event);
|
|
414
|
+
};
|
|
415
|
+
if (hasOnClick) {
|
|
416
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
403
417
|
}
|
|
404
418
|
|
|
405
419
|
// Cleanup function
|
|
406
420
|
return () => {
|
|
407
421
|
window.removeEventListener("resize", handleResize);
|
|
408
422
|
|
|
409
|
-
if (
|
|
410
|
-
chartInstanceRef.current?.off("click",
|
|
423
|
+
if (hasOnClick) {
|
|
424
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
411
425
|
}
|
|
412
426
|
};
|
|
413
427
|
}, [
|
|
@@ -416,7 +430,8 @@ const Bubble: React.FC<ChartBubbleProps> = (props) => {
|
|
|
416
430
|
containerSize.height,
|
|
417
431
|
handleResize,
|
|
418
432
|
onChartRef,
|
|
419
|
-
|
|
433
|
+
onClickRef,
|
|
434
|
+
hasOnClick,
|
|
420
435
|
]);
|
|
421
436
|
|
|
422
437
|
// Dispose the chart instance when the component unmounts
|
|
@@ -4,9 +4,14 @@ import styles from "../index.module.scss";
|
|
|
4
4
|
import { ChartCommonProps, ChartOptionType, ChartsOption } from "..";
|
|
5
5
|
import * as echarts from "echarts";
|
|
6
6
|
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
7
|
+
import useShallowStable from "../../../hooks/useShallowStable";
|
|
7
8
|
|
|
8
9
|
const Common: React.FC<ChartCommonProps> = (props) => {
|
|
9
|
-
const { className, style, onChart, ...
|
|
10
|
+
const { className, style, onChart, ...restOption } = props;
|
|
11
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
12
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
13
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
14
|
+
const coreOption = useShallowStable(restOption);
|
|
10
15
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
11
16
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
12
17
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|
11
11
|
|
|
12
12
|
import styles from "../index.module.scss";
|
|
13
|
+
import useShallowStable from "../../../hooks/useShallowStable";
|
|
13
14
|
|
|
14
15
|
export interface ChartGaugeProps extends ChartCommonProps {
|
|
15
16
|
color?: string[] | string;
|
|
@@ -28,8 +29,12 @@ const Gauge: React.FC<ChartGaugeProps> = (props) => {
|
|
|
28
29
|
axisLabel,
|
|
29
30
|
title,
|
|
30
31
|
color = "#1675FB",
|
|
31
|
-
...
|
|
32
|
+
...restSeries
|
|
32
33
|
} = series as echarts.GaugeSeriesOption;
|
|
34
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
35
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
36
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
37
|
+
const coreSeries = useShallowStable(restSeries);
|
|
33
38
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
34
39
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
35
40
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { resolveChartChrome } from "../_utils/chartTheme";
|
|
11
11
|
import useIsDark from "../../../hooks/useIsDark";
|
|
12
12
|
import { useLatestRef } from "../../../hooks/useLatestRef";
|
|
13
|
+
import useShallowStable from "../../../hooks/useShallowStable";
|
|
13
14
|
|
|
14
15
|
export interface HeatmapDataItem {
|
|
15
16
|
/** X-axis index or name */
|
|
@@ -43,6 +44,19 @@ export interface ChartHeatmapProps extends ChartCommonProps {
|
|
|
43
44
|
inRangeColor?: string[];
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
// 解构默认值写成字面量会每次渲染新建一个数组,进依赖数组就让 memo 恒不命中;提到模块级常量
|
|
48
|
+
const DEFAULT_IN_RANGE_COLOR = [
|
|
49
|
+
"#8BCCFB",
|
|
50
|
+
"#75C3FB",
|
|
51
|
+
"#5AB5F6",
|
|
52
|
+
"#42AEFA",
|
|
53
|
+
"#2DA1F5",
|
|
54
|
+
"#0F89E1",
|
|
55
|
+
"#056FBB",
|
|
56
|
+
"#025794",
|
|
57
|
+
"#0A4089",
|
|
58
|
+
];
|
|
59
|
+
|
|
46
60
|
const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
47
61
|
const {
|
|
48
62
|
className,
|
|
@@ -61,19 +75,13 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
|
61
75
|
onChart,
|
|
62
76
|
onClick,
|
|
63
77
|
zeroColor,
|
|
64
|
-
inRangeColor =
|
|
65
|
-
|
|
66
|
-
"#75C3FB",
|
|
67
|
-
"#5AB5F6",
|
|
68
|
-
"#42AEFA",
|
|
69
|
-
"#2DA1F5",
|
|
70
|
-
"#0F89E1",
|
|
71
|
-
"#056FBB",
|
|
72
|
-
"#025794",
|
|
73
|
-
"#0A4089",
|
|
74
|
-
],
|
|
75
|
-
...coreOption
|
|
78
|
+
inRangeColor = DEFAULT_IN_RANGE_COLOR,
|
|
79
|
+
...restOption
|
|
76
80
|
} = props;
|
|
81
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
82
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
83
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
84
|
+
const coreOption = useShallowStable(restOption);
|
|
77
85
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
78
86
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
79
87
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -269,6 +277,12 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
|
269
277
|
|
|
270
278
|
const onChartRef = useLatestRef(onChart);
|
|
271
279
|
|
|
280
|
+
// onClick 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 跟着重跑就会
|
|
281
|
+
// 再走一遍 setOption(notMerge),动画重播、悬浮/高亮态被清掉。注册与否看布尔量,
|
|
282
|
+
// 这样「从无到有传入 onClick」仍会注册;实际调用取 ref 里的最新引用。
|
|
283
|
+
const onClickRef = useLatestRef(onClick);
|
|
284
|
+
const hasOnClick = !!onClick;
|
|
285
|
+
|
|
272
286
|
// 回调 prop 不进依赖数组:消费方传内联箭头时每次渲染都是新引用,effect 会跟着
|
|
273
287
|
// 重跑并再调一次回调 —— 回调里 setState 就是死循环(详见 Input/TextArea 的说明)
|
|
274
288
|
// Initialize the chart
|
|
@@ -297,19 +311,27 @@ const Heatmap: React.FC<ChartHeatmapProps> = (props) => {
|
|
|
297
311
|
}
|
|
298
312
|
|
|
299
313
|
// Add click event
|
|
300
|
-
|
|
301
|
-
|
|
314
|
+
const handleClick = (event: echarts.ECElementEvent) => {
|
|
315
|
+
onClickRef.current?.(event);
|
|
316
|
+
};
|
|
317
|
+
if (hasOnClick) {
|
|
318
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
302
319
|
}
|
|
303
320
|
|
|
304
321
|
// Cleanup function
|
|
305
322
|
return () => {
|
|
306
323
|
window.removeEventListener("resize", handleResize);
|
|
307
324
|
|
|
308
|
-
if (
|
|
309
|
-
chartInstanceRef.current?.off("click",
|
|
325
|
+
if (hasOnClick) {
|
|
326
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
310
327
|
}
|
|
311
328
|
};
|
|
312
|
-
}, [
|
|
329
|
+
}, [
|
|
330
|
+
chartOption,
|
|
331
|
+
handleResize,
|
|
332
|
+
onChartRef,
|
|
333
|
+
onClickRef,
|
|
334
|
+
hasOnClick,
|
|
313
335
|
containerReady,
|
|
314
336
|
]);
|
|
315
337
|
|
|
@@ -24,6 +24,8 @@ 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";
|
|
28
|
+
import useShallowStable from "../../../hooks/useShallowStable";
|
|
27
29
|
|
|
28
30
|
export interface ChartLineProps extends ChartCommonProps {
|
|
29
31
|
chartTitle?: string;
|
|
@@ -70,8 +72,12 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
70
72
|
enableLegendAutoScroll = false,
|
|
71
73
|
legendVisibleCount = 8,
|
|
72
74
|
legendScrollInterval = 1500,
|
|
73
|
-
...
|
|
75
|
+
...restOption
|
|
74
76
|
} = props;
|
|
77
|
+
// rest 解构出来的对象每次渲染都是新引用,直接进依赖数组会让 chartOption 的 memo
|
|
78
|
+
// 恒不命中 —— 父组件每渲染一次就重跑一次 setOption(notMerge),动画重播、悬浮态被清掉。
|
|
79
|
+
// useShallowStable 让它回到值语义:内容浅相等就复用同一引用,真变了立刻透出新引用。
|
|
80
|
+
const coreOption = useShallowStable(restOption);
|
|
75
81
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
76
82
|
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
|
|
77
83
|
// Defers `echarts.init` until the container has a box — see the hook for why
|
|
@@ -90,6 +96,36 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
90
96
|
const isDark = useIsDark();
|
|
91
97
|
const chrome = useMemo(() => resolveChartChrome(isDark), [isDark]);
|
|
92
98
|
|
|
99
|
+
// 回调 prop 一律走 useLatestRef,不进依赖数组:消费方传内联箭头(React 里最常见的
|
|
100
|
+
// 写法)时每次渲染都是新引用,进依赖数组会让「init + setOption(notMerge)」整段重跑,
|
|
101
|
+
// 还会把当前可视窗口再同步一次给父级 —— 父级据此改 state 就是死循环。
|
|
102
|
+
// 但「有没有传回调」决定要不要注册监听,所以单独拆成布尔量进依赖数组,
|
|
103
|
+
// 这样「从无到有传入回调」仍会重新注册,不会漏。
|
|
104
|
+
const onClickRef = useLatestRef(onClick);
|
|
105
|
+
const onLegendSelectChangedRef = useLatestRef(onLegendSelectChanged);
|
|
106
|
+
const onDataZoomWindowChangedRef = useLatestRef(onDataZoomWindowChanged);
|
|
107
|
+
const hasOnClick = !!onClick;
|
|
108
|
+
const hasOnLegendSelectChanged = !!onLegendSelectChanged;
|
|
109
|
+
const hasOnDataZoomWindowChanged = !!onDataZoomWindowChanged;
|
|
110
|
+
// 已通知过父级的窗口用 ref 记账(不参与渲染):窗口没变就不再通知,
|
|
111
|
+
// 掐断「通知 → 父级重算坐标轴 → chartOption 变 → effect 重跑 → 再通知」的自激回路
|
|
112
|
+
const notifiedZoomWindowRef = useRef<DataZoomIndexWindow | null>(null);
|
|
113
|
+
const emitZoomWindow = useCallback(
|
|
114
|
+
(next: DataZoomIndexWindow) => {
|
|
115
|
+
const prev = notifiedZoomWindowRef.current;
|
|
116
|
+
if (
|
|
117
|
+
prev &&
|
|
118
|
+
prev.startIndex === next.startIndex &&
|
|
119
|
+
prev.endIndex === next.endIndex
|
|
120
|
+
) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
notifiedZoomWindowRef.current = next;
|
|
124
|
+
onDataZoomWindowChangedRef.current?.(next);
|
|
125
|
+
},
|
|
126
|
+
[onDataZoomWindowChangedRef],
|
|
127
|
+
);
|
|
128
|
+
|
|
93
129
|
// Cache the chart option with useMemo
|
|
94
130
|
const chartOption = useMemo(() => {
|
|
95
131
|
const def_xAxis = createDefaultCategoryXAxis(xAxisData, chrome);
|
|
@@ -326,18 +362,18 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
326
362
|
}
|
|
327
363
|
|
|
328
364
|
// 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 (
|
|
365
|
+
if (hasOnDataZoomWindowChanged) {
|
|
330
366
|
const zooms = (chart.getOption() as ChartsOption | undefined)
|
|
331
367
|
?.dataZoom as Array<{ start?: number; end?: number }> | undefined;
|
|
332
368
|
const zoom = zooms?.[0];
|
|
333
369
|
const totalLen = xAxisData?.length ?? 0;
|
|
334
370
|
if (typeof zoom?.start === "number" && typeof zoom?.end === "number") {
|
|
335
|
-
|
|
371
|
+
emitZoomWindow(
|
|
336
372
|
percentWindowToIndexWindow(zoom.start, zoom.end, totalLen),
|
|
337
373
|
);
|
|
338
374
|
} else {
|
|
339
375
|
// 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
|
-
|
|
376
|
+
emitZoomWindow({
|
|
341
377
|
startIndex: 0,
|
|
342
378
|
endIndex: Math.max(0, totalLen - 1),
|
|
343
379
|
});
|
|
@@ -353,18 +389,21 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
353
389
|
resizeObserverRef.current.observe(chartRef.current);
|
|
354
390
|
}
|
|
355
391
|
|
|
356
|
-
// Add click event
|
|
357
|
-
|
|
358
|
-
|
|
392
|
+
// Add click event(注册与否看布尔量,实际调用取最新引用)
|
|
393
|
+
const handleClick = (event: echarts.ECElementEvent) => {
|
|
394
|
+
onClickRef.current?.(event);
|
|
395
|
+
};
|
|
396
|
+
if (hasOnClick) {
|
|
397
|
+
chartInstanceRef.current?.on("click", handleClick);
|
|
359
398
|
}
|
|
360
399
|
|
|
361
400
|
// Legend selection event
|
|
362
401
|
const handleLegendSelectChanged = (params: unknown) => {
|
|
363
|
-
|
|
402
|
+
onLegendSelectChangedRef.current?.(
|
|
364
403
|
(params as { selected: Record<string, boolean> }).selected,
|
|
365
404
|
);
|
|
366
405
|
};
|
|
367
|
-
if (
|
|
406
|
+
if (hasOnLegendSelectChanged) {
|
|
368
407
|
chartInstanceRef.current?.on(
|
|
369
408
|
"legendselectchanged",
|
|
370
409
|
handleLegendSelectChanged,
|
|
@@ -390,7 +429,7 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
390
429
|
typeof info?.startValue === "number" &&
|
|
391
430
|
typeof info?.endValue === "number"
|
|
392
431
|
) {
|
|
393
|
-
|
|
432
|
+
emitZoomWindow({
|
|
394
433
|
startIndex: Math.max(0, Math.round(info.startValue)),
|
|
395
434
|
endIndex: Math.max(0, Math.round(info.endValue)),
|
|
396
435
|
});
|
|
@@ -398,7 +437,7 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
398
437
|
typeof info?.start === "number" &&
|
|
399
438
|
typeof info?.end === "number"
|
|
400
439
|
) {
|
|
401
|
-
|
|
440
|
+
emitZoomWindow(
|
|
402
441
|
percentWindowToIndexWindow(
|
|
403
442
|
info.start,
|
|
404
443
|
info.end,
|
|
@@ -407,7 +446,7 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
407
446
|
);
|
|
408
447
|
}
|
|
409
448
|
};
|
|
410
|
-
if (
|
|
449
|
+
if (hasOnDataZoomWindowChanged) {
|
|
411
450
|
chartInstanceRef.current?.on("datazoom", handleDataZoom);
|
|
412
451
|
}
|
|
413
452
|
|
|
@@ -443,18 +482,18 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
443
482
|
return () => {
|
|
444
483
|
window.removeEventListener("resize", handleResize);
|
|
445
484
|
|
|
446
|
-
if (
|
|
447
|
-
chartInstanceRef.current?.off("click",
|
|
485
|
+
if (hasOnClick) {
|
|
486
|
+
chartInstanceRef.current?.off("click", handleClick);
|
|
448
487
|
}
|
|
449
488
|
|
|
450
|
-
if (
|
|
489
|
+
if (hasOnLegendSelectChanged) {
|
|
451
490
|
chartInstanceRef.current?.off(
|
|
452
491
|
"legendselectchanged",
|
|
453
492
|
handleLegendSelectChanged,
|
|
454
493
|
);
|
|
455
494
|
}
|
|
456
495
|
|
|
457
|
-
if (
|
|
496
|
+
if (hasOnDataZoomWindowChanged) {
|
|
458
497
|
chartInstanceRef.current?.off("datazoom", handleDataZoom);
|
|
459
498
|
}
|
|
460
499
|
|
|
@@ -466,9 +505,12 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
466
505
|
}, [
|
|
467
506
|
chartOption,
|
|
468
507
|
handleResize,
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
508
|
+
onClickRef,
|
|
509
|
+
onLegendSelectChangedRef,
|
|
510
|
+
hasOnClick,
|
|
511
|
+
hasOnLegendSelectChanged,
|
|
512
|
+
hasOnDataZoomWindowChanged,
|
|
513
|
+
emitZoomWindow,
|
|
472
514
|
xAxisData,
|
|
473
515
|
enableLegendAutoScroll,
|
|
474
516
|
legendVisibleCount,
|
|
@@ -505,9 +547,9 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
505
547
|
startIndex: normalizedScroll.startIndex,
|
|
506
548
|
autoPlay: normalizedScroll.autoScroll,
|
|
507
549
|
enableWheelScroll,
|
|
508
|
-
onWindowChange:
|
|
550
|
+
onWindowChange: hasOnDataZoomWindowChanged
|
|
509
551
|
? (startPercent, endPercent) =>
|
|
510
|
-
|
|
552
|
+
emitZoomWindow(
|
|
511
553
|
percentWindowToIndexWindow(
|
|
512
554
|
startPercent,
|
|
513
555
|
endPercent,
|
|
@@ -523,7 +565,8 @@ const ChartLine: React.FC<ChartLineProps> = (props) => {
|
|
|
523
565
|
normalizedScroll,
|
|
524
566
|
sliderDataZoom,
|
|
525
567
|
xAxisData,
|
|
526
|
-
|
|
568
|
+
hasOnDataZoomWindowChanged,
|
|
569
|
+
emitZoomWindow,
|
|
527
570
|
]);
|
|
528
571
|
|
|
529
572
|
// Clean up resources when the component unmounts
|