@diagrammo/dgmo 0.0.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/LICENSE +21 -0
- package/README.md +335 -0
- package/dist/index.cjs +6698 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +685 -0
- package/dist/index.d.ts +685 -0
- package/dist/index.js +6611 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
- package/src/chartjs.ts +784 -0
- package/src/colors.ts +75 -0
- package/src/d3.ts +5021 -0
- package/src/dgmo-mermaid.ts +247 -0
- package/src/dgmo-router.ts +77 -0
- package/src/echarts.ts +1207 -0
- package/src/index.ts +126 -0
- package/src/palettes/bold.ts +59 -0
- package/src/palettes/catppuccin.ts +76 -0
- package/src/palettes/color-utils.ts +191 -0
- package/src/palettes/gruvbox.ts +77 -0
- package/src/palettes/index.ts +35 -0
- package/src/palettes/mermaid-bridge.ts +220 -0
- package/src/palettes/nord.ts +59 -0
- package/src/palettes/one-dark.ts +62 -0
- package/src/palettes/registry.ts +92 -0
- package/src/palettes/rose-pine.ts +76 -0
- package/src/palettes/solarized.ts +69 -0
- package/src/palettes/tokyo-night.ts +78 -0
- package/src/palettes/types.ts +67 -0
- package/src/sequence/parser.ts +531 -0
- package/src/sequence/participant-inference.ts +178 -0
- package/src/sequence/renderer.ts +1487 -0
package/src/chartjs.ts
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
import type { ChartConfiguration } from 'chart.js';
|
|
2
|
+
import 'chartjs-plugin-datalabels';
|
|
3
|
+
|
|
4
|
+
// ============================================================
|
|
5
|
+
// Types
|
|
6
|
+
// ============================================================
|
|
7
|
+
|
|
8
|
+
export type ChartJsChartType =
|
|
9
|
+
| 'bar'
|
|
10
|
+
| 'line'
|
|
11
|
+
| 'pie'
|
|
12
|
+
| 'doughnut'
|
|
13
|
+
| 'area'
|
|
14
|
+
| 'polar-area'
|
|
15
|
+
| 'radar'
|
|
16
|
+
| 'bar-stacked';
|
|
17
|
+
|
|
18
|
+
export interface ChartJsDataPoint {
|
|
19
|
+
label: string;
|
|
20
|
+
value: number;
|
|
21
|
+
extraValues?: number[];
|
|
22
|
+
color?: string;
|
|
23
|
+
lineNumber: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ParsedChartJs {
|
|
27
|
+
type: ChartJsChartType;
|
|
28
|
+
title?: string;
|
|
29
|
+
series?: string;
|
|
30
|
+
xlabel?: string;
|
|
31
|
+
ylabel?: string;
|
|
32
|
+
seriesNames?: string[];
|
|
33
|
+
seriesNameColors?: (string | undefined)[];
|
|
34
|
+
orientation?: 'horizontal' | 'vertical';
|
|
35
|
+
color?: string;
|
|
36
|
+
label?: string;
|
|
37
|
+
data: ChartJsDataPoint[];
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================
|
|
42
|
+
// Nord Colors for Charts
|
|
43
|
+
// ============================================================
|
|
44
|
+
|
|
45
|
+
import { resolveColor } from './colors';
|
|
46
|
+
import type { PaletteColors } from './palettes';
|
|
47
|
+
import { getSeriesColors } from './palettes';
|
|
48
|
+
|
|
49
|
+
// ============================================================
|
|
50
|
+
// Parser
|
|
51
|
+
// ============================================================
|
|
52
|
+
|
|
53
|
+
const VALID_TYPES = new Set<ChartJsChartType>([
|
|
54
|
+
'bar',
|
|
55
|
+
'line',
|
|
56
|
+
'pie',
|
|
57
|
+
'doughnut',
|
|
58
|
+
'area',
|
|
59
|
+
'polar-area',
|
|
60
|
+
'radar',
|
|
61
|
+
'bar-stacked',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const TYPE_ALIASES: Record<string, ChartJsChartType> = {
|
|
65
|
+
'multi-line': 'line',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parses the simple chartjs text format into a structured object.
|
|
70
|
+
*
|
|
71
|
+
* Format:
|
|
72
|
+
* ```
|
|
73
|
+
* chart: bar
|
|
74
|
+
* title: My Chart
|
|
75
|
+
* series: Revenue
|
|
76
|
+
*
|
|
77
|
+
* Jan: 120
|
|
78
|
+
* Feb: 200
|
|
79
|
+
* Mar: 150
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function parseChartJs(
|
|
83
|
+
content: string,
|
|
84
|
+
palette?: PaletteColors
|
|
85
|
+
): ParsedChartJs {
|
|
86
|
+
const lines = content.split('\n');
|
|
87
|
+
const result: ParsedChartJs = {
|
|
88
|
+
type: 'bar',
|
|
89
|
+
data: [],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < lines.length; i++) {
|
|
93
|
+
const trimmed = lines[i].trim();
|
|
94
|
+
const lineNumber = i + 1;
|
|
95
|
+
|
|
96
|
+
// Skip empty lines
|
|
97
|
+
if (!trimmed) continue;
|
|
98
|
+
|
|
99
|
+
// Recognize ## section headers (skip, but don't treat as comments)
|
|
100
|
+
if (/^#{2,}\s+/.test(trimmed)) continue;
|
|
101
|
+
|
|
102
|
+
// Skip comments
|
|
103
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('//')) continue;
|
|
104
|
+
|
|
105
|
+
// Parse key: value pairs
|
|
106
|
+
const colonIndex = trimmed.indexOf(':');
|
|
107
|
+
if (colonIndex === -1) continue;
|
|
108
|
+
|
|
109
|
+
const key = trimmed.substring(0, colonIndex).trim().toLowerCase();
|
|
110
|
+
const value = trimmed.substring(colonIndex + 1).trim();
|
|
111
|
+
|
|
112
|
+
// Handle metadata
|
|
113
|
+
if (key === 'chart') {
|
|
114
|
+
const raw = value.toLowerCase();
|
|
115
|
+
const chartType = (TYPE_ALIASES[raw] ?? raw) as ChartJsChartType;
|
|
116
|
+
if (VALID_TYPES.has(chartType)) {
|
|
117
|
+
result.type = chartType;
|
|
118
|
+
} else {
|
|
119
|
+
result.error = `Unsupported chart type: ${value}. Supported types: ${[...VALID_TYPES].join(', ')}.`;
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (key === 'title') {
|
|
126
|
+
result.title = value;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (key === 'xlabel') {
|
|
131
|
+
result.xlabel = value;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (key === 'ylabel') {
|
|
136
|
+
result.ylabel = value;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (key === 'label') {
|
|
141
|
+
result.label = value;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (key === 'orientation') {
|
|
146
|
+
const v = value.toLowerCase();
|
|
147
|
+
if (v === 'horizontal' || v === 'vertical') {
|
|
148
|
+
result.orientation = v;
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (key === 'color') {
|
|
154
|
+
result.color = resolveColor(value.trim(), palette);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (key === 'series') {
|
|
159
|
+
result.series = value;
|
|
160
|
+
// Parse comma-separated series names for multi-series chart types
|
|
161
|
+
const rawNames = value
|
|
162
|
+
.split(',')
|
|
163
|
+
.map((s) => s.trim())
|
|
164
|
+
.filter(Boolean);
|
|
165
|
+
const names: string[] = [];
|
|
166
|
+
const nameColors: (string | undefined)[] = [];
|
|
167
|
+
for (const raw of rawNames) {
|
|
168
|
+
const colorMatch = raw.match(/\(([^)]+)\)\s*$/);
|
|
169
|
+
if (colorMatch) {
|
|
170
|
+
const resolved = resolveColor(colorMatch[1].trim(), palette);
|
|
171
|
+
nameColors.push(resolved);
|
|
172
|
+
names.push(raw.substring(0, colorMatch.index!).trim());
|
|
173
|
+
} else {
|
|
174
|
+
nameColors.push(undefined);
|
|
175
|
+
names.push(raw);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (names.length === 1) {
|
|
179
|
+
result.series = names[0];
|
|
180
|
+
}
|
|
181
|
+
if (names.length > 1) {
|
|
182
|
+
result.seriesNames = names;
|
|
183
|
+
}
|
|
184
|
+
if (nameColors.some(Boolean)) result.seriesNameColors = nameColors;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Data point: Label: value or Label: v1, v2, ...
|
|
189
|
+
const parts = value.split(',').map((s) => s.trim());
|
|
190
|
+
const numValue = parseFloat(parts[0]);
|
|
191
|
+
if (!isNaN(numValue)) {
|
|
192
|
+
let rawLabel = trimmed.substring(0, colonIndex).trim();
|
|
193
|
+
let pointColor: string | undefined;
|
|
194
|
+
const colorMatch = rawLabel.match(/\(([^)]+)\)\s*$/);
|
|
195
|
+
if (colorMatch) {
|
|
196
|
+
const resolved = resolveColor(colorMatch[1].trim(), palette);
|
|
197
|
+
pointColor = resolved;
|
|
198
|
+
rawLabel = rawLabel.substring(0, colorMatch.index!).trim();
|
|
199
|
+
}
|
|
200
|
+
const extra = parts
|
|
201
|
+
.slice(1)
|
|
202
|
+
.map((s) => parseFloat(s))
|
|
203
|
+
.filter((n) => !isNaN(n));
|
|
204
|
+
result.data.push({
|
|
205
|
+
label: rawLabel,
|
|
206
|
+
value: numValue,
|
|
207
|
+
...(extra.length > 0 && { extraValues: extra }),
|
|
208
|
+
...(pointColor && { color: pointColor }),
|
|
209
|
+
lineNumber,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Validation
|
|
215
|
+
if (!result.error && result.data.length === 0) {
|
|
216
|
+
result.error = 'No data points found. Add data in format: Label: 123';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!result.error && result.type === 'bar-stacked' && !result.seriesNames) {
|
|
220
|
+
result.error = `Chart type "bar-stacked" requires multiple series names. Use: series: Name1, Name2, Name3`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!result.error && result.seriesNames) {
|
|
224
|
+
const expectedCount = result.seriesNames.length;
|
|
225
|
+
for (const dp of result.data) {
|
|
226
|
+
const actualCount = 1 + (dp.extraValues?.length ?? 0);
|
|
227
|
+
if (actualCount !== expectedCount) {
|
|
228
|
+
result.error = `Data point "${dp.label}" has ${actualCount} value(s), but ${expectedCount} series defined. Each row must have ${expectedCount} comma-separated values.`;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ============================================================
|
|
238
|
+
// Chart.js Config Builder
|
|
239
|
+
// ============================================================
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Converts parsed chartjs data to a Chart.js configuration object.
|
|
243
|
+
*/
|
|
244
|
+
export function buildChartJsConfig(
|
|
245
|
+
parsed: ParsedChartJs,
|
|
246
|
+
palette: PaletteColors,
|
|
247
|
+
_isDark: boolean
|
|
248
|
+
): ChartConfiguration {
|
|
249
|
+
const textColor = palette.text;
|
|
250
|
+
const gridColor = palette.border + '80';
|
|
251
|
+
const crosshairColor = palette.border + '60';
|
|
252
|
+
const colors = getSeriesColors(palette);
|
|
253
|
+
|
|
254
|
+
// Plugin: draws a vertical line at the hovered x-position on line/area charts
|
|
255
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
256
|
+
const verticalCrosshairPlugin: any = {
|
|
257
|
+
id: 'verticalCrosshair',
|
|
258
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
259
|
+
afterDraw(chart: any) {
|
|
260
|
+
const tooltip = chart.tooltip;
|
|
261
|
+
if (!tooltip || !tooltip.getActiveElements().length) return;
|
|
262
|
+
const { ctx, chartArea } = chart;
|
|
263
|
+
const x = tooltip.caretX;
|
|
264
|
+
ctx.save();
|
|
265
|
+
ctx.beginPath();
|
|
266
|
+
ctx.moveTo(x, chartArea.top);
|
|
267
|
+
ctx.lineTo(x, chartArea.bottom);
|
|
268
|
+
ctx.lineWidth = 1;
|
|
269
|
+
ctx.strokeStyle = crosshairColor;
|
|
270
|
+
ctx.stroke();
|
|
271
|
+
ctx.restore();
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const labels = parsed.data.map((d) => d.label);
|
|
276
|
+
const values = parsed.data.map((d) => d.value);
|
|
277
|
+
const perPointColors = parsed.data.map(
|
|
278
|
+
(d, i) => d.color ?? colors[i % colors.length]
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const titlePlugin = parsed.title
|
|
282
|
+
? {
|
|
283
|
+
display: true as const,
|
|
284
|
+
text: parsed.title,
|
|
285
|
+
color: textColor,
|
|
286
|
+
font: { size: 18, weight: 'bold' as const },
|
|
287
|
+
padding: { bottom: 16 },
|
|
288
|
+
}
|
|
289
|
+
: { display: false as const };
|
|
290
|
+
|
|
291
|
+
const tooltipConfig = {
|
|
292
|
+
backgroundColor: palette.surface,
|
|
293
|
+
titleColor: palette.text,
|
|
294
|
+
bodyColor: palette.text,
|
|
295
|
+
borderColor: palette.border,
|
|
296
|
+
borderWidth: 1,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Resolve `label:` to the value axis (Y for vertical, X for horizontal)
|
|
300
|
+
const isHorizontalChart = parsed.orientation === 'horizontal';
|
|
301
|
+
const resolvedXLabel =
|
|
302
|
+
parsed.xlabel ?? (isHorizontalChart ? parsed.label : undefined);
|
|
303
|
+
const resolvedYLabel =
|
|
304
|
+
parsed.ylabel ?? (isHorizontalChart ? undefined : parsed.label);
|
|
305
|
+
|
|
306
|
+
// Axis title configs (used by chart types with x/y scales)
|
|
307
|
+
const xAxisTitle = resolvedXLabel
|
|
308
|
+
? { display: true, text: resolvedXLabel, color: textColor }
|
|
309
|
+
: undefined;
|
|
310
|
+
const yAxisTitle = resolvedYLabel
|
|
311
|
+
? { display: true, text: resolvedYLabel, color: textColor }
|
|
312
|
+
: undefined;
|
|
313
|
+
|
|
314
|
+
// Radar chart
|
|
315
|
+
if (parsed.type === 'radar') {
|
|
316
|
+
const radarColor =
|
|
317
|
+
parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
|
|
318
|
+
// Subtle grid color for concentric reference lines drawn on top
|
|
319
|
+
const radarGridColor = palette.border + '60';
|
|
320
|
+
|
|
321
|
+
// Plugin: draws concentric polygon grid lines ON TOP of the data area.
|
|
322
|
+
// This makes the reference shapes visible even with solid fill.
|
|
323
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
324
|
+
const radarGridOverlayPlugin: any = {
|
|
325
|
+
id: 'radarGridOverlay',
|
|
326
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
327
|
+
afterDatasetsDraw(chart: any) {
|
|
328
|
+
const scale = chart.scales.r;
|
|
329
|
+
if (!scale) return;
|
|
330
|
+
const ticks = scale.ticks as { value: number }[];
|
|
331
|
+
if (!ticks || ticks.length < 1) return;
|
|
332
|
+
|
|
333
|
+
const { ctx } = chart;
|
|
334
|
+
const pointCount = chart.data.labels?.length ?? 0;
|
|
335
|
+
if (pointCount < 3) return;
|
|
336
|
+
|
|
337
|
+
ctx.save();
|
|
338
|
+
ctx.strokeStyle = radarGridColor;
|
|
339
|
+
ctx.lineWidth = 1;
|
|
340
|
+
|
|
341
|
+
// Draw concentric polygon lines for each tick
|
|
342
|
+
for (let i = 0; i < ticks.length; i++) {
|
|
343
|
+
const dist = scale.getDistanceFromCenterForValue(
|
|
344
|
+
ticks[i].value
|
|
345
|
+
) as number;
|
|
346
|
+
if (dist <= 0) continue;
|
|
347
|
+
|
|
348
|
+
ctx.beginPath();
|
|
349
|
+
for (let p = 0; p < pointCount; p++) {
|
|
350
|
+
const pos = scale.getPointPosition(p, dist);
|
|
351
|
+
if (p === 0) ctx.moveTo(pos.x, pos.y);
|
|
352
|
+
else ctx.lineTo(pos.x, pos.y);
|
|
353
|
+
}
|
|
354
|
+
ctx.closePath();
|
|
355
|
+
ctx.stroke();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Draw angle lines from center to each point
|
|
359
|
+
const outerDist = scale.getDistanceFromCenterForValue(
|
|
360
|
+
ticks[ticks.length - 1].value
|
|
361
|
+
) as number;
|
|
362
|
+
for (let p = 0; p < pointCount; p++) {
|
|
363
|
+
const pos = scale.getPointPosition(p, outerDist);
|
|
364
|
+
ctx.beginPath();
|
|
365
|
+
ctx.moveTo(scale.xCenter, scale.yCenter);
|
|
366
|
+
ctx.lineTo(pos.x, pos.y);
|
|
367
|
+
ctx.stroke();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
ctx.restore();
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
type: 'radar',
|
|
376
|
+
data: {
|
|
377
|
+
labels,
|
|
378
|
+
datasets: [
|
|
379
|
+
{
|
|
380
|
+
label: parsed.series ?? 'Value',
|
|
381
|
+
data: values,
|
|
382
|
+
backgroundColor: radarColor,
|
|
383
|
+
borderColor: 'transparent',
|
|
384
|
+
borderWidth: 0,
|
|
385
|
+
pointBackgroundColor: radarColor,
|
|
386
|
+
pointRadius: 5,
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
options: {
|
|
391
|
+
responsive: true,
|
|
392
|
+
maintainAspectRatio: false,
|
|
393
|
+
animation: false,
|
|
394
|
+
plugins: {
|
|
395
|
+
legend: { display: false },
|
|
396
|
+
title: titlePlugin,
|
|
397
|
+
tooltip: tooltipConfig,
|
|
398
|
+
datalabels: {
|
|
399
|
+
display: true,
|
|
400
|
+
color: textColor,
|
|
401
|
+
backgroundColor: palette.bg + 'cc',
|
|
402
|
+
borderRadius: 3,
|
|
403
|
+
padding: { top: 2, bottom: 2, left: 4, right: 4 },
|
|
404
|
+
font: { size: 11, weight: 'bold' as const },
|
|
405
|
+
anchor: 'center' as const,
|
|
406
|
+
align: 'center' as const,
|
|
407
|
+
formatter: (value: number) => value.toString(),
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
scales: {
|
|
411
|
+
r: {
|
|
412
|
+
beginAtZero: true,
|
|
413
|
+
ticks: {
|
|
414
|
+
// Hide tick labels - we show actual values on data points instead
|
|
415
|
+
display: false,
|
|
416
|
+
},
|
|
417
|
+
grid: {
|
|
418
|
+
// Hide default grid - we draw it on top via plugin
|
|
419
|
+
display: false,
|
|
420
|
+
},
|
|
421
|
+
angleLines: {
|
|
422
|
+
// Hide default angle lines - we draw them on top via plugin
|
|
423
|
+
display: false,
|
|
424
|
+
},
|
|
425
|
+
pointLabels: {
|
|
426
|
+
color: textColor,
|
|
427
|
+
font: { size: 12, weight: 'bold' as const },
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
plugins: [radarGridOverlayPlugin],
|
|
433
|
+
} as ChartConfiguration;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Polar Area chart (styled like pie/doughnut: outer labels, no legend)
|
|
437
|
+
if (parsed.type === 'polar-area') {
|
|
438
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
439
|
+
const polarConnectorPlugin: any = {
|
|
440
|
+
id: 'polarConnectorLines',
|
|
441
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
442
|
+
afterDatasetsDraw(chart: any) {
|
|
443
|
+
const meta = chart.getDatasetMeta(0);
|
|
444
|
+
if (!meta?.data?.length) return;
|
|
445
|
+
|
|
446
|
+
const { ctx } = chart;
|
|
447
|
+
ctx.save();
|
|
448
|
+
ctx.strokeStyle = textColor;
|
|
449
|
+
ctx.lineWidth = 1;
|
|
450
|
+
|
|
451
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
452
|
+
meta.data.forEach((arc: any) => {
|
|
453
|
+
const {
|
|
454
|
+
startAngle,
|
|
455
|
+
endAngle,
|
|
456
|
+
outerRadius,
|
|
457
|
+
x: cx,
|
|
458
|
+
y: cy,
|
|
459
|
+
} = arc.getProps(['startAngle', 'endAngle', 'outerRadius', 'x', 'y']);
|
|
460
|
+
const midAngle = (startAngle + endAngle) / 2;
|
|
461
|
+
const r1 = outerRadius + 2;
|
|
462
|
+
const r2 = outerRadius + 14;
|
|
463
|
+
|
|
464
|
+
ctx.beginPath();
|
|
465
|
+
ctx.moveTo(
|
|
466
|
+
cx + Math.cos(midAngle) * r1,
|
|
467
|
+
cy + Math.sin(midAngle) * r1
|
|
468
|
+
);
|
|
469
|
+
ctx.lineTo(
|
|
470
|
+
cx + Math.cos(midAngle) * r2,
|
|
471
|
+
cy + Math.sin(midAngle) * r2
|
|
472
|
+
);
|
|
473
|
+
ctx.stroke();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
ctx.restore();
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const polarTitlePlugin = parsed.title
|
|
481
|
+
? {
|
|
482
|
+
display: true as const,
|
|
483
|
+
text: parsed.title,
|
|
484
|
+
color: textColor,
|
|
485
|
+
font: { size: 18, weight: 'bold' as const },
|
|
486
|
+
padding: { bottom: 24 },
|
|
487
|
+
}
|
|
488
|
+
: { display: false as const };
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
type: 'polarArea',
|
|
492
|
+
data: {
|
|
493
|
+
labels,
|
|
494
|
+
datasets: [
|
|
495
|
+
{
|
|
496
|
+
label: parsed.series ?? 'Value',
|
|
497
|
+
data: values,
|
|
498
|
+
backgroundColor: perPointColors,
|
|
499
|
+
borderWidth: 0,
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
options: {
|
|
504
|
+
responsive: true,
|
|
505
|
+
maintainAspectRatio: false,
|
|
506
|
+
animation: false,
|
|
507
|
+
layout: { padding: { top: 10, bottom: 40, left: 60, right: 60 } },
|
|
508
|
+
plugins: {
|
|
509
|
+
legend: { display: false },
|
|
510
|
+
title: polarTitlePlugin,
|
|
511
|
+
tooltip: tooltipConfig,
|
|
512
|
+
datalabels: {
|
|
513
|
+
display: true,
|
|
514
|
+
color: textColor,
|
|
515
|
+
font: { weight: 'bold' as const },
|
|
516
|
+
formatter: (_value: number, ctx: { dataIndex: number }) =>
|
|
517
|
+
labels[ctx.dataIndex] ?? '',
|
|
518
|
+
anchor: 'end' as const,
|
|
519
|
+
align: 'end' as const,
|
|
520
|
+
offset: 16,
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
scales: {
|
|
524
|
+
r: {
|
|
525
|
+
ticks: { display: false },
|
|
526
|
+
grid: { color: gridColor },
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
plugins: [polarConnectorPlugin],
|
|
531
|
+
} as ChartConfiguration;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Pie / Doughnut chart
|
|
535
|
+
if (parsed.type === 'pie' || parsed.type === 'doughnut') {
|
|
536
|
+
// Inline plugin to draw connector lines from each slice to its outer label
|
|
537
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
538
|
+
const pieConnectorPlugin: any = {
|
|
539
|
+
id: 'pieConnectorLines',
|
|
540
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
541
|
+
afterDatasetsDraw(chart: any) {
|
|
542
|
+
const meta = chart.getDatasetMeta(0);
|
|
543
|
+
if (!meta?.data?.length) return;
|
|
544
|
+
|
|
545
|
+
const { ctx } = chart;
|
|
546
|
+
ctx.save();
|
|
547
|
+
ctx.strokeStyle = textColor;
|
|
548
|
+
ctx.lineWidth = 1;
|
|
549
|
+
|
|
550
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
551
|
+
meta.data.forEach((arc: any) => {
|
|
552
|
+
const {
|
|
553
|
+
startAngle,
|
|
554
|
+
endAngle,
|
|
555
|
+
outerRadius,
|
|
556
|
+
x: cx,
|
|
557
|
+
y: cy,
|
|
558
|
+
} = arc.getProps(['startAngle', 'endAngle', 'outerRadius', 'x', 'y']);
|
|
559
|
+
const midAngle = (startAngle + endAngle) / 2;
|
|
560
|
+
const r1 = outerRadius + 2;
|
|
561
|
+
const r2 = outerRadius + 14;
|
|
562
|
+
|
|
563
|
+
ctx.beginPath();
|
|
564
|
+
ctx.moveTo(
|
|
565
|
+
cx + Math.cos(midAngle) * r1,
|
|
566
|
+
cy + Math.sin(midAngle) * r1
|
|
567
|
+
);
|
|
568
|
+
ctx.lineTo(
|
|
569
|
+
cx + Math.cos(midAngle) * r2,
|
|
570
|
+
cy + Math.sin(midAngle) * r2
|
|
571
|
+
);
|
|
572
|
+
ctx.stroke();
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
ctx.restore();
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const pieTitlePlugin = parsed.title
|
|
580
|
+
? {
|
|
581
|
+
display: true as const,
|
|
582
|
+
text: parsed.title,
|
|
583
|
+
color: textColor,
|
|
584
|
+
font: { size: 18, weight: 'bold' as const },
|
|
585
|
+
padding: { bottom: 24 },
|
|
586
|
+
}
|
|
587
|
+
: { display: false as const };
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
type: parsed.type,
|
|
591
|
+
data: {
|
|
592
|
+
labels,
|
|
593
|
+
datasets: [
|
|
594
|
+
{
|
|
595
|
+
label: parsed.series ?? 'Value',
|
|
596
|
+
data: values,
|
|
597
|
+
backgroundColor: perPointColors,
|
|
598
|
+
borderWidth: 0,
|
|
599
|
+
},
|
|
600
|
+
],
|
|
601
|
+
},
|
|
602
|
+
options: {
|
|
603
|
+
responsive: true,
|
|
604
|
+
maintainAspectRatio: false,
|
|
605
|
+
animation: false,
|
|
606
|
+
// radius is valid for pie/doughnut at runtime but not in the strict type
|
|
607
|
+
radius: '70%',
|
|
608
|
+
layout: { padding: { top: 10, bottom: 40, left: 60, right: 60 } },
|
|
609
|
+
plugins: {
|
|
610
|
+
legend: { display: false },
|
|
611
|
+
title: pieTitlePlugin,
|
|
612
|
+
tooltip: tooltipConfig,
|
|
613
|
+
datalabels: {
|
|
614
|
+
display: true,
|
|
615
|
+
color: textColor,
|
|
616
|
+
font: { weight: 'bold' as const },
|
|
617
|
+
formatter: (_value: number, ctx: { dataIndex: number }) =>
|
|
618
|
+
labels[ctx.dataIndex] ?? '',
|
|
619
|
+
anchor: 'end' as const,
|
|
620
|
+
align: 'end' as const,
|
|
621
|
+
offset: 16,
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
plugins: [pieConnectorPlugin],
|
|
626
|
+
} as ChartConfiguration;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Multi-series: bar-stacked, or line with multiple series
|
|
630
|
+
const isMultiSeries =
|
|
631
|
+
parsed.type === 'bar-stacked' ||
|
|
632
|
+
(parsed.type === 'line' && parsed.seriesNames);
|
|
633
|
+
if (isMultiSeries) {
|
|
634
|
+
const seriesNames = parsed.seriesNames ?? ['Value'];
|
|
635
|
+
const isHorizontal = parsed.orientation === 'horizontal';
|
|
636
|
+
const isMultiLine = parsed.type === 'line';
|
|
637
|
+
|
|
638
|
+
// Transpose row-based data into per-series datasets
|
|
639
|
+
const datasets = seriesNames.map((name, seriesIdx) => {
|
|
640
|
+
const data = parsed.data.map((dp) => {
|
|
641
|
+
if (seriesIdx === 0) return dp.value;
|
|
642
|
+
return dp.extraValues?.[seriesIdx - 1] ?? 0;
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const color =
|
|
646
|
+
parsed.seriesNameColors?.[seriesIdx] ??
|
|
647
|
+
colors[seriesIdx % colors.length];
|
|
648
|
+
|
|
649
|
+
if (isMultiLine) {
|
|
650
|
+
return {
|
|
651
|
+
label: name,
|
|
652
|
+
data,
|
|
653
|
+
borderColor: color,
|
|
654
|
+
backgroundColor: color + '40',
|
|
655
|
+
borderWidth: 3,
|
|
656
|
+
pointBackgroundColor: color,
|
|
657
|
+
pointRadius: 4,
|
|
658
|
+
tension: 0,
|
|
659
|
+
fill: false,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
label: name,
|
|
665
|
+
data,
|
|
666
|
+
backgroundColor: color,
|
|
667
|
+
borderColor: color,
|
|
668
|
+
borderWidth: 1,
|
|
669
|
+
};
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const scaleOptions = isMultiLine
|
|
673
|
+
? {
|
|
674
|
+
x: {
|
|
675
|
+
grid: { color: gridColor },
|
|
676
|
+
ticks: { color: textColor },
|
|
677
|
+
...(xAxisTitle && { title: xAxisTitle }),
|
|
678
|
+
},
|
|
679
|
+
y: {
|
|
680
|
+
grid: { color: gridColor },
|
|
681
|
+
ticks: { color: textColor },
|
|
682
|
+
...(yAxisTitle && { title: yAxisTitle }),
|
|
683
|
+
},
|
|
684
|
+
}
|
|
685
|
+
: {
|
|
686
|
+
x: {
|
|
687
|
+
stacked: true as const,
|
|
688
|
+
grid: { color: gridColor },
|
|
689
|
+
ticks: { color: textColor },
|
|
690
|
+
...(xAxisTitle && { title: xAxisTitle }),
|
|
691
|
+
},
|
|
692
|
+
y: {
|
|
693
|
+
stacked: true as const,
|
|
694
|
+
grid: { color: gridColor },
|
|
695
|
+
ticks: { color: textColor },
|
|
696
|
+
...(yAxisTitle && { title: yAxisTitle }),
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
type: isMultiLine ? 'line' : 'bar',
|
|
702
|
+
data: {
|
|
703
|
+
labels,
|
|
704
|
+
datasets,
|
|
705
|
+
},
|
|
706
|
+
options: {
|
|
707
|
+
indexAxis: isHorizontal ? 'y' : 'x',
|
|
708
|
+
responsive: true,
|
|
709
|
+
maintainAspectRatio: false,
|
|
710
|
+
animation: false,
|
|
711
|
+
plugins: {
|
|
712
|
+
legend: { position: 'top' as const, labels: { color: textColor } },
|
|
713
|
+
title: titlePlugin,
|
|
714
|
+
tooltip: tooltipConfig,
|
|
715
|
+
datalabels: { display: false },
|
|
716
|
+
},
|
|
717
|
+
...(isMultiLine
|
|
718
|
+
? { interaction: { mode: 'index' as const, intersect: false } }
|
|
719
|
+
: {}),
|
|
720
|
+
scales: scaleOptions,
|
|
721
|
+
},
|
|
722
|
+
...(isMultiLine ? { plugins: [verticalCrosshairPlugin] } : {}),
|
|
723
|
+
} as ChartConfiguration;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Bar, line, area
|
|
727
|
+
const isHorizontal = parsed.orientation === 'horizontal';
|
|
728
|
+
const isLine = parsed.type === 'line' || parsed.type === 'area';
|
|
729
|
+
const isArea = parsed.type === 'area';
|
|
730
|
+
const lineColor =
|
|
731
|
+
parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
|
|
732
|
+
|
|
733
|
+
return {
|
|
734
|
+
type: isLine ? 'line' : 'bar',
|
|
735
|
+
data: {
|
|
736
|
+
labels,
|
|
737
|
+
datasets: [
|
|
738
|
+
{
|
|
739
|
+
label: parsed.series ?? 'Value',
|
|
740
|
+
data: values,
|
|
741
|
+
backgroundColor: isLine
|
|
742
|
+
? isArea
|
|
743
|
+
? lineColor + '40'
|
|
744
|
+
: lineColor
|
|
745
|
+
: perPointColors,
|
|
746
|
+
borderColor: isLine ? lineColor : undefined,
|
|
747
|
+
borderWidth: isLine ? 3 : 0,
|
|
748
|
+
pointBackgroundColor: isLine ? lineColor : undefined,
|
|
749
|
+
pointRadius: isLine ? 4 : undefined,
|
|
750
|
+
tension: isLine ? 0 : undefined,
|
|
751
|
+
fill: isArea ? true : undefined,
|
|
752
|
+
},
|
|
753
|
+
],
|
|
754
|
+
},
|
|
755
|
+
options: {
|
|
756
|
+
indexAxis: isHorizontal ? 'y' : 'x',
|
|
757
|
+
responsive: true,
|
|
758
|
+
maintainAspectRatio: false,
|
|
759
|
+
animation: false,
|
|
760
|
+
...(isLine && !isArea
|
|
761
|
+
? { interaction: { mode: 'index' as const, intersect: false } }
|
|
762
|
+
: {}),
|
|
763
|
+
plugins: {
|
|
764
|
+
legend: { display: false },
|
|
765
|
+
title: titlePlugin,
|
|
766
|
+
tooltip: tooltipConfig,
|
|
767
|
+
datalabels: { display: false },
|
|
768
|
+
},
|
|
769
|
+
scales: {
|
|
770
|
+
x: {
|
|
771
|
+
grid: { color: gridColor },
|
|
772
|
+
ticks: { color: textColor },
|
|
773
|
+
...(xAxisTitle && { title: xAxisTitle }),
|
|
774
|
+
},
|
|
775
|
+
y: {
|
|
776
|
+
grid: { color: gridColor },
|
|
777
|
+
ticks: { color: textColor },
|
|
778
|
+
...(yAxisTitle && { title: yAxisTitle }),
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
...(isLine && !isArea ? { plugins: [verticalCrosshairPlugin] } : {}),
|
|
783
|
+
};
|
|
784
|
+
}
|