@agions/taroviz 1.6.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.
- package/README.md +30 -21
- package/dist/cjs/index.js +1 -1
- package/dist/esm/index.js +49471 -2199
- package/package.json +2 -1
- package/src/adapters/__tests__/index.test.ts +4 -2
- package/src/adapters/h5/index.ts +16 -0
- package/src/adapters/types.ts +28 -120
- package/src/charts/common/BaseChartWrapper.tsx +193 -32
- package/src/charts/index.ts +5 -1
- package/src/charts/liquid/index.tsx +227 -0
- package/src/charts/liquid/types.ts +130 -0
- package/src/charts/tree/index.tsx +117 -0
- package/src/charts/tree/types.ts +174 -0
- package/src/charts/types.ts +1 -1
- package/src/components/DataFilter/index.tsx +587 -0
- package/src/core/animation/AnimationManager.ts +69 -42
- package/src/core/components/BaseChart.tsx +72 -9
- package/src/core/types/common.ts +21 -110
- package/src/core/types/index.ts +4 -135
- package/src/core/types/platform.ts +38 -230
- package/src/core/utils/drillDown.ts +643 -0
- package/src/core/utils/export/ExportUtils.ts +10 -1
- package/src/core/utils/performance/PerformanceAnalyzer.ts +21 -1
- package/src/core/utils/performance/types.ts +5 -0
- package/src/hooks/__tests__/index.test.tsx +7 -5
- package/src/hooks/index.ts +41 -2
- package/src/hooks/useAnimation.ts +427 -0
- package/src/hooks/useChartConnect.ts +362 -0
- package/src/hooks/useChartDownload.ts +692 -0
- package/src/hooks/useDataZoom.ts +323 -0
- package/src/hooks/usePerformance.ts +291 -0
- package/src/index.ts +25 -2
- package/src/themes/__tests__/index.test.ts +7 -13
- package/src/themes/index.ts +3 -0
- package/src/themes/useAutoTheme.ts +66 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useDataZoom - 数据缩放 Hook
|
|
3
|
+
* 控制图表的数据缩放区域(dataZoom),支持拖拽和滚轮缩放
|
|
4
|
+
*/
|
|
5
|
+
import { useRef, useCallback, useEffect } from 'react';
|
|
6
|
+
import type { RefObject } from 'react';
|
|
7
|
+
import type { ChartInstance } from './index';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// 类型定义
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/** dataZoom 类型 */
|
|
14
|
+
export type DataZoomType = 'inside' | 'slider';
|
|
15
|
+
|
|
16
|
+
/** dataZoom 配置选项 */
|
|
17
|
+
export interface UseDataZoomOptions {
|
|
18
|
+
/** dataZoom 类型: inside=内置型(滚轮/双指), slider=滑块型 */
|
|
19
|
+
type?: DataZoomType;
|
|
20
|
+
/** 起始位置 (0-100) */
|
|
21
|
+
start?: number;
|
|
22
|
+
/** 结束位置 (0-100) */
|
|
23
|
+
end?: number;
|
|
24
|
+
/** 最小跨度 (0-100) */
|
|
25
|
+
minSpan?: number;
|
|
26
|
+
/** 最大跨度 (0-100) */
|
|
27
|
+
maxSpan?: number;
|
|
28
|
+
/** 是否锁定缩放 */
|
|
29
|
+
zoomLock?: boolean;
|
|
30
|
+
/** 节流时间 (ms) */
|
|
31
|
+
throttle?: number;
|
|
32
|
+
/** 是否禁用 */
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
/** 是否显示缩放痕迹 */
|
|
35
|
+
brushSelect?: boolean;
|
|
36
|
+
/** 缩放模式: 'scale' | 'mix' */
|
|
37
|
+
zoomMode?: 'scale' | 'mix';
|
|
38
|
+
/** 是否在组件卸载时重置缩放 */
|
|
39
|
+
resetOnUnmount?: boolean;
|
|
40
|
+
/** 缩放变化回调 */
|
|
41
|
+
onZoomChange?: (range: { start: number; end: number }) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 缩放范围 */
|
|
45
|
+
export interface ZoomRange {
|
|
46
|
+
start: number;
|
|
47
|
+
end: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** dataZoom 返回值 */
|
|
51
|
+
export interface UseDataZoomReturn {
|
|
52
|
+
/** 绑定 dataZoom 到图表实例 */
|
|
53
|
+
bindDataZoom: (chartInstance: ChartInstance) => void;
|
|
54
|
+
/** 设置缩放范围 */
|
|
55
|
+
setZoomRange: (start: number, end: number) => void;
|
|
56
|
+
/** 重置缩放到初始状态 */
|
|
57
|
+
resetZoom: () => void;
|
|
58
|
+
/** 获取当前缩放范围 */
|
|
59
|
+
getZoomRange: () => ZoomRange;
|
|
60
|
+
/** 起始位置的原始值 (Ref) */
|
|
61
|
+
startValue: RefObject<number | string | Date | undefined>;
|
|
62
|
+
/** 结束位置的原始值 (Ref) */
|
|
63
|
+
endValue: RefObject<number | string | Date | undefined>;
|
|
64
|
+
/** 绑定事件处理 */
|
|
65
|
+
bindEvents: (chartInstance: ChartInstance) => void;
|
|
66
|
+
/** 缩放变化回调 */
|
|
67
|
+
onZoomChange?: (range: ZoomRange) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Hook 实现
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 使用图表数据缩放
|
|
76
|
+
* @param options 配置选项
|
|
77
|
+
* @returns dataZoom 操作接口
|
|
78
|
+
*/
|
|
79
|
+
export function useDataZoom(options: UseDataZoomOptions = {}): UseDataZoomReturn {
|
|
80
|
+
const {
|
|
81
|
+
type = 'inside',
|
|
82
|
+
start = 0,
|
|
83
|
+
end = 100,
|
|
84
|
+
minSpan,
|
|
85
|
+
maxSpan,
|
|
86
|
+
zoomLock = false,
|
|
87
|
+
throttle = 16,
|
|
88
|
+
disabled = false,
|
|
89
|
+
brushSelect = false,
|
|
90
|
+
zoomMode = 'scale',
|
|
91
|
+
resetOnUnmount = false,
|
|
92
|
+
onZoomChange,
|
|
93
|
+
} = options;
|
|
94
|
+
|
|
95
|
+
// Refs
|
|
96
|
+
const chartRef = useRef<ChartInstance | null>(null);
|
|
97
|
+
const startValueRef = useRef<number | string | Date | undefined>(undefined);
|
|
98
|
+
const endValueRef = useRef<number | string | Date | undefined>(undefined);
|
|
99
|
+
const isBindingRef = useRef(false);
|
|
100
|
+
const throttledHandlerRef = useRef<((...args: any[]) => void) | null>(null);
|
|
101
|
+
|
|
102
|
+
// 节流处理
|
|
103
|
+
const throttledCallback = useCallback(
|
|
104
|
+
<T extends (...args: any[]) => void>(fn: T): T => {
|
|
105
|
+
let lastCall = 0;
|
|
106
|
+
return ((...args: any[]) => {
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
if (now - lastCall >= throttle) {
|
|
109
|
+
lastCall = now;
|
|
110
|
+
fn(...args);
|
|
111
|
+
}
|
|
112
|
+
}) as T;
|
|
113
|
+
},
|
|
114
|
+
[throttle]
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// 构建 dataZoom 配置
|
|
118
|
+
const buildDataZoomConfig = useCallback(() => {
|
|
119
|
+
const config: any[] = [];
|
|
120
|
+
|
|
121
|
+
// 内置型 dataZoom
|
|
122
|
+
if (type === 'inside' || zoomMode === 'mix') {
|
|
123
|
+
config.push({
|
|
124
|
+
type: 'inside',
|
|
125
|
+
start,
|
|
126
|
+
end,
|
|
127
|
+
zoomLock,
|
|
128
|
+
disabled,
|
|
129
|
+
zoomOnMouseWheel: true,
|
|
130
|
+
moveOnMouseMove: false,
|
|
131
|
+
moveOnMouseWheel: false,
|
|
132
|
+
preventDefaultMouseMove: true,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 滑块型 dataZoom
|
|
137
|
+
if (type === 'slider' || zoomMode === 'mix') {
|
|
138
|
+
config.push({
|
|
139
|
+
type: 'slider',
|
|
140
|
+
start,
|
|
141
|
+
end,
|
|
142
|
+
minSpan,
|
|
143
|
+
maxSpan,
|
|
144
|
+
zoomLock,
|
|
145
|
+
disabled,
|
|
146
|
+
show: true,
|
|
147
|
+
brushSelect,
|
|
148
|
+
throttle,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return config;
|
|
153
|
+
}, [type, start, end, minSpan, maxSpan, zoomLock, disabled, brushSelect, throttle]);
|
|
154
|
+
|
|
155
|
+
// 绑定 dataZoom 到图表
|
|
156
|
+
const bindDataZoom = useCallback(
|
|
157
|
+
(chartInstance: ChartInstance) => {
|
|
158
|
+
if (!chartInstance || isBindingRef.current) return;
|
|
159
|
+
|
|
160
|
+
isBindingRef.current = true;
|
|
161
|
+
chartRef.current = chartInstance;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// 设置 dataZoom 配置
|
|
165
|
+
const dataZoomConfig = buildDataZoomConfig();
|
|
166
|
+
chartInstance.setOption(
|
|
167
|
+
{
|
|
168
|
+
dataZoom: dataZoomConfig,
|
|
169
|
+
},
|
|
170
|
+
false,
|
|
171
|
+
true
|
|
172
|
+
);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.warn('[useDataZoom] Failed to bind dataZoom:', e);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
isBindingRef.current = false;
|
|
178
|
+
},
|
|
179
|
+
[buildDataZoomConfig]
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// 解绑 dataZoom
|
|
183
|
+
const unbindDataZoom = useCallback(
|
|
184
|
+
(chartInstance: ChartInstance) => {
|
|
185
|
+
if (!chartInstance) return;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// 通过 dispatchAction 关闭 dataZoom
|
|
189
|
+
chartInstance.dispatchAction?.({
|
|
190
|
+
type: 'dataZoom',
|
|
191
|
+
start: 0,
|
|
192
|
+
end: 100,
|
|
193
|
+
});
|
|
194
|
+
} catch (e) {
|
|
195
|
+
console.warn('[useDataZoom] Failed to unbind dataZoom:', e);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
[]
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// 设置缩放范围
|
|
202
|
+
const setZoomRange = useCallback((newStart: number, newEnd: number) => {
|
|
203
|
+
const chart = chartRef.current;
|
|
204
|
+
if (!chart) return;
|
|
205
|
+
|
|
206
|
+
const clampedStart = Math.max(0, Math.min(100, newStart));
|
|
207
|
+
const clampedEnd = Math.max(0, Math.min(100, newEnd));
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
chart.dispatchAction?.({
|
|
211
|
+
type: 'dataZoom',
|
|
212
|
+
start: clampedStart,
|
|
213
|
+
end: clampedEnd,
|
|
214
|
+
});
|
|
215
|
+
} catch (e) {
|
|
216
|
+
console.warn('[useDataZoom] Failed to set zoom range:', e);
|
|
217
|
+
}
|
|
218
|
+
}, []);
|
|
219
|
+
|
|
220
|
+
// 重置缩放
|
|
221
|
+
const resetZoom = useCallback(() => {
|
|
222
|
+
setZoomRange(start, end);
|
|
223
|
+
}, [setZoomRange, start, end]);
|
|
224
|
+
|
|
225
|
+
// 获取当前缩放范围
|
|
226
|
+
const getZoomRange = useCallback((): ZoomRange => {
|
|
227
|
+
const chart = chartRef.current;
|
|
228
|
+
if (!chart) {
|
|
229
|
+
return { start, end };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const option = chart.getOption?.();
|
|
234
|
+
const dataZoom = option?.dataZoom as any[];
|
|
235
|
+
if (Array.isArray(dataZoom) && dataZoom.length > 0) {
|
|
236
|
+
// 优先获取 inside 类型的范围
|
|
237
|
+
const insideZoom = dataZoom.find((dz: any) => dz.type === 'inside');
|
|
238
|
+
const sliderZoom = dataZoom.find((dz: any) => dz.type === 'slider');
|
|
239
|
+
const zoom = insideZoom || sliderZoom || dataZoom[0];
|
|
240
|
+
return {
|
|
241
|
+
start: zoom.start ?? start,
|
|
242
|
+
end: zoom.end ?? end,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
} catch (e) {
|
|
246
|
+
console.warn('[useDataZoom] Failed to get zoom range:', e);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { start, end };
|
|
250
|
+
}, [start, end]);
|
|
251
|
+
|
|
252
|
+
// 绑定事件
|
|
253
|
+
const bindEvents = useCallback(
|
|
254
|
+
(chartInstance: ChartInstance) => {
|
|
255
|
+
if (!chartInstance) return;
|
|
256
|
+
|
|
257
|
+
// 创建缩放变化处理器
|
|
258
|
+
const handleZoomChange = (params: any) => {
|
|
259
|
+
if (disabled) return;
|
|
260
|
+
|
|
261
|
+
const { start: newStart, end: newEnd } = params || {};
|
|
262
|
+
if (newStart !== undefined) startValueRef.current = newStart;
|
|
263
|
+
if (newEnd !== undefined) endValueRef.current = newEnd;
|
|
264
|
+
|
|
265
|
+
onZoomChange?.({ start: newStart ?? start, end: newEnd ?? end });
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// 使用节流包装
|
|
269
|
+
throttledHandlerRef.current = throttledCallback(handleZoomChange);
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
// 监听 dataZoom 事件
|
|
273
|
+
chartInstance.on('datazoom', throttledHandlerRef.current);
|
|
274
|
+
} catch (e) {
|
|
275
|
+
console.warn('[useDataZoom] Failed to bind events:', e);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
[disabled, onZoomChange, throttledCallback]
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// 解绑事件
|
|
282
|
+
const unbindEvents = useCallback(
|
|
283
|
+
(chartInstance: ChartInstance) => {
|
|
284
|
+
if (!chartInstance || !throttledHandlerRef.current) return;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
chartInstance.off('datazoom', throttledHandlerRef.current);
|
|
288
|
+
} catch (e) {
|
|
289
|
+
console.warn('[useDataZoom] Failed to unbind events:', e);
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
[]
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// 清理:组件卸载时
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
return () => {
|
|
298
|
+
const chart = chartRef.current;
|
|
299
|
+
if (chart) {
|
|
300
|
+
if (resetOnUnmount) {
|
|
301
|
+
unbindDataZoom(chart);
|
|
302
|
+
}
|
|
303
|
+
unbindEvents(chart);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}, [resetOnUnmount, unbindDataZoom, unbindEvents]);
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
bindDataZoom,
|
|
310
|
+
setZoomRange,
|
|
311
|
+
resetZoom,
|
|
312
|
+
getZoomRange,
|
|
313
|
+
startValue: startValueRef,
|
|
314
|
+
endValue: endValueRef,
|
|
315
|
+
bindEvents,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ============================================================================
|
|
320
|
+
// 导出
|
|
321
|
+
// ============================================================================
|
|
322
|
+
|
|
323
|
+
export default useDataZoom;
|
|
@@ -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;
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TaroViz - 基于 Taro 和 ECharts 的多端图表组件库
|
|
3
|
-
* @version 1.
|
|
3
|
+
* @version 1.7.0
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// 核心组件
|
|
@@ -59,6 +59,25 @@ export { default as WordCloudChart } from './charts/wordcloud';
|
|
|
59
59
|
export { default as BoxplotChart } from './charts/boxplot';
|
|
60
60
|
export { default as ParallelChart } from './charts/parallel';
|
|
61
61
|
|
|
62
|
+
// v1.7.0 新增组件
|
|
63
|
+
export { DataFilter, type DataFilterProps, type FilterField, type FilterValues } from './components/DataFilter';
|
|
64
|
+
export {
|
|
65
|
+
createDrillDown,
|
|
66
|
+
canDrillDown,
|
|
67
|
+
buildHierarchy,
|
|
68
|
+
createRegionDrillDown,
|
|
69
|
+
createCategoryDrillDown,
|
|
70
|
+
type DrillDownConfig,
|
|
71
|
+
type DrillDownSource,
|
|
72
|
+
type DrillDownReturn,
|
|
73
|
+
type DrillDownEventParams,
|
|
74
|
+
type DrillUpEventParams,
|
|
75
|
+
} from './core/utils/drillDown';
|
|
76
|
+
|
|
77
|
+
// v1.7.0 新增图表组件
|
|
78
|
+
export { default as LiquidChart } from './charts/liquid';
|
|
79
|
+
export { default as TreeChart } from './charts/tree';
|
|
80
|
+
|
|
62
81
|
// 适配器
|
|
63
82
|
export { getAdapter, detectPlatform, getEnv } from './adapters';
|
|
64
83
|
export { default as H5Adapter } from './adapters/h5';
|
|
@@ -152,10 +171,14 @@ export {
|
|
|
152
171
|
useDataTransform,
|
|
153
172
|
useTableTransform,
|
|
154
173
|
useTimeSeriesTransform,
|
|
174
|
+
// v1.7.0 新增 Hooks
|
|
175
|
+
useDataZoom,
|
|
176
|
+
useChartConnect,
|
|
177
|
+
useChartDownload,
|
|
155
178
|
} from './hooks';
|
|
156
179
|
|
|
157
180
|
/**
|
|
158
181
|
* 库信息
|
|
159
182
|
*/
|
|
160
183
|
export const name = 'taroviz';
|
|
161
|
-
export const version = '1.
|
|
184
|
+
export const version = '1.7.0';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getTheme, registerTheme, defaultTheme, darkTheme } from '../index';
|
|
1
|
+
import { getTheme, registerTheme, defaultTheme, darkTheme, getThemeByName } from '../index';
|
|
2
2
|
|
|
3
3
|
describe('Theme System', () => {
|
|
4
4
|
describe('getTheme', () => {
|
|
@@ -11,7 +11,7 @@ describe('Theme System', () => {
|
|
|
11
11
|
const result = getTheme({ darkMode: true });
|
|
12
12
|
expect(result.darkMode).toBe(true);
|
|
13
13
|
expect(result.theme).toBe('dark');
|
|
14
|
-
expect(result.backgroundColor).toBe('#
|
|
14
|
+
expect(result.backgroundColor).toBe('#1a1a2e');
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it('should merge custom options with defaultTheme', () => {
|
|
@@ -50,16 +50,10 @@ describe('Theme System', () => {
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
describe('registerTheme', () => {
|
|
53
|
-
it('should
|
|
54
|
-
// Mock console.log
|
|
55
|
-
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
56
|
-
|
|
53
|
+
it('should register a theme', () => {
|
|
57
54
|
registerTheme('custom-theme', { backgroundColor: '#123' });
|
|
58
|
-
|
|
59
|
-
expect(
|
|
60
|
-
|
|
61
|
-
// Restore console.log
|
|
62
|
-
consoleSpy.mockRestore();
|
|
55
|
+
const result = getThemeByName('custom-theme');
|
|
56
|
+
expect(result?.backgroundColor).toBe('#123');
|
|
63
57
|
});
|
|
64
58
|
});
|
|
65
59
|
|
|
@@ -70,8 +64,8 @@ describe('Theme System', () => {
|
|
|
70
64
|
expect(defaultTheme).toHaveProperty('colors');
|
|
71
65
|
expect(Array.isArray(defaultTheme.colors)).toBe(true);
|
|
72
66
|
expect(defaultTheme.colors).toHaveLength(9);
|
|
73
|
-
expect(defaultTheme).toHaveProperty('backgroundColor', '
|
|
74
|
-
expect(defaultTheme).toHaveProperty('textColor', '#
|
|
67
|
+
expect(defaultTheme).toHaveProperty('backgroundColor', '#ffffff');
|
|
68
|
+
expect(defaultTheme).toHaveProperty('textColor', '#333333');
|
|
75
69
|
expect(defaultTheme).toHaveProperty('fontFamily');
|
|
76
70
|
});
|
|
77
71
|
});
|