yukit-web 0.5.1 → 0.6.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.
@@ -0,0 +1,650 @@
1
+ import { _IdGenerator } from '@angular/cdk/a11y';
2
+ import * as i0 from '@angular/core';
3
+ import { signal, inject, DestroyRef, afterNextRender, viewChild, input, booleanAttribute, computed, linkedSignal, ChangeDetectionStrategy, Component } from '@angular/core';
4
+ import { HlmSkeleton } from 'yukit-web/helm/skeleton';
5
+ import { hlm } from 'yukit-web/helm/utils';
6
+ import { area, curveMonotoneX, curveMonotoneY, stack, stackOffsetNone, stackOffsetDiverging, line, pie, arc } from 'd3-shape';
7
+ import { scaleBand, scalePoint, scaleLinear } from 'd3-scale';
8
+
9
+ /** Every chart token, in palette order. */
10
+ const YUW_CHART_COLORS = [
11
+ 'chart-1',
12
+ 'chart-2',
13
+ 'chart-3',
14
+ 'chart-4',
15
+ 'chart-5',
16
+ 'chart-6',
17
+ 'chart-7',
18
+ 'chart-8',
19
+ ];
20
+ /** Color token of the series at `index`, cycling through the palette: builds a config for a dynamic set of series. */
21
+ function yuwChartColorAt(index) {
22
+ const count = YUW_CHART_COLORS.length;
23
+ return YUW_CHART_COLORS[((index % count) + count) % count];
24
+ }
25
+
26
+ /** Average advance of a glyph at the `text-xs` size axis labels use, to reserve room without measuring text. */
27
+ const CHART_CHAR_WIDTH = 7;
28
+ function cssColor(color) {
29
+ return `var(--${color})`;
30
+ }
31
+ function seriesOf(config) {
32
+ return Object.entries(config).map(([key, series]) => ({ key, label: series.label, color: cssColor(series.color) }));
33
+ }
34
+ /** A finite number, or `null` for anything else: a missing value is a gap, never a zero. */
35
+ function valueOf(row, key) {
36
+ const value = row[key];
37
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
38
+ }
39
+ function textWidth(labels) {
40
+ return labels.reduce((widest, label) => Math.max(widest, label.length), 0) * CHART_CHAR_WIDTH;
41
+ }
42
+ function rectPath(x, y, width, height, radius = 0) {
43
+ const r = Math.max(0, Math.min(radius, width / 2, height / 2));
44
+ return (`M${x + r},${y}h${width - 2 * r}a${r},${r} 0 0 1 ${r},${r}v${height - 2 * r}` +
45
+ `a${r},${r} 0 0 1 ${-r},${r}h${2 * r - width}a${r},${r} 0 0 1 ${-r},${-r}` +
46
+ `v${2 * r - height}a${r},${r} 0 0 1 ${r},${-r}Z`);
47
+ }
48
+ function emptyScene(table) {
49
+ return { cursorPaint: 'band', empty: true, grid: [], hits: [], marks: [], origin: { x: 0, y: 0 }, table, ticks: [] };
50
+ }
51
+
52
+ const LABEL_GAP = 8;
53
+ const AXIS_ROOM = 24;
54
+ const LINE_HEIGHT = 16;
55
+ const MAX_VALUE_TICKS = 5;
56
+ function tickCount(length, room) {
57
+ return Math.max(2, Math.min(MAX_VALUE_TICKS, Math.floor(length / room)));
58
+ }
59
+ const VERTICAL = {
60
+ curve: curveMonotoneX,
61
+ anchor: (plot, category) => ({ x: category, y: (plot.top + plot.bottom) / 2 }),
62
+ area: (spans) => area()
63
+ .x((span) => span.category)
64
+ .y0((span) => span.base)
65
+ .y1((span) => span.top)
66
+ .curve(curveMonotoneX)(spans) ?? '',
67
+ band: (plot, start, end) => rectPath(start, plot.top, end - start, plot.bottom - plot.top),
68
+ bar: (category, size, base, top, radius) => rectPath(category, Math.min(base, top), size, Math.abs(base - top), radius),
69
+ categoryLabelChars: () => Number.POSITIVE_INFINITY,
70
+ categoryLabelRoom: (categoryLabelWidth) => categoryLabelWidth + LABEL_GAP,
71
+ categoryRange: (plot) => [plot.left, plot.right],
72
+ categoryTick: (plot, category, label) => ({ anchor: 'middle', label, x: category, y: plot.bottom + AXIS_ROOM - 6 }),
73
+ cursor: (plot, category) => `M${category},${plot.top}V${plot.bottom}`,
74
+ grid: (plot, value) => `M${plot.left},${value}H${plot.right}`,
75
+ margins: (_width, valueLabelWidth) => ({
76
+ top: LABEL_GAP,
77
+ right: LABEL_GAP,
78
+ bottom: AXIS_ROOM,
79
+ left: valueLabelWidth + LABEL_GAP,
80
+ }),
81
+ point: (category, value) => [category, value],
82
+ valueRange: (plot) => [plot.bottom, plot.top],
83
+ valueTick: (plot, value, label) => ({ anchor: 'end', label, x: plot.left - LABEL_GAP, y: value }),
84
+ valueTickCount: (plot) => tickCount(plot.bottom - plot.top, 2 * LINE_HEIGHT),
85
+ };
86
+ const HORIZONTAL = {
87
+ curve: curveMonotoneY,
88
+ anchor: (plot, category) => ({ x: plot.right, y: category }),
89
+ area: (spans) => area()
90
+ .y((span) => span.category)
91
+ .x0((span) => span.base)
92
+ .x1((span) => span.top)
93
+ .curve(curveMonotoneY)(spans) ?? '',
94
+ band: (plot, start, end) => rectPath(plot.left, start, plot.right - plot.left, end - start),
95
+ bar: (category, size, base, top, radius) => rectPath(Math.min(base, top), category, Math.abs(base - top), size, radius),
96
+ categoryLabelChars: (margins) => Math.max(1, Math.floor((margins.left - LABEL_GAP) / CHART_CHAR_WIDTH)),
97
+ categoryLabelRoom: () => LINE_HEIGHT,
98
+ categoryRange: (plot) => [plot.top, plot.bottom],
99
+ categoryTick: (plot, category, label) => ({ anchor: 'end', label, x: plot.left - LABEL_GAP, y: category }),
100
+ cursor: (plot, category) => `M${plot.left},${category}H${plot.right}`,
101
+ grid: (plot, value) => `M${value},${plot.top}V${plot.bottom}`,
102
+ margins: (width, valueLabelWidth, categoryLabelWidth) => ({
103
+ top: LABEL_GAP,
104
+ right: Math.max(LABEL_GAP, valueLabelWidth / 2),
105
+ bottom: AXIS_ROOM,
106
+ left: Math.min(categoryLabelWidth, width * 0.4) + LABEL_GAP,
107
+ }),
108
+ point: (category, value) => [value, category],
109
+ valueRange: (plot) => [plot.left, plot.right],
110
+ valueTick: (plot, value, label) => ({ anchor: 'middle', label, x: value, y: plot.bottom + AXIS_ROOM - 6 }),
111
+ valueTickCount: (plot, valueLabelWidth) => tickCount(plot.right - plot.left, valueLabelWidth + 2 * LABEL_GAP),
112
+ };
113
+
114
+ function extentWithZero(values) {
115
+ const ends = values.flat().flatMap((value) => (value ? [value.base, value.top] : []));
116
+ const min = Math.min(0, ...ends);
117
+ const max = Math.max(0, ...ends);
118
+ return [min, max === min ? min + 1 : max];
119
+ }
120
+ /** Every series starts from zero. */
121
+ const sideBySide = (rows, keys) => {
122
+ const columns = keys.map((key) => rows.map((row) => {
123
+ const value = valueOf(row, key);
124
+ return value === null ? null : { base: 0, top: value, value };
125
+ }));
126
+ return { domain: extentWithZero(columns), byKey: new Map(keys.map((key, index) => [key, columns[index]])) };
127
+ };
128
+ /** Each series starts where the previous one ended, following `offset`. */
129
+ function stackedBy(offset) {
130
+ return (rows, keys) => {
131
+ const layers = stack()
132
+ .keys(keys)
133
+ .value((row, key) => valueOf(row, key) ?? 0)
134
+ .offset(offset)(rows);
135
+ const columns = layers.map((layer) => layer.map(([base, top], index) => ({ base, top, value: valueOf(rows[index], layer.key) })));
136
+ return { domain: extentWithZero(columns), byKey: new Map(keys.map((key, index) => [key, columns[index]])) };
137
+ };
138
+ }
139
+ /** A continuous stack, so an area keeps its height where a series holds nothing. */
140
+ const stacked = stackedBy(stackOffsetNone);
141
+ /** Positive values stack upwards and negative ones downwards from zero, as bars need. */
142
+ const stackedDiverging = stackedBy(stackOffsetDiverging);
143
+
144
+ const VALUE_TICKS = 5;
145
+ const BAR_RADIUS = 4;
146
+ /** What highlights the active category: its whole band behind bars, a rule across lines and areas. */
147
+ const CURSORS = {
148
+ band: (orientation, plot, start, end) => orientation.band(plot, start, end),
149
+ line: (orientation, plot, _start, _end, center) => orientation.cursor(plot, center),
150
+ };
151
+ /** Categories as bands, so bars have a width. */
152
+ const bands = (count, range) => {
153
+ const scale = scaleBand().domain(indices(count)).range(range).paddingInner(0.2).paddingOuter(0.1);
154
+ const starts = indices(count).map((index) => scale(index) ?? 0);
155
+ return { starts, size: scale.bandwidth(), centers: starts.map((start) => start + scale.bandwidth() / 2) };
156
+ };
157
+ /** Categories as points spanning the whole axis, so lines reach both edges. */
158
+ const points = (count, range) => {
159
+ const scale = scalePoint().domain(indices(count)).range(range);
160
+ const centers = indices(count).map((index) => scale(index) ?? 0);
161
+ return { starts: centers, size: 0, centers };
162
+ };
163
+ /** Bars of every series side by side inside the category band. */
164
+ const groupedBars = {
165
+ cursor: 'band',
166
+ points: false,
167
+ marks: ({ categories, orientation, scale, series, values }) => {
168
+ const inner = scaleBand()
169
+ .domain(series.map((item) => item.key))
170
+ .range([0, categories.size])
171
+ .padding(0.1);
172
+ return series.flatMap((item) => barsOf(values, item).map(({ index, value }) => ({
173
+ color: item.color,
174
+ d: orientation.bar(categories.starts[index] + (inner(item.key) ?? 0), inner.bandwidth(), scale(value.base), scale(value.top), BAR_RADIUS),
175
+ hit: index,
176
+ key: item.key,
177
+ paint: 'fill',
178
+ })));
179
+ },
180
+ };
181
+ /** One bar per category, each series a segment of it. */
182
+ const stackedBars = {
183
+ cursor: 'band',
184
+ points: false,
185
+ marks: ({ categories, orientation, scale, series, values }) => series.flatMap((item) => barsOf(values, item).map(({ index, value }) => ({
186
+ color: item.color,
187
+ d: orientation.bar(categories.starts[index], categories.size, scale(value.base), scale(value.top), 0),
188
+ hit: index,
189
+ key: item.key,
190
+ paint: 'fill',
191
+ }))),
192
+ };
193
+ const lines = {
194
+ cursor: 'line',
195
+ points: true,
196
+ marks: (frame) => frame.series.map((item) => strokeOf(frame, item)),
197
+ };
198
+ /** A translucent band under each series, outlined by its line. */
199
+ const areas = {
200
+ cursor: 'line',
201
+ points: true,
202
+ marks: (frame) => frame.series.flatMap((item) => [
203
+ {
204
+ color: item.color,
205
+ d: frame.orientation.area(spansOf(frame, item)),
206
+ hit: null,
207
+ key: item.key,
208
+ paint: 'area',
209
+ },
210
+ strokeOf(frame, item),
211
+ ]),
212
+ };
213
+ /** A bar, line or area chart: the categories along one axis, the values along the other. */
214
+ function cartesianLayout(options) {
215
+ return (input) => {
216
+ const series = seriesOf(input.config);
217
+ const table = tableOf$1(input, series);
218
+ if (input.data.length === 0 || series.length === 0) {
219
+ return emptyScene(table);
220
+ }
221
+ return sceneOf(options, input, series, table);
222
+ };
223
+ }
224
+ function sceneOf(options, input, series, table) {
225
+ const { orientation } = options;
226
+ const values = options.values(input.data, series.map((item) => item.key));
227
+ const value = scaleLinear().domain(values.domain).nice();
228
+ const labelWidth = textWidth(value.ticks(VALUE_TICKS).map((tick) => input.valueFormat(tick)));
229
+ const titles = input.data.map((row) => titleOf(input, row));
230
+ const margins = orientation.margins(input.width, labelWidth, textWidth(titles));
231
+ const plot = {
232
+ top: margins.top,
233
+ left: margins.left,
234
+ right: Math.max(margins.left + 1, input.width - margins.right),
235
+ bottom: Math.max(margins.top + 1, input.height - margins.bottom),
236
+ };
237
+ const tickCount = orientation.valueTickCount(plot, labelWidth);
238
+ const candidates = value.ticks(tickCount);
239
+ const tickValues = candidates.filter((_tick, index) => index % Math.ceil(candidates.length / tickCount) === 0);
240
+ const valueLabels = tickValues.map((tick) => input.valueFormat(tick));
241
+ const scale = value.range(orientation.valueRange(plot));
242
+ const range = orientation.categoryRange(plot);
243
+ const categories = options.categories(input.data.length, range);
244
+ const frame = { categories, orientation, scale, series, values };
245
+ const boundaries = boundariesOf(categories.centers, range);
246
+ return {
247
+ cursorPaint: options.marks.cursor,
248
+ empty: false,
249
+ grid: tickValues.map((tick) => orientation.grid(plot, scale(tick))),
250
+ hits: categories.centers.map((center, index) => ({
251
+ anchor: orientation.anchor(plot, center),
252
+ cursor: CURSORS[options.marks.cursor](orientation, plot, boundaries[index], boundaries[index + 1], center),
253
+ d: orientation.band(plot, boundaries[index], boundaries[index + 1]),
254
+ points: options.marks.points ? pointsOf(frame, index) : [],
255
+ tooltip: tooltipOf(input, series, values, index, titles[index]),
256
+ })),
257
+ marks: options.marks.marks(frame),
258
+ origin: { x: 0, y: 0 },
259
+ table,
260
+ ticks: [
261
+ ...tickValues.map((tick, index) => orientation.valueTick(plot, scale(tick), valueLabels[index])),
262
+ ...categoryTicksOf(orientation, plot, margins, categories.centers, range, titles),
263
+ ],
264
+ };
265
+ }
266
+ function categoryTicksOf(orientation, plot, margins, centers, range, titles) {
267
+ const step = Math.abs(range[1] - range[0]) / Math.max(1, centers.length);
268
+ const every = Math.max(1, Math.ceil(orientation.categoryLabelRoom(textWidth(titles)) / Math.max(step, 1)));
269
+ const chars = orientation.categoryLabelChars(margins);
270
+ return centers
271
+ .map((center, index) => orientation.categoryTick(plot, center, truncate(titles[index], chars)))
272
+ .filter((_tick, index) => index % every === 0);
273
+ }
274
+ function pointsOf({ categories, orientation, scale, series, values }, index) {
275
+ return series.flatMap((item) => {
276
+ const value = values.byKey.get(item.key)?.[index];
277
+ if (!value || value.value === null) {
278
+ return [];
279
+ }
280
+ const [cx, cy] = orientation.point(categories.centers[index], scale(value.top));
281
+ return [{ color: item.color, cx, cy, key: item.key }];
282
+ });
283
+ }
284
+ function strokeOf({ categories, orientation, scale, values }, item) {
285
+ const coordinates = (values.byKey.get(item.key) ?? []).map((value, index) => value ? orientation.point(categories.centers[index], scale(value.top)) : null);
286
+ const path = line()
287
+ .defined((point) => point !== null)
288
+ .x((point) => point?.[0] ?? 0)
289
+ .y((point) => point?.[1] ?? 0)
290
+ .curve(orientation.curve);
291
+ return { color: item.color, d: path(coordinates) ?? '', hit: null, key: item.key, paint: 'stroke' };
292
+ }
293
+ function spansOf({ categories, scale, values }, item) {
294
+ return (values.byKey.get(item.key) ?? []).flatMap((value, index) => value ? [{ base: scale(value.base), category: categories.centers[index], top: scale(value.top) }] : []);
295
+ }
296
+ function barsOf(values, item) {
297
+ return (values.byKey.get(item.key) ?? []).flatMap((value, index) => value && value.value !== null && value.base !== value.top ? [{ index, value }] : []);
298
+ }
299
+ function tooltipOf(input, series, values, index, title) {
300
+ return {
301
+ title: input.categoryKey === undefined ? null : title,
302
+ items: series.flatMap((item) => {
303
+ const value = values.byKey.get(item.key)?.[index]?.value ?? null;
304
+ return value === null
305
+ ? []
306
+ : [{ color: item.color, key: item.key, label: item.label, value: input.valueFormat(value) }];
307
+ }),
308
+ };
309
+ }
310
+ function tableOf$1(input, series) {
311
+ return {
312
+ corner: input.categoryLabel ?? '',
313
+ head: series.map((item) => item.label),
314
+ rows: input.data.map((row) => ({
315
+ header: titleOf(input, row),
316
+ cells: series.map((item) => {
317
+ const value = valueOf(row, item.key);
318
+ return value === null ? '' : input.valueFormat(value);
319
+ }),
320
+ })),
321
+ };
322
+ }
323
+ function titleOf(input, row) {
324
+ return input.categoryKey === undefined ? '' : input.categoryFormat(row[input.categoryKey]);
325
+ }
326
+ /** Edges between consecutive categories, so every pixel of the axis activates the nearest one. */
327
+ function boundariesOf(centers, range) {
328
+ const middles = centers.slice(1).map((center, index) => (centers[index] + center) / 2);
329
+ return [range[0], ...middles, range[1]];
330
+ }
331
+ function truncate(label, chars) {
332
+ return label.length > chars ? `${label.slice(0, Math.max(1, chars - 1))}…` : label;
333
+ }
334
+ function indices(count) {
335
+ return Array.from({ length: count }, (_, index) => index);
336
+ }
337
+
338
+ const EDGE = 4;
339
+ /** One slice per series, sized by its value summed across every row. */
340
+ function pieLayout(options) {
341
+ return (input) => {
342
+ const slices = slicesOf(input);
343
+ const table = tableOf(input, slices);
344
+ const shown = slices.filter((slice) => slice.total > 0);
345
+ if (shown.length === 0) {
346
+ return emptyScene(table);
347
+ }
348
+ const radius = Math.max(1, Math.min(input.width, input.height) / 2 - EDGE);
349
+ const centerX = input.width / 2;
350
+ const centerY = input.height / 2;
351
+ const arcs = pie()
352
+ .sort(null)
353
+ .value((slice) => slice.total)(shown);
354
+ const shape = arc()
355
+ .innerRadius(radius * options.hole)
356
+ .outerRadius(radius)
357
+ .padAngle(options.hole > 0 ? 0.01 : 0)
358
+ .cornerRadius(options.hole > 0 ? 3 : 0);
359
+ const paths = arcs.map((datum) => shape(datum) ?? '');
360
+ return {
361
+ cursorPaint: 'band',
362
+ empty: false,
363
+ grid: [],
364
+ origin: { x: centerX, y: centerY },
365
+ hits: arcs.map((datum, index) => {
366
+ const [x, y] = shape.centroid(datum);
367
+ return {
368
+ anchor: { x: centerX + x, y: centerY + y },
369
+ cursor: null,
370
+ d: paths[index],
371
+ points: [],
372
+ tooltip: {
373
+ title: null,
374
+ items: [
375
+ {
376
+ color: datum.data.color,
377
+ key: datum.data.key,
378
+ label: datum.data.label,
379
+ value: input.valueFormat(datum.data.total),
380
+ },
381
+ ],
382
+ },
383
+ };
384
+ }),
385
+ marks: arcs.map((datum, index) => ({
386
+ color: datum.data.color,
387
+ d: paths[index],
388
+ hit: index,
389
+ key: datum.data.key,
390
+ paint: 'fill',
391
+ })),
392
+ table,
393
+ ticks: [],
394
+ };
395
+ };
396
+ }
397
+ function slicesOf(input) {
398
+ return seriesOf(input.config).map((series) => ({
399
+ ...series,
400
+ total: input.data.reduce((sum, row) => sum + Math.max(0, valueOf(row, series.key) ?? 0), 0),
401
+ }));
402
+ }
403
+ function tableOf(input, slices) {
404
+ return {
405
+ corner: null,
406
+ head: slices.map((slice) => slice.label),
407
+ rows: input.data.length === 0 ? [] : [{ header: null, cells: slices.map((slice) => input.valueFormat(slice.total)) }],
408
+ };
409
+ }
410
+
411
+ /** The layout drawing each chart kind. A new kind is one entry here, built from the same parts. */
412
+ const YUW_CHART_LAYOUTS = {
413
+ area: cartesianLayout({ categories: points, marks: areas, orientation: VERTICAL, values: sideBySide }),
414
+ 'area-stacked': cartesianLayout({ categories: points, marks: areas, orientation: VERTICAL, values: stacked }),
415
+ bar: cartesianLayout({ categories: bands, marks: groupedBars, orientation: VERTICAL, values: sideBySide }),
416
+ 'bar-horizontal': cartesianLayout({
417
+ categories: bands,
418
+ marks: groupedBars,
419
+ orientation: HORIZONTAL,
420
+ values: sideBySide,
421
+ }),
422
+ 'bar-horizontal-stacked': cartesianLayout({
423
+ categories: bands,
424
+ marks: stackedBars,
425
+ orientation: HORIZONTAL,
426
+ values: stackedDiverging,
427
+ }),
428
+ 'bar-stacked': cartesianLayout({
429
+ categories: bands,
430
+ marks: stackedBars,
431
+ orientation: VERTICAL,
432
+ values: stackedDiverging,
433
+ }),
434
+ donut: pieLayout({ hole: 0.6 }),
435
+ line: cartesianLayout({ categories: points, marks: lines, orientation: VERTICAL, values: sideBySide }),
436
+ pie: pieLayout({ hole: 0 }),
437
+ };
438
+
439
+ const UNMEASURED = { height: 0, width: 0 };
440
+ /**
441
+ * Tracks the content box of `element` as a signal. Stays at zero on the server and wherever `ResizeObserver` is
442
+ * missing, so nothing is drawn until there is a real box to draw into. Must be called in an injection context.
443
+ */
444
+ function injectChartSize(element) {
445
+ const size = signal(UNMEASURED, { ...(ngDevMode ? { debugName: "size" } : /* istanbul ignore next */ {}), equal: (a, b) => a.width === b.width && a.height === b.height });
446
+ const destroyRef = inject(DestroyRef);
447
+ afterNextRender(() => {
448
+ const target = element()?.nativeElement;
449
+ if (!target || typeof ResizeObserver === 'undefined') {
450
+ return;
451
+ }
452
+ const observer = new ResizeObserver(([entry]) => size.set({ width: Math.floor(entry.contentRect.width), height: Math.floor(entry.contentRect.height) }));
453
+ observer.observe(target);
454
+ destroyRef.onDestroy(() => observer.disconnect());
455
+ });
456
+ return size.asReadonly();
457
+ }
458
+
459
+ /** Classes of each kind of mark; the color arrives separately, as a `var(--chart-n)` style. */
460
+ const MARK_CLASS = {
461
+ area: 'opacity-30',
462
+ fill: 'stroke-background stroke-1 motion-safe:transition-opacity motion-safe:duration-150',
463
+ stroke: 'fill-none stroke-2',
464
+ };
465
+ /** Which style property carries the color of each kind of mark. */
466
+ const MARK_COLOR_PROPERTY = {
467
+ area: 'fill',
468
+ fill: 'fill',
469
+ stroke: 'stroke',
470
+ };
471
+ const MARK_DIMMED_CLASS = 'opacity-40';
472
+ const CURSOR_CLASS = {
473
+ band: 'fill-muted',
474
+ line: 'fill-none stroke-border stroke-1',
475
+ };
476
+ /** Which side of its anchor the tooltip opens to, so it stays inside the plot. */
477
+ const TOOLTIP_SIDE_CLASS = {
478
+ left: '-translate-x-[calc(100%+0.75rem)]',
479
+ right: 'translate-x-3',
480
+ };
481
+ /** What each key does to the active position, given it and how many there are; `null` hides the tooltip. */
482
+ const KEY_MOVES = {
483
+ ArrowDown: (active, count) => Math.min(active + 1, count - 1),
484
+ ArrowLeft: (active) => Math.max(active - 1, 0),
485
+ ArrowRight: (active, count) => Math.min(active + 1, count - 1),
486
+ ArrowUp: (active) => Math.max(active - 1, 0),
487
+ End: (_active, count) => count - 1,
488
+ Escape: () => null,
489
+ Home: () => 0,
490
+ };
491
+
492
+ /**
493
+ * Chart drawn as SVG from a series `config` and `data` rows, with a tooltip and a legend that read the same config.
494
+ *
495
+ * Every series is a key of `config` naming the row field that holds its values, plus its label (already translated)
496
+ * and a `chart-n` color token, so light and dark mode recolor through the theme with no redraw. `kind` picks the
497
+ * drawing: bars (grouped, stacked, horizontal), lines, areas, or a pie / donut with one slice per series summed across
498
+ * the rows. The chart fills its root element and redraws when it resizes; size it with `class` (default `h-72`).
499
+ *
500
+ * The pointer and the keyboard (focus the plot, then the arrow keys, Home, End and Escape) move a tooltip across the
501
+ * categories or slices, announced politely to screen readers. The same numbers are always available to assistive
502
+ * technology as a visually hidden table, which also renders on the server, where the plot is not drawn.
503
+ *
504
+ * The library hardcodes no locale: `valueFormat` and `categoryFormat` turn values and categories into text.
505
+ *
506
+ * Inputs:
507
+ * - `ariaLabel`: Required accessible name of the chart, e.g. its title. The app supplies and translates it.
508
+ * - `categoryFormat`: Turns the value under `categoryKey` into its label. Cartesian kinds only. Default is `String`.
509
+ * - `categoryKey`: Row field holding each category, e.g. the period. Cartesian kinds only. Default is `undefined`.
510
+ * - `categoryLabel`: Header of the category column in the accessible table, e.g. "Month". Cartesian kinds only.
511
+ * Default is `undefined`.
512
+ * - `config`: Required series, keyed by the row field holding their values (`YuwChartConfigDto`). Its order is the
513
+ * drawing, stacking, legend and tooltip order.
514
+ * - `data`: Required rows (`YuwChartRowDto[]`). A non-numeric value is a gap, never a zero.
515
+ * - `description`: Accessible description, e.g. the period the chart covers. Default is `undefined`.
516
+ * - `emptyText`: Required message shown when there is nothing to draw. The app supplies and translates it.
517
+ * - `kind`: The drawing (`YuwChartKindDto`). Default is `'bar'`.
518
+ * - `legend`: Shows the legend under the plot. Default is `true`.
519
+ * - `loading`: Shows a skeleton instead of the plot and marks the chart busy. Default is `false`.
520
+ * - `valueFormat`: Turns a value into the text of axes, tooltips and the accessible table, e.g. a localized currency.
521
+ * Default is `String`.
522
+ * - `class`: Extra classes merged last onto the root element; set its height here.
523
+ *
524
+ * Usage:
525
+ * ```html
526
+ * <yuw-chart
527
+ * kind="bar-stacked"
528
+ * ariaLabel="Purchases per month"
529
+ * emptyText="No purchases in this period"
530
+ * categoryKey="month"
531
+ * categoryLabel="Month"
532
+ * [config]="config"
533
+ * [data]="rows"
534
+ * [valueFormat]="formatEuros"
535
+ * class="h-80"
536
+ * />
537
+ * ```
538
+ */
539
+ class YuwChartComponent {
540
+ constructor() {
541
+ this._id = inject(_IdGenerator).getId('yuw-chart-');
542
+ this._plot = viewChild('plot', ...(ngDevMode ? [{ debugName: "_plot" }] : /* istanbul ignore next */ []));
543
+ this.ariaLabel = input.required(...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
544
+ this.categoryFormat = input(String, ...(ngDevMode ? [{ debugName: "categoryFormat" }] : /* istanbul ignore next */ []));
545
+ this.categoryKey = input(...(ngDevMode ? [undefined, { debugName: "categoryKey" }] : /* istanbul ignore next */ []));
546
+ this.categoryLabel = input(...(ngDevMode ? [undefined, { debugName: "categoryLabel" }] : /* istanbul ignore next */ []));
547
+ this.config = input.required(...(ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []));
548
+ this.data = input.required(...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
549
+ this.description = input(...(ngDevMode ? [undefined, { debugName: "description" }] : /* istanbul ignore next */ []));
550
+ this.emptyText = input.required(...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
551
+ this.kind = input('bar', ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
552
+ this.legend = input(true, { ...(ngDevMode ? { debugName: "legend" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
553
+ this.loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
554
+ this.userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
555
+ this.valueFormat = input(String, ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
556
+ this.descriptionId = `${this._id}-description`;
557
+ this.size = injectChartSize(this._plot);
558
+ this.scene = computed(() => YUW_CHART_LAYOUTS[this.kind()]({
559
+ categoryFormat: this.categoryFormat(),
560
+ categoryKey: this.categoryKey(),
561
+ categoryLabel: this.categoryLabel(),
562
+ config: this.config(),
563
+ data: this.data(),
564
+ height: this.size().height,
565
+ valueFormat: this.valueFormat(),
566
+ width: this.size().width,
567
+ }), ...(ngDevMode ? [{ debugName: "scene" }] : /* istanbul ignore next */ []));
568
+ this.active = linkedSignal({ ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), source: this.scene, computation: () => null });
569
+ this.activeHit = computed(() => {
570
+ const active = this.active();
571
+ return active === null ? null : (this.scene().hits[active] ?? null);
572
+ }, ...(ngDevMode ? [{ debugName: "activeHit" }] : /* istanbul ignore next */ []));
573
+ this.activePoints = computed(() => this.activeHit()?.points ?? [], ...(ngDevMode ? [{ debugName: "activePoints" }] : /* istanbul ignore next */ []));
574
+ this.announcement = computed(() => {
575
+ const tooltip = this.activeHit()?.tooltip;
576
+ if (!tooltip) {
577
+ return '';
578
+ }
579
+ return [tooltip.title, ...tooltip.items.map((item) => `${item.label}: ${item.value}`)]
580
+ .filter((part) => !!part)
581
+ .join(', ');
582
+ }, ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
583
+ this.cursorClass = computed(() => CURSOR_CLASS[this.scene().cursorPaint], ...(ngDevMode ? [{ debugName: "cursorClass" }] : /* istanbul ignore next */ []));
584
+ this.describedBy = computed(() => (this.description() ? this.descriptionId : null), ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
585
+ this.drawable = computed(() => this.size().width > 0 && this.size().height > 0, ...(ngDevMode ? [{ debugName: "drawable" }] : /* istanbul ignore next */ []));
586
+ this.interactive = computed(() => !this.loading() && this.scene().hits.length > 0, ...(ngDevMode ? [{ debugName: "interactive" }] : /* istanbul ignore next */ []));
587
+ this.legendItems = computed(() => Object.entries(this.config()).map(([key, series]) => ({ key, label: series.label, color: cssColor(series.color) })), ...(ngDevMode ? [{ debugName: "legendItems" }] : /* istanbul ignore next */ []));
588
+ this.marks = computed(() => {
589
+ const active = this.active();
590
+ return this.scene().marks.map((mark) => ({
591
+ d: mark.d,
592
+ key: mark.key,
593
+ class: hlm(MARK_CLASS[mark.paint], active !== null && mark.hit !== null && mark.hit !== active && MARK_DIMMED_CLASS),
594
+ style: { [MARK_COLOR_PROPERTY[mark.paint]]: mark.color },
595
+ }));
596
+ }, ...(ngDevMode ? [{ debugName: "marks" }] : /* istanbul ignore next */ []));
597
+ this.origin = computed(() => `translate(${this.scene().origin.x},${this.scene().origin.y})`, ...(ngDevMode ? [{ debugName: "origin" }] : /* istanbul ignore next */ []));
598
+ this.rootClass = computed(() => hlm('flex h-72 w-full flex-col gap-3', this.userClass()), ...(ngDevMode ? [{ debugName: "rootClass" }] : /* istanbul ignore next */ []));
599
+ this.showLegend = computed(() => this.legend() && !this.loading() && !this.scene().empty, ...(ngDevMode ? [{ debugName: "showLegend" }] : /* istanbul ignore next */ []));
600
+ this.tooltip = computed(() => {
601
+ const hit = this.activeHit();
602
+ if (!hit || hit.tooltip.items.length === 0) {
603
+ return null;
604
+ }
605
+ return {
606
+ ...hit.tooltip,
607
+ left: hit.anchor.x,
608
+ top: hit.anchor.y,
609
+ class: TOOLTIP_SIDE_CLASS[hit.anchor.x > this.size().width / 2 ? 'left' : 'right'],
610
+ };
611
+ }, ...(ngDevMode ? [{ debugName: "tooltip" }] : /* istanbul ignore next */ []));
612
+ this.viewBox = computed(() => `0 0 ${this.size().width} ${this.size().height}`, ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
613
+ }
614
+ activate(index) {
615
+ this.active.set(index);
616
+ }
617
+ clear() {
618
+ this.active.set(null);
619
+ }
620
+ onFocus(event) {
621
+ const target = event.target;
622
+ if (this.active() === null && target.matches(':focus-visible')) {
623
+ this.active.set(0);
624
+ }
625
+ }
626
+ onKeydown(event) {
627
+ const move = KEY_MOVES[event.key];
628
+ const count = this.scene().hits.length;
629
+ if (!move || count === 0) {
630
+ return;
631
+ }
632
+ event.preventDefault();
633
+ this.active.set(move(this.active() ?? -1, count));
634
+ }
635
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: YuwChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
636
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: YuwChartComponent, isStandalone: true, selector: "yuw-chart", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: true, transformFunction: null }, categoryFormat: { classPropertyName: "categoryFormat", publicName: "categoryFormat", isSignal: true, isRequired: false, transformFunction: null }, categoryKey: { classPropertyName: "categoryKey", publicName: "categoryKey", isSignal: true, isRequired: false, transformFunction: null }, categoryLabel: { classPropertyName: "categoryLabel", publicName: "categoryLabel", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: true, transformFunction: null }, kind: { classPropertyName: "kind", publicName: "kind", isSignal: true, isRequired: false, transformFunction: null }, legend: { classPropertyName: "legend", publicName: "legend", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, viewQueries: [{ propertyName: "_plot", first: true, predicate: ["plot"], descendants: true, isSignal: true }], ngImport: i0, template: "<div role=\"figure\" [class]=\"rootClass()\" [attr.aria-label]=\"ariaLabel()\" [attr.aria-busy]=\"loading() || null\">\n @if (description(); as text) {\n <p class=\"sr-only\" [id]=\"descriptionId\">{{ text }}</p>\n }\n\n <!-- Plot -->\n <div\n #plot\n role=\"group\"\n class=\"relative min-h-0 flex-1 rounded-md outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"interactive() ? 0 : null\"\n (focus)=\"onFocus($event)\"\n (blur)=\"clear()\"\n (keydown)=\"onKeydown($event)\"\n (pointerleave)=\"clear()\"\n >\n @if (loading()) {\n <div hlmSkeleton class=\"size-full\" aria-hidden=\"true\"></div>\n } @else if (scene().empty) {\n <div class=\"flex size-full items-center justify-center text-center text-sm text-muted-foreground\">\n {{ emptyText() }}\n </div>\n } @else if (drawable()) {\n <svg\n aria-hidden=\"true\"\n class=\"absolute inset-0 overflow-visible\"\n [attr.width]=\"size().width\"\n [attr.height]=\"size().height\"\n [attr.viewBox]=\"viewBox()\"\n >\n @for (line of scene().grid; track $index) {\n <path class=\"fill-none stroke-border [stroke-dasharray:3_3]\" [attr.d]=\"line\" />\n }\n\n @if (activeHit()?.cursor; as cursor) {\n <path [class]=\"cursorClass()\" [attr.d]=\"cursor\" />\n }\n\n <g [attr.transform]=\"origin()\">\n @for (mark of marks(); track $index) {\n <path [class]=\"mark.class\" [style]=\"mark.style\" [attr.d]=\"mark.d\" />\n }\n </g>\n\n @for (point of activePoints(); track point.key) {\n <circle\n r=\"4\"\n class=\"stroke-background stroke-2\"\n [style.fill]=\"point.color\"\n [attr.cx]=\"point.cx\"\n [attr.cy]=\"point.cy\"\n />\n }\n\n @for (tick of scene().ticks; track $index) {\n <text\n dominant-baseline=\"middle\"\n class=\"fill-muted-foreground text-xs\"\n [attr.x]=\"tick.x\"\n [attr.y]=\"tick.y\"\n [attr.text-anchor]=\"tick.anchor\"\n >\n {{ tick.label }}\n </text>\n }\n\n <g [attr.transform]=\"origin()\">\n @for (hit of scene().hits; track $index) {\n <path class=\"fill-transparent\" [attr.d]=\"hit.d\" (pointerenter)=\"activate($index)\" />\n }\n </g>\n </svg>\n\n @if (tooltip(); as tip) {\n <div\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute z-10 grid min-w-32 -translate-y-1/2 gap-1.5 rounded-md border bg-popover px-2.5 py-1.5 text-xs text-popover-foreground shadow-md motion-safe:transition-[left,top] motion-safe:duration-150\"\n [class]=\"tip.class\"\n [style.left.px]=\"tip.left\"\n [style.top.px]=\"tip.top\"\n >\n @if (tip.title) {\n <span class=\"font-medium\">{{ tip.title }}</span>\n }\n @for (item of tip.items; track item.key) {\n <span class=\"flex items-center gap-2\">\n <span class=\"size-2.5 shrink-0 rounded-[2px]\" [style.background]=\"item.color\"></span>\n <span class=\"text-muted-foreground\">{{ item.label }}</span>\n <span class=\"ml-auto font-mono font-medium tabular-nums\">{{ item.value }}</span>\n </span>\n }\n </div>\n }\n }\n\n <div role=\"status\" class=\"sr-only\">{{ announcement() }}</div>\n </div>\n\n <!-- Legend -->\n @if (showLegend()) {\n <ul class=\"flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-xs text-muted-foreground\">\n @for (item of legendItems(); track item.key) {\n <li class=\"flex items-center gap-1.5\">\n <span aria-hidden=\"true\" class=\"size-2.5 shrink-0 rounded-[2px]\" [style.background]=\"item.color\"></span>\n {{ item.label }}\n </li>\n }\n </ul>\n }\n\n <!-- Accessible data -->\n <table class=\"sr-only\">\n <caption>\n {{ ariaLabel() }}\n </caption>\n <thead>\n <tr>\n @if (scene().table.corner !== null) {\n <th scope=\"col\">{{ scene().table.corner }}</th>\n }\n @for (head of scene().table.head; track $index) {\n <th scope=\"col\">{{ head }}</th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of scene().table.rows; track $index) {\n <tr>\n @if (row.header !== null) {\n <th scope=\"row\">{{ row.header }}</th>\n }\n @for (cell of row.cells; track $index) {\n <td>{{ cell }}</td>\n }\n </tr>\n }\n </tbody>\n </table>\n</div>\n", dependencies: [{ kind: "directive", type: HlmSkeleton, selector: "[hlmSkeleton],hlm-skeleton" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
637
+ }
638
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: YuwChartComponent, decorators: [{
639
+ type: Component,
640
+ args: [{ selector: 'yuw-chart', imports: [HlmSkeleton], host: {
641
+ class: 'contents',
642
+ }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div role=\"figure\" [class]=\"rootClass()\" [attr.aria-label]=\"ariaLabel()\" [attr.aria-busy]=\"loading() || null\">\n @if (description(); as text) {\n <p class=\"sr-only\" [id]=\"descriptionId\">{{ text }}</p>\n }\n\n <!-- Plot -->\n <div\n #plot\n role=\"group\"\n class=\"relative min-h-0 flex-1 rounded-md outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"interactive() ? 0 : null\"\n (focus)=\"onFocus($event)\"\n (blur)=\"clear()\"\n (keydown)=\"onKeydown($event)\"\n (pointerleave)=\"clear()\"\n >\n @if (loading()) {\n <div hlmSkeleton class=\"size-full\" aria-hidden=\"true\"></div>\n } @else if (scene().empty) {\n <div class=\"flex size-full items-center justify-center text-center text-sm text-muted-foreground\">\n {{ emptyText() }}\n </div>\n } @else if (drawable()) {\n <svg\n aria-hidden=\"true\"\n class=\"absolute inset-0 overflow-visible\"\n [attr.width]=\"size().width\"\n [attr.height]=\"size().height\"\n [attr.viewBox]=\"viewBox()\"\n >\n @for (line of scene().grid; track $index) {\n <path class=\"fill-none stroke-border [stroke-dasharray:3_3]\" [attr.d]=\"line\" />\n }\n\n @if (activeHit()?.cursor; as cursor) {\n <path [class]=\"cursorClass()\" [attr.d]=\"cursor\" />\n }\n\n <g [attr.transform]=\"origin()\">\n @for (mark of marks(); track $index) {\n <path [class]=\"mark.class\" [style]=\"mark.style\" [attr.d]=\"mark.d\" />\n }\n </g>\n\n @for (point of activePoints(); track point.key) {\n <circle\n r=\"4\"\n class=\"stroke-background stroke-2\"\n [style.fill]=\"point.color\"\n [attr.cx]=\"point.cx\"\n [attr.cy]=\"point.cy\"\n />\n }\n\n @for (tick of scene().ticks; track $index) {\n <text\n dominant-baseline=\"middle\"\n class=\"fill-muted-foreground text-xs\"\n [attr.x]=\"tick.x\"\n [attr.y]=\"tick.y\"\n [attr.text-anchor]=\"tick.anchor\"\n >\n {{ tick.label }}\n </text>\n }\n\n <g [attr.transform]=\"origin()\">\n @for (hit of scene().hits; track $index) {\n <path class=\"fill-transparent\" [attr.d]=\"hit.d\" (pointerenter)=\"activate($index)\" />\n }\n </g>\n </svg>\n\n @if (tooltip(); as tip) {\n <div\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute z-10 grid min-w-32 -translate-y-1/2 gap-1.5 rounded-md border bg-popover px-2.5 py-1.5 text-xs text-popover-foreground shadow-md motion-safe:transition-[left,top] motion-safe:duration-150\"\n [class]=\"tip.class\"\n [style.left.px]=\"tip.left\"\n [style.top.px]=\"tip.top\"\n >\n @if (tip.title) {\n <span class=\"font-medium\">{{ tip.title }}</span>\n }\n @for (item of tip.items; track item.key) {\n <span class=\"flex items-center gap-2\">\n <span class=\"size-2.5 shrink-0 rounded-[2px]\" [style.background]=\"item.color\"></span>\n <span class=\"text-muted-foreground\">{{ item.label }}</span>\n <span class=\"ml-auto font-mono font-medium tabular-nums\">{{ item.value }}</span>\n </span>\n }\n </div>\n }\n }\n\n <div role=\"status\" class=\"sr-only\">{{ announcement() }}</div>\n </div>\n\n <!-- Legend -->\n @if (showLegend()) {\n <ul class=\"flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-xs text-muted-foreground\">\n @for (item of legendItems(); track item.key) {\n <li class=\"flex items-center gap-1.5\">\n <span aria-hidden=\"true\" class=\"size-2.5 shrink-0 rounded-[2px]\" [style.background]=\"item.color\"></span>\n {{ item.label }}\n </li>\n }\n </ul>\n }\n\n <!-- Accessible data -->\n <table class=\"sr-only\">\n <caption>\n {{ ariaLabel() }}\n </caption>\n <thead>\n <tr>\n @if (scene().table.corner !== null) {\n <th scope=\"col\">{{ scene().table.corner }}</th>\n }\n @for (head of scene().table.head; track $index) {\n <th scope=\"col\">{{ head }}</th>\n }\n </tr>\n </thead>\n <tbody>\n @for (row of scene().table.rows; track $index) {\n <tr>\n @if (row.header !== null) {\n <th scope=\"row\">{{ row.header }}</th>\n }\n @for (cell of row.cells; track $index) {\n <td>{{ cell }}</td>\n }\n </tr>\n }\n </tbody>\n </table>\n</div>\n" }]
643
+ }], propDecorators: { _plot: [{ type: i0.ViewChild, args: ['plot', { isSignal: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: true }] }], categoryFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "categoryFormat", required: false }] }], categoryKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "categoryKey", required: false }] }], categoryLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "categoryLabel", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: true }] }], kind: [{ type: i0.Input, args: [{ isSignal: true, alias: "kind", required: false }] }], legend: [{ type: i0.Input, args: [{ isSignal: true, alias: "legend", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }] } });
644
+
645
+ /**
646
+ * Generated bundle index. Do not edit.
647
+ */
648
+
649
+ export { YUW_CHART_COLORS, YuwChartComponent, yuwChartColorAt };
650
+ //# sourceMappingURL=yukit-web-chart.mjs.map