@easyv/charts 1.10.7 → 1.10.9

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.
@@ -1,318 +1,320 @@
1
- import { useEffect, useMemo, useRef, useState } from "react";
2
- import { animate, linear } from "popmotion";
3
-
4
- const initialState = {
5
- currentIndex: null,
6
- flag: false, // 首次加载标识:true-首次加载不执行动画,false-可执行轮播
7
- };
8
-
9
- /**
10
- * x轴滚动逻辑
11
- * @param {Object} axis x轴配置项
12
- * @param {Object} config x轴轮播动画的配置项
13
- * @returns {Map} 返回更新后的x轴配置(ticks/scaler/range 变更)
14
- */
15
- export default (axis, config, isHover, controlInfo, active) => {
16
- const { show, interval, duration, hover } = config;
17
- const { isC, cPercent } = controlInfo;
18
-
19
- const {
20
- tickCount: count,
21
- allTicks,
22
- scaler,
23
- start,
24
- end,
25
- step,
26
- ticks,
27
- lengthWithoutPaddingOuter,
28
- } = axis;
29
- const tickLength = ticks.length;
30
- const tickCount = isC ? allTicks.length : count;
31
-
32
- const scale = isC ? cPercent : 1;
33
- const _start = start / scale;
34
- const _end = end / scale;
35
- const canCarousel = useMemo(
36
- () => show && active && tickLength > tickCount,
37
- [show, active, tickLength, tickCount]
38
- );
39
-
40
- const [state, setState] = useState({
41
- scaler,
42
- step,
43
- ticks,
44
- });
45
- const [status, setStatus] = useState(initialState);
46
- const [ready, setReady] = useState(true); // true 才允许开启下一轮动画(用于 interval 等待)
47
- const [wakeToken, setWakeToken] = useState(0);
48
- const hoverRef = useRef(isHover);
49
- const prevHoverRef = useRef(isHover);
50
- const intervalTimerRef = useRef(null);
51
- const isAnimatingRef = useRef(false);
52
- const latestRef = useRef({
53
- allTicks,
54
- scaler,
55
- tickCount,
56
- tickLength,
57
- lengthWithoutPaddingOuter,
58
- _start,
59
- _end,
60
- interval,
61
- duration,
62
- hover,
63
- });
64
-
65
- // 避免 hover 触发 render 时 effect 误重启动画:用 ref 持有最新输入
66
- useEffect(() => {
67
- latestRef.current = {
68
- allTicks,
69
- scaler,
70
- tickCount,
71
- tickLength,
72
- lengthWithoutPaddingOuter,
73
- _start,
74
- _end,
75
- interval,
76
- duration,
77
- hover,
78
- };
79
- }, [
80
- allTicks,
81
- scaler,
82
- tickCount,
83
- tickLength,
84
- lengthWithoutPaddingOuter,
85
- _start,
86
- _end,
87
- interval,
88
- duration,
89
- hover,
90
- ]);
91
-
92
- // hover 变化不打断正在进行的动画:只更新引用、并在需要时取消「下一轮」定时器
93
- useEffect(() => {
94
- const prev = prevHoverRef.current;
95
- prevHoverRef.current = isHover;
96
- hoverRef.current = isHover;
97
- if (latestRef.current.hover && isHover && intervalTimerRef.current) {
98
- clearTimeout(intervalTimerRef.current);
99
- intervalTimerRef.current = null;
100
- }
101
-
102
- // 仅在 hover 从 true -> false 时唤醒轮播(避免在非 hover 状态下反复唤醒导致 interval 失效)
103
- if (
104
- latestRef.current.hover &&
105
- prev === true &&
106
- isHover === false &&
107
- canCarousel &&
108
- status.currentIndex !== null &&
109
- !isAnimatingRef.current
110
- ) {
111
- setReady(true);
112
- setWakeToken((v) => v + 1);
113
- }
114
- }, [isHover, canCarousel, status.currentIndex]);
115
-
116
- // 初始化:仅在基础条件变化时设置初始索引
117
- useEffect(() => {
118
- if (show && tickLength > tickCount) {
119
- setStatus({
120
- currentIndex: 0,
121
- flag: true,
122
- });
123
- } else {
124
- setStatus(initialState);
125
- }
126
- }, [show, tickCount, tickLength]);
127
- useEffect(() => {
128
- let animation;
129
- const { currentIndex, flag } = status;
130
- const latest = latestRef.current;
131
-
132
- // 执行条件:显示、非hover暂停、激活、有足够的轮播项
133
- const canRun = canCarousel && !(latest.hover && hoverRef.current);
134
-
135
- if (currentIndex !== null && canRun) {
136
- if (flag) {
137
- // 首次加载:仅初始化视图,不执行动画
138
- const step = latest.lengthWithoutPaddingOuter / latest.tickCount;
139
- const _ticks = latest.allTicks.slice(currentIndex, latest.tickCount);
140
- setState({
141
- step,
142
- ticks: _ticks,
143
- scaler: latest.scaler
144
- .copy()
145
- .domain(_ticks)
146
- .range([latest._start, latest._end]),
147
- });
148
-
149
- // 首次加载完成后,启动第一轮动画(等待interval后执行)
150
- if (latest.interval >= 0) {
151
- intervalTimerRef.current = setTimeout(() => {
152
- setStatus((prev) => ({ ...prev, flag: false }));
153
- }, latest.interval * 1000);
154
- } else {
155
- setStatus((prev) => ({ ...prev, flag: false }));
156
- }
157
- } else {
158
- // 非首次加载:执行动画 + 后续逻辑
159
- if (!ready) return;
160
- const step = latest.lengthWithoutPaddingOuter / latest.tickCount;
161
-
162
- animation = animate({
163
- from: 0,
164
- to: -1,
165
- duration: latest.duration * 1000,
166
- ease: linear,
167
- onPlay: () => {
168
- isAnimatingRef.current = true;
169
- // 动画开始:更新ticks和scaler初始范围
170
- // 这里需要保证动画是「当前项滚出,下一项滚入」
171
- // 因此后续窗口从 currentIndex + 1 开始,避免首项重复导致视觉上「从第一项滚到第一项」的闪烁
172
- setState((axis) => {
173
- const { ticks, scaler } = axis;
174
- const [tick] = ticks;
175
- const _ticks = [
176
- tick,
177
- ...getTicks(
178
- latest.allTicks,
179
- currentIndex + 1,
180
- latest.tickCount
181
- ),
182
- ];
183
- return {
184
- ...axis,
185
- ticks: _ticks,
186
- scaler: scaler
187
- .copy()
188
- .range([latest._start, latest._end + step])
189
- .domain(_ticks),
190
- };
191
- });
192
- },
193
- onUpdate: (v) => {
194
- // 动画过程:实时更新scaler范围(实现滚动效果)
195
- setState((axis) => {
196
- const { scaler, step } = axis;
197
- return {
198
- ...axis,
199
- scaler: scaler
200
- .copy()
201
- .range([
202
- latest._start + step * v,
203
- latest._end + step + step * v,
204
- ]),
205
- };
206
- });
207
- },
208
- onComplete: () => {
209
- isAnimatingRef.current = false;
210
- // 本轮结束后进入等待态,避免立即开启下一轮(保证 interval 生效)
211
- setReady(false);
212
- // 动画完成:重置ticks和scaler
213
- setState((axis) => {
214
- const { scaler, ticks } = axis;
215
- const _ticks = ticks.slice(1, ticks.length);
216
- return {
217
- ...axis,
218
- ticks: _ticks,
219
- scaler: scaler
220
- .copy()
221
- .range([latest._start, latest._end])
222
- .domain(_ticks),
223
- };
224
- });
225
-
226
- const nextIndex = (currentIndex + 1) % latest.tickLength;
227
-
228
- // 无论是否 hover,都先把 currentIndex 同步到下一屏起点
229
- // 否则在 interval 暂停期触发 hover/离开,会用旧索引重启,视觉上“回滚一格”
230
- setStatus((prev) => ({ ...prev, currentIndex: nextIndex }));
231
-
232
- // hover 触发时:让当前动画跑完后暂停(不再排下一轮)
233
- if (latest.hover && hoverRef.current) return;
234
-
235
- // hover:动画完成后,等待interval时长,再触发下一轮动画
236
- intervalTimerRef.current = setTimeout(() => {
237
- setReady(true);
238
- setWakeToken((v) => v + 1);
239
- }, latest.interval * 1000);
240
- },
241
- });
242
- }
243
- }
244
-
245
- // 清理副作用:停止动画、清除定时器
246
- return () => {
247
- if (animation) {
248
- animation.stop();
249
- isAnimatingRef.current = false;
250
- }
251
- if (intervalTimerRef.current) {
252
- clearTimeout(intervalTimerRef.current);
253
- intervalTimerRef.current = null;
254
- }
255
- };
256
- }, [canCarousel, status.currentIndex, status.flag, wakeToken, ready]);
257
-
258
- // 重置逻辑:当无有效索引时恢复初始状态
259
- useEffect(() => {
260
- if (status.currentIndex === null) {
261
- const _ticks = scaler.type === "linear" ? scaler.domain() : allTicks;
262
- setState({
263
- step,
264
- scaler: scaler.copy().domain(_ticks).range([_start, _end]),
265
- ticks,
266
- });
267
- }
268
- }, [status.currentIndex, scaler, _start, _end, step, ticks, allTicks]);
269
-
270
- // x 轴可见项数量(count)发生变化时,安全地重置轮播节奏
271
- useEffect(() => {
272
- // 清掉当前的 interval 计时器,避免用旧配置继续排队
273
- if (intervalTimerRef.current) {
274
- clearTimeout(intervalTimerRef.current);
275
- intervalTimerRef.current = null;
276
- }
277
-
278
- // 重置动画节奏,让配置变更后「立即」按最新的 tickCount/tickLength 重新初始化
279
- // 不能简单 setStatus(initialState),否则会覆盖上方的初始化 effect(先开轮播再改数量时会被清成 null,导致展示全部且不轮播)
280
- isAnimatingRef.current = false;
281
- setReady(true);
282
- setStatus(() => {
283
- // 与初始化 effect 保持一致:只要满足条件就从 0 开始重新初始化
284
- if (show && active && tickLength > tickCount) {
285
- return { currentIndex: 0, flag: true };
286
- }
287
- return initialState;
288
- });
289
- setWakeToken((v) => v + 1);
290
- }, [count, show, active, tickLength, tickCount]);
291
-
292
- return {
293
- ...axis,
294
- ...state,
295
- controlEnd: _end,
296
- };
297
- };
298
-
299
- /**
300
- * 获取指定索引范围的ticks(支持循环)
301
- * @param {Array} ticks 所有ticks
302
- * @param {number} currentIndex 当前起始索引
303
- * @param {number} length 需要的长度
304
- * @returns {Array} 目标ticks数组
305
- */
306
- const getTicks = (ticks, currentIndex, length) => {
307
- const _currentIndex = +currentIndex;
308
- const ticksLength = ticks.length;
309
- if (ticksLength <= length) return ticks;
310
-
311
- const _end = _currentIndex + length;
312
- if (ticksLength < _end) {
313
- const prev = ticks.slice(_currentIndex, ticksLength);
314
- const next = ticks.slice(0, _end - ticksLength);
315
- return [...prev, ...next];
316
- }
317
- return ticks.slice(_currentIndex, _end);
318
- };
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { animate, linear } from "popmotion";
3
+
4
+ const initialState = {
5
+ currentIndex: null,
6
+ flag: false, // 首次加载标识:true-首次加载不执行动画,false-可执行轮播
7
+ };
8
+
9
+ /**
10
+ * x轴滚动逻辑
11
+ * @param {Object} axis x轴配置项
12
+ * @param {Object} config x轴轮播动画的配置项
13
+ * @returns {Map} 返回更新后的x轴配置(ticks/scaler/range 变更)
14
+ */
15
+ export default (axis, config, isHover, controlInfo, active) => {
16
+ const { show, interval, duration, hover } = config;
17
+ const { isC, cPercent } = controlInfo;
18
+
19
+ const {
20
+ tickCount: count,
21
+ allTicks,
22
+ scaler,
23
+ start,
24
+ end,
25
+ step,
26
+ ticks,
27
+ lengthWithoutPaddingOuter,
28
+ } = axis;
29
+ const tickLength = ticks.length;
30
+ const tickCount = isC ? allTicks.length : count;
31
+
32
+ const scale = isC && cPercent > 0 ? cPercent : 1;
33
+ const _start = start / scale;
34
+ const _end = end / scale;
35
+ const canCarousel = useMemo(
36
+ () => show && active && tickCount > 0 && tickLength > tickCount,
37
+ [show, active, tickLength, tickCount]
38
+ );
39
+
40
+ const [state, setState] = useState({
41
+ scaler,
42
+ step,
43
+ ticks,
44
+ });
45
+ const [status, setStatus] = useState(initialState);
46
+ const [ready, setReady] = useState(true); // true 才允许开启下一轮动画(用于 interval 等待)
47
+ const [wakeToken, setWakeToken] = useState(0);
48
+ const hoverRef = useRef(isHover);
49
+ const prevHoverRef = useRef(isHover);
50
+ const intervalTimerRef = useRef(null);
51
+ const isAnimatingRef = useRef(false);
52
+ const latestRef = useRef({
53
+ allTicks,
54
+ scaler,
55
+ tickCount,
56
+ tickLength,
57
+ lengthWithoutPaddingOuter,
58
+ _start,
59
+ _end,
60
+ interval,
61
+ duration,
62
+ hover,
63
+ });
64
+
65
+ // 避免 hover 触发 render 时 effect 误重启动画:用 ref 持有最新输入
66
+ useEffect(() => {
67
+ latestRef.current = {
68
+ allTicks,
69
+ scaler,
70
+ tickCount,
71
+ tickLength,
72
+ lengthWithoutPaddingOuter,
73
+ _start,
74
+ _end,
75
+ interval,
76
+ duration,
77
+ hover,
78
+ };
79
+ }, [
80
+ allTicks,
81
+ scaler,
82
+ tickCount,
83
+ tickLength,
84
+ lengthWithoutPaddingOuter,
85
+ _start,
86
+ _end,
87
+ interval,
88
+ duration,
89
+ hover,
90
+ ]);
91
+
92
+ // hover 变化不打断正在进行的动画:只更新引用、并在需要时取消「下一轮」定时器
93
+ useEffect(() => {
94
+ const prev = prevHoverRef.current;
95
+ prevHoverRef.current = isHover;
96
+ hoverRef.current = isHover;
97
+ if (latestRef.current.hover && isHover && intervalTimerRef.current) {
98
+ clearTimeout(intervalTimerRef.current);
99
+ intervalTimerRef.current = null;
100
+ }
101
+
102
+ // 仅在 hover 从 true -> false 时唤醒轮播(避免在非 hover 状态下反复唤醒导致 interval 失效)
103
+ if (
104
+ latestRef.current.hover &&
105
+ prev === true &&
106
+ isHover === false &&
107
+ canCarousel &&
108
+ status.currentIndex !== null &&
109
+ !isAnimatingRef.current
110
+ ) {
111
+ setReady(true);
112
+ setWakeToken((v) => v + 1);
113
+ }
114
+ }, [isHover, canCarousel, status.currentIndex]);
115
+
116
+ // 初始化:仅在基础条件变化时设置初始索引
117
+ useEffect(() => {
118
+ if (show && tickCount > 0 && tickLength > tickCount) {
119
+ setStatus({
120
+ currentIndex: 0,
121
+ flag: true,
122
+ });
123
+ } else {
124
+ setStatus(initialState);
125
+ }
126
+ }, [show, tickCount, tickLength]);
127
+ useEffect(() => {
128
+ let animation;
129
+ const { currentIndex, flag } = status;
130
+ const latest = latestRef.current;
131
+
132
+ // 执行条件:显示、非hover暂停、激活、有足够的轮播项
133
+ const canRun = canCarousel && !(latest.hover && hoverRef.current);
134
+
135
+ if (currentIndex !== null && canRun && latest.tickCount > 0) {
136
+ if (flag) {
137
+ // 首次加载:仅初始化视图,不执行动画
138
+ const step = latest.lengthWithoutPaddingOuter / latest.tickCount;
139
+ if (!Number.isFinite(step)) return;
140
+ const _ticks = latest.allTicks.slice(currentIndex, latest.tickCount);
141
+ setState({
142
+ step,
143
+ ticks: _ticks,
144
+ scaler: latest.scaler
145
+ .copy()
146
+ .domain(_ticks)
147
+ .range([latest._start, latest._end]),
148
+ });
149
+
150
+ // 首次加载完成后,启动第一轮动画(等待interval后执行)
151
+ if (latest.interval >= 0) {
152
+ intervalTimerRef.current = setTimeout(() => {
153
+ setStatus((prev) => ({ ...prev, flag: false }));
154
+ }, latest.interval * 1000);
155
+ } else {
156
+ setStatus((prev) => ({ ...prev, flag: false }));
157
+ }
158
+ } else {
159
+ // 非首次加载:执行动画 + 后续逻辑
160
+ if (!ready) return;
161
+ const step = latest.lengthWithoutPaddingOuter / latest.tickCount;
162
+ if (!Number.isFinite(step)) return;
163
+
164
+ animation = animate({
165
+ from: 0,
166
+ to: -1,
167
+ duration: latest.duration * 1000,
168
+ ease: linear,
169
+ onPlay: () => {
170
+ isAnimatingRef.current = true;
171
+ // 动画开始:更新ticks和scaler初始范围
172
+ // 这里需要保证动画是「当前项滚出,下一项滚入」
173
+ // 因此后续窗口从 currentIndex + 1 开始,避免首项重复导致视觉上「从第一项滚到第一项」的闪烁
174
+ setState((axis) => {
175
+ const { ticks, scaler } = axis;
176
+ const [tick] = ticks;
177
+ const _ticks = [
178
+ tick,
179
+ ...getTicks(
180
+ latest.allTicks,
181
+ currentIndex + 1,
182
+ latest.tickCount
183
+ ),
184
+ ];
185
+ return {
186
+ ...axis,
187
+ ticks: _ticks,
188
+ scaler: scaler
189
+ .copy()
190
+ .range([latest._start, latest._end + step])
191
+ .domain(_ticks),
192
+ };
193
+ });
194
+ },
195
+ onUpdate: (v) => {
196
+ // 动画过程:实时更新scaler范围(实现滚动效果)
197
+ setState((axis) => {
198
+ const { scaler, step } = axis;
199
+ return {
200
+ ...axis,
201
+ scaler: scaler
202
+ .copy()
203
+ .range([
204
+ latest._start + step * v,
205
+ latest._end + step + step * v,
206
+ ]),
207
+ };
208
+ });
209
+ },
210
+ onComplete: () => {
211
+ isAnimatingRef.current = false;
212
+ // 本轮结束后进入等待态,避免立即开启下一轮(保证 interval 生效)
213
+ setReady(false);
214
+ // 动画完成:重置ticks和scaler
215
+ setState((axis) => {
216
+ const { scaler, ticks } = axis;
217
+ const _ticks = ticks.slice(1, ticks.length);
218
+ return {
219
+ ...axis,
220
+ ticks: _ticks,
221
+ scaler: scaler
222
+ .copy()
223
+ .range([latest._start, latest._end])
224
+ .domain(_ticks),
225
+ };
226
+ });
227
+
228
+ const nextIndex = (currentIndex + 1) % latest.tickLength;
229
+
230
+ // 无论是否 hover,都先把 currentIndex 同步到下一屏起点
231
+ // 否则在 interval 暂停期触发 hover/离开,会用旧索引重启,视觉上“回滚一格”
232
+ setStatus((prev) => ({ ...prev, currentIndex: nextIndex }));
233
+
234
+ // hover 触发时:让当前动画跑完后暂停(不再排下一轮)
235
+ if (latest.hover && hoverRef.current) return;
236
+
237
+ // 非hover:动画完成后,等待interval时长,再触发下一轮动画
238
+ intervalTimerRef.current = setTimeout(() => {
239
+ setReady(true);
240
+ setWakeToken((v) => v + 1);
241
+ }, latest.interval * 1000);
242
+ },
243
+ });
244
+ }
245
+ }
246
+
247
+ // 清理副作用:停止动画、清除定时器
248
+ return () => {
249
+ if (animation) {
250
+ animation.stop();
251
+ isAnimatingRef.current = false;
252
+ }
253
+ if (intervalTimerRef.current) {
254
+ clearTimeout(intervalTimerRef.current);
255
+ intervalTimerRef.current = null;
256
+ }
257
+ };
258
+ }, [canCarousel, status.currentIndex, status.flag, wakeToken, ready]);
259
+
260
+ // 重置逻辑:当无有效索引时恢复初始状态
261
+ useEffect(() => {
262
+ if (status.currentIndex === null) {
263
+ const _ticks = scaler.type === "linear" ? scaler.domain() : allTicks;
264
+ setState({
265
+ step,
266
+ scaler: scaler.copy().domain(_ticks).range([_start, _end]),
267
+ ticks,
268
+ });
269
+ }
270
+ }, [status.currentIndex, scaler, _start, _end, step, ticks, allTicks]);
271
+
272
+ // x 轴可见项数量(count)发生变化时,安全地重置轮播节奏
273
+ useEffect(() => {
274
+ // 清掉当前的 interval 计时器,避免用旧配置继续排队
275
+ if (intervalTimerRef.current) {
276
+ clearTimeout(intervalTimerRef.current);
277
+ intervalTimerRef.current = null;
278
+ }
279
+
280
+ // 重置动画节奏,让配置变更后「立即」按最新的 tickCount/tickLength 重新初始化
281
+ // 不能简单 setStatus(initialState),否则会覆盖上方的初始化 effect(先开轮播再改数量时会被清成 null,导致展示全部且不轮播)
282
+ isAnimatingRef.current = false;
283
+ setReady(true);
284
+ setStatus(() => {
285
+ // 与初始化 effect 保持一致:只要满足条件就从 0 开始重新初始化
286
+ if (show && active && tickCount > 0 && tickLength > tickCount) {
287
+ return { currentIndex: 0, flag: true };
288
+ }
289
+ return initialState;
290
+ });
291
+ setWakeToken((v) => v + 1);
292
+ }, [count, show, active, tickLength, tickCount]);
293
+
294
+ return {
295
+ ...axis,
296
+ ...state,
297
+ controlEnd: _end,
298
+ };
299
+ };
300
+
301
+ /**
302
+ * 获取指定索引范围的ticks(支持循环)
303
+ * @param {Array} ticks 所有ticks
304
+ * @param {number} currentIndex 当前起始索引
305
+ * @param {number} length 需要的长度
306
+ * @returns {Array} 目标ticks数组
307
+ */
308
+ const getTicks = (ticks, currentIndex, length) => {
309
+ const _currentIndex = +currentIndex;
310
+ const ticksLength = ticks.length;
311
+ if (ticksLength <= length) return ticks;
312
+
313
+ const _end = _currentIndex + length;
314
+ if (ticksLength < _end) {
315
+ const prev = ticks.slice(_currentIndex, ticksLength);
316
+ const next = ticks.slice(0, _end - ticksLength);
317
+ return [...prev, ...next];
318
+ }
319
+ return ticks.slice(_currentIndex, _end);
320
+ };