@agions/taroviz 1.11.1 → 2.0.3

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 (164) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/README.md +104 -302
  3. package/dist/cjs/index.js +1 -1
  4. package/dist/cjs/vendors.js +1 -0
  5. package/dist/cjs/vendors~echarts.js +1 -0
  6. package/dist/esm/index.js +1 -58151
  7. package/dist/esm/vendors.js +1 -0
  8. package/dist/esm/vendors~echarts.js +1 -0
  9. package/package.json +19 -25
  10. package/src/adapters/MiniAppAdapter.ts +136 -0
  11. package/src/adapters/__tests__/index.test.ts +1 -1
  12. package/src/adapters/h5/__tests__/index.test.ts +4 -2
  13. package/src/adapters/h5/index.ts +63 -64
  14. package/src/adapters/harmony/index.ts +23 -245
  15. package/src/adapters/index.ts +49 -45
  16. package/src/adapters/swan/index.ts +6 -69
  17. package/src/adapters/tt/index.ts +7 -70
  18. package/src/adapters/types.ts +25 -58
  19. package/src/adapters/weapp/index.ts +6 -69
  20. package/src/charts/__tests__/testUtils.tsx +87 -0
  21. package/src/charts/boxplot/__tests__/index.test.tsx +49 -103
  22. package/src/charts/boxplot/index.tsx +2 -1
  23. package/src/charts/boxplot/types.ts +17 -16
  24. package/src/charts/common/BaseChartWrapper.tsx +90 -82
  25. package/src/charts/common/__mocks__/BaseChartWrapper.tsx +17 -0
  26. package/src/charts/createChartComponent.tsx +36 -0
  27. package/src/charts/createOptionChartComponent.tsx +32 -0
  28. package/src/charts/funnel/__tests__/index.test.tsx +99 -0
  29. package/src/charts/funnel/index.tsx +60 -10
  30. package/src/charts/funnel/types.ts +6 -0
  31. package/src/charts/graph/__tests__/index.test.tsx +102 -33
  32. package/src/charts/graph/index.tsx +66 -9
  33. package/src/charts/graph/types.ts +6 -0
  34. package/src/charts/heatmap/__tests__/index.test.tsx +139 -0
  35. package/src/charts/heatmap/index.tsx +103 -10
  36. package/src/charts/heatmap/types.ts +6 -0
  37. package/src/charts/index.ts +74 -26
  38. package/src/charts/liquid/__tests__/index.test.tsx +52 -0
  39. package/src/charts/liquid/index.tsx +239 -182
  40. package/src/charts/liquid/types.ts +11 -11
  41. package/src/charts/parallel/__tests__/index.test.tsx +40 -67
  42. package/src/charts/parallel/index.tsx +2 -1
  43. package/src/charts/parallel/types.ts +19 -18
  44. package/src/charts/radar/__tests__/index.test.tsx +210 -0
  45. package/src/charts/radar/index.tsx +143 -10
  46. package/src/charts/radar/types.ts +13 -0
  47. package/src/charts/sankey/__tests__/index.test.tsx +124 -0
  48. package/src/charts/sankey/index.tsx +62 -10
  49. package/src/charts/sankey/types.ts +6 -0
  50. package/src/charts/tree/__tests__/index.test.tsx +71 -0
  51. package/src/charts/tree/index.tsx +5 -2
  52. package/src/charts/tree/types.ts +9 -9
  53. package/src/charts/types.ts +208 -106
  54. package/src/charts/utils.ts +9 -7
  55. package/src/charts/wordcloud/__tests__/index.test.tsx +98 -31
  56. package/src/charts/wordcloud/index.tsx +75 -9
  57. package/src/charts/wordcloud/types.ts +6 -0
  58. package/src/components/DataFilter/index.tsx +32 -10
  59. package/src/core/animation/types.ts +6 -6
  60. package/src/core/components/Annotation.tsx +6 -7
  61. package/src/core/components/BaseChart.tsx +110 -168
  62. package/src/core/components/ErrorBoundary.tsx +17 -4
  63. package/src/core/components/LazyChart.tsx +54 -55
  64. package/src/core/components/hooks/index.ts +6 -2
  65. package/src/core/components/hooks/useChartInit.ts +6 -3
  66. package/src/core/components/hooks/usePerformance.ts +8 -2
  67. package/src/core/components/hooks/useVirtualScroll.ts +2 -1
  68. package/src/core/index.ts +1 -1
  69. package/src/core/themes/ThemeManager.ts +1 -1
  70. package/src/core/types/common.ts +2 -1
  71. package/src/core/types/index.ts +0 -12
  72. package/src/core/types/platform.ts +3 -5
  73. package/src/core/utils/__tests__/deepClone.test.ts +317 -0
  74. package/src/core/utils/__tests__/index.test.ts +2 -1
  75. package/src/core/utils/chartInstances.ts +13 -0
  76. package/src/core/utils/common.ts +20 -29
  77. package/src/core/utils/deepClone.ts +114 -0
  78. package/src/core/utils/download.ts +128 -0
  79. package/src/core/utils/drillDown.ts +34 -353
  80. package/src/core/utils/drillDownHelpers.ts +426 -0
  81. package/src/core/utils/events.ts +12 -0
  82. package/src/core/utils/export/ExportUtils.ts +36 -67
  83. package/src/core/utils/format.ts +44 -0
  84. package/src/core/utils/index.ts +21 -154
  85. package/src/core/utils/merge.ts +25 -0
  86. package/src/core/utils/performance/PerformanceAnalyzer.ts +38 -21
  87. package/src/core/utils/performance/hooks.ts +7 -0
  88. package/src/core/utils/performance/index.ts +2 -0
  89. package/src/{hooks → core/utils/performance}/useAnimation.ts +45 -41
  90. package/src/core/utils/performance/useDataZoom.ts +324 -0
  91. package/src/{hooks → core/utils/performance}/usePerformance.ts +49 -41
  92. package/src/core/utils/performance/usePerformanceHooks.ts +278 -0
  93. package/src/core/utils/performanceUtils.ts +310 -0
  94. package/src/core/utils/runtime.ts +190 -0
  95. package/src/core/utils/setOptionUtils.ts +59 -0
  96. package/src/core/version.ts +14 -0
  97. package/src/editor/EnhancedThemeEditor.tsx +362 -540
  98. package/src/editor/ThemeEditor.tsx +55 -321
  99. package/src/editor/components/ThemeBasicSettings.tsx +113 -0
  100. package/src/editor/components/ThemeColorEditor.tsx +105 -0
  101. package/src/editor/components/ThemeSelector.tsx +70 -0
  102. package/src/editor/hooks/useThemeEditorState.ts +201 -0
  103. package/src/editor/index.ts +10 -2
  104. package/src/hooks/__tests__/index.test.tsx +3 -1
  105. package/src/hooks/chartConnectHelpers.ts +341 -0
  106. package/src/hooks/index.ts +55 -660
  107. package/src/hooks/types.ts +189 -0
  108. package/src/hooks/useChartAutoResize.ts +73 -0
  109. package/src/hooks/useChartConnect.ts +92 -238
  110. package/src/hooks/useChartDownload.ts +25 -27
  111. package/src/hooks/useChartHistory.ts +34 -49
  112. package/src/hooks/useChartInit.ts +59 -0
  113. package/src/hooks/useChartOptions.ts +259 -0
  114. package/src/hooks/useChartPerformance.ts +109 -0
  115. package/src/hooks/useChartSelection.ts +52 -49
  116. package/src/hooks/useChartTheme.ts +51 -0
  117. package/src/hooks/useDataTransform.ts +19 -4
  118. package/src/hooks/utils/chartDownloadUtils.ts +40 -53
  119. package/src/hooks/utils/dataTransformUtils.ts +22 -0
  120. package/src/index.ts +48 -34
  121. package/src/main.tsx +4 -9
  122. package/src/react-dom.d.ts +3 -3
  123. package/src/themes/index.ts +30 -855
  124. package/src/themes/palettes/blue-green.ts +13 -0
  125. package/src/themes/palettes/chalk.ts +13 -0
  126. package/src/themes/palettes/cyber.ts +44 -0
  127. package/src/themes/palettes/dark.ts +52 -0
  128. package/src/themes/palettes/default.ts +52 -0
  129. package/src/themes/palettes/elegant.ts +34 -0
  130. package/src/themes/palettes/forest.ts +13 -0
  131. package/src/themes/palettes/glass.ts +49 -0
  132. package/src/themes/palettes/golden.ts +13 -0
  133. package/src/themes/palettes/neon.ts +43 -0
  134. package/src/themes/palettes/ocean.ts +39 -0
  135. package/src/themes/palettes/pastel.ts +37 -0
  136. package/src/themes/palettes/purple-passion.ts +13 -0
  137. package/src/themes/palettes/retro.ts +33 -0
  138. package/src/themes/palettes/sunset.ts +40 -0
  139. package/src/themes/palettes/walden.ts +13 -0
  140. package/src/themes/registry.ts +184 -0
  141. package/src/themes/types.ts +213 -0
  142. package/src/charts/bar/__tests__/index.test.tsx +0 -113
  143. package/src/charts/bar/index.tsx +0 -14
  144. package/src/charts/candlestick/__tests__/index.test.tsx +0 -40
  145. package/src/charts/candlestick/index.tsx +0 -13
  146. package/src/charts/gauge/index.tsx +0 -14
  147. package/src/charts/line/__tests__/index.test.tsx +0 -107
  148. package/src/charts/line/index.tsx +0 -15
  149. package/src/charts/pie/__tests__/index.test.tsx +0 -112
  150. package/src/charts/pie/index.tsx +0 -14
  151. package/src/charts/scatter/index.tsx +0 -14
  152. package/src/charts/sunburst/index.tsx +0 -18
  153. package/src/charts/treemap/index.tsx +0 -18
  154. package/src/core/utils/codeGenerator/CodeGenerator.ts +0 -669
  155. package/src/core/utils/codeGenerator/index.ts +0 -13
  156. package/src/core/utils/codeGenerator/types.ts +0 -198
  157. package/src/core/utils/configGenerator/ConfigGenerator.ts +0 -583
  158. package/src/core/utils/configGenerator/index.ts +0 -13
  159. package/src/core/utils/configGenerator/types.ts +0 -445
  160. package/src/core/utils/debug/DebugPanel.tsx +0 -637
  161. package/src/core/utils/debug/debugger.ts +0 -322
  162. package/src/core/utils/debug/index.ts +0 -21
  163. package/src/core/utils/debug/types.ts +0 -142
  164. package/src/hooks/useDataZoom.ts +0 -323
