@agions/taroviz 1.3.1 → 1.6.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 (64) hide show
  1. package/README.md +4 -4
  2. package/dist/cjs/index.js +1 -1
  3. package/dist/esm/index.js +3814 -2724
  4. package/package.json +1 -1
  5. package/src/__tests__/integration.test.tsx +12 -10
  6. package/src/adapters/BaseAdapter.ts +116 -0
  7. package/src/adapters/__tests__/index.test.ts +10 -10
  8. package/src/adapters/index.ts +63 -74
  9. package/src/adapters/swan/index.ts +26 -223
  10. package/src/adapters/tt/index.ts +28 -225
  11. package/src/adapters/types.ts +36 -0
  12. package/src/adapters/weapp/index.ts +29 -189
  13. package/src/charts/bar/index.tsx +5 -9
  14. package/src/charts/boxplot/__tests__/index.test.tsx +130 -0
  15. package/src/charts/boxplot/index.tsx +18 -0
  16. package/src/charts/boxplot/types.ts +46 -0
  17. package/src/charts/candlestick/__tests__/index.test.tsx +37 -0
  18. package/src/charts/candlestick/index.tsx +13 -0
  19. package/src/charts/common/BaseChartWrapper.tsx +47 -38
  20. package/src/charts/funnel/index.tsx +5 -9
  21. package/src/charts/gauge/index.tsx +5 -9
  22. package/src/charts/graph/__tests__/index.test.tsx +41 -0
  23. package/src/charts/graph/index.tsx +13 -0
  24. package/src/charts/heatmap/index.tsx +5 -9
  25. package/src/charts/index.ts +10 -1
  26. package/src/charts/line/index.tsx +4 -7
  27. package/src/charts/parallel/__tests__/index.test.tsx +164 -0
  28. package/src/charts/parallel/index.tsx +18 -0
  29. package/src/charts/parallel/types.ts +73 -0
  30. package/src/charts/pie/index.tsx +5 -10
  31. package/src/charts/radar/index.tsx +5 -9
  32. package/src/charts/scatter/index.tsx +5 -9
  33. package/src/charts/types.ts +48 -4
  34. package/src/charts/wordcloud/__tests__/index.test.tsx +36 -0
  35. package/src/charts/wordcloud/index.tsx +13 -0
  36. package/src/core/animation/AnimationManager.ts +15 -0
  37. package/src/core/components/Annotation.tsx +26 -21
  38. package/src/core/components/BaseChart.tsx +280 -1105
  39. package/src/core/components/ErrorBoundary.tsx +4 -1
  40. package/src/core/components/LazyChart.tsx +42 -55
  41. package/src/core/components/hooks/index.ts +20 -0
  42. package/src/core/components/hooks/useChartEvents.ts +143 -0
  43. package/src/core/components/hooks/useChartInit.ts +80 -0
  44. package/src/core/components/hooks/usePerformance.ts +186 -0
  45. package/src/core/components/hooks/useVirtualScroll.ts +156 -0
  46. package/src/core/echarts.ts +1 -1
  47. package/src/core/themes/ThemeManager.ts +31 -15
  48. package/src/core/types/index.ts +2 -2
  49. package/src/core/utils/chartInstances.ts +10 -3
  50. package/src/core/utils/chartUtils.ts +46 -0
  51. package/src/core/utils/common.ts +14 -1
  52. package/src/core/utils/export/ExportUtils.ts +13 -22
  53. package/src/core/utils/performance/PerformanceAnalyzer.ts +32 -5
  54. package/src/core/utils/uuid.ts +1 -1
  55. package/src/editor/EnhancedThemeEditor.tsx +624 -0
  56. package/src/editor/ThemeEditor.tsx +1 -6
  57. package/src/hooks/__tests__/index.test.tsx +14 -11
  58. package/src/hooks/__tests__/useDataTransform.test.ts +159 -0
  59. package/src/hooks/index.ts +54 -19
  60. package/src/hooks/useDataTransform.ts +503 -0
  61. package/src/index.ts +27 -9
  62. package/src/main.tsx +4 -4
  63. package/src/themes/__tests__/index.test.ts +2 -2
  64. package/src/themes/index.ts +13 -0
@@ -145,7 +145,10 @@ export function withErrorBoundary<Props extends object>(
145
145
  </ErrorBoundary>
146
146
  );
147
147
 
