@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,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart Download Utilities
|
|
3
|
+
* 图表下载工具函数
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 生成默认文件名
|
|
8
|
+
*/
|
|
9
|
+
export function generateFilename(prefix: string = 'chart'): string {
|
|
10
|
+
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, '');
|
|
11
|
+
return `${prefix}_${timestamp}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 下载 Blob 对象
|
|
16
|
+
*/
|
|
17
|
+
export function downloadBlob(blob: Blob, filename: string): void {
|
|
18
|
+
const url = URL.createObjectURL(blob);
|
|
19
|
+
const link = document.createElement('a');
|
|
20
|
+
link.href = url;
|
|
21
|
+
link.download = filename;
|
|
22
|
+
document.body.appendChild(link);
|
|
23
|
+
link.click();
|
|
24
|
+
document.body.removeChild(link);
|
|
25
|
+
URL.revokeObjectURL(url);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 下载数据 URL
|
|
30
|
+
*/
|
|
31
|
+
export function downloadDataUrl(dataUrl: string, filename: string): void {
|
|
32
|
+
const link = document.createElement('a');
|
|
33
|
+
link.href = dataUrl;
|
|
34
|
+
link.download = filename;
|
|
35
|
+
document.body.appendChild(link);
|
|
36
|
+
link.click();
|
|
37
|
+
document.body.removeChild(link);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* CSV 转 Blob
|
|
42
|
+
*/
|
|
43
|
+
export function csvToBlob(csv: string, _filename: string): Blob {
|
|
44
|
+
return new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* JSON 转 Blob
|
|
49
|
+
*/
|
|
50
|
+
export function jsonToBlob(json: string, _filename: string): Blob {
|
|
51
|
+
return new Blob([json], { type: 'application/json;charset=utf-8;' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 将数据转换为 CSV 格式
|
|
56
|
+
*/
|
|
57
|
+
export function convertToCSV(data: unknown, options?: { includeLabels?: boolean }): string {
|
|
58
|
+
if (!data) return '';
|
|
59
|
+
|
|
60
|
+
// 处理 ECharts 格式的数据
|
|
61
|
+
if (typeof data === 'object' && (data as { series?: unknown }).series) {
|
|
62
|
+
return convertSeriesToCSV(data as { series?: unknown[]; xAxis?: { data?: unknown[] }; dataset?: { dimensions?: string[]; source?: unknown[][] } }, options);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 处理数组数据
|
|
66
|
+
if (Array.isArray(data)) {
|
|
67
|
+
return convertArrayToCSV(data);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 处理普通对象
|
|
71
|
+
if (typeof data === 'object') {
|
|
72
|
+
return convertObjectToCSV(data as Record<string, unknown>);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return String(data);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 将 ECharts series 数据转换为 CSV
|
|
80
|
+
*/
|
|
81
|
+
function convertSeriesToCSV(
|
|
82
|
+
chartData: { series?: unknown[]; xAxis?: { data?: unknown[] }; dataset?: { dimensions?: string[]; source?: unknown[][] } },
|
|
83
|
+
options?: { includeLabels?: boolean }
|
|
84
|
+
): string {
|
|
85
|
+
const { series = [], xAxis, dataset } = chartData;
|
|
86
|
+
const includeLabels = options?.includeLabels ?? true;
|
|
87
|
+
|
|
88
|
+
if (!Array.isArray(series) || series.length === 0) return '';
|
|
89
|
+
|
|
90
|
+
// 获取类别轴数据
|
|
91
|
+
let categories: unknown[] = [];
|
|
92
|
+
if (xAxis?.data && Array.isArray(xAxis.data)) {
|
|
93
|
+
categories = xAxis.data;
|
|
94
|
+
} else if (dataset?.dimensions && dataset?.source) {
|
|
95
|
+
categories = dataset.source.map((row: unknown[]) => row[0]);
|
|
96
|
+
} else if ((series[0] as { data?: unknown[] })?.data) {
|
|
97
|
+
const firstSeries = series[0] as { data: unknown[] };
|
|
98
|
+
categories = firstSeries.data.map((item: unknown, index: number) =>
|
|
99
|
+
typeof item === 'object' && item !== null ? (item as unknown[])[0] || index : index
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 构建 CSV 头
|
|
104
|
+
const headers = includeLabels
|
|
105
|
+
? ['Category', ...series.map((s: unknown) => (s as { name?: string; seriesIndex?: number }).name || (s as { seriesIndex?: number }).seriesIndex)]
|
|
106
|
+
: [];
|
|
107
|
+
|
|
108
|
+
// 构建 CSV 行
|
|
109
|
+
const rows: unknown[][] = [];
|
|
110
|
+
|
|
111
|
+
series.forEach((s: unknown, seriesIndex: number) => {
|
|
112
|
+
const seriesObj = s as { data?: unknown[]; name?: string; seriesIndex?: number };
|
|
113
|
+
const seriesData = seriesObj.data || [];
|
|
114
|
+
seriesData.forEach((item: unknown, dataIndex: number) => {
|
|
115
|
+
const value = typeof item === 'object' && item !== null ? (item as unknown[])[1] : item;
|
|
116
|
+
const category = categories[dataIndex] || dataIndex;
|
|
117
|
+
|
|
118
|
+
if (includeLabels) {
|
|
119
|
+
if (seriesIndex === 0) {
|
|
120
|
+
rows[dataIndex] = [category, value];
|
|
121
|
+
} else {
|
|
122
|
+
rows[dataIndex] = rows[dataIndex] || [category];
|
|
123
|
+
rows[dataIndex].push(value);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
if (seriesIndex === 0) {
|
|
127
|
+
rows[dataIndex] = [value];
|
|
128
|
+
} else {
|
|
129
|
+
rows[dataIndex] = rows[dataIndex] || [];
|
|
130
|
+
rows[dataIndex].push(value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// 生成 CSV 字符串
|
|
137
|
+
const csvRows = includeLabels
|
|
138
|
+
? [headers.join(','), ...rows.map((row) => row.join(','))]
|
|
139
|
+
: rows.map((row) => row.join(','));
|
|
140
|
+
|
|
141
|
+
return csvRows.join('\n');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 将数组数据转换为 CSV
|
|
146
|
+
*/
|
|
147
|
+
function convertArrayToCSV(data: unknown[]): string {
|
|
148
|
+
if (data.length === 0) return '';
|
|
149
|
+
|
|
150
|
+
// 检查是否为对象数组
|
|
151
|
+
if (typeof data[0] === 'object' && data[0] !== null) {
|
|
152
|
+
const keys = Object.keys(data[0]);
|
|
153
|
+
const headers = keys.join(',');
|
|
154
|
+
const rows = data.map((item) =>
|
|
155
|
+
keys.map((key) => JSON.stringify((item as Record<string, unknown>)[key] ?? '')).join(',')
|
|
156
|
+
);
|
|
157
|
+
return [headers, ...rows].join('\n');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 简单数组
|
|
161
|
+
return data.join('\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 将对象数据转换为 CSV
|
|
166
|
+
*/
|
|
167
|
+
function convertObjectToCSV(data: Record<string, unknown>): string {
|
|
168
|
+
const entries = Object.entries(data);
|
|
169
|
+
return entries.map(([key, value]) => `${key},${JSON.stringify(value)}`).join('\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 将 ECharts 数据转换为 JSON
|
|
174
|
+
*/
|
|
175
|
+
export function convertToJSON(data: unknown): string {
|
|
176
|
+
if (!data) return '{}';
|
|
177
|
+
|
|
178
|
+
const dataObj = data as { series?: unknown[]; title?: { text?: string }; legend?: { data?: unknown }; xAxis?: { data?: unknown[] } };
|
|
179
|
+
|
|
180
|
+
// 如果是 ECharts 格式,简化数据
|
|
181
|
+
if (dataObj.series) {
|
|
182
|
+
const simplified = {
|
|
183
|
+
title: dataObj.title?.text,
|
|
184
|
+
legend: dataObj.legend?.data,
|
|
185
|
+
xAxis: dataObj.xAxis?.data,
|
|
186
|
+
series: dataObj.series.map((s: unknown) => {
|
|
187
|
+
const seriesObj = s as { name?: string; type?: string; data?: unknown[] };
|
|
188
|
+
return {
|
|
189
|
+
name: seriesObj.name,
|
|
190
|
+
type: seriesObj.type,
|
|
191
|
+
data: seriesObj.data?.map((item: unknown) =>
|
|
192
|
+
typeof item === 'object' && item !== null ? (item as unknown[])[1] : item
|
|
193
|
+
),
|
|
194
|
+
};
|
|
195
|
+
}),
|
|
196
|
+
};
|
|
197
|
+
return JSON.stringify(simplified, null, 2);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return JSON.stringify(data, null, 2);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 从图片创建 PDF DataURL
|
|
205
|
+
*/
|
|
206
|
+
export async function createPdfFromImage(
|
|
207
|
+
imageDataUrl: string,
|
|
208
|
+
title?: string
|
|
209
|
+
): Promise<string | null> {
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
try {
|
|
212
|
+
// 创建画布
|
|
213
|
+
const canvas = document.createElement('canvas');
|
|
214
|
+
const ctx = canvas.getContext('2d');
|
|
215
|
+
if (!ctx) {
|
|
216
|
+
resolve(null);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 加载图片
|
|
221
|
+
const img = new Image();
|
|
222
|
+
img.onload = () => {
|
|
223
|
+
// 设置 PDF 尺寸(A4 纵向)
|
|
224
|
+
const pdfWidth = 595.28; // A4 width in points
|
|
225
|
+
const pdfHeight = 841.89; // A4 height in points
|
|
226
|
+
|
|
227
|
+
canvas.width = pdfWidth;
|
|
228
|
+
canvas.height = pdfHeight;
|
|
229
|
+
|
|
230
|
+
// 填充背景
|
|
231
|
+
ctx.fillStyle = '#ffffff';
|
|
232
|
+
ctx.fillRect(0, 0, pdfWidth, pdfHeight);
|
|
233
|
+
|
|
234
|
+
// 计算图片位置和尺寸(居中)
|
|
235
|
+
const imgRatio = img.width / img.height;
|
|
236
|
+
const canvasRatio = pdfWidth / pdfHeight;
|
|
237
|
+
let drawWidth: number, drawHeight: number, offsetX: number, offsetY: number;
|
|
238
|
+
|
|
239
|
+
if (imgRatio > canvasRatio) {
|
|
240
|
+
drawWidth = pdfWidth * 0.8;
|
|
241
|
+
drawHeight = drawWidth / imgRatio;
|
|
242
|
+
} else {
|
|
243
|
+
drawHeight = pdfHeight * 0.6;
|
|
244
|
+
drawWidth = drawHeight * imgRatio;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
offsetX = (pdfWidth - drawWidth) / 2;
|
|
248
|
+
offsetY = (pdfHeight - drawHeight) / 2;
|
|
249
|
+
|
|
250
|
+
// 绘制图片
|
|
251
|
+
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
|
|
252
|
+
|
|
253
|
+
// 添加标题
|
|
254
|
+
ctx.fillStyle = '#333333';
|
|
255
|
+
ctx.font = '16px Arial';
|
|
256
|
+
ctx.textAlign = 'center';
|
|
257
|
+
ctx.fillText(title || 'Chart Export', pdfWidth / 2, offsetY - 20);
|
|
258
|
+
|
|
259
|
+
// 输出为 PNG(实际应用中应该使用 jsPDF 生成真正的 PDF)
|
|
260
|
+
resolve(canvas.toDataURL('image/png'));
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
img.onerror = () => {
|
|
264
|
+
resolve(null);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
img.src = imageDataUrl;
|
|
268
|
+
} catch (e) {
|
|
269
|
+
console.warn('[chartDownloadUtils] Failed to create PDF:', e);
|
|
270
|
+
resolve(null);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Transform Utilities
|
|
3
|
+
* 数据转换工具函数
|
|
4
|
+
*/
|
|
5
|
+
import type { EChartsOption } from 'echarts';
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// 类型定义
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/** 原始数据条目 */
|
|
12
|
+
export interface DataItem {
|
|
13
|
+
name?: string;
|
|
14
|
+
value?: number | number[];
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 数据源 */
|
|
19
|
+
export interface DataSource {
|
|
20
|
+
categories?: (string | number)[];
|
|
21
|
+
series?: DataItem[];
|
|
22
|
+
rows?: Record<string, unknown>[];
|
|
23
|
+
columns?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 聚合方式 */
|
|
27
|
+
export type AggregationType = 'sum' | 'average' | 'max' | 'min' | 'count' | 'first' | 'last';
|
|
28
|
+
|
|
29
|
+
/** 时间周期 */
|
|
30
|
+
export type TimePeriod = 'day' | 'week' | 'month' | 'quarter' | 'year';
|
|
31
|
+
|
|
32
|
+
/** 映射配置 */
|
|
33
|
+
export interface TransformMapping {
|
|
34
|
+
xField?: string;
|
|
35
|
+
yField?: string;
|
|
36
|
+
seriesField?: string;
|
|
37
|
+
sizeField?: string;
|
|
38
|
+
colorField?: string;
|
|
39
|
+
nameField?: string;
|
|
40
|
+
valueField?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// 转换函数
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
export function transformLineOrBar(
|
|
48
|
+
data: DataSource,
|
|
49
|
+
chartType: 'line' | 'bar',
|
|
50
|
+
mapping: TransformMapping,
|
|
51
|
+
extraConfig: Partial<EChartsOption>
|
|
52
|
+
): EChartsOption {
|
|
53
|
+
const { xField = 'name', yField = 'value', seriesField } = mapping || {};
|
|
54
|
+
|
|
55
|
+
const categories = data.categories || data.rows?.map((r) => String(r[xField])) || [];
|
|
56
|
+
const seriesData = data.series || data.rows || [];
|
|
57
|
+
|
|
58
|
+
if (seriesField) {
|
|
59
|
+
const groups = new Map<string, DataItem[]>();
|
|
60
|
+
seriesData.forEach((item) => {
|
|
61
|
+
const key = String((item as Record<string, unknown>)[seriesField] || 'default');
|
|
62
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
63
|
+
groups.get(key)!.push(item);
|
|
64
|
+
});
|
|
65
|
+
const series = Array.from(groups.entries()).map(([name, items]) => ({
|
|
66
|
+
name,
|
|
67
|
+
type: chartType,
|
|
68
|
+
data: items.map((item) => (item as Record<string, unknown>)[yField] ?? 0),
|
|
69
|
+
}));
|
|
70
|
+
return {
|
|
71
|
+
xAxis: { type: 'category', data: categories },
|
|
72
|
+
yAxis: { type: 'value' },
|
|
73
|
+
series,
|
|
74
|
+
...extraConfig,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const series = [
|
|
79
|
+
{
|
|
80
|
+
type: chartType as 'line' | 'bar',
|
|
81
|
+
data: seriesData.map((item) => (item as Record<string, unknown>)[yField] ?? 0),
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
xAxis: { type: 'category', data: categories },
|
|
87
|
+
yAxis: { type: 'value' },
|
|
88
|
+
series,
|
|
89
|
+
...extraConfig,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function transformPie(
|
|
94
|
+
data: DataSource,
|
|
95
|
+
mapping: TransformMapping,
|
|
96
|
+
extraConfig: Partial<EChartsOption>
|
|
97
|
+
): EChartsOption {
|
|
98
|
+
const { nameField = 'name', valueField = 'value' } = mapping || {};
|
|
99
|
+
|
|
100
|
+
const seriesData: Array<{ name: string; value: number }> = (data.series || data.rows || []).map(
|
|
101
|
+
(item) => ({
|
|
102
|
+
name: String((item as Record<string, unknown>)[nameField] || ''),
|
|
103
|
+
value: Number((item as Record<string, unknown>)[valueField]) || 0,
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
series: [{ type: 'pie', radius: '60%', data: seriesData }],
|
|
109
|
+
...extraConfig,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function transformScatter(
|
|
114
|
+
data: DataSource,
|
|
115
|
+
mapping: TransformMapping,
|
|
116
|
+
extraConfig: Partial<EChartsOption>
|
|
117
|
+
): EChartsOption {
|
|
118
|
+
const { xField = 'x', yField = 'y', sizeField } = mapping || {};
|
|
119
|
+
|
|
120
|
+
const seriesData = (data.series || data.rows || []).map((item) => {
|
|
121
|
+
const record = item as Record<string, unknown>;
|
|
122
|
+
const point: (number | string)[] = [Number(record[xField]) || 0, Number(record[yField]) || 0];
|
|
123
|
+
if (sizeField) point.push(Number(record[sizeField]) || 1);
|
|
124
|
+
return point;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
xAxis: { type: 'value', scale: true },
|
|
129
|
+
yAxis: { type: 'value', scale: true },
|
|
130
|
+
series: [{ type: 'scatter', data: seriesData }],
|
|
131
|
+
...extraConfig,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function transformRadar(
|
|
136
|
+
data: DataSource,
|
|
137
|
+
mapping: TransformMapping,
|
|
138
|
+
extraConfig: Partial<EChartsOption>
|
|
139
|
+
): EChartsOption {
|
|
140
|
+
const { nameField = 'name', valueField = 'value' } = mapping || {};
|
|
141
|
+
|
|
142
|
+
const indicators = (data.series || data.rows || []).map((item) => {
|
|
143
|
+
const record = item as Record<string, unknown>;
|
|
144
|
+
return {
|
|
145
|
+
name: String(record[nameField] || ''),
|
|
146
|
+
max: Math.max(Number(record[valueField]) || 100, 100),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const values = (data.series || data.rows || []).map(
|
|
151
|
+
(item) => Number((item as Record<string, unknown>)[valueField]) || 0
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
radar: { indicator: indicators },
|
|
156
|
+
series: [{ type: 'radar', data: [{ value: values }] }],
|
|
157
|
+
...extraConfig,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function transformHeatmap(
|
|
162
|
+
data: DataSource,
|
|
163
|
+
mapping: TransformMapping,
|
|
164
|
+
extraConfig: Partial<EChartsOption>
|
|
165
|
+
): EChartsOption {
|
|
166
|
+
const { xField = 'x', yField = 'y', valueField = 'value' } = mapping || {};
|
|
167
|
+
|
|
168
|
+
const xCategories = [
|
|
169
|
+
...new Set(
|
|
170
|
+
(data.series || data.rows || []).map((d) => String((d as Record<string, unknown>)[xField]))
|
|
171
|
+
),
|
|
172
|
+
];
|
|
173
|
+
const yCategories = [
|
|
174
|
+
...new Set(
|
|
175
|
+
(data.series || data.rows || []).map((d) => String((d as Record<string, unknown>)[yField]))
|
|
176
|
+
),
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const seriesData = (data.series || data.rows || []).map((item) => {
|
|
180
|
+
const record = item as Record<string, unknown>;
|
|
181
|
+
const xIndex = xCategories.indexOf(String(record[xField]));
|
|
182
|
+
const yIndex = yCategories.indexOf(String(record[yField]));
|
|
183
|
+
return [xIndex, yIndex, Number(record[valueField]) || 0];
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
xAxis: { type: 'category', data: xCategories },
|
|
188
|
+
yAxis: { type: 'category', data: yCategories },
|
|
189
|
+
visualMap: { min: 0, calculable: true },
|
|
190
|
+
series: [{ type: 'heatmap', data: seriesData }],
|
|
191
|
+
...extraConfig,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function groupByTime(
|
|
196
|
+
data: DataItem[],
|
|
197
|
+
dateField: string,
|
|
198
|
+
period: TimePeriod
|
|
199
|
+
): Record<string, DataItem[]> {
|
|
200
|
+
return data.reduce(
|
|
201
|
+
(acc, item) => {
|
|
202
|
+
const date = new Date(String((item as Record<string, unknown>)[dateField]));
|
|
203
|
+
let key: string;
|
|
204
|
+
|
|
205
|
+
switch (period) {
|
|
206
|
+
case 'day':
|
|
207
|
+
key = date.toISOString().split('T')[0];
|
|
208
|
+
break;
|
|
209
|
+
case 'week': {
|
|
210
|
+
const week = getWeekNumber(date);
|
|
211
|
+
key = `${date.getFullYear()}-W${week}`;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case 'month':
|
|
215
|
+
key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
216
|
+
break;
|
|
217
|
+
case 'quarter':
|
|
218
|
+
key = `${date.getFullYear()}-Q${Math.ceil((date.getMonth() + 1) / 3)}`;
|
|
219
|
+
break;
|
|
220
|
+
case 'year':
|
|
221
|
+
key = String(date.getFullYear());
|
|
222
|
+
break;
|
|
223
|
+
default:
|
|
224
|
+
key = date.toISOString().split('T')[0];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!acc[key]) acc[key] = [];
|
|
228
|
+
acc[key].push(item);
|
|
229
|
+
return acc;
|
|
230
|
+
},
|
|
231
|
+
{} as Record<string, DataItem[]>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getWeekNumber(date: Date): number {
|
|
236
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
237
|
+
const dayNum = d.getUTCDay() || 7;
|
|
238
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
239
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
240
|
+
return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function aggregateValues(
|
|
244
|
+
items: DataItem[],
|
|
245
|
+
field: string,
|
|
246
|
+
method: AggregationType,
|
|
247
|
+
fillMissing?: 'zero' | 'forward' | 'interpolate'
|
|
248
|
+
): number {
|
|
249
|
+
if (items.length === 0) {
|
|
250
|
+
if (fillMissing === 'zero') return 0;
|
|
251
|
+
return NaN;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const values = items.map((item) => Number((item as Record<string, unknown>)[field]) || 0);
|
|
255
|
+
|
|
256
|
+
switch (method) {
|
|
257
|
+
case 'sum': {
|
|
258
|
+
let sum = 0;
|
|
259
|
+
for (let i = 0; i < values.length; i++) sum += values[i];
|
|
260
|
+
return sum;
|
|
261
|
+
}
|
|
262
|
+
case 'average': {
|
|
263
|
+
if (values.length === 0) return 0;
|
|
264
|
+
let sum = 0;
|
|
265
|
+
for (let i = 0; i < values.length; i++) sum += values[i];
|
|
266
|
+
return sum / values.length;
|
|
267
|
+
}
|
|
268
|
+
case 'max': {
|
|
269
|
+
let max = values[0];
|
|
270
|
+
for (let i = 1; i < values.length; i++) if (values[i] > max) max = values[i];
|
|
271
|
+
return max;
|
|
272
|
+
}
|
|
273
|
+
case 'min': {
|
|
274
|
+
let min = values[0];
|
|
275
|
+
for (let i = 1; i < values.length; i++) if (values[i] < min) min = values[i];
|
|
276
|
+
return min;
|
|
277
|
+
}
|
|
278
|
+
case 'count':
|
|
279
|
+
return values.length;
|
|
280
|
+
case 'first':
|
|
281
|
+
return values[0];
|
|
282
|
+
case 'last':
|
|
283
|
+
return values[values.length - 1];
|
|
284
|
+
default:
|
|
285
|
+
return values[0];
|
|
286
|
+
}
|
|
287
|
+
}
|