@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.
@@ -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
+ }