@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
@@ -0,0 +1,156 @@
1
+ /**
2
+ * 虚拟滚动 Hook
3
+ * 负责大数据集的分页渲染和数据筛选
4
+ */
5
+ import { useRef, useCallback } from 'react';
6
+ import type { EChartsOption } from 'echarts';
7
+
8
+ export interface UseVirtualScrollOptions {
9
+ enabled?: boolean;
10
+ pageSize?: number;
11
+ preloadSize?: number;
12
+ filters?: Record<string, unknown>;
13
+ }
14
+
15
+ export interface VirtualScrollState {
16
+ currentPage: number;
17
+ totalPages: number;
18
+ totalDataCount: number;
19
+ startIndex: number;
20
+ endIndex: number;
21
+ isScrolling: boolean;
22
+ }
23
+
24
+ export function useVirtualScroll(options: UseVirtualScrollOptions = {}) {
25
+ const { enabled = false, pageSize = 100, preloadSize = 50 } = options;
26
+
27
+ const stateRef = useRef<VirtualScrollState>({
28
+ currentPage: 0,
29
+ totalPages: 1,
30
+ totalDataCount: 0,
31
+ startIndex: 0,
32
+ endIndex: 0,
33
+ isScrolling: false,
34
+ });
35
+
36
+ /**
37
+ * 筛选数据
38
+ */
39
+ const filterData = useCallback(
40
+ (data: unknown[], filters: Record<string, unknown> = {}): unknown[] => {
41
+ if (!filters || Object.keys(filters).length === 0) {
42
+ return data;
43
+ }
44
+
45
+ return data.filter((item) => {
46
+ const record = item as Record<string, unknown>;
47
+ for (const [key, value] of Object.entries(filters)) {
48
+ if (record[key] !== value && !(record[key] as unknown[])?.includes?.(value)) {
49
+ return false;
50
+ }
51
+ }
52
+ return true;
53
+ });
54
+ },
55
+ []
56
+ );
57
+
58
+ /**
59
+ * 处理虚拟滚动
60
+ * 返回处理后的配置和当前页数据范围
61
+ */
62
+ const processVirtualScroll = useCallback(
63
+ (
64
+ originalOption: EChartsOption,
65
+ filters: Record<string, unknown> = {}
66
+ ): { processedOption: EChartsOption; state: VirtualScrollState } => {
67
+ const state = stateRef.current;
68
+
69
+ if (!enabled || !originalOption?.series) {
70
+ return { processedOption: originalOption, state };
71
+ }
72
+
73
+ // 深拷贝避免修改原始数据
74
+ const processedOption = JSON.parse(JSON.stringify(originalOption)) as EChartsOption;
75
+
76
+ (processedOption.series as unknown[]).forEach((seriesItem: unknown, _index: number) => {
77
+ const s = seriesItem as { data?: unknown[] };
78
+ if (!s.data || !Array.isArray(s.data)) return;
79
+
80
+ // 应用筛选
81
+ const filteredData = filterData(s.data, filters);
82
+
83
+ // 计算分页
84
+ state.totalDataCount = filteredData.length;
85
+ state.totalPages = Math.ceil(filteredData.length / pageSize);
86
+
87
+ const startIndex = state.currentPage * pageSize;
88
+ const endIndex = Math.min(startIndex + pageSize + preloadSize, filteredData.length);
89
+
90
+ state.startIndex = startIndex;
91
+ state.endIndex = endIndex;
92
+
93
+ // 返回当前页数据
94
+ s.data = filteredData.slice(startIndex, endIndex);
95
+ });
96
+
97
+ return { processedOption, state };
98
+ },
99
+ [enabled, pageSize, preloadSize, filterData]
100
+ );
101
+
102
+ /**
103
+ * 跳转到指定页
104
+ */
105
+ const goToPage = useCallback((page: number) => {
106
+ const state = stateRef.current;
107
+ if (page >= 0 && page < state.totalPages) {
108
+ state.currentPage = page;
109
+ }
110
+ }, []);
111
+
112
+ /**
113
+ * 下一页
114
+ */
115
+ const nextPage = useCallback(() => {
116
+ goToPage(stateRef.current.currentPage + 1);
117
+ }, [goToPage]);
118
+
119
+ /**
120
+ * 上一页
121
+ */
122
+ const prevPage = useCallback(() => {
123
+ goToPage(stateRef.current.currentPage - 1);
124
+ }, [goToPage]);
125
+
126
+ /**
127
+ * 根据滚动位置更新页码
128
+ */
129
+ const updatePageFromScroll = useCallback((scrollPercent: number) => {
130
+ const state = stateRef.current;
131
+ const newPage = Math.floor((scrollPercent / 100) * state.totalPages);
132
+ if (newPage !== state.currentPage && newPage >= 0 && newPage < state.totalPages) {
133
+ state.currentPage = newPage;
134
+ return true; // 页码变化
135
+ }
136
+ return false; // 页码未变化
137
+ }, []);
138
+
139
+ /**
140
+ * 设置滚动状态
141
+ */
142
+ const setScrolling = useCallback((scrolling: boolean) => {
143
+ stateRef.current.isScrolling = scrolling;
144
+ }, []);
145
+
146
+ return {
147
+ stateRef,
148
+ processVirtualScroll,
149
+ filterData,
150
+ goToPage,
151
+ nextPage,
152
+ prevPage,
153
+ updatePageFromScroll,
154
+ setScrolling,
155
+ };
156
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ECharts 组件注册中心
3
3
  * 统一管理所有图表组件的注册,避免重复注册
4
- *
4
+ *
5
5
  * 使用 tree-shaking 友好型导入
6
6
  * 仅在用户使用特定图表时加载对应模块
7
7
  */
@@ -2,7 +2,6 @@
2
2
  * TaroViz 主题系统
3
3
  * 支持 CSS 变量、动态主题切换、自定义主题
4
4
  */
5
- import type { EChartsOption } from 'echarts';
6
5
 
7
6
  // ============================================================================
8
7
  // 类型定义
@@ -79,7 +78,17 @@ export interface ThemeVariables {
79
78
  /**
80
79
  * 预设主题
81
80
  */
82
- export type PresetThemeName = 'default' | 'dark' | 'vintage' | 'macarons' | 'infographic' | 'helianthus' | 'blue' | 'red' | 'green' | 'purple';
81
+ export type PresetThemeName =
82
+ | 'default'
83
+ | 'dark'
84
+ | 'vintage'
85
+ | 'macarons'
86
+ | 'infographic'
87
+ | 'helianthus'
88
+ | 'blue'
89
+ | 'red'
90
+ | 'green'
91
+ | 'purple';
83
92
 
84
93
  // ============================================================================
85
94
  // 预设主题配置
@@ -112,7 +121,8 @@ const PRESET_THEMES: Record<PresetThemeName, ThemeConfig> = {
112
121
  '--tv-chart-color-6': '#3ba272',
113
122
  '--tv-chart-color-7': '#fc8452',
114
123
  '--tv-chart-color-8': '#9a60b4',
115
- '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
124
+ '--tv-font-family':
125
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
116
126
  '--tv-font-size': '14px',
117
127
  '--tv-font-size-small': '12px',
118
128
  '--tv-font-size-large': '16px',
@@ -147,7 +157,8 @@ const PRESET_THEMES: Record<PresetThemeName, ThemeConfig> = {
147
157
  '--tv-chart-color-6': '#3ba272',
148
158
  '--tv-chart-color-7': '#fc8452',
149
159
  '--tv-chart-color-8': '#9a60b4',
150
- '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
160
+ '--tv-font-family':
161
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
151
162
  '--tv-font-size': '14px',
152
163
  '--tv-font-size-small': '12px',
153
164
  '--tv-font-size-large': '16px',
@@ -182,7 +193,8 @@ const PRESET_THEMES: Record<PresetThemeName, ThemeConfig> = {
182
193
  '--tv-chart-color-6': '#c9b8d4',
183
194
  '--tv-chart-color-7': '#a8c4d4',
184
195
  '--tv-chart-color-8': '#d4c49a',
185
- '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
196
+ '--tv-font-family':
197
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
186
198
  '--tv-font-size': '14px',
187
199
  '--tv-font-size-small': '12px',
188
200
  '--tv-font-size-large': '16px',
@@ -217,7 +229,8 @@ const PRESET_THEMES: Record<PresetThemeName, ThemeConfig> = {
217
229
  '--tv-chart-color-6': '#a8e6cf',
218
230
  '--tv-chart-color-7': '#ffd3b6',
219
231
  '--tv-chart-color-8': '#ffaaa5',
220
- '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
232
+ '--tv-font-family':
233
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
221
234
  '--tv-font-size': '14px',
222
235
  '--tv-font-size-small': '12px',
223
236
  '--tv-font-size-large': '16px',
@@ -252,7 +265,8 @@ const PRESET_THEMES: Record<PresetThemeName, ThemeConfig> = {
252
265
  '--tv-chart-color-6': '#e8352e',
253
266
  '--tv-chart-color-7': '#b02ad3',
254
267
  '--tv-chart-color-8': '#6475d4',
255
- '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
268
+ '--tv-font-family':
269
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
256
270
  '--tv-font-size': '14px',
257
271
  '--tv-font-size-small': '12px',
258
272
  '--tv-font-size-large': '16px',
@@ -287,7 +301,8 @@ const PRESET_THEMES: Record<PresetThemeName, ThemeConfig> = {
287
301
  '--tv-chart-color-6': '#f5a623',
288
302
  '--tv-chart-color-7': '#6dd3ce',
289
303
  '--tv-chart-color-8': '#d4778b',
290
- '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
304
+ '--tv-font-family':
305
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
291
306
  '--tv-font-size': '14px',
292
307
  '--tv-font-size-small': '12px',
293
308
  '--tv-font-size-large': '16px',
@@ -322,7 +337,8 @@ const PRESET_THEMES: Record<PresetThemeName, ThemeConfig> = {
322
337
  '--tv-chart-color-6': '#3ba272',
323
338
  '--tv-chart-color-7': '#fc8452',
324
339
  '--tv-chart-color-8': '#9a60b4',
325
- '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
340
+ '--tv-font-family':
341
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
326
342
  '--tv-font-size': '14px',
327
343
  '--tv-font-size-small': '12px',
328
344
  '--tv-font-size-large': '16px',
@@ -357,7 +373,8 @@ const PRESET_THEMES: Record<PresetThemeName, ThemeConfig> = {
357
373
  '--tv-chart-color-6': '#52c41a',
358
374
  '--tv-chart-color-7': '#faad14',
359
375
  '--tv-chart-color-8': '#8b5cf6',
360
- '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
376
+ '--tv-font-family':
377
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
361
378
  '--tv-font-size': '14px',
362
379
  '--tv-font-size-small': '12px',
363
380
  '--tv-font-size-large': '16px',
@@ -392,7 +409,8 @@ const PRESET_THEMES: Record<PresetThemeName, ThemeConfig> = {
392
409
  '--tv-chart-color-6': '#13c2c2',
393
410
  '--tv-chart-color-7': '#fa8c16',
394
411
  '--tv-chart-color-8': '#eb2f96',
395
- '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
412
+ '--tv-font-family':
413
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
396
414
  '--tv-font-size': '14px',
397
415
  '--tv-font-size-small': '12px',
398
416
  '--tv-font-size-large': '16px',
@@ -427,7 +445,8 @@ const PRESET_THEMES: Record<PresetThemeName, ThemeConfig> = {
427
445
  '--tv-chart-color-6': '#ff4d4f',
428
446
  '--tv-chart-color-7': '#13c2c2',
429
447
  '--tv-chart-color-8': '#fa8c16',
430
- '--tv-font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
448
+ '--tv-font-family':
449
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
431
450
  '--tv-font-size': '14px',
432
451
  '--tv-font-size-small': '12px',
433
452
  '--tv-font-size-large': '16px',
@@ -619,9 +638,6 @@ class ThemeManager {
619
638
  // 导出单例实例
620
639
  export const themeManager = ThemeManager.getInstance();
621
640
 
622
- // 导出类型
623
- export type { ThemeConfig, ThemeVariables, PresetThemeName };
624
-
625
641
  // 导出预设主题
626
642
  export { PRESET_THEMES };
627
643
 
@@ -230,9 +230,9 @@ export interface Adapter {
230
230
  convertToDataURL?(opts?: any): string | undefined;
231
231
 
232
232
  /**
233
- * 渲染图表
233
+ * 渲染图表(仅 H5 环境需要,小程序适配器不需要)
234
234
  */
235
- render(): JSX.Element;
235
+ render?(): JSX.Element | null;
236
236
  }
237
237
 
238
238
  /**
@@ -14,7 +14,9 @@ export function registerChart(id: string, instance: EChartsType): void {
14
14
  // 如果已存在同名ID,先释放旧实例防止内存泄漏
15
15
  if (CHART_INSTANCES[id]) {
16
16
  try {
17
- console.warn(`[TaroViz] Chart instance '${id}' already exists, replacing and disposing old instance`);
17
+ console.warn(
18
+ `[TaroViz] Chart instance '${id}' already exists, replacing and disposing old instance`
19
+ );
18
20
  CHART_INSTANCES[id].dispose();
19
21
  } catch (e) {
20
22
  console.warn(`Failed to dispose old chart instance: ${id}`, e);
@@ -38,16 +40,21 @@ export function getChart(id: string): EChartsType | undefined {
38
40
  */
39
41
  export function removeChart(id: string): void {
40
42
  if (CHART_INSTANCES[id]) {
43
+ try {
44
+ CHART_INSTANCES[id].dispose();
45
+ } catch (e) {
46
+ console.warn(`Failed to dispose chart on removal: ${id}`, e);
47
+ }
41
48
  delete CHART_INSTANCES[id];
42
49
  }
43
50
  }
44
51
 
45
52
  /**
46
53
  * 获取所有图表实例
47
- * @returns 所有图表实例
54
+ * @returns 所有图表实例的浅拷贝
48
55
  */
49
56
  export function getAllCharts(): Record<string, EChartsType> {
50
- return CHART_INSTANCES;
57
+ return { ...CHART_INSTANCES };
51
58
  }
52
59
 
53
60
  /**
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Chart utilities shared between BaseChart and BaseChartWrapper
3
+ */
4
+ import type { EChartsOption } from 'echarts';
5
+
6
+ /**
7
+ * Normalize size value to CSS string
8
+ */
9
+ export function normalizeSize(value: number | string | undefined, fallback: string): string {
10
+ if (value === undefined) return fallback;
11
+ return typeof value === 'number' ? `${value}px` : value;
12
+ }
13
+
14
+ /**
15
+ * Calculate total data points in an ECharts option for animation optimization
16
+ */
17
+ export function calculateDataLength(option: { series?: unknown } | undefined): number {
18
+ if (!option) return 0;
19
+ let count = 0;
20
+ if (option.series) {
21
+ const series = Array.isArray(option.series) ? option.series : [option.series];
22
+ for (const seriesItem of series as any[]) {
23
+ if (seriesItem.data) {
24
+ if (Array.isArray(seriesItem.data)) {
25
+ count += seriesItem.data.length;
26
+ } else if (typeof seriesItem.data === 'object') {
27
+ count += Object.keys(seriesItem.data).length;
28
+ }
29
+ }
30
+ }
31
+ }
32
+ return count;
33
+ }
34
+
35
+ /**
36
+ * Filter data by filter conditions
37
+ */
38
+ export function filterDataByKeys(data: any[], filters: Record<string, any>): any[] {
39
+ if (!filters || Object.keys(filters).length === 0) return data;
40
+ return data.filter((item) => {
41
+ for (const [key, value] of Object.entries(filters)) {
42
+ if (item[key] !== value && !item[key]?.includes?.(value)) return false;
43
+ }
44
+ return true;
45
+ });
46
+ }
@@ -20,7 +20,20 @@ export const isBrowser = typeof window !== 'undefined' && typeof document !== 'u
20
20
  * 是否为NodeJS环境
21
21
  * @returns 是否为NodeJS环境
22
22
  */
23
- export const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
23
+ export const isNode = (() => {
24
+ // 更可靠的环境检测:检查是否是真正的 Node.js 环境
25
+ // 而不是打包后的代码(如 webpack 定义的 process.env)
26
+ try {
27
+ return (
28
+ typeof process !== 'undefined' &&
29
+ process.versions &&
30
+ process.versions.node &&
31
+ Object.prototype.toString.call(globalThis.process) === '[object process]'
32
+ );
33
+ } catch {
34
+ return false;
35
+ }
36
+ })();
24
37
 
25
38
  /**
26
39
  * 是否为React Native环境
@@ -103,7 +103,7 @@ function dataURLToBlob(dataURL: string): Blob {
103
103
  /**
104
104
  * 下载文件
105
105
  */
106
- function downloadFile(data: string | Blob, filename: string, mimeType: string): void {
106
+ function downloadFile(data: string | Blob, filename: string, _mimeType: string): void {
107
107
  const blob = typeof data === 'string' ? dataURLToBlob(data) : data;
108
108
  const url = URL.createObjectURL(blob);
109
109
 
@@ -142,19 +142,12 @@ class ChartExporter {
142
142
  /**
143
143
  * 导出图表为图片
144
144
  */
145
- static exportImage(
146
- chart: ECharts,
147
- options: ExportImageOptions = {}
148
- ): ExportResult {
149
- const {
150
- type = 'png',
151
- pixelRatio = 2,
152
- backgroundColor = '#ffffff',
153
- quality = 0.8,
154
- } = options;
145
+ static exportImage(chart: ECharts, options: ExportImageOptions = {}): ExportResult {
146
+ const { type = 'png', pixelRatio = 2, backgroundColor = '#ffffff', quality = 0.8 } = options;
155
147
 
156
148
  const mimeType = `image/${type}`;
157
- const data = chart.getDataURL({
149
+ // 使用 any 避免 ECharts 类型定义与实际支持类型不匹配
150
+ const data = (chart.getDataURL as any)({
158
151
  type,
159
152
  pixelRatio,
160
153
  backgroundColor,
@@ -174,8 +167,9 @@ class ChartExporter {
174
167
  static exportSVG(chart: ECharts, options: ExportSVGOptions = {}): ExportResult {
175
168
  const { compress = false } = options;
176
169
 
177
- const svgData = chart.getSvgData();
178
- if (!svgData) {
170
+ // ECharts 5.x 使用 getDataURL 获取 SVG
171
+ const svgData = (chart.getDataURL as any)({ type: 'svg' });
172
+ if (!svgData || svgData === 'data:image/svg+xml;charset=utf8,') {
179
173
  throw new Error('SVG export is not supported. Please use canvas renderer.');
180
174
  }
181
175
 
@@ -198,10 +192,7 @@ class ChartExporter {
198
192
  /**
199
193
  * 导出图表为 PDF (需要 jspdf 库支持)
200
194
  */
201
- static async exportPDF(
202
- chart: ECharts,
203
- options: ExportPDFOptions = {}
204
- ): Promise<ExportResult> {
195
+ static async exportPDF(chart: ECharts, options: ExportPDFOptions = {}): Promise<ExportResult> {
205
196
  const {
206
197
  orientation = 'portrait',
207
198
  pageSize = 'a4',
@@ -222,7 +213,7 @@ class ChartExporter {
222
213
  let jsPDF: any;
223
214
  try {
224
215
  // 尝试使用动态导入,使用 webpackIgnore 注释避免预解析
225
- // @ts-ignore - 动态导入
216
+ // @ts-expect-error - 动态导入
226
217
  jsPDF = (await import(/* webpackIgnore: true */ 'jspdf')).default;
227
218
  } catch {
228
219
  // 如果没有 jspdf,提供备选方案
@@ -283,7 +274,7 @@ class ChartExporter {
283
274
  doc.addImage(imageData, 'PNG', chartX, chartY, chartWidth, chartHeight);
284
275
 
285
276
  // 添加页脚
286
- const footerY = pageHeight - margin.bottom / 2;
277
+ const footerY = pageHeight - (margin.bottom ?? 40) / 2;
287
278
  doc.setFontSize(10);
288
279
  doc.setTextColor(153, 153, 153);
289
280
  doc.text(`Generated by TaroViz on ${new Date().toLocaleDateString()}`, marginLeft, footerY);
@@ -306,7 +297,7 @@ class ChartExporter {
306
297
  charts: Array<{ name: string; chart: ECharts }>,
307
298
  options: BatchExportOptions
308
299
  ): Promise<ExportResult[]> {
309
- const { format, filenamePrefix = 'chart', compress } = options;
300
+ const { format, filenamePrefix: _filenamePrefix = 'chart', compress } = options;
310
301
 
311
302
  const results: ExportResult[] = [];
312
303
 
@@ -347,7 +338,7 @@ class ChartExporter {
347
338
  static async copyToClipboard(chart: ECharts, options: ExportImageOptions = {}): Promise<boolean> {
348
339
  try {
349
340
  const result = this.exportImage(chart, { ...options, type: 'png' });
350
- const blob = dataURLToBlob(result.data);
341
+ const blob = dataURLToBlob(result.data as string);
351
342
 
352
343
  await navigator.clipboard.write([
353
344
  new ClipboardItem({
@@ -156,6 +156,27 @@ export class PerformanceAnalyzer {
156
156
  this.emit(PerformanceEventType.MONITORING_END);
157
157
  }
158
158
 
159
+ /**
160
+ * 释放资源
161
+ * 完全清理性能分析器,包括单例实例
162
+ */
163
+ public dispose(): void {
164
+ this.stop();
165
+ this.metrics.clear();
166
+ this.eventHandlers.clear();
167
+ this.frameRateHistory = [];
168
+
169
+ // 如果是当前单例,清除单例引用
170
+ if (PerformanceAnalyzer.instance === this) {
171
+ PerformanceAnalyzer.instance = null;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * RAF 动画帧 ID,用于取消
177
+ */
178
+ private rafId: number | null = null;
179
+
159
180
  /**
160
181
  * 开始帧率监控
161
182
  */
@@ -169,23 +190,29 @@ export class PerformanceAnalyzer {
169
190
  const deltaTime = currentTime - this.lastFrameTime;
170
191
  const frameRate = deltaTime > 0 ? Math.round(1000 / deltaTime) : 0;
171
192
 
172
- this.frameRateHistory.push(frameRate);
173
- if (this.frameRateHistory.length > 60) {
193
+ // 使用 config.maxSamples 限制历史记录长度
194
+ const maxSamples = this.config.maxSamples ?? 100;
195
+ if (this.frameRateHistory.length >= maxSamples) {
174
196
  this.frameRateHistory.shift();
175
197
  }
198
+ this.frameRateHistory.push(frameRate);
176
199
 
177
200
  this.lastFrameTime = currentTime;
178
- requestAnimationFrame(updateFrameRate);
201
+ this.rafId = requestAnimationFrame(updateFrameRate);
179
202
  };
180
203
 
181
- requestAnimationFrame(updateFrameRate);
204
+ this.rafId = requestAnimationFrame(updateFrameRate);
182
205
  }
183
206
 
184
207
  /**
185
208
  * 停止帧率监控
186
209
  */
187
210
  private stopFrameRateMonitoring(): void {
188
- // requestAnimationFrame 会自动停止,不需要额外清理
211
+ // 取消 RAF 循环,防止继续运行
212
+ if (this.rafId !== null) {
213
+ cancelAnimationFrame(this.rafId);
214
+ this.rafId = null;
215
+ }
189
216
  this.frameRateHistory = [];
190
217
  }
191
218
 
@@ -8,7 +8,7 @@ export function uuid(): string {
8
8
  if (typeof globalThis.crypto?.randomUUID === 'function') {
9
9
  return globalThis.crypto.randomUUID();
10
10
  }
11
-
11
+
12
12
  // 回退:使用 Math.random() + 时间戳混合
13
13
  const timestamp = Date.now().toString(36);
14
14
  const randomPart = Math.random().toString(36).substring(2, 15);