@agions/taroviz 1.1.0 → 1.2.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 (73) hide show
  1. package/README.md +318 -53
  2. package/dist/index.esm.js +67955 -3318
  3. package/package.json +102 -20
  4. package/src/__tests__/integration.test.tsx +168 -0
  5. package/src/adapters/__tests__/index.test.ts +91 -0
  6. package/src/adapters/h5/__tests__/index.test.ts +156 -0
  7. package/src/adapters/h5/index.ts +301 -0
  8. package/src/adapters/harmony/index.ts +274 -0
  9. package/src/adapters/index.ts +234 -0
  10. package/src/adapters/swan/index.ts +274 -0
  11. package/src/adapters/tt/index.ts +274 -0
  12. package/src/adapters/types.ts +162 -0
  13. package/src/adapters/weapp/index.ts +237 -0
  14. package/src/charts/bar/__tests__/index.test.tsx +113 -0
  15. package/src/charts/bar/index.tsx +27 -0
  16. package/src/charts/common/BaseChartWrapper.tsx +136 -0
  17. package/src/charts/funnel/index.tsx +33 -0
  18. package/src/charts/gauge/index.tsx +33 -0
  19. package/src/charts/heatmap/index.tsx +33 -0
  20. package/src/charts/index.ts +21 -0
  21. package/src/charts/line/__tests__/index.test.tsx +107 -0
  22. package/src/charts/line/index.tsx +27 -0
  23. package/src/charts/pie/__tests__/index.test.tsx +112 -0
  24. package/src/charts/pie/index.tsx +22 -0
  25. package/src/charts/radar/index.tsx +33 -0
  26. package/src/charts/scatter/index.tsx +33 -0
  27. package/src/charts/types.ts +146 -0
  28. package/src/charts/utils.ts +56 -0
  29. package/src/core/__tests__/platform.test.ts +48 -0
  30. package/src/core/animation/AnimationManager.ts +391 -0
  31. package/src/core/animation/index.ts +20 -0
  32. package/src/core/animation/types.ts +248 -0
  33. package/src/core/components/BaseChart.tsx +1319 -0
  34. package/src/core/index.ts +19 -0
  35. package/src/core/types/chart.ts +66 -0
  36. package/src/core/types/common.ts +224 -0
  37. package/src/core/types/index.ts +281 -0
  38. package/src/core/types/platform.ts +325 -0
  39. package/src/core/utils/__tests__/common.test.ts +52 -0
  40. package/src/core/utils/__tests__/environment.test.ts +94 -0
  41. package/src/core/utils/__tests__/i18n.test.ts +247 -0
  42. package/src/core/utils/__tests__/index.test.ts +219 -0
  43. package/src/core/utils/__tests__/uuid.test.ts +78 -0
  44. package/src/core/utils/chartInstances.ts +69 -0
  45. package/src/core/utils/codeGenerator/CodeGenerator.ts +655 -0
  46. package/src/core/utils/codeGenerator/index.ts +13 -0
  47. package/src/core/utils/codeGenerator/types.ts +200 -0
  48. package/src/core/utils/common.ts +58 -0
  49. package/src/core/utils/configGenerator/ConfigGenerator.ts +583 -0
  50. package/src/core/utils/configGenerator/index.ts +13 -0
  51. package/src/core/utils/configGenerator/types.ts +445 -0
  52. package/src/core/utils/debug/DebugPanel.tsx +637 -0
  53. package/src/core/utils/debug/debugger.ts +322 -0
  54. package/src/core/utils/debug/index.ts +21 -0
  55. package/src/core/utils/debug/types.ts +142 -0
  56. package/src/core/utils/i18n.ts +452 -0
  57. package/src/core/utils/index.ts +162 -0
  58. package/src/core/utils/performance/PerformanceAnalyzer.ts +586 -0
  59. package/src/core/utils/performance/index.ts +13 -0
  60. package/src/core/utils/performance/types.ts +180 -0
  61. package/src/core/utils/uuid.ts +30 -0
  62. package/src/editor/ThemeEditor.tsx +449 -0
  63. package/src/editor/index.ts +10 -0
  64. package/src/hooks/__tests__/index.test.tsx +333 -0
  65. package/src/hooks/index.ts +214 -0
  66. package/src/index.ts +75 -0
  67. package/src/main.tsx +247 -0
  68. package/src/react-dom.d.ts +7 -0
  69. package/src/themes/__tests__/index.test.ts +91 -0
  70. package/src/themes/index.ts +465 -0
  71. package/dist/index.esm.js.map +0 -1
  72. package/dist/index.js +0 -4012
  73. package/dist/index.js.map +0 -1
