@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.
- package/README.md +4 -4
- package/dist/cjs/index.js +1 -1
- package/dist/esm/index.js +3814 -2724
- package/package.json +1 -1
- package/src/__tests__/integration.test.tsx +12 -10
- package/src/adapters/BaseAdapter.ts +116 -0
- package/src/adapters/__tests__/index.test.ts +10 -10
- package/src/adapters/index.ts +63 -74
- package/src/adapters/swan/index.ts +26 -223
- package/src/adapters/tt/index.ts +28 -225
- package/src/adapters/types.ts +36 -0
- package/src/adapters/weapp/index.ts +29 -189
- package/src/charts/bar/index.tsx +5 -9
- package/src/charts/boxplot/__tests__/index.test.tsx +130 -0
- package/src/charts/boxplot/index.tsx +18 -0
- package/src/charts/boxplot/types.ts +46 -0
- package/src/charts/candlestick/__tests__/index.test.tsx +37 -0
- package/src/charts/candlestick/index.tsx +13 -0
- package/src/charts/common/BaseChartWrapper.tsx +47 -38
- package/src/charts/funnel/index.tsx +5 -9
- package/src/charts/gauge/index.tsx +5 -9
- package/src/charts/graph/__tests__/index.test.tsx +41 -0
- package/src/charts/graph/index.tsx +13 -0
- package/src/charts/heatmap/index.tsx +5 -9
- package/src/charts/index.ts +10 -1
- package/src/charts/line/index.tsx +4 -7
- package/src/charts/parallel/__tests__/index.test.tsx +164 -0
- package/src/charts/parallel/index.tsx +18 -0
- package/src/charts/parallel/types.ts +73 -0
- package/src/charts/pie/index.tsx +5 -10
- package/src/charts/radar/index.tsx +5 -9
- package/src/charts/scatter/index.tsx +5 -9
- package/src/charts/types.ts +48 -4
- package/src/charts/wordcloud/__tests__/index.test.tsx +36 -0
- package/src/charts/wordcloud/index.tsx +13 -0
- package/src/core/animation/AnimationManager.ts +15 -0
- package/src/core/components/Annotation.tsx +26 -21
- package/src/core/components/BaseChart.tsx +280 -1105
- package/src/core/components/ErrorBoundary.tsx +4 -1
- package/src/core/components/LazyChart.tsx +42 -55
- package/src/core/components/hooks/index.ts +20 -0
- package/src/core/components/hooks/useChartEvents.ts +143 -0
- package/src/core/components/hooks/useChartInit.ts +80 -0
- package/src/core/components/hooks/usePerformance.ts +186 -0
- package/src/core/components/hooks/useVirtualScroll.ts +156 -0
- package/src/core/echarts.ts +1 -1
- package/src/core/themes/ThemeManager.ts +31 -15
- package/src/core/types/index.ts +2 -2
- package/src/core/utils/chartInstances.ts +10 -3
- package/src/core/utils/chartUtils.ts +46 -0
- package/src/core/utils/common.ts +14 -1
- package/src/core/utils/export/ExportUtils.ts +13 -22
- package/src/core/utils/performance/PerformanceAnalyzer.ts +32 -5
- package/src/core/utils/uuid.ts +1 -1
- package/src/editor/EnhancedThemeEditor.tsx +624 -0
- package/src/editor/ThemeEditor.tsx +1 -6
- package/src/hooks/__tests__/index.test.tsx +14 -11
- package/src/hooks/__tests__/useDataTransform.test.ts +159 -0
- package/src/hooks/index.ts +54 -19
- package/src/hooks/useDataTransform.ts +503 -0
- package/src/index.ts +27 -9
- package/src/main.tsx +4 -4
- package/src/themes/__tests__/index.test.ts +2 -2
- 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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
+
}
|