@agions/taroviz 1.7.0 → 1.10.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.
Files changed (34) hide show
  1. package/README.md +7 -3
  2. package/dist/cjs/index.js +1 -1
  3. package/dist/esm/index.js +960 -136
  4. package/package.json +1 -1
  5. package/src/adapters/__tests__/index.test.ts +4 -2
  6. package/src/adapters/h5/index.ts +16 -0
  7. package/src/adapters/types.ts +28 -120
  8. package/src/charts/boxplot/types.ts +5 -3
  9. package/src/charts/common/BaseChartWrapper.tsx +193 -32
  10. package/src/charts/liquid/index.tsx +6 -5
  11. package/src/charts/liquid/types.ts +4 -4
  12. package/src/charts/parallel/types.ts +6 -3
  13. package/src/charts/tree/types.ts +4 -4
  14. package/src/charts/types.ts +1 -1
  15. package/src/core/animation/AnimationManager.ts +69 -42
  16. package/src/core/components/Annotation.tsx +12 -10
  17. package/src/core/components/BaseChart.tsx +75 -12
  18. package/src/core/components/ErrorBoundary.tsx +30 -17
  19. package/src/core/components/LazyChart.tsx +14 -9
  20. package/src/core/themes/ThemeManager.ts +33 -0
  21. package/src/core/types/common.ts +21 -110
  22. package/src/core/types/index.ts +4 -135
  23. package/src/core/types/platform.ts +38 -230
  24. package/src/core/utils/chartUtils.ts +8 -3
  25. package/src/core/utils/export/ExportUtils.ts +10 -1
  26. package/src/core/utils/performance/PerformanceAnalyzer.ts +21 -1
  27. package/src/core/utils/performance/types.ts +5 -0
  28. package/src/hooks/__tests__/index.test.tsx +7 -5
  29. package/src/hooks/index.ts +23 -1
  30. package/src/hooks/useAnimation.ts +427 -0
  31. package/src/hooks/useChartHistory.ts +273 -0
  32. package/src/hooks/useChartSelection.ts +350 -0
  33. package/src/hooks/usePerformance.ts +291 -0
  34. package/src/themes/__tests__/index.test.ts +7 -13