@@ -0,0 +1,1319 @@
1
+ /**
2
+ * TaroViz 基础图表组件
3
+ * 所有图表组件的基类
4
+ *
5
+ * 该组件提供了图表的基础功能,包括初始化、事件处理、主题设置等
6
+ * 所有具体的图表组件(如折线图、柱状图等)都继承自该组件
7
+ */
8
+ import React, { useEffect, useRef } from 'react';
9
+
10
+ import { getAdapter } from '../../adapters';
11
+ import { AnimationManager, generateEChartsAnimationConfig } from '../animation';
12
+ import { EChartsOption, EChartsType, AnimationConfig } from '../types';
13
+ import { registerChart, removeChart, getChart } from '../utils/chartInstances';
14
+ import {
15
+ DebugPanel,
16
+ DebugPanelOptions,
17
+ updateDebugInfo,
18
+ addDebugEvent,
19
+ addDebugError,
20
+ } from '../utils/debug';
21
+ import { PerformanceAnalyzer, PerformanceMetricType } from '../utils/performance';
22
+
23
+ /**
24
+ * 图表事件参数类型
25
+ *
26
+ * @interface ChartEventParams
27
+ * @description 定义了图表事件回调函数的参数类型
28
+ */
29
+ export interface ChartEventParams {
30
+ /**
31
+ * 事件参数的键值对
32
+ */
33
+ [key: string]: any;
34
+ }
35
+
36
+ /**
37
+ * 图表导出选项
38
+ */
39
+ export interface ChartExportOptions {
40
+ /**
41
+ * 导出类型
42
+ */
43
+ type?: 'png' | 'jpeg' | 'svg';
44
+
45
+ /**
46
+ * 导出文件名
47
+ */
48
+ filename?: string;
49
+
50
+ /**
51
+ * 像素比
52
+ */
53
+ pixelRatio?: number;
54
+
55
+ /**
56
+ * 背景色
57
+ */
58
+ backgroundColor?: string;
59
+ }
60
+
61
+ /**
62
+ * 图表联动配置
63
+ */
64
+ export interface ChartLinkageConfig {
65
+ /**
66
+ * 联动的图表ID列表
67
+ */
68
+ linkedChartIds?: string[];
69
+
70
+ /**
71
+ * 是否启用点击联动
72
+ */
73
+ enableClickLinkage?: boolean;
74
+
75
+ /**
76
+ * 是否启用缩放联动
77
+ */
78
+ enableZoomLinkage?: boolean;
79
+
80
+ /**
81
+ * 是否启用图例联动
82
+ */
83
+ enableLegendLinkage?: boolean;
84
+
85
+ /**
86
+ * 是否启用数据筛选联动
87
+ */
88
+ enableFilterLinkage?: boolean;
89
+ }
90
+
91
+ /**
92
+ * 图表组件基础属性
93
+ *
94
+ * @interface ChartProps
95
+ * @description 定义了所有图表组件的基础属性
96
+ */
97
+ export interface ChartProps {
98
+ /**
99
+ * 图表ID,用于图表联动和实例管理
100
+ */
101
+ chartId?: string;
102
+
103
+ /**
104
+ * 图表配置项
105
+ *
106
+ * @type {EChartsOption}
107
+ * @description 符合 ECharts 规范的图表配置对象
108
+ */
109
+ option?: EChartsOption;
110
+
111
+ /**
112
+ * 动画配置
113
+ *
114
+ * @type {AnimationConfig}
115
+ * @description 图表动画配置,支持自定义动画时长、缓动函数等
116
+ */
117
+ animation?: AnimationConfig;
118
+
119
+ /**
120
+ * 调试配置
121
+ *
122
+ * @type {boolean | DebugPanelOptions}
123
+ * @description 是否启用调试面板或调试面板配置
124
+ */
125
+ debug?: boolean | DebugPanelOptions;
126
+
127
+ /**
128
+ * 图表宽度
129
+ *
130
+ * @type {number | string}
131
+ * @default '100%'
132
+ * @description 图表的宽度,可以是像素值或百分比
133
+ */
134
+ width?: number | string;
135
+
136
+ /**
137
+ * 图表高度
138
+ *
139
+ * @type {number | string}
140
+ * @default '300px'
141
+ * @description 图表的高度,可以是像素值或百分比
142
+ */
143
+ height?: number | string;
144
+
145
+ /**
146
+ * 图表主题
147
+ *
148
+ * @type {string | object}
149
+ * @description 图表的主题,可以是内置主题名称或自定义主题对象
150
+ */
151
+ theme?: string | object;
152
+
153
+ /**
154
+ * 是否自动调整大小
155
+ *
156
+ * @type {boolean}
157
+ * @default true
158
+ * @description 当窗口大小变化时,是否自动调整图表大小
159
+ */
160
+ autoResize?: boolean;
161
+
162
+ /**
163
+ * 布局方向
164
+ *
165
+ * @type {'ltr' | 'rtl'}
166
+ * @default 'ltr'
167
+ * @description 图表的布局方向,支持从左到右(ltr)和从右到左(rtl)
168
+ */
169
+ direction?: 'ltr' | 'rtl';
170
+
171
+ /**
172
+ * 初始化回调
173
+ *
174
+ * @type {(instance: EChartsType) => void}
175
+ * @description 图表初始化完成后的回调函数,返回 ECharts 实例
176
+ */
177
+ onInit?: (instance: EChartsType) => void;
178
+
179
+ /**
180
+ * 图表点击事件
181
+ *
182
+ * @type {(params: ChartEventParams) => void}
183
+ * @description 图表点击事件的回调函数
184
+ */
185
+ onClick?: (params: ChartEventParams) => void;
186
+
187
+ /**
188
+ * 图表数据变化事件
189
+ *
190
+ * @type {(params: ChartEventParams) => void}
191
+ * @description 图表数据缩放事件的回调函数
192
+ */
193
+ onDataZoom?: (params: ChartEventParams) => void;
194
+
195
+ /**
196
+ * 样式属性
197
+ *
198
+ * @type {React.CSSProperties}
199
+ * @description 图表容器的 CSS 样式
200
+ */
201
+ style?: React.CSSProperties;
202
+
203
+ /**
204
+ * CSS类名
205
+ *
206
+ * @type {string}
207
+ * @description 图表容器的 CSS 类名
208
+ */
209
+ className?: string;
210
+
211
+ /**
212
+ * 子元素
213
+ *
214
+ * @type {React.ReactNode}
215
+ * @description 图表容器的子元素
216
+ */
217
+ children?: React.ReactNode;
218
+
219
+ /**
220
+ * 是否启用虚拟滚动
221
+ *
222
+ * @type {boolean}
223
+ * @default false
224
+ * @description 当数据量较大时,启用虚拟滚动可以提高性能
225
+ */
226
+ virtualScroll?: boolean;
227
+
228
+ /**
229
+ * 虚拟滚动的每页数据量
230
+ *
231
+ * @type {number}
232
+ * @default 100
233
+ * @description 每次渲染的数据量
234
+ */
235
+ virtualScrollPageSize?: number;
236
+
237
+ /**
238
+ * 虚拟滚动的预加载数量
239
+ *
240
+ * @type {number}
241
+ * @default 50
242
+ * @description 预加载的数据量,用于平滑滚动
243
+ */
244
+ virtualScrollPreloadSize?: number;
245
+
246
+ /**
247
+ * 是否启用性能监控
248
+ *
249
+ * @type {boolean}
250
+ * @default false
251
+ * @description 启用后会收集图表渲染性能数据
252
+ */
253
+ enablePerformanceMonitoring?: boolean;
254
+
255
+ /**
256
+ * 性能监控回调函数
257
+ *
258
+ * @type {(performanceData: { renderTime: number; initTime: number; updateTime: number; dataSize: number }) => void}
259
+ * @description 当图表渲染、初始化或更新时调用,返回性能数据
260
+ */
261
+ onPerformance?: (performanceData: {
262
+ renderTime: number;
263
+ initTime: number;
264
+ updateTime: number;
265
+ dataSize: number;
266
+ }) => void;
267
+
268
+ /**
269
+ * 是否启用图表缩放功能
270
+ *
271
+ * @type {boolean}
272
+ * @default false
273
+ * @description 启用后允许用户通过鼠标滚轮或触摸手势缩放图表
274
+ */
275
+ enableZoom?: boolean;
276
+
277
+ /**
278
+ * 缩放事件回调函数
279
+ *
280
+ * @type {(zoomData: { start: number; end: number; dataZoomIndex: number }) => void}
281
+ * @description 当图表缩放时调用,返回缩放数据
282
+ */
283
+ onZoom?: (zoomData: { start: number; end: number; dataZoomIndex: number }) => void;
284
+
285
+ /**
286
+ * 是否启用数据筛选功能
287
+ *
288
+ * @type {boolean}
289
+ * @default false
290
+ * @description 启用后允许用户筛选图表数据
291
+ */
292
+ enableDataFiltering?: boolean;
293
+
294
+ /**
295
+ * 当前筛选条件
296
+ *
297
+ * @type {Record<string, any>}
298
+ * @description 用于筛选图表数据的条件
299
+ */
300
+ filters?: Record<string, any>;
301
+
302
+ /**
303
+ * 数据筛选回调函数
304
+ *
305
+ * @type {(filteredData: any[], filters: Record<string, any>) => void}
306
+ * @description 当数据筛选时调用,返回筛选后的数据和筛选条件
307
+ */
308
+ onDataFiltered?: (filteredData: any[], filters: Record<string, any>) => void;
309
+
310
+ /**
311
+ * 是否启用增强图例交互
312
+ *
313
+ * @type {boolean}
314
+ * @default false
315
+ * @description 启用后允许用户通过图例进行更丰富的交互操作
316
+ */
317
+ enableLegendInteraction?: boolean;
318
+
319
+ /**
320
+ * 图例交互模式
321
+ *
322
+ * @type {'single' | 'multiple' | 'all'}
323
+ * @default 'single'
324
+ * @description 图例交互模式:single(单选)、multiple(多选)、all(全选/反选)
325
+ */
326
+ legendInteractionMode?: 'single' | 'multiple' | 'all';
327
+
328
+ /**
329
+ * 图例选择回调函数
330
+ *
331
+ * @type {(params: { name: string; selected: Record<string, boolean> }) => void}
332
+ * @description 当图例项被选择时调用
333
+ */
334
+ onLegendSelect?: (params: { name: string; selected: Record<string, boolean> }) => void;
335
+
336
+ /**
337
+ * 图例取消选择回调函数
338
+ *
339
+ * @type {(params: { name: string; selected: Record<string, boolean> }) => void}
340
+ * @description 当图例项被取消选择时调用
341
+ */
342
+ onLegendUnselect?: (params: { name: string; selected: Record<string, boolean> }) => void;
343
+
344
+ /**
345
+ * 图例全选回调函数
346
+ *
347
+ * @type {(params: { selected: Record<string, boolean> }) => void}
348
+ * @description 当图例全选时调用
349
+ */
350
+ onLegendSelectAll?: (params: { selected: Record<string, boolean> }) => void;
351
+
352
+ /**
353
+ * 图例反选回调函数
354
+ *
355
+ * @type {(params: { selected: Record<string, boolean> }) => void}
356
+ * @description 当图例反选时调用
357
+ */
358
+ onLegendInverseSelect?: (params: { selected: Record<string, boolean> }) => void;
359
+
360
+ /**
361
+ * 是否启用自定义提示框
362
+ *
363
+ * @type {boolean}
364
+ * @default false
365
+ * @description 启用后允许自定义提示框内容和样式
366
+ */
367
+ enableCustomTooltip?: boolean;
368
+
369
+ /**
370
+ * 自定义提示框内容
371
+ *
372
+ * @type {(params: any) => React.ReactNode}
373
+ * @description 自定义提示框内容的渲染函数
374
+ */
375
+ customTooltipContent?: (params: any) => React.ReactNode;
376
+
377
+ /**
378
+ * 自定义提示框样式
379
+ *
380
+ * @type {React.CSSProperties}
381
+ * @description 自定义提示框的样式
382
+ */
383
+ customTooltipStyle?: React.CSSProperties;
384
+
385
+ /**
386
+ * 提示框显示事件回调函数
387
+ *
388
+ * @type {(params: any) => void}
389
+ * @description 当提示框显示时调用
390
+ */
391
+ onTooltipShow?: (params: any) => void;
392
+
393
+ /**
394
+ * 提示框隐藏事件回调函数
395
+ *
396
+ * @type {(params: any) => void}
397
+ * @description 当提示框隐藏时调用
398
+ */
399
+ onTooltipHide?: (params: any) => void;
400
+
401
+ /**
402
+ * 图表导出回调函数
403
+ *
404
+ * @type {(dataURL: string, options: ChartExportOptions) => void}
405
+ * @description 当图表导出完成时调用,返回导出的数据URL和选项
406
+ */
407
+ onExport?: (dataURL: string, options: ChartExportOptions) => void;
408
+
409
+ /**
410
+ * 图表联动配置
411
+ */
412
+ linkageConfig?: ChartLinkageConfig;
413
+
414
+ /**
415
+ * 数据更新回调函数
416
+ *
417
+ * @type {(oldOption: EChartsOption | undefined, newOption: EChartsOption | undefined) => void}
418
+ * @description 当图表数据更新时调用,返回旧的配置和新的配置
419
+ */
420
+ onDataUpdate?: (
421
+ oldOption: EChartsOption | undefined,
422
+ newOption: EChartsOption | undefined
423
+ ) => void;
424
+
425
+ /**
426
+ * 数据更新监听选项
427
+ */
428
+ dataUpdateOptions?: {
429
+ /**
430
+ * 是否启用数据更新监听
431
+ */
432
+ enabled?: boolean;
433
+
434
+ /**
435
+ * 数据更新监听的深度
436
+ */
437
+ deepCompare?: boolean;
438
+
439
+ /**
440
+ * 防抖延迟(毫秒)
441
+ */
442
+ debounceDelay?: number;
443
+ };
444
+ }
445
+
446
+ /**
447
+ * 基础图表组件
448
+ *
449
+ * @param {ChartProps} props - 组件属性
450
+ * @returns {JSX.Element} - 返回的 JSX 元素
451
+ *
452
+ * @description 基础图表组件,所有具体图表组件的基类
453
+ * 负责处理图表的初始化、事件绑定、主题设置等核心逻辑
454
+ */
455
+ const BaseChart: React.FC<ChartProps> = props => {
456
+ const {
457
+ chartId,
458
+ option,
459
+ width = '100%',
460
+ height = '300px',
461
+ theme,
462
+ autoResize = true,
463
+ direction = 'ltr',
464
+ onInit,
465
+ onClick,
466
+ onDataZoom,
467
+ style,
468
+ className,
469
+ children,
470
+ virtualScroll = false,
471
+ virtualScrollPageSize = 100,
472
+ virtualScrollPreloadSize = 50,
473
+ enablePerformanceMonitoring = false,
474
+ onPerformance,
475
+ enableZoom = false,
476
+ onZoom,
477
+ enableDataFiltering = false,
478
+ filters = {},
479
+ onDataFiltered,
480
+ enableLegendInteraction = false,
481
+ legendInteractionMode = 'single',
482
+ onLegendSelect,
483
+ onLegendUnselect,
484
+ onLegendSelectAll,
485
+ onLegendInverseSelect,
486
+ enableCustomTooltip = false,
487
+ customTooltipContent,
488
+ customTooltipStyle,
489
+ onTooltipShow,
490
+ onTooltipHide,
491
+ onExport,
492
+ linkageConfig = {},
493
+ onDataUpdate,
494
+ dataUpdateOptions = {},
495
+ } = props;
496
+
497
+ /**
498
+ * 图表容器的引用
499
+ */
500
+ const chartRef = useRef<HTMLDivElement>(null);
501
+
502
+ /**
503
+ * 图表适配器的引用
504
+ */
505
+ const adapterRef = useRef<any>(null);
506
+
507
+ /**
508
+ * 虚拟滚动状态
509
+ */
510
+ const virtualScrollRef = useRef({
511
+ currentPage: 0,
512
+ totalPages: 1,
513
+ totalDataCount: 0,
514
+ startIndex: 0,
515
+ endIndex: 0,
516
+ isScrolling: false,
517
+ });
518
+
519
+ /**
520
+ * 性能分析器实例引用
521
+ */
522
+ const performanceAnalyzerRef = useRef<PerformanceAnalyzer | null>(null);
523
+
524
+ /**
525
+ * 性能监控状态
526
+ */
527
+ const performanceRef = useRef({
528
+ initStartTime: 0,
529
+ initEndTime: 0,
530
+ renderStartTime: 0,
531
+ renderEndTime: 0,
532
+ updateStartTime: 0,
533
+ updateEndTime: 0,
534
+ dataSize: 0,
535
+ });
536
+
537
+ /**
538
+ * 旧的图表配置引用,用于数据更新监听
539
+ */
540
+ const oldOptionRef = useRef<EChartsOption | undefined>(option);
541
+
542
+ /**
543
+ * 调试配置引用
544
+ */
545
+ const debugConfigRef = useRef<DebugPanelOptions | null>(null);
546
+
547
+ /**
548
+ * 处理调试配置
549
+ */
550
+ const processDebugConfig = (): DebugPanelOptions | null => {
551
+ if (!props.debug) {
552
+ return null;
553
+ }
554
+
555
+ if (typeof props.debug === 'boolean') {
556
+ return {
557
+ enabled: props.debug,
558
+ autoExpand: false,
559
+ };
560
+ }
561
+
562
+ return {
563
+ enabled: true,
564
+ ...props.debug,
565
+ };
566
+ };
567
+
568
+ /**
569
+ * 导出图表为DataURL
570
+ * @param options 导出选项
571
+ * @returns 导出的数据URL
572
+ */
573
+ const exportChartToDataURL = (options: ChartExportOptions = {}): string | undefined => {
574
+ if (!adapterRef.current) {
575
+ console.error('Chart adapter not initialized');
576
+ return undefined;
577
+ }
578
+
579
+ const { type = 'png', pixelRatio = 2, backgroundColor = 'transparent' } = options;
580
+
581
+ const dataURL = adapterRef.current.convertToDataURL({
582
+ type,
583
+ pixelRatio,
584
+ backgroundColor,
585
+ });
586
+
587
+ // 触发导出回调
588
+ if (onExport && dataURL) {
589
+ onExport(dataURL, options);
590
+ }
591
+
592
+ return dataURL;
593
+ };
594
+
595
+ /**
596
+ * 导出图表并下载
597
+ * @param options 导出选项
598
+ */
599
+ const exportChart = (options: ChartExportOptions = {}): void => {
600
+ const dataURL = exportChartToDataURL(options);
601
+ if (!dataURL) {
602
+ console.error('Failed to export chart');
603
+ return;
604
+ }
605
+
606
+ const { type = 'png', filename = `chart-${Date.now()}` } = options;
607
+ const fullFilename = `${filename}.${type}`;
608
+
609
+ // 创建下载链接
610
+ const link = document.createElement('a');
611
+ link.download = fullFilename;
612
+ link.href = dataURL;
613
+ link.click();
614
+ };
615
+
616
+ /**
617
+ * 组件实例引用,用于暴露公共方法
618
+ */
619
+ const chartInstanceRef = useRef({
620
+ exportChartToDataURL,
621
+ exportChart,
622
+ });
623
+
624
+ // 暴露组件实例方法
625
+ React.useImperativeHandle(props as any, () => chartInstanceRef.current);
626
+
627
+ /**
628
+ * 记录性能数据
629
+ * @param type 性能数据类型
630
+ * @param data 性能数据
631
+ */
632
+ const recordPerformance = (type: 'init' | 'render' | 'update', data?: any) => {
633
+ const now = Date.now();
634
+
635
+ switch (type) {
636
+ case 'init':
637
+ if (!performanceRef.current.initStartTime) {
638
+ performanceRef.current.initStartTime = now;
639
+ } else {
640
+ performanceRef.current.initEndTime = now;
641
+ const initTime =
642
+ performanceRef.current.initEndTime - performanceRef.current.initStartTime;
643
+
644
+ // 计算数据大小
645
+ const dataSize = JSON.stringify(option).length;
646
+
647
+ // 使用性能分析器记录数据
648
+ if (performanceAnalyzerRef.current) {
649
+ performanceAnalyzerRef.current.recordInitTime(initTime);
650
+ performanceAnalyzerRef.current.recordDataSize(option);
651
+ }
652
+
653
+ // 触发性能回调
654
+ if (onPerformance) {
655
+ onPerformance({
656
+ renderTime: 0,
657
+ initTime,
658
+ updateTime: 0,
659
+ dataSize,
660
+ });
661
+ }
662
+ }
663
+ break;
664
+
665
+ case 'render':
666
+ if (!performanceRef.current.renderStartTime) {
667
+ performanceRef.current.renderStartTime = now;
668
+ } else {
669
+ performanceRef.current.renderEndTime = now;
670
+ const renderTime =
671
+ performanceRef.current.renderEndTime - performanceRef.current.renderStartTime;
672
+
673
+ // 计算数据大小
674
+ const dataSize = JSON.stringify(option).length;
675
+
676
+ // 使用性能分析器记录数据
677
+ if (performanceAnalyzerRef.current) {
678
+ performanceAnalyzerRef.current.recordRenderTime(renderTime);
679
+ }
680
+
681
+ // 触发性能回调
682
+ if (onPerformance) {
683
+ onPerformance({
684
+ renderTime,
685
+ initTime: performanceRef.current.initEndTime - performanceRef.current.initStartTime,
686
+ updateTime: 0,
687
+ dataSize,
688
+ });
689
+ }
690
+ }
691
+ break;
692
+
693
+ case 'update':
694
+ if (!performanceRef.current.updateStartTime) {
695
+ performanceRef.current.updateStartTime = now;
696
+ } else {
697
+ performanceRef.current.updateEndTime = now;
698
+ const updateTime =
699
+ performanceRef.current.updateEndTime - performanceRef.current.updateStartTime;
700
+
701
+ // 计算数据大小
702
+ const dataSize = JSON.stringify(option).length;
703
+
704
+ // 使用性能分析器记录数据
705
+ if (performanceAnalyzerRef.current) {
706
+ performanceAnalyzerRef.current.recordUpdateTime(updateTime);
707
+ performanceAnalyzerRef.current.recordDataSize(option);
708
+ }
709
+
710
+ // 触发性能回调
711
+ if (onPerformance) {
712
+ onPerformance({
713
+ renderTime: 0,
714
+ initTime: 0,
715
+ updateTime,
716
+ dataSize,
717
+ });
718
+ }
719
+ }
720
+ break;
721
+ }
722
+ };
723
+
724
+ /**
725
+ * 筛选数据
726
+ * @param data 原始数据
727
+ * @param filters 筛选条件
728
+ * @returns 筛选后的数据
729
+ */
730
+ const filterData = (data: any[], filters: Record<string, any>): any[] => {
731
+ if (!enableDataFiltering || !filters || Object.keys(filters).length === 0) {
732
+ return data;
733
+ }
734
+
735
+ return data.filter(item => {
736
+ // 遍历所有筛选条件
737
+ for (const [key, value] of Object.entries(filters)) {
738
+ // 检查数据项是否满足筛选条件
739
+ if (item[key] !== value && !item[key]?.includes?.(value)) {
740
+ return false;
741
+ }
742
+ }
743
+ return true;
744
+ });
745
+ };
746
+
747
+ /**
748
+ * 计算数据长度,用于动画性能优化
749
+ */
750
+ const calculateDataLength = (option: EChartsOption): number => {
751
+ let count = 0;
752
+
753
+ // 计算系列数据长度
754
+ if (option.series) {
755
+ const series = Array.isArray(option.series) ? option.series : [option.series];
756
+ series.forEach((seriesItem: any) => {
757
+ if (seriesItem.data) {
758
+ if (Array.isArray(seriesItem.data)) {
759
+ count += seriesItem.data.length;
760
+ } else if (typeof seriesItem.data === 'object') {
761
+ // 处理对象类型数据
762
+ count += Object.keys(seriesItem.data).length;
763
+ }
764
+ }
765
+ });
766
+ }
767
+
768
+ return count;
769
+ };
770
+
771
+ /**
772
+ * 处理大数据集的虚拟滚动和数据筛选
773
+ * @param originalOption 原始图表配置
774
+ * @returns 处理后的图表配置,只包含当前页的数据和筛选后的数据
775
+ */
776
+ const processVirtualScroll = (originalOption: EChartsOption): EChartsOption => {
777
+ if (!originalOption) {
778
+ return originalOption;
779
+ }
780
+
781
+ // 深拷贝原始配置,避免修改原始数据
782
+ const processedOption = JSON.parse(JSON.stringify(originalOption)) as EChartsOption;
783
+
784
+ // 处理series数据
785
+ if (processedOption.series && Array.isArray(processedOption.series)) {
786
+ // 使用类型断言解决类型不匹配问题
787
+ (processedOption.series as any) = processedOption.series.map((series: any) => {
788
+ if (series.data && Array.isArray(series.data)) {
789
+ const data = series.data;
790
+
791
+ // 应用数据筛选
792
+ const filteredData = filterData(data, filters);
793
+
794
+ // 触发数据筛选回调
795
+ if (onDataFiltered) {
796
+ onDataFiltered(filteredData, filters);
797
+ }
798
+
799
+ // 应用虚拟滚动
800
+ if (virtualScroll) {
801
+ virtualScrollRef.current.totalDataCount = filteredData.length;
802
+ virtualScrollRef.current.totalPages = Math.ceil(
803
+ filteredData.length / virtualScrollPageSize
804
+ );
805
+
806
+ // 计算当前页的起始和结束索引
807
+ const startIndex = virtualScrollRef.current.currentPage * virtualScrollPageSize;
808
+ const endIndex = Math.min(
809
+ startIndex + virtualScrollPageSize + virtualScrollPreloadSize,
810
+ filteredData.length
811
+ );
812
+
813
+ virtualScrollRef.current.startIndex = startIndex;
814
+ virtualScrollRef.current.endIndex = endIndex;
815
+
816
+ // 返回只包含当前页数据的series
817
+ return {
818
+ ...series,
819
+ data: filteredData.slice(startIndex, endIndex),
820
+ };
821
+ }
822
+
823
+ // 只应用数据筛选,不应用虚拟滚动
824
+ return {
825
+ ...series,
826
+ data: filteredData,
827
+ };
828
+ }
829
+ return series;
830
+ });
831
+ }
832
+
833
+ // 生成动画配置
834
+ const dataLength = calculateDataLength(processedOption);
835
+ const animationOption = generateEChartsAnimationConfig(props.animation, dataLength);
836
+
837
+ // 合并动画配置到图表选项
838
+ return {
839
+ ...processedOption,
840
+ ...animationOption,
841
+ };
842
+ };
843
+
844
+ /**
845
+ * 初始化图表的 useEffect
846
+ *
847
+ * @description 当图表容器 ref 变化时,初始化图表适配器和图表实例
848
+ * 负责图表的创建、事件绑定和清理工作
849
+ */
850
+ useEffect(() => {
851
+ if (chartRef.current) {
852
+ // 处理调试配置
853
+ const debugConfig = processDebugConfig();
854
+ debugConfigRef.current = debugConfig;
855
+
856
+ // 初始化性能分析器
857
+ if (enablePerformanceMonitoring) {
858
+ performanceAnalyzerRef.current = PerformanceAnalyzer.getInstance({
859
+ enabled: true,
860
+ metrics: ['initTime', 'renderTime', 'updateTime', 'dataSize', 'frameRate'],
861
+ sampleInterval: 1000,
862
+ maxSamples: 100,
863
+ realTime: true,
864
+ autoStart: true,
865
+ });
866
+ }
867
+
868
+ // 开始记录初始化时间
869
+ recordPerformance('init');
870
+
871
+ // 处理虚拟滚动
872
+ const processedOption = processVirtualScroll(option as EChartsOption);
873
+
874
+ // 获取适配器实例
875
+ const chartAdapter = getAdapter({
876
+ width,
877
+ height,
878
+ theme,
879
+ option: processedOption,
880
+ onInit,
881
+ containerRef: chartRef,
882
+ direction,
883
+ });
884
+
885
+ // 设置组件实例(针对小程序环境)
886
+ if (typeof chartAdapter.setComponent === 'function') {
887
+ chartAdapter.setComponent({
888
+ createChart: (_config: Record<string, any>) => {
889
+ // 这里应该根据具体平台实现创建图表的逻辑
890
+ return {};
891
+ },
892
+ });
893
+ }
894
+
895
+ // 开始记录渲染时间
896
+ recordPerformance('render');
897
+
898
+ // 初始化图表
899
+ const instance = chartAdapter.init();
900
+
901
+ // 结束记录渲染时间
902
+ recordPerformance('render');
903
+
904
+ // 注册图表实例
905
+ if (chartId && instance) {
906
+ registerChart(chartId, instance);
907
+ }
908
+
909
+ // 更新调试信息
910
+ if (debugConfig?.enabled) {
911
+ // 更新实例信息
912
+ updateDebugInfo({
913
+ instance: {
914
+ id: chartId,
915
+ type: 'ECharts',
916
+ renderer: 'canvas', // 简化处理,使用默认值
917
+ width: typeof width === 'number' ? width : undefined,
918
+ height: typeof height === 'number' ? height : undefined,
919
+ platform: 'web', // 简化处理,使用默认值
920
+ },
921
+ config: processedOption,
922
+ data: {
923
+ series: Array.isArray(processedOption.series) ? processedOption.series : [],
924
+ totalDataCount: calculateDataLength(processedOption),
925
+ currentDataCount: calculateDataLength(processedOption),
926
+ },
927
+ performance: {
928
+ initTime: performanceRef.current.initEndTime - performanceRef.current.initStartTime,
929
+ renderTime:
930
+ performanceRef.current.renderEndTime - performanceRef.current.renderStartTime,
931
+ dataSize: JSON.stringify(processedOption).length,
932
+ },
933
+ });
934
+ }
935
+
936
+ // 绑定事件
937
+ if (instance && onClick) {
938
+ instance.on('click', (params: any) => {
939
+ onClick(params as any);
940
+
941
+ // 点击联动
942
+ if (linkageConfig.enableClickLinkage && chartId && linkageConfig.linkedChartIds) {
943
+ linkageConfig.linkedChartIds.forEach(linkedChartId => {
944
+ const linkedChart = getChart(linkedChartId);
945
+ if (linkedChart) {
946
+ // 这里可以根据需要实现点击联动逻辑
947
+ // 例如:高亮联动图表中对应的系列或数据点
948
+ linkedChart.dispatchAction({
949
+ type: 'highlight',
950
+ name: params.name,
951
+ });
952
+ }
953
+ });
954
+ }
955
+ });
956
+ }
957
+
958
+ // 绑定datazoom事件
959
+ if (instance) {
960
+ instance.on('datazoom', (params: any) => {
961
+ // 触发原始的datazoom事件
962
+ if (onDataZoom) {
963
+ onDataZoom(params as any);
964
+ }
965
+
966
+ // 触发缩放事件
967
+ if (onZoom) {
968
+ onZoom({
969
+ start: params.start || 0,
970
+ end: params.end || 100,
971
+ dataZoomIndex: params.dataZoomIndex || 0,
972
+ });
973
+ }
974
+
975
+ // 缩放联动
976
+ if (linkageConfig.enableZoomLinkage && chartId && linkageConfig.linkedChartIds) {
977
+ linkageConfig.linkedChartIds.forEach(linkedChartId => {
978
+ const linkedChart = getChart(linkedChartId);
979
+ if (linkedChart) {
980
+ linkedChart.dispatchAction({
981
+ type: 'dataZoom',
982
+ start: params.start,
983
+ end: params.end,
984
+ dataZoomIndex: params.dataZoomIndex,
985
+ });
986
+ }
987
+ });
988
+ }
989
+
990
+ // 处理虚拟滚动
991
+ if (virtualScroll) {
992
+ if (virtualScrollRef.current.isScrolling) {
993
+ return;
994
+ }
995
+
996
+ virtualScrollRef.current.isScrolling = true;
997
+
998
+ // 根据滚动位置计算当前页码
999
+ const scrollPercent = params.start || 0;
1000
+ const newPage = Math.floor((scrollPercent / 100) * virtualScrollRef.current.totalPages);
1001
+
1002
+ if (newPage !== virtualScrollRef.current.currentPage) {
1003
+ virtualScrollRef.current.currentPage = newPage;
1004
+
1005
+ // 更新图表数据
1006
+ const updatedOption = processVirtualScroll(option as EChartsOption);
1007
+ chartAdapter.setOption(updatedOption);
1008
+ }
1009
+
1010
+ // 延迟重置滚动状态,避免频繁触发
1011
+ setTimeout(() => {
1012
+ virtualScrollRef.current.isScrolling = false;
1013
+ }, 100);
1014
+ }
1015
+ });
1016
+ }
1017
+
1018
+ // 启用图表缩放功能
1019
+ if (instance && enableZoom) {
1020
+ // 这里可以根据需要添加更多缩放相关的配置
1021
+ // 例如:instance.setOption({ dataZoom: [{ type: 'inside', start: 0, end: 100 }] });
1022
+ }
1023
+
1024
+ // 增强图例交互功能
1025
+ if (instance && enableLegendInteraction) {
1026
+ // 图例选择事件
1027
+ instance.on('legendselectchanged', (params: any) => {
1028
+ const { name, selected } = params;
1029
+
1030
+ // 图例联动
1031
+ if (linkageConfig.enableLegendLinkage && chartId && linkageConfig.linkedChartIds) {
1032
+ linkageConfig.linkedChartIds.forEach(linkedChartId => {
1033
+ const linkedChart = getChart(linkedChartId);
1034
+ if (linkedChart) {
1035
+ linkedChart.setOption({ legend: { selected } });
1036
+ }
1037
+ });
1038
+ }
1039
+
1040
+ // 根据交互模式处理图例选择
1041
+ if (legendInteractionMode === 'single') {
1042
+ // 单选模式:只显示当前选中项
1043
+ const newSelected: Record<string, boolean> = {};
1044
+ Object.keys(selected).forEach(key => {
1045
+ newSelected[key] = key === name;
1046
+ });
1047
+ instance.setOption({ legend: { selected: newSelected } });
1048
+
1049
+ // 触发回调
1050
+ if (onLegendSelect) {
1051
+ onLegendSelect({ name, selected: newSelected });
1052
+ }
1053
+ } else {
1054
+ // 多选或全选模式:保持原有选择
1055
+ if (selected[name]) {
1056
+ // 选择图例项
1057
+ if (onLegendSelect) {
1058
+ onLegendSelect({ name, selected });
1059
+ }
1060
+ } else {
1061
+ // 取消选择图例项
1062
+ if (onLegendUnselect) {
1063
+ onLegendUnselect({ name, selected });
1064
+ }
1065
+ }
1066
+ }
1067
+ });
1068
+
1069
+ // 图例全选功能
1070
+ if (legendInteractionMode === 'all') {
1071
+ // 这里可以添加全选/反选的逻辑
1072
+ // 例如:监听特定事件或添加自定义按钮
1073
+ }
1074
+ }
1075
+
1076
+ // 自定义提示框功能
1077
+ if (instance && enableCustomTooltip) {
1078
+ // 提示框显示事件
1079
+ instance.on('tooltipshow', (params: any) => {
1080
+ // 触发提示框显示回调
1081
+ if (onTooltipShow) {
1082
+ onTooltipShow(params);
1083
+ }
1084
+ });
1085
+
1086
+ // 提示框隐藏事件
1087
+ instance.on('tooltiphide', (params: any) => {
1088
+ // 触发提示框隐藏回调
1089
+ if (onTooltipHide) {
1090
+ onTooltipHide(params);
1091
+ }
1092
+ });
1093
+
1094
+ // 提示框格式化功能
1095
+ if (customTooltipContent) {
1096
+ // 这里可以添加自定义提示框的格式化逻辑
1097
+ // 例如:使用ECharts的tooltip.formatter选项
1098
+ instance.setOption({
1099
+ tooltip: {
1100
+ formatter: (params: any) => {
1101
+ // 这里可以返回自定义的HTML内容
1102
+ // 由于ECharts的tooltip.formatter只支持返回字符串,我们需要将React组件转换为HTML字符串
1103
+ // 注意:这种方式有局限性,更复杂的自定义提示框可能需要使用其他方案
1104
+ return (
1105
+ '<div style="background: white; padding: 10px; border: 1px solid #ccc;">' +
1106
+ JSON.stringify(params) +
1107
+ '</div>'
1108
+ );
1109
+ },
1110
+ ...(customTooltipStyle && {
1111
+ backgroundColor: 'transparent',
1112
+ borderColor: 'transparent',
1113
+ textStyle: {},
1114
+ }),
1115
+ },
1116
+ });
1117
+ }
1118
+ }
1119
+
1120
+ adapterRef.current = chartAdapter;
1121
+
1122
+ // 结束记录初始化时间
1123
+ recordPerformance('init');
1124
+
1125
+ // 清理函数
1126
+ return () => {
1127
+ // 移除图表实例
1128
+ if (chartId) {
1129
+ removeChart(chartId);
1130
+ }
1131
+
1132
+ // 停止性能监控
1133
+ if (performanceAnalyzerRef.current) {
1134
+ performanceAnalyzerRef.current.stop();
1135
+ performanceAnalyzerRef.current = null;
1136
+ }
1137
+
1138
+ if (chartAdapter) {
1139
+ chartAdapter.dispose();
1140
+ }
1141
+ };
1142
+ }
1143
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1144
+ }, [chartRef, width, height, theme, option, onInit, onClick, onDataZoom, chartId, linkageConfig]);
1145
+
1146
+ /**
1147
+ * 更新图表尺寸的 useEffect
1148
+ *
1149
+ * @description 当图表宽度或高度变化时,调整图表大小
1150
+ */
1151
+ useEffect(() => {
1152
+ if (adapterRef.current) {
1153
+ adapterRef.current.resize();
1154
+ }
1155
+ }, [width, height]);
1156
+
1157
+ /**
1158
+ * 比较两个配置是否相同
1159
+ * @param oldOption 旧的配置
1160
+ * @param newOption 新的配置
1161
+ * @returns 是否相同
1162
+ */
1163
+ const isOptionEqual = (
1164
+ oldOption: EChartsOption | undefined,
1165
+ newOption: EChartsOption | undefined
1166
+ ): boolean => {
1167
+ if (oldOption === newOption) {
1168
+ return true;
1169
+ }
1170
+
1171
+ if (!oldOption || !newOption) {
1172
+ return false;
1173
+ }
1174
+
1175
+ if (dataUpdateOptions.deepCompare) {
1176
+ // 深度比较
1177
+ return JSON.stringify(oldOption) === JSON.stringify(newOption);
1178
+ } else {
1179
+ // 浅比较,只比较引用
1180
+ return oldOption === newOption;
1181
+ }
1182
+ };
1183
+
1184
+ /**
1185
+ * 更新图表配置的 useEffect
1186
+ *
1187
+ * @description 当图表配置项变化时,更新图表
1188
+ */
1189
+ useEffect(() => {
1190
+ if (adapterRef.current && option) {
1191
+ // 开始记录更新时间
1192
+ recordPerformance('update');
1193
+
1194
+ // 处理虚拟滚动
1195
+ const processedOption = processVirtualScroll(option);
1196
+ adapterRef.current.setOption(processedOption);
1197
+
1198
+ // 结束记录更新时间
1199
+ recordPerformance('update');
1200
+ }
1201
+
1202
+ // 数据更新监听
1203
+ if (onDataUpdate && dataUpdateOptions.enabled !== false) {
1204
+ const oldOption = oldOptionRef.current;
1205
+ if (!isOptionEqual(oldOption, option)) {
1206
+ onDataUpdate(oldOption, option);
1207
+ // 更新旧的配置引用
1208
+ oldOptionRef.current = option;
1209
+ }
1210
+ }
1211
+ }, [option, onDataUpdate, dataUpdateOptions]);
1212
+
1213
+ /**
1214
+ * 更新图表主题的 useEffect
1215
+ *
1216
+ * @description 当图表主题变化时,更新图表主题
1217
+ */
1218
+ useEffect(() => {
1219
+ if (adapterRef.current && theme) {
1220
+ adapterRef.current.setTheme(theme);
1221
+ }
1222
+ }, [theme]);
1223
+
1224
+ /**
1225
+ * 处理窗口大小变化的 useEffect
1226
+ *
1227
+ * @description 当窗口大小变化时,如果开启了自动调整大小,则调整图表大小
1228
+ */
1229
+ useEffect(() => {
1230
+ if (!autoResize || !adapterRef.current) {
1231
+ return;
1232
+ }
1233
+
1234
+ const handleResize = () => {
1235
+ adapterRef.current.resize();
1236
+ };
1237
+
1238
+ window.addEventListener('resize', handleResize);
1239
+
1240
+ return () => {
1241
+ window.removeEventListener('resize', handleResize);
1242
+ };
1243
+ }, [autoResize]);
1244
+
1245
+ /**
1246
+ * 合并后的样式对象
1247
+ *
1248
+ * @description 合并用户传入的样式和组件默认样式
1249
+ */
1250
+ const mergedStyle = {
1251
+ width: typeof width === 'number' ? `${width}px` : width,
1252
+ height: typeof height === 'number' ? `${height}px` : height,
1253
+ direction,
1254
+ ...style,
1255
+ };
1256
+
1257
+ /**
1258
+ * 组件渲染函数
1259
+ *
1260
+ * @returns {JSX.Element} - 返回渲染后的 JSX 元素
1261
+ * @description 渲染图表容器,并将 chartRef 绑定到容器上
1262
+ */
1263
+ const debugConfig = processDebugConfig();
1264
+
1265
+ // 渲染图表容器
1266
+ const chartContainer = React.createElement(
1267
+ 'div',
1268
+ {
1269
+ ref: chartRef,
1270
+ style: mergedStyle,
1271
+ className,
1272
+ },
1273
+ children
1274
+ );
1275
+
1276
+ // 如果启用了调试面板,渲染调试面板
1277
+ if (debugConfig?.enabled) {
1278
+ return React.createElement(
1279
+ React.Fragment,
1280
+ null,
1281
+ chartContainer,
1282
+ React.createElement(DebugPanel, {
1283
+ options: debugConfig,
1284
+ debugInfo: {
1285
+ instance: {
1286
+ id: chartId,
1287
+ type: 'ECharts',
1288
+ renderer: 'canvas', // 简化处理,使用默认值
1289
+ width: typeof width === 'number' ? width : undefined,
1290
+ height: typeof height === 'number' ? height : undefined,
1291
+ platform: 'web', // 简化处理,使用默认值
1292
+ },
1293
+ config: option,
1294
+ data: {
1295
+ series: Array.isArray(option?.series) ? option.series : [],
1296
+ totalDataCount: calculateDataLength(option as EChartsOption),
1297
+ currentDataCount: calculateDataLength(option as EChartsOption),
1298
+ },
1299
+ performance: {
1300
+ initTime: performanceRef.current.initEndTime - performanceRef.current.initStartTime,
1301
+ renderTime:
1302
+ performanceRef.current.renderEndTime - performanceRef.current.renderStartTime,
1303
+ updateTime:
1304
+ performanceRef.current.updateEndTime - performanceRef.current.updateStartTime,
1305
+ dataSize: JSON.stringify(option).length,
1306
+ },
1307
+ },
1308
+ })
1309
+ );
1310
+ }
1311
+
1312
+ // 否则只渲染图表容器
1313
+ return chartContainer;
1314
+ };
1315
+
1316
+ /**
1317
+ * 导出基础图表组件
1318
+ */
1319
+ export default BaseChart;