148
- WrappedChart.displayName = `withErrorBoundary(${ChartComponent.displayName || ChartComponent.name || 'Chart'})`;
148
+ const componentName = ChartComponent.displayName || ChartComponent.name;
149
+ WrappedChart.displayName = componentName
150
+ ? `withErrorBoundary(${componentName})`
151
+ : 'withErrorBoundary(WrappedChart)';
149
152
 
150
153
  return WrappedChart;
151
154
  }
@@ -17,15 +17,22 @@ const LazyTreeMapChart = lazy(() => import('../../charts/treemap'));
17
17
  const LazySunburstChart = lazy(() => import('../../charts/sunburst'));
18
18
  const LazySankeyChart = lazy(() => import('../../charts/sankey'));
19
19
 
20
- /**
21
- * 懒加载图表包装器
22
- */
23
- interface LazyChartWrapperProps {
24
- children?: React.ReactNode;
25
- fallback?: React.ReactNode;
26
- loading?: boolean;
27
- loadingText?: string;
28
- }
20
+ // 统一的图表类型到懒加载组件映射
21
+ const LAZY_CHART_MODULES: Record<string, () => Promise<{ default: ComponentType<any> }>> = {
22
+ line: () => import('../../charts/line'),
23
+ bar: () => import('../../charts/bar'),
24
+ pie: () => import('../../charts/pie'),
25
+ scatter: () => import('../../charts/scatter'),
26
+ radar: () => import('../../charts/radar'),
27
+ heatmap: () => import('../../charts/heatmap'),
28
+ gauge: () => import('../../charts/gauge'),
29
+ funnel: () => import('../../charts/funnel'),
30
+ treemap: () => import('../../charts/treemap'),
31
+ sunburst: () => import('../../charts/sunburst'),
32
+ sankey: () => import('../../charts/sankey'),
33
+ };
34
+
35
+ export const LAZY_CHART_TYPES = Object.keys(LAZY_CHART_MODULES);
29
36
 
30
37
  /**
31
38
  * 默认加载状态组件
@@ -73,22 +80,22 @@ const DefaultLoadingFallback: React.FC<{ text?: string }> = ({ text = '加载中
73
80
  export function withLazyLoad<P extends object>(
74
81
  ChartComponent: ComponentType<P>,
75
82
  loadingFallback?: ComponentType<{ text?: string }>
76
- ): ComponentType<P & { loadingText?: string; fallback?: React.ReactNode }> {
77
- const LazyWrapper: React.FC<P & { loadingText?: string; fallback?: React.ReactNode }> = ({
78
- loadingText,
79
- fallback,
80
- ...props
81
- }) => {
83
+ ): ComponentType<
84
+ Omit<P, 'loadingText' | 'fallback'> & { loadingText?: string; fallback?: React.ReactNode }
85
+ > {
86
+ const LazyWrapper: React.FC<
87
+ Omit<P, 'loadingText' | 'fallback'> & { loadingText?: string; fallback?: React.ReactNode }
88
+ > = ({ loadingText, fallback, ...props }) => {
82
89
  const LoadingComponent = loadingFallback || DefaultLoadingFallback;
83
90
  return (
84
91
  <Suspense fallback={<LoadingComponent text={loadingText} />}>
85
92
  {fallback ? (
86
93
  <React.Fragment>
87
94
  {fallback}
88
- <ChartComponent {...props} />
95
+ <ChartComponent {...(props as P)} />
89
96
  </React.Fragment>
90
97
  ) : (
91
- <ChartComponent {...props} />
98
+ <ChartComponent {...(props as P)} />
92
99
  )}
93
100
  </Suspense>
94
101
  );
@@ -102,47 +109,27 @@ export function withLazyLoad<P extends object>(
102
109
  /**
103
110
  * 预加载图表组件
104
111
  * 在需要显示图表之前预先加载
112
+ * @param silent - 如果为 true,错误不会被打印到控制台(保持旧行为兼容)
113
+ * @returns Promise that resolves when loaded, rejects on error
105
114
  */