@@ -0,0 +1,350 @@
1
+ /**
2
+ * useChartSelection - 图表数据点选择/高亮 Hook
3
+ * 支持单个/批量选择、反选、清除选择,配合 ECharts select 事件
4
+ *
5
+ * 特性:
6
+ * - 支持按 seriesIndex + dataIndex 选择
7
+ * - 支持按 dataIndex 跨系列批量选择
8
+ * - 支持反选(invertSelection)
9
+ * - 支持多选模式(multi)
10
+ * - 自动绑定图表 select/unselect 事件
11
+ */
12
+ import { useEffect, useRef, useCallback, useState } from 'react';
13
+ import type { ChartInstance } from './index';
14
+
15
+ // ============================================================================
16
+ // 类型定义
17
+ // ============================================================================
18
+
19
+ /** 单个数据点的选择键 */
20
+ export interface DataPointKey {
21
+ seriesIndex: number;
22
+ dataIndex: number;
23
+ }
24
+
25
+ /** 选择模式 */
26
+ export type SelectionMode = 'single' | 'multiple';
27
+
28
+ /** 选择事件参数 */
29
+ export interface SelectionEvent {
30
+ /** 被选中的数据点 */
31
+ selected: DataPointKey[];
32
+ /** 取消选择的数据点 */
33
+ unselected: DataPointKey[];
34
+ }
35
+
36
+ /** 选择配置选项 */
37
+ export interface UseChartSelectionOptions {
38
+ /** 选择模式:'single' 单选,'multiple' 多选,默认 'multiple' */
39
+ mode?: SelectionMode;
40
+ /** 是否在组件卸载时清除所有选择,默认 true */
41
+ clearOnUnmount?: boolean;
42
+ /** 是否启用 Ctrl+Click 多选,默认 true */
43
+ enableCtrlMultiSelect?: boolean;
44
+ /** 是否启用 Shift+Click 范围选择,默认 true */
45
+ enableShiftRangeSelect?: boolean;
46
+ /** 选择变化时的回调 */
47
+ onSelectionChange?: (event: SelectionEvent) => void;
48
+ }
49
+
50
+ /** 选择返回值 */
51
+ export interface UseChartSelectionReturn {
52
+ /** 当前选中的数据点 */
53
+ selectedPoints: DataPointKey[];
54
+ /** 是否存在选中 */
55
+ hasSelection: boolean;
56
+ /** 选中数量 */
57
+ selectionCount: number;
58
+ /** 选中指定数据点 */
59
+ select: (key: DataPointKey) => void;
60
+ /** 取消选中指定数据点 */
61
+ deselect: (key: DataPointKey) => void;
62
+ /** 批量选中 */
63
+ selectMultiple: (keys: DataPointKey[]) => void;
64
+ /** 批量取消选中 */
65
+ deselectMultiple: (keys: DataPointKey[]) => void;
66
+ /** 切换选中状态 */
67
+ toggle: (key: DataPointKey) => void;
68
+ /** 反选 */
69
+ invertSelection: (seriesIndex: number, dataIndices: number[]) => void;
70
+ /** 全选指定系列 */
71
+ selectAll: (seriesIndex: number, dataIndices: number[]) => void;
72
+ /** 清除所有选择 */
73
+ clearSelection: () => void;
74
+ /** 判断某点是否被选中 */
75
+ isSelected: (key: DataPointKey) => boolean;
76
+ }
77
+
78
+ // ============================================================================
79
+ // 工具函数
80
+ // ============================================================================
81
+
82
+ /** 生成唯一键字符串 */
83
+ function keyToString(key: DataPointKey): string {
84
+ return `${key.seriesIndex}:${key.dataIndex}`;
85
+ }
86
+
87
+ function stringToKey(str: string): DataPointKey {
88
+ const [seriesIndex, dataIndex] = str.split(':').map(Number);
89
+ return { seriesIndex, dataIndex };
90
+ }
91
+
92
+ // ============================================================================
93
+ // Hook 实现
94
+ // ============================================================================
95
+
96
+ /**
97
+ * 使用图表数据点选择功能
98
+ * @param chartInstance 图表实例
99
+ * @param options 配置选项
100
+ * @returns 选择控制接口
101
+ */
102
+ export function useChartSelection(
103
+ chartInstance: ChartInstance | null,
104
+ options: UseChartSelectionOptions = {}
105
+ ): UseChartSelectionReturn {
106
+ const {
107
+ mode = 'multiple',
108
+ clearOnUnmount = true,
109
+ enableCtrlMultiSelect = true,
110
+ enableShiftRangeSelect = true,
111
+ onSelectionChange,
112
+ } = options;
113
+
114
+ // 选中点集合(字符串键)
115
+ const [selectedPoints, setSelectedPoints] = useState<DataPointKey[]>([]);
116
+
117
+ // Chart instance ref
118
+ const chartRef = useRef<ChartInstance | null>(null);
119
+ chartRef.current = chartInstance;
120
+
121
+ // 上一次 shift+click 的数据索引(用于范围选择)
122
+ const lastShiftIndexRef = useRef<number | null>(null);
123
+
124
+ // 当前模式 ref(用于事件处理)
125
+ const modeRef = useRef(mode);
126
+ modeRef.current = mode;
127
+
128
+ // 绑定图表 select/unselect 事件
129
+ useEffect(() => {
130
+ const chart = chartRef.current;
131
+ if (!chart || !chart.on) return;
132
+
133
+ const handleSelect = (params: { selected: Record<string, boolean>; type: string }) => {
134
+ // ECharts 内置 select 会同步更新 legend
135
+ // 这里我们用 dispatchAction 来实现纯数据点选择
136
+ };
137
+
138
+ // 单击 legend 时清除数据点选择(保持一致性)
139
+ const handleLegendSelectChanged = (params: { name?: string; selected?: Record<string, boolean> }) => {
140
+ // 清除选择时的视觉反馈
141
+ };
142
+
143
+ chart.on('selectchanged', (params: unknown) => {
144
+ // 当图表内部选择变化时同步状态
145
+ const p = params as {
146
+ isFromClick?: boolean;
147
+ selected?: Record<string, boolean>;
148
+ notSelected?: Record<string, boolean>;
149
+ };
150
+ if (p.isFromClick) {
151
+ // 用户点击了图例,清除所有数据点选择
152
+ setSelectedPoints([]);
153
+ onSelectionChange?.({ selected: [], unselected: [] });
154
+ }
155
+ });
156
+
157
+ return () => {
158
+ if (chart.off) {
159
+ chart.off('selectchanged');
160
+ }
161
+ };
162
+ }, [onSelectionChange]);
163
+
164
+ // 组件卸载时清除选择
165
+ useEffect(() => {
166
+ return () => {
167
+ if (clearOnUnmount) {
168
+ const chart = chartRef.current;
169
+ if (chart?.dispatchAction) {
170
+ // 清除所有系列的选择状态
171
+ chart.dispatchAction({ type: 'unselect' });
172
+ }
173
+ }
174
+ };
175
+ }, [clearOnUnmount]);
176
+
177
+ // ─── 私有方法 ───────────────────────────────────────────────────────────────
178
+
179
+ /** 触发选择变化回调 */
180
+ const notifyChange = useCallback(
181
+ (selected: DataPointKey[], unselected: DataPointKey[]) => {
182
+ onSelectionChange?.({ selected, unselected });
183
+ },
184
+ [onSelectionChange]
185
+ );
186
+
187
+ /** 执行 ECharts dispatchAction */
188
+ const dispatchSelect = useCallback(
189
+ (key: DataPointKey, select: boolean) => {
190
+ const chart = chartRef.current;
191
+ if (!chart?.dispatchAction) return;
192
+ chart.dispatchAction({
193
+ type: select ? 'select' : 'unselect',
194
+ seriesIndex: key.seriesIndex,
195
+ dataIndex: key.dataIndex,
196
+ });
197
+ },
198
+ []
199
+ );
200
+
201
+ // ─── Public API ───────────────────────────────────────────────────────────
202
+
203
+ const select = useCallback(
204
+ (key: DataPointKey) => {
205
+ setSelectedPoints((prev) => {
206
+ const str = keyToString(key);
207
+ if (prev.some((p) => keyToString(p) === str)) return prev;
208
+ const next = mode === 'single' ? [key] : [...prev, key];
209
+ notifyChange(next, []);
210
+ return next;
211
+ });
212
+ dispatchSelect(key, true);
213
+ },
214
+ [mode, notifyChange, dispatchSelect]
215
+ );
216
+
217
+ const deselect = useCallback(
218
+ (key: DataPointKey) => {
219
+ setSelectedPoints((prev) => {
220
+ const str = keyToString(key);
221
+ const removed = prev.filter((p) => keyToString(p) !== str);
222
+ notifyChange([], removed);
223
+ return removed;
224
+ });
225
+ dispatchSelect(key, false);
226
+ },
227
+ [notifyChange, dispatchSelect]
228
+ );
229
+
230
+ const toggle = useCallback(
231
+ (key: DataPointKey) => {
232
+ const str = keyToString(key);
233
+ if (selectedPoints.some((p) => keyToString(p) === str)) {
234
+ deselect(key);
235
+ } else {
236
+ select(key);
237
+ }
238
+ },
239
+ [selectedPoints, select, deselect]
240
+ );
241
+
242
+ const selectMultiple = useCallback(
243
+ (keys: DataPointKey[]) => {
244
+ setSelectedPoints((prev) => {
245
+ const newPoints = mode === 'single' ? keys : [...prev, ...keys.filter(
246
+ (k) => !prev.some((p) => keyToString(p) === keyToString(k))
247
+ )];
248
+ notifyChange(newPoints, []);
249
+ return newPoints;
250
+ });
251
+ keys.forEach((key) => dispatchSelect(key, true));
252
+ },
253
+ [mode, notifyChange, dispatchSelect]
254
+ );
255
+
256
+ const deselectMultiple = useCallback(
257
+ (keys: DataPointKey[]) => {
258
+ setSelectedPoints((prev) => {
259
+ const keySet = new Set(keys.map(keyToString));
260
+ const removed = prev.filter((p) => keySet.has(keyToString(p)));
261
+ const remaining = prev.filter((p) => !keySet.has(keyToString(p)));
262
+ notifyChange([], removed);
263
+ return remaining;
264
+ });
265
+ keys.forEach((key) => dispatchSelect(key, false));
266
+ },
267
+ [notifyChange, dispatchSelect]
268
+ );
269
+
270
+ const invertSelection = useCallback(
271
+ (seriesIndex: number, dataIndices: number[]) => {
272
+ setSelectedPoints((prev) => {
273
+ const selectedSet = new Set(prev.filter((p) => p.seriesIndex === seriesIndex).map((p) => p.dataIndex));
274
+ const toSelect: DataPointKey[] = [];
275
+ const toDeselect: DataPointKey[] = [];
276
+
277
+ dataIndices.forEach((dataIndex) => {
278
+ const key = { seriesIndex, dataIndex };
279
+ const str = keyToString(key);
280
+ if (selectedSet.has(dataIndex)) {
281
+ toDeselect.push(key);
282
+ } else {
283
+ toSelect.push(key);
284
+ }
285
+ });
286
+
287
+ toDeselect.forEach((k) => dispatchSelect(k, false));
288
+ toSelect.forEach((k) => dispatchSelect(k, true));
289
+
290
+ const newSelected = prev.filter(
291
+ (p) => !(p.seriesIndex === seriesIndex && selectedSet.has(p.dataIndex))
292
+ ).concat(toSelect);
293
+
294
+ notifyChange(toSelect, toDeselect);
295
+ return newSelected;
296
+ });
297
+ },
298
+ [notifyChange, dispatchSelect]
299
+ );
300
+
301
+ const selectAll = useCallback(
302
+ (seriesIndex: number, dataIndices: number[]) => {
303
+ const keys = dataIndices.map((dataIndex) => ({ seriesIndex, dataIndex }));
304
+ setSelectedPoints((prev) => {
305
+ const newPoints = mode === 'single' ? keys : [...prev, ...keys.filter(
306
+ (k) => !prev.some((p) => keyToString(p) === keyToString(k))
307
+ )];
308
+ notifyChange(newPoints, []);
309
+ return newPoints;
310
+ });
311
+ keys.forEach((key) => dispatchSelect(key, true));
312
+ },
313
+ [mode, notifyChange, dispatchSelect]
314
+ );
315
+
316
+ const clearSelection = useCallback(() => {
317
+ const chart = chartRef.current;
318
+ if (chart?.dispatchAction) {
319
+ chart.dispatchAction({ type: 'unselect' });
320
+ }
321
+ const prev = selectedPoints;
322
+ setSelectedPoints([]);
323
+ notifyChange([], prev);
324
+ }, [selectedPoints, notifyChange]);
325
+
326
+ const isSelected = useCallback(
327
+ (key: DataPointKey) => {
328
+ const str = keyToString(key);
329
+ return selectedPoints.some((p) => keyToString(p) === str);
330
+ },
331
+ [selectedPoints]
332
+ );
333
+
334
+ return {
335
+ selectedPoints,
336
+ hasSelection: selectedPoints.length > 0,
337
+ selectionCount: selectedPoints.length,
338
+ select,
339
+ deselect,
340
+ selectMultiple,
341
+ deselectMultiple,
342
+ toggle,
343
+ invertSelection,
344
+ selectAll,
345
+ clearSelection,
346
+ isSelected,
347
+ };
348
+ }
349
+
350
+ export default useChartSelection;
@@ -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;
@@ -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('#0f1117');
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 log when registering a theme', () => {
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(consoleSpy).toHaveBeenCalledWith('Registering theme: custom-theme');
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', 'transparent');
74
- expect(defaultTheme).toHaveProperty('textColor', '#333');
67
+ expect(defaultTheme).toHaveProperty('backgroundColor', '#ffffff');
68
+ expect(defaultTheme).toHaveProperty('textColor', '#333333');
75
69
  expect(defaultTheme).toHaveProperty('fontFamily');
76
70
  });
77
71
  });