@forgeroute/ui 0.4.0 → 0.4.2

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.
@@ -1,71 +1,265 @@
1
1
  import * as i0 from "@angular/core";
2
- import { ChangeDetectionStrategy, Component, DestroyRef, NgZone, computed, effect, inject, input, viewChild } from "@angular/core";
3
- import { BarController, BarElement, CategoryScale, Chart, LinearScale, Tooltip } from "chart.js";
4
- let registered = false;
5
- function registerChartParts() {
6
- if (registered) return;
2
+ import { ChangeDetectionStrategy, Component, DestroyRef, NgZone, PLATFORM_ID, computed, effect, inject, input, viewChild } from "@angular/core";
3
+ import { ArcElement, BarController, BarElement, CategoryScale, Chart, DoughnutController, LinearScale, Tooltip } from "chart.js";
4
+ import { isPlatformBrowser } from "@angular/common";
5
+ let registered$1 = false;
6
+ function registerChartParts$1() {
7
+ if (registered$1) return;
7
8
  Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip);
8
- registered = true;
9
+ registered$1 = true;
10
+ }
11
+ const HEX_RE = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
12
+ const RGB_RE = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d*\.?\d+)\s*)?\)$/i;
13
+ const HOVER_DARKEN = .8;
14
+ function darkenColor(input) {
15
+ const c = (input ?? "").trim();
16
+ let r, g, b, a = null;
17
+ const hex = HEX_RE.exec(c);
18
+ if (hex) {
19
+ let h = hex[1];
20
+ if (h.length <= 4) h = h.split("").map((ch) => ch + ch).join("");
21
+ r = parseInt(h.slice(0, 2), 16);
22
+ g = parseInt(h.slice(2, 4), 16);
23
+ b = parseInt(h.slice(4, 6), 16);
24
+ if (h.length === 8) a = Math.round(parseInt(h.slice(6, 8), 16) / 255 * 1e3) / 1e3;
25
+ } else {
26
+ const m = RGB_RE.exec(c);
27
+ if (!m) return null;
28
+ r = Number(m[1]);
29
+ g = Number(m[2]);
30
+ b = Number(m[3]);
31
+ if (r > 255 || g > 255 || b > 255) return null;
32
+ if (m[4] !== void 0) {
33
+ a = Number(m[4]);
34
+ if (a > 1) return null;
35
+ }
36
+ }
37
+ const d = (n) => Math.round(n * HOVER_DARKEN);
38
+ return a === null ? `rgb(${d(r)}, ${d(g)}, ${d(b)})` : `rgba(${d(r)}, ${d(g)}, ${d(b)}, ${a})`;
39
+ }
40
+ const BAR_CHART_SERIES_PALETTE = [
41
+ "#2563eb",
42
+ "#16a34a",
43
+ "#d27006",
44
+ "#dc2626",
45
+ "#7c3aed",
46
+ "#0891b2",
47
+ "#db2777",
48
+ "#4d7c0f",
49
+ "#475569",
50
+ "#0d9488",
51
+ "#c2410c",
52
+ "#4f46e5",
53
+ "#a21caf",
54
+ "#b45309"
55
+ ];
56
+ function tryFormat(locale, options) {
57
+ try {
58
+ return new Intl.NumberFormat(locale, options);
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+ function safeLocale(locale) {
64
+ const l = (locale ?? "").trim();
65
+ return l && tryFormat(l) ? l : "en-US";
66
+ }
67
+ function safeCurrency(currency) {
68
+ const c = (currency ?? "").trim().toUpperCase();
69
+ return c && tryFormat("en-US", {
70
+ style: "currency",
71
+ currency: c
72
+ }) ? c : "USD";
73
+ }
74
+ function formatWith(nf, n, scale = 1) {
75
+ if (!Number.isFinite(n)) return String(n);
76
+ return nf.format(n === 0 ? 0 : n / scale);
9
77
  }
10
78
  const VALUE_LABELS = {
11
79
  id: "uiBarChartValueLabels",
12
80
  afterDatasetsDraw(chart) {
13
- if (chart.options.plugins["uiBarChartValueLabels"]?.enabled === false) return;
81
+ const opts = chart.options.plugins["uiBarChartValueLabels"];
82
+ if (opts?.enabled === false) return;
14
83
  const ctx = chart.ctx;
15
84
  ctx.save();
16
85
  ctx.fillStyle = "#475569";
17
86
  ctx.font = "600 11px sans-serif";
18
87
  ctx.textAlign = "center";
19
88
  ctx.textBaseline = "bottom";
20
- chart.getDatasetMeta(0).data.forEach((bar, i) => {
21
- const value = chart.data.datasets[0]?.data[i];
22
- if (typeof value === "number" && Number.isFinite(value) && value !== 0) ctx.fillText(String(value), bar.x, bar.y - 2);
89
+ chart.data.datasets.forEach((dataset, di) => {
90
+ if (!chart.isDatasetVisible(di)) return;
91
+ chart.getDatasetMeta(di).data.forEach((bar, i) => {
92
+ const value = dataset.data[i];
93
+ if (typeof value === "number" && Number.isFinite(value) && value !== 0) ctx.fillText(opts?.format ? opts.format(value) : String(value), bar.x, bar.y - 2);
94
+ });
23
95
  });
24
96
  ctx.restore();
25
97
  }
26
98
  };
27
99
  var BarChartComponent = class BarChartComponent {
100
+ static fills(s) {
101
+ if (!s.colors?.length) return {
102
+ bg: s.color,
103
+ hover: s.hoverColor
104
+ };
105
+ const bg = [], hover = [];
106
+ for (let i = 0; i < s.data.length; i++) {
107
+ const own = typeof s.colors[i] === "string" ? s.colors[i].trim() : "";
108
+ if (!own) {
109
+ bg.push(s.color);
110
+ hover.push(s.hoverColor);
111
+ continue;
112
+ }
113
+ bg.push(s.colors[i]);
114
+ hover.push(darkenColor(own) ?? s.colors[i]);
115
+ }
116
+ return {
117
+ bg,
118
+ hover
119
+ };
120
+ }
121
+ static groupDataset(g) {
122
+ return {
123
+ label: g.name,
124
+ data: g.data,
125
+ backgroundColor: g.color,
126
+ hoverBackgroundColor: g.hover,
127
+ borderRadius: 3,
128
+ borderSkipped: false
129
+ };
130
+ }
28
131
  constructor() {
29
132
  this.labels = input([], ...ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []);
30
133
  this.values = input([], ...ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []);
31
134
  this.color = input("#3b82f6", ...ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []);
135
+ this.colors = input([], ...ngDevMode ? [{ debugName: "colors" }] : /* istanbul ignore next */ []);
32
136
  this.hoverColor = input("#2563eb", ...ngDevMode ? [{ debugName: "hoverColor" }] : /* istanbul ignore next */ []);
33
137
  this.height = input(240, ...ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []);
34
138
  this.ariaLabel = input("", ...ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []);
35
139
  this.showValues = input(true, ...ngDevMode ? [{ debugName: "showValues" }] : /* istanbul ignore next */ []);
36
140
  this.unitSingular = input("", ...ngDevMode ? [{ debugName: "unitSingular" }] : /* istanbul ignore next */ []);
37
141
  this.unitPlural = input("", ...ngDevMode ? [{ debugName: "unitPlural" }] : /* istanbul ignore next */ []);
142
+ this.valueFormat = input("number", ...ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []);
143
+ this.currency = input("USD", ...ngDevMode ? [{ debugName: "currency" }] : /* istanbul ignore next */ []);
144
+ this.locale = input("en-US", ...ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []);
145
+ this.decimals = input(null, ...ngDevMode ? [{ debugName: "decimals" }] : /* istanbul ignore next */ []);
146
+ this.series = input([], ...ngDevMode ? [{ debugName: "series" }] : /* istanbul ignore next */ []);
147
+ this.stacked = input(false, ...ngDevMode ? [{ debugName: "stacked" }] : /* istanbul ignore next */ []);
148
+ this.showLegend = input(true, ...ngDevMode ? [{ debugName: "showLegend" }] : /* istanbul ignore next */ []);
38
149
  this.canvas = viewChild.required("canvas", ...ngDevMode ? [{ debugName: "canvas" }] : /* istanbul ignore next */ []);
39
150
  this.zone = inject(NgZone);
40
151
  this.chart = null;
152
+ this.multiMode = null;
153
+ this.formatter = computed(() => {
154
+ const kind = this.valueFormat();
155
+ if (kind !== "currency" && kind !== "percent") return null;
156
+ const locale = safeLocale(this.locale());
157
+ const d = this.decimals();
158
+ const fixed = typeof d === "number" && Number.isFinite(d) ? Math.min(6, Math.max(0, Math.trunc(d))) : null;
159
+ const digits = (min, max) => ({
160
+ minimumFractionDigits: min,
161
+ maximumFractionDigits: max
162
+ });
163
+ const make = (extra) => {
164
+ try {
165
+ return new Intl.NumberFormat(locale, {
166
+ signDisplay: "negative",
167
+ ...extra
168
+ });
169
+ } catch {
170
+ return new Intl.NumberFormat(locale, extra);
171
+ }
172
+ };
173
+ if (kind === "currency") {
174
+ const base = {
175
+ style: "currency",
176
+ currency: safeCurrency(this.currency())
177
+ };
178
+ const full = make(fixed === null ? base : {
179
+ ...base,
180
+ ...digits(fixed, fixed)
181
+ });
182
+ const whole = fixed === null ? make({
183
+ ...base,
184
+ ...digits(0, 0)
185
+ }) : full;
186
+ return {
187
+ value: (n) => formatWith(full, n),
188
+ tick: (n) => formatWith(Number.isInteger(n) ? whole : full, n)
189
+ };
190
+ }
191
+ const pct = make({
192
+ style: "percent",
193
+ ...fixed === null ? digits(0, 1) : digits(fixed, fixed)
194
+ });
195
+ return {
196
+ value: (n) => formatWith(pct, n, 100),
197
+ tick: (n) => formatWith(pct, n, 100)
198
+ };
199
+ }, ...ngDevMode ? [{ debugName: "formatter" }] : /* istanbul ignore next */ []);
200
+ this.resolvedSeries = computed(() => {
201
+ const list = this.series();
202
+ if (!Array.isArray(list)) return [];
203
+ const count = this.labels().length;
204
+ return list.map((raw, i) => {
205
+ const s = raw && typeof raw === "object" ? raw : {};
206
+ const name = typeof s.name === "string" && s.name.trim() ? s.name.trim() : `Series ${i + 1}`;
207
+ const color = (typeof s.color === "string" && s.color.trim() ? s.color : "") || BAR_CHART_SERIES_PALETTE[i % BAR_CHART_SERIES_PALETTE.length];
208
+ const hover = (typeof s.hoverColor === "string" && s.hoverColor.trim() ? s.hoverColor : "") || (darkenColor(color) ?? color);
209
+ const source = Array.isArray(s.values) ? s.values : [];
210
+ return {
211
+ name,
212
+ data: Array.from({ length: count }, (_, k) => typeof source[k] === "number" ? source[k] : 0),
213
+ color,
214
+ hover
215
+ };
216
+ });
217
+ }, ...ngDevMode ? [{ debugName: "resolvedSeries" }] : /* istanbul ignore next */ []);
218
+ this.legendVisible = computed(() => this.resolvedSeries().length > 0 && this.showLegend(), ...ngDevMode ? [{ debugName: "legendVisible" }] : /* istanbul ignore next */ []);
219
+ this.srRows = computed(() => {
220
+ const groups = this.resolvedSeries();
221
+ if (!groups.length) return [];
222
+ const rows = [];
223
+ this.labels().forEach((label, k) => groups.forEach((g) => rows.push(`${label}, ${g.name}: ${this.tooltipText(g.data[k])}`)));
224
+ return rows;
225
+ }, ...ngDevMode ? [{ debugName: "srRows" }] : /* istanbul ignore next */ []);
41
226
  this.tooltipText = (value) => {
227
+ const f = this.formatter();
228
+ if (f) return f.value(value);
42
229
  const unit = value === 1 ? this.unitSingular() : this.unitPlural();
43
230
  return unit ? `${value} ${unit}` : String(value);
44
231
  };
45
- this.series = computed(() => ({
232
+ this.model = computed(() => ({
233
+ groups: this.resolvedSeries(),
234
+ stacked: this.stacked(),
46
235
  labels: this.labels(),
47
236
  data: this.values(),
48
237
  color: this.color(),
238
+ colors: this.colors(),
49
239
  hoverColor: this.hoverColor(),
50
- showValues: this.showValues()
51
- }), ...ngDevMode ? [{ debugName: "series" }] : /* istanbul ignore next */ []);
240
+ showValues: this.showValues(),
241
+ formatter: this.formatter()
242
+ }), ...ngDevMode ? [{ debugName: "model" }] : /* istanbul ignore next */ []);
52
243
  effect(() => {
53
- const s = this.series();
244
+ const s = this.model();
245
+ const multi = s.groups.length > 0;
246
+ const fills = BarChartComponent.fills(s);
247
+ const freshDatasets = () => multi ? s.groups.map(BarChartComponent.groupDataset) : [{
248
+ data: s.data,
249
+ backgroundColor: fills.bg,
250
+ hoverBackgroundColor: fills.hover,
251
+ borderRadius: 3,
252
+ borderSkipped: false
253
+ }];
54
254
  const canvas = this.canvas().nativeElement;
55
255
  if (!this.chart) {
56
- registerChartParts();
256
+ registerChartParts$1();
57
257
  this.zone.runOutsideAngular(() => {
58
258
  this.chart = new Chart(canvas, {
59
259
  type: "bar",
60
260
  data: {
61
261
  labels: s.labels,
62
- datasets: [{
63
- data: s.data,
64
- backgroundColor: s.color,
65
- hoverBackgroundColor: s.hoverColor,
66
- borderRadius: 3,
67
- borderSkipped: false
68
- }]
262
+ datasets: freshDatasets()
69
263
  },
70
264
  options: {
71
265
  responsive: true,
@@ -74,7 +268,10 @@ var BarChartComponent = class BarChartComponent {
74
268
  layout: { padding: { top: 20 } },
75
269
  plugins: {
76
270
  legend: { display: false },
77
- tooltip: { callbacks: { label: (item) => this.tooltipText(Number(item.raw)) } }
271
+ tooltip: { callbacks: { label: (item) => {
272
+ const text = this.tooltipText(Number(item.raw));
273
+ return this.multiMode ? `${item.dataset?.label ?? ""}: ${text}` : text;
274
+ } } }
78
275
  },
79
276
  scales: {
80
277
  y: {
@@ -103,12 +300,40 @@ var BarChartComponent = class BarChartComponent {
103
300
  });
104
301
  }
105
302
  const chart = this.chart;
303
+ if (this.numberTickCallback === void 0) this.numberTickCallback = chart.options.scales["y"].ticks.callback;
106
304
  chart.data.labels = s.labels;
107
- const ds = chart.data.datasets[0];
108
- ds.data = s.data;
109
- ds.backgroundColor = s.color;
110
- ds.hoverBackgroundColor = s.hoverColor;
111
- chart.options.plugins["uiBarChartValueLabels"] = { enabled: s.showValues };
305
+ if (this.multiMode !== null && this.multiMode !== multi) chart.data.datasets = freshDatasets();
306
+ this.multiMode = multi;
307
+ if (multi) {
308
+ const datasets = chart.data.datasets;
309
+ s.groups.forEach((g, i) => {
310
+ const next = BarChartComponent.groupDataset(g);
311
+ if (datasets[i]) Object.assign(datasets[i], next);
312
+ else datasets.push(next);
313
+ });
314
+ datasets.splice(s.groups.length);
315
+ } else {
316
+ const ds = chart.data.datasets[0];
317
+ ds.data = s.data;
318
+ ds.backgroundColor = fills.bg;
319
+ ds.hoverBackgroundColor = fills.hover;
320
+ }
321
+ const axes = chart.options.scales;
322
+ for (const axis of ["x", "y"]) if (multi || axes[axis].stacked !== void 0) axes[axis].stacked = multi ? s.stacked : void 0;
323
+ const labelsOn = multi ? s.showValues && !s.stacked && s.labels.length * s.groups.length <= 24 : s.showValues;
324
+ chart.options.plugins["uiBarChartValueLabels"] = s.formatter ? {
325
+ enabled: labelsOn,
326
+ format: s.formatter.value
327
+ } : { enabled: labelsOn };
328
+ const ticks = chart.options.scales["y"].ticks;
329
+ if (s.formatter) {
330
+ const tick = s.formatter.tick;
331
+ ticks.precision = void 0;
332
+ ticks.callback = (v) => tick(Number(v));
333
+ } else {
334
+ ticks.precision = 0;
335
+ if (this.numberTickCallback !== void 0) ticks.callback = this.numberTickCallback;
336
+ }
112
337
  chart.update("none");
113
338
  });
114
339
  inject(DestroyRef).onDestroy(() => {
@@ -128,7 +353,7 @@ var BarChartComponent = class BarChartComponent {
128
353
  }
129
354
  static {
130
355
  this.ɵcmp = i0.ɵɵngDeclareComponent({
131
- minVersion: "17.2.0",
356
+ minVersion: "17.0.0",
132
357
  version: "22.2.2",
133
358
  type: BarChartComponent,
134
359
  isStandalone: true,
@@ -155,6 +380,13 @@ var BarChartComponent = class BarChartComponent {
155
380
  isRequired: false,
156
381
  transformFunction: null
157
382
  },
383
+ colors: {
384
+ classPropertyName: "colors",
385
+ publicName: "colors",
386
+ isSignal: true,
387
+ isRequired: false,
388
+ transformFunction: null
389
+ },
158
390
  hoverColor: {
159
391
  classPropertyName: "hoverColor",
160
392
  publicName: "hoverColor",
@@ -196,6 +428,55 @@ var BarChartComponent = class BarChartComponent {
196
428
  isSignal: true,
197
429
  isRequired: false,
198
430
  transformFunction: null
431
+ },
432
+ valueFormat: {
433
+ classPropertyName: "valueFormat",
434
+ publicName: "valueFormat",
435
+ isSignal: true,
436
+ isRequired: false,
437
+ transformFunction: null
438
+ },
439
+ currency: {
440
+ classPropertyName: "currency",
441
+ publicName: "currency",
442
+ isSignal: true,
443
+ isRequired: false,
444
+ transformFunction: null
445
+ },
446
+ locale: {
447
+ classPropertyName: "locale",
448
+ publicName: "locale",
449
+ isSignal: true,
450
+ isRequired: false,
451
+ transformFunction: null
452
+ },
453
+ decimals: {
454
+ classPropertyName: "decimals",
455
+ publicName: "decimals",
456
+ isSignal: true,
457
+ isRequired: false,
458
+ transformFunction: null
459
+ },
460
+ series: {
461
+ classPropertyName: "series",
462
+ publicName: "series",
463
+ isSignal: true,
464
+ isRequired: false,
465
+ transformFunction: null
466
+ },
467
+ stacked: {
468
+ classPropertyName: "stacked",
469
+ publicName: "stacked",
470
+ isSignal: true,
471
+ isRequired: false,
472
+ transformFunction: null
473
+ },
474
+ showLegend: {
475
+ classPropertyName: "showLegend",
476
+ publicName: "showLegend",
477
+ isSignal: true,
478
+ isRequired: false,
479
+ transformFunction: null
199
480
  }
200
481
  },
201
482
  viewQueries: [{
@@ -211,9 +492,24 @@ var BarChartComponent = class BarChartComponent {
211
492
  [attr.role]="ariaLabel() ? 'img' : null" [attr.aria-label]="ariaLabel() || null">
212
493
  <canvas #canvas></canvas>
213
494
  </div>
495
+ @if (legendVisible()) {
496
+ <ul class="bc-legend" aria-hidden="true">
497
+ @for (g of resolvedSeries(); track $index) {
498
+ <li class="bc-legend-item">
499
+ <span class="bc-swatch" [style.background]="g.color"></span>
500
+ <span class="bc-legend-label">{{ g.name }}</span>
501
+ </li>
502
+ }
503
+ </ul>
504
+ }
505
+ @if (srRows().length) {
506
+ <ul class="bc-sr-only" role="list">
507
+ @for (row of srRows(); track $index) { <li>{{ row }}</li> }
508
+ </ul>
509
+ }
214
510
  `,
215
511
  isInline: true,
216
- styles: [":host{display:block}.bc-wrap{position:relative}@media(max-width:768px){.bc-wrap.bc-default-height{height:200px!important}}@media(max-width:480px){.bc-wrap.bc-default-height{height:160px!important}}\n"],
512
+ styles: [":host{display:block;position:relative}.bc-wrap{position:relative}.bc-legend{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-wrap:wrap;justify-content:center;gap:6px 16px}.bc-legend-item{display:flex;align-items:center;gap:6px;font-size:.8125rem;color:#334155;max-width:100%;min-width:0}.bc-legend-label{min-width:0;overflow-wrap:anywhere;word-break:break-word}.bc-swatch{width:10px;height:10px;border-radius:2px;flex-shrink:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;forced-color-adjust:none}.bc-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;list-style:none}@media(max-width:768px){.bc-wrap.bc-default-height{height:200px!important}}@media(max-width:480px){.bc-wrap.bc-default-height{height:160px!important}.bc-legend{gap:4px 12px}}\n"],
217
513
  changeDetection: i0.ChangeDetectionStrategy.OnPush
218
514
  });
219
515
  }
@@ -234,8 +530,23 @@ i0.ɵɵngDeclareClassMetadata({
234
530
  [attr.role]="ariaLabel() ? 'img' : null" [attr.aria-label]="ariaLabel() || null">
235
531
  <canvas #canvas></canvas>
236
532
  </div>
533
+ @if (legendVisible()) {
534
+ <ul class="bc-legend" aria-hidden="true">
535
+ @for (g of resolvedSeries(); track $index) {
536
+ <li class="bc-legend-item">
537
+ <span class="bc-swatch" [style.background]="g.color"></span>
538
+ <span class="bc-legend-label">{{ g.name }}</span>
539
+ </li>
540
+ }
541
+ </ul>
542
+ }
543
+ @if (srRows().length) {
544
+ <ul class="bc-sr-only" role="list">
545
+ @for (row of srRows(); track $index) { <li>{{ row }}</li> }
546
+ </ul>
547
+ }
237
548
  `,
238
- styles: [":host{display:block}.bc-wrap{position:relative}@media(max-width:768px){.bc-wrap.bc-default-height{height:200px!important}}@media(max-width:480px){.bc-wrap.bc-default-height{height:160px!important}}\n"]
549
+ styles: [":host{display:block;position:relative}.bc-wrap{position:relative}.bc-legend{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-wrap:wrap;justify-content:center;gap:6px 16px}.bc-legend-item{display:flex;align-items:center;gap:6px;font-size:.8125rem;color:#334155;max-width:100%;min-width:0}.bc-legend-label{min-width:0;overflow-wrap:anywhere;word-break:break-word}.bc-swatch{width:10px;height:10px;border-radius:2px;flex-shrink:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;forced-color-adjust:none}.bc-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;list-style:none}@media(max-width:768px){.bc-wrap.bc-default-height{height:200px!important}}@media(max-width:480px){.bc-wrap.bc-default-height{height:160px!important}.bc-legend{gap:4px 12px}}\n"]
239
550
  }]
240
551
  }],
241
552
  ctorParameters: () => [],
@@ -264,6 +575,14 @@ i0.ɵɵngDeclareClassMetadata({
264
575
  required: false
265
576
  }]
266
577
  }],
578
+ colors: [{
579
+ type: i0.Input,
580
+ args: [{
581
+ isSignal: true,
582
+ alias: "colors",
583
+ required: false
584
+ }]
585
+ }],
267
586
  hoverColor: [{
268
587
  type: i0.Input,
269
588
  args: [{
@@ -312,12 +631,518 @@ i0.ɵɵngDeclareClassMetadata({
312
631
  required: false
313
632
  }]
314
633
  }],
634
+ valueFormat: [{
635
+ type: i0.Input,
636
+ args: [{
637
+ isSignal: true,
638
+ alias: "valueFormat",
639
+ required: false
640
+ }]
641
+ }],
642
+ currency: [{
643
+ type: i0.Input,
644
+ args: [{
645
+ isSignal: true,
646
+ alias: "currency",
647
+ required: false
648
+ }]
649
+ }],
650
+ locale: [{
651
+ type: i0.Input,
652
+ args: [{
653
+ isSignal: true,
654
+ alias: "locale",
655
+ required: false
656
+ }]
657
+ }],
658
+ decimals: [{
659
+ type: i0.Input,
660
+ args: [{
661
+ isSignal: true,
662
+ alias: "decimals",
663
+ required: false
664
+ }]
665
+ }],
666
+ series: [{
667
+ type: i0.Input,
668
+ args: [{
669
+ isSignal: true,
670
+ alias: "series",
671
+ required: false
672
+ }]
673
+ }],
674
+ stacked: [{
675
+ type: i0.Input,
676
+ args: [{
677
+ isSignal: true,
678
+ alias: "stacked",
679
+ required: false
680
+ }]
681
+ }],
682
+ showLegend: [{
683
+ type: i0.Input,
684
+ args: [{
685
+ isSignal: true,
686
+ alias: "showLegend",
687
+ required: false
688
+ }]
689
+ }],
690
+ canvas: [{
691
+ type: i0.ViewChild,
692
+ args: ["canvas", { isSignal: true }]
693
+ }]
694
+ }
695
+ });
696
+ let registered = false;
697
+ function registerChartParts() {
698
+ if (registered) return;
699
+ Chart.register(DoughnutController, ArcElement, Tooltip);
700
+ registered = true;
701
+ }
702
+ const PIE_CHART_PALETTE = [
703
+ "#2563eb",
704
+ "#16a34a",
705
+ "#d27006",
706
+ "#dc2626",
707
+ "#7c3aed",
708
+ "#0891b2",
709
+ "#db2777",
710
+ "#4d7c0f",
711
+ "#475569",
712
+ "#0d9488",
713
+ "#c2410c",
714
+ "#4f46e5",
715
+ "#a21caf",
716
+ "#b45309"
717
+ ];
718
+ const DOUGHNUT_CUTOUT = "60%";
719
+ const PIE_CUTOUT = "0%";
720
+ const EMPTY_COLOR = "#e2e8f0";
721
+ function formatPercent(value, total) {
722
+ if (!(total > 0) || !(value > 0)) return "0%";
723
+ const rounded = Math.round(value / total * 1e3) / 10;
724
+ return rounded === 0 ? "<0.1%" : `${rounded}%`;
725
+ }
726
+ function prefersReducedMotion() {
727
+ try {
728
+ return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
729
+ } catch {
730
+ return false;
731
+ }
732
+ }
733
+ var PieChartComponent = class PieChartComponent {
734
+ constructor() {
735
+ this.labels = input([], ...ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []);
736
+ this.values = input([], ...ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []);
737
+ this.doughnut = input(false, ...ngDevMode ? [{ debugName: "doughnut" }] : /* istanbul ignore next */ []);
738
+ this.colors = input([], ...ngDevMode ? [{ debugName: "colors" }] : /* istanbul ignore next */ []);
739
+ this.height = input(240, ...ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []);
740
+ this.ariaLabel = input("", ...ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []);
741
+ this.showLegend = input(true, ...ngDevMode ? [{ debugName: "showLegend" }] : /* istanbul ignore next */ []);
742
+ this.showPercent = input(true, ...ngDevMode ? [{ debugName: "showPercent" }] : /* istanbul ignore next */ []);
743
+ this.unitSingular = input("", ...ngDevMode ? [{ debugName: "unitSingular" }] : /* istanbul ignore next */ []);
744
+ this.unitPlural = input("", ...ngDevMode ? [{ debugName: "unitPlural" }] : /* istanbul ignore next */ []);
745
+ this.centerLabel = input("", ...ngDevMode ? [{ debugName: "centerLabel" }] : /* istanbul ignore next */ []);
746
+ this.centerValue = input("", ...ngDevMode ? [{ debugName: "centerValue" }] : /* istanbul ignore next */ []);
747
+ this.canvas = viewChild.required("canvas", ...ngDevMode ? [{ debugName: "canvas" }] : /* istanbul ignore next */ []);
748
+ this.zone = inject(NgZone);
749
+ this.browser = isPlatformBrowser(inject(PLATFORM_ID));
750
+ this.chart = null;
751
+ this.slices = computed(() => {
752
+ const labels = this.labels();
753
+ const values = this.values();
754
+ const palette = this.colors().length ? this.colors() : PIE_CHART_PALETTE;
755
+ const clean = labels.map((_, i) => {
756
+ const v = values[i];
757
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
758
+ });
759
+ let total = clean.reduce((a, b) => a + b, 0);
760
+ const scale = Number.isFinite(total) ? 1 : Math.max(...clean);
761
+ if (scale !== 1) total = clean.reduce((a, b) => a + b / scale, 0);
762
+ const out = [];
763
+ clean.forEach((value, index) => {
764
+ if (value > 0) out.push({
765
+ index,
766
+ label: labels[index],
767
+ value,
768
+ percent: formatPercent(value / scale, total),
769
+ color: palette[index % palette.length]
770
+ });
771
+ });
772
+ return out;
773
+ }, ...ngDevMode ? [{ debugName: "slices" }] : /* istanbul ignore next */ []);
774
+ this.isEmpty = computed(() => this.slices().length === 0, ...ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []);
775
+ this.showCenter = computed(() => this.doughnut() && !this.isEmpty() && !!(this.centerLabel() || this.centerValue()), ...ngDevMode ? [{ debugName: "showCenter" }] : /* istanbul ignore next */ []);
776
+ this.valueText = (value) => {
777
+ const unit = value === 1 ? this.unitSingular() : this.unitPlural();
778
+ return unit ? `${value} ${unit}` : String(value);
779
+ };
780
+ this.tooltipText = (dataIndex, value) => {
781
+ const base = this.valueText(value);
782
+ const slice = this.slices()[dataIndex];
783
+ return this.showPercent() && slice ? `${base} (${slice.percent})` : base;
784
+ };
785
+ this.chartData = computed(() => {
786
+ const empty = this.isEmpty();
787
+ const s = this.slices();
788
+ return {
789
+ labels: empty ? ["No data"] : s.map((x) => x.label),
790
+ data: empty ? [1] : s.map((x) => x.value),
791
+ colors: empty ? [EMPTY_COLOR] : s.map((x) => x.color),
792
+ cutout: this.doughnut() ? DOUGHNUT_CUTOUT : PIE_CUTOUT,
793
+ tooltip: !empty
794
+ };
795
+ }, ...ngDevMode ? [{ debugName: "chartData" }] : /* istanbul ignore next */ []);
796
+ effect(() => {
797
+ const d = this.chartData();
798
+ if (!this.browser) return;
799
+ const canvas = this.canvas().nativeElement;
800
+ const reduced = prefersReducedMotion();
801
+ if (!this.chart) {
802
+ registerChartParts();
803
+ this.zone.runOutsideAngular(() => {
804
+ this.chart = new Chart(canvas, {
805
+ type: "doughnut",
806
+ data: {
807
+ labels: d.labels,
808
+ datasets: [{
809
+ data: d.data,
810
+ backgroundColor: d.colors,
811
+ borderColor: "#ffffff",
812
+ borderWidth: 2,
813
+ hoverOffset: 4
814
+ }]
815
+ },
816
+ options: {
817
+ responsive: true,
818
+ maintainAspectRatio: false,
819
+ animation: reduced ? false : { duration: 400 },
820
+ cutout: d.cutout,
821
+ plugins: {
822
+ legend: { display: false },
823
+ tooltip: {
824
+ enabled: d.tooltip,
825
+ callbacks: { label: (item) => this.tooltipText(item.dataIndex, Number(item.raw)) }
826
+ }
827
+ }
828
+ }
829
+ });
830
+ });
831
+ }
832
+ const chart = this.chart;
833
+ chart.data.labels = d.labels;
834
+ const ds = chart.data.datasets[0];
835
+ ds.data = d.data;
836
+ ds.backgroundColor = d.colors;
837
+ chart.options.cutout = d.cutout;
838
+ chart.options.animation = reduced ? false : { duration: 400 };
839
+ chart.options.plugins.tooltip.enabled = d.tooltip;
840
+ this.zone.runOutsideAngular(() => chart.update(reduced ? "none" : void 0));
841
+ });
842
+ inject(DestroyRef).onDestroy(() => {
843
+ this.chart?.destroy();
844
+ this.chart = null;
845
+ });
846
+ }
847
+ static {
848
+ this.ɵfac = i0.ɵɵngDeclareFactory({
849
+ minVersion: "12.0.0",
850
+ version: "22.2.2",
851
+ ngImport: i0,
852
+ type: PieChartComponent,
853
+ deps: [],
854
+ target: i0.ɵɵFactoryTarget.Component
855
+ });
856
+ }
857
+ static {
858
+ this.ɵcmp = i0.ɵɵngDeclareComponent({
859
+ minVersion: "17.0.0",
860
+ version: "22.2.2",
861
+ type: PieChartComponent,
862
+ isStandalone: true,
863
+ selector: "ui-pie-chart",
864
+ inputs: {
865
+ labels: {
866
+ classPropertyName: "labels",
867
+ publicName: "labels",
868
+ isSignal: true,
869
+ isRequired: false,
870
+ transformFunction: null
871
+ },
872
+ values: {
873
+ classPropertyName: "values",
874
+ publicName: "values",
875
+ isSignal: true,
876
+ isRequired: false,
877
+ transformFunction: null
878
+ },
879
+ doughnut: {
880
+ classPropertyName: "doughnut",
881
+ publicName: "doughnut",
882
+ isSignal: true,
883
+ isRequired: false,
884
+ transformFunction: null
885
+ },
886
+ colors: {
887
+ classPropertyName: "colors",
888
+ publicName: "colors",
889
+ isSignal: true,
890
+ isRequired: false,
891
+ transformFunction: null
892
+ },
893
+ height: {
894
+ classPropertyName: "height",
895
+ publicName: "height",
896
+ isSignal: true,
897
+ isRequired: false,
898
+ transformFunction: null
899
+ },
900
+ ariaLabel: {
901
+ classPropertyName: "ariaLabel",
902
+ publicName: "ariaLabel",
903
+ isSignal: true,
904
+ isRequired: false,
905
+ transformFunction: null
906
+ },
907
+ showLegend: {
908
+ classPropertyName: "showLegend",
909
+ publicName: "showLegend",
910
+ isSignal: true,
911
+ isRequired: false,
912
+ transformFunction: null
913
+ },
914
+ showPercent: {
915
+ classPropertyName: "showPercent",
916
+ publicName: "showPercent",
917
+ isSignal: true,
918
+ isRequired: false,
919
+ transformFunction: null
920
+ },
921
+ unitSingular: {
922
+ classPropertyName: "unitSingular",
923
+ publicName: "unitSingular",
924
+ isSignal: true,
925
+ isRequired: false,
926
+ transformFunction: null
927
+ },
928
+ unitPlural: {
929
+ classPropertyName: "unitPlural",
930
+ publicName: "unitPlural",
931
+ isSignal: true,
932
+ isRequired: false,
933
+ transformFunction: null
934
+ },
935
+ centerLabel: {
936
+ classPropertyName: "centerLabel",
937
+ publicName: "centerLabel",
938
+ isSignal: true,
939
+ isRequired: false,
940
+ transformFunction: null
941
+ },
942
+ centerValue: {
943
+ classPropertyName: "centerValue",
944
+ publicName: "centerValue",
945
+ isSignal: true,
946
+ isRequired: false,
947
+ transformFunction: null
948
+ }
949
+ },
950
+ viewQueries: [{
951
+ propertyName: "canvas",
952
+ first: true,
953
+ predicate: ["canvas"],
954
+ descendants: true,
955
+ isSignal: true
956
+ }],
957
+ ngImport: i0,
958
+ template: `
959
+ <div class="pc-wrap" [class.pc-default-height]="height() === 240" [style.height.px]="height()"
960
+ [attr.role]="ariaLabel() ? 'img' : null" [attr.aria-label]="ariaLabel() || null">
961
+ <canvas #canvas></canvas>
962
+ @if (isEmpty()) {
963
+ <div class="pc-center"><span class="pc-center-label">No data</span></div>
964
+ } @else if (showCenter()) {
965
+ <div class="pc-center">
966
+ @if (centerValue()) { <span class="pc-center-value">{{ centerValue() }}</span> }
967
+ @if (centerLabel()) { <span class="pc-center-label">{{ centerLabel() }}</span> }
968
+ </div>
969
+ }
970
+ </div>
971
+ @if (showLegend() && !isEmpty()) {
972
+ <ul class="pc-legend" aria-hidden="true">
973
+ @for (s of slices(); track s.index) {
974
+ <li class="pc-legend-item">
975
+ <span class="pc-swatch" [style.background]="s.color"></span>
976
+ <span class="pc-legend-label">{{ s.label }}</span>
977
+ @if (showPercent()) { <span class="pc-legend-percent">{{ s.percent }}</span> }
978
+ </li>
979
+ }
980
+ </ul>
981
+ }
982
+ <ul class="pc-sr-only" role="list">
983
+ @if (showCenter() && (centerLabel() || centerValue())) { <li>{{ centerLabel() }} {{ centerValue() }}</li> }
984
+ @if (isEmpty()) { <li>No data</li> }
985
+ @for (s of slices(); track s.index) {
986
+ <li>{{ s.label }}: {{ valueText(s.value) }}@if (showPercent()) { ({{ s.percent }}) }</li>
987
+ }
988
+ </ul>
989
+ `,
990
+ isInline: true,
991
+ styles: [":host{display:block;position:relative}.pc-wrap{position:relative}.pc-center{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);display:flex;flex-direction:column;align-items:center;text-align:center;pointer-events:none;max-width:40%}.pc-center-value{font-size:1.5rem;font-weight:700;color:#0f172a;line-height:1.1}.pc-center-label{font-size:.75rem;color:#475569}.pc-legend{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-wrap:wrap;justify-content:center;gap:6px 16px}.pc-legend-item{display:flex;align-items:center;gap:6px;font-size:.8125rem;color:#334155;max-width:100%;min-width:0}.pc-legend-label{min-width:0;overflow-wrap:anywhere;word-break:break-word}.pc-swatch{width:10px;height:10px;border-radius:2px;flex-shrink:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;forced-color-adjust:none}.pc-legend-percent{color:#64748b}.pc-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;list-style:none}@media(max-width:768px){.pc-wrap.pc-default-height{height:200px!important}}@media(max-width:480px){.pc-wrap.pc-default-height{height:160px!important}.pc-legend{gap:4px 12px}.pc-center-value{font-size:1.125rem}}\n"],
992
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
993
+ });
994
+ }
995
+ };
996
+ i0.ɵɵngDeclareClassMetadata({
997
+ minVersion: "12.0.0",
998
+ version: "22.2.2",
999
+ ngImport: i0,
1000
+ type: PieChartComponent,
1001
+ decorators: [{
1002
+ type: Component,
1003
+ args: [{
1004
+ selector: "ui-pie-chart",
1005
+ standalone: true,
1006
+ changeDetection: ChangeDetectionStrategy.OnPush,
1007
+ template: `
1008
+ <div class="pc-wrap" [class.pc-default-height]="height() === 240" [style.height.px]="height()"
1009
+ [attr.role]="ariaLabel() ? 'img' : null" [attr.aria-label]="ariaLabel() || null">
1010
+ <canvas #canvas></canvas>
1011
+ @if (isEmpty()) {
1012
+ <div class="pc-center"><span class="pc-center-label">No data</span></div>
1013
+ } @else if (showCenter()) {
1014
+ <div class="pc-center">
1015
+ @if (centerValue()) { <span class="pc-center-value">{{ centerValue() }}</span> }
1016
+ @if (centerLabel()) { <span class="pc-center-label">{{ centerLabel() }}</span> }
1017
+ </div>
1018
+ }
1019
+ </div>
1020
+ @if (showLegend() && !isEmpty()) {
1021
+ <ul class="pc-legend" aria-hidden="true">
1022
+ @for (s of slices(); track s.index) {
1023
+ <li class="pc-legend-item">
1024
+ <span class="pc-swatch" [style.background]="s.color"></span>
1025
+ <span class="pc-legend-label">{{ s.label }}</span>
1026
+ @if (showPercent()) { <span class="pc-legend-percent">{{ s.percent }}</span> }
1027
+ </li>
1028
+ }
1029
+ </ul>
1030
+ }
1031
+ <ul class="pc-sr-only" role="list">
1032
+ @if (showCenter() && (centerLabel() || centerValue())) { <li>{{ centerLabel() }} {{ centerValue() }}</li> }
1033
+ @if (isEmpty()) { <li>No data</li> }
1034
+ @for (s of slices(); track s.index) {
1035
+ <li>{{ s.label }}: {{ valueText(s.value) }}@if (showPercent()) { ({{ s.percent }}) }</li>
1036
+ }
1037
+ </ul>
1038
+ `,
1039
+ styles: [":host{display:block;position:relative}.pc-wrap{position:relative}.pc-center{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);display:flex;flex-direction:column;align-items:center;text-align:center;pointer-events:none;max-width:40%}.pc-center-value{font-size:1.5rem;font-weight:700;color:#0f172a;line-height:1.1}.pc-center-label{font-size:.75rem;color:#475569}.pc-legend{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-wrap:wrap;justify-content:center;gap:6px 16px}.pc-legend-item{display:flex;align-items:center;gap:6px;font-size:.8125rem;color:#334155;max-width:100%;min-width:0}.pc-legend-label{min-width:0;overflow-wrap:anywhere;word-break:break-word}.pc-swatch{width:10px;height:10px;border-radius:2px;flex-shrink:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;forced-color-adjust:none}.pc-legend-percent{color:#64748b}.pc-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;list-style:none}@media(max-width:768px){.pc-wrap.pc-default-height{height:200px!important}}@media(max-width:480px){.pc-wrap.pc-default-height{height:160px!important}.pc-legend{gap:4px 12px}.pc-center-value{font-size:1.125rem}}\n"]
1040
+ }]
1041
+ }],
1042
+ ctorParameters: () => [],
1043
+ propDecorators: {
1044
+ labels: [{
1045
+ type: i0.Input,
1046
+ args: [{
1047
+ isSignal: true,
1048
+ alias: "labels",
1049
+ required: false
1050
+ }]
1051
+ }],
1052
+ values: [{
1053
+ type: i0.Input,
1054
+ args: [{
1055
+ isSignal: true,
1056
+ alias: "values",
1057
+ required: false
1058
+ }]
1059
+ }],
1060
+ doughnut: [{
1061
+ type: i0.Input,
1062
+ args: [{
1063
+ isSignal: true,
1064
+ alias: "doughnut",
1065
+ required: false
1066
+ }]
1067
+ }],
1068
+ colors: [{
1069
+ type: i0.Input,
1070
+ args: [{
1071
+ isSignal: true,
1072
+ alias: "colors",
1073
+ required: false
1074
+ }]
1075
+ }],
1076
+ height: [{
1077
+ type: i0.Input,
1078
+ args: [{
1079
+ isSignal: true,
1080
+ alias: "height",
1081
+ required: false
1082
+ }]
1083
+ }],
1084
+ ariaLabel: [{
1085
+ type: i0.Input,
1086
+ args: [{
1087
+ isSignal: true,
1088
+ alias: "ariaLabel",
1089
+ required: false
1090
+ }]
1091
+ }],
1092
+ showLegend: [{
1093
+ type: i0.Input,
1094
+ args: [{
1095
+ isSignal: true,
1096
+ alias: "showLegend",
1097
+ required: false
1098
+ }]
1099
+ }],
1100
+ showPercent: [{
1101
+ type: i0.Input,
1102
+ args: [{
1103
+ isSignal: true,
1104
+ alias: "showPercent",
1105
+ required: false
1106
+ }]
1107
+ }],
1108
+ unitSingular: [{
1109
+ type: i0.Input,
1110
+ args: [{
1111
+ isSignal: true,
1112
+ alias: "unitSingular",
1113
+ required: false
1114
+ }]
1115
+ }],
1116
+ unitPlural: [{
1117
+ type: i0.Input,
1118
+ args: [{
1119
+ isSignal: true,
1120
+ alias: "unitPlural",
1121
+ required: false
1122
+ }]
1123
+ }],
1124
+ centerLabel: [{
1125
+ type: i0.Input,
1126
+ args: [{
1127
+ isSignal: true,
1128
+ alias: "centerLabel",
1129
+ required: false
1130
+ }]
1131
+ }],
1132
+ centerValue: [{
1133
+ type: i0.Input,
1134
+ args: [{
1135
+ isSignal: true,
1136
+ alias: "centerValue",
1137
+ required: false
1138
+ }]
1139
+ }],
315
1140
  canvas: [{
316
1141
  type: i0.ViewChild,
317
1142
  args: ["canvas", { isSignal: true }]
318
1143
  }]
319
1144
  }
320
1145
  });
321
- export { BarChartComponent };
1146
+ export { BarChartComponent, PieChartComponent };
322
1147
 
323
1148
  //# sourceMappingURL=forgeroute-ui-charts.mjs.map