@@ -0,0 +1,201 @@
1
+ /**
2
+ * 主题编辑器共享状态管理 Hook
3
+ * 提取 ThemeEditor 和 EnhancedThemeEditor 的公共状态逻辑
4
+ */
5
+ import { useState, useEffect, useCallback } from 'react';
6
+ import type { ThemeOptions } from '../../themes';
7
+ import { getRegisteredThemes, registerTheme, switchTheme } from '../../themes';
8
+
9
+ export interface UseThemeEditorStateOptions {
10
+ selectedTheme?: string;
11
+ enableLivePreview?: boolean;
12
+ onThemeChange?: (theme: ThemeOptions) => void;
13
+ }
14
+
15
+ export interface UseThemeEditorStateReturn {
16
+ /** 当前主题 */
17
+ currentTheme: ThemeOptions;
18
+ /** 已注册的主题列表 */
19
+ registeredThemes: ThemeOptions[];
20
+ /** 颜色列表 */
21
+ colors: string[];
22
+ /** 背景色 */
23
+ backgroundColor: string;
24
+ /** 文本颜色 */
25
+ textColor: string;
26
+ /** 深色模式 */
27
+ darkMode: boolean;
28
+ /** 是否处于编辑模式 */
29
+ isEditing: boolean;
30
+ /** 新主题名称 */
31
+ newThemeName: string;
32
+ /** 设置当前主题 */
33
+ setCurrentTheme: (theme: ThemeOptions) => void;
34
+ /** 更新主题(部分更新) */
35
+ updateTheme: (updates: Partial<ThemeOptions>) => void;
36
+ /** 处理颜色变化 */
37
+ handleColorChange: (index: number, color: string) => void;
38
+ /** 添加颜色 */
39
+ handleAddColor: () => void;
40
+ /** 删除颜色 */
41
+ handleRemoveColor: (index: number) => void;
42
+ /** 开始编辑新主题 */
43
+ handleStartEdit: () => void;
44
+ /** 设置新主题名称 */
45
+ setNewThemeName: (name: string) => void;
46
+ /** 保存主题 */
47
+ saveTheme: () => ThemeOptions;
48
+ /** 取消编辑 */
49
+ cancelEdit: () => void;
50
+ }
51
+
52
+ /** 带有必填字段的默认主题 */
53
+ const DEFAULT_THEME: Required<
54
+ Pick<ThemeOptions, 'colors' | 'backgroundColor' | 'textColor' | 'darkMode'>
55
+ > = {
56
+ colors: ['#1890ff'],
57
+ backgroundColor: '#ffffff',
58
+ textColor: '#333333',
59
+ darkMode: false,
60
+ };
61
+
62
+ /**
63
+ * 主题编辑器状态管理 Hook
64
+ */
65
+ export function useThemeEditorState(
66
+ options: UseThemeEditorStateOptions = {}
67
+ ): UseThemeEditorStateReturn {
68
+ const { selectedTheme, enableLivePreview = true, onThemeChange } = options;
69
+
70
+ const registeredThemes = getRegisteredThemes();
71
+
72
+ // 计算初始主题
73
+ const getInitialTheme = useCallback((): ThemeOptions => {
74
+ if (selectedTheme) {
75
+ const theme = registeredThemes.find((t) => t.name === selectedTheme);
76
+ if (theme) return theme;
77
+ }
78
+ return registeredThemes[0] || DEFAULT_THEME;
79
+ }, [selectedTheme, registeredThemes]);
80
+
81
+ const [currentTheme, setCurrentThemeState] = useState<ThemeOptions>(getInitialTheme);
82
+ const [newThemeName, setNewThemeName] = useState<string>('');
83
+ const [isEditing, setIsEditing] = useState<boolean>(false);
84
+
85
+ // 派生状态
86
+ const colors: string[] = currentTheme.colors || DEFAULT_THEME.colors;
87
+ const backgroundColor: string = currentTheme.backgroundColor || DEFAULT_THEME.backgroundColor;
88
+ const textColor: string = currentTheme.textColor || DEFAULT_THEME.textColor;
89
+ const darkMode: boolean = currentTheme.darkMode ?? DEFAULT_THEME.darkMode;
90
+
91
+ // 当外部 selectedTheme 变化时同步
92
+ useEffect(() => {
93
+ if (selectedTheme) {
94
+ const theme = registeredThemes.find((t) => t.name === selectedTheme);
95
+ if (theme) {
96
+ setCurrentThemeState(theme);
97
+ }
98
+ }
99
+ }, [selectedTheme, registeredThemes]);
100
+
101
+ // 设置当前主题并触发回调
102
+ const setCurrentTheme = useCallback(
103
+ (theme: ThemeOptions) => {
104
+ setCurrentThemeState(theme);
105
+ if (enableLivePreview) {
106
+ switchTheme(theme);
107
+ }
108
+ onThemeChange?.(theme);
109
+ },
110
+ [enableLivePreview, onThemeChange]
111
+ );
112
+
113
+ // 更新主题(部分更新)
114
+ const updateTheme = useCallback(
115
+ (updates: Partial<ThemeOptions>) => {
116
+ const updated = { ...currentTheme, ...updates };
117
+ setCurrentThemeState(updated);
118
+ if (enableLivePreview) {
119
+ switchTheme(updated);
120
+ }
121
+ onThemeChange?.(updated);
122
+ },
123
+ [currentTheme, enableLivePreview, onThemeChange]
124
+ );
125
+
126
+ // 处理颜色变化
127
+ const handleColorChange = useCallback(
128
+ (index: number, color: string) => {
129
+ const newColors = [...colors];
130
+ newColors[index] = color;
131
+ updateTheme({ colors: newColors });
132
+ },
133
+ [colors, updateTheme]
134
+ );
135
+
136
+ // 添加颜色
137
+ const handleAddColor = useCallback(() => {
138
+ updateTheme({ colors: [...colors, '#000000'] });
139
+ }, [colors, updateTheme]);
140
+
141
+ // 删除颜色
142
+ const handleRemoveColor = useCallback(
143
+ (index: number) => {
144
+ if (colors.length <= 1) return;
145
+ const newColors = colors.filter((_, i) => i !== index);
146
+ updateTheme({ colors: newColors });
147
+ },
148
+ [colors, updateTheme]
149
+ );
150
+
151
+ // 开始编辑新主题
152
+ const handleStartEdit = useCallback(() => {
153
+ setIsEditing(true);
154
+ setNewThemeName('');
155
+ }, []);
156
+
157
+ // 取消编辑
158
+ const cancelEdit = useCallback(() => {
159
+ setIsEditing(false);
160
+ setNewThemeName('');
161
+ }, []);
162
+
163
+ // 保存主题
164
+ const saveTheme = useCallback((): ThemeOptions => {
165
+ const name = isEditing && newThemeName ? newThemeName : currentTheme.name || 'custom';
166
+ const themeToSave: ThemeOptions = {
167
+ ...currentTheme,
168
+ name,
169
+ colors,
170
+ backgroundColor,
171
+ textColor,
172
+ darkMode,
173
+ };
174
+
175
+ registerTheme(name, themeToSave);
176
+ setIsEditing(false);
177
+ setNewThemeName('');
178
+
179
+ return themeToSave;
180
+ }, [isEditing, newThemeName, currentTheme, colors, backgroundColor, textColor, darkMode]);
181
+
182
+ return {
183
+ currentTheme,
184
+ registeredThemes,
185
+ colors,
186
+ backgroundColor,
187
+ textColor,
188
+ darkMode,
189
+ isEditing,
190
+ newThemeName,
191
+ setCurrentTheme,
192
+ updateTheme,
193
+ handleColorChange,
194
+ handleAddColor,
195
+ handleRemoveColor,
196
+ handleStartEdit,
197
+ setNewThemeName,
198
+ saveTheme,
199
+ cancelEdit,
200
+ };
201
+ }
@@ -3,8 +3,16 @@
3
3
  * 提供可视化的主题编辑功能
