@skdx/angular-charts 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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/fesm2022/skdx-angular-charts-area.mjs +48 -0
  4. package/fesm2022/skdx-angular-charts-area.mjs.map +1 -0
  5. package/fesm2022/skdx-angular-charts-bar.mjs +57 -0
  6. package/fesm2022/skdx-angular-charts-bar.mjs.map +1 -0
  7. package/fesm2022/skdx-angular-charts-boxplot.mjs +50 -0
  8. package/fesm2022/skdx-angular-charts-boxplot.mjs.map +1 -0
  9. package/fesm2022/skdx-angular-charts-candlestick.mjs +50 -0
  10. package/fesm2022/skdx-angular-charts-candlestick.mjs.map +1 -0
  11. package/fesm2022/skdx-angular-charts-chord.mjs +53 -0
  12. package/fesm2022/skdx-angular-charts-chord.mjs.map +1 -0
  13. package/fesm2022/skdx-angular-charts-funnel.mjs +50 -0
  14. package/fesm2022/skdx-angular-charts-funnel.mjs.map +1 -0
  15. package/fesm2022/skdx-angular-charts-gantt.mjs +53 -0
  16. package/fesm2022/skdx-angular-charts-gantt.mjs.map +1 -0
  17. package/fesm2022/skdx-angular-charts-heatmap.mjs +56 -0
  18. package/fesm2022/skdx-angular-charts-heatmap.mjs.map +1 -0
  19. package/fesm2022/skdx-angular-charts-internal.mjs +1779 -0
  20. package/fesm2022/skdx-angular-charts-internal.mjs.map +1 -0
  21. package/fesm2022/skdx-angular-charts-line.mjs +48 -0
  22. package/fesm2022/skdx-angular-charts-line.mjs.map +1 -0
  23. package/fesm2022/skdx-angular-charts-map.mjs +45 -0
  24. package/fesm2022/skdx-angular-charts-map.mjs.map +1 -0
  25. package/fesm2022/skdx-angular-charts-pie.mjs +53 -0
  26. package/fesm2022/skdx-angular-charts-pie.mjs.map +1 -0
  27. package/fesm2022/skdx-angular-charts-radar.mjs +53 -0
  28. package/fesm2022/skdx-angular-charts-radar.mjs.map +1 -0
  29. package/fesm2022/skdx-angular-charts-sankey.mjs +50 -0
  30. package/fesm2022/skdx-angular-charts-sankey.mjs.map +1 -0
  31. package/fesm2022/skdx-angular-charts-scatter.mjs +50 -0
  32. package/fesm2022/skdx-angular-charts-scatter.mjs.map +1 -0
  33. package/fesm2022/skdx-angular-charts-sparkline.mjs +52 -0
  34. package/fesm2022/skdx-angular-charts-sparkline.mjs.map +1 -0
  35. package/fesm2022/skdx-angular-charts-sunburst.mjs +50 -0
  36. package/fesm2022/skdx-angular-charts-sunburst.mjs.map +1 -0
  37. package/fesm2022/skdx-angular-charts-treemap.mjs +42 -0
  38. package/fesm2022/skdx-angular-charts-treemap.mjs.map +1 -0
  39. package/fesm2022/skdx-angular-charts-waterfall.mjs +53 -0
  40. package/fesm2022/skdx-angular-charts-waterfall.mjs.map +1 -0
  41. package/fesm2022/skdx-angular-charts.mjs +29 -0
  42. package/fesm2022/skdx-angular-charts.mjs.map +1 -0
  43. package/package.json +115 -0
  44. package/types/skdx-angular-charts-area.d.ts +12 -0
  45. package/types/skdx-angular-charts-bar.d.ts +20 -0
  46. package/types/skdx-angular-charts-boxplot.d.ts +13 -0
  47. package/types/skdx-angular-charts-candlestick.d.ts +13 -0
  48. package/types/skdx-angular-charts-chord.d.ts +15 -0
  49. package/types/skdx-angular-charts-funnel.d.ts +13 -0
  50. package/types/skdx-angular-charts-gantt.d.ts +15 -0
  51. package/types/skdx-angular-charts-heatmap.d.ts +18 -0
  52. package/types/skdx-angular-charts-internal.d.ts +358 -0
  53. package/types/skdx-angular-charts-line.d.ts +12 -0
  54. package/types/skdx-angular-charts-map.d.ts +15 -0
  55. package/types/skdx-angular-charts-pie.d.ts +15 -0
  56. package/types/skdx-angular-charts-radar.d.ts +15 -0
  57. package/types/skdx-angular-charts-sankey.d.ts +13 -0
  58. package/types/skdx-angular-charts-scatter.d.ts +13 -0
  59. package/types/skdx-angular-charts-sparkline.d.ts +14 -0
  60. package/types/skdx-angular-charts-sunburst.d.ts +13 -0
  61. package/types/skdx-angular-charts-treemap.d.ts +13 -0
  62. package/types/skdx-angular-charts-waterfall.d.ts +15 -0
  63. package/types/skdx-angular-charts.d.ts +19 -0
