@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.
Files changed (35) hide show
  1. package/README.md +30 -21
  2. package/dist/cjs/index.js +1 -1
  3. package/dist/esm/index.js +49471 -2199
  4. package/package.json +2 -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/common/BaseChartWrapper.tsx +193 -32
  9. package/src/charts/index.ts +5 -1
  10. package/src/charts/liquid/index.tsx +227 -0
  11. package/src/charts/liquid/types.ts +130 -0
  12. package/src/charts/tree/index.tsx +117 -0
  13. package/src/charts/tree/types.ts +174 -0
  14. package/src/charts/types.ts +1 -1
  15. package/src/components/DataFilter/index.tsx +587 -0
  16. package/src/core/animation/AnimationManager.ts +69 -42
  17. package/src/core/components/BaseChart.tsx +72 -9
  18. package/src/core/types/common.ts +21 -110
  19. package/src/core/types/index.ts +4 -135
  20. package/src/core/types/platform.ts +38 -230
  21. package/src/core/utils/drillDown.ts +643 -0
  22. package/src/core/utils/export/ExportUtils.ts +10 -1
  23. package/src/core/utils/performance/PerformanceAnalyzer.ts +21 -1
  24. package/src/core/utils/performance/types.ts +5 -0
  25. package/src/hooks/__tests__/index.test.tsx +7 -5
  26. package/src/hooks/index.ts +41 -2
  27. package/src/hooks/useAnimation.ts +427 -0
  28. package/src/hooks/useChartConnect.ts +362 -0
  29. package/src/hooks/useChartDownload.ts +692 -0
  30. package/src/hooks/useDataZoom.ts +323 -0
  31. package/src/hooks/usePerformance.ts +291 -0
  32. package/src/index.ts +25 -2
  33. package/src/themes/__tests__/index.test.ts +7 -13
  34. package/src/themes/index.ts +3 -0
  35. package/src/themes/useAutoTheme.ts +66 -0