106
- export function preloadChart(chartType: string): void {
107
- const chartModules: Record<string, () => Promise<{ default: ComponentType<any> }>> = {
108
- line: () => import('../../charts/line'),
109
- bar: () => import('../../charts/bar'),
110
- pie: () => import('../../charts/pie'),
111
- scatter: () => import('../../charts/scatter'),
112
- radar: () => import('../../charts/radar'),
113
- heatmap: () => import('../../charts/heatmap'),
114
- gauge: () => import('../../charts/gauge'),
115
- funnel: () => import('../../charts/funnel'),
116
- treemap: () => import('../../charts/treemap'),
117
- sunburst: () => import('../../charts/sunburst'),
118
- sankey: () => import('../../charts/sankey'),
119
- };
120
-
121
- const loader = chartModules[chartType];
122
- if (loader) {
123
- loader().catch(console.error);
115
+ export function preloadChart(chartType: string, silent = true): Promise<void> {
116
+ const loader = LAZY_CHART_MODULES[chartType];
117
+ if (!loader) {
118
+ if (silent) return Promise.resolve();
119
+ return Promise.reject(new Error(`Unknown chart type: ${chartType}`));
124
120
  }
121
+ return loader()
122
+ .then(() => undefined)
123
+ .catch((e) => {
124
+ if (!silent) console.error('[TaroViz] Failed to preload chart:', chartType, e);
125
+ });
125
126
  }
126
127
 
127
128
  /**
128
129
  * 预加载所有图表组件
129
130
  */
130
- export function preloadAllCharts(): void {
131
- const chartTypes = [
132
- 'line',
133
- 'bar',
134
- 'pie',
135
- 'scatter',
136
- 'radar',
137
- 'heatmap',
138
- 'gauge',
139
- 'funnel',
140
- 'treemap',
141
- 'sunburst',
142
- 'sankey',
143
- ];
144
-
145
- chartTypes.forEach((type) => preloadChart(type));
131
+ export function preloadAllCharts(): Promise<void[]> {
132
+ return Promise.all(LAZY_CHART_TYPES.map((type) => preloadChart(type)));
146
133
  }
147
134
 
148
135
  /**
@@ -176,12 +163,12 @@ export const LazyChartRegistry = {
176
163
  return createLazyChart(chartType);
177
164
  },
178
165
 
179
- preload(chartType: string): void {
180
- preloadChart(chartType);
166
+ preload(chartType: string, silent = true): Promise<void> {
167
+ return preloadChart(chartType, silent);
181
168
  },
182
169
 
183
- preloadAll(): void {
184
- preloadAllCharts();
170
+ preloadAll(): Promise<void[]> {
171
+ return preloadAllCharts();
185
172
  },
186
173
  };
187
174
 
@@ -0,0 +1,20 @@
1
+ /**
2
+ * TaroViz 图表组件 Hooks
3
+ * 提供图表初始化、事件处理、虚拟滚动、性能监控等能力
4
+ */
5
+
6
+ export { useChartInit } from './useChartInit';
7
+ export type { UseChartInitOptions, UseChartInitResult } from './useChartInit';
8
+
9
+ export { useChartEvents } from './useChartEvents';
10
+ export type {
11
+ ChartEventHandlers,
12
+ ChartLinkageConfig,
13
+ UseChartEventsOptions,
14
+ } from './useChartEvents';
15
+
16
+ export { useVirtualScroll } from './useVirtualScroll';
17
+ export type { UseVirtualScrollOptions, VirtualScrollState } from './useVirtualScroll';
18
+
19
+ export { usePerformance } from './usePerformance';
20
+ export type { PerformanceData, UsePerformanceOptions } from './usePerformance';
@@ -0,0 +1,143 @@
1
+ /**
2
+ * 图表事件 Hook
3
+ * 负责图表事件的绑定、解绑和联动
4
+ */
5
+ import { useEffect, useRef } from 'react';
6
+ import type { Adapter } from '../../../adapters/types';
7
+ import { getChart } from '../../utils/chartInstances';
8
+
9
+ export interface ChartEventHandlers {
10
+ onClick?: (params: unknown) => void;
11
+ onDataZoom?: (params: unknown) => void;
12
+ onZoom?: (data: { start: number; end: number; dataZoomIndex: number }) => void;
13
+ onLegendSelect?: (params: { name: string; selected: Record<string, boolean> }) => void;
14
+ onLegendUnselect?: (params: { name: string; selected: Record<string, boolean> }) => void;
15
+ onTooltipShow?: (params: unknown) => void;
16
+ onTooltipHide?: (params: unknown) => void;
17
+ }
18
+
19
+ export interface ChartLinkageConfig {
20
+ linkedChartIds?: string[];
21
+ enableClickLinkage?: boolean;
22
+ enableZoomLinkage?: boolean;
23
+ enableLegendLinkage?: boolean;
24
+ }
25
+
26
+ export interface UseChartEventsOptions extends ChartEventHandlers {
27
+ chartId?: string;
28
+ linkageConfig?: ChartLinkageConfig;
29
+ }
30
+
31
+ export function useChartEvents(
32
+ adapterRef: React.MutableRefObject<Adapter | null>,
33
+ options: UseChartEventsOptions
34
+ ) {
35
+ const handlersRef = useRef(options);
36
+ handlersRef.current = options;
37
+
38
+ useEffect(() => {
39
+ const adapter = adapterRef.current;
40
+ if (!adapter) return;
41
+
42
+ const instance = adapter.getInstance();
43
+ if (!instance) return;
44
+
45
+ const { chartId, linkageConfig = {} } = handlersRef.current;
46
+
47
+ // Click 事件
48
+ if (handlersRef.current.onClick) {
49
+ instance.on('click', handlersRef.current.onClick);
50
+
51
+ // 点击联动
52
+ if (linkageConfig.enableClickLinkage && chartId && linkageConfig.linkedChartIds) {
53
+ linkageConfig.linkedChartIds.forEach((linkedChartId) => {
54
+ const linkedChart = getChart(linkedChartId);
55
+ if (linkedChart) {
56
+ linkedChart.dispatchAction({ type: 'highlight', name: '' });
57
+ }
58
+ });
59
+ }
60
+ }
61
+
62
+ // DataZoom 事件
63
+ if (handlersRef.current.onDataZoom || handlersRef.current.onZoom) {
64
+ instance.on('datazoom', (params: unknown) => {
65
+ handlersRef.current.onDataZoom?.(params);
66
+
67
+ if (handlersRef.current.onZoom) {
68
+ const p = params as { start?: number; end?: number; dataZoomIndex?: number };
69
+ handlersRef.current.onZoom({
70
+ start: p.start || 0,
71
+ end: p.end || 100,
72
+ dataZoomIndex: p.dataZoomIndex || 0,
73
+ });
74
+ }
75
+
76
+ // 缩放联动
77
+ if (linkageConfig.enableZoomLinkage && chartId && linkageConfig.linkedChartIds) {
78
+ linkageConfig.linkedChartIds.forEach((linkedChartId) => {
79
+ const linkedChart = getChart(linkedChartId);
80
+ if (linkedChart) {
81
+ const p = params as { start?: number; end?: number; dataZoomIndex?: number };
82
+ linkedChart.dispatchAction({
83
+ type: 'dataZoom',
84
+ start: p.start,
85
+ end: p.end,
86
+ dataZoomIndex: p.dataZoomIndex,
87
+ });
88
+ }
89
+ });
90
+ }
91
+ });
92
+ }
93
+
94
+ // Legend 事件
95
+ if (handlersRef.current.onLegendSelect || handlersRef.current.onLegendUnselect) {
96
+ instance.on('legendselectchanged', (params: unknown) => {
97
+ const p = params as { name: string; selected: Record<string, boolean> };
98
+
99
+ // Legend 联动
100
+ if (linkageConfig.enableLegendLinkage && chartId && linkageConfig.linkedChartIds) {
101
+ linkageConfig.linkedChartIds.forEach((linkedChartId) => {
102
+ const linkedChart = getChart(linkedChartId);
103
+ if (linkedChart) {
104
+ linkedChart.setOption({ legend: { selected: p.selected } });
105
+ }
106
+ });
107
+ }
108
+
109
+ if (p.selected[p.name]) {
110
+ handlersRef.current.onLegendSelect?.(p);
111
+ } else {
112
+ handlersRef.current.onLegendUnselect?.(p);
113
+ }
114
+ });
115
+ }
116
+
117
+ // Tooltip 事件
118
+ if (handlersRef.current.onTooltipShow) {
119
+ instance.on('tooltipshow', handlersRef.current.onTooltipShow);
120
+ }
121
+ if (handlersRef.current.onTooltipHide) {
122
+ instance.on('tooltiphide', handlersRef.current.onTooltipHide);
123
+ }
124
+
125
+ return () => {
126
+ if (handlersRef.current.onClick) {
127
+ instance.off('click', handlersRef.current.onClick);
128
+ }
129
+ if (handlersRef.current.onDataZoom || handlersRef.current.onZoom) {
130
+ instance.off('datazoom');
131
+ }
132
+ if (handlersRef.current.onLegendSelect || handlersRef.current.onLegendUnselect) {
133
+ instance.off('legendselectchanged');
134
+ }
135
+ if (handlersRef.current.onTooltipShow) {
136
+ instance.off('tooltipshow');
137
+ }
138
+ if (handlersRef.current.onTooltipHide) {
139
+ instance.off('tooltiphide');
140
+ }
141
+ };
142
+ }, [adapterRef]);
143
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * 图表初始化 Hook
3
+ * 负责图表的创建、适配器获取和生命周期管理
4
+ */
5
+ import { useEffect, useRef } from 'react';
6
+ import { getAdapter } from '../../../adapters';
7
+ import type { Adapter } from '../../../adapters/types';
8
+ import type { EChartsOption } from 'echarts';
9
+
10
+ export interface UseChartInitOptions {
11
+ width?: number | string;
12
+ height?: number | string;
13
+ theme?: string | object;
14
+ option?: EChartsOption;
15
+ onInit?: (instance: unknown) => void;
16
+ direction?: 'ltr' | 'rtl';
17
+ }
18
+
19
+ export interface UseChartInitResult {
20
+ adapterRef: React.MutableRefObject<Adapter | null>;
21
+ chartRef: React.RefObject<HTMLDivElement | null>;
22
+ isReady: boolean;
23
+ }
24
+
25
+ export function useChartInit(
26
+ containerRef: React.RefObject<HTMLDivElement | null>,
27
+ options: UseChartInitOptions
28
+ ): UseChartInitResult {
29
+ const adapterRef = useRef<Adapter | null>(null);
30
+ const isReadyRef = useRef(false);
31
+
32
+ useEffect(() => {
33
+ if (!containerRef.current) return;
34
+
35
+ let mounted = true;
36
+ let adapter: Adapter | null = null;
37
+
38
+ const initChart = async () => {
39
+ try {
40
+ adapter = await getAdapter({
41
+ width: options.width,
42
+ height: options.height,
43
+ theme: options.theme,
44
+ option: options.option,
45
+ onInit: options.onInit,
46
+ containerRef,
47
+ direction: options.direction,
48
+ });
49
+
50
+ if (!mounted) return;
51
+ if (!adapter) return;
52
+
53
+ adapterRef.current = adapter;
54
+ isReadyRef.current = true;
55
+ adapter.init();
56
+ } catch (error) {
57
+ console.error('[TaroViz] Failed to initialize chart:', error);
58
+ }
59
+ };
60
+
61
+ initChart();
62
+
63
+ return () => {
64
+ mounted = false;
65
+ if (adapter) {
66
+ adapter.dispose();
67
+ adapterRef.current = null;
68
+ isReadyRef.current = false;
69
+ }
70
+ };
71
+ }, []); // 一次性初始化
72
+
73
+ return {
74
+ adapterRef,
75
+ chartRef: containerRef,
76
+ get isReady() {
77
+ return isReadyRef.current;
78
+ },
79
+ };
80
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * 性能监控 Hook
3
+ * 负责图表性能数据的收集和上报
4
+ */
5
+ import { useRef, useCallback } from 'react';
6
+ import { PerformanceAnalyzer } from '../../utils/performance';
7
+ import type { EChartsOption } from 'echarts';
8
+
9
+ export interface PerformanceData {
10
+ renderTime: number;
11
+ initTime: number;
12
+ updateTime: number;
13
+ dataSize: number;
14
+ }
15
+
16
+ export interface UsePerformanceOptions {
17
+ enabled?: boolean;
18
+ onPerformance?: (data: PerformanceData) => void;
19
+ }
20
+
21
+ export function usePerformance(options: UsePerformanceOptions = {}) {
22
+ const { enabled = false, onPerformance } = options;
23
+
24
+ const analyzerRef = useRef<PerformanceAnalyzer | null>(null);
25
+ const perfRef = useRef({
26
+ initStartTime: 0,
27
+ initEndTime: 0,
28
+ renderStartTime: 0,
29
+ renderEndTime: 0,
30
+ updateStartTime: 0,
31
+ updateEndTime: 0,
32
+ dataSize: 0,
33
+ });
34
+
35
+ /**
36
+ * 初始化性能分析器
37
+ */
38
+ const initAnalyzer = useCallback(() => {
39
+ if (!enabled) return;
40
+
41
+ if (!analyzerRef.current) {
42
+ analyzerRef.current = PerformanceAnalyzer.getInstance({
43
+ enabled: true,
44
+ metrics: ['initTime', 'renderTime', 'updateTime', 'dataSize', 'frameRate'],
45
+ sampleInterval: 1000,
46
+ maxSamples: 100,
47
+ realTime: true,
48
+ autoStart: true,
49
+ });
50
+ }
51
+ }, [enabled]);
52
+
53
+ /**
54
+ * 记录性能数据
55
+ */
56
+ const recordPerformance = useCallback(
57
+ (type: 'init' | 'render' | 'update', _data?: unknown) => {
58
+ const now = Date.now();
59
+ const perf = perfRef.current;
60
+ const _dataSize = 0; // 可通过参数传入
61
+
62
+ switch (type) {
63
+ case 'init':
64
+ if (!perf.initStartTime) {
65
+ perf.initStartTime = now;
66
+ } else {
67
+ perf.initEndTime = now;
68
+ const initTime = perf.initEndTime - perf.initStartTime;
69
+
70
+ if (analyzerRef.current) {
71
+ analyzerRef.current.recordInitTime(initTime);
72
+ }
73
+
74
+ onPerformance?.({
75
+ renderTime: 0,
76
+ initTime,
77
+ updateTime: 0,
78
+ dataSize: perf.dataSize,
79
+ });
80
+ }
81
+ break;
82
+
83
+ case 'render':
84
+ if (!perf.renderStartTime) {
85
+ perf.renderStartTime = now;
86
+ } else {
87
+ perf.renderEndTime = now;
88
+ const renderTime = perf.renderEndTime - perf.renderStartTime;
89
+
90
+ if (analyzerRef.current) {
91
+ analyzerRef.current.recordRenderTime(renderTime);
92
+ }
93
+
94
+ onPerformance?.({
95
+ renderTime,
96
+ initTime: perf.initEndTime - perf.initStartTime,
97
+ updateTime: 0,
98
+ dataSize: perf.dataSize,
99
+ });
100
+ }
101
+ break;
102
+
103
+ case 'update':
104
+ if (!perf.updateStartTime) {
105
+ perf.updateStartTime = now;
106
+ } else {
107
+ perf.updateEndTime = now;
108
+ const updateTime = perf.updateEndTime - perf.updateStartTime;
109
+
110
+ if (analyzerRef.current) {
111
+ analyzerRef.current.recordUpdateTime(updateTime);
112
+ }
113
+
114
+ onPerformance?.({
115
+ renderTime: 0,
116
+ initTime: 0,
117
+ updateTime,
118
+ dataSize: perf.dataSize,
119
+ });
120
+ }
121
+ break;
122
+ }
123
+ },
124
+ [onPerformance]
125
+ );
126
+
127
+ /**
128
+ * 计算数据大小
129
+ */
130
+ const calculateDataSize = useCallback((option?: EChartsOption): number => {
131
+ if (!option) return 0;
132
+ try {
133
+ return JSON.stringify(option).length;
134
+ } catch {
135
+ return 0;
136
+ }
137
+ }, []);
138
+
139
+ /**
140
+ * 更新数据大小
141
+ */
142
+ const updateDataSize = useCallback(
143
+ (option?: EChartsOption) => {
144
+ perfRef.current.dataSize = calculateDataSize(option);
145
+ if (analyzerRef.current && option) {
146
+ analyzerRef.current.recordDataSize(option);
147
+ }
148
+ },
149
+ [calculateDataSize]
150
+ );
151
+
152
+ /**
153
+ * 获取性能分析器实例
154
+ */
155
+ const getAnalyzer = useCallback(() => {
156
+ return analyzerRef.current;
157
+ }, []);
158
+
159
+ /**
160
+ * 清理性能监控
161
+ */
162
+ const dispose = useCallback(() => {
163
+ if (analyzerRef.current) {
164
+ analyzerRef.current.dispose();
165
+ analyzerRef.current = null;
166
+ }
167
+ perfRef.current = {
168
+ initStartTime: 0,
169
+ initEndTime: 0,
170
+ renderStartTime: 0,
171
+ renderEndTime: 0,
172
+ updateStartTime: 0,
173
+ updateEndTime: 0,
174
+ dataSize: 0,
175
+ };
176
+ }, []);
177
+
178
+ return {
179
+ initAnalyzer,
180
+ recordPerformance,
181
+ calculateDataSize,
182
+ updateDataSize,
183
+ getAnalyzer,
184
+ dispose,
185
+ };
186
+ }