@allternit/viz 0.1.0
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 +217 -0
- package/dist/charts/canvas-renderer.d.ts +40 -0
- package/dist/charts/canvas-renderer.d.ts.bak +40 -0
- package/dist/charts/canvas-renderer.d.ts.map +1 -0
- package/dist/charts/canvas-renderer.js +580 -0
- package/dist/charts/canvas-renderer.js.bak +580 -0
- package/dist/charts/canvas-renderer.js.map +1 -0
- package/dist/charts/svg-renderer.d.ts +21 -0
- package/dist/charts/svg-renderer.d.ts.bak +21 -0
- package/dist/charts/svg-renderer.d.ts.map +1 -0
- package/dist/charts/svg-renderer.js +285 -0
- package/dist/charts/svg-renderer.js.bak +285 -0
- package/dist/charts/svg-renderer.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.bak +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.bak +35 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +342 -0
- package/dist/types.d.ts.bak +342 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +39 -0
- package/dist/types.js.bak +39 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/data-processor.d.ts +64 -0
- package/dist/utils/data-processor.d.ts.bak +64 -0
- package/dist/utils/data-processor.d.ts.map +1 -0
- package/dist/utils/data-processor.js +326 -0
- package/dist/utils/data-processor.js.bak +326 -0
- package/dist/utils/data-processor.js.map +1 -0
- package/package.json +41 -0
- package/src/charts/canvas-renderer.ts +735 -0
- package/src/charts/canvas-renderer.ts.bak +735 -0
- package/src/charts/svg-renderer.test.ts.bak +837 -0
- package/src/charts/svg-renderer.ts +380 -0
- package/src/charts/svg-renderer.ts.bak +380 -0
- package/src/index.ts +64 -0
- package/src/index.ts.bak +64 -0
- package/src/types.ts +410 -0
- package/src/types.ts.bak +410 -0
- package/src/utils/data-processor.test.ts.bak +720 -0
- package/src/utils/data-processor.ts +420 -0
- package/src/utils/data-processor.ts.bak +420 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG Chart Renderer
|
|
3
|
+
*
|
|
4
|
+
* Renders charts to SVG format.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ChartConfig, DataSeries, DataPoint, ChartMetadata, ChartPalette } from '../types';
|
|
8
|
+
import { palettes } from '../types';
|
|
9
|
+
|
|
10
|
+
export interface SVGRenderer {
|
|
11
|
+
/** Render chart to SVG string */
|
|
12
|
+
render(config: ChartConfig, series: DataSeries[]): string;
|
|
13
|
+
/** Get chart metadata */
|
|
14
|
+
getMetadata(type: string): ChartMetadata;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create SVG chart renderer
|
|
19
|
+
*/
|
|
20
|
+
export function createSVGRenderer(): SVGRenderer {
|
|
21
|
+
const defaultWidth = 800;
|
|
22
|
+
const defaultHeight = 400;
|
|
23
|
+
const margin = { top: 60, right: 40, bottom: 60, left: 80 };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Render chart to SVG
|
|
27
|
+
*/
|
|
28
|
+
function render(config: ChartConfig, series: DataSeries[]): string {
|
|
29
|
+
const width = config.width || defaultWidth;
|
|
30
|
+
const height = config.height || defaultHeight;
|
|
31
|
+
const palette = palettes[config.theme || 'default'];
|
|
32
|
+
|
|
33
|
+
const chartWidth = width - margin.left - margin.right;
|
|
34
|
+
const chartHeight = height - margin.top - margin.bottom;
|
|
35
|
+
|
|
36
|
+
let svg = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">\n`;
|
|
37
|
+
|
|
38
|
+
// Background
|
|
39
|
+
svg += ` <rect width="${width}" height="${height}" fill="${palette.background[0]}" />\n`;
|
|
40
|
+
|
|
41
|
+
// Title
|
|
42
|
+
if (config.title) {
|
|
43
|
+
svg += ` <text x="${width / 2}" y="30" text-anchor="middle" font-family="sans-serif" font-size="18" font-weight="bold" fill="${palette.text.primary}">${escapeHtml(config.title)}</text>\n`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Chart group
|
|
47
|
+
svg += ` <g transform="translate(${margin.left}, ${margin.top})">\n`;
|
|
48
|
+
|
|
49
|
+
// Render based on chart type
|
|
50
|
+
switch (config.type) {
|
|
51
|
+
case 'bar':
|
|
52
|
+
svg += renderBarChart(chartWidth, chartHeight, series, palette, config);
|
|
53
|
+
break;
|
|
54
|
+
case 'line':
|
|
55
|
+
svg += renderLineChart(chartWidth, chartHeight, series, palette, config);
|
|
56
|
+
break;
|
|
57
|
+
case 'pie':
|
|
58
|
+
case 'donut':
|
|
59
|
+
svg += renderPieChart(chartWidth, chartHeight, series, palette, config);
|
|
60
|
+
break;
|
|
61
|
+
case 'area':
|
|
62
|
+
svg += renderAreaChart(chartWidth, chartHeight, series, palette, config);
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
svg += renderPlaceholder(chartWidth, chartHeight, config.type);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Axes
|
|
69
|
+
if (['bar', 'line', 'area'].includes(config.type)) {
|
|
70
|
+
svg += renderAxes(chartWidth, chartHeight, series, config, palette);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
svg += ` </g>\n`;
|
|
74
|
+
svg += `</svg>`;
|
|
75
|
+
|
|
76
|
+
return svg;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Render bar chart
|
|
81
|
+
*/
|
|
82
|
+
function renderBarChart(
|
|
83
|
+
width: number,
|
|
84
|
+
height: number,
|
|
85
|
+
series: DataSeries[],
|
|
86
|
+
palette: ChartPalette,
|
|
87
|
+
config: ChartConfig
|
|
88
|
+
): string {
|
|
89
|
+
let svg = '';
|
|
90
|
+
|
|
91
|
+
if (series.length === 0) return svg;
|
|
92
|
+
|
|
93
|
+
const allData = series.flatMap(s => s.data);
|
|
94
|
+
const maxValue = Math.max(...allData.map(d => (d.y || d.value || 0) as number));
|
|
95
|
+
const categories = [...new Set(allData.map(d => d.x || d.name))];
|
|
96
|
+
|
|
97
|
+
const barWidth = (width / categories.length) * 0.6 / series.length;
|
|
98
|
+
const scaleY = height / (maxValue * 1.1);
|
|
99
|
+
|
|
100
|
+
series.forEach((s, seriesIndex) => {
|
|
101
|
+
const color = s.color || palette.primary[seriesIndex % palette.primary.length];
|
|
102
|
+
|
|
103
|
+
s.data.forEach((point, pointIndex) => {
|
|
104
|
+
const value = (point.y || point.value || 0) as number;
|
|
105
|
+
const barHeight = value * scaleY;
|
|
106
|
+
const x = (pointIndex + 0.2) * (width / categories.length) + seriesIndex * barWidth;
|
|
107
|
+
const y = height - barHeight;
|
|
108
|
+
|
|
109
|
+
svg += ` <rect x="${x}" y="${y}" width="${barWidth}" height="${barHeight}" fill="${color}" />\n`;
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return svg;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Render line chart
|
|
118
|
+
*/
|
|
119
|
+
function renderLineChart(
|
|
120
|
+
width: number,
|
|
121
|
+
height: number,
|
|
122
|
+
series: DataSeries[],
|
|
123
|
+
palette: ChartPalette,
|
|
124
|
+
config: ChartConfig
|
|
125
|
+
): string {
|
|
126
|
+
let svg = '';
|
|
127
|
+
|
|
128
|
+
if (series.length === 0) return svg;
|
|
129
|
+
|
|
130
|
+
const allData = series.flatMap(s => s.data);
|
|
131
|
+
const maxValue = Math.max(...allData.map(d => (d.y || 0) as number));
|
|
132
|
+
const scaleX = width / (allData.length - 1 || 1);
|
|
133
|
+
const scaleY = height / (maxValue * 1.1);
|
|
134
|
+
|
|
135
|
+
series.forEach((s, seriesIndex) => {
|
|
136
|
+
const color = s.color || palette.primary[seriesIndex % palette.primary.length];
|
|
137
|
+
|
|
138
|
+
// Generate path
|
|
139
|
+
let path = '';
|
|
140
|
+
s.data.forEach((point, index) => {
|
|
141
|
+
const x = index * scaleX;
|
|
142
|
+
const y = height - ((point.y || 0) as number) * scaleY;
|
|
143
|
+
path += index === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
svg += ` <path d="${path}" fill="none" stroke="${color}" stroke-width="2" />\n`;
|
|
147
|
+
|
|
148
|
+
// Data points
|
|
149
|
+
if (config.dataLabels) {
|
|
150
|
+
s.data.forEach((point, index) => {
|
|
151
|
+
const x = index * scaleX;
|
|
152
|
+
const y = height - ((point.y || 0) as number) * scaleY;
|
|
153
|
+
svg += ` <circle cx="${x}" cy="${y}" r="4" fill="${color}" />\n`;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return svg;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Render pie/donut chart
|
|
163
|
+
*/
|
|
164
|
+
function renderPieChart(
|
|
165
|
+
width: number,
|
|
166
|
+
height: number,
|
|
167
|
+
series: DataSeries[],
|
|
168
|
+
palette: ChartPalette,
|
|
169
|
+
config: ChartConfig
|
|
170
|
+
): string {
|
|
171
|
+
let svg = '';
|
|
172
|
+
|
|
173
|
+
if (series.length === 0) return svg;
|
|
174
|
+
|
|
175
|
+
const data = series[0].data;
|
|
176
|
+
const total = data.reduce((sum, d) => sum + ((d.y || d.value || 0) as number), 0);
|
|
177
|
+
|
|
178
|
+
const centerX = width / 2;
|
|
179
|
+
const centerY = height / 2;
|
|
180
|
+
const radius = Math.min(width, height) / 2 * 0.8;
|
|
181
|
+
const innerRadius = config.type === 'donut' ? radius * 0.5 : 0;
|
|
182
|
+
|
|
183
|
+
let currentAngle = -Math.PI / 2;
|
|
184
|
+
|
|
185
|
+
data.forEach((point, index) => {
|
|
186
|
+
const value = (point.y || point.value || 0) as number;
|
|
187
|
+
const angle = (value / total) * 2 * Math.PI;
|
|
188
|
+
const color = palette.primary[index % palette.primary.length];
|
|
189
|
+
|
|
190
|
+
const x1 = centerX + Math.cos(currentAngle) * radius;
|
|
191
|
+
const y1 = centerY + Math.sin(currentAngle) * radius;
|
|
192
|
+
const x2 = centerX + Math.cos(currentAngle + angle) * radius;
|
|
193
|
+
const y2 = centerY + Math.sin(currentAngle + angle) * radius;
|
|
194
|
+
|
|
195
|
+
const largeArc = angle > Math.PI ? 1 : 0;
|
|
196
|
+
|
|
197
|
+
if (innerRadius > 0) {
|
|
198
|
+
// Donut
|
|
199
|
+
const ix1 = centerX + Math.cos(currentAngle) * innerRadius;
|
|
200
|
+
const iy1 = centerY + Math.sin(currentAngle) * innerRadius;
|
|
201
|
+
const ix2 = centerX + Math.cos(currentAngle + angle) * innerRadius;
|
|
202
|
+
const iy2 = centerY + Math.sin(currentAngle + angle) * innerRadius;
|
|
203
|
+
|
|
204
|
+
svg += ` <path d="M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} L ${ix2} ${iy2} A ${innerRadius} ${innerRadius} 0 ${largeArc} 0 ${ix1} ${iy1} Z" fill="${color}" />\n`;
|
|
205
|
+
} else {
|
|
206
|
+
// Pie
|
|
207
|
+
svg += ` <path d="M ${centerX} ${centerY} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z" fill="${color}" />\n`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
currentAngle += angle;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return svg;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Render area chart
|
|
218
|
+
*/
|
|
219
|
+
function renderAreaChart(
|
|
220
|
+
width: number,
|
|
221
|
+
height: number,
|
|
222
|
+
series: DataSeries[],
|
|
223
|
+
palette: ChartPalette,
|
|
224
|
+
config: ChartConfig
|
|
225
|
+
): string {
|
|
226
|
+
let svg = '';
|
|
227
|
+
|
|
228
|
+
if (series.length === 0) return svg;
|
|
229
|
+
|
|
230
|
+
const allData = series.flatMap(s => s.data);
|
|
231
|
+
const maxValue = Math.max(...allData.map(d => (d.y || 0) as number));
|
|
232
|
+
const scaleX = width / (allData.length - 1 || 1);
|
|
233
|
+
const scaleY = height / (maxValue * 1.1);
|
|
234
|
+
|
|
235
|
+
series.forEach((s, seriesIndex) => {
|
|
236
|
+
const color = s.color || palette.primary[seriesIndex % palette.primary.length];
|
|
237
|
+
const fillColor = color + '40'; // 25% opacity
|
|
238
|
+
|
|
239
|
+
// Generate area path
|
|
240
|
+
let path = '';
|
|
241
|
+
s.data.forEach((point, index) => {
|
|
242
|
+
const x = index * scaleX;
|
|
243
|
+
const y = height - ((point.y || 0) as number) * scaleY;
|
|
244
|
+
path += index === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Close the area
|
|
248
|
+
path += ` L ${width} ${height} L 0 ${height} Z`;
|
|
249
|
+
|
|
250
|
+
svg += ` <path d="${path}" fill="${fillColor}" stroke="${color}" stroke-width="2" />\n`;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return svg;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Render placeholder for unsupported chart types
|
|
258
|
+
*/
|
|
259
|
+
function renderPlaceholder(width: number, height: number, type: string): string {
|
|
260
|
+
return ` <rect x="${width / 4}" y="${height / 3}" width="${width / 2}" height="${height / 3}" fill="#e5e7eb" rx="8" />\n <text x="${width / 2}" y="${height / 2}" text-anchor="middle" font-family="sans-serif" font-size="14" fill="#6b7280">${type} chart</text>\n`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Render axes
|
|
265
|
+
*/
|
|
266
|
+
function renderAxes(
|
|
267
|
+
width: number,
|
|
268
|
+
height: number,
|
|
269
|
+
series: DataSeries[],
|
|
270
|
+
config: ChartConfig,
|
|
271
|
+
palette: ChartPalette
|
|
272
|
+
): string {
|
|
273
|
+
let svg = '';
|
|
274
|
+
|
|
275
|
+
// Y-axis
|
|
276
|
+
svg += ` <line x1="0" y1="0" x2="0" y2="${height}" stroke="${palette.background[2]}" stroke-width="1" />\n`;
|
|
277
|
+
|
|
278
|
+
// X-axis
|
|
279
|
+
svg += ` <line x1="0" y1="${height}" x2="${width}" y2="${height}" stroke="${palette.background[2]}" stroke-width="1" />\n`;
|
|
280
|
+
|
|
281
|
+
// Y-axis labels
|
|
282
|
+
const yAxis = config.yAxis || {};
|
|
283
|
+
const maxValue = Math.max(...series.flatMap(s => s.data.map(d => (d.y || 0) as number)));
|
|
284
|
+
const steps = 5;
|
|
285
|
+
|
|
286
|
+
for (let i = 0; i <= steps; i++) {
|
|
287
|
+
const value = (maxValue / steps) * i;
|
|
288
|
+
const y = height - (height / steps) * i;
|
|
289
|
+
svg += ` <text x="-10" y="${y + 4}" text-anchor="end" font-family="sans-serif" font-size="12" fill="${palette.text.secondary}">${Math.round(value)}</text>\n`;
|
|
290
|
+
|
|
291
|
+
// Grid line
|
|
292
|
+
if (config.xAxis?.grid?.enabled !== false && i > 0) {
|
|
293
|
+
svg += ` <line x1="0" y1="${y}" x2="${width}" y2="${y}" stroke="${palette.background[2]}" stroke-width="1" stroke-dasharray="4,4" />\n`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Axis titles
|
|
298
|
+
if (yAxis.title) {
|
|
299
|
+
svg += ` <text x="${-margin.left + 20}" y="${height / 2}" transform="rotate(-90, ${-margin.left + 20}, ${height / 2})" text-anchor="middle" font-family="sans-serif" font-size="12" fill="${palette.text.secondary}">${escapeHtml(yAxis.title)}</text>\n`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (config.xAxis?.title) {
|
|
303
|
+
svg += ` <text x="${width / 2}" y="${height + 40}" text-anchor="middle" font-family="sans-serif" font-size="12" fill="${palette.text.secondary}">${escapeHtml(config.xAxis.title)}</text>\n`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return svg;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Escape HTML special characters
|
|
311
|
+
*/
|
|
312
|
+
function escapeHtml(text: string): string {
|
|
313
|
+
return text
|
|
314
|
+
.replace(/&/g, '&')
|
|
315
|
+
.replace(/</g, '<')
|
|
316
|
+
.replace(/>/g, '>')
|
|
317
|
+
.replace(/"/g, '"')
|
|
318
|
+
.replace(/'/g, ''');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get chart metadata
|
|
323
|
+
*/
|
|
324
|
+
function getMetadata(type: string): ChartMetadata {
|
|
325
|
+
const metadata: Record<string, ChartMetadata> = {
|
|
326
|
+
line: {
|
|
327
|
+
type: 'line',
|
|
328
|
+
name: 'Line Chart',
|
|
329
|
+
description: 'Show trends over time or categories',
|
|
330
|
+
axes: ['x', 'y'],
|
|
331
|
+
dataStructure: 'Array of { x, y } points',
|
|
332
|
+
useCases: ['Time series', 'Trends', 'Progress over time'],
|
|
333
|
+
},
|
|
334
|
+
bar: {
|
|
335
|
+
type: 'bar',
|
|
336
|
+
name: 'Bar Chart',
|
|
337
|
+
description: 'Compare values across categories',
|
|
338
|
+
axes: ['x', 'y'],
|
|
339
|
+
dataStructure: 'Array of { x, y } points',
|
|
340
|
+
useCases: ['Comparisons', 'Rankings', 'Category distribution'],
|
|
341
|
+
},
|
|
342
|
+
pie: {
|
|
343
|
+
type: 'pie',
|
|
344
|
+
name: 'Pie Chart',
|
|
345
|
+
description: 'Show part-to-whole relationships',
|
|
346
|
+
axes: [],
|
|
347
|
+
dataStructure: 'Array of { name, value }',
|
|
348
|
+
useCases: ['Composition', 'Percentage distribution'],
|
|
349
|
+
},
|
|
350
|
+
donut: {
|
|
351
|
+
type: 'donut',
|
|
352
|
+
name: 'Donut Chart',
|
|
353
|
+
description: 'Show part-to-whole with center space',
|
|
354
|
+
axes: [],
|
|
355
|
+
dataStructure: 'Array of { name, value }',
|
|
356
|
+
useCases: ['Composition', 'Percentage distribution'],
|
|
357
|
+
},
|
|
358
|
+
area: {
|
|
359
|
+
type: 'area',
|
|
360
|
+
name: 'Area Chart',
|
|
361
|
+
description: 'Show cumulative totals over time',
|
|
362
|
+
axes: ['x', 'y'],
|
|
363
|
+
dataStructure: 'Array of { x, y } points',
|
|
364
|
+
useCases: ['Volume over time', 'Cumulative data'],
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
return metadata[type] || metadata.line;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
render,
|
|
373
|
+
getMetadata,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Global SVG renderer instance
|
|
379
|
+
*/
|
|
380
|
+
export const globalSVGRenderer = createSVGRenderer();
|