@@ -0,0 +1,643 @@
1
+ /**
2
+ * DrillDown - 数据下钻工具
3
+ * 支持点击图表数据项时,自动下钻到更细粒度的数据视图
4
+ */
5
+ import type { EChartsOption } from 'echarts';
6
+
7
+ // ============================================================================
8
+ // 类型定义
9
+ // ============================================================================
10
+
11
+ /**
12
+ * 下钻数据源
13
+ * 描述一个可下钻的数据节点
14
+ */
15
+ export interface DrillDownSource {
16
+ /** 节点名称(用于显示和匹配) */
17
+ name: string | number;
18
+ /** 节点值 */
19
+ value: string | number;
20
+ /** 子级数据(点击后显示) */
21
+ children?: DrillDownSource[];
22
+ /** 点击后执行的图表 option 更新(可选,优先级高于 children) */
23
+ chartOption?: EChartsOption;
24
+ /** 额外数据元 */
25
+ meta?: Record<string, unknown>;
26
+ }
27
+
28
+ /**
29
+ * 下钻配置
30
+ */
31
+ export interface DrillDownConfig {
32
+ /** 下钻的维度 key */
33
+ dimension: string;
34
+ /** 下钻层级数据源(key 为维度值,value 为该维度下的下钻数据) */
35
+ sources: Record<string, DrillDownSource[]>;
36
+ /** 初始层级数据(不经过 dimension 匹配的直接数据) */
37
+ initialSources?: DrillDownSource[];
38
+ /** 当前层级 */
39
+ currentLevel?: number;
40
+ /** 是否自动绑定点击事件 */
41
+ autoBind?: boolean;
42
+ /** 下钻回调 */
43
+ onDrillDown?: (params: DrillDownEventParams) => void;
44
+ /** 上钻回调 */
45
+ onDrillUp?: (params: DrillUpEventParams) => void;
46
+ /** 重置回调 */
47
+ onReset?: (params: { level: number }) => void;
48
+ }
49
+
50
+ /**
51
+ * 下钻事件参数
52
+ */
53
+ export interface DrillDownEventParams {
54
+ /** 当前层级 */
55
+ level: number;
56
+ /** 点击的数据项名称 */
57
+ name: string | number;
58
+ /** 点击的数据项值 */
59
+ value: string | number;
60
+ /** 下钻后的数据源 */
61
+ sources: DrillDownSource[];
62
+ /** 下钻后的图表配置 */
63
+ chartOption: EChartsOption;
64
+ /** 原始 ECharts click params */
65
+ rawParams: Record<string, unknown>;
66
+ }
67
+
68
+ /**
69
+ * 上钻事件参数
70
+ */
71
+ export interface DrillUpEventParams {
72
+ /** 之前所在的层级 */
73
+ previousLevel: number;
74
+ /** 当前层级(上钻后) */
75
+ currentLevel: number;
76
+ /** 上钻后的图表配置 */
77
+ chartOption: EChartsOption;
78
+ }
79
+
80
+ /**
81
+ * DrillDown 返回接口
82
+ */
83
+ export interface DrillDownReturn {
84
+ /**
85
+ * 初始化下钻功能
86
+ * @param chartInstance ECharts 实例
87
+ * @param config 下钻配置
88
+ */
89
+ init: (chartInstance: any, config: DrillDownConfig) => void;
90
+ /**
91
+ * 返回上一层
92
+ */
93
+ drillUp: () => void;
94
+ /**
95
+ * 重置到第一层
96
+ */
97
+ reset: () => void;
98
+ /**
99
+ * 获取当前层级
100
+ * @returns 当前层级序号(从 0 开始)
101
+ */
102
+ getCurrentLevel: () => number;
103
+ /**
104
+ * 绑定图表点击事件
105
+ * @param chartInstance ECharts 实例
106
+ */
107
+ bindClick: (chartInstance: any) => void;
108
+ /**
109
+ * 解绑图表点击事件
110
+ * @param chartInstance ECharts 实例
111
+ */
112
+ unbindClick: (chartInstance: any) => void;
113
+ /**
114
+ * 手动触发下钻到指定层级
115
+ * @param level 目标层级
116
+ * @param dataItem 可选,指定的数据项
117
+ */
118
+ drillTo: (level: number, dataItem?: DrillDownSource) => void;
119
+ /**
120
+ * 获取层级历史记录
121
+ * @returns 层级历史(数组,每个元素为进入该层级时的点击数据项)
122
+ */
123
+ getHistory: () => Array<{ level: number; dataItem: DrillDownSource }>;
124
+ /**
125
+ * 检查是否可以上钻
126
+ * @returns 是否可以上钻
127
+ */
128
+ canDrillUp: () => boolean;
129
+ /**
130
+ * 销毁下钻实例,清理所有事件绑定
131
+ */
132
+ dispose: () => void;
133
+ }
134
+
135
+ // ============================================================================
136
+ // 内部状态
137
+ // ============================================================================
138
+
139
+ interface DrillDownState {
140
+ /** 图表实例 */
141
+ chartInstance: any;
142
+ /** 当前配置 */
143
+ config: DrillDownConfig;
144
+ /** 当前层级 */
145
+ currentLevel: number;
146
+ /** 层级历史 */
147
+ history: Array<{ level: number; dataItem: DrillDownSource }>;
148
+ /** 当前图表 option */
149
+ currentOption: EChartsOption;
150
+ /** 初始 option(用于重置) */
151
+ initialOption: EChartsOption;
152
+ /** 是否已初始化 */
153
+ initialized: boolean;
154
+ /** 事件处理器引用(用于解绑) */
155
+ clickHandler: ((params: any) => void) | null;
156
+ }
157
+
158
+ // ============================================================================
159
+ // 默认配置
160
+ // ============================================================================
161
+
162
+ const DEFAULT_CONFIG: Partial<DrillDownConfig> = {
163
+ autoBind: true,
164
+ currentLevel: 0,
165
+ };
166
+
167
+ // ============================================================================
168
+ // 创建下钻工具函数
169
+ // ============================================================================
170
+
171
+ /**
172
+ * 创建下钻工具
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * const drillDown = createDrillDown({
177
+ * dimension: 'category',
178
+ * sources: {
179
+ * '电子产品': [
180
+ * { name: '手机', value: 100, children: [...] },
181
+ * { name: '电脑', value: 80, children: [...] },
182
+ * ],
183
+ * '服装': [...]
184
+ * },
185
+ * onDrillDown: (params) => console.log('下钻到:', params.name),
186
+ * });
187
+ *
188
+ * drillDown.init(chartInstance);
189
+ * ```
190
+ *
191
+ * @param initialConfig 初始下钻配置
192
+ * @returns DrillDownReturn
193
+ */
194
+ export function createDrillDown(initialConfig?: Partial<DrillDownConfig>): DrillDownReturn {
195
+ // 内部状态
196
+ const state: DrillDownState = {
197
+ chartInstance: null,
198
+ config: { ...DEFAULT_CONFIG, ...initialConfig } as DrillDownConfig,
199
+ currentLevel: 0,
200
+ history: [],
201
+ currentOption: {},
202
+ initialOption: {},
203
+ initialized: false,
204
+ clickHandler: null,
205
+ };
206
+
207
+ // ============================================================
208
+ // 内部方法
209
+ // ============================================================
210
+
211
+ /**
212
+ * 根据层级和触发数据项获取下钻后的图表配置
213
+ */
214
+ const getDrillDownOption = (
215
+ level: number,
216
+ dataItem: DrillDownSource | undefined,
217
+ direction: 'down' | 'up' | 'reset'
218
+ ): EChartsOption => {
219
+ const { config } = state;
220
+ let targetSources: DrillDownSource[] = [];
221
+ let targetOption: EChartsOption = {};
222
+
223
+ if (direction === 'reset' || level === 0) {
224
+ // 重置或回到第一层:使用初始数据
225
+ if (config.initialSources) {
226
+ targetSources = config.initialSources;
227
+ // 从 initialSources 构建图表 option
228
+ targetOption = buildOptionFromSources(targetSources);
229
+ } else {
230
+ // 如果没有 initialSources,返回空 option,让用户重新 setOption
231
+ targetOption = {};
232
+ }
233
+ } else if (direction === 'up') {
234
+ // 上钻:从 history 中找到上一层的状态
235
+ const prevHistory = state.history[level - 1];
236
+ if (prevHistory && prevHistory.dataItem.chartOption) {
237
+ targetOption = prevHistory.dataItem.chartOption;
238
+ } else {
239
+ // 尝试从 children 构建
240
+ targetSources = prevHistory?.dataItem.children ?? [];
241
+ targetOption = buildOptionFromSources(targetSources);
242
+ }
243
+ } else if (direction === 'down' && dataItem) {
244
+ // 下钻
245
+ if (dataItem.chartOption) {
246
+ // 优先使用自定义 chartOption
247
+ targetOption = dataItem.chartOption;
248
+ } else if (dataItem.children && dataItem.children.length > 0) {
249
+ // 从 children 构建图表 option
250
+ targetSources = dataItem.children;
251
+ targetOption = buildOptionFromSources(targetSources);
252
+ }
253
+ }
254
+
255
+ return targetOption;
256
+ };
257
+
258
+ /**
259
+ * 从 DrillDownSource 数组构建 ECharts option
260
+ */
261
+ const buildOptionFromSources = (sources: DrillDownSource[]): EChartsOption => {
262
+ if (!sources || sources.length === 0) return {};
263
+
264
+ const names = sources.map((s) => s.name);
265
+ const values = sources.map((s) => s.value);
266
+
267
+ return {
268
+ xAxis: {
269
+ type: 'category',
270
+ data: names,
271
+ },
272
+ yAxis: {
273
+ type: 'value',
274
+ },
275
+ series: [
276
+ {
277
+ type: 'bar',
278
+ data: values,
279
+ },
280
+ ],
281
+ };
282
+ };
283
+
284
+ /**
285
+ * 检查是否有下钻数据
286
+ */
287
+ const hasDrillDownData = (dataItem: DrillDownSource | undefined): boolean => {
288
+ if (!dataItem) return false;
289
+ return !!(dataItem.children && dataItem.children.length > 0) || !!dataItem.chartOption;
290
+ };
291
+
292
+ /**
293
+ * 执行下钻
294
+ */
295
+ const executeDrillDown = (params: any) => {
296
+ const { config, chartInstance } = state;
297
+ if (!chartInstance) return;
298
+
299
+ const { name, value } = params;
300
+
301
+ // 在当前层级的数据中查找匹配项
302
+ let matchedSource: DrillDownSource | undefined;
303
+
304
+ // 尝试从 history 中获取当前层级的数据源
305
+ const currentLevelSources = getCurrentLevelSources();
306
+ matchedSource = currentLevelSources.find(
307
+ (s) => String(s.name) === String(name) || s.value === value
308
+ );
309
+
310
+ // 如果没找到,尝试在 initialSources 中查找
311
+ if (!matchedSource && state.currentLevel === 0 && config.initialSources) {
312
+ matchedSource = config.initialSources.find(
313
+ (s) => String(s.name) === String(name) || s.value === value
314
+ );
315
+ }
316
+
317
+ // 如果是最后一级(不能再下钻)或者找不到匹配项,不执行下钻
318
+ if (!hasDrillDownData(matchedSource)) {
319
+ console.warn('[DrillDown] No drill-down data available for:', name);
320
+ return;
321
+ }
322
+
323
+ // 记录历史
324
+ if (matchedSource) {
325
+ state.history.push({ level: state.currentLevel, dataItem: matchedSource });
326
+ }
327
+
328
+ // 更新层级
329
+ state.currentLevel += 1;
330
+
331
+ // 获取新的图表配置
332
+ const newOption = getDrillDownOption(state.currentLevel, matchedSource, 'down');
333
+
334
+ // 更新图表
335
+ if (newOption && Object.keys(newOption).length > 0) {
336
+ chartInstance.setOption(newOption, true);
337
+ state.currentOption = newOption;
338
+ }
339
+
340
+ // 触发回调
341
+ config.onDrillDown?.({
342
+ level: state.currentLevel,
343
+ name: matchedSource?.name ?? name,
344
+ value: matchedSource?.value ?? value,
345
+ sources: matchedSource?.children ?? [],
346
+ chartOption: newOption,
347
+ rawParams: params,
348
+ });
349
+ };
350
+
351
+ /**
352
+ * 获取当前层级的数据源列表
353
+ */
354
+ const getCurrentLevelSources = (): DrillDownSource[] => {
355
+ const { config } = state;
356
+
357
+ if (state.currentLevel === 0) {
358
+ // 第 0 层:使用 initialSources 或从 dimension 匹配
359
+ if (config.initialSources) {
360
+ return config.initialSources;
361
+ }
362
+ return [];
363
+ }
364
+
365
+ // 其他层级:从 history 中找到上一层点击的数据项的 children
366
+ const lastHistory = state.history[state.history.length - 1];
367
+ if (lastHistory && lastHistory.dataItem.children) {
368
+ return lastHistory.dataItem.children;
369
+ }
370
+
371
+ return [];
372
+ };
373
+
374
+ // ============================================================
375
+ // 实例方法
376
+ // ============================================================
377
+
378
+ const init = (chartInstance: any, config: DrillDownConfig): void => {
379
+ if (!chartInstance) {
380
+ console.error('[DrillDown] Invalid chart instance');
381
+ return;
382
+ }
383
+
384
+ state.chartInstance = chartInstance;
385
+ state.config = { ...DEFAULT_CONFIG, ...config } as DrillDownConfig;
386
+ state.currentLevel = config.currentLevel ?? 0;
387
+ state.initialized = true;
388
+
389
+ // 保存初始 option
390
+ try {
391
+ state.initialOption = chartInstance.getOption() || {};
392
+ } catch {
393
+ state.initialOption = {};
394
+ }
395
+
396
+ // 绑定点击事件
397
+ if (state.config.autoBind) {
398
+ bindClick(chartInstance);
399
+ }
400
+ };
401
+
402
+ const drillUp = (): void => {
403
+ const { config, chartInstance } = state;
404
+ if (!chartInstance || state.currentLevel <= 0) {
405
+ console.warn('[DrillDown] Cannot drill up: already at top level');
406
+ return;
407
+ }
408
+
409
+ const previousLevel = state.currentLevel;
410
+
411
+ // 弹出历史
412
+ state.history.pop();
413
+
414
+ // 更新层级
415
+ state.currentLevel -= 1;
416
+
417
+ // 获取新的图表配置
418
+ const newOption = getDrillDownOption(state.currentLevel, undefined, 'up');
419
+
420
+ // 更新图表
421
+ if (newOption && Object.keys(newOption).length > 0) {
422
+ chartInstance.setOption(newOption, true);
423
+ state.currentOption = newOption;
424
+ } else if (state.currentLevel === 0 && state.initialOption) {
425
+ chartInstance.setOption(state.initialOption, true);
426
+ state.currentOption = state.initialOption;
427
+ }
428
+
429
+ // 触发回调
430
+ config.onDrillUp?.({
431
+ previousLevel,
432
+ currentLevel: state.currentLevel,
433
+ chartOption: newOption,
434
+ });
435
+ };
436
+
437
+ const reset = (): void => {
438
+ const { config, chartInstance } = state;
439
+ if (!chartInstance) return;
440
+
441
+ const previousLevel = state.currentLevel;
442
+
443
+ // 清空历史
444
+ state.history = [];
445
+ state.currentLevel = 0;
446
+
447
+ // 获取初始配置
448
+ const newOption = getDrillDownOption(0, undefined, 'reset');
449
+
450
+ // 更新图表
451
+ if (newOption && Object.keys(newOption).length > 0) {
452
+ chartInstance.setOption(newOption, true);
453
+ state.currentOption = newOption;
454
+ } else if (state.initialOption && Object.keys(state.initialOption).length > 0) {
455
+ chartInstance.setOption(state.initialOption, true);
456
+ state.currentOption = state.initialOption;
457
+ }
458
+
459
+ // 触发回调
460
+ config.onReset?.({ level: 0 });
461
+ };
462
+
463
+ const getCurrentLevel = (): number => state.currentLevel;
464
+
465
+ const bindClick = (chartInstance: any): void => {
466
+ const instance = chartInstance || state.chartInstance;
467
+ if (!instance) {
468
+ console.error('[DrillDown] No chart instance to bind');
469
+ return;
470
+ }
471
+
472
+ // 解绑旧的
473
+ if (state.clickHandler) {
474
+ unbindClick(instance);
475
+ }
476
+
477
+ // 创建新的点击处理器
478
+ state.clickHandler = (params: any) => {
479
+ executeDrillDown(params);
480
+ };
481
+
482
+ instance.on('click', state.clickHandler);
483
+ };
484
+
485
+ const unbindClick = (chartInstance: any): void => {
486
+ const instance = chartInstance || state.chartInstance;
487
+ if (!instance || !state.clickHandler) return;
488
+
489
+ instance.off('click', state.clickHandler);
490
+ state.clickHandler = null;
491
+ };
492
+
493
+ const drillTo = (level: number, dataItem?: DrillDownSource): void => {
494
+ const { chartInstance, config } = state;
495
+ if (!chartInstance) return;
496
+
497
+ if (level < 0 || level > state.history.length) {
498
+ console.warn('[DrillDown] Invalid drill level:', level);
499
+ return;
500
+ }
501
+
502
+ const previousLevel = state.currentLevel;
503
+ state.currentLevel = level;
504
+
505
+ if (level === state.history.length) {
506
+ // 下钻
507
+ if (dataItem) {
508
+ state.history.push({ level: level - 1, dataItem });
509
+ }
510
+ } else if (level < state.history.length) {
511
+ // 上钻或跳转:调整 history
512
+ state.history = state.history.slice(0, level);
513
+ } else {
514
+ // level === 0, reset
515
+ state.history = [];
516
+ }
517
+
518
+ const newOption = getDrillDownOption(level, dataItem, level === 0 ? 'reset' : 'down');
519
+
520
+ if (newOption && Object.keys(newOption).length > 0) {
521
+ chartInstance.setOption(newOption, true);
522
+ state.currentOption = newOption;
523
+ }
524
+
525
+ if (level > previousLevel) {
526
+ config.onDrillDown?.({
527
+ level,
528
+ name: dataItem?.name ?? '',
529
+ value: dataItem?.value ?? 0,
530
+ sources: dataItem?.children ?? [],
531
+ chartOption: newOption,
532
+ rawParams: {},
533
+ });
534
+ } else if (level < previousLevel) {
535
+ config.onDrillUp?.({
536
+ previousLevel,
537
+ currentLevel: level,
538
+ chartOption: newOption,
539
+ });
540
+ } else {
541
+ config.onReset?.({ level: 0 });
542
+ }
543
+ };
544
+
545
+ const getHistory = (): Array<{ level: number; dataItem: DrillDownSource }> => {
546
+ return [...state.history];
547
+ };
548
+
549
+ const canDrillUp = (): boolean => {
550
+ return state.currentLevel > 0;
551
+ };
552
+
553
+ const dispose = (): void => {
554
+ if (state.chartInstance && state.clickHandler) {
555
+ unbindClick(state.chartInstance);
556
+ }
557
+ state.chartInstance = null;
558
+ state.config = {} as DrillDownConfig;
559
+ state.history = [];
560
+ state.currentLevel = 0;
561
+ state.initialized = false;
562
+ };
563
+
564
+ // ============================================================
565
+ // 返回公开接口
566
+ // ============================================================
567
+
568
+ return {
569
+ init,
570
+ drillUp,
571
+ reset,
572
+ getCurrentLevel,
573
+ bindClick,
574
+ unbindClick,
575
+ drillTo,
576
+ getHistory,
577
+ canDrillUp,
578
+ dispose,
579
+ };
580
+ }
581
+
582
+ // ============================================================================
583
+ // 辅助函数
584
+ // ============================================================================
585
+
586
+ /**
587
+ * 判断 DrillDownSource 是否有下钻能力
588
+ */
589
+ export function canDrillDown(source: DrillDownSource): boolean {
590
+ return !!(source.children && source.children.length > 0) || !!source.chartOption;
591
+ }
592
+
593
+ /**
594
+ * 从扁平数据构建层级结构(辅助函数)
595
+ */
596
+ export function buildHierarchy(
597
+ data: Array<{ [key: string]: unknown }>,
598
+ dimensionKey: string,
599
+ valueKey: string,
600
+ childrenKey: string = 'children'
601
+ ): DrillDownSource[] {
602
+ const sourceMap: Record<string, DrillDownSource[]> = {};
603
+
604
+ data.forEach((item) => {
605
+ const dimValue = String(item[dimensionKey]);
606
+ if (!sourceMap[dimValue]) {
607
+ sourceMap[dimValue] = [];
608
+ }
609
+ sourceMap[dimValue].push({
610
+ name: item[dimensionKey] as string | number,
611
+ value: item[valueKey] as string | number,
612
+ children: item[childrenKey]
613
+ ? buildHierarchy(item[childrenKey] as Array<{ [key: string]: unknown }>, dimensionKey, valueKey, childrenKey)
614
+ : undefined,
615
+ });
616
+ });
617
+
618
+ return Object.values(sourceMap).flat();
619
+ }
620
+
621
+ /**
622
+ * 创建典型的地区下钻示例配置
623
+ */
624
+ export function createRegionDrillDown(
625
+ regionData: Record<string, DrillDownSource[]>
626
+ ): DrillDownConfig {
627
+ return {
628
+ dimension: 'region',
629
+ sources: regionData,
630
+ };
631
+ }
632
+
633
+ /**
634
+ * 创建典型的分类下钻示例配置
635
+ */
636
+ export function createCategoryDrillDown(
637
+ categoryData: Record<string, DrillDownSource[]>
638
+ ): DrillDownConfig {
639
+ return {
640
+ dimension: 'category',
641
+ sources: categoryData,
642
+ };
643
+ }
@@ -13,13 +13,22 @@ import type { ECharts } from 'echarts';
13
13
  */
