@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,426 @@
1
+ /**
2
+ * DrillDown 辅助函数
3
+ * 将 createDrillDown 中的逻辑拆分为独立函数,提高代码可维护性
4
+ */
5
+
6
+ import type { ECharts, EChartsOption, ECElementEvent } from 'echarts';
7
+ import type { DrillDownSource, DrillDownConfig } from './drillDown';
8
+
9
+ // ============================================================================
10
+ // 图表配置构建
11
+ // ============================================================================
12
+
13
+ /**
14
+ * 从 DrillDownSource 数组构建 ECharts option
15
+ */
16
+ export function buildOptionFromSources(sources: DrillDownSource[]): EChartsOption {
17
+ if (!sources || sources.length === 0) return {};
18
+
19
+ const names = sources.map((s) => s.name);
20
+ const values = sources.map((s) => s.value);
21
+
22
+ return {
23
+ xAxis: {
24
+ type: 'category',
25
+ data: names,
26
+ },
27
+ yAxis: {
28
+ type: 'value',
29
+ },
30
+ series: [
31
+ {
32
+ type: 'bar',
33
+ data: values,
34
+ },
35
+ ],
36
+ };
37
+ }
38
+
39
+ // ============================================================================
40
+ // 下钻配置获取
41
+ // ============================================================================
42
+
43
+ /**
44
+ * 根据层级和触发数据项获取下钻后的图表配置
45
+ */
46
+ export function getDrillDownOption(
47
+ level: number,
48
+ dataItem: DrillDownSource | undefined,
49
+ direction: 'down' | 'up' | 'reset',
50
+ sources: DrillDownSource[],
51
+ initialSources: DrillDownSource[] | undefined,
52
+ currentOption: EChartsOption,
53
+ initialOption: Record<string, unknown>
54
+ ): EChartsOption {
55
+ let targetOption: EChartsOption = {};
56
+
57
+ if (direction === 'reset' || level === 0) {
58
+ // 重置或回到第一层:使用初始数据
59
+ if (initialSources) {
60
+ targetOption = buildOptionFromSources(initialSources);
61
+ } else {
62
+ targetOption = {};
63
+ }
64
+ } else if (direction === 'up') {
65
+ // 上钻:从 history 中找到上一层的状态
66
+ if (sources.length > 0 && sources[0].chartOption) {
67
+ targetOption = sources[0].chartOption;
68
+ } else {
69
+ targetOption = buildOptionFromSources(sources);
70
+ }
71
+ } else if (direction === 'down' && dataItem) {
72
+ // 下钻
73
+ if (dataItem.chartOption) {
74
+ // 优先使用自定义 chartOption
75
+ targetOption = dataItem.chartOption;
76
+ } else if (dataItem.children && dataItem.children.length > 0) {
77
+ // 从 children 构建图表 option
78
+ targetOption = buildOptionFromSources(dataItem.children);
79
+ }
80
+ }
81
+
82
+ return targetOption;
83
+ }
84
+
85
+ // ============================================================================
86
+ // 数据源获取
87
+ // ============================================================================
88
+
89
+ /**
90
+ * 获取当前层级的数据源列表
91
+ */
92
+ export function getCurrentLevelSources(
93
+ currentLevel: number,
94
+ initialSources: DrillDownSource[] | undefined,
95
+ history: Array<{ level: number; dataItem: DrillDownSource }>
96
+ ): DrillDownSource[] {
97
+ if (currentLevel === 0) {
98
+ // 第 0 层:使用 initialSources
99
+ return initialSources ?? [];
100
+ }
101
+
102
+ // 其他层级:从 history 中找到上一层点击的数据项的 children
103
+ const lastHistory = history[history.length - 1];
104
+ if (lastHistory && lastHistory.dataItem.children) {
105
+ return lastHistory.dataItem.children;
106
+ }
107
+
108
+ return [];
109
+ }
110
+
111
+ // ============================================================================
112
+ // 下钻执行
113
+ // ============================================================================
114
+
115
+ /**
116
+ * 检查是否有下钻数据
117
+ */
118
+ export function hasDrillDownData(
119
+ dataItem: DrillDownSource | undefined
120
+ ): dataItem is DrillDownSource {
121
+ if (!dataItem) return false;
122
+ return !!(dataItem.children && dataItem.children.length > 0) || !!dataItem.chartOption;
123
+ }
124
+
125
+ /**
126
+ * 在当前层级数据中查找匹配的数据项
127
+ */
128
+ export function findMatchedSource(
129
+ name: string | number,
130
+ value: unknown,
131
+ currentLevelSources: DrillDownSource[],
132
+ initialSources: DrillDownSource[] | undefined,
133
+ currentLevel: number
134
+ ): DrillDownSource | undefined {
135
+ // 在当前层级的数据中查找匹配项
136
+ let matchedSource = currentLevelSources.find(
137
+ (s) => String(s.name) === String(name) || s.value === value
138
+ );
139
+
140
+ // 如果没找到,尝试在 initialSources 中查找
141
+ if (!matchedSource && currentLevel === 0 && initialSources) {
142
+ matchedSource = initialSources.find(
143
+ (s) => String(s.name) === String(name) || s.value === value
144
+ );
145
+ }
146
+
147
+ return matchedSource;
148
+ }
149
+
150
+ /**
151
+ * 执行下钻操作
152
+ */
153
+ export function executeDrillDown(
154
+ params: ECElementEvent,
155
+ state: {
156
+ chartInstance: ECharts | null;
157
+ config: DrillDownConfig;
158
+ currentLevel: number;
159
+ history: Array<{ level: number; dataItem: DrillDownSource }>;
160
+ currentOption: EChartsOption;
161
+ }
162
+ ): boolean {
163
+ const { chartInstance, config } = state;
164
+ if (!chartInstance) return false;
165
+
166
+ const { name, value } = params;
167
+
168
+ // 获取当前层级数据源
169
+ const currentLevelSources = getCurrentLevelSources(
170
+ state.currentLevel,
171
+ config.initialSources,
172
+ state.history
173
+ );
174
+
175
+ // 查找匹配项
176
+ const matchedSource = findMatchedSource(
177
+ name,
178
+ value,
179
+ currentLevelSources,
180
+ config.initialSources,
181
+ state.currentLevel
182
+ );
183
+
184
+ // 如果没有下钻数据,不执行
185
+ if (!hasDrillDownData(matchedSource)) {
186
+ console.warn('[DrillDown] No drill-down data available for:', name);
187
+ return false;
188
+ }
189
+
190
+ // 记录历史
191
+ if (matchedSource) {
192
+ state.history.push({ level: state.currentLevel, dataItem: matchedSource });
193
+ }
194
+
195
+ // 更新层级
196
+ state.currentLevel += 1;
197
+
198
+ // 获取新的图表配置
199
+ const newOption = getDrillDownOption(
200
+ state.currentLevel,
201
+ matchedSource,
202
+ 'down',
203
+ matchedSource.children ?? [],
204
+ config.initialSources,
205
+ state.currentOption,
206
+ {}
207
+ );
208
+
209
+ // 更新图表
210
+ if (newOption && Object.keys(newOption).length > 0) {
211
+ chartInstance.setOption(newOption, true);
212
+ state.currentOption = newOption;
213
+ }
214
+
215
+ // 触发回调
216
+ config.onDrillDown?.({
217
+ level: state.currentLevel,
218
+ name: matchedSource?.name ?? (name as string | number),
219
+ value: matchedSource?.value ?? value,
220
+ sources: matchedSource?.children ?? [],
221
+ chartOption: newOption,
222
+ rawParams: params as unknown as Record<string, unknown>,
223
+ });
224
+
225
+ return true;
226
+ }
227
+
228
+ // ============================================================================
229
+ // 状态管理
230
+ // ============================================================================
231
+
232
+ /**
233
+ * 执行上钻操作
234
+ */
235
+ export function executeDrillUp(
236
+ state: {
237
+ chartInstance: ECharts | null;
238
+ currentLevel: number;
239
+ history: Array<{ level: number; dataItem: DrillDownSource }>;
240
+ currentOption: EChartsOption;
241
+ initialOption: Record<string, unknown>;
242
+ },
243
+ config: DrillDownConfig
244
+ ): boolean {
245
+ const { chartInstance } = state;
246
+ if (!chartInstance || state.currentLevel <= 0) {
247
+ console.warn('[DrillDown] Cannot drill up: already at top level');
248
+ return false;
249
+ }
250
+
251
+ const previousLevel = state.currentLevel;
252
+
253
+ // 弹出历史
254
+ state.history.pop();
255
+
256
+ // 更新层级
257
+ state.currentLevel -= 1;
258
+
259
+ // 获取当前层级数据源
260
+ const currentLevelSources = getCurrentLevelSources(
261
+ state.currentLevel,
262
+ config.initialSources,
263
+ state.history
264
+ );
265
+
266
+ // 获取新的图表配置
267
+ const newOption = getDrillDownOption(
268
+ state.currentLevel,
269
+ undefined,
270
+ 'up',
271
+ currentLevelSources,
272
+ config.initialSources,
273
+ state.currentOption,
274
+ state.initialOption
275
+ );
276
+
277
+ // 更新图表
278
+ if (newOption && Object.keys(newOption).length > 0) {
279
+ chartInstance.setOption(newOption, true);
280
+ state.currentOption = newOption;
281
+ } else if (state.currentLevel === 0 && state.initialOption) {
282
+ chartInstance.setOption(state.initialOption as EChartsOption, true);
283
+ state.currentOption = state.initialOption as EChartsOption;
284
+ }
285
+
286
+ // 触发回调
287
+ config.onDrillUp?.({
288
+ previousLevel,
289
+ currentLevel: state.currentLevel,
290
+ chartOption: newOption,
291
+ });
292
+
293
+ return true;
294
+ }
295
+
296
+ /**
297
+ * 执行重置操作
298
+ */
299
+ export function executeReset(
300
+ state: {
301
+ chartInstance: ECharts | null;
302
+ currentLevel: number;
303
+ history: Array<{ level: number; dataItem: DrillDownSource }>;
304
+ currentOption: EChartsOption;
305
+ initialOption: Record<string, unknown>;
306
+ },
307
+ config: DrillDownConfig
308
+ ): void {
309
+ const { chartInstance } = state;
310
+ if (!chartInstance) return;
311
+
312
+ // 清空历史
313
+ state.history = [];
314
+ state.currentLevel = 0;
315
+
316
+ // 获取当前层级数据源
317
+ const currentLevelSources = getCurrentLevelSources(
318
+ state.currentLevel,
319
+ config.initialSources,
320
+ state.history
321
+ );
322
+
323
+ // 获取初始配置
324
+ const newOption = getDrillDownOption(
325
+ 0,
326
+ undefined,
327
+ 'reset',
328
+ currentLevelSources,
329
+ config.initialSources,
330
+ state.currentOption,
331
+ state.initialOption
332
+ );
333
+
334
+ // 更新图表
335
+ if (newOption && Object.keys(newOption).length > 0) {
336
+ chartInstance.setOption(newOption, true);
337
+ state.currentOption = newOption;
338
+ } else if (state.initialOption && Object.keys(state.initialOption).length > 0) {
339
+ chartInstance.setOption(state.initialOption as EChartsOption, true);
340
+ state.currentOption = state.initialOption as EChartsOption;
341
+ }
342
+
343
+ // 触发回调
344
+ config.onReset?.({ level: 0 });
345
+ }
346
+
347
+ /**
348
+ * 执行跳转到指定层级
349
+ */
350
+ export function executeDrillTo(
351
+ level: number,
352
+ dataItem: DrillDownSource | undefined,
353
+ state: {
354
+ chartInstance: ECharts | null;
355
+ currentLevel: number;
356
+ history: Array<{ level: number; dataItem: DrillDownSource }>;
357
+ currentOption: EChartsOption;
358
+ },
359
+ config: DrillDownConfig
360
+ ): void {
361
+ const { chartInstance } = state;
362
+ if (!chartInstance) return;
363
+
364
+ if (level < 0 || level > state.history.length) {
365
+ console.warn('[DrillDown] Invalid drill level:', level);
366
+ return;
367
+ }
368
+
369
+ const previousLevel = state.currentLevel;
370
+ state.currentLevel = level;
371
+
372
+ if (level === state.history.length) {
373
+ // 下钻
374
+ if (dataItem) {
375
+ state.history.push({ level: level - 1, dataItem });
376
+ }
377
+ } else if (level < state.history.length) {
378
+ // 上钻或跳转:调整 history
379
+ state.history = state.history.slice(0, level);
380
+ } else {
381
+ // level === 0, reset
382
+ state.history = [];
383
+ }
384
+
385
+ // 获取当前层级数据源
386
+ const currentLevelSources = getCurrentLevelSources(
387
+ state.currentLevel,
388
+ config.initialSources,
389
+ state.history
390
+ );
391
+
392
+ const newOption = getDrillDownOption(
393
+ level,
394
+ dataItem,
395
+ level === 0 ? 'reset' : 'down',
396
+ currentLevelSources,
397
+ config.initialSources,
398
+ state.currentOption,
399
+ {}
400
+ );
401
+
402
+ if (newOption && Object.keys(newOption).length > 0) {
403
+ chartInstance.setOption(newOption, true);
404
+ state.currentOption = newOption;
405
+ }
406
+
407
+ // 触发回调
408
+ if (level > previousLevel) {
409
+ config.onDrillDown?.({
410
+ level,
411
+ name: dataItem?.name ?? '',
412
+ value: dataItem?.value ?? 0,
413
+ sources: dataItem?.children ?? [],
414
+ chartOption: newOption,
415
+ rawParams: {},
416
+ });
417
+ } else if (level < previousLevel) {
418
+ config.onDrillUp?.({
419
+ previousLevel,
420
+ currentLevel: level,
421
+ chartOption: newOption,
422
+ });
423
+ } else {
424
+ config.onReset?.({ level: 0 });
425
+ }
426
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 事件名称常量定义
3
+ */
4
+ export const events = {
5
+ click: 'click',
6
+ mousemove: 'mousemove',
7
+ mouseup: 'mouseup',
8
+ mousedown: 'mousedown',
9
+ mouseover: 'mouseover',
10
+ mouseout: 'mouseout',
11
+ globalout: 'globalout',
12
+ };
@@ -1,8 +1,10 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
1
2
  /**
2
3
  * TaroViz 图表导出工具
3
4
  * 支持导出为 PNG、JPEG、SVG、PDF 等格式
4
5
  */
5
6
  import type { ECharts } from 'echarts';
7
+ import { generateFormattedFilename, downloadBlob, downloadFile, dataURLToBlob } from '../download';
6
8
 
7
9
  // ============================================================================
8
10
  // 类型定义
@@ -17,7 +19,7 @@ type ExportImageType = 'png' | 'jpeg' | 'svg' | 'webp' | 'gif';
17
19
  /**
18
20
  * getDataURL 方法参数(基于 ECharts 官方类型,扩展 webp/gif 支持)
19
21
  */
20
- interface EChartsDataURLOptions {
22
+ interface __EChartsDataURLOptions {
21
23
  type?: ExportImageType;
22
24
  pixelRatio?: number;
23
25
  backgroundColor?: string;
@@ -30,11 +32,18 @@ interface EChartsDataURLOptions {
30
32
  */
31
33
  interface JSPDFInstance {
32
34
  setProperties(props: Record<string, string>): void;
33
- setFontSize(size: number): void;
34
- setTextColor(r: number, g: number, b: number): void;
35
- text(text: string, x: number, y: number): void;
36
- addImage(imageData: string, format: string, x: number, y: number, width: number, height: number): void;
37
- output(type: 'blob'): Blob;
35
+ setFontSize(size: number): JSPDFInstance;
36
+ setTextColor(r: number, g: number, b: number): JSPDFInstance;
37
+ text(text: string, x: number, y: number): JSPDFInstance;
38
+ addImage(
39
+ imageData: string,
40
+ format: string,
41
+ x: number,
42
+ y: number,
43
+ width: number,
44
+ height: number
45
+ ): JSPDFInstance;
46
+ output(type: string): Blob | string | Uint8Array;
38
47
  }
39
48
 
40
49
  /**
@@ -123,52 +132,6 @@ export interface ExportResult {
123
132
  // 工具函数
124
133
  // ============================================================================
125
134
 
126
- /**
127
- * 将 Data URL 转换为 Blob
128
- */
129
- function dataURLToBlob(dataURL: string): Blob {
130
- const arr = dataURL.split(',');
131
- const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/png';
132
- const bstr = atob(arr[1]);
133
- let n = bstr.length;
134
- const u8arr = new Uint8Array(n);
135
- while (n--) {
136
- u8arr[n] = bstr.charCodeAt(n);
137
- }
138
- return new Blob([u8arr], { type: mime });
139
- }
140
-
141
- /**
142
- * 下载文件
143
- */
144
- function downloadFile(data: string | Blob, filename: string, _mimeType: string): void {
145
- const blob = typeof data === 'string' ? dataURLToBlob(data) : data;
146
- const url = URL.createObjectURL(blob);
147
-
148
- const link = document.createElement('a');
149
- link.href = url;
150
- link.download = filename;
151
- link.style.display = 'none';
152
-
153
- document.body.appendChild(link);
154
- link.click();
155
-
156
- // 清理
157
- setTimeout(() => {
158
- document.body.removeChild(link);
159
- URL.revokeObjectURL(url);
160
- }, 100);
161
- }
162
-
163
- /**
164
- * 生成文件名
165
- */
166
- function generateFilename(name: string, format: string): string {
167
- const timestamp = new Date().toISOString().slice(0, 10);
168
- const sanitizedName = name.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_');
169
- return `${sanitizedName}_${timestamp}.${format}`;
170
- }
171
-
172
135
  // ============================================================================
173
136
  // 导出类
174
137
  // ============================================================================
@@ -194,7 +157,7 @@ class ChartExporter {
194
157
 
195
158
  return {
196
159
  data,
197
- filename: generateFilename('chart', type),
160
+ filename: generateFormattedFilename('chart', type),
198
161
  mimeType,
199
162
  };
200
163
  }
@@ -222,7 +185,7 @@ class ChartExporter {
222
185
 
223
186
  return {
224
187
  data,
225
- filename: generateFilename('chart', 'svg'),
188
+ filename: generateFormattedFilename('chart', 'svg'),
226
189
  mimeType,
227
190
  };
228
191
  }
@@ -250,16 +213,15 @@ class ChartExporter {
250
213
  // 动态导入 jspdf
251
214
  let JSPDFClass: new (options: Record<string, string>) => JSPDFInstance;
252
215
  try {
253
- // 尝试使用动态导入,使用 webpackIgnore 注释避免预解析
254
- // @ts-expect-error - 动态导入
216
+ // @ts-expect-error - 动态导入,使用 webpackIgnore 注释避免预解析
255
217
  const module = await import(/* webpackIgnore: true */ 'jspdf');
256
218
  JSPDFClass = module.default as new (options: Record<string, string>) => JSPDFInstance;
257
219
  } catch {
258
- // 如果没有 jspdf,提供备选方案
259
- console.warn('[TaroViz] jspdf not found, falling back to image download');
220
+ // 如果没有 jspdf,提供备选方案(返回 PNG 而非 PDF)
221
+ console.warn('[TaroViz] jspdf not found, falling back to PNG export');
260
222
  return {
261
223
  data: imageData,
262
- filename: generateFilename('chart', 'png'),
224
+ filename: generateFormattedFilename('chart', 'png'),
263
225
  mimeType: 'image/png',
264
226
  };
265
227
  }
@@ -301,9 +263,7 @@ class ChartExporter {
301
263
 
302
264
  // 添加标题
303
265
  if (includeTitle) {
304
- doc.setFontSize(16);
305
- doc.setTextColor(51, 51, 51);
306
- doc.text(title, marginLeft, marginTop);
266
+ doc.setFontSize(16).setTextColor(51, 51, 51).text(title, marginLeft, marginTop);
307
267
  }
308
268
 
309
269
  // 添加图表
@@ -314,16 +274,25 @@ class ChartExporter {
314
274
 
315
275
  // 添加页脚
316
276
  const footerY = pageHeight - (margin.bottom ?? 40) / 2;
317
- doc.setFontSize(10);
318
- doc.setTextColor(153, 153, 153);
277
+ doc.setFontSize(10).setTextColor(153, 153, 153);
319
278
  doc.text(`Generated by TaroViz on ${new Date().toLocaleDateString()}`, marginLeft, footerY);
320
279
 
321
- // 导出为 Blob
322
- const pdfBlob = doc.output('blob') as Blob;
280
+ // 导出为 Blob(兼容不同版本的 jsPDF output 返回类型)
281
+ let pdfBlob: Blob;
282
+ const outputResult = doc.output('blob');
283
+ if (outputResult instanceof Blob) {
284
+ pdfBlob = outputResult;
285
+ } else if (typeof outputResult === 'string') {
286
+ pdfBlob = new Blob([outputResult], { type: 'application/pdf' });
287
+ } else {
288
+ pdfBlob = new Blob([new Uint8Array(outputResult as unknown as ArrayBuffer)], {
289
+ type: 'application/pdf',
290
+ });
291
+ }
323
292
 
324
293
  return {
325
294
  data: pdfBlob,
326
- filename: generateFilename(title.replace(/\s+/g, '_'), 'pdf'),
295
+ filename: generateFormattedFilename(title.replace(/\s+/g, '_'), 'pdf'),
327
296
  mimeType: 'application/pdf',
328
297
  size: pdfBlob.size,
329
298
  };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 格式化数值
3
+ * @param value 要格式化的数值
4
+ * @param digits 小数位数
5
+ * @param options 配置选项
6
+ * @returns 格式化后的字符串
7
+ */
8
+ export function formatNumber(
9
+ value: number,
10
+ digits: number = 2,
11
+ options: {
12
+ useGrouping?: boolean;
13
+ locale?: string;
14
+ } = {}
15
+ ): string {
16
+ const { useGrouping = true, locale = 'zh-CN' } = options;
17
+
18
+ return new Intl.NumberFormat(locale, {
19
+ minimumFractionDigits: digits,
20
+ maximumFractionDigits: digits,
21
+ useGrouping,
22
+ }).format(value);
23
+ }
24
+
25
+ /**
26
+ * 获取颜色的对比色
27
+ * @param color 十六进制颜色值
28
+ * @returns 对比色
29
+ */
30
+ export function getContrastColor(color: string): string {
31
+ // 移除#前缀
32
+ const hex = color.replace('#', '');
33
+
34
+ // 转换为RGB
35
+ const r = parseInt(hex.substring(0, 2), 16);
36
+ const g = parseInt(hex.substring(2, 4), 16);
37
+ const b = parseInt(hex.substring(4, 6), 16);
38
+
39
+ // 计算亮度
40
+ const brightness = (r * 299 + g * 587 + b * 114) / 1000;
41
+
42
+ // 根据亮度返回黑色或白色
43
+ return brightness > 128 ? '#000000' : '#FFFFFF';
44
+ }