@@ -0,0 +1,1779 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, ChangeDetectionStrategy, Component, signal, Directive } from '@angular/core';
3
+
4
+ /*
5
+ * Copyright (c) 2026 SkandaDX
6
+ * This file is part of the SkandaDX organization.
7
+ * Licensed under the MIT License. See LICENSE in the repository root.
8
+ */
9
+ /**
10
+ * Series colors, cycling through the `@skdx/tokens` scales so charts follow the
11
+ * active theme. Load `@skdx/tokens/css` (or any theme) for these to resolve.
12
+ */
13
+ const SERIES_COLORS = [
14
+ 'primary',
15
+ 'success',
16
+ 'warning',
17
+ 'danger',
18
+ 'neutral',
19
+ ]
20
+ .flatMap((scale) => [500, 700].map((shade) => `var(--skdx-color-${scale}-${shade})`))
21
+ // Interleave: every 500 first, then every 700.
22
+ .sort((a, b) => Number(a.endsWith('700)')) - Number(b.endsWith('700)')));
23
+ const CHART_COLORS = {
24
+ axis: 'var(--skdx-color-border-strong)',
25
+ grid: 'var(--skdx-color-border-subtle)',
26
+ label: 'var(--skdx-color-text-muted)',
27
+ fontFamily: 'var(--skdx-font-family)',
28
+ fontSize: 'var(--skdx-font-size-xs)',
29
+ };
30
+ const seriesColor = (index, override) => override ?? SERIES_COLORS[index % SERIES_COLORS.length];
31
+
32
+ /*
33
+ * Copyright (c) 2026 SkandaDX
34
+ * This file is part of the SkandaDX organization.
35
+ * Licensed under the MIT License. See LICENSE in the repository root.
36
+ */
37
+ /** Maps `[d0, d1]` onto `[r0, r1]` linearly; a zero-width domain maps to `r0`. */
38
+ function linearScale([d0, d1], [r0, r1]) {
39
+ const span = d1 - d0;
40
+ return (value) => (span === 0 ? r0 : r0 + ((value - d0) / span) * (r1 - r0));
41
+ }
42
+ /** Round tick values covering `[min, max]` with a 1/2/5 step, about `count` of them. */
43
+ function niceTicks(min, max, count = 5) {
44
+ if (min === max)
45
+ return [min];
46
+ const raw = (max - min) / Math.max(1, count);
47
+ const magnitude = 10 ** Math.floor(Math.log10(raw));
48
+ const norm = raw / magnitude;
49
+ const step = (norm < 1.5 ? 1 : norm < 3.5 ? 2 : norm < 7.5 ? 5 : 10) * magnitude;
50
+ const start = Math.floor(min / step) * step;
51
+ const end = Math.ceil(max / step) * step;
52
+ const ticks = [];
53
+ for (let v = start; v <= end + step / 2; v += step)
54
+ ticks.push(round(v));
55
+ return ticks;
56
+ }
57
+ /** Evenly spaced slots for `count` categories across `[r0, r1]` with 20% gaps. */
58
+ function bandScale(count, [r0, r1]) {
59
+ const step = count === 0 ? 0 : (r1 - r0) / count;
60
+ const bandwidth = step * 0.8;
61
+ return {
62
+ step,
63
+ bandwidth,
64
+ start: (i) => r0 + i * step + (step - bandwidth) / 2,
65
+ center: (i) => r0 + (i + 0.5) * step,
66
+ };
67
+ }
68
+ const round = (v) => Number(v.toPrecision(12));
69
+ /** Pixel coordinates rounded to 2 decimals, so the markup stays readable. */
70
+ const px = (n) => Number(n.toFixed(2));
71
+ const formatValue = (v) => new Intl.NumberFormat().format(round(v));
72
+ /** SVG path through the points: straight segments, or a monotone cubic (Fritsch–Carlson) that never overshoots. */
73
+ function pathThrough(points, curve = 'linear') {
74
+ if (points.length === 0)
75
+ return '';
76
+ const first = points[0];
77
+ if (curve === 'linear' || points.length < 3)
78
+ return `M${points.map((p) => `${p.x} ${p.y}`).join('L')}`;
79
+ const n = points.length;
80
+ const dx = points.slice(1).map((p, i) => p.x - points[i].x);
81
+ const dy = points.slice(1).map((p, i) => p.y - points[i].y);
82
+ const slope = dx.map((d, i) => (d === 0 ? 0 : dy[i] / d));
83
+ const m = [slope[0]];
84
+ for (let i = 1; i < n - 1; i++) {
85
+ const a = slope[i - 1];
86
+ const b = slope[i];
87
+ m.push(a * b <= 0 ? 0 : (a + b) / 2);
88
+ }
89
+ m.push(slope[n - 2]);
90
+ for (let i = 0; i < n - 1; i++) {
91
+ const s = slope[i];
92
+ if (s === 0) {
93
+ m[i] = 0;
94
+ m[i + 1] = 0;
95
+ continue;
96
+ }
97
+ const a = m[i] / s;
98
+ const b = m[i + 1] / s;
99
+ const h = Math.hypot(a, b);
100
+ if (h > 3) {
101
+ m[i] = (3 * a * s) / h;
102
+ m[i + 1] = (3 * b * s) / h;
103
+ }
104
+ }
105
+ let d = `M${first.x} ${first.y}`;
106
+ for (let i = 0; i < n - 1; i++) {
107
+ const p0 = points[i];
108
+ const p1 = points[i + 1];
109
+ const h = dx[i] / 3;
110
+ d += `C${px(p0.x + h)} ${px(p0.y + h * m[i])} ${px(p1.x - h)} ${px(p1.y - h * m[i + 1])} ${p1.x} ${p1.y}`;
111
+ }
112
+ return d;
113
+ }
114
+ /** Ticks at powers of ten covering `[min, max]` (both > 0), with 2× and 5× steps when a decade or less. */
115
+ function logTicks(min, max) {
116
+ const lo = Math.floor(Math.log10(min));
117
+ const hi = Math.ceil(Math.log10(max));
118
+ const steps = hi - lo <= 1 ? [1, 2, 5] : [1];
119
+ const ticks = [];
120
+ for (let k = lo; k <= hi; k++)
121
+ for (const s of steps)
122
+ ticks.push(round(s * 10 ** k));
123
+ return ticks.filter((t) => t >= min / 1.0001 && t <= max * 1.0001);
124
+ }
125
+
126
+ /*
127
+ * Copyright (c) 2026 SkandaDX
128
+ * This file is part of the SkandaDX organization.
129
+ * Licensed under the MIT License. See LICENSE in the repository root.
130
+ */
131
+ const MARGIN$1 = { top: 16, right: 16, bottom: 32, left: 48 };
132
+ function plotBox(width, height, margin = MARGIN$1) {
133
+ return {
134
+ x: margin.left,
135
+ y: margin.top,
136
+ width: Math.max(0, width - margin.left - margin.right),
137
+ height: Math.max(0, height - margin.top - margin.bottom),
138
+ };
139
+ }
140
+ const size = (input, w = 600, h = 300) => ({
141
+ width: input.width ?? w,
142
+ height: input.height ?? h,
143
+ });
144
+ const LEGEND_HEIGHT = 20;
145
+ /** Vertical room the legend row takes, to shift the plot down by. */
146
+ const legendTop = (input) => (input.legend ? LEGEND_HEIGHT : 0);
147
+ const CHAR = 6.5;
148
+ /** One row of swatches and names along the top edge, wrapping when it runs out of width. */
149
+ function legendShapes(items, width) {
150
+ const shapes = [];
151
+ let x = 8;
152
+ let y = 10;
153
+ for (const item of items) {
154
+ const w = 18 + item.name.length * CHAR;
155
+ if (x + w > width - 8 && x > 8) {
156
+ x = 8;
157
+ y += LEGEND_HEIGHT;
158
+ }
159
+ shapes.push({ kind: 'rect', part: 'legend-swatch', x, y: y - 5, width: 10, height: 10, fill: item.color }, {
160
+ kind: 'text',
161
+ part: 'legend-label',
162
+ x: x + 14,
163
+ y,
164
+ text: item.name,
165
+ baseline: 'middle',
166
+ fill: CHART_COLORS.label,
167
+ });
168
+ x += w + 12;
169
+ }
170
+ return shapes;
171
+ }
172
+ /** The common tail of every layout: size, legend decorations and the tooltip switch. */
173
+ function canvas(input, width, height, legend = []) {
174
+ return {
175
+ width,
176
+ height,
177
+ tooltip: input.tooltip ?? true,
178
+ shapes: input.legend ? legendShapes(legend, width) : [],
179
+ };
180
+ }
181
+ /** A tooltip box next to the pointer, kept inside the canvas. */
182
+ function tooltipShapes(x, y, text, width, height) {
183
+ const w = text.length * CHAR + 12;
184
+ const h = 22;
185
+ const bx = Math.max(0, Math.min(width - w, x + 12));
186
+ const by = Math.max(0, Math.min(height - h, y - h - 6));
187
+ return [
188
+ {
189
+ kind: 'rect',
190
+ part: 'tooltip',
191
+ x: px(bx),
192
+ y: px(by),
193
+ width: px(w),
194
+ height: h,
195
+ fill: 'var(--skdx-color-surface-raised)',
196
+ stroke: CHART_COLORS.axis,
197
+ },
198
+ {
199
+ kind: 'text',
200
+ part: 'tooltip-label',
201
+ x: px(bx + 6),
202
+ y: px(by + h / 2),
203
+ text,
204
+ baseline: 'middle',
205
+ fill: 'var(--skdx-color-text-default)',
206
+ },
207
+ ];
208
+ }
209
+ /** `[min, max]` of the values, widened to include zero when asked; `[0, 0]` when empty. */
210
+ function extent(values, includeZero) {
211
+ if (values.length === 0)
212
+ return [0, 0];
213
+ const min = Math.min(...values);
214
+ const max = Math.max(...values);
215
+ return includeZero ? [Math.min(0, min), Math.max(0, max)] : [min, max];
216
+ }
217
+ /** A nice-ticked linear axis over `[min, max]` mapped onto `range`. */
218
+ function axis([min, max], range, log = false) {
219
+ if (log) {
220
+ const lo = Math.max(min, Number.EPSILON);
221
+ const ticks = logTicks(lo, Math.max(max, lo));
222
+ const domain = [Math.log10(lo), Math.log10(Math.max(max, lo))];
223
+ const lin = linearScale(domain, range);
224
+ const scale = (v) => lin(Math.log10(Math.max(v, Number.EPSILON)));
225
+ return {
226
+ scale,
227
+ domain,
228
+ ticks: ticks.map((v) => ({ pos: px(scale(v)), label: formatValue(v) })),
229
+ zero: px(range[0]),
230
+ };
231
+ }
232
+ const ticks = niceTicks(min, max);
233
+ const domain = [ticks[0], ticks[ticks.length - 1]];
234
+ const scale = linearScale(domain, range);
235
+ return {
236
+ scale,
237
+ domain,
238
+ ticks: ticks.map((v) => ({ pos: px(scale(v)), label: formatValue(v) })),
239
+ /** Pixel position of zero, clamped into the domain. */
240
+ zero: px(scale(Math.min(Math.max(0, domain[0]), domain[1]))),
241
+ };
242
+ }
243
+ /** Axes, ticks and scales shared by the category charts; `yExtent` is the y domain to cover. */
244
+ function buildFrame(input, yExtent) {
245
+ const { width, height } = size(input);
246
+ const plot = plotBox(width, height, { ...MARGIN$1, top: MARGIN$1.top + legendTop(input) });
247
+ const count = Math.max(input.categories?.length ?? 0, ...input.series.map((s) => s.data.length));
248
+ const categories = Array.from({ length: count }, (_, i) => input.categories?.[i] ?? String(i + 1));
249
+ const y = axis(yExtent, [plot.y + plot.height, plot.y]);
250
+ const x = bandScale(count, [plot.x, plot.x + plot.width]);
251
+ return {
252
+ frame: {
253
+ ...canvas(input, width, height, input.series.map((s, i) => ({ name: s.name, color: seriesColor(i, s.color) }))),
254
+ plot,
255
+ xTicks: categories.map((label, i) => ({ pos: px(x.center(i)), label })),
256
+ yTicks: y.ticks,
257
+ baseline: y.zero,
258
+ },
259
+ x,
260
+ y: y.scale,
261
+ categories,
262
+ };
263
+ }
264
+ const values = (input) => input.series.flatMap((s) => s.data);
265
+ /** Tooltip text for a value in a series: `Series, Category: 1,234`. */
266
+ const markTitle = (series, category, value) => `${series}, ${category}: ${formatValue(value)}`;
267
+ /**
268
+ * Tooltip shapes for the mark under the pointer, or none. Reads the mark's
269
+ * `<title>` and converts the pointer position into viewBox units.
270
+ */
271
+ function hoverTooltip(target, svg, clientX, clientY, width, height) {
272
+ const mark = target?.closest?.('[data-skdx-part]');
273
+ const title = mark && Array.from(mark.children).find((c) => c.tagName.toLowerCase() === 'title')?.textContent;
274
+ if (!title)
275
+ return [];
276
+ const r = svg.getBoundingClientRect();
277
+ const x = r.width ? ((clientX - r.left) * width) / r.width : clientX;
278
+ const y = r.height ? ((clientY - r.top) * height) / r.height : clientY;
279
+ return tooltipShapes(x, y, title, width, height);
280
+ }
281
+
282
+ /*
283
+ * Copyright (c) 2026 SkandaDX
284
+ * This file is part of the SkandaDX organization.
285
+ * Licensed under the MIT License. See LICENSE in the repository root.
286
+ */
287
+ /** Grid, axis line and tick labels of an axis frame, as shapes. */
288
+ function frameShapes(frame) {
289
+ const { plot, xTicks, yTicks, baseline } = frame;
290
+ const right = plot.x + plot.width;
291
+ return [
292
+ ...yTicks.map((t) => ({
293
+ kind: 'line',
294
+ part: 'grid',
295
+ x1: plot.x,
296
+ x2: right,
297
+ y1: t.pos,
298
+ y2: t.pos,
299
+ stroke: CHART_COLORS.grid,
300
+ })),
301
+ {
302
+ kind: 'line',
303
+ part: 'axis',
304
+ x1: plot.x,
305
+ x2: right,
306
+ y1: baseline,
307
+ y2: baseline,
308
+ stroke: CHART_COLORS.axis,
309
+ },
310
+ ...yTicks.map((t) => ({
311
+ kind: 'text',
312
+ part: 'y-label',
313
+ x: plot.x - 8,
314
+ y: t.pos,
315
+ text: t.label,
316
+ anchor: 'end',
317
+ baseline: 'middle',
318
+ fill: CHART_COLORS.label,
319
+ })),
320
+ ...xTicks.map((t) => ({
321
+ kind: 'text',
322
+ part: 'x-label',
323
+ x: t.pos,
324
+ y: plot.y + plot.height + 20,
325
+ text: t.label,
326
+ anchor: 'middle',
327
+ fill: CHART_COLORS.label,
328
+ })),
329
+ ];
330
+ }
331
+ const label = (x, y, text, extra = {}) => ({
332
+ kind: 'text',
333
+ part: 'label',
334
+ x,
335
+ y,
336
+ text,
337
+ fill: CHART_COLORS.label,
338
+ ...extra,
339
+ });
340
+ /** A dot with its native tooltip. */
341
+ const point = (cx, cy, r, fill, title) => ({
342
+ kind: 'circle',
343
+ part: 'point',
344
+ cx,
345
+ cy,
346
+ r,
347
+ fill,
348
+ title,
349
+ });
350
+
351
+ /*
352
+ * Copyright (c) 2026 SkandaDX
353
+ * This file is part of the SkandaDX organization.
354
+ * Licensed under the MIT License. See LICENSE in the repository root.
355
+ */
356
+ function layoutBarChart(input) {
357
+ const yExtent = input.stacked ? stackedExtent(input) : extent(values(input), true);
358
+ const { frame, x, y, categories } = buildFrame(input, yExtent);
359
+ const bars = [];
360
+ // Running stack tops per category, positives up and negatives down.
361
+ const up = new Array(categories.length).fill(0);
362
+ const down = new Array(categories.length).fill(0);
363
+ const groupWidth = input.stacked ? x.bandwidth : x.bandwidth / Math.max(1, input.series.length);
364
+ input.series.forEach((s, si) => {
365
+ const color = seriesColor(si, s.color);
366
+ categories.forEach((category, ci) => {
367
+ const value = s.data[ci];
368
+ if (value === undefined)
369
+ return;
370
+ let from = 0;
371
+ let to = value;
372
+ if (input.stacked) {
373
+ const stack = value >= 0 ? up : down;
374
+ from = stack[ci];
375
+ to = from + value;
376
+ stack[ci] = to;
377
+ }
378
+ const y0 = y(from);
379
+ const y1 = y(to);
380
+ bars.push({
381
+ kind: 'rect',
382
+ part: 'bar',
383
+ x: px(x.start(ci) + (input.stacked ? 0 : si * groupWidth)),
384
+ y: px(Math.min(y0, y1)),
385
+ width: px(groupWidth),
386
+ height: px(Math.abs(y1 - y0)),
387
+ fill: color,
388
+ title: markTitle(s.name, category, value),
389
+ });
390
+ });
391
+ });
392
+ return { ...frame, shapes: [...frameShapes(frame), ...bars, ...frame.shapes] };
393
+ }
394
+ function stackedExtent(input) {
395
+ let min = 0;
396
+ let max = 0;
397
+ const count = Math.max(0, ...input.series.map((s) => s.data.length));
398
+ for (let ci = 0; ci < count; ci++) {
399
+ let up = 0;
400
+ let down = 0;
401
+ for (const s of input.series) {
402
+ const v = s.data[ci] ?? 0;
403
+ if (v >= 0)
404
+ up += v;
405
+ else
406
+ down += v;
407
+ }
408
+ min = Math.min(min, down);
409
+ max = Math.max(max, up);
410
+ }
411
+ return [min, max];
412
+ }
413
+
414
+ /*
415
+ * Copyright (c) 2026 SkandaDX
416
+ * This file is part of the SkandaDX organization.
417
+ * Licensed under the MIT License. See LICENSE in the repository root.
418
+ */
419
+ /** A stroke per series and a dot per value; `close` turns a stroke into a filled area. */
420
+ function polylines(input, { frame, x, y, categories }, area) {
421
+ const points = [];
422
+ const lines = [];
423
+ const areas = [];
424
+ input.series.forEach((s, si) => {
425
+ const color = seriesColor(si, s.color);
426
+ const coords = s.data.map((value, ci) => {
427
+ const p = { x: px(x.center(ci)), y: px(y(value)) };
428
+ points.push(point(p.x, p.y, 3, color, markTitle(s.name, categories[ci], value)));
429
+ return p;
430
+ });
431
+ const d = pathThrough(coords, input.curve);
432
+ lines.push({
433
+ kind: 'path',
434
+ part: 'line',
435
+ d,
436
+ stroke: color,
437
+ strokeWidth: 2,
438
+ strokeLinecap: 'round',
439
+ strokeLinejoin: 'round',
440
+ });
441
+ if (area) {
442
+ const first = coords[0];
443
+ const last = coords[coords.length - 1];
444
+ areas.push({
445
+ kind: 'path',
446
+ part: 'area',
447
+ d: d && first && last
448
+ ? `${d}L${last.x} ${frame.baseline}L${first.x} ${frame.baseline}Z`
449
+ : '',
450
+ fill: color,
451
+ fillOpacity: 0.25,
452
+ });
453
+ }
454
+ });
455
+ return {
456
+ ...frame,
457
+ shapes: [...frameShapes(frame), ...areas, ...lines, ...points, ...frame.shapes],
458
+ };
459
+ }
460
+ function layoutLineChart(input) {
461
+ return polylines(input, buildFrame(input, extent(values(input), false)), false);
462
+ }
463
+ function layoutAreaChart(input) {
464
+ return polylines(input, buildFrame(input, extent(values(input), true)), true);
465
+ }
466
+
467
+ /*
468
+ * Copyright (c) 2026 SkandaDX
469
+ * This file is part of the SkandaDX organization.
470
+ * Licensed under the MIT License. See LICENSE in the repository root.
471
+ */
472
+ function layoutScatterChart(input) {
473
+ const { width, height } = size(input);
474
+ const plot = plotBox(width, height, { ...MARGIN$1, top: MARGIN$1.top + legendTop(input) });
475
+ const pairs = input.series.flatMap((s) => s.data);
476
+ const x = axis(extent(pairs.map((p) => p[0]), false), [plot.x, plot.x + plot.width]);
477
+ const y = axis(extent(pairs.map((p) => p[1]), false), [plot.y + plot.height, plot.y]);
478
+ const legend = input.series.map((s, i) => ({ name: s.name, color: seriesColor(i, s.color) }));
479
+ const c = canvas(input, width, height, legend);
480
+ const points = input.series.flatMap((s, si) => {
481
+ const color = seriesColor(si, s.color);
482
+ return s.data.map(([vx, vy]) => point(px(x.scale(vx)), px(y.scale(vy)), 3, color, `${s.name}: ${formatValue(vx)}, ${formatValue(vy)}`));
483
+ });
484
+ const frame = { ...c, plot, xTicks: x.ticks, yTicks: y.ticks, baseline: y.zero };
485
+ return { ...c, shapes: [...frameShapes(frame), ...points, ...c.shapes] };
486
+ }
487
+
488
+ /*
489
+ * Copyright (c) 2026 SkandaDX
490
+ * This file is part of the SkandaDX organization.
491
+ * Licensed under the MIT License. See LICENSE in the repository root.
492
+ */
493
+ const TAU$2 = 2 * Math.PI;
494
+ /** Wedge (or ring segment when `r0 > 0`) from angle `a0` to `a1`, clockwise from 12 o'clock. */
495
+ function arc(cx, cy, r, r0, a0, a1) {
496
+ const at = (radius, a) => `${px(cx + radius * Math.sin(a))} ${px(cy - radius * Math.cos(a))}`;
497
+ // A full circle has coincident endpoints, which an arc command cannot draw: split it in two.
498
+ if (a1 - a0 >= TAU$2 - 1e-9) {
499
+ const half = a0 + TAU$2 / 2;
500
+ return `${arc(cx, cy, r, r0, a0, half)}${arc(cx, cy, r, r0, half, a1)}`;
501
+ }
502
+ const large = a1 - a0 > Math.PI ? 1 : 0;
503
+ const outer = `M${at(r, a0)}A${r} ${r} 0 ${large} 1 ${at(r, a1)}`;
504
+ return r0 > 0
505
+ ? `${outer}L${at(r0, a1)}A${r0} ${r0} 0 ${large} 0 ${at(r0, a0)}Z`
506
+ : `${outer}L${px(cx)} ${px(cy)}Z`;
507
+ }
508
+ function layoutPieChart(input) {
509
+ const { width, height } = size(input, 300, 300);
510
+ const top = legendTop(input);
511
+ const cx = width / 2;
512
+ const cy = top + (height - top) / 2;
513
+ const r = Math.max(0, Math.min(width, height - top) / 2 - 8);
514
+ const r0 = r * Math.min(0.95, Math.max(0, input.innerRadius ?? 0));
515
+ const total = input.data.reduce((sum, d) => sum + Math.max(0, d.value), 0);
516
+ let angle = 0;
517
+ const labels = [];
518
+ const slices = input.data.map((d, i) => {
519
+ const percent = total > 0 ? Math.max(0, d.value) / total : 0;
520
+ const a0 = angle;
521
+ angle += percent * TAU$2;
522
+ if (input.labels && percent > 0.03) {
523
+ const mid = (a0 + angle) / 2;
524
+ const lr = (r + r0) / 2;
525
+ labels.push(label(px(cx + lr * Math.sin(mid)), px(cy - lr * Math.cos(mid)), `${d.name} ${Math.round(percent * 100)}%`, {
526
+ part: 'slice-label',
527
+ anchor: 'middle',
528
+ baseline: 'middle',
529
+ fill: 'var(--skdx-color-text-on-accent)',
530
+ }));
531
+ }
532
+ return {
533
+ kind: 'path',
534
+ part: 'slice',
535
+ d: percent > 0 ? arc(cx, cy, r, r0, a0, angle) : '',
536
+ fill: seriesColor(i, d.color),
537
+ title: `${d.name}: ${formatValue(d.value)} (${Math.round(percent * 100)}%)`,
538
+ };
539
+ });
540
+ const c = canvas(input, width, height, input.data.map((d, i) => ({ name: d.name, color: seriesColor(i, d.color) })));
541
+ return { ...c, shapes: [...slices, ...c.shapes, ...labels] };
542
+ }
543
+
544
+ /*
545
+ * Copyright (c) 2026 SkandaDX
546
+ * This file is part of the SkandaDX organization.
547
+ * Licensed under the MIT License. See LICENSE in the repository root.
548
+ */
549
+ const PAD$3 = 2;
550
+ function layoutSparkline(input) {
551
+ const { width, height } = size(input, 120, 32);
552
+ const n = input.data.length;
553
+ const y = linearScale(extent(input.data, false), [height - PAD$3, PAD$3]);
554
+ const x = (i) => (n < 2 ? width / 2 : PAD$3 + (i / (n - 1)) * (width - 2 * PAD$3));
555
+ const coords = input.data.map((v, i) => ({ x: px(x(i)), y: px(y(v)) }));
556
+ const line = pathThrough(coords, input.curve);
557
+ const last = coords[coords.length - 1];
558
+ const first = coords[0];
559
+ const color = seriesColor(0, input.color);
560
+ return {
561
+ width,
562
+ height,
563
+ tooltip: input.tooltip ?? true,
564
+ shapes: [
565
+ {
566
+ kind: 'path',
567
+ part: 'area',
568
+ d: line && first && last
569
+ ? `${line}L${last.x} ${height - PAD$3}L${first.x} ${height - PAD$3}Z`
570
+ : '',
571
+ fill: color,
572
+ fillOpacity: 0.15,
573
+ },
574
+ { kind: 'path', part: 'line', d: line, stroke: color, strokeWidth: 1.5 },
575
+ // The most recent point, drawn as a dot.
576
+ ...(last ? [point(last.x, last.y, 2, color)] : []),
577
+ ],
578
+ };
579
+ }
580
+
581
+ /*
582
+ * Copyright (c) 2026 SkandaDX
583
+ * This file is part of the SkandaDX organization.
584
+ * Licensed under the MIT License. See LICENSE in the repository root.
585
+ */
586
+ function layoutRadarChart(input) {
587
+ const { width, height } = size(input, 300, 300);
588
+ const top = legendTop(input);
589
+ const cx = width / 2;
590
+ const cy = top + (height - top) / 2;
591
+ const radius = Math.max(0, Math.min(width, height - top) / 2 - 36);
592
+ const n = input.categories.length;
593
+ const angle = (i) => -Math.PI / 2 + (i * 2 * Math.PI) / Math.max(1, n);
594
+ // Normalized: every series is scaled to its own maximum and the rings read as percentages.
595
+ const ticks = input.normalize
596
+ ? [0, 25, 50, 75, 100]
597
+ : niceTicks(0, Math.max(0, ...input.series.flatMap((s) => s.data)), 4);
598
+ const max = ticks[ticks.length - 1];
599
+ const seriesMax = input.series.map((s) => Math.max(0, ...s.data));
600
+ const norm = (si, v) => input.normalize
601
+ ? seriesMax[si] > 0
602
+ ? (v / seriesMax[si]) * 100
603
+ : 0
604
+ : v;
605
+ const at = (i, value) => {
606
+ const r = max > 0 ? (value / max) * radius : 0;
607
+ return { x: px(cx + r * Math.cos(angle(i))), y: px(cy + r * Math.sin(angle(i))) };
608
+ };
609
+ const polygon = (value) => n === 0
610
+ ? ''
611
+ : `M${input.categories
612
+ .map((_, i) => {
613
+ const p = at(i, value(i));
614
+ return `${p.x} ${p.y}`;
615
+ })
616
+ .join('L')}Z`;
617
+ // Concentric grid polygons, one per tick, innermost first, then a spoke per category.
618
+ const rings = ticks.filter((t) => t > 0);
619
+ const grid = [
620
+ ...rings.map((t) => ({
621
+ kind: 'path',
622
+ part: 'grid',
623
+ d: polygon(() => t),
624
+ stroke: CHART_COLORS.grid,
625
+ })),
626
+ ...input.categories.map((_, i) => {
627
+ const p = at(i, max);
628
+ return {
629
+ kind: 'line',
630
+ part: 'grid',
631
+ x1: px(cx),
632
+ y1: px(cy),
633
+ x2: p.x,
634
+ y2: p.y,
635
+ stroke: CHART_COLORS.grid,
636
+ };
637
+ }),
638
+ ...rings.map((t) => label(px(cx + 4), at(0, t).y, input.normalize ? `${t}%` : formatValue(t), {
639
+ part: 'y-label',
640
+ baseline: 'middle',
641
+ })),
642
+ ...input.categories.map((text, i) => {
643
+ const cos = Math.cos(angle(i));
644
+ const r = radius + 14;
645
+ return label(px(cx + r * cos), px(cy + r * Math.sin(angle(i))), text, {
646
+ part: 'x-label',
647
+ anchor: cos > 0.1 ? 'start' : cos < -0.1 ? 'end' : 'middle',
648
+ baseline: 'middle',
649
+ });
650
+ }),
651
+ ];
652
+ const points = [];
653
+ const polygons = input.series.map((s, si) => {
654
+ const color = seriesColor(si, s.color);
655
+ input.categories.forEach((category, i) => {
656
+ const value = s.data[i] ?? 0;
657
+ const p = at(i, norm(si, value));
658
+ points.push(point(p.x, p.y, 3, color, markTitle(s.name, category, value)));
659
+ });
660
+ return {
661
+ kind: 'path',
662
+ part: 'area',
663
+ d: polygon((i) => norm(si, s.data[i] ?? 0)),
664
+ fill: color,
665
+ fillOpacity: 0.25,
666
+ stroke: color,
667
+ strokeWidth: 2,
668
+ };
669
+ });
670
+ const c = canvas(input, width, height, input.series.map((s, i) => ({ name: s.name, color: seriesColor(i, s.color) })));
671
+ return { ...c, shapes: [...grid, ...polygons, ...points, ...c.shapes] };
672
+ }
673
+
674
+ /*
675
+ * Copyright (c) 2026 SkandaDX
676
+ * This file is part of the SkandaDX organization.
677
+ * Licensed under the MIT License. See LICENSE in the repository root.
678
+ */
679
+ const MARGIN = { top: 24, right: 8, bottom: 8, left: 64 };
680
+ function layoutHeatmap(input) {
681
+ const { width, height } = size(input);
682
+ const plot = plotBox(width, height, MARGIN);
683
+ const cellW = plot.width / Math.max(1, input.columns.length);
684
+ const cellH = plot.height / Math.max(1, input.rows.length);
685
+ const all = input.data.flat();
686
+ const min = Math.min(...all);
687
+ const max = Math.max(...all);
688
+ const color = seriesColor(0, input.color);
689
+ const negative = input.negativeColor ?? 'var(--skdx-color-danger-500)';
690
+ const span = Math.max(Math.abs(min), Math.abs(max));
691
+ // Diverging: opacity is the distance from zero, color the sign.
692
+ // Otherwise 0.15..1, the value's place between the smallest and largest cell.
693
+ const opacity = (v) => input.diverging
694
+ ? span === 0
695
+ ? 1
696
+ : px(0.15 + (0.85 * Math.abs(v)) / span)
697
+ : max === min
698
+ ? 1
699
+ : px(0.15 + (0.85 * (v - min)) / (max - min));
700
+ const cells = [];
701
+ input.rows.forEach((row, ri) => {
702
+ input.columns.forEach((column, ci) => {
703
+ const value = input.data[ri]?.[ci];
704
+ if (value === undefined)
705
+ return;
706
+ cells.push({
707
+ kind: 'rect',
708
+ part: 'cell',
709
+ x: px(plot.x + ci * cellW),
710
+ y: px(plot.y + ri * cellH),
711
+ width: px(cellW),
712
+ height: px(cellH),
713
+ fill: input.diverging && value < 0 ? negative : color,
714
+ opacity: opacity(value),
715
+ title: `${row}, ${column}: ${formatValue(value)}`,
716
+ });
717
+ });
718
+ });
719
+ const c = canvas(input, width, height);
720
+ return {
721
+ ...c,
722
+ shapes: [
723
+ ...input.rows.map((text, i) => label(plot.x - 8, px(plot.y + (i + 0.5) * cellH), text, {
724
+ part: 'y-label',
725
+ anchor: 'end',
726
+ baseline: 'middle',
727
+ })),
728
+ ...input.columns.map((text, i) => label(px(plot.x + (i + 0.5) * cellW), plot.y - 8, text, {
729
+ part: 'x-label',
730
+ anchor: 'middle',
731
+ })),
732
+ ...cells,
733
+ ...c.shapes,
734
+ ],
735
+ };
736
+ }
737
+
738
+ /*
739
+ * Copyright (c) 2026 SkandaDX
740
+ * This file is part of the SkandaDX organization.
741
+ * Licensed under the MIT License. See LICENSE in the repository root.
742
+ */
743
+ const PAD$2 = 8;
744
+ const GAP = 4;
745
+ function layoutFunnelChart(input) {
746
+ const { width, height } = size(input, 400, 300);
747
+ const n = input.data.length;
748
+ const max = Math.max(0, ...input.data.map((d) => d.value));
749
+ const first = input.data[0]?.value ?? 0;
750
+ const full = Math.max(0, width - 2 * PAD$2);
751
+ const offset = legendTop(input);
752
+ const stageH = n === 0 ? 0 : (height - offset - 2 * PAD$2) / n;
753
+ const widthOf = (value) => (max > 0 ? (Math.max(0, value) / max) * full : 0);
754
+ const legend = input.data.map((d, i) => ({ name: d.name, color: seriesColor(i, d.color) }));
755
+ const stages = input.data.flatMap((d, i) => {
756
+ const top = widthOf(d.value);
757
+ const bottom = widthOf(input.data[i + 1]?.value ?? d.value);
758
+ const y0 = offset + PAD$2 + i * stageH;
759
+ const y1 = y0 + stageH - GAP;
760
+ const cx = width / 2;
761
+ const percent = first > 0 ? d.value / first : 0;
762
+ return [
763
+ {
764
+ kind: 'path',
765
+ part: 'stage',
766
+ d: `M${px(cx - top / 2)} ${px(y0)}L${px(cx + top / 2)} ${px(y0)}L${px(cx + bottom / 2)} ${px(y1)}L${px(cx - bottom / 2)} ${px(y1)}Z`,
767
+ fill: seriesColor(i, d.color),
768
+ title: `${d.name}: ${formatValue(d.value)} (${Math.round(percent * 100)}%)`,
769
+ },
770
+ label(px(cx), px((y0 + y1) / 2), `${d.name} · ${formatValue(d.value)}`, {
771
+ part: 'stage-label',
772
+ anchor: 'middle',
773
+ baseline: 'middle',
774
+ fill: 'var(--skdx-color-text-on-accent)',
775
+ }),
776
+ ];
777
+ });
778
+ const c = canvas(input, width, height, legend);
779
+ return { ...c, shapes: [...stages, ...c.shapes] };
780
+ }
781
+
782
+ /*
783
+ * Copyright (c) 2026 SkandaDX
784
+ * This file is part of the SkandaDX organization.
785
+ * Licensed under the MIT License. See LICENSE in the repository root.
786
+ */
787
+ const NODE_WIDTH = 16;
788
+ const NODE_PAD = 8;
789
+ function layoutSankey(input) {
790
+ const { width, height } = size(input);
791
+ const plot = plotBox(width, height, { top: 8 + legendTop(input), right: 96, bottom: 8, left: 8 });
792
+ const nodes = new Map();
793
+ const node = (name) => {
794
+ let n = nodes.get(name);
795
+ if (!n) {
796
+ n = {
797
+ name,
798
+ depth: 0,
799
+ value: 0,
800
+ color: seriesColor(nodes.size),
801
+ x: 0,
802
+ y: 0,
803
+ height: 0,
804
+ outY: 0,
805
+ inY: 0,
806
+ };
807
+ nodes.set(name, n);
808
+ }
809
+ return n;
810
+ };
811
+ const links = dropBackEdges(input.links.filter((l) => l.value > 0 && l.source !== l.target));
812
+ for (const l of links) {
813
+ node(l.source);
814
+ node(l.target);
815
+ }
816
+ // Depth = longest path from a source; the graph is acyclic after dropBackEdges.
817
+ for (let pass = 0, changed = true; changed && pass <= nodes.size; pass++) {
818
+ changed = false;
819
+ for (const l of links) {
820
+ const d = node(l.source).depth + 1;
821
+ if (d > node(l.target).depth) {
822
+ node(l.target).depth = d;
823
+ changed = true;
824
+ }
825
+ }
826
+ }
827
+ const outSum = new Map();
828
+ const inSum = new Map();
829
+ for (const l of links) {
830
+ outSum.set(l.source, (outSum.get(l.source) ?? 0) + l.value);
831
+ inSum.set(l.target, (inSum.get(l.target) ?? 0) + l.value);
832
+ }
833
+ for (const n of nodes.values())
834
+ n.value = Math.max(outSum.get(n.name) ?? 0, inSum.get(n.name) ?? 0);
835
+ const columns = new Map();
836
+ for (const n of nodes.values())
837
+ columns.set(n.depth, [...(columns.get(n.depth) ?? []), n]);
838
+ const depths = Math.max(0, ...columns.keys());
839
+ const scale = Math.min(...[...columns.values()].map((col) => (plot.height - NODE_PAD * (col.length - 1)) / col.reduce((s, n) => s + n.value, 0)));
840
+ for (const [depth, col] of columns) {
841
+ let y = plot.y;
842
+ for (const n of col) {
843
+ n.x = plot.x + (depths === 0 ? 0 : (depth * (plot.width - NODE_WIDTH)) / depths);
844
+ n.y = y;
845
+ n.height = n.value * scale;
846
+ n.outY = n.inY = y;
847
+ y += n.height + NODE_PAD;
848
+ }
849
+ }
850
+ const shapes = [];
851
+ for (const l of links) {
852
+ const s = node(l.source);
853
+ const t = node(l.target);
854
+ const h = l.value * scale;
855
+ const x0 = s.x + NODE_WIDTH;
856
+ const x1 = t.x;
857
+ const mx = (x0 + x1) / 2;
858
+ const sy = s.outY;
859
+ const ty = t.inY;
860
+ s.outY += h;
861
+ t.inY += h;
862
+ shapes.push({
863
+ kind: 'path',
864
+ part: 'link',
865
+ d: `M${px(x0)} ${px(sy)}C${px(mx)} ${px(sy)} ${px(mx)} ${px(ty)} ${px(x1)} ${px(ty)}L${px(x1)} ${px(ty + h)}C${px(mx)} ${px(ty + h)} ${px(mx)} ${px(sy + h)} ${px(x0)} ${px(sy + h)}Z`,
866
+ fill: s.color,
867
+ opacity: 0.4,
868
+ title: `${l.source} → ${l.target}: ${formatValue(l.value)}`,
869
+ });
870
+ }
871
+ for (const n of nodes.values()) {
872
+ shapes.push({
873
+ kind: 'rect',
874
+ part: 'node',
875
+ x: px(n.x),
876
+ y: px(n.y),
877
+ width: NODE_WIDTH,
878
+ height: px(n.height),
879
+ fill: n.color,
880
+ title: `${n.name}: ${formatValue(n.value)}`,
881
+ });
882
+ const last = n.depth === depths;
883
+ shapes.push(label(px(last ? n.x - 6 : n.x + NODE_WIDTH + 6), px(n.y + n.height / 2), n.name, {
884
+ anchor: last ? 'end' : 'start',
885
+ baseline: 'middle',
886
+ }));
887
+ }
888
+ return { ...canvas(input, width, height), shapes };
889
+ }
890
+ /** Removes the links that close a cycle (depth-first back edges), so every remaining path terminates. */
891
+ function dropBackEdges(links) {
892
+ const out = new Map();
893
+ for (const l of links)
894
+ out.set(l.source, [...(out.get(l.source) ?? []), l]);
895
+ const state = new Map();
896
+ const back = new Set();
897
+ const visit = (name) => {
898
+ state.set(name, 'active');
899
+ for (const l of out.get(name) ?? []) {
900
+ const s = state.get(l.target);
901
+ if (s === 'active')
902
+ back.add(l);
903
+ else if (s === undefined)
904
+ visit(l.target);
905
+ }
906
+ state.set(name, 'done');
907
+ };
908
+ for (const l of links)
909
+ if (!state.has(l.source))
910
+ visit(l.source);
911
+ return links.filter((l) => !back.has(l));
912
+ }
913
+
914
+ /*
915
+ * Copyright (c) 2026 SkandaDX
916
+ * This file is part of the SkandaDX organization.
917
+ * Licensed under the MIT License. See LICENSE in the repository root.
918
+ */
919
+ const nodeValue$1 = (n) => n.value ?? (n.children ?? []).reduce((s, c) => s + nodeValue$1(c), 0);
920
+ /** Squarified treemap (Bruls, Huizing, van Wijk): rows of tiles kept as close to square as possible. */
921
+ function squarify(items, x, y, w, h, out) {
922
+ if (items.length === 0)
923
+ return;
924
+ const worst = (row, side) => {
925
+ const sum = row.reduce((s, t) => s + t.area, 0);
926
+ const max = Math.max(...row.map((t) => t.area));
927
+ const min = Math.min(...row.map((t) => t.area));
928
+ return Math.max((side * side * max) / (sum * sum), (sum * sum) / (side * side * min));
929
+ };
930
+ const side = Math.min(w, h);
931
+ let row = [];
932
+ let i = 0;
933
+ while (i < items.length) {
934
+ const next = [...row, items[i]];
935
+ if (row.length === 0 || worst(next, side) <= worst(row, side)) {
936
+ row = next;
937
+ i++;
938
+ }
939
+ else
940
+ break;
941
+ }
942
+ const sum = row.reduce((s, t) => s + t.area, 0);
943
+ if (w >= h) {
944
+ const rowW = sum / h;
945
+ let ry = y;
946
+ for (const t of row) {
947
+ const th = t.area / rowW;
948
+ out(t, [x, ry, rowW, th]);
949
+ ry += th;
950
+ }
951
+ squarify(items.slice(i), x + rowW, y, w - rowW, h, out);
952
+ }
953
+ else {
954
+ const rowH = sum / w;
955
+ let rx = x;
956
+ for (const t of row) {
957
+ const tw = t.area / rowH;
958
+ out(t, [rx, y, tw, rowH]);
959
+ rx += tw;
960
+ }
961
+ squarify(items.slice(i), x, y + rowH, w, h - rowH, out);
962
+ }
963
+ }
964
+ function layoutTreemap(input) {
965
+ const { width, height } = size(input);
966
+ const top = legendTop(input);
967
+ const shapes = [];
968
+ const legend = input.data.map((n, i) => ({ name: n.name, color: seriesColor(i, n.color) }));
969
+ // Branches are outlined and their children tiled inside a 2px inset; leaves are filled.
970
+ const tile = (nodes, x, y, w, h, inherited) => {
971
+ const total = nodes.reduce((s, n) => s + Math.max(0, nodeValue$1(n)), 0);
972
+ const tiles = nodes
973
+ .map((node, i) => ({
974
+ node,
975
+ value: nodeValue$1(node),
976
+ color: node.color ?? inherited ?? seriesColor(i),
977
+ area: total > 0 ? (Math.max(0, nodeValue$1(node)) / total) * w * h : 0,
978
+ }))
979
+ .filter((t) => t.area > 0)
980
+ .sort((a, b) => b.area - a.area);
981
+ squarify(tiles, x, y, w, h, (t, [tx, ty, tw, th]) => {
982
+ const title = `${t.node.name}: ${formatValue(t.value)}`;
983
+ const branch = (t.node.children?.length ?? 0) > 0;
984
+ shapes.push({
985
+ kind: 'rect',
986
+ part: branch ? 'branch' : 'tile',
987
+ x: px(tx),
988
+ y: px(ty),
989
+ width: px(tw),
990
+ height: px(th),
991
+ fill: t.color,
992
+ opacity: branch ? 0.15 : undefined,
993
+ stroke: 'var(--skdx-color-surface-default)',
994
+ title,
995
+ });
996
+ if (branch)
997
+ tile(t.node.children, tx + 2, ty + 2, Math.max(0, tw - 4), Math.max(0, th - 4), t.color);
998
+ else if (tw > 40 && th > 20)
999
+ shapes.push(label(px(tx + 6), px(ty + 6), t.node.name, {
1000
+ baseline: 'hanging',
1001
+ fill: 'var(--skdx-color-text-on-accent)',
1002
+ }));
1003
+ });
1004
+ };
1005
+ tile(input.data, 0, top, width, height - top, undefined);
1006
+ const c = canvas(input, width, height, legend);
1007
+ return { ...c, shapes: [...shapes, ...c.shapes] };
1008
+ }
1009
+
1010
+ /*
1011
+ * Copyright (c) 2026 SkandaDX
1012
+ * This file is part of the SkandaDX organization.
1013
+ * Licensed under the MIT License. See LICENSE in the repository root.
1014
+ */
1015
+ const UP$1 = 'var(--skdx-color-success-500)';
1016
+ const DOWN$1 = 'var(--skdx-color-danger-500)';
1017
+ function layoutCandlestick(input) {
1018
+ const { width, height } = size(input);
1019
+ const plot = plotBox(width, height, { ...MARGIN$1, top: MARGIN$1.top + legendTop(input) });
1020
+ const y = axis(extent(input.data.flatMap((c) => [c.low, c.high]), false), [plot.y + plot.height, plot.y], input.scale === 'log');
1021
+ const x = bandScale(input.data.length, [plot.x, plot.x + plot.width]);
1022
+ const legend = [
1023
+ { name: 'Up', color: UP$1 },
1024
+ { name: 'Down', color: DOWN$1 },
1025
+ ];
1026
+ const c = canvas(input, width, height, legend);
1027
+ const frame = {
1028
+ ...c,
1029
+ plot,
1030
+ xTicks: input.data.map((c, i) => ({ pos: px(x.center(i)), label: c.label })),
1031
+ yTicks: y.ticks,
1032
+ baseline: plot.y + plot.height,
1033
+ };
1034
+ const shapes = frameShapes(frame);
1035
+ input.data.forEach((c, i) => {
1036
+ const color = c.close >= c.open ? UP$1 : DOWN$1;
1037
+ const title = `${c.label}: O ${formatValue(c.open)} H ${formatValue(c.high)} L ${formatValue(c.low)} C ${formatValue(c.close)}`;
1038
+ const top = y.scale(Math.max(c.open, c.close));
1039
+ const bottom = y.scale(Math.min(c.open, c.close));
1040
+ shapes.push({
1041
+ kind: 'line',
1042
+ part: 'wick',
1043
+ x1: px(x.center(i)),
1044
+ x2: px(x.center(i)),
1045
+ y1: px(y.scale(c.high)),
1046
+ y2: px(y.scale(c.low)),
1047
+ stroke: color,
1048
+ title,
1049
+ }, {
1050
+ kind: 'rect',
1051
+ part: 'candle',
1052
+ x: px(x.start(i)),
1053
+ y: px(top),
1054
+ width: px(x.bandwidth),
1055
+ height: px(Math.max(1, bottom - top)),
1056
+ fill: color,
1057
+ title,
1058
+ });
1059
+ });
1060
+ return { ...c, shapes: [...shapes, ...c.shapes] };
1061
+ }
1062
+
1063
+ /*
1064
+ * Copyright (c) 2026 SkandaDX
1065
+ * This file is part of the SkandaDX organization.
1066
+ * Licensed under the MIT License. See LICENSE in the repository root.
1067
+ */
1068
+ const UP = 'var(--skdx-color-success-500)';
1069
+ const DOWN = 'var(--skdx-color-danger-500)';
1070
+ function layoutWaterfall(input) {
1071
+ const { width, height } = size(input);
1072
+ const plot = plotBox(width, height, { ...MARGIN$1, top: MARGIN$1.top + legendTop(input) });
1073
+ const steps = [];
1074
+ let running = 0;
1075
+ for (const d of input.data) {
1076
+ const from = running;
1077
+ running += d.value;
1078
+ steps.push({
1079
+ name: d.name,
1080
+ from,
1081
+ to: running,
1082
+ color: d.color ?? (d.value >= 0 ? UP : DOWN),
1083
+ title: `${d.name}: ${d.value >= 0 ? '+' : ''}${formatValue(d.value)}`,
1084
+ });
1085
+ }
1086
+ if (input.total !== undefined)
1087
+ steps.push({
1088
+ name: input.total,
1089
+ from: 0,
1090
+ to: running,
1091
+ color: seriesColor(0),
1092
+ title: `${input.total}: ${formatValue(running)}`,
1093
+ });
1094
+ const totals = steps.flatMap((s) => [s.from, s.to]);
1095
+ const y = axis([Math.min(0, ...totals), Math.max(0, ...totals)], [plot.y + plot.height, plot.y]);
1096
+ const x = bandScale(steps.length, [plot.x, plot.x + plot.width]);
1097
+ const c = canvas(input, width, height, [
1098
+ { name: 'Increase', color: UP },
1099
+ { name: 'Decrease', color: DOWN },
1100
+ ]);
1101
+ const shapes = frameShapes({
1102
+ ...c,
1103
+ plot,
1104
+ xTicks: steps.map((s, i) => ({ pos: px(x.center(i)), label: s.name })),
1105
+ yTicks: y.ticks,
1106
+ baseline: y.zero,
1107
+ });
1108
+ steps.forEach((s, i) => {
1109
+ const y0 = y.scale(s.from);
1110
+ const y1 = y.scale(s.to);
1111
+ shapes.push({
1112
+ kind: 'rect',
1113
+ part: 'bar',
1114
+ x: px(x.start(i)),
1115
+ y: px(Math.min(y0, y1)),
1116
+ width: px(x.bandwidth),
1117
+ height: px(Math.max(1, Math.abs(y1 - y0))),
1118
+ fill: s.color,
1119
+ title: s.title,
1120
+ });
1121
+ const next = steps[i + 1];
1122
+ if (next)
1123
+ shapes.push({
1124
+ kind: 'line',
1125
+ part: 'connector',
1126
+ x1: px(x.start(i) + x.bandwidth),
1127
+ x2: px(x.start(i + 1)),
1128
+ y1: px(y1),
1129
+ y2: px(y.scale(next.from)),
1130
+ stroke: CHART_COLORS.axis,
1131
+ dash: '4 3',
1132
+ });
1133
+ });
1134
+ return { ...c, shapes: [...shapes, ...c.shapes] };
1135
+ }
1136
+
1137
+ /*
1138
+ * Copyright (c) 2026 SkandaDX
1139
+ * This file is part of the SkandaDX organization.
1140
+ * Licensed under the MIT License. See LICENSE in the repository root.
1141
+ */
1142
+ function layoutGantt(input) {
1143
+ const { width, height } = size(input);
1144
+ const plot = plotBox(width, height, {
1145
+ top: 8 + legendTop(input),
1146
+ right: 16,
1147
+ bottom: 32,
1148
+ left: 112,
1149
+ });
1150
+ const format = input.format ?? formatValue;
1151
+ const times = input.tasks.flatMap((t) => [t.start, t.end]);
1152
+ const x = axis(times.length ? [Math.min(...times), Math.max(...times)] : [0, 0], [
1153
+ plot.x,
1154
+ plot.x + plot.width,
1155
+ ]);
1156
+ const rows = bandScale(input.tasks.length, [plot.y, plot.y + plot.height]);
1157
+ const shapes = [];
1158
+ const ticks = x.ticks.map((t) => ({ ...t, label: format(Number(t.label.replace(/,/g, ''))) }));
1159
+ for (const t of ticks) {
1160
+ shapes.push({
1161
+ kind: 'line',
1162
+ part: 'grid',
1163
+ x1: t.pos,
1164
+ x2: t.pos,
1165
+ y1: plot.y,
1166
+ y2: plot.y + plot.height,
1167
+ stroke: CHART_COLORS.grid,
1168
+ });
1169
+ shapes.push(label(t.pos, plot.y + plot.height + 20, t.label, { part: 'x-label', anchor: 'middle' }));
1170
+ }
1171
+ input.tasks.forEach((t, i) => {
1172
+ const x0 = x.scale(Math.min(t.start, t.end));
1173
+ const x1 = x.scale(Math.max(t.start, t.end));
1174
+ shapes.push(label(plot.x - 8, px(rows.center(i)), t.name, {
1175
+ part: 'y-label',
1176
+ anchor: 'end',
1177
+ baseline: 'middle',
1178
+ }));
1179
+ shapes.push({
1180
+ kind: 'rect',
1181
+ part: 'bar',
1182
+ x: px(x0),
1183
+ y: px(rows.start(i)),
1184
+ width: px(Math.max(1, x1 - x0)),
1185
+ height: px(rows.bandwidth),
1186
+ fill: seriesColor(i, t.color),
1187
+ title: `${t.name}: ${format(t.start)} – ${format(t.end)}`,
1188
+ });
1189
+ });
1190
+ const c = canvas(input, width, height, input.tasks.map((t, i) => ({ name: t.name, color: seriesColor(i, t.color) })));
1191
+ return { ...c, shapes: [...shapes, ...c.shapes] };
1192
+ }
1193
+
1194
+ /*
1195
+ * Copyright (c) 2026 SkandaDX
1196
+ * This file is part of the SkandaDX organization.
1197
+ * Licensed under the MIT License. See LICENSE in the repository root.
1198
+ */
1199
+ /** Linear-interpolated quantile of a sorted sample. */
1200
+ const quantile = (sorted, q) => {
1201
+ if (sorted.length === 0)
1202
+ return 0;
1203
+ const pos = (sorted.length - 1) * q;
1204
+ const lo = Math.floor(pos);
1205
+ const hi = Math.ceil(pos);
1206
+ const a = sorted[lo];
1207
+ const b = sorted[hi];
1208
+ return a + (b - a) * (pos - lo);
1209
+ };
1210
+ /** Five-number summary with Tukey fences: whiskers reach the last sample within 1.5 IQR. */
1211
+ function boxStats(data) {
1212
+ const sorted = [...data].sort((a, b) => a - b);
1213
+ const q1 = quantile(sorted, 0.25);
1214
+ const median = quantile(sorted, 0.5);
1215
+ const q3 = quantile(sorted, 0.75);
1216
+ const iqr = q3 - q1;
1217
+ const inside = sorted.filter((v) => v >= q1 - 1.5 * iqr && v <= q3 + 1.5 * iqr);
1218
+ return {
1219
+ q1,
1220
+ median,
1221
+ q3,
1222
+ low: inside[0] ?? q1,
1223
+ high: inside[inside.length - 1] ?? q3,
1224
+ outliers: sorted.filter((v) => v < q1 - 1.5 * iqr || v > q3 + 1.5 * iqr),
1225
+ };
1226
+ }
1227
+ function layoutBoxplot(input) {
1228
+ const { width, height } = size(input);
1229
+ const plot = plotBox(width, height, { ...MARGIN$1, top: MARGIN$1.top + legendTop(input) });
1230
+ const y = axis(extent(input.series.flatMap((s) => s.data), false), [plot.y + plot.height, plot.y]);
1231
+ const x = bandScale(input.series.length, [plot.x, plot.x + plot.width]);
1232
+ const c = canvas(input, width, height, input.series.map((s, i) => ({ name: s.name, color: seriesColor(i, s.color) })));
1233
+ const shapes = frameShapes({
1234
+ ...c,
1235
+ plot,
1236
+ xTicks: input.series.map((s, i) => ({ pos: px(x.center(i)), label: s.name })),
1237
+ yTicks: y.ticks,
1238
+ baseline: plot.y + plot.height,
1239
+ });
1240
+ input.series.forEach((s, i) => {
1241
+ const st = boxStats(s.data);
1242
+ const color = seriesColor(i, s.color);
1243
+ const cx = px(x.center(i));
1244
+ const title = `${s.name}: min ${formatValue(st.low)} Q1 ${formatValue(st.q1)} median ${formatValue(st.median)} Q3 ${formatValue(st.q3)} max ${formatValue(st.high)}`;
1245
+ const boxW = x.bandwidth * 0.6;
1246
+ const bx = px(x.center(i) - boxW / 2);
1247
+ shapes.push({
1248
+ kind: 'line',
1249
+ part: 'whisker',
1250
+ x1: cx,
1251
+ x2: cx,
1252
+ y1: px(y.scale(st.high)),
1253
+ y2: px(y.scale(st.low)),
1254
+ stroke: color,
1255
+ title,
1256
+ }, {
1257
+ kind: 'rect',
1258
+ part: 'box',
1259
+ x: bx,
1260
+ y: px(y.scale(st.q3)),
1261
+ width: px(boxW),
1262
+ height: px(Math.max(1, y.scale(st.q1) - y.scale(st.q3))),
1263
+ fill: color,
1264
+ stroke: color,
1265
+ opacity: 0.5,
1266
+ title,
1267
+ }, {
1268
+ kind: 'line',
1269
+ part: 'median',
1270
+ x1: bx,
1271
+ x2: px(bx + boxW),
1272
+ y1: px(y.scale(st.median)),
1273
+ y2: px(y.scale(st.median)),
1274
+ stroke: color,
1275
+ title,
1276
+ }, ...st.outliers.map((v) => ({
1277
+ kind: 'circle',
1278
+ part: 'outlier',
1279
+ cx,
1280
+ cy: px(y.scale(v)),
1281
+ r: 3,
1282
+ fill: color,
1283
+ title: `${s.name}: ${formatValue(v)}`,
1284
+ })));
1285
+ });
1286
+ return { ...c, shapes: [...shapes, ...c.shapes] };
1287
+ }
1288
+
1289
+ /*
1290
+ * Copyright (c) 2026 SkandaDX
1291
+ * This file is part of the SkandaDX organization.
1292
+ * Licensed under the MIT License. See LICENSE in the repository root.
1293
+ */
1294
+ const TAU$1 = 2 * Math.PI;
1295
+ const nodeValue = (n) => n.value ?? (n.children ?? []).reduce((s, c) => s + nodeValue(c), 0);
1296
+ const depthOf = (nodes) => nodes.length === 0 ? 0 : 1 + Math.max(...nodes.map((n) => depthOf(n.children ?? [])));
1297
+ function layoutSunburst(input) {
1298
+ const { width, height } = size(input, 300, 300);
1299
+ const top = legendTop(input);
1300
+ const cx = width / 2;
1301
+ const cy = top + (height - top) / 2;
1302
+ const ringWidth = Math.max(0, Math.min(width, height - top) / 2 - 8) / Math.max(1, depthOf(input.data));
1303
+ const shapes = [];
1304
+ const walk = (nodes, depth, a0, span, color) => {
1305
+ const total = nodes.reduce((s, n) => s + Math.max(0, nodeValue(n)), 0);
1306
+ let angle = a0;
1307
+ nodes.forEach((n, i) => {
1308
+ const value = Math.max(0, nodeValue(n));
1309
+ const sweep = total > 0 ? (value / total) * span : 0;
1310
+ const fill = n.color ?? color ?? seriesColor(i);
1311
+ if (sweep > 0) {
1312
+ shapes.push({
1313
+ kind: 'path',
1314
+ part: 'arc',
1315
+ d: arc(cx, cy, ringWidth * (depth + 1), ringWidth * depth, angle, angle + sweep),
1316
+ fill,
1317
+ opacity: 1 - depth * 0.2,
1318
+ stroke: 'var(--skdx-color-surface-default)',
1319
+ title: `${n.name}: ${formatValue(value)}`,
1320
+ });
1321
+ walk(n.children ?? [], depth + 1, angle, sweep, fill);
1322
+ }
1323
+ angle += sweep;
1324
+ });
1325
+ };
1326
+ walk(input.data, 0, 0, TAU$1, undefined);
1327
+ const c = canvas(input, width, height, input.data.map((n, i) => ({ name: n.name, color: n.color ?? seriesColor(i) })));
1328
+ return { ...c, shapes: [...shapes, ...c.shapes] };
1329
+ }
1330
+
1331
+ /*
1332
+ * Copyright (c) 2026 SkandaDX
1333
+ * This file is part of the SkandaDX organization.
1334
+ * Licensed under the MIT License. See LICENSE in the repository root.
1335
+ */
1336
+ const TAU = 2 * Math.PI;
1337
+ const PAD$1 = 0.04;
1338
+ const RING = 12;
1339
+ function layoutChord(input) {
1340
+ const { width, height } = size(input, 300, 300);
1341
+ const top = legendTop(input);
1342
+ const cx = width / 2;
1343
+ const cy = top + (height - top) / 2;
1344
+ const r = Math.max(0, Math.min(width, height - top) / 2 - 36);
1345
+ const r0 = Math.max(0, r - RING);
1346
+ const n = input.names.length;
1347
+ const cell = (i, j) => Math.max(0, input.matrix[i]?.[j] ?? 0);
1348
+ const rowSum = input.names.map((_, i) => input.names.reduce((s, _n, j) => s + cell(i, j), 0));
1349
+ const total = rowSum.reduce((s, v) => s + v, 0);
1350
+ const unit = total > 0 ? (TAU - PAD$1 * n) / total : 0;
1351
+ const at = (a, radius) => `${px(cx + radius * Math.sin(a))} ${px(cy - radius * Math.cos(a))}`;
1352
+ // Each group spans its row sum; inside it, subgroup j spans matrix[i][j].
1353
+ const groups = [];
1354
+ let angle = 0;
1355
+ for (let i = 0; i < n; i++) {
1356
+ const a0 = angle;
1357
+ const sub = input.names.map((_, j) => {
1358
+ const s0 = angle;
1359
+ angle += cell(i, j) * unit;
1360
+ return { a0: s0, a1: angle };
1361
+ });
1362
+ groups.push({ a0, a1: angle, sub });
1363
+ angle += PAD$1;
1364
+ }
1365
+ const shapes = [];
1366
+ for (let i = 0; i < n; i++) {
1367
+ for (let j = i; j < n; j++) {
1368
+ const out = cell(i, j);
1369
+ const back = cell(j, i);
1370
+ if (out + back === 0)
1371
+ continue;
1372
+ const a = groups[i].sub[j];
1373
+ const b = groups[j].sub[i];
1374
+ const large = (s) => (s.a1 - s.a0 > Math.PI ? 1 : 0);
1375
+ const d = i === j
1376
+ ? `M${at(a.a0, r0)}A${r0} ${r0} 0 ${large(a)} 1 ${at(a.a1, r0)}Q${px(cx)} ${px(cy)} ${at(a.a0, r0)}Z`
1377
+ : `M${at(a.a0, r0)}A${r0} ${r0} 0 ${large(a)} 1 ${at(a.a1, r0)}Q${px(cx)} ${px(cy)} ${at(b.a0, r0)}A${r0} ${r0} 0 ${large(b)} 1 ${at(b.a1, r0)}Q${px(cx)} ${px(cy)} ${at(a.a0, r0)}Z`;
1378
+ const from = out >= back ? i : j;
1379
+ shapes.push({
1380
+ kind: 'path',
1381
+ part: 'chord',
1382
+ d,
1383
+ fill: seriesColor(from),
1384
+ opacity: 0.5,
1385
+ title: i === j
1386
+ ? `${input.names[i]}: ${formatValue(out)}`
1387
+ : `${input.names[i]} ↔ ${input.names[j]}: ${formatValue(out)} / ${formatValue(back)}`,
1388
+ });
1389
+ }
1390
+ }
1391
+ groups.forEach((g, i) => {
1392
+ if (g.a1 <= g.a0)
1393
+ return;
1394
+ shapes.push({
1395
+ kind: 'path',
1396
+ part: 'group',
1397
+ d: arc(cx, cy, r, r0, g.a0, g.a1),
1398
+ fill: seriesColor(i),
1399
+ title: `${input.names[i]}: ${formatValue(rowSum[i])}`,
1400
+ });
1401
+ const mid = (g.a0 + g.a1) / 2;
1402
+ const sin = Math.sin(mid);
1403
+ shapes.push(label(px(cx + (r + 8) * sin), px(cy - (r + 8) * Math.cos(mid)), input.names[i], {
1404
+ anchor: sin > 0.1 ? 'start' : sin < -0.1 ? 'end' : 'middle',
1405
+ baseline: 'middle',
1406
+ }));
1407
+ });
1408
+ const c = canvas(input, width, height, input.names.map((name, i) => ({ name, color: seriesColor(i) })));
1409
+ return { ...c, shapes: [...shapes, ...c.shapes] };
1410
+ }
1411
+
1412
+ /*
1413
+ * Copyright (c) 2026 SkandaDX
1414
+ * This file is part of the SkandaDX organization.
1415
+ * Licensed under the MIT License. See LICENSE in the repository root.
1416
+ */
1417
+ const PAD = 8;
1418
+ const rings = (f) => f.geometry.type === 'Polygon' ? f.geometry.coordinates : f.geometry.coordinates.flat();
1419
+ /** Mercator latitude, clamped to ±85° so the poles stay finite. */
1420
+ const mercatorY = (lat) => Math.log(Math.tan(Math.PI / 4 + (Math.max(-85, Math.min(85, lat)) * Math.PI) / 360));
1421
+ /** Projection fitted to the features' bounds (equirectangular by default, or Mercator); opacity follows the value. */
1422
+ function layoutMap(input) {
1423
+ const { width, height } = size(input);
1424
+ const top = legendTop(input);
1425
+ const toY = input.projection === 'mercator' ? mercatorY : (lat) => lat;
1426
+ const points = input.features.flatMap((f) => rings(f).flat());
1427
+ const lons = points.map((p) => p[0]);
1428
+ const lats = points.map((p) => toY(p[1]));
1429
+ const [minLon, maxLon, minLat, maxLat] = points.length
1430
+ ? [Math.min(...lons), Math.max(...lons), Math.min(...lats), Math.max(...lats)]
1431
+ : [0, 1, 0, 1];
1432
+ const scale = Math.min((width - 2 * PAD) / Math.max(1e-9, maxLon - minLon), (height - top - 2 * PAD) / Math.max(1e-9, maxLat - minLat));
1433
+ const ox = (width - (maxLon - minLon) * scale) / 2;
1434
+ const oy = top + (height - top - (maxLat - minLat) * scale) / 2;
1435
+ const project = ([lon, lat]) => `${px(ox + (lon - minLon) * scale)} ${px(oy + (maxLat - toY(lat)) * scale)}`;
1436
+ const values = input.features.map((f) => f.value);
1437
+ const min = Math.min(...values);
1438
+ const max = Math.max(...values);
1439
+ const color = seriesColor(0, input.color);
1440
+ const shapes = input.features.map((f) => ({
1441
+ kind: 'path',
1442
+ part: 'region',
1443
+ d: rings(f)
1444
+ .map((ring) => `M${ring.map(project).join('L')}Z`)
1445
+ .join(''),
1446
+ fill: color,
1447
+ opacity: max === min ? 1 : px(0.15 + (0.85 * (f.value - min)) / (max - min)),
1448
+ stroke: 'var(--skdx-color-surface-default)',
1449
+ title: `${f.name}: ${formatValue(f.value)}`,
1450
+ }));
1451
+ return { ...canvas(input, width, height), shapes };
1452
+ }
1453
+
1454
+ /*
1455
+ * Copyright (c) 2026 SkandaDX
1456
+ * This file is part of the SkandaDX organization.
1457
+ * Licensed under the MIT License. See LICENSE in the repository root.
1458
+ */
1459
+
1460
+ /*
1461
+ * Copyright (c) 2026 SkandaDX
1462
+ * This file is part of the SkandaDX organization.
1463
+ * Licensed under the MIT License. See LICENSE in the repository root.
1464
+ */
1465
+ /** Draws a core shape list back to front. */
1466
+ class SkdxChartShapesComponent {
1467
+ constructor() {
1468
+ this.shapes = input.required(/* @ts-ignore */
1469
+ ...(ngDevMode ? [{ debugName: "shapes" }] : /* istanbul ignore next */ []));
1470
+ }
1471
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SkdxChartShapesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1472
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: SkdxChartShapesComponent, isStandalone: true, selector: "g[skdxChartShapes]", inputs: { shapes: { classPropertyName: "shapes", publicName: "shapes", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
1473
+ @for (s of shapes(); track $index) {
1474
+ @switch (s.kind) {
1475
+ @case ('path') {
1476
+ <svg:path
1477
+ [attr.data-skdx-part]="s.part"
1478
+ [attr.d]="s.d"
1479
+ [attr.fill]="s.fill ?? 'none'"
1480
+ [attr.stroke]="s.stroke"
1481
+ [attr.stroke-width]="s.strokeWidth"
1482
+ [attr.stroke-linecap]="s.strokeLinecap"
1483
+ [attr.stroke-linejoin]="s.strokeLinejoin"
1484
+ [attr.opacity]="s.opacity"
1485
+ [attr.fill-opacity]="s.fillOpacity"
1486
+ >
1487
+ @if (s.title) {
1488
+ <svg:title>{{ s.title }}</svg:title>
1489
+ }
1490
+ </svg:path>
1491
+ }
1492
+ @case ('rect') {
1493
+ <svg:rect
1494
+ [attr.data-skdx-part]="s.part"
1495
+ [attr.x]="s.x"
1496
+ [attr.y]="s.y"
1497
+ [attr.width]="s.width"
1498
+ [attr.height]="s.height"
1499
+ [attr.fill]="s.fill"
1500
+ [attr.stroke]="s.stroke"
1501
+ [attr.fill-opacity]="s.opacity"
1502
+ >
1503
+ @if (s.title) {
1504
+ <svg:title>{{ s.title }}</svg:title>
1505
+ }
1506
+ </svg:rect>
1507
+ }
1508
+ @case ('line') {
1509
+ <svg:line
1510
+ [attr.data-skdx-part]="s.part"
1511
+ [attr.x1]="s.x1"
1512
+ [attr.y1]="s.y1"
1513
+ [attr.x2]="s.x2"
1514
+ [attr.y2]="s.y2"
1515
+ [attr.stroke]="s.stroke"
1516
+ [attr.stroke-dasharray]="s.dash"
1517
+ >
1518
+ @if (s.title) {
1519
+ <svg:title>{{ s.title }}</svg:title>
1520
+ }
1521
+ </svg:line>
1522
+ }
1523
+ @case ('circle') {
1524
+ <svg:circle
1525
+ [attr.data-skdx-part]="s.part"
1526
+ [attr.cx]="s.cx"
1527
+ [attr.cy]="s.cy"
1528
+ [attr.r]="s.r"
1529
+ [attr.fill]="s.fill"
1530
+ >
1531
+ @if (s.title) {
1532
+ <svg:title>{{ s.title }}</svg:title>
1533
+ }
1534
+ </svg:circle>
1535
+ }
1536
+ @case ('text') {
1537
+ <svg:text
1538
+ [attr.data-skdx-part]="s.part"
1539
+ [attr.x]="s.x"
1540
+ [attr.y]="s.y"
1541
+ [attr.text-anchor]="s.anchor"
1542
+ [attr.dominant-baseline]="s.baseline"
1543
+ [attr.fill]="s.fill"
1544
+ >
1545
+ {{ s.text }}
1546
+ </svg:text>
1547
+ }
1548
+ }
1549
+ }
1550
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1551
+ }
1552
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SkdxChartShapesComponent, decorators: [{
1553
+ type: Component,
1554
+ args: [{
1555
+ selector: 'g[skdxChartShapes]',
1556
+ standalone: true,
1557
+ changeDetection: ChangeDetectionStrategy.OnPush,
1558
+ template: `
1559
+ @for (s of shapes(); track $index) {
1560
+ @switch (s.kind) {
1561
+ @case ('path') {
1562
+ <svg:path
1563
+ [attr.data-skdx-part]="s.part"
1564
+ [attr.d]="s.d"
1565
+ [attr.fill]="s.fill ?? 'none'"
1566
+ [attr.stroke]="s.stroke"
1567
+ [attr.stroke-width]="s.strokeWidth"
1568
+ [attr.stroke-linecap]="s.strokeLinecap"
1569
+ [attr.stroke-linejoin]="s.strokeLinejoin"
1570
+ [attr.opacity]="s.opacity"
1571
+ [attr.fill-opacity]="s.fillOpacity"
1572
+ >
1573
+ @if (s.title) {
1574
+ <svg:title>{{ s.title }}</svg:title>
1575
+ }
1576
+ </svg:path>
1577
+ }
1578
+ @case ('rect') {
1579
+ <svg:rect
1580
+ [attr.data-skdx-part]="s.part"
1581
+ [attr.x]="s.x"
1582
+ [attr.y]="s.y"
1583
+ [attr.width]="s.width"
1584
+ [attr.height]="s.height"
1585
+ [attr.fill]="s.fill"
1586
+ [attr.stroke]="s.stroke"
1587
+ [attr.fill-opacity]="s.opacity"
1588
+ >
1589
+ @if (s.title) {
1590
+ <svg:title>{{ s.title }}</svg:title>
1591
+ }
1592
+ </svg:rect>
1593
+ }
1594
+ @case ('line') {
1595
+ <svg:line
1596
+ [attr.data-skdx-part]="s.part"
1597
+ [attr.x1]="s.x1"
1598
+ [attr.y1]="s.y1"
1599
+ [attr.x2]="s.x2"
1600
+ [attr.y2]="s.y2"
1601
+ [attr.stroke]="s.stroke"
1602
+ [attr.stroke-dasharray]="s.dash"
1603
+ >
1604
+ @if (s.title) {
1605
+ <svg:title>{{ s.title }}</svg:title>
1606
+ }
1607
+ </svg:line>
1608
+ }
1609
+ @case ('circle') {
1610
+ <svg:circle
1611
+ [attr.data-skdx-part]="s.part"
1612
+ [attr.cx]="s.cx"
1613
+ [attr.cy]="s.cy"
1614
+ [attr.r]="s.r"
1615
+ [attr.fill]="s.fill"
1616
+ >
1617
+ @if (s.title) {
1618
+ <svg:title>{{ s.title }}</svg:title>
1619
+ }
1620
+ </svg:circle>
1621
+ }
1622
+ @case ('text') {
1623
+ <svg:text
1624
+ [attr.data-skdx-part]="s.part"
1625
+ [attr.x]="s.x"
1626
+ [attr.y]="s.y"
1627
+ [attr.text-anchor]="s.anchor"
1628
+ [attr.dominant-baseline]="s.baseline"
1629
+ [attr.fill]="s.fill"
1630
+ >
1631
+ {{ s.text }}
1632
+ </svg:text>
1633
+ }
1634
+ }
1635
+ }
1636
+ `,
1637
+ }]
1638
+ }], propDecorators: { shapes: [{ type: i0.Input, args: [{ isSignal: true, alias: "shapes", required: true }] }] } });
1639
+
1640
+ /*
1641
+ * Copyright (c) 2026 SkandaDX
1642
+ * This file is part of the SkandaDX organization.
1643
+ * Licensed under the MIT License. See LICENSE in the repository root.
1644
+ */
1645
+ /** The accessible `<svg>` root every chart renders into: projected marks, then decorations, then the hover tooltip. */
1646
+ class SkdxChartRootComponent {
1647
+ constructor() {
1648
+ this.layout = input.required(/* @ts-ignore */
1649
+ ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
1650
+ this.kind = input.required(/* @ts-ignore */
1651
+ ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
1652
+ this.label = input.required(/* @ts-ignore */
1653
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1654
+ this.colors = CHART_COLORS;
1655
+ this.tip = signal([], /* @ts-ignore */
1656
+ ...(ngDevMode ? [{ debugName: "tip" }] : /* istanbul ignore next */ []));
1657
+ }
1658
+ onPointerMove(e) {
1659
+ if (!this.layout().tooltip)
1660
+ return;
1661
+ const { width, height } = this.layout();
1662
+ const next = hoverTooltip(e.target, e.currentTarget, e.clientX, e.clientY, width, height);
1663
+ if (next.length > 0 || this.tip().length > 0)
1664
+ this.tip.set(next);
1665
+ }
1666
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SkdxChartRootComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1667
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.4", type: SkdxChartRootComponent, isStandalone: true, selector: "skdx-chart-root", inputs: { layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: true, transformFunction: null }, kind: { classPropertyName: "kind", publicName: "kind", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null } }, host: { styleAttribute: "display: inline-block" }, ngImport: i0, template: `
1668
+ <svg
1669
+ xmlns="http://www.w3.org/2000/svg"
1670
+ [attr.viewBox]="'0 0 ' + layout().width + ' ' + layout().height"
1671
+ [attr.width]="layout().width"
1672
+ [attr.height]="layout().height"
1673
+ role="img"
1674
+ [attr.aria-label]="label()"
1675
+ [attr.data-skdx-chart]="kind()"
1676
+ [attr.font-family]="colors.fontFamily"
1677
+ [attr.font-size]="colors.fontSize"
1678
+ style="max-width: 100%; height: auto"
1679
+ (pointermove)="onPointerMove($event)"
1680
+ (pointerleave)="tip.set([])"
1681
+ >
1682
+ <title>{{ label() }}</title>
1683
+ <svg:g skdxChartShapes [shapes]="layout().shapes"></svg:g>
1684
+ <svg:g skdxChartShapes [shapes]="tip()"></svg:g>
1685
+ </svg>
1686
+ `, isInline: true, dependencies: [{ kind: "component", type: SkdxChartShapesComponent, selector: "g[skdxChartShapes]", inputs: ["shapes"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1687
+ }
1688
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SkdxChartRootComponent, decorators: [{
1689
+ type: Component,
1690
+ args: [{
1691
+ selector: 'skdx-chart-root',
1692
+ standalone: true,
1693
+ changeDetection: ChangeDetectionStrategy.OnPush,
1694
+ host: { style: 'display: inline-block' },
1695
+ imports: [SkdxChartShapesComponent],
1696
+ template: `
1697
+ <svg
1698
+ xmlns="http://www.w3.org/2000/svg"
1699
+ [attr.viewBox]="'0 0 ' + layout().width + ' ' + layout().height"
1700
+ [attr.width]="layout().width"
1701
+ [attr.height]="layout().height"
1702
+ role="img"
1703
+ [attr.aria-label]="label()"
1704
+ [attr.data-skdx-chart]="kind()"
1705
+ [attr.font-family]="colors.fontFamily"
1706
+ [attr.font-size]="colors.fontSize"
1707
+ style="max-width: 100%; height: auto"
1708
+ (pointermove)="onPointerMove($event)"
1709
+ (pointerleave)="tip.set([])"
1710
+ >
1711
+ <title>{{ label() }}</title>
1712
+ <svg:g skdxChartShapes [shapes]="layout().shapes"></svg:g>
1713
+ <svg:g skdxChartShapes [shapes]="tip()"></svg:g>
1714
+ </svg>
1715
+ `,
1716
+ }]
1717
+ }], propDecorators: { layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: true }] }], kind: [{ type: i0.Input, args: [{ isSignal: true, alias: "kind", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }] } });
1718
+ /** The `<svg>` root, grid, axis and tick labels every axis chart shares; marks are projected. */
1719
+ class SkdxSizedBase {
1720
+ constructor() {
1721
+ this.width = input(/* @ts-ignore */
1722
+ ...(ngDevMode ? [undefined, { debugName: "width" }] : /* istanbul ignore next */ []));
1723
+ this.height = input(/* @ts-ignore */
1724
+ ...(ngDevMode ? [undefined, { debugName: "height" }] : /* istanbul ignore next */ []));
1725
+ /** Accessible name of the chart; defaults to "<Kind> chart". */
1726
+ this.label = input(/* @ts-ignore */
1727
+ ...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
1728
+ /** Draw a legend row above the plot. */
1729
+ this.legend = input(false, /* @ts-ignore */
1730
+ ...(ngDevMode ? [{ debugName: "legend" }] : /* istanbul ignore next */ []));
1731
+ /** Show a hover tooltip for the mark under the pointer. */
1732
+ this.tooltip = input(true, /* @ts-ignore */
1733
+ ...(ngDevMode ? [{ debugName: "tooltip" }] : /* istanbul ignore next */ []));
1734
+ }
1735
+ sized() {
1736
+ return {
1737
+ width: this.width(),
1738
+ height: this.height(),
1739
+ legend: this.legend(),
1740
+ tooltip: this.tooltip(),
1741
+ };
1742
+ }
1743
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SkdxSizedBase, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1744
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: SkdxSizedBase, isStandalone: true, inputs: { width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, legend: { classPropertyName: "legend", publicName: "legend", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
1745
+ }
1746
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SkdxSizedBase, decorators: [{
1747
+ type: Directive
1748
+ }], propDecorators: { width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], legend: [{ type: i0.Input, args: [{ isSignal: true, alias: "legend", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }] } });
1749
+ /** Inputs the category charts take. */
1750
+ class SkdxChartBase extends SkdxSizedBase {
1751
+ constructor() {
1752
+ super(...arguments);
1753
+ this.series = input.required(/* @ts-ignore */
1754
+ ...(ngDevMode ? [{ debugName: "series" }] : /* istanbul ignore next */ []));
1755
+ this.categories = input(/* @ts-ignore */
1756
+ ...(ngDevMode ? [undefined, { debugName: "categories" }] : /* istanbul ignore next */ []));
1757
+ }
1758
+ input() {
1759
+ return { ...this.sized(), series: this.series(), categories: this.categories() };
1760
+ }
1761
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SkdxChartBase, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1762
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: SkdxChartBase, isStandalone: true, inputs: { series: { classPropertyName: "series", publicName: "series", isSignal: true, isRequired: true, transformFunction: null }, categories: { classPropertyName: "categories", publicName: "categories", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0 }); }
1763
+ }
1764
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SkdxChartBase, decorators: [{
1765
+ type: Directive
1766
+ }], propDecorators: { series: [{ type: i0.Input, args: [{ isSignal: true, alias: "series", required: true }] }], categories: [{ type: i0.Input, args: [{ isSignal: true, alias: "categories", required: false }] }] } });
1767
+
1768
+ /*
1769
+ * Copyright (c) 2026 SkandaDX
1770
+ * This file is part of the SkandaDX organization.
1771
+ * Licensed under the MIT License. See LICENSE in the repository root.
1772
+ */
1773
+
1774
+ /**
1775
+ * Generated bundle index. Do not edit.
1776
+ */
1777
+
1778
+ export { CHART_COLORS, SkdxChartBase, SkdxChartRootComponent, SkdxChartShapesComponent, SkdxSizedBase, hoverTooltip, layoutAreaChart, layoutBarChart, layoutBoxplot, layoutCandlestick, layoutChord, layoutFunnelChart, layoutGantt, layoutHeatmap, layoutLineChart, layoutMap, layoutPieChart, layoutRadarChart, layoutSankey, layoutScatterChart, layoutSparkline, layoutSunburst, layoutTreemap, layoutWaterfall };
1779
+ //# sourceMappingURL=skdx-angular-charts-internal.mjs.map