@agions/taroviz 1.7.0 → 1.9.0

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.
@@ -0,0 +1,427 @@
1
+ /**
2
+ * useAnimation - 图表动画控制 Hook
3
+ * 提供图表动画的播放、暂停、控制等功能
4
+ */
5
+ import { useState, useCallback, useRef, useEffect } from 'react';
6
+ import type { ChartInstance } from './index';
7
+
8
+ /**
9
+ * 动画状态
10
+ */
11
+ export type AnimationStatus = 'playing' | 'paused' | 'stopped';
12
+
13
+ /**
14
+ * 动画配置选项
15
+ */
16
+ export interface UseAnimationOptions {
17
+ /** 动画时长 (ms) */
18
+ duration?: number;
19
+ /** 动画缓动函数 */
20
+ easing?: 'cubicOut' | 'cubicIn' | 'cubicInOut' | 'linear' | 'sinusoidalIn' | 'sinusoidalOut';
21
+ /** 是否禁用动画 */
22
+ disabled?: boolean;
23
+ /** 动画延迟 (ms) */
24
+ delay?: number;
25
+ /** 是否循环 */
26
+ loop?: boolean;
27
+ /** 循环次数 (-1 表示无限) */
28
+ loopCount?: number;
29
+ }
30
+
31
+ /**
32
+ * 动画状态返回值
33
+ */
34
+ export interface UseAnimationReturn {
35
+ /** 当前动画状态 */
36
+ status: AnimationStatus;
37
+ /** 当前动画帧 */
38
+ frame: number;
39
+ /** 总帧数 */
40
+ totalFrames: number;
41
+ /** 动画进度 (0-1) */
42
+ progress: number;
43
+ /** 播放动画 */
44
+ play: () => void;
45
+ /** 暂停动画 */
46
+ pause: () => void;
47
+ /** 停止动画并重置 */
48
+ stop: () => void;
49
+ /** 跳转到指定帧 */
50
+ seekTo: (frame: number) => void;
51
+ /** 跳转到指定进度 */
52
+ seekToProgress: (progress: number) => void;
53
+ /** 设置播放速度 */
54
+ setPlaybackSpeed: (speed: number) => void;
55
+ /** 播放速度 */
56
+ playbackSpeed: number;
57
+ }
58
+
59
+ /**
60
+ * 使用图表动画控制
61
+ * @param chartInstance 图表实例
62
+ * @param options 配置选项
63
+ * @returns 动画控制接口
64
+ */
65
+ export function useAnimation(
66
+ chartInstance: ChartInstance | null,
67
+ options: UseAnimationOptions = {}
68
+ ): UseAnimationReturn {
69
+ const {
70
+ duration = 1000,
71
+ easing = 'cubicOut',
72
+ disabled = false,
73
+ delay = 0,
74
+ loop = false,
75
+ loopCount = -1,
76
+ } = options;
77
+
78
+ // State
79
+ const [status, setStatus] = useState<AnimationStatus>('stopped');
80
+ const [frame, setFrame] = useState(0);
81
+ const [playbackSpeed, setPlaybackSpeedState] = useState(1);
82
+
83
+ // Refs
84
+ const animationRef = useRef<{
85
+ startTime: number;
86
+ currentFrame: number;
87
+ loopCounter: number;
88
+ animationId: number | null;
89
+ isPaused: boolean;
90
+ }>({
91
+ startTime: 0,
92
+ currentFrame: 0,
93
+ loopCounter: 0,
94
+ animationId: null,
95
+ isPaused: false,
96
+ });
97
+
98
+ const chartRef = useRef<ChartInstance | null>(null);
99
+ chartRef.current = chartInstance;
100
+
101
+ // 计算总帧数(假设 60fps)
102
+ const totalFrames = Math.ceil((duration / 1000) * 60);
103
+
104
+ // 缓动函数
105
+ const easingFunctions: Record<string, (t: number) => number> = {
106
+ cubicOut: (t) => 1 - Math.pow(1 - t, 3),
107
+ cubicIn: (t) => t * t * t,
108
+ cubicInOut: (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
109
+ linear: (t) => t,
110
+ sinusoidalIn: (t) => 1 - Math.cos((t * Math.PI) / 2),
111
+ sinusoidalOut: (t) => Math.sin((t * Math.PI) / 2),
112
+ };
113
+
114
+ // 计算当前进度对应的帧
115
+ const calculateFrame = useCallback(
116
+ (progress: number) => {
117
+ return Math.floor(progress * totalFrames);
118
+ },
119
+ [totalFrames]
120
+ );
121
+
122
+ // 设置播放速度
123
+ const setPlaybackSpeed = useCallback((speed: number) => {
124
+ setPlaybackSpeedState(Math.max(0.1, Math.min(10, speed)));
125
+ }, []);
126
+
127
+ // 动画更新函数
128
+ const animate = useCallback(() => {
129
+ const anim = animationRef.current;
130
+ const chart = chartRef.current;
131
+
132
+ if (!chart || anim.isPaused || disabled) {
133
+ return;
134
+ }
135
+
136
+ const elapsed = performance.now() - anim.startTime;
137
+ const adjustedDuration = duration / playbackSpeed;
138
+ const effectiveElapsed = anim.isPaused ? 0 : elapsed - delay;
139
+
140
+ if (effectiveElapsed < 0) {
141
+ anim.animationId = requestAnimationFrame(animate);
142
+ return;
143
+ }
144
+
145
+ // 计算当前进度
146
+ let progress = Math.min(effectiveElapsed / adjustedDuration, 1);
147
+ progress = easingFunctions[easing](progress);
148
+
149
+ // 计算当前帧
150
+ const currentFrame = calculateFrame(progress);
151
+ anim.currentFrame = currentFrame;
152
+
153
+ setFrame(currentFrame);
154
+
155
+ // 更新图表配置以反映动画进度
156
+ try {
157
+ // ECharts 通过 setOption 配合 notMerge: false 更新动画
158
+ if (chart.setOption && !anim.isPaused) {
159
+ // 触发图表重新渲染
160
+ chart.setOption({}, false, true);
161
+ }
162
+ } catch (e) {
163
+ console.warn('[useAnimation] Failed to update chart:', e);
164
+ }
165
+
166
+ // 检查是否完成
167
+ if (effectiveElapsed >= adjustedDuration) {
168
+ if (loop && (loopCount === -1 || anim.loopCounter < loopCount)) {
169
+ // 继续循环
170
+ anim.loopCounter++;
171
+ anim.startTime = performance.now();
172
+ anim.isPaused = false;
173
+ setStatus('playing');
174
+ } else {
175
+ // 动画结束
176
+ setStatus('stopped');
177
+ anim.animationId = null;
178
+ return;
179
+ }
180
+ }
181
+
182
+ anim.animationId = requestAnimationFrame(animate);
183
+ }, [duration, easing, disabled, delay, loop, loopCount, playbackSpeed, easingFunctions, calculateFrame]);
184
+
185
+ // 播放动画
186
+ const play = useCallback(() => {
187
+ const chart = chartRef.current;
188
+ if (!chart || disabled) return;
189
+
190
+ const anim = animationRef.current;
191
+
192
+ if (status === 'paused') {
193
+ // 从暂停恢复
194
+ anim.isPaused = false;
195
+ anim.startTime = performance.now() - (anim.currentFrame / totalFrames) * duration;
196
+ setStatus('playing');
197
+ } else {
198
+ // 开始新动画
199
+ anim.startTime = performance.now();
200
+ anim.currentFrame = 0;
201
+ anim.loopCounter = 0;
202
+ anim.isPaused = false;
203
+ setStatus('playing');
204
+ }
205
+
206
+ if (anim.animationId === null) {
207
+ anim.animationId = requestAnimationFrame(animate);
208
+ }
209
+ }, [status, disabled, totalFrames, duration, animate]);
210
+
211
+ // 暂停动画
212
+ const pause = useCallback(() => {
213
+ const anim = animationRef.current;
214
+ anim.isPaused = true;
215
+ setStatus('paused');
216
+ }, []);
217
+
218
+ // 停止动画并重置
219
+ const stop = useCallback(() => {
220
+ const anim = animationRef.current;
221
+
222
+ if (anim.animationId !== null) {
223
+ cancelAnimationFrame(anim.animationId);
224
+ anim.animationId = null;
225
+ }
226
+
227
+ anim.isPaused = false;
228
+ anim.currentFrame = 0;
229
+ anim.loopCounter = 0;
230
+ setStatus('stopped');
231
+ setFrame(0);
232
+ }, []);
233
+
234
+ // 跳转到指定帧
235
+ const seekTo = useCallback(
236
+ (targetFrame: number) => {
237
+ const anim = animationRef.current;
238
+ const chart = chartRef.current;
239
+ if (!chart) return;
240
+
241
+ const clampedFrame = Math.max(0, Math.min(totalFrames, targetFrame));
242
+ anim.currentFrame = clampedFrame;
243
+ setFrame(clampedFrame);
244
+
245
+ // 更新图表状态
246
+ const progress = clampedFrame / totalFrames;
247
+ try {
248
+ if (chart.setOption) {
249
+ chart.setOption({}, false, true);
250
+ }
251
+ } catch (e) {
252
+ console.warn('[useAnimation] Failed to seek:', e);
253
+ }
254
+ },
255
+ [totalFrames]
256
+ );
257
+
258
+ // 跳转到指定进度
259
+ const seekToProgress = useCallback(
260
+ (progress: number) => {
261
+ const targetProgress = Math.max(0, Math.min(1, progress));
262
+ const targetFrame = calculateFrame(targetProgress);
263
+ seekTo(targetFrame);
264
+ },
265
+ [calculateFrame, seekTo]
266
+ );
267
+
268
+ // 进度
269
+ const progress = totalFrames > 0 ? frame / totalFrames : 0;
270
+
271
+ // 清理
272
+ useEffect(() => {
273
+ return () => {
274
+ const anim = animationRef.current;
275
+ if (anim.animationId !== null) {
276
+ cancelAnimationFrame(anim.animationId);
277
+ }
278
+ };
279
+ }, []);
280
+
281
+ // 组件卸载时停止动画
282
+ useEffect(() => {
283
+ if (!chartInstance) {
284
+ stop();
285
+ }
286
+ }, [chartInstance, stop]);
287
+
288
+ return {
289
+ status,
290
+ frame,
291
+ totalFrames,
292
+ progress,
293
+ play,
294
+ pause,
295
+ stop,
296
+ seekTo,
297
+ seekToProgress,
298
+ setPlaybackSpeed,
299
+ playbackSpeed,
300
+ };
301
+ }
302
+
303
+ /**
304
+ * 渐进式加载动画 Hook
305
+ * 用于大数据的分批次加载动画
306
+ */
307
+ export interface UseProgressiveLoadingOptions {
308
+ /** 每批加载的数据量 */
309
+ batchSize?: number;
310
+ /** 批次间隔 (ms) */
311
+ interval?: number;
312
+ /** 是否自动开始 */
313
+ autoStart?: boolean;
314
+ /** 加载完成回调 */
315
+ onBatchLoaded?: (batchIndex: number, totalBatches: number) => void;
316
+ /** 全部加载完成回调 */
317
+ onComplete?: () => void;
318
+ }
319
+
320
+ export interface UseProgressiveLoadingReturn {
321
+ /** 当前批次 */
322
+ currentBatch: number;
323
+ /** 总批次数 */
324
+ totalBatches: number;
325
+ /** 加载进度 */
326
+ progress: number;
327
+ /** 是否正在加载 */
328
+ isLoading: boolean;
329
+ /** 开始加载 */
330
+ start: () => void;
331
+ /** 暂停加载 */
332
+ pause: () => void;
333
+ /** 停止加载 */
334
+ stop: () => void;
335
+ /** 重置 */
336
+ reset: () => void;
337
+ }
338
+
339
+ export function useProgressiveLoading(
340
+ totalCount: number,
341
+ options: UseProgressiveLoadingOptions = {}
342
+ ): UseProgressiveLoadingReturn {
343
+ const {
344
+ batchSize = 1000,
345
+ interval = 100,
346
+ autoStart = false,
347
+ onBatchLoaded,
348
+ onComplete,
349
+ } = options;
350
+
351
+ const totalBatches = Math.ceil(totalCount / batchSize);
352
+ const [currentBatch, setCurrentBatch] = useState(0);
353
+ const [isLoading, setIsLoading] = useState(false);
354
+ const intervalRef = useRef<number | null>(null);
355
+
356
+ const progress = totalBatches > 0 ? currentBatch / totalBatches : 0;
357
+
358
+ const start = useCallback(() => {
359
+ if (isLoading) return;
360
+
361
+ setIsLoading(true);
362
+ setCurrentBatch(0);
363
+
364
+ intervalRef.current = window.setInterval(() => {
365
+ setCurrentBatch(prev => {
366
+ const next = prev + 1;
367
+ onBatchLoaded?.(next, totalBatches);
368
+
369
+ if (next >= totalBatches) {
370
+ if (intervalRef.current !== null) {
371
+ clearInterval(intervalRef.current);
372
+ intervalRef.current = null;
373
+ }
374
+ setIsLoading(false);
375
+ onComplete?.();
376
+ return totalBatches;
377
+ }
378
+
379
+ return next;
380
+ });
381
+ }, interval);
382
+ }, [isLoading, interval, totalBatches, onBatchLoaded, onComplete]);
383
+
384
+ const pause = useCallback(() => {
385
+ if (intervalRef.current !== null) {
386
+ clearInterval(intervalRef.current);
387
+ intervalRef.current = null;
388
+ }
389
+ setIsLoading(false);
390
+ }, []);
391
+
392
+ const stop = useCallback(() => {
393
+ pause();
394
+ setCurrentBatch(0);
395
+ }, [pause]);
396
+
397
+ const reset = useCallback(() => {
398
+ stop();
399
+ }, [stop]);
400
+
401
+ useEffect(() => {
402
+ if (autoStart) {
403
+ start();
404
+ }
405
+ }, [autoStart, start]);
406
+
407
+ useEffect(() => {
408
+ return () => {
409
+ if (intervalRef.current !== null) {
410
+ clearInterval(intervalRef.current);
411
+ }
412
+ };
413
+ }, []);
414
+
415
+ return {
416
+ currentBatch,
417
+ totalBatches,
418
+ progress,
419
+ isLoading,
420
+ start,
421
+ pause,
422
+ stop,
423
+ reset,
424
+ };
425
+ }
426
+
427
+ export default useAnimation;
@@ -0,0 +1,291 @@
1
+ /**
2
+ * usePerformance - 图表性能监控 Hook
3
+ * 提供实时性能指标监控和报告功能
4
+ */
5
+ import { useState, useEffect, useCallback, useRef } from 'react';
6
+ import { PerformanceAnalyzer, PerformanceMetricType, PerformanceMetric } from '../core/utils/performance';
7
+
8
+ /**
9
+ * 性能监控配置
10
+ */
11
+ export interface UsePerformanceOptions {
12
+ /** 是否启用监控 */
13
+ enabled?: boolean;
14
+ /** 采样间隔 (ms) */
15
+ sampleInterval?: number;
16
+ /** 是否显示实时 FPS */
17
+ showFPS?: boolean;
18
+ /** FPS 告警阈值 */
19
+ fpsWarningThreshold?: number;
20
+ /** FPS 严重告警阈值 */
21
+ fpsCriticalThreshold?: number;
22
+ /** 自动启动 */
23
+ autoStart?: boolean;
24
+ }
25
+
26
+ /**
27
+ * 性能指标状态
28
+ */
29
+ export interface PerformanceState {
30
+ /** 当前 FPS */
31
+ fps: number;
32
+ /** 平均 FPS */
33
+ avgFps: number;
34
+ /** 最低 FPS */
35
+ minFps: number;
36
+ /** 最高 FPS */
37
+ maxFps: number;
38
+ /** FPS 状态: normal | warning | critical */
39
+ fpsStatus: 'normal' | 'warning' | 'critical';
40
+ /** 帧率历史 */
41
+ fpsHistory: number[];
42
+ /** 是否正在监控 */
43
+ isMonitoring: boolean;
44
+ }
45
+
46
+ /**
47
+ * 性能监控返回值
48
+ */
49
+ export interface UsePerformanceReturn {
50
+ /** 性能状态 */
51
+ state: PerformanceState;
52
+ /** 开始监控 */
53
+ start: () => void;
54
+ /** 停止监控 */
55
+ stop: () => void;
56
+ /** 重置统计数据 */
57
+ reset: () => void;
58
+ /** 获取性能报告 */
59
+ getReport: () => PerformanceMetric[];
60
+ /** FPS 告警回调 */
61
+ onFpsWarning?: (fps: number) => void;
62
+ }
63
+
64
+ /**
65
+ * 使用图表性能监控
66
+ * @param options 配置选项
67
+ * @returns 性能监控接口
68
+ */
69
+ export function usePerformance(options: UsePerformanceOptions = {}): UsePerformanceReturn {
70
+ const {
71
+ enabled = true,
72
+ sampleInterval = 1000,
73
+ showFPS = true,
74
+ fpsWarningThreshold = 30,
75
+ fpsCriticalThreshold = 15,
76
+ autoStart = true,
77
+ } = options;
78
+
79
+ // Refs
80
+ const analyzerRef = useRef<PerformanceAnalyzer | null>(null);
81
+ const fpsHistoryRef = useRef<number[]>([]);
82
+ const animationFrameRef = useRef<number | null>(null);
83
+ const lastFrameTimeRef = useRef<number>(0);
84
+ const fpsAccumulatorRef = useRef<{ frames: number; lastTime: number }>({ frames: 0, lastTime: 0 });
85
+
86
+ // State
87
+ const [state, setState] = useState<PerformanceState>({
88
+ fps: 60,
89
+ avgFps: 60,
90
+ minFps: 60,
91
+ maxFps: 60,
92
+ fpsStatus: 'normal',
93
+ fpsHistory: [],
94
+ isMonitoring: false,
95
+ });
96
+
97
+ /**
98
+ * 计算 FPS 状态
99
+ */
100
+ const calculateFpsStatus = useCallback(
101
+ (fps: number): 'normal' | 'warning' | 'critical' => {
102
+ if (fps <= fpsCriticalThreshold) return 'critical';
103
+ if (fps <= fpsWarningThreshold) return 'warning';
104
+ return 'normal';
105
+ },
106
+ [fpsWarningThreshold, fpsCriticalThreshold]
107
+ );
108
+
109
+ /**
110
+ * 更新 FPS 计算
111
+ */
112
+ const updateFps = useCallback(() => {
113
+ if (!enabled) return;
114
+
115
+ const now = performance.now();
116
+ const delta = now - lastFrameTimeRef.current;
117
+ lastFrameTimeRef.current = now;
118
+
119
+ // 累积帧数
120
+ fpsAccumulatorRef.current.frames++;
121
+
122
+ // 每秒计算一次 FPS
123
+ if (now - fpsAccumulatorRef.current.lastTime >= 1000) {
124
+ const fps = Math.round((fpsAccumulatorRef.current.frames * 1000) / (now - fpsAccumulatorRef.current.lastTime));
125
+ const history = fpsHistoryRef.current;
126
+
127
+ // 更新历史
128
+ history.push(fps);
129
+ if (history.length > 60) {
130
+ history.shift();
131
+ }
132
+
133
+ // 计算统计数据
134
+ const avgFps = Math.round(history.reduce((a, b) => a + b, 0) / history.length);
135
+ const minFps = Math.min(...history);
136
+ const maxFps = Math.max(...history);
137
+
138
+ setState(prev => ({
139
+ ...prev,
140
+ fps,
141
+ avgFps,
142
+ minFps,
143
+ maxFps,
144
+ fpsStatus: calculateFpsStatus(fps),
145
+ fpsHistory: [...history],
146
+ }));
147
+
148
+ // 重置计数器
149
+ fpsAccumulatorRef.current.frames = 0;
150
+ fpsAccumulatorRef.current.lastTime = now;
151
+ }
152
+
153
+ // 继续下一帧
154
+ animationFrameRef.current = requestAnimationFrame(updateFps);
155
+ }, [enabled, calculateFpsStatus]);
156
+
157
+ /**
158
+ * 开始监控
159
+ */
160
+ const start = useCallback(() => {
161
+ if (!enabled) return;
162
+
163
+ // Prevent starting multiple RAF loops
164
+ if (animationFrameRef.current !== null) return;
165
+
166
+ // 初始化分析器
167
+ if (!analyzerRef.current) {
168
+ analyzerRef.current = PerformanceAnalyzer.getInstance({
169
+ enabled: true,
170
+ sampleInterval,
171
+ autoStart: false,
172
+ });
173
+ }
174
+
175
+ analyzerRef.current.start();
176
+
177
+ // 重置 FPS 计算
178
+ fpsAccumulatorRef.current = { frames: 0, lastTime: performance.now() };
179
+ lastFrameTimeRef.current = performance.now();
180
+ fpsHistoryRef.current = [];
181
+
182
+ // 开始 FPS 监控循环
183
+ animationFrameRef.current = requestAnimationFrame(updateFps);
184
+
185
+ setState(prev => ({ ...prev, isMonitoring: true }));
186
+ }, [enabled, sampleInterval, updateFps]);
187
+
188
+ /**
189
+ * 停止监控
190
+ */
191
+ const stop = useCallback(() => {
192
+ // 停止 FPS 监控
193
+ if (animationFrameRef.current !== null) {
194
+ cancelAnimationFrame(animationFrameRef.current);
195
+ animationFrameRef.current = null;
196
+ }
197
+
198
+ // 停止分析器
199
+ analyzerRef.current?.stop();
200
+
201
+ setState(prev => ({ ...prev, isMonitoring: false }));
202
+ }, []);
203
+
204
+ /**
205
+ * 重置统计数据
206
+ */
207
+ const reset = useCallback(() => {
208
+ PerformanceAnalyzer.resetInstance();
209
+ fpsHistoryRef.current = [];
210
+ setState(prev => ({
211
+ ...prev,
212
+ fps: 60,
213
+ avgFps: 60,
214
+ minFps: 60,
215
+ maxFps: 60,
216
+ fpsStatus: 'normal',
217
+ fpsHistory: [],
218
+ }));
219
+ }, []);
220
+
221
+ /**
222
+ * 获取性能报告
223
+ */
224
+ const getReport = useCallback((): PerformanceMetric[] => {
225
+ if (!analyzerRef.current) return [];
226
+
227
+ try {
228
+ const report = analyzerRef.current.getAllMetrics?.();
229
+ if (!report) return [];
230
+ return Array.from(report.values()).flat();
231
+ } catch {
232
+ return [];
233
+ }
234
+ }, []);
235
+
236
+ // 自动启动
237
+ useEffect(() => {
238
+ if (autoStart && enabled) {
239
+ start();
240
+ }
241
+
242
+ return () => {
243
+ stop();
244
+ };
245
+ }, [autoStart, enabled, start, stop]);
246
+
247
+ return {
248
+ state,
249
+ start,
250
+ stop,
251
+ reset,
252
+ getReport,
253
+ };
254
+ }
255
+
256
+ /**
257
+ * 轻量级 FPS 监控 Hook
258
+ * 用于实时显示图表 FPS
259
+ */
260
+ export function useFpsMonitor(): number {
261
+ const [fps, setFps] = useState(60);
262
+ const frameCountRef = useRef(0);
263
+ const lastTimeRef = useRef(performance.now());
264
+
265
+ useEffect(() => {
266
+ let animationId: number;
267
+
268
+ const tick = () => {
269
+ frameCountRef.current++;
270
+ const now = performance.now();
271
+ const elapsed = now - lastTimeRef.current;
272
+
273
+ if (elapsed >= 1000) {
274
+ const currentFps = Math.round((frameCountRef.current * 1000) / elapsed);
275
+ setFps(currentFps);
276
+ frameCountRef.current = 0;
277
+ lastTimeRef.current = now;
278
+ }
279
+
280
+ animationId = requestAnimationFrame(tick);
281
+ };
282
+
283
+ animationId = requestAnimationFrame(tick);
284
+
285
+ return () => cancelAnimationFrame(animationId);
286
+ }, []);
287
+
288
+ return fps;
289
+ }
290
+
291
+ export default usePerformance;