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