4
4
  */
5
5
  import ThemeEditor from './ThemeEditor';
6
- import type { ThemeEditorProps } from './ThemeEditor';
6
+ import EnhancedThemeEditor from './EnhancedThemeEditor';
7
7
 
8
- export { ThemeEditor, ThemeEditorProps };
8
+ export type { ThemeEditorProps } from './ThemeEditor';
9
+ export type { EnhancedThemeEditorProps, ThemeExportOptions } from './EnhancedThemeEditor';
10
+ export { useThemeEditorState } from './hooks/useThemeEditorState';
11
+ export type {
12
+ UseThemeEditorStateOptions,
13
+ UseThemeEditorStateReturn,
14
+ } from './hooks/useThemeEditorState';
15
+
16
+ export { ThemeEditor, EnhancedThemeEditor };
9
17
 
10
18
  export default ThemeEditor;
@@ -249,7 +249,9 @@ describe('React Hooks', () => {
249
249
  );
250
250
 
251
251
  // getThemeByName('default') returns the defaultTheme object
252
- expect(result.current).toEqual(expect.objectContaining({ backgroundColor: '#ffffff', textColor: '#333333' }));
252
+ expect(result.current).toEqual(
253
+ expect.objectContaining({ backgroundColor: '#ffffff', textColor: '#333333' })
254
+ );
253
255
  });
