@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.
- package/CHANGELOG.md +245 -0
- package/README.md +104 -302
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/vendors.js +1 -0
- package/dist/cjs/vendors~echarts.js +1 -0
- package/dist/esm/index.js +1 -58151
- package/dist/esm/vendors.js +1 -0
- package/dist/esm/vendors~echarts.js +1 -0
- package/package.json +19 -25
- package/src/adapters/MiniAppAdapter.ts +136 -0
- package/src/adapters/__tests__/index.test.ts +1 -1
- package/src/adapters/h5/__tests__/index.test.ts +4 -2
- package/src/adapters/h5/index.ts +63 -64
- package/src/adapters/harmony/index.ts +23 -245
- package/src/adapters/index.ts +49 -45
- package/src/adapters/swan/index.ts +6 -69
- package/src/adapters/tt/index.ts +7 -70
- package/src/adapters/types.ts +25 -58
- package/src/adapters/weapp/index.ts +6 -69
- package/src/charts/__tests__/testUtils.tsx +87 -0
- package/src/charts/boxplot/__tests__/index.test.tsx +49 -103
- package/src/charts/boxplot/index.tsx +2 -1
- package/src/charts/boxplot/types.ts +17 -16
- package/src/charts/common/BaseChartWrapper.tsx +90 -82
- package/src/charts/common/__mocks__/BaseChartWrapper.tsx +17 -0
- package/src/charts/createChartComponent.tsx +36 -0
- package/src/charts/createOptionChartComponent.tsx +32 -0
- package/src/charts/funnel/__tests__/index.test.tsx +99 -0
- package/src/charts/funnel/index.tsx +60 -10
- package/src/charts/funnel/types.ts +6 -0
- package/src/charts/graph/__tests__/index.test.tsx +102 -33
- package/src/charts/graph/index.tsx +66 -9
- package/src/charts/graph/types.ts +6 -0
- package/src/charts/heatmap/__tests__/index.test.tsx +139 -0
- package/src/charts/heatmap/index.tsx +103 -10
- package/src/charts/heatmap/types.ts +6 -0
- package/src/charts/index.ts +74 -26
- package/src/charts/liquid/__tests__/index.test.tsx +52 -0
- package/src/charts/liquid/index.tsx +239 -182
- package/src/charts/liquid/types.ts +11 -11
- package/src/charts/parallel/__tests__/index.test.tsx +40 -67
- package/src/charts/parallel/index.tsx +2 -1
- package/src/charts/parallel/types.ts +19 -18
- package/src/charts/radar/__tests__/index.test.tsx +210 -0
- package/src/charts/radar/index.tsx +143 -10
- package/src/charts/radar/types.ts +13 -0
- package/src/charts/sankey/__tests__/index.test.tsx +124 -0
- package/src/charts/sankey/index.tsx +62 -10
- package/src/charts/sankey/types.ts +6 -0
- package/src/charts/tree/__tests__/index.test.tsx +71 -0
- package/src/charts/tree/index.tsx +5 -2
- package/src/charts/tree/types.ts +9 -9
- package/src/charts/types.ts +208 -106
- package/src/charts/utils.ts +9 -7
- package/src/charts/wordcloud/__tests__/index.test.tsx +98 -31
- package/src/charts/wordcloud/index.tsx +75 -9
- package/src/charts/wordcloud/types.ts +6 -0
- package/src/components/DataFilter/index.tsx +32 -10
- package/src/core/animation/types.ts +6 -6
- package/src/core/components/Annotation.tsx +6 -7
- package/src/core/components/BaseChart.tsx +110 -168
- package/src/core/components/ErrorBoundary.tsx +17 -4
- package/src/core/components/LazyChart.tsx +54 -55
- package/src/core/components/hooks/index.ts +6 -2
- package/src/core/components/hooks/useChartInit.ts +6 -3
- package/src/core/components/hooks/usePerformance.ts +8 -2
- package/src/core/components/hooks/useVirtualScroll.ts +2 -1
- package/src/core/index.ts +1 -1
- package/src/core/themes/ThemeManager.ts +1 -1
- package/src/core/types/common.ts +2 -1
- package/src/core/types/index.ts +0 -12
- package/src/core/types/platform.ts +3 -5
- package/src/core/utils/__tests__/deepClone.test.ts +317 -0
- package/src/core/utils/__tests__/index.test.ts +2 -1
- package/src/core/utils/chartInstances.ts +13 -0
- package/src/core/utils/common.ts +20 -29
- package/src/core/utils/deepClone.ts +114 -0
- package/src/core/utils/download.ts +128 -0
- package/src/core/utils/drillDown.ts +34 -353
- package/src/core/utils/drillDownHelpers.ts +426 -0
- package/src/core/utils/events.ts +12 -0
- package/src/core/utils/export/ExportUtils.ts +36 -67
- package/src/core/utils/format.ts +44 -0
- package/src/core/utils/index.ts +21 -154
- package/src/core/utils/merge.ts +25 -0
- package/src/core/utils/performance/PerformanceAnalyzer.ts +38 -21
- package/src/core/utils/performance/hooks.ts +7 -0
- package/src/core/utils/performance/index.ts +2 -0
- package/src/{hooks → core/utils/performance}/useAnimation.ts +45 -41
- package/src/core/utils/performance/useDataZoom.ts +324 -0
- package/src/{hooks → core/utils/performance}/usePerformance.ts +49 -41
- package/src/core/utils/performance/usePerformanceHooks.ts +278 -0
- package/src/core/utils/performanceUtils.ts +310 -0
- package/src/core/utils/runtime.ts +190 -0
- package/src/core/utils/setOptionUtils.ts +59 -0
- package/src/core/version.ts +14 -0
- package/src/editor/EnhancedThemeEditor.tsx +362 -540
- package/src/editor/ThemeEditor.tsx +55 -321
- package/src/editor/components/ThemeBasicSettings.tsx +113 -0
- package/src/editor/components/ThemeColorEditor.tsx +105 -0
- package/src/editor/components/ThemeSelector.tsx +70 -0
- package/src/editor/hooks/useThemeEditorState.ts +201 -0
- package/src/editor/index.ts +10 -2
- package/src/hooks/__tests__/index.test.tsx +3 -1
- package/src/hooks/chartConnectHelpers.ts +341 -0
- package/src/hooks/index.ts +55 -660
- package/src/hooks/types.ts +189 -0
- package/src/hooks/useChartAutoResize.ts +73 -0
- package/src/hooks/useChartConnect.ts +92 -238
- package/src/hooks/useChartDownload.ts +25 -27
- package/src/hooks/useChartHistory.ts +34 -49
- package/src/hooks/useChartInit.ts +59 -0
- package/src/hooks/useChartOptions.ts +259 -0
- package/src/hooks/useChartPerformance.ts +109 -0
- package/src/hooks/useChartSelection.ts +52 -49
- package/src/hooks/useChartTheme.ts +51 -0
- package/src/hooks/useDataTransform.ts +19 -4
- package/src/hooks/utils/chartDownloadUtils.ts +40 -53
- package/src/hooks/utils/dataTransformUtils.ts +22 -0
- package/src/index.ts +48 -34
- package/src/main.tsx +4 -9
- package/src/react-dom.d.ts +3 -3
- package/src/themes/index.ts +30 -855
- package/src/themes/palettes/blue-green.ts +13 -0
- package/src/themes/palettes/chalk.ts +13 -0
- package/src/themes/palettes/cyber.ts +44 -0
- package/src/themes/palettes/dark.ts +52 -0
- package/src/themes/palettes/default.ts +52 -0
- package/src/themes/palettes/elegant.ts +34 -0
- package/src/themes/palettes/forest.ts +13 -0
- package/src/themes/palettes/glass.ts +49 -0
- package/src/themes/palettes/golden.ts +13 -0
- package/src/themes/palettes/neon.ts +43 -0
- package/src/themes/palettes/ocean.ts +39 -0
- package/src/themes/palettes/pastel.ts +37 -0
- package/src/themes/palettes/purple-passion.ts +13 -0
- package/src/themes/palettes/retro.ts +33 -0
- package/src/themes/palettes/sunset.ts +40 -0
- package/src/themes/palettes/walden.ts +13 -0
- package/src/themes/registry.ts +184 -0
- package/src/themes/types.ts +213 -0
- package/src/charts/bar/__tests__/index.test.tsx +0 -113
- package/src/charts/bar/index.tsx +0 -14
- package/src/charts/candlestick/__tests__/index.test.tsx +0 -40
- package/src/charts/candlestick/index.tsx +0 -13
- package/src/charts/gauge/index.tsx +0 -14
- package/src/charts/line/__tests__/index.test.tsx +0 -107
- package/src/charts/line/index.tsx +0 -15
- package/src/charts/pie/__tests__/index.test.tsx +0 -112
- package/src/charts/pie/index.tsx +0 -14
- package/src/charts/scatter/index.tsx +0 -14
- package/src/charts/sunburst/index.tsx +0 -18
- package/src/charts/treemap/index.tsx +0 -18
- package/src/core/utils/codeGenerator/CodeGenerator.ts +0 -669
- package/src/core/utils/codeGenerator/index.ts +0 -13
- package/src/core/utils/codeGenerator/types.ts +0 -198
- package/src/core/utils/configGenerator/ConfigGenerator.ts +0 -583
- package/src/core/utils/configGenerator/index.ts +0 -13
- package/src/core/utils/configGenerator/types.ts +0 -445
- package/src/core/utils/debug/DebugPanel.tsx +0 -637
- package/src/core/utils/debug/debugger.ts +0 -322
- package/src/core/utils/debug/index.ts +0 -21
- package/src/core/utils/debug/types.ts +0 -142
- 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
|
+
}
|
|
@@ -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
|
|
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):
|
|
34
|
-
setTextColor(r: number, g: number, b: number):
|
|
35
|
-
text(text: string, x: number, y: number):
|
|
36
|
-
addImage(
|
|
37
|
-
|
|
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:
|
|
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:
|
|
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
|
-
//
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
+
}
|