@agions/taroviz 1.9.0 → 1.11.1
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/README.md +35 -5
- package/dist/cjs/index.js +1 -1
- package/dist/esm/index.js +1128 -432
- package/package.json +1 -1
- package/src/charts/boxplot/types.ts +5 -3
- package/src/charts/candlestick/__tests__/index.test.tsx +4 -1
- package/src/charts/graph/__tests__/index.test.tsx +8 -2
- package/src/charts/parallel/types.ts +6 -3
- package/src/charts/tree/types.ts +4 -4
- package/src/charts/wordcloud/__tests__/index.test.tsx +4 -1
- package/src/core/animation/AnimationManager.ts +9 -6
- package/src/core/animation/types.ts +30 -0
- package/src/core/components/Annotation.tsx +12 -10
- package/src/core/components/BaseChart.tsx +41 -31
- package/src/core/components/ErrorBoundary.tsx +30 -17
- package/src/core/components/LazyChart.tsx +36 -9
- package/src/core/themes/ThemeManager.ts +33 -0
- package/src/core/types/common.ts +100 -5
- package/src/core/utils/chartUtils.ts +8 -3
- package/src/core/utils/export/ExportUtils.ts +39 -9
- package/src/core/utils/performance/PerformanceAnalyzer.ts +15 -5
- package/src/core/utils/performance/types.ts +10 -1
- package/src/hooks/index.ts +53 -0
- package/src/hooks/useChartDownload.ts +17 -261
- package/src/hooks/useChartHistory.ts +273 -0
- package/src/hooks/useChartSelection.ts +350 -0
- package/src/hooks/useDataTransform.ts +39 -286
- package/src/hooks/utils/chartDownloadUtils.ts +273 -0
- package/src/hooks/utils/dataTransformUtils.ts +287 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useChartSelection - 图表数据点选择/高亮 Hook
|
|
3
|
+
* 支持单个/批量选择、反选、清除选择,配合 ECharts select 事件
|
|
4
|
+
*
|
|
5
|
+
* 特性:
|
|
6
|
+
* - 支持按 seriesIndex + dataIndex 选择
|
|
7
|
+
* - 支持按 dataIndex 跨系列批量选择
|
|
8
|
+
* - 支持反选(invertSelection)
|
|
9
|
+
* - 支持多选模式(multi)
|
|
10
|
+
* - 自动绑定图表 select/unselect 事件
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect, useRef, useCallback, useState } from 'react';
|
|
13
|
+
import type { ChartInstance } from './index';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// 类型定义
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/** 单个数据点的选择键 */
|
|
20
|
+
export interface DataPointKey {
|
|
21
|
+
seriesIndex: number;
|
|
22
|
+
dataIndex: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 选择模式 */
|
|
26
|
+
export type SelectionMode = 'single' | 'multiple';
|
|
27
|
+
|
|
28
|
+
/** 选择事件参数 */
|
|
29
|
+
export interface SelectionEvent {
|
|
30
|
+
/** 被选中的数据点 */
|
|
31
|
+
selected: DataPointKey[];
|
|
32
|
+
/** 取消选择的数据点 */
|
|
33
|
+
unselected: DataPointKey[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 选择配置选项 */
|
|
37
|
+
export interface UseChartSelectionOptions {
|
|
38
|
+
/** 选择模式:'single' 单选,'multiple' 多选,默认 'multiple' */
|
|
39
|
+
mode?: SelectionMode;
|
|
40
|
+
/** 是否在组件卸载时清除所有选择,默认 true */
|
|
41
|
+
clearOnUnmount?: boolean;
|
|
42
|
+
/** 是否启用 Ctrl+Click 多选,默认 true */
|
|
43
|
+
enableCtrlMultiSelect?: boolean;
|
|
44
|
+
/** 是否启用 Shift+Click 范围选择,默认 true */
|
|
45
|
+
enableShiftRangeSelect?: boolean;
|
|
46
|
+
/** 选择变化时的回调 */
|
|
47
|
+
onSelectionChange?: (event: SelectionEvent) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 选择返回值 */
|
|
51
|
+
export interface UseChartSelectionReturn {
|
|
52
|
+
/** 当前选中的数据点 */
|
|
53
|
+
selectedPoints: DataPointKey[];
|
|
54
|
+
/** 是否存在选中 */
|
|
55
|
+
hasSelection: boolean;
|
|
56
|
+
/** 选中数量 */
|
|
57
|
+
selectionCount: number;
|
|
58
|
+
/** 选中指定数据点 */
|
|
59
|
+
select: (key: DataPointKey) => void;
|
|
60
|
+
/** 取消选中指定数据点 */
|
|
61
|
+
deselect: (key: DataPointKey) => void;
|
|
62
|
+
/** 批量选中 */
|
|
63
|
+
selectMultiple: (keys: DataPointKey[]) => void;
|
|
64
|
+
/** 批量取消选中 */
|
|
65
|
+
deselectMultiple: (keys: DataPointKey[]) => void;
|
|
66
|
+
/** 切换选中状态 */
|
|
67
|
+
toggle: (key: DataPointKey) => void;
|
|
68
|
+
/** 反选 */
|
|
69
|
+
invertSelection: (seriesIndex: number, dataIndices: number[]) => void;
|
|
70
|
+
/** 全选指定系列 */
|
|
71
|
+
selectAll: (seriesIndex: number, dataIndices: number[]) => void;
|
|
72
|
+
/** 清除所有选择 */
|
|
73
|
+
clearSelection: () => void;
|
|
74
|
+
/** 判断某点是否被选中 */
|
|
75
|
+
isSelected: (key: DataPointKey) => boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// 工具函数
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
/** 生成唯一键字符串 */
|
|
83
|
+
function keyToString(key: DataPointKey): string {
|
|
84
|
+
return `${key.seriesIndex}:${key.dataIndex}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stringToKey(str: string): DataPointKey {
|
|
88
|
+
const [seriesIndex, dataIndex] = str.split(':').map(Number);
|
|
89
|
+
return { seriesIndex, dataIndex };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Hook 实现
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 使用图表数据点选择功能
|
|
98
|
+
* @param chartInstance 图表实例
|
|
99
|
+
* @param options 配置选项
|
|
100
|
+
* @returns 选择控制接口
|
|
101
|
+
*/
|
|
102
|
+
export function useChartSelection(
|
|
103
|
+
chartInstance: ChartInstance | null,
|
|
104
|
+
options: UseChartSelectionOptions = {}
|
|
105
|
+
): UseChartSelectionReturn {
|
|
106
|
+
const {
|
|
107
|
+
mode = 'multiple',
|
|
108
|
+
clearOnUnmount = true,
|
|
109
|
+
enableCtrlMultiSelect = true,
|
|
110
|
+
enableShiftRangeSelect = true,
|
|
111
|
+
onSelectionChange,
|
|
112
|
+
} = options;
|
|
113
|
+
|
|
114
|
+
// 选中点集合(字符串键)
|
|
115
|
+
const [selectedPoints, setSelectedPoints] = useState<DataPointKey[]>([]);
|
|
116
|
+
|
|
117
|
+
// Chart instance ref
|
|
118
|
+
const chartRef = useRef<ChartInstance | null>(null);
|
|
119
|
+
chartRef.current = chartInstance;
|
|
120
|
+
|
|
121
|
+
// 上一次 shift+click 的数据索引(用于范围选择)
|
|
122
|
+
const lastShiftIndexRef = useRef<number | null>(null);
|
|
123
|
+
|
|
124
|
+
// 当前模式 ref(用于事件处理)
|
|
125
|
+
const modeRef = useRef(mode);
|
|
126
|
+
modeRef.current = mode;
|
|
127
|
+
|
|
128
|
+
// 绑定图表 select/unselect 事件
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const chart = chartRef.current;
|
|
131
|
+
if (!chart || !chart.on) return;
|
|
132
|
+
|
|
133
|
+
const handleSelect = (params: { selected: Record<string, boolean>; type: string }) => {
|
|
134
|
+
// ECharts 内置 select 会同步更新 legend
|
|
135
|
+
// 这里我们用 dispatchAction 来实现纯数据点选择
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// 单击 legend 时清除数据点选择(保持一致性)
|
|
139
|
+
const handleLegendSelectChanged = (params: { name?: string; selected?: Record<string, boolean> }) => {
|
|
140
|
+
// 清除选择时的视觉反馈
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
chart.on('selectchanged', (params: unknown) => {
|
|
144
|
+
// 当图表内部选择变化时同步状态
|
|
145
|
+
const p = params as {
|
|
146
|
+
isFromClick?: boolean;
|
|
147
|
+
selected?: Record<string, boolean>;
|
|
148
|
+
notSelected?: Record<string, boolean>;
|
|
149
|
+
};
|
|
150
|
+
if (p.isFromClick) {
|
|
151
|
+
// 用户点击了图例,清除所有数据点选择
|
|
152
|
+
setSelectedPoints([]);
|
|
153
|
+
onSelectionChange?.({ selected: [], unselected: [] });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return () => {
|
|
158
|
+
if (chart.off) {
|
|
159
|
+
chart.off('selectchanged');
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}, [onSelectionChange]);
|
|
163
|
+
|
|
164
|
+
// 组件卸载时清除选择
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
return () => {
|
|
167
|
+
if (clearOnUnmount) {
|
|
168
|
+
const chart = chartRef.current;
|
|
169
|
+
if (chart?.dispatchAction) {
|
|
170
|
+
// 清除所有系列的选择状态
|
|
171
|
+
chart.dispatchAction({ type: 'unselect' });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}, [clearOnUnmount]);
|
|
176
|
+
|
|
177
|
+
// ─── 私有方法 ───────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/** 触发选择变化回调 */
|
|
180
|
+
const notifyChange = useCallback(
|
|
181
|
+
(selected: DataPointKey[], unselected: DataPointKey[]) => {
|
|
182
|
+
onSelectionChange?.({ selected, unselected });
|
|
183
|
+
},
|
|
184
|
+
[onSelectionChange]
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
/** 执行 ECharts dispatchAction */
|
|
188
|
+
const dispatchSelect = useCallback(
|
|
189
|
+
(key: DataPointKey, select: boolean) => {
|
|
190
|
+
const chart = chartRef.current;
|
|
191
|
+
if (!chart?.dispatchAction) return;
|
|
192
|
+
chart.dispatchAction({
|
|
193
|
+
type: select ? 'select' : 'unselect',
|
|
194
|
+
seriesIndex: key.seriesIndex,
|
|
195
|
+
dataIndex: key.dataIndex,
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
[]
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// ─── Public API ───────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
const select = useCallback(
|
|
204
|
+
(key: DataPointKey) => {
|
|
205
|
+
setSelectedPoints((prev) => {
|
|
206
|
+
const str = keyToString(key);
|
|
207
|
+
if (prev.some((p) => keyToString(p) === str)) return prev;
|
|
208
|
+
const next = mode === 'single' ? [key] : [...prev, key];
|
|
209
|
+
notifyChange(next, []);
|
|
210
|
+
return next;
|
|
211
|
+
});
|
|
212
|
+
dispatchSelect(key, true);
|
|
213
|
+
},
|
|
214
|
+
[mode, notifyChange, dispatchSelect]
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const deselect = useCallback(
|
|
218
|
+
(key: DataPointKey) => {
|
|
219
|
+
setSelectedPoints((prev) => {
|
|
220
|
+
const str = keyToString(key);
|
|
221
|
+
const removed = prev.filter((p) => keyToString(p) !== str);
|
|
222
|
+
notifyChange([], removed);
|
|
223
|
+
return removed;
|
|
224
|
+
});
|
|
225
|
+
dispatchSelect(key, false);
|
|
226
|
+
},
|
|
227
|
+
[notifyChange, dispatchSelect]
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const toggle = useCallback(
|
|
231
|
+
(key: DataPointKey) => {
|
|
232
|
+
const str = keyToString(key);
|
|
233
|
+
if (selectedPoints.some((p) => keyToString(p) === str)) {
|
|
234
|
+
deselect(key);
|
|
235
|
+
} else {
|
|
236
|
+
select(key);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
[selectedPoints, select, deselect]
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const selectMultiple = useCallback(
|
|
243
|
+
(keys: DataPointKey[]) => {
|
|
244
|
+
setSelectedPoints((prev) => {
|
|
245
|
+
const newPoints = mode === 'single' ? keys : [...prev, ...keys.filter(
|
|
246
|
+
(k) => !prev.some((p) => keyToString(p) === keyToString(k))
|
|
247
|
+
)];
|
|
248
|
+
notifyChange(newPoints, []);
|
|
249
|
+
return newPoints;
|
|
250
|
+
});
|
|
251
|
+
keys.forEach((key) => dispatchSelect(key, true));
|
|
252
|
+
},
|
|
253
|
+
[mode, notifyChange, dispatchSelect]
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const deselectMultiple = useCallback(
|
|
257
|
+
(keys: DataPointKey[]) => {
|
|
258
|
+
setSelectedPoints((prev) => {
|
|
259
|
+
const keySet = new Set(keys.map(keyToString));
|
|
260
|
+
const removed = prev.filter((p) => keySet.has(keyToString(p)));
|
|
261
|
+
const remaining = prev.filter((p) => !keySet.has(keyToString(p)));
|
|
262
|
+
notifyChange([], removed);
|
|
263
|
+
return remaining;
|
|
264
|
+
});
|
|
265
|
+
keys.forEach((key) => dispatchSelect(key, false));
|
|
266
|
+
},
|
|
267
|
+
[notifyChange, dispatchSelect]
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const invertSelection = useCallback(
|
|
271
|
+
(seriesIndex: number, dataIndices: number[]) => {
|
|
272
|
+
setSelectedPoints((prev) => {
|
|
273
|
+
const selectedSet = new Set(prev.filter((p) => p.seriesIndex === seriesIndex).map((p) => p.dataIndex));
|
|
274
|
+
const toSelect: DataPointKey[] = [];
|
|
275
|
+
const toDeselect: DataPointKey[] = [];
|
|
276
|
+
|
|
277
|
+
dataIndices.forEach((dataIndex) => {
|
|
278
|
+
const key = { seriesIndex, dataIndex };
|
|
279
|
+
const str = keyToString(key);
|
|
280
|
+
if (selectedSet.has(dataIndex)) {
|
|
281
|
+
toDeselect.push(key);
|
|
282
|
+
} else {
|
|
283
|
+
toSelect.push(key);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
toDeselect.forEach((k) => dispatchSelect(k, false));
|
|
288
|
+
toSelect.forEach((k) => dispatchSelect(k, true));
|
|
289
|
+
|
|
290
|
+
const newSelected = prev.filter(
|
|
291
|
+
(p) => !(p.seriesIndex === seriesIndex && selectedSet.has(p.dataIndex))
|
|
292
|
+
).concat(toSelect);
|
|
293
|
+
|
|
294
|
+
notifyChange(toSelect, toDeselect);
|
|
295
|
+
return newSelected;
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
[notifyChange, dispatchSelect]
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const selectAll = useCallback(
|
|
302
|
+
(seriesIndex: number, dataIndices: number[]) => {
|
|
303
|
+
const keys = dataIndices.map((dataIndex) => ({ seriesIndex, dataIndex }));
|
|
304
|
+
setSelectedPoints((prev) => {
|
|
305
|
+
const newPoints = mode === 'single' ? keys : [...prev, ...keys.filter(
|
|
306
|
+
(k) => !prev.some((p) => keyToString(p) === keyToString(k))
|
|
307
|
+
)];
|
|
308
|
+
notifyChange(newPoints, []);
|
|
309
|
+
return newPoints;
|
|
310
|
+
});
|
|
311
|
+
keys.forEach((key) => dispatchSelect(key, true));
|
|
312
|
+
},
|
|
313
|
+
[mode, notifyChange, dispatchSelect]
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const clearSelection = useCallback(() => {
|
|
317
|
+
const chart = chartRef.current;
|
|
318
|
+
if (chart?.dispatchAction) {
|
|
319
|
+
chart.dispatchAction({ type: 'unselect' });
|
|
320
|
+
}
|
|
321
|
+
const prev = selectedPoints;
|
|
322
|
+
setSelectedPoints([]);
|
|
323
|
+
notifyChange([], prev);
|
|
324
|
+
}, [selectedPoints, notifyChange]);
|
|
325
|
+
|
|
326
|
+
const isSelected = useCallback(
|
|
327
|
+
(key: DataPointKey) => {
|
|
328
|
+
const str = keyToString(key);
|
|
329
|
+
return selectedPoints.some((p) => keyToString(p) === str);
|
|
330
|
+
},
|
|
331
|
+
[selectedPoints]
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
selectedPoints,
|
|
336
|
+
hasSelection: selectedPoints.length > 0,
|
|
337
|
+
selectionCount: selectedPoints.length,
|
|
338
|
+
select,
|
|
339
|
+
deselect,
|
|
340
|
+
selectMultiple,
|
|
341
|
+
deselectMultiple,
|
|
342
|
+
toggle,
|
|
343
|
+
invertSelection,
|
|
344
|
+
selectAll,
|
|
345
|
+
clearSelection,
|
|
346
|
+
isSelected,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export default useChartSelection;
|
|
@@ -4,32 +4,46 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { useMemo } from 'react';
|
|
6
6
|
import type { EChartsOption } from 'echarts';
|
|
7
|
+
import {
|
|
8
|
+
DataItem,
|
|
9
|
+
DataSource,
|
|
10
|
+
AggregationType,
|
|
11
|
+
TimePeriod,
|
|
12
|
+
TransformMapping,
|
|
13
|
+
transformLineOrBar as utilsTransformLineOrBar,
|
|
14
|
+
transformPie as utilsTransformPie,
|
|
15
|
+
transformScatter as utilsTransformScatter,
|
|
16
|
+
transformRadar as utilsTransformRadar,
|
|
17
|
+
transformHeatmap as utilsTransformHeatmap,
|
|
18
|
+
groupByTime,
|
|
19
|
+
aggregateValues,
|
|
20
|
+
} from './utils/dataTransformUtils';
|
|
21
|
+
|
|
22
|
+
// Re-export types for external use
|
|
23
|
+
export type { DataItem, DataSource, AggregationType, TimePeriod, TransformMapping };
|
|
7
24
|
|
|
8
25
|
// ============================================================================
|
|
9
26
|
// 类型定义
|
|
10
27
|
// ============================================================================
|
|
11
28
|
|
|
12
|
-
/**
|
|
13
|
-
export interface
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
29
|
+
/** 映射配置 */
|
|
30
|
+
export interface MappingConfig {
|
|
31
|
+
/** 类别字段 (X轴) */
|
|
32
|
+
xField?: string;
|
|
33
|
+
/** 值字段 (Y轴) */
|
|
34
|
+
yField?: string;
|
|
35
|
+
/** 系列字段 */
|
|
36
|
+
seriesField?: string;
|
|
37
|
+
/** 大小字段 (用于散点图) */
|
|
38
|
+
sizeField?: string;
|
|
39
|
+
/** 颜色字段 */
|
|
40
|
+
colorField?: string;
|
|
41
|
+
/** 名称字段 (用于饼图等) */
|
|
42
|
+
nameField?: string;
|
|
43
|
+
/** 值字段 (用于饼图等) */
|
|
44
|
+
valueField?: string;
|
|
17
45
|
}
|
|
18
46
|
|
|
19
|
-
/** 数据源 */
|
|
20
|
-
export interface DataSource {
|
|
21
|
-
categories?: (string | number)[];
|
|
22
|
-
series?: DataItem[];
|
|
23
|
-
rows?: Record<string, unknown>[];
|
|
24
|
-
columns?: string[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** 聚合方式 */
|
|
28
|
-
export type AggregationType = 'sum' | 'average' | 'max' | 'min' | 'count' | 'first' | 'last';
|
|
29
|
-
|
|
30
|
-
/** 时间周期 */
|
|
31
|
-
export type TimePeriod = 'day' | 'week' | 'month' | 'quarter' | 'year';
|
|
32
|
-
|
|
33
47
|
/** 转换选项 */
|
|
34
48
|
export interface TransformOptions {
|
|
35
49
|
/** 数据源 */
|
|
@@ -37,22 +51,7 @@ export interface TransformOptions {
|
|
|
37
51
|
/** 图表类型 */
|
|
38
52
|
chartType: 'line' | 'bar' | 'pie' | 'scatter' | 'radar' | 'heatmap';
|
|
39
53
|
/** 映射配置 */
|
|
40
|
-
mapping?:
|
|
41
|
-
/** 类别字段 (X轴) */
|
|
42
|
-
xField?: string;
|
|
43
|
-
/** 值字段 (Y轴) */
|
|
44
|
-
yField?: string;
|
|
45
|
-
/** 系列字段 */
|
|
46
|
-
seriesField?: string;
|
|
47
|
-
/** 大小字段 (用于散点图) */
|
|
48
|
-
sizeField?: string;
|
|
49
|
-
/** 颜色字段 */
|
|
50
|
-
colorField?: string;
|
|
51
|
-
/** 名称字段 (用于饼图等) */
|
|
52
|
-
nameField?: string;
|
|
53
|
-
/** 值字段 (用于饼图等) */
|
|
54
|
-
valueField?: string;
|
|
55
|
-
};
|
|
54
|
+
mapping?: MappingConfig;
|
|
56
55
|
/** 聚合配置 */
|
|
57
56
|
aggregation?: {
|
|
58
57
|
field: string;
|
|
@@ -122,19 +121,19 @@ export function useDataTransform(options: TransformOptions): EChartsOption {
|
|
|
122
121
|
switch (chartType) {
|
|
123
122
|
case 'line':
|
|
124
123
|
case 'bar':
|
|
125
|
-
return
|
|
124
|
+
return utilsTransformLineOrBar(data, chartType, mapping, extraConfig);
|
|
126
125
|
|
|
127
126
|
case 'pie':
|
|
128
|
-
return
|
|
127
|
+
return utilsTransformPie(data, mapping, extraConfig);
|
|
129
128
|
|
|
130
129
|
case 'scatter':
|
|
131
|
-
return
|
|
130
|
+
return utilsTransformScatter(data, mapping, extraConfig);
|
|
132
131
|
|
|
133
132
|
case 'radar':
|
|
134
|
-
return
|
|
133
|
+
return utilsTransformRadar(data, mapping, extraConfig);
|
|
135
134
|
|
|
136
135
|
case 'heatmap':
|
|
137
|
-
return
|
|
136
|
+
return utilsTransformHeatmap(data, mapping, extraConfig);
|
|
138
137
|
|
|
139
138
|
default:
|
|
140
139
|
return {};
|
|
@@ -244,252 +243,6 @@ export function useTimeSeriesTransform(options: TimeSeriesTransformOptions): ECh
|
|
|
244
243
|
}, [data, dateField, valueField, groupField, period, aggregation, fillMissing, extraConfig]);
|
|
245
244
|
}
|
|
246
245
|
|
|
247
|
-
// ============================================================================
|
|
248
|
-
// 辅助函数
|
|
249
|
-
// ============================================================================
|
|
250
|
-
|
|
251
|
-
function transformLineOrBar(
|
|
252
|
-
data: DataSource,
|
|
253
|
-
chartType: 'line' | 'bar',
|
|
254
|
-
mapping: TransformOptions['mapping'],
|
|
255
|
-
extraConfig: Partial<EChartsOption>
|
|
256
|
-
): EChartsOption {
|
|
257
|
-
const { xField = 'name', yField = 'value', seriesField } = mapping || {};
|
|
258
|
-
|
|
259
|
-
const categories = data.categories || data.rows?.map((r) => String(r[xField])) || [];
|
|
260
|
-
const seriesData = data.series || data.rows || [];
|
|
261
|
-
|
|
262
|
-
if (seriesField) {
|
|
263
|
-
const groups = new Map<string, DataItem[]>();
|
|
264
|
-
seriesData.forEach((item) => {
|
|
265
|
-
const key = String((item as Record<string, unknown>)[seriesField] || 'default');
|
|
266
|
-
if (!groups.has(key)) groups.set(key, []);
|
|
267
|
-
groups.get(key)!.push(item);
|
|
268
|
-
});
|
|
269
|
-
const series = Array.from(groups.entries()).map(([name, items]) => ({
|
|
270
|
-
name,
|
|
271
|
-
type: chartType,
|
|
272
|
-
data: items.map((item) => (item as Record<string, unknown>)[yField] ?? 0),
|
|
273
|
-
}));
|
|
274
|
-
return {
|
|
275
|
-
xAxis: { type: 'category', data: categories },
|
|
276
|
-
yAxis: { type: 'value' },
|
|
277
|
-
series,
|
|
278
|
-
...extraConfig,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const series = [
|
|
283
|
-
{
|
|
284
|
-
type: chartType as 'line' | 'bar',
|
|
285
|
-
data: seriesData.map((item) => (item as Record<string, unknown>)[yField] ?? 0),
|
|
286
|
-
},
|
|
287
|
-
];
|
|
288
|
-
|
|
289
|
-
return {
|
|
290
|
-
xAxis: { type: 'category', data: categories },
|
|
291
|
-
yAxis: { type: 'value' },
|
|
292
|
-
series,
|
|
293
|
-
...extraConfig,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function transformPie(
|
|
298
|
-
data: DataSource,
|
|
299
|
-
mapping: TransformOptions['mapping'],
|
|
300
|
-
extraConfig: Partial<EChartsOption>
|
|
301
|
-
): EChartsOption {
|
|
302
|
-
const { nameField = 'name', valueField = 'value' } = mapping || {};
|
|
303
|
-
|
|
304
|
-
const seriesData: Array<{ name: string; value: number }> = (data.series || data.rows || []).map(
|
|
305
|
-
(item) => ({
|
|
306
|
-
name: String((item as Record<string, unknown>)[nameField] || ''),
|
|
307
|
-
value: Number((item as Record<string, unknown>)[valueField]) || 0,
|
|
308
|
-
})
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
return {
|
|
312
|
-
series: [{ type: 'pie', radius: '60%', data: seriesData }],
|
|
313
|
-
...extraConfig,
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function transformScatter(
|
|
318
|
-
data: DataSource,
|
|
319
|
-
mapping: TransformOptions['mapping'],
|
|
320
|
-
extraConfig: Partial<EChartsOption>
|
|
321
|
-
): EChartsOption {
|
|
322
|
-
const { xField = 'x', yField = 'y', sizeField } = mapping || {};
|
|
323
|
-
|
|
324
|
-
const seriesData = (data.series || data.rows || []).map((item) => {
|
|
325
|
-
const record = item as Record<string, unknown>;
|
|
326
|
-
const point: (number | string)[] = [Number(record[xField]) || 0, Number(record[yField]) || 0];
|
|
327
|
-
if (sizeField) point.push(Number(record[sizeField]) || 1);
|
|
328
|
-
return point;
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
xAxis: { type: 'value', scale: true },
|
|
333
|
-
yAxis: { type: 'value', scale: true },
|
|
334
|
-
series: [{ type: 'scatter', data: seriesData }],
|
|
335
|
-
...extraConfig,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function transformRadar(
|
|
340
|
-
data: DataSource,
|
|
341
|
-
mapping: TransformOptions['mapping'],
|
|
342
|
-
extraConfig: Partial<EChartsOption>
|
|
343
|
-
): EChartsOption {
|
|
344
|
-
const { nameField = 'name', valueField = 'value' } = mapping || {};
|
|
345
|
-
|
|
346
|
-
const indicators = (data.series || data.rows || []).map((item) => {
|
|
347
|
-
const record = item as Record<string, unknown>;
|
|
348
|
-
return {
|
|
349
|
-
name: String(record[nameField] || ''),
|
|
350
|
-
max: Math.max(Number(record[valueField]) || 100, 100),
|
|
351
|
-
};
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
const values = (data.series || data.rows || []).map(
|
|
355
|
-
(item) => Number((item as Record<string, unknown>)[valueField]) || 0
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
return {
|
|
359
|
-
radar: { indicator: indicators },
|
|
360
|
-
series: [{ type: 'radar', data: [{ value: values }] }],
|
|
361
|
-
...extraConfig,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function transformHeatmap(
|
|
366
|
-
data: DataSource,
|
|
367
|
-
mapping: TransformOptions['mapping'],
|
|
368
|
-
extraConfig: Partial<EChartsOption>
|
|
369
|
-
): EChartsOption {
|
|
370
|
-
const { xField = 'x', yField = 'y', valueField = 'value' } = mapping || {};
|
|
371
|
-
|
|
372
|
-
const xCategories = [
|
|
373
|
-
...new Set(
|
|
374
|
-
(data.series || data.rows || []).map((d) => String((d as Record<string, unknown>)[xField]))
|
|
375
|
-
),
|
|
376
|
-
];
|
|
377
|
-
const yCategories = [
|
|
378
|
-
...new Set(
|
|
379
|
-
(data.series || data.rows || []).map((d) => String((d as Record<string, unknown>)[yField]))
|
|
380
|
-
),
|
|
381
|
-
];
|
|
382
|
-
|
|
383
|
-
const seriesData = (data.series || data.rows || []).map((item) => {
|
|
384
|
-
const record = item as Record<string, unknown>;
|
|
385
|
-
const xIndex = xCategories.indexOf(String(record[xField]));
|
|
386
|
-
const yIndex = yCategories.indexOf(String(record[yField]));
|
|
387
|
-
return [xIndex, yIndex, Number(record[valueField]) || 0];
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
return {
|
|
391
|
-
xAxis: { type: 'category', data: xCategories },
|
|
392
|
-
yAxis: { type: 'category', data: yCategories },
|
|
393
|
-
visualMap: { min: 0, calculable: true },
|
|
394
|
-
series: [{ type: 'heatmap', data: seriesData }],
|
|
395
|
-
...extraConfig,
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function groupByTime(
|
|
400
|
-
data: DataItem[],
|
|
401
|
-
dateField: string,
|
|
402
|
-
period: TimePeriod
|
|
403
|
-
): Record<string, DataItem[]> {
|
|
404
|
-
return data.reduce(
|
|
405
|
-
(acc, item) => {
|
|
406
|
-
const date = new Date(String((item as Record<string, unknown>)[dateField]));
|
|
407
|
-
let key: string;
|
|
408
|
-
|
|
409
|
-
switch (period) {
|
|
410
|
-
case 'day':
|
|
411
|
-
key = date.toISOString().split('T')[0];
|
|
412
|
-
break;
|
|
413
|
-
case 'week': {
|
|
414
|
-
const week = getWeekNumber(date);
|
|
415
|
-
key = `${date.getFullYear()}-W${week}`;
|
|
416
|
-
break;
|
|
417
|
-
}
|
|
418
|
-
case 'month':
|
|
419
|
-
key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
420
|
-
break;
|
|
421
|
-
case 'quarter':
|
|
422
|
-
key = `${date.getFullYear()}-Q${Math.ceil((date.getMonth() + 1) / 3)}`;
|
|
423
|
-
break;
|
|
424
|
-
case 'year':
|
|
425
|
-
key = String(date.getFullYear());
|
|
426
|
-
break;
|
|
427
|
-
default:
|
|
428
|
-
key = date.toISOString().split('T')[0];
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (!acc[key]) acc[key] = [];
|
|
432
|
-
acc[key].push(item);
|
|
433
|
-
return acc;
|
|
434
|
-
},
|
|
435
|
-
{} as Record<string, DataItem[]>
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function getWeekNumber(date: Date): number {
|
|
440
|
-
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
441
|
-
const dayNum = d.getUTCDay() || 7;
|
|
442
|
-
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
443
|
-
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
444
|
-
return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function aggregateValues(
|
|
448
|
-
items: DataItem[],
|
|
449
|
-
field: string,
|
|
450
|
-
method: AggregationType,
|
|
451
|
-
fillMissing?: 'zero' | 'forward' | 'interpolate'
|
|
452
|
-
): number {
|
|
453
|
-
if (items.length === 0) {
|
|
454
|
-
if (fillMissing === 'zero') return 0;
|
|
455
|
-
return NaN;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const values = items.map((item) => Number((item as Record<string, unknown>)[field]) || 0);
|
|
459
|
-
|
|
460
|
-
switch (method) {
|
|
461
|
-
case 'sum': {
|
|
462
|
-
let sum = 0;
|
|
463
|
-
for (let i = 0; i < values.length; i++) sum += values[i];
|
|
464
|
-
return sum;
|
|
465
|
-
}
|
|
466
|
-
case 'average': {
|
|
467
|
-
if (values.length === 0) return 0;
|
|
468
|
-
let sum = 0;
|
|
469
|
-
for (let i = 0; i < values.length; i++) sum += values[i];
|
|
470
|
-
return sum / values.length;
|
|
471
|
-
}
|
|
472
|
-
case 'max': {
|
|
473
|
-
let max = values[0];
|
|
474
|
-
for (let i = 1; i < values.length; i++) if (values[i] > max) max = values[i];
|
|
475
|
-
return max;
|
|
476
|
-
}
|
|
477
|
-
case 'min': {
|
|
478
|
-
let min = values[0];
|
|
479
|
-
for (let i = 1; i < values.length; i++) if (values[i] < min) min = values[i];
|
|
480
|
-
return min;
|
|
481
|
-
}
|
|
482
|
-
case 'count':
|
|
483
|
-
return values.length;
|
|
484
|
-
case 'first':
|
|
485
|
-
return values[0];
|
|
486
|
-
case 'last':
|
|
487
|
-
return values[values.length - 1];
|
|
488
|
-
default:
|
|
489
|
-
return values[0];
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
246
|
// ============================================================================
|
|
494
247
|
// 导出
|
|
495
248
|
// ============================================================================
|