254
256
 
255
257
  it('should return original theme when darkMode is false', () => {
@@ -0,0 +1,341 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ /**
3
+ * useChartConnect 辅助函数
4
+ * 将 useChartConnect 中的逻辑拆分为独立函数,提高代码可维护性
5
+ */
6
+
7
+ import type {
8
+ ChartInstance,
9
+ ConnectEventType,
10
+ ChartPointerEventParams,
11
+ ChartSelectEventParams,
12
+ ChartDataZoomEventParams,
13
+ EventHandler,
14
+ } from './types';
15
+
16
+ // ============================================================================
17
+ // 导出类型
18
+ // ============================================================================
19
+
20
+ export type {
21
+ ConnectEventType,
22
+ ChartPointerEventParams,
23
+ ChartSelectEventParams,
24
+ ChartDataZoomEventParams,
25
+ } from './types';
26
+
27
+ // ============================================================================
28
+ // 内部类型
29
+ // ============================================================================
30
+
31
+ /** 图表联动项 */
32
+ export interface ChartConnectItem {
33
+ instance: ChartInstance;
34
+ id: string;
35
+ }
36
+
37
+ /** 事件处理器映射 */
38
+ export type EventHandlersMap = Map<string, Map<ConnectEventType, EventHandler>>;
39
+
40
+ // ============================================================================
41
+ // 事件处理器创建
42
+ // ============================================================================
43
+
44
+ /**
45
+ * 创建联动事件处理器
46
+ */
47
+ export function createConnectHandler(
48
+ sourceId: string,
49
+
50
+ eventType: ConnectEventType,
51
+ disabled: boolean,
52
+
53
+ eventFilter: ((eventType: ConnectEventType, params: unknown) => boolean) | undefined,
54
+ chartsRef: React.MutableRefObject<Map<string, ChartConnectItem>>,
55
+ optionsRef: React.MutableRefObject<{
56
+ onConnect?: (
57
+ sourceId: string,
58
+ targetId: string,
59
+
60
+ payload: { eventType: ConnectEventType; params: unknown }
61
+ ) => void;
62
+ }>
63
+ ): EventHandler {
64
+ return (params: unknown) => {
65
+ if (disabled) return;
66
+
67
+ // 应用事件过滤器
68
+ if (eventFilter && !eventFilter(eventType, params)) {
69
+ return;
70
+ }
71
+
72
+ // 分发联动事件到所有其他图表
73
+ dispatchToOthers(sourceId, eventType, params, chartsRef);
74
+
75
+ // 触发回调
76
+ const currentOptions = optionsRef.current;
77
+ if (currentOptions.onConnect) {
78
+ chartsRef.current.forEach((item, targetId) => {
79
+ if (targetId !== sourceId) {
80
+ currentOptions.onConnect?.(sourceId, targetId, { eventType, params });
81
+ }
82
+ });
83
+ }
84
+ };
85
+ }
86
+
87
+ /**
88
+ * 分发事件到其他图表
89
+ */
90
+ export function dispatchToOthers(
91
+ sourceId: string,
92
+
93
+ eventType: ConnectEventType,
94
+ params: unknown,
95
+ chartsRef: React.MutableRefObject<Map<string, ChartConnectItem>>
96
+ ): void {
97
+ chartsRef.current.forEach((item, targetId) => {
98
+ if (targetId === sourceId) return;
99
+
100
+ try {
101
+ switch (eventType) {
102
+ case 'click':
103
+ case 'hover': {
104
+ const p = params as ChartPointerEventParams;
105
+ item.instance.dispatchAction?.({
106
+ type: 'showTip',
107
+ seriesIndex: p.seriesIndex,
108
+ dataIndex: p.dataIndex,
109
+ });
110
+ break;
111
+ }
112
+ case 'select': {
113
+ const p = params as ChartSelectEventParams;
114
+ item.instance.dispatchAction?.({
115
+ type: 'toggleSelect',
116
+ seriesIndex: p.seriesIndex,
117
+ dataIndex: p.dataIndex,
118
+ });
119
+ break;
120
+ }
121
+ case 'dataZoom': {
122
+ const p = params as ChartDataZoomEventParams;
123
+ item.instance.dispatchAction?.({
124
+ type: 'dataZoom',
125
+ start: p.start,
126
+ end: p.end,
127
+ dataZoomIndex: p.dataZoomIndex,
128
+ dataZoomIndexs: p.dataZoomIndexs,
129
+ });
130
+ break;
131
+ }
132
+ default:
133
+ break;
134
+ }
135
+ } catch (e) {
136
+ console.warn(`[useChartConnect] Failed to dispatch ${eventType} to ${targetId}:`, e);
137
+ }
138
+ });
139
+ }
140
+
141
+ // ============================================================================
142
+ // 事件绑定管理
143
+ // ============================================================================
144
+
145
+ /**
146
+ * 绑定单个图表的联动事件
147
+ */
148
+ export function bindChartEvents(
149
+ chartInstance: ChartInstance,
150
+ chartId: string,
151
+ events: ConnectEventType[],
152
+ createHandler: (chartId: string, eventType: ConnectEventType) => EventHandler,
153
+ eventHandlersRef: React.MutableRefObject<EventHandlersMap>
154
+ ): void {
155
+ if (!chartInstance) return;
156
+
157
+ const handlers = new Map<ConnectEventType, EventHandler>();
158
+
159
+ // 为每个事件类型创建并绑定处理器
160
+ events.forEach((eventType) => {
161
+ const handler = createHandler(chartId, eventType);
162
+ handlers.set(eventType, handler);
163
+
164
+ try {
165
+ chartInstance.on(eventType, handler);
166
+ } catch (e) {
167
+ console.warn(`[useChartConnect] Failed to bind ${eventType} event:`, e);
168
+ }
169
+ });
170
+
171
+ eventHandlersRef.current.set(chartId, handlers);
172
+ }
173
+
174
+ /**
175
+ * 解绑单个图表的联动事件
176
+ */
177
+ export function unbindChartEvents(
178
+ chartId: string,
179
+ eventHandlersRef: React.MutableRefObject<EventHandlersMap>,
180
+ chartsRef: React.MutableRefObject<Map<string, ChartConnectItem>>
181
+ ): void {
182
+ const handlers = eventHandlersRef.current.get(chartId);
183
+ if (!handlers) return;
184
+
185
+ const chartItem = chartsRef.current.get(chartId);
186
+ if (!chartItem) return;
187
+
188
+ handlers.forEach((handler, eventType) => {
189
+ try {
190
+ chartItem.instance.off(eventType, handler);
191
+ } catch (e) {
192
+ console.warn(`[useChartConnect] Failed to unbind ${eventType} event:`, e);
193
+ }
194
+ });
195
+
196
+ eventHandlersRef.current.delete(chartId);
197
+ }
198
+
199
+ // ============================================================================
200
+ // 连接管理
201
+ // ============================================================================
202
+
203
+ /**
204
+ * 生成唯一图表 ID
205
+ */
206
+ export function generateChartId(): string {
207
+ return `chart_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
208
+ }
209
+
210
+ /**
211
+ * 绑定图表到联动组
212
+ */
213
+ export function connectChart(
214
+ chartInstance: ChartInstance,
215
+ chartId: string | undefined,
216
+ groupName: string | undefined,
217
+ chartsRef: React.MutableRefObject<Map<string, ChartConnectItem>>,
218
+ connectedRef: React.MutableRefObject<boolean>,
219
+ eventHandlersRef: React.MutableRefObject<EventHandlersMap>,
220
+ bindEvents: (chartInstance: ChartInstance, chartId: string) => void
221
+ ): string {
222
+ const id = chartId || generateChartId();
223
+
224
+ // 检查是否已连接
225
+ if (chartsRef.current.has(id)) {
226
+ console.warn(`[useChartConnect] Chart ${id} is already connected`);
227
+ return id;
228
+ }
229
+
230
+ // 存储图表实例
231
+ chartsRef.current.set(id, {
232
+ instance: chartInstance,
233
+ id,
234
+ });
235
+
236
+ // 绑定事件
237
+ bindEvents(chartInstance, id);
238
+ connectedRef.current = true;
239
+
240
+ // 如果有 groupName,设置图表组
241
+ if (groupName && 'group' in chartInstance) {
242
+ try {
243
+ (chartInstance as ChartInstance & { group?: string }).group = groupName;
244
+ } catch (e) {
245
+ console.warn('[useChartConnect] Failed to set chart group:', e);
246
+ }
247
+ }
248
+
249
+ return id;
250
+ }
251
+
252
+ /**
253
+ * 从联动组移除图表
254
+ */
255
+ export function disconnectChart(
256
+ chartInstance: ChartInstance,
257
+ chartId: string | undefined,
258
+ chartsRef: React.MutableRefObject<Map<string, ChartConnectItem>>,
259
+ connectedRef: React.MutableRefObject<boolean>,
260
+ unbindEvents: (chartId: string) => void
261
+ ): void {
262
+ // 查找图表 ID
263
+ let targetId = chartId;
264
+ if (!targetId) {
265
+ chartsRef.current.forEach((item, id) => {
266
+ if (item.instance === chartInstance) {
267
+ targetId = id;
268
+ }
269
+ });
270
+ }
271
+
272
+ if (!targetId || !chartsRef.current.has(targetId)) {
273
+ console.warn(`[useChartConnect] Chart ${targetId} not found in connection group`);
274
+ return;
275
+ }
276
+
277
+ // 解绑事件
278
+ unbindEvents(targetId);
279
+
280
+ // 移除图表
281
+ chartsRef.current.delete(targetId);
282
+
283
+ // 更新连接状态
284
+ connectedRef.current = chartsRef.current.size > 0;
285
+ }
286
+
287
+ /**
288
+ * 批量连接图表
289
+ */
290
+ export function connectAllCharts(
291
+ charts: Array<{ instance: ChartInstance; id: string }>,
292
+ connect: (instance: ChartInstance, id?: string) => void
293
+ ): void {
294
+ charts.forEach(({ instance, id }) => {
295
+ connect(instance, id);
296
+ });
297
+ }
298
+
299
+ /**
300
+ * 批量断开所有图表
301
+ */
302
+ export function disconnectAllCharts(
303
+ chartsRef: React.MutableRefObject<Map<string, ChartConnectItem>>,
304
+ connectedRef: React.MutableRefObject<boolean>,
305
+ unbindEvents: (chartId: string) => void
306
+ ): void {
307
+ chartsRef.current.forEach((item, id) => {
308
+ unbindEvents(id);
309
+ });
310
+ chartsRef.current.clear();
311
+ connectedRef.current = false;
312
+ }
313
+
314
+ /**
315
+ * 触发联动事件
316
+ */
317
+ export function dispatchConnectEvent(
318
+ sourceId: string,
319
+
320
+ payload: { eventType: ConnectEventType; params: unknown } | undefined,
321
+ disabled: boolean,
322
+ chartsRef: React.MutableRefObject<Map<string, ChartConnectItem>>,
323
+ dispatchToOthers: (sourceId: string, eventType: ConnectEventType, params: unknown) => void
324
+ ): void {
325
+ if (disabled) return;
326
+
327
+ const chartItem = chartsRef.current.get(sourceId);
328
+ if (!chartItem) {
329
+ console.warn(`[useChartConnect] Source chart ${sourceId} not found`);
330
+ return;
331
+ }
332
+
333
+ const { eventType, params } = payload || {};
334
+ if (!eventType) {
335
+ console.warn('[useChartConnect] Payload must include eventType');
336
+ return;
337
+ }
338
+
339
+ // 分发事件到其他图表
340
+ dispatchToOthers(sourceId, eventType, params);
341
+ }