14
14
  export interface ExportImageOptions {
15
15
  /** 图片类型 */
16
- type?: 'png' | 'jpeg' | 'webp';
16
+ type?: 'png' | 'jpeg' | 'webp' | 'gif';
17
17
  /** 设备像素比 */
18
18
  pixelRatio?: number;
19
19
  /** 背景色 */
20
20
  backgroundColor?: string;
21
21
  /** 质量 (仅对 jpeg/webp 有效) */
22
22
  quality?: number;
23
+ /** GIF 选项 */
24
+ gifOptions?: {
25
+ /** 每帧延迟 (ms) */
26
+ delay?: number;
27
+ /** 重复次数 (-1 = 无限) */
28
+ repeat?: number;
29
+ /** 帧数 */
30
+ frames?: number;
31
+ };
23
32
  }
24
33
 
25
34
  /**
@@ -18,6 +18,8 @@ import {
18
18
  */
19
19
  export class PerformanceAnalyzer {
20
20
  private static instance: PerformanceAnalyzer | null = null;
21
+ // Per-chart isolated instances
22
+ private static instances: Map<string, PerformanceAnalyzer> = new Map();
21
23
  private config: PerformanceAnalysisConfig;
22
24
  private metrics: Map<PerformanceMetricType, PerformanceMetric[]> = new Map();
23
25
  private eventHandlers: Map<PerformanceEventType, PerformanceEventHandler[]> = new Map();
@@ -53,9 +55,18 @@ export class PerformanceAnalyzer {
53
55
  }
54
56
 
55
57
  /**
56
- * 获取单例实例
58
+ * 获取实例
59
+ * - 传入 chartId 时:每个 chartId 获得独立实例(指标隔离)
60
+ * - 不传 chartId 时:全局单例(向后兼容)
57
61
  */
58
62
  public static getInstance(config?: PerformanceAnalysisConfig): PerformanceAnalyzer {
63
+ if (config?.chartId) {
64
+ if (!PerformanceAnalyzer.instances.has(config.chartId)) {
65
+ PerformanceAnalyzer.instances.set(config.chartId, new PerformanceAnalyzer(config));
66
+ }
67
+ return PerformanceAnalyzer.instances.get(config.chartId)!;
68
+ }
69
+ // Legacy: global singleton
59
70
  if (!PerformanceAnalyzer.instance) {
60
71
  PerformanceAnalyzer.instance = new PerformanceAnalyzer(config);
61
72
  }
@@ -72,6 +83,15 @@ export class PerformanceAnalyzer {
72
83
  }
73
84
  }
74
85
 
86
+ /**
87
+ * 重置所有图表实例(用于测试/清理)
88
+ */
89
+ public static resetAllInstances(): void {
90
+ PerformanceAnalyzer.resetInstance();
91
+ PerformanceAnalyzer.instances.forEach((analyzer) => analyzer.stop());
92
+ PerformanceAnalyzer.instances.clear();
93
+ }
94
+
75
95
  /**
76
96
  * 注册事件处理器
77
97
  */