@ojiepermana/angular 0.1.1 → 21.0.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 (92) hide show
  1. package/README.md +41 -249
  2. package/fesm2022/ojiepermana-angular-chart.mjs +3714 -0
  3. package/fesm2022/ojiepermana-angular-chart.mjs.map +1 -0
  4. package/fesm2022/ojiepermana-angular-component.mjs +3463 -0
  5. package/fesm2022/ojiepermana-angular-component.mjs.map +1 -0
  6. package/fesm2022/ojiepermana-angular-layout.mjs +276 -408
  7. package/fesm2022/ojiepermana-angular-layout.mjs.map +1 -1
  8. package/fesm2022/ojiepermana-angular-navigation.mjs +2198 -404
  9. package/fesm2022/ojiepermana-angular-navigation.mjs.map +1 -1
  10. package/fesm2022/ojiepermana-angular-theme.mjs +381 -1
  11. package/fesm2022/ojiepermana-angular-theme.mjs.map +1 -1
  12. package/fesm2022/ojiepermana-angular.mjs +15 -1
  13. package/fesm2022/ojiepermana-angular.mjs.map +1 -1
  14. package/package.json +49 -36
  15. package/theme/styles/etos.css +38 -0
  16. package/theme/styles/index.css +32 -8
  17. package/theme/styles/themes/brand/etos/color.css +21 -0
  18. package/theme/styles/themes/brand/etos/style.css +50 -0
  19. package/theme/styles/themes/library/_components.css +63 -0
  20. package/theme/styles/themes/library/_layers.css +15 -0
  21. package/theme/styles/themes/library/_material-overrides.css +254 -0
  22. package/theme/styles/themes/library/_tokens.css +54 -0
  23. package/theme/styles/themes/library/color/amber.css +18 -0
  24. package/theme/styles/themes/library/color/blue.css +23 -0
  25. package/theme/styles/themes/library/color/green.css +18 -0
  26. package/theme/styles/themes/library/color/index.css +9 -0
  27. package/theme/styles/themes/library/color/purple.css +18 -0
  28. package/theme/styles/themes/library/color/red.css +18 -0
  29. package/theme/styles/themes/library/style/brutal.css +47 -0
  30. package/theme/styles/themes/library/style/default.css +51 -0
  31. package/theme/styles/themes/library/style/index.css +8 -0
  32. package/theme/styles/themes/library/style/sharp.css +47 -0
  33. package/theme/styles/themes/library/style/soft.css +47 -0
  34. package/theme/styles/themes/mode/dark.css +20 -0
  35. package/theme/styles/themes/mode/index.css +6 -0
  36. package/theme/styles/themes/mode/light.css +24 -0
  37. package/theme/styles/themes/taildwind.css +109 -0
  38. package/types/ojiepermana-angular-chart.d.ts +1094 -0
  39. package/types/ojiepermana-angular-component.d.ts +1174 -0
  40. package/types/ojiepermana-angular-layout.d.ts +123 -76
  41. package/types/ojiepermana-angular-navigation.d.ts +256 -116
  42. package/types/ojiepermana-angular-theme.d.ts +170 -1
  43. package/types/ojiepermana-angular.d.ts +2 -1
  44. package/fesm2022/ojiepermana-angular-internal.mjs +0 -489
  45. package/fesm2022/ojiepermana-angular-internal.mjs.map +0 -1
  46. package/fesm2022/ojiepermana-angular-navigation-horizontal.mjs +0 -721
  47. package/fesm2022/ojiepermana-angular-navigation-horizontal.mjs.map +0 -1
  48. package/fesm2022/ojiepermana-angular-navigation-vertical.mjs +0 -1647
  49. package/fesm2022/ojiepermana-angular-navigation-vertical.mjs.map +0 -1
  50. package/fesm2022/ojiepermana-angular-shell.mjs +0 -19
  51. package/fesm2022/ojiepermana-angular-shell.mjs.map +0 -1
  52. package/fesm2022/ojiepermana-angular-theme-component.mjs +0 -235
  53. package/fesm2022/ojiepermana-angular-theme-component.mjs.map +0 -1
  54. package/fesm2022/ojiepermana-angular-theme-directive.mjs +0 -29
  55. package/fesm2022/ojiepermana-angular-theme-directive.mjs.map +0 -1
  56. package/fesm2022/ojiepermana-angular-theme-service.mjs +0 -241
  57. package/fesm2022/ojiepermana-angular-theme-service.mjs.map +0 -1
  58. package/layout/README.md +0 -144
  59. package/layout/src/component/horizontal/horizontal.css +0 -130
  60. package/layout/src/component/vertical/vertical.css +0 -75
  61. package/layout/src/layout.css +0 -16
  62. package/navigation/README.md +0 -301
  63. package/navigation/horizontal/README.md +0 -49
  64. package/shell/README.md +0 -41
  65. package/styles/index.css +0 -2
  66. package/styles/resets.css +0 -22
  67. package/theme/README.md +0 -379
  68. package/theme/styles/adapters/material-ui/index.css +0 -205
  69. package/theme/styles/modes/dark.css +0 -84
  70. package/theme/styles/presets/colors/blue.css +0 -45
  71. package/theme/styles/presets/colors/brand.css +0 -52
  72. package/theme/styles/presets/colors/cyan.css +0 -45
  73. package/theme/styles/presets/colors/green.css +0 -45
  74. package/theme/styles/presets/colors/index.css +0 -7
  75. package/theme/styles/presets/colors/orange.css +0 -45
  76. package/theme/styles/presets/colors/purple.css +0 -45
  77. package/theme/styles/presets/colors/red.css +0 -45
  78. package/theme/styles/presets/styles/flat.css +0 -61
  79. package/theme/styles/presets/styles/glass.css +0 -28
  80. package/theme/styles/presets/styles/index.css +0 -2
  81. package/theme/styles/roles/index.css +0 -67
  82. package/theme/styles/tokens/foundation.css +0 -136
  83. package/theme/styles/tokens/semantic.css +0 -87
  84. package/theme/styles/utilities/index.css +0 -88
  85. package/types/ojiepermana-angular-internal.d.ts +0 -90
  86. package/types/ojiepermana-angular-navigation-horizontal.d.ts +0 -81
  87. package/types/ojiepermana-angular-navigation-vertical.d.ts +0 -262
  88. package/types/ojiepermana-angular-shell.d.ts +0 -14
  89. package/types/ojiepermana-angular-theme-component.d.ts +0 -46
  90. package/types/ojiepermana-angular-theme-directive.d.ts +0 -10
  91. package/types/ojiepermana-angular-theme-service.d.ts +0 -68
  92. /package/{navigation/vertical → chart}/README.md +0 -0
@@ -0,0 +1,3714 @@
1
+ import * as i0 from '@angular/core';
2
+ import { signal, computed, Injectable, inject, ElementRef, Renderer2, effect, ChangeDetectionStrategy, Component, NgZone, PLATFORM_ID, DestroyRef, input, afterNextRender, viewChild, Directive, contentChild, TemplateRef, output } from '@angular/core';
3
+ import { isPlatformBrowser, NgTemplateOutlet, NgComponentOutlet } from '@angular/common';
4
+ import { scaleBand, scaleLinear, scalePoint } from 'd3-scale';
5
+ import { min, max, extent } from 'd3-array';
6
+ import { stack, curveLinear, curveStep, curveMonotoneX, line, area, stackOffsetExpand, pie, arc, curveCardinalClosed, curveLinearClosed, lineRadial } from 'd3-shape';
7
+
8
+ /** CSS selector under which a chart instance is scoped. */
9
+ const CHART_DATA_ATTRIBUTE = 'data-chart';
10
+ /** Default color schemes supported by the generated `<style>` block. */
11
+ const CHART_THEMES = [
12
+ { key: 'light', selector: '' },
13
+ { key: 'dark', selector: '[data-mode="dark"]' },
14
+ ];
15
+ /**
16
+ * Generate the CSS rule-set for a chart instance: one `--color-<key>` per
17
+ * series, scoped to the owning `[data-chart]` element, with optional dark
18
+ * variant via `[data-mode="dark"] [data-chart="…"]`.
19
+ *
20
+ * Series without any color are skipped (consumer can fall back to a default).
21
+ *
22
+ * @param chartId Unique chart id (used as attribute value).
23
+ * @param config Series configuration map.
24
+ */
25
+ function buildChartCss(chartId, config) {
26
+ const entries = Object.entries(config).filter(([, cfg]) => cfg.color || cfg.theme);
27
+ if (entries.length === 0) {
28
+ return '';
29
+ }
30
+ return CHART_THEMES.map(({ key, selector }) => {
31
+ const vars = entries
32
+ .map(([seriesKey, cfg]) => {
33
+ const value = cfg.theme?.[key] ?? cfg.color;
34
+ return value ? ` --color-${escapeCssIdent(seriesKey)}: ${value};` : '';
35
+ })
36
+ .filter(Boolean)
37
+ .join('\n');
38
+ if (!vars) {
39
+ return '';
40
+ }
41
+ const scope = selector
42
+ ? `${selector} [${CHART_DATA_ATTRIBUTE}="${chartId}"]`
43
+ : `[${CHART_DATA_ATTRIBUTE}="${chartId}"]`;
44
+ return `${scope} {\n${vars}\n}`;
45
+ })
46
+ .filter(Boolean)
47
+ .join('\n');
48
+ }
49
+ /**
50
+ * Escape a string so it is safe to use as a CSS custom-property identifier.
51
+ * Allows `[A-Za-z0-9_-]`; everything else becomes `_`.
52
+ */
53
+ function escapeCssIdent(input) {
54
+ return input.replace(/[^a-zA-Z0-9_-]/g, '_');
55
+ }
56
+ /** Resolve the `var(--color-<key>)` reference for a given series. */
57
+ function seriesColorVar(seriesKey) {
58
+ return `var(--color-${seriesKey.replace(/[^a-zA-Z0-9_-]/g, '_')})`;
59
+ }
60
+
61
+ /**
62
+ * Shared chart state provided by `ChartContainer` to all nested chart
63
+ * components (axes, grid, tooltip, legend, chart types).
64
+ *
65
+ * All state is exposed as signals so consumers can derive computed state
66
+ * (scales, visible series, tooltip position) without manual subscriptions.
67
+ */
68
+ class ChartContext {
69
+ /** Stable instance id — used in the `data-chart` attribute and CSS scope. */
70
+ id = signal('', ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
71
+ /** User-provided series config. */
72
+ config = signal({}, ...(ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []));
73
+ /** Measured render-area dimensions (ResizeObserver-driven). */
74
+ dimensions = signal({ width: 0, height: 0 }, ...(ngDevMode ? [{ debugName: "dimensions" }] : /* istanbul ignore next */ []));
75
+ /** Currently highlighted data point (tooltip / crosshair). */
76
+ activePoint = signal(null, ...(ngDevMode ? [{ debugName: "activePoint" }] : /* istanbul ignore next */ []));
77
+ /** Series keys the user has toggled off via legend. */
78
+ hiddenSeries = signal(new Set(), ...(ngDevMode ? [{ debugName: "hiddenSeries" }] : /* istanbul ignore next */ []));
79
+ /** Ordered list of series keys (from `config`). */
80
+ seriesKeys = computed(() => Object.keys(this.config()), ...(ngDevMode ? [{ debugName: "seriesKeys" }] : /* istanbul ignore next */ []));
81
+ /** Series keys currently visible (config order minus `hiddenSeries`). */
82
+ visibleSeriesKeys = computed(() => {
83
+ const hidden = this.hiddenSeries();
84
+ return this.seriesKeys().filter((k) => !hidden.has(k));
85
+ }, ...(ngDevMode ? [{ debugName: "visibleSeriesKeys" }] : /* istanbul ignore next */ []));
86
+ /** Toggle visibility of a series. */
87
+ toggleSeries(key) {
88
+ this.hiddenSeries.update((set) => {
89
+ const next = new Set(set);
90
+ if (next.has(key)) {
91
+ next.delete(key);
92
+ }
93
+ else {
94
+ next.add(key);
95
+ }
96
+ return next;
97
+ });
98
+ }
99
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
100
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartContext });
101
+ }
102
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartContext, decorators: [{
103
+ type: Injectable
104
+ }] });
105
+
106
+ /**
107
+ * Emits a scoped `<style>` block mapping every series key in the current
108
+ * `ChartConfig` to a `--color-<key>` CSS custom property, scoped to
109
+ * `[data-chart="<id>"]`. Dark-mode values are scoped under `[data-mode="dark"]`.
110
+ *
111
+ * Implemented as an empty component that injects a real `<style>` element
112
+ * into its host via `Renderer2`. We avoid rendering `<style>` directly in a
113
+ * template — Angular hoists those into component CSS and strips
114
+ * interpolations from them.
115
+ *
116
+ * Consumers never instantiate this directly — `ChartContainer` renders it.
117
+ */
118
+ class ChartStyle {
119
+ ctx = inject(ChartContext);
120
+ host = inject((ElementRef));
121
+ renderer = inject(Renderer2);
122
+ styleEl = null;
123
+ css = computed(() => buildChartCss(this.ctx.id(), this.ctx.config()), ...(ngDevMode ? [{ debugName: "css" }] : /* istanbul ignore next */ []));
124
+ constructor() {
125
+ effect(() => {
126
+ const css = this.css();
127
+ if (!this.styleEl) {
128
+ this.styleEl = this.renderer.createElement('style');
129
+ this.renderer.appendChild(this.host.nativeElement, this.styleEl);
130
+ }
131
+ this.styleEl.textContent = css;
132
+ });
133
+ }
134
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartStyle, deps: [], target: i0.ɵɵFactoryTarget.Component });
135
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: ChartStyle, isStandalone: true, selector: "ui-chart-style", ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
136
+ }
137
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartStyle, decorators: [{
138
+ type: Component,
139
+ args: [{
140
+ selector: 'ui-chart-style',
141
+ changeDetection: ChangeDetectionStrategy.OnPush,
142
+ template: '',
143
+ }]
144
+ }], ctorParameters: () => [] });
145
+
146
+ let chartIdCounter = 0;
147
+ /**
148
+ * Root of every chart. Provides `ChartContext` to descendants, reflects the
149
+ * chart id via `data-chart`, injects the per-instance CSS-variable
150
+ * `<style>` block, and tracks its render-area dimensions via
151
+ * `ResizeObserver`.
152
+ *
153
+ * Usage:
154
+ * ```html
155
+ * <ui-chart-container [config]="cfg">
156
+ * <ui-bar-chart [data]="data" />
157
+ * </ui-chart-container>
158
+ * ```
159
+ */
160
+ class ChartContainer {
161
+ ctx = inject(ChartContext);
162
+ host = inject((ElementRef));
163
+ zone = inject(NgZone);
164
+ platformId = inject(PLATFORM_ID);
165
+ destroyRef = inject(DestroyRef);
166
+ /** Series configuration. Required for color / label resolution. */
167
+ config = input.required(...(ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []));
168
+ /**
169
+ * Tailwind aspect-ratio utility for the container. Defaults to `aspect-video`
170
+ * for cartesian charts; override with `aspect-square` for radial / pie layouts.
171
+ */
172
+ aspect = input('aspect-video', ...(ngDevMode ? [{ debugName: "aspect" }] : /* istanbul ignore next */ []));
173
+ hostClass = computed(() => `relative flex ${this.aspect()} justify-center text-xs`, ...(ngDevMode ? [{ debugName: "hostClass" }] : /* istanbul ignore next */ []));
174
+ /**
175
+ * Optional explicit id override. When omitted, a stable auto-id is
176
+ * generated (`chart-<n>`), unique across the document.
177
+ */
178
+ chartId = input(null, ...(ngDevMode ? [{ debugName: "chartId" }] : /* istanbul ignore next */ []));
179
+ constructor() {
180
+ const autoId = `chart-${++chartIdCounter}`;
181
+ // Sync id + config into the shared context.
182
+ effect(() => {
183
+ this.ctx.id.set(this.chartId() ?? autoId);
184
+ });
185
+ effect(() => {
186
+ this.ctx.config.set(this.config());
187
+ });
188
+ // Observe host size (browser only; client-only is a confirmed constraint).
189
+ if (isPlatformBrowser(this.platformId)) {
190
+ afterNextRender(() => this.observeSize());
191
+ }
192
+ }
193
+ observeSize() {
194
+ const el = this.host.nativeElement;
195
+ if (typeof ResizeObserver === 'undefined') {
196
+ const rect = el.getBoundingClientRect();
197
+ this.ctx.dimensions.set({ width: rect.width, height: rect.height });
198
+ return;
199
+ }
200
+ const observer = new ResizeObserver((entries) => {
201
+ for (const entry of entries) {
202
+ const { width, height } = entry.contentRect;
203
+ // Avoid NgZone churn — dimensions signal drives CD on its own.
204
+ this.zone.run(() => this.ctx.dimensions.set({ width, height }));
205
+ }
206
+ });
207
+ observer.observe(el);
208
+ this.destroyRef.onDestroy(() => observer.disconnect());
209
+ }
210
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartContainer, deps: [], target: i0.ɵɵFactoryTarget.Component });
211
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: ChartContainer, isStandalone: true, selector: "ui-chart-container", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, aspect: { classPropertyName: "aspect", publicName: "aspect", isSignal: true, isRequired: false, transformFunction: null }, chartId: { classPropertyName: "chartId", publicName: "chartId", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.data-chart": "ctx.id()", "class": "hostClass()" } }, providers: [ChartContext], ngImport: i0, template: `
212
+ <ui-chart-style />
213
+ <ng-content />
214
+ `, isInline: true, dependencies: [{ kind: "component", type: ChartStyle, selector: "ui-chart-style" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
215
+ }
216
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartContainer, decorators: [{
217
+ type: Component,
218
+ args: [{
219
+ selector: 'ui-chart-container',
220
+ changeDetection: ChangeDetectionStrategy.OnPush,
221
+ providers: [ChartContext],
222
+ imports: [ChartStyle],
223
+ host: {
224
+ '[attr.data-chart]': 'ctx.id()',
225
+ '[class]': 'hostClass()',
226
+ },
227
+ template: `
228
+ <ui-chart-style />
229
+ <ng-content />
230
+ `,
231
+ }]
232
+ }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], aspect: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspect", required: false }] }], chartId: [{ type: i0.Input, args: [{ isSignal: true, alias: "chartId", required: false }] }] } });
233
+
234
+ /**
235
+ * Cartesian plotting frame shared between a chart type (Bar, Line, Area…)
236
+ * and its axis / grid primitives.
237
+ *
238
+ * The owning chart component provides an instance and publishes its scales;
239
+ * descendants read them via signals and re-render when dimensions or data
240
+ * change.
241
+ */
242
+ class CartesianContext {
243
+ /** Inner width (outer width − margin.left − margin.right). */
244
+ innerWidth = signal(0, ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
245
+ /** Inner height (outer height − margin.top − margin.bottom). */
246
+ innerHeight = signal(0, ...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
247
+ /** Margins around the plotting area. */
248
+ margin = signal({ top: 8, right: 8, bottom: 24, left: 40 }, ...(ngDevMode ? [{ debugName: "margin" }] : /* istanbul ignore next */ []));
249
+ /** Layout orientation (drives which axis holds the band scale). */
250
+ orientation = signal('vertical', ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
251
+ /** Band (categorical) scale. */
252
+ categoryScale = signal(null, ...(ngDevMode ? [{ debugName: "categoryScale" }] : /* istanbul ignore next */ []));
253
+ /** Linear (numeric) scale. */
254
+ valueScale = signal(null, ...(ngDevMode ? [{ debugName: "valueScale" }] : /* istanbul ignore next */ []));
255
+ /** Ordered category domain (e.g. x labels for vertical, y labels for horizontal). */
256
+ categories = signal([], ...(ngDevMode ? [{ debugName: "categories" }] : /* istanbul ignore next */ []));
257
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CartesianContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
258
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CartesianContext });
259
+ }
260
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CartesianContext, decorators: [{
261
+ type: Injectable
262
+ }] });
263
+ /** Resolve the scale that maps to the X axis for a given orientation. */
264
+ function xScale(ctx) {
265
+ return ctx.orientation() === 'vertical' ? ctx.categoryScale : ctx.valueScale;
266
+ }
267
+ /** Resolve the scale that maps to the Y axis for a given orientation. */
268
+ function yScale(ctx) {
269
+ return ctx.orientation() === 'vertical' ? ctx.valueScale : ctx.categoryScale;
270
+ }
271
+
272
+ /** Produce evenly-spaced ticks for a band (categorical) scale. */
273
+ function bandTicks(scale) {
274
+ const half = scale.bandwidth() / 2;
275
+ return scale.domain().map((value) => ({
276
+ value,
277
+ offset: (scale(value) ?? 0) + half,
278
+ label: value,
279
+ }));
280
+ }
281
+ /**
282
+ * Produce ticks for a linear scale.
283
+ *
284
+ * @param scale The linear scale.
285
+ * @param count Approximate number of ticks (hint, d3 may return fewer/more).
286
+ * @param formatter Label formatter.
287
+ */
288
+ function linearTicks(scale, count = 5, formatter = (v) => String(v)) {
289
+ return scale.ticks(count).map((value) => ({
290
+ value,
291
+ offset: scale(value),
292
+ label: formatter(value),
293
+ }));
294
+ }
295
+
296
+ function clamp$1(value, min, max) {
297
+ return Math.min(max, Math.max(min, value));
298
+ }
299
+ function normalizeIndexRange(start, end, maxCount) {
300
+ if (maxCount <= 0) {
301
+ return null;
302
+ }
303
+ const lo = clamp$1(Math.floor(Math.min(start, end)), 0, maxCount - 1);
304
+ const hi = clamp$1(Math.floor(Math.max(start, end)), 0, maxCount - 1);
305
+ return { startIndex: lo, endIndex: hi };
306
+ }
307
+ function effectiveIndexRange(range, maxCount) {
308
+ if (maxCount <= 0) {
309
+ return null;
310
+ }
311
+ return range
312
+ ? normalizeIndexRange(range.startIndex, range.endIndex, maxCount)
313
+ : { startIndex: 0, endIndex: maxCount - 1 };
314
+ }
315
+ function indexRangeSize(range, maxCount) {
316
+ const effective = effectiveIndexRange(range, maxCount);
317
+ return effective ? effective.endIndex - effective.startIndex + 1 : 0;
318
+ }
319
+ function sliceByIndexRange(data, range) {
320
+ if (!range) {
321
+ return data;
322
+ }
323
+ return data.slice(range.startIndex, range.endIndex + 1);
324
+ }
325
+ function zoomIndexRange(current, maxCount, anchorIndex, factor) {
326
+ const base = effectiveIndexRange(current, maxCount);
327
+ if (!base) {
328
+ return null;
329
+ }
330
+ const currentSize = base.endIndex - base.startIndex + 1;
331
+ const nextSize = clamp$1(Math.round(currentSize * factor), 2, maxCount);
332
+ if (nextSize >= maxCount) {
333
+ return null;
334
+ }
335
+ const boundedAnchor = clamp$1(anchorIndex, base.startIndex, base.endIndex);
336
+ const ratio = currentSize <= 1 ? 0.5 : (boundedAnchor - base.startIndex) / (currentSize - 1);
337
+ let start = Math.round(boundedAnchor - ratio * (nextSize - 1));
338
+ start = clamp$1(start, 0, maxCount - nextSize);
339
+ return { startIndex: start, endIndex: start + nextSize - 1 };
340
+ }
341
+ function panIndexRange(current, maxCount, deltaSteps) {
342
+ const base = effectiveIndexRange(current, maxCount);
343
+ if (!base) {
344
+ return null;
345
+ }
346
+ const size = base.endIndex - base.startIndex + 1;
347
+ if (size >= maxCount) {
348
+ return null;
349
+ }
350
+ const start = clamp$1(base.startIndex + deltaSteps, 0, maxCount - size);
351
+ return { startIndex: start, endIndex: start + size - 1 };
352
+ }
353
+ function normalizeNumericDomain(a, b) {
354
+ if (a === b) {
355
+ return [a - 1, b + 1];
356
+ }
357
+ return a < b ? [a, b] : [b, a];
358
+ }
359
+ function zoomNumericDomain(current, full, anchor, factor) {
360
+ const currentWidth = current[1] - current[0];
361
+ const fullWidth = full[1] - full[0];
362
+ const nextWidth = clamp$1(currentWidth * factor, fullWidth / 50, fullWidth);
363
+ if (nextWidth >= fullWidth) {
364
+ return full;
365
+ }
366
+ const ratio = currentWidth === 0 ? 0.5 : (anchor - current[0]) / currentWidth;
367
+ let start = anchor - ratio * nextWidth;
368
+ start = clamp$1(start, full[0], full[1] - nextWidth);
369
+ return [start, start + nextWidth];
370
+ }
371
+ function panNumericDomain(current, full, delta) {
372
+ const width = current[1] - current[0];
373
+ const start = clamp$1(current[0] + delta, full[0], full[1] - width);
374
+ return [start, start + width];
375
+ }
376
+
377
+ /**
378
+ * X axis for a cartesian chart. Reads scales from `CartesianContext`.
379
+ *
380
+ * Renders as `<svg:g>` — must be placed inside the owning chart's SVG.
381
+ */
382
+ class ChartAxisX {
383
+ ctx = inject(CartesianContext);
384
+ /** Approximate tick count for linear (value) scales. */
385
+ tickCount = input(5, ...(ngDevMode ? [{ debugName: "tickCount" }] : /* istanbul ignore next */ []));
386
+ /** Show 6-px tick marks between the axis line and the labels. */
387
+ tickLine = input(true, ...(ngDevMode ? [{ debugName: "tickLine" }] : /* istanbul ignore next */ []));
388
+ /** Formatter for numeric tick labels. */
389
+ tickFormat = input((v) => String(v), ...(ngDevMode ? [{ debugName: "tickFormat" }] : /* istanbul ignore next */ []));
390
+ innerWidth = this.ctx.innerWidth;
391
+ transform = computed(() => `translate(0,${this.ctx.innerHeight()})`, ...(ngDevMode ? [{ debugName: "transform" }] : /* istanbul ignore next */ []));
392
+ ticks = computed(() => {
393
+ const horizontal = this.ctx.orientation() === 'horizontal';
394
+ if (horizontal) {
395
+ const scale = this.ctx.valueScale();
396
+ return scale ? linearTicks(scale, this.tickCount(), this.tickFormat()) : [];
397
+ }
398
+ const scale = this.ctx.categoryScale();
399
+ return scale ? bandTicks(scale) : [];
400
+ }, ...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
401
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartAxisX, deps: [], target: i0.ɵɵFactoryTarget.Component });
402
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ChartAxisX, isStandalone: true, selector: "svg:g[ui-chart-axis-x]", inputs: { tickCount: { classPropertyName: "tickCount", publicName: "tickCount", isSignal: true, isRequired: false, transformFunction: null }, tickLine: { classPropertyName: "tickLine", publicName: "tickLine", isSignal: true, isRequired: false, transformFunction: null }, tickFormat: { classPropertyName: "tickFormat", publicName: "tickFormat", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.transform": "transform()" }, classAttribute: "chart-axis chart-axis-x text-muted-foreground" }, ngImport: i0, template: `
403
+ <svg:line class="stroke-border" [attr.x1]="0" [attr.x2]="innerWidth()" y1="0" y2="0" />
404
+ @for (t of ticks(); track t.value) {
405
+ <svg:g [attr.transform]="'translate(' + t.offset + ',0)'">
406
+ @if (tickLine()) {
407
+ <svg:line class="stroke-border" y1="0" y2="6" />
408
+ }
409
+ <svg:text
410
+ class="fill-current"
411
+ y="18"
412
+ text-anchor="middle"
413
+ style="font-size: var(--text-xs); font-family: var(--font-sans)">
414
+ {{ t.label }}
415
+ </svg:text>
416
+ </svg:g>
417
+ }
418
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
419
+ }
420
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartAxisX, decorators: [{
421
+ type: Component,
422
+ args: [{
423
+ selector: 'svg:g[ui-chart-axis-x]',
424
+ changeDetection: ChangeDetectionStrategy.OnPush,
425
+ host: {
426
+ class: 'chart-axis chart-axis-x text-muted-foreground',
427
+ '[attr.transform]': 'transform()',
428
+ },
429
+ template: `
430
+ <svg:line class="stroke-border" [attr.x1]="0" [attr.x2]="innerWidth()" y1="0" y2="0" />
431
+ @for (t of ticks(); track t.value) {
432
+ <svg:g [attr.transform]="'translate(' + t.offset + ',0)'">
433
+ @if (tickLine()) {
434
+ <svg:line class="stroke-border" y1="0" y2="6" />
435
+ }
436
+ <svg:text
437
+ class="fill-current"
438
+ y="18"
439
+ text-anchor="middle"
440
+ style="font-size: var(--text-xs); font-family: var(--font-sans)">
441
+ {{ t.label }}
442
+ </svg:text>
443
+ </svg:g>
444
+ }
445
+ `,
446
+ }]
447
+ }], propDecorators: { tickCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "tickCount", required: false }] }], tickLine: [{ type: i0.Input, args: [{ isSignal: true, alias: "tickLine", required: false }] }], tickFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "tickFormat", required: false }] }] } });
448
+
449
+ /**
450
+ * Y axis for a cartesian chart. Reads scales from `CartesianContext`.
451
+ *
452
+ * Renders as `<svg:g>` — must be placed inside the owning chart's SVG.
453
+ */
454
+ class ChartAxisY {
455
+ ctx = inject(CartesianContext);
456
+ tickCount = input(5, ...(ngDevMode ? [{ debugName: "tickCount" }] : /* istanbul ignore next */ []));
457
+ tickLine = input(true, ...(ngDevMode ? [{ debugName: "tickLine" }] : /* istanbul ignore next */ []));
458
+ tickFormat = input((v) => String(v), ...(ngDevMode ? [{ debugName: "tickFormat" }] : /* istanbul ignore next */ []));
459
+ innerHeight = this.ctx.innerHeight;
460
+ ticks = computed(() => {
461
+ const horizontal = this.ctx.orientation() === 'horizontal';
462
+ if (horizontal) {
463
+ const scale = this.ctx.categoryScale();
464
+ return scale ? bandTicks(scale) : [];
465
+ }
466
+ const scale = this.ctx.valueScale();
467
+ return scale ? linearTicks(scale, this.tickCount(), this.tickFormat()) : [];
468
+ }, ...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
469
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartAxisY, deps: [], target: i0.ɵɵFactoryTarget.Component });
470
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ChartAxisY, isStandalone: true, selector: "svg:g[ui-chart-axis-y]", inputs: { tickCount: { classPropertyName: "tickCount", publicName: "tickCount", isSignal: true, isRequired: false, transformFunction: null }, tickLine: { classPropertyName: "tickLine", publicName: "tickLine", isSignal: true, isRequired: false, transformFunction: null }, tickFormat: { classPropertyName: "tickFormat", publicName: "tickFormat", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "chart-axis chart-axis-y text-muted-foreground" }, ngImport: i0, template: `
471
+ <svg:line class="stroke-border" x1="0" x2="0" [attr.y1]="0" [attr.y2]="innerHeight()" />
472
+ @for (t of ticks(); track t.value) {
473
+ <svg:g [attr.transform]="'translate(0,' + t.offset + ')'">
474
+ @if (tickLine()) {
475
+ <svg:line class="stroke-border" x1="-6" x2="0" />
476
+ }
477
+ <svg:text
478
+ class="fill-current"
479
+ x="-8"
480
+ dy="0.32em"
481
+ text-anchor="end"
482
+ style="font-size: var(--text-xs); font-family: var(--font-sans)">
483
+ {{ t.label }}
484
+ </svg:text>
485
+ </svg:g>
486
+ }
487
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
488
+ }
489
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartAxisY, decorators: [{
490
+ type: Component,
491
+ args: [{
492
+ selector: 'svg:g[ui-chart-axis-y]',
493
+ changeDetection: ChangeDetectionStrategy.OnPush,
494
+ host: {
495
+ class: 'chart-axis chart-axis-y text-muted-foreground',
496
+ },
497
+ template: `
498
+ <svg:line class="stroke-border" x1="0" x2="0" [attr.y1]="0" [attr.y2]="innerHeight()" />
499
+ @for (t of ticks(); track t.value) {
500
+ <svg:g [attr.transform]="'translate(0,' + t.offset + ')'">
501
+ @if (tickLine()) {
502
+ <svg:line class="stroke-border" x1="-6" x2="0" />
503
+ }
504
+ <svg:text
505
+ class="fill-current"
506
+ x="-8"
507
+ dy="0.32em"
508
+ text-anchor="end"
509
+ style="font-size: var(--text-xs); font-family: var(--font-sans)">
510
+ {{ t.label }}
511
+ </svg:text>
512
+ </svg:g>
513
+ }
514
+ `,
515
+ }]
516
+ }], propDecorators: { tickCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "tickCount", required: false }] }], tickLine: [{ type: i0.Input, args: [{ isSignal: true, alias: "tickLine", required: false }] }], tickFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "tickFormat", required: false }] }] } });
517
+
518
+ /**
519
+ * Horizontal / vertical grid lines for the cartesian plotting area.
520
+ *
521
+ * Reads tick positions from `CartesianContext.valueScale`. Direction of the
522
+ * grid lines follows `orientation`:
523
+ * - vertical → horizontal grid lines (one per y-tick)
524
+ * - horizontal → vertical grid lines (one per x-tick)
525
+ */
526
+ class ChartGrid {
527
+ ctx = inject(CartesianContext);
528
+ tickCount = input(5, ...(ngDevMode ? [{ debugName: "tickCount" }] : /* istanbul ignore next */ []));
529
+ ticks = computed(() => {
530
+ const scale = this.ctx.valueScale();
531
+ return scale ? linearTicks(scale, this.tickCount()) : [];
532
+ }, ...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
533
+ line = (offset) => {
534
+ if (this.ctx.orientation() === 'vertical') {
535
+ return { x1: 0, x2: this.ctx.innerWidth(), y1: offset, y2: offset };
536
+ }
537
+ return { x1: offset, x2: offset, y1: 0, y2: this.ctx.innerHeight() };
538
+ };
539
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartGrid, deps: [], target: i0.ɵɵFactoryTarget.Component });
540
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ChartGrid, isStandalone: true, selector: "svg:g[ui-chart-grid]", inputs: { tickCount: { classPropertyName: "tickCount", publicName: "tickCount", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "chart-grid text-border" }, ngImport: i0, template: `
541
+ @for (t of ticks(); track t.value) {
542
+ <svg:line
543
+ class="stroke-border"
544
+ stroke-dasharray="3 3"
545
+ [attr.x1]="line(t.offset).x1"
546
+ [attr.x2]="line(t.offset).x2"
547
+ [attr.y1]="line(t.offset).y1"
548
+ [attr.y2]="line(t.offset).y2" />
549
+ }
550
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
551
+ }
552
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartGrid, decorators: [{
553
+ type: Component,
554
+ args: [{
555
+ selector: 'svg:g[ui-chart-grid]',
556
+ changeDetection: ChangeDetectionStrategy.OnPush,
557
+ host: {
558
+ class: 'chart-grid text-border',
559
+ },
560
+ template: `
561
+ @for (t of ticks(); track t.value) {
562
+ <svg:line
563
+ class="stroke-border"
564
+ stroke-dasharray="3 3"
565
+ [attr.x1]="line(t.offset).x1"
566
+ [attr.x2]="line(t.offset).x2"
567
+ [attr.y1]="line(t.offset).y1"
568
+ [attr.y2]="line(t.offset).y2" />
569
+ }
570
+ `,
571
+ }]
572
+ }], propDecorators: { tickCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "tickCount", required: false }] }] } });
573
+
574
+ /**
575
+ * Crosshair primitive — a line drawn through the currently active data point
576
+ * perpendicular to the categorical axis. Reads `activePoint` from
577
+ * `ChartContext` and the scales from `CartesianContext`.
578
+ *
579
+ * Place inside a cartesian chart's SVG inner group.
580
+ */
581
+ class ChartCrosshair {
582
+ root = inject(ChartContext);
583
+ cart = inject(CartesianContext);
584
+ line = computed(() => {
585
+ const active = this.root.activePoint();
586
+ const scale = this.cart.categoryScale();
587
+ const categories = this.cart.categories();
588
+ if (!active || !scale || active.index < 0 || active.index >= categories.length) {
589
+ return null;
590
+ }
591
+ const base = scale(categories[active.index]) ?? 0;
592
+ const pos = base + scale.bandwidth() / 2;
593
+ if (this.cart.orientation() === 'vertical') {
594
+ return { x1: pos, x2: pos, y1: 0, y2: this.cart.innerHeight() };
595
+ }
596
+ return { x1: 0, x2: this.cart.innerWidth(), y1: pos, y2: pos };
597
+ }, ...(ngDevMode ? [{ debugName: "line" }] : /* istanbul ignore next */ []));
598
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartCrosshair, deps: [], target: i0.ɵɵFactoryTarget.Component });
599
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ChartCrosshair, isStandalone: true, selector: "svg:g[ui-chart-crosshair]", host: { classAttribute: "chart-crosshair" }, ngImport: i0, template: `
600
+ @if (line(); as l) {
601
+ <svg:line
602
+ class="stroke-border"
603
+ stroke-dasharray="3 3"
604
+ [attr.x1]="l.x1"
605
+ [attr.x2]="l.x2"
606
+ [attr.y1]="l.y1"
607
+ [attr.y2]="l.y2" />
608
+ }
609
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
610
+ }
611
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartCrosshair, decorators: [{
612
+ type: Component,
613
+ args: [{
614
+ selector: 'svg:g[ui-chart-crosshair]',
615
+ changeDetection: ChangeDetectionStrategy.OnPush,
616
+ host: { class: 'chart-crosshair' },
617
+ template: `
618
+ @if (line(); as l) {
619
+ <svg:line
620
+ class="stroke-border"
621
+ stroke-dasharray="3 3"
622
+ [attr.x1]="l.x1"
623
+ [attr.x2]="l.x2"
624
+ [attr.y1]="l.y1"
625
+ [attr.y2]="l.y2" />
626
+ }
627
+ `,
628
+ }]
629
+ }] });
630
+
631
+ class CategoricalViewportContext {
632
+ dataCount = signal(0, ...(ngDevMode ? [{ debugName: "dataCount" }] : /* istanbul ignore next */ []));
633
+ brushRange = signal(null, ...(ngDevMode ? [{ debugName: "brushRange" }] : /* istanbul ignore next */ []));
634
+ zoomRange = signal(null, ...(ngDevMode ? [{ debugName: "zoomRange" }] : /* istanbul ignore next */ []));
635
+ hasZoom = computed(() => {
636
+ const range = this.zoomRange();
637
+ const count = this.dataCount();
638
+ return !!range && count > 0 && (range.startIndex > 0 || range.endIndex < count - 1);
639
+ }, ...(ngDevMode ? [{ debugName: "hasZoom" }] : /* istanbul ignore next */ []));
640
+ resetZoom() {
641
+ this.brushRange.set(null);
642
+ this.zoomRange.set(null);
643
+ }
644
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CategoricalViewportContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
645
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CategoricalViewportContext });
646
+ }
647
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CategoricalViewportContext, decorators: [{
648
+ type: Injectable
649
+ }] });
650
+
651
+ class ScatterViewportContext {
652
+ innerWidth = signal(0, ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
653
+ innerHeight = signal(0, ...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
654
+ fullXDomain = signal(null, ...(ngDevMode ? [{ debugName: "fullXDomain" }] : /* istanbul ignore next */ []));
655
+ fullYDomain = signal(null, ...(ngDevMode ? [{ debugName: "fullYDomain" }] : /* istanbul ignore next */ []));
656
+ zoomXDomain = signal(null, ...(ngDevMode ? [{ debugName: "zoomXDomain" }] : /* istanbul ignore next */ []));
657
+ zoomYDomain = signal(null, ...(ngDevMode ? [{ debugName: "zoomYDomain" }] : /* istanbul ignore next */ []));
658
+ xScale = signal(null, ...(ngDevMode ? [{ debugName: "xScale" }] : /* istanbul ignore next */ []));
659
+ yScale = signal(null, ...(ngDevMode ? [{ debugName: "yScale" }] : /* istanbul ignore next */ []));
660
+ hasZoom = computed(() => this.zoomXDomain() !== null || this.zoomYDomain() !== null, ...(ngDevMode ? [{ debugName: "hasZoom" }] : /* istanbul ignore next */ []));
661
+ resetZoom() {
662
+ this.zoomXDomain.set(null);
663
+ this.zoomYDomain.set(null);
664
+ }
665
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ScatterViewportContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
666
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ScatterViewportContext });
667
+ }
668
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ScatterViewportContext, decorators: [{
669
+ type: Injectable
670
+ }] });
671
+
672
+ /**
673
+ * Given a pointer event's local (x, y) relative to the chart's inner group,
674
+ * resolve the nearest category index along the categorical axis.
675
+ *
676
+ * @returns index into `ctx.categories()` (or −1 if no scale / no data).
677
+ */
678
+ function nearestCategoryIndex(ctx, localX, localY) {
679
+ const scale = ctx.categoryScale();
680
+ const categories = ctx.categories();
681
+ if (!scale || categories.length === 0)
682
+ return -1;
683
+ const isVertical = ctx.orientation() === 'vertical';
684
+ const pointerAlong = isVertical ? localX : localY;
685
+ const bandwidth = scale.bandwidth();
686
+ // scale.step() is only defined for band scales; fall back to bandwidth.
687
+ const step = scale.step?.() ?? bandwidth;
688
+ let bestIndex = -1;
689
+ let bestDelta = Infinity;
690
+ for (let i = 0; i < categories.length; i++) {
691
+ const base = scale(categories[i]) ?? 0;
692
+ const center = base + bandwidth / 2;
693
+ const delta = Math.abs(pointerAlong - center);
694
+ if (delta < bestDelta) {
695
+ bestDelta = delta;
696
+ bestIndex = i;
697
+ }
698
+ }
699
+ // Ignore clicks far outside any band (> 1 step away).
700
+ if (bestDelta > step) {
701
+ return -1;
702
+ }
703
+ return bestIndex;
704
+ }
705
+ /** Resolve the client-space center point of a focused or clicked SVG/HTML element. */
706
+ function elementClientCenter(target) {
707
+ const el = target;
708
+ if (!el || typeof el.getBoundingClientRect !== 'function') {
709
+ return null;
710
+ }
711
+ const rect = el.getBoundingClientRect();
712
+ return {
713
+ clientX: rect.left + rect.width / 2,
714
+ clientY: rect.top + rect.height / 2,
715
+ };
716
+ }
717
+
718
+ function clamp(value, min, max) {
719
+ return Math.min(max, Math.max(min, value));
720
+ }
721
+ function sameDomain(a, b) {
722
+ return Math.abs(a[0] - b[0]) < 1e-9 && Math.abs(a[1] - b[1]) < 1e-9;
723
+ }
724
+ class ChartBrush {
725
+ hitbox = viewChild.required('hitbox');
726
+ cart = inject(CartesianContext, { optional: true });
727
+ categorical = inject(CategoricalViewportContext, { optional: true });
728
+ scatter = inject(ScatterViewportContext, { optional: true });
729
+ mode = signal(null, ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
730
+ activePointerId = signal(null, ...(ngDevMode ? [{ debugName: "activePointerId" }] : /* istanbul ignore next */ []));
731
+ categoryStartIndex = signal(null, ...(ngDevMode ? [{ debugName: "categoryStartIndex" }] : /* istanbul ignore next */ []));
732
+ categoryPanStart = signal(null, ...(ngDevMode ? [{ debugName: "categoryPanStart" }] : /* istanbul ignore next */ []));
733
+ scatterBrush = signal(null, ...(ngDevMode ? [{ debugName: "scatterBrush" }] : /* istanbul ignore next */ []));
734
+ scatterPan = signal(null, ...(ngDevMode ? [{ debugName: "scatterPan" }] : /* istanbul ignore next */ []));
735
+ width = computed(() => this.scatter?.innerWidth() ?? this.cart?.innerWidth() ?? 0, ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
736
+ height = computed(() => this.scatter?.innerHeight() ?? this.cart?.innerHeight() ?? 0, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
737
+ categoryPreview = computed(() => {
738
+ const cart = this.cart;
739
+ const viewport = this.categorical;
740
+ const range = viewport?.brushRange();
741
+ const scale = cart?.categoryScale();
742
+ const categories = cart?.categories() ?? [];
743
+ if (!cart || !viewport || !range || !scale || categories.length === 0) {
744
+ return null;
745
+ }
746
+ const visibleStart = viewport.zoomRange()?.startIndex ?? 0;
747
+ const startVisible = range.startIndex - visibleStart;
748
+ const endVisible = range.endIndex - visibleStart;
749
+ if (startVisible < 0 || endVisible >= categories.length) {
750
+ return null;
751
+ }
752
+ const first = scale(categories[startVisible]) ?? 0;
753
+ const last = (scale(categories[endVisible]) ?? 0) + scale.bandwidth();
754
+ if (cart.orientation() === 'vertical') {
755
+ return { x: Math.min(first, last), y: 0, width: Math.abs(last - first), height: cart.innerHeight() };
756
+ }
757
+ return { x: 0, y: Math.min(first, last), width: cart.innerWidth(), height: Math.abs(last - first) };
758
+ }, ...(ngDevMode ? [{ debugName: "categoryPreview" }] : /* istanbul ignore next */ []));
759
+ scatterPreviewRect = computed(() => {
760
+ const preview = this.scatterBrush();
761
+ if (!preview) {
762
+ return null;
763
+ }
764
+ return {
765
+ x: Math.min(preview.start.x, preview.current.x),
766
+ y: Math.min(preview.start.y, preview.current.y),
767
+ width: Math.abs(preview.current.x - preview.start.x),
768
+ height: Math.abs(preview.current.y - preview.start.y),
769
+ };
770
+ }, ...(ngDevMode ? [{ debugName: "scatterPreviewRect" }] : /* istanbul ignore next */ []));
771
+ onPointerDown(event) {
772
+ if (this.activePointerId() != null) {
773
+ return;
774
+ }
775
+ const local = this.localPoint(event);
776
+ if (!local) {
777
+ return;
778
+ }
779
+ if (this.scatter) {
780
+ if (!this.scatter.fullXDomain() || !this.scatter.fullYDomain()) {
781
+ return;
782
+ }
783
+ event.preventDefault();
784
+ this.activePointerId.set(event.pointerId);
785
+ this.hitbox().nativeElement.setPointerCapture(event.pointerId);
786
+ this.onScatterPointerDown(event, local);
787
+ return;
788
+ }
789
+ if (!this.cart || !this.categorical) {
790
+ return;
791
+ }
792
+ const index = nearestCategoryIndex({
793
+ categoryScale: this.cart.categoryScale,
794
+ categories: this.cart.categories,
795
+ orientation: this.cart.orientation,
796
+ }, local.x, local.y);
797
+ if (index < 0) {
798
+ return;
799
+ }
800
+ event.preventDefault();
801
+ this.activePointerId.set(event.pointerId);
802
+ this.hitbox().nativeElement.setPointerCapture(event.pointerId);
803
+ const visibleStart = this.categorical.zoomRange()?.startIndex ?? 0;
804
+ const absoluteIndex = visibleStart + index;
805
+ if (event.pointerType === 'touch' && this.categorical.hasZoom()) {
806
+ this.mode.set('category-pan');
807
+ this.categoryPanStart.set({ coord: this.pointerAxis(local), range: this.categorical.zoomRange() });
808
+ return;
809
+ }
810
+ this.mode.set('category-brush');
811
+ this.categoryStartIndex.set(absoluteIndex);
812
+ this.categorical.brushRange.set({ startIndex: absoluteIndex, endIndex: absoluteIndex });
813
+ }
814
+ onPointerMove(event) {
815
+ if (this.activePointerId() !== event.pointerId) {
816
+ return;
817
+ }
818
+ const local = this.localPoint(event);
819
+ if (!local) {
820
+ return;
821
+ }
822
+ if (this.mode() === 'category-brush' && this.cart && this.categorical) {
823
+ const startIndex = this.categoryStartIndex();
824
+ if (startIndex == null) {
825
+ return;
826
+ }
827
+ const index = nearestCategoryIndex({
828
+ categoryScale: this.cart.categoryScale,
829
+ categories: this.cart.categories,
830
+ orientation: this.cart.orientation,
831
+ }, local.x, local.y);
832
+ if (index < 0) {
833
+ return;
834
+ }
835
+ const visibleStart = this.categorical.zoomRange()?.startIndex ?? 0;
836
+ this.categorical.brushRange.set(normalizeIndexRange(startIndex, visibleStart + index, this.categorical.dataCount()));
837
+ return;
838
+ }
839
+ if (this.mode() === 'category-pan' && this.cart && this.categorical) {
840
+ const pan = this.categoryPanStart();
841
+ if (!pan) {
842
+ return;
843
+ }
844
+ const scale = this.cart.categoryScale();
845
+ const step = scale?.step?.() ?? scale?.bandwidth() ?? 0;
846
+ if (step <= 0) {
847
+ return;
848
+ }
849
+ const deltaSteps = Math.round((pan.coord - this.pointerAxis(local)) / step);
850
+ this.categorical.zoomRange.set(panIndexRange(pan.range, this.categorical.dataCount(), deltaSteps));
851
+ return;
852
+ }
853
+ if (this.mode() === 'scatter-brush') {
854
+ const preview = this.scatterBrush();
855
+ if (!preview) {
856
+ return;
857
+ }
858
+ this.scatterBrush.set({ start: preview.start, current: local });
859
+ return;
860
+ }
861
+ if (this.mode() === 'scatter-pan' && this.scatter) {
862
+ const pan = this.scatterPan();
863
+ const xScale = this.scatter.xScale();
864
+ const yScale = this.scatter.yScale();
865
+ const fullX = this.scatter.fullXDomain();
866
+ const fullY = this.scatter.fullYDomain();
867
+ if (!pan || !xScale || !yScale || !fullX || !fullY) {
868
+ return;
869
+ }
870
+ const deltaX = xScale.invert(pan.start.x) - xScale.invert(local.x);
871
+ const deltaY = yScale.invert(pan.start.y) - yScale.invert(local.y);
872
+ this.scatter.zoomXDomain.set(panNumericDomain(pan.xDomain, fullX, deltaX));
873
+ this.scatter.zoomYDomain.set(panNumericDomain(pan.yDomain, fullY, deltaY));
874
+ }
875
+ }
876
+ onPointerUp(event) {
877
+ if (event && this.activePointerId() !== event.pointerId) {
878
+ return;
879
+ }
880
+ if (this.mode() === 'category-brush' && this.categorical) {
881
+ const range = this.categorical.brushRange();
882
+ if (range && indexRangeSize(range, this.categorical.dataCount()) > 1) {
883
+ this.categorical.zoomRange.set(range);
884
+ }
885
+ this.categorical.brushRange.set(null);
886
+ }
887
+ if (this.mode() === 'scatter-brush' && this.scatter) {
888
+ const preview = this.scatterBrush();
889
+ const xScale = this.scatter.xScale();
890
+ const yScale = this.scatter.yScale();
891
+ const fullX = this.scatter.fullXDomain();
892
+ const fullY = this.scatter.fullYDomain();
893
+ if (preview && xScale && yScale && fullX && fullY) {
894
+ const width = Math.abs(preview.current.x - preview.start.x);
895
+ const height = Math.abs(preview.current.y - preview.start.y);
896
+ if (width >= 6 && height >= 6) {
897
+ const nextX = normalizeNumericDomain(xScale.invert(preview.start.x), xScale.invert(preview.current.x));
898
+ const nextY = normalizeNumericDomain(yScale.invert(preview.start.y), yScale.invert(preview.current.y));
899
+ this.scatter.zoomXDomain.set(sameDomain(nextX, fullX) ? null : nextX);
900
+ this.scatter.zoomYDomain.set(sameDomain(nextY, fullY) ? null : nextY);
901
+ }
902
+ }
903
+ this.scatterBrush.set(null);
904
+ }
905
+ this.mode.set(null);
906
+ this.categoryStartIndex.set(null);
907
+ this.categoryPanStart.set(null);
908
+ this.scatterPan.set(null);
909
+ if (event && this.hitbox().nativeElement.hasPointerCapture(event.pointerId)) {
910
+ this.hitbox().nativeElement.releasePointerCapture(event.pointerId);
911
+ }
912
+ this.activePointerId.set(null);
913
+ }
914
+ onPointerCancel(event) {
915
+ if (this.activePointerId() !== event.pointerId) {
916
+ return;
917
+ }
918
+ if (this.hitbox().nativeElement.hasPointerCapture(event.pointerId)) {
919
+ this.hitbox().nativeElement.releasePointerCapture(event.pointerId);
920
+ }
921
+ this.mode.set(null);
922
+ this.categorical?.brushRange.set(null);
923
+ this.scatterBrush.set(null);
924
+ this.categoryStartIndex.set(null);
925
+ this.categoryPanStart.set(null);
926
+ this.scatterPan.set(null);
927
+ this.activePointerId.set(null);
928
+ }
929
+ onWheel(event) {
930
+ const local = this.localPoint(event);
931
+ if (!local) {
932
+ return;
933
+ }
934
+ event.preventDefault();
935
+ if (this.scatter) {
936
+ const xScale = this.scatter.xScale();
937
+ const yScale = this.scatter.yScale();
938
+ const fullX = this.scatter.fullXDomain();
939
+ const fullY = this.scatter.fullYDomain();
940
+ if (!xScale || !yScale || !fullX || !fullY) {
941
+ return;
942
+ }
943
+ const factor = event.deltaY < 0 ? 0.8 : 1.25;
944
+ const currentX = this.scatter.zoomXDomain() ?? fullX;
945
+ const currentY = this.scatter.zoomYDomain() ?? fullY;
946
+ const nextX = zoomNumericDomain(currentX, fullX, xScale.invert(local.x), factor);
947
+ const nextY = zoomNumericDomain(currentY, fullY, yScale.invert(local.y), factor);
948
+ this.scatter.zoomXDomain.set(sameDomain(nextX, fullX) ? null : nextX);
949
+ this.scatter.zoomYDomain.set(sameDomain(nextY, fullY) ? null : nextY);
950
+ return;
951
+ }
952
+ if (!this.cart || !this.categorical) {
953
+ return;
954
+ }
955
+ const visibleIndex = nearestCategoryIndex({
956
+ categoryScale: this.cart.categoryScale,
957
+ categories: this.cart.categories,
958
+ orientation: this.cart.orientation,
959
+ }, local.x, local.y);
960
+ if (visibleIndex < 0) {
961
+ return;
962
+ }
963
+ const anchor = (this.categorical.zoomRange()?.startIndex ?? 0) + visibleIndex;
964
+ const factor = event.deltaY < 0 ? 0.75 : 1.25;
965
+ this.categorical.zoomRange.set(zoomIndexRange(this.categorical.zoomRange(), this.categorical.dataCount(), anchor, factor));
966
+ this.categorical.brushRange.set(null);
967
+ }
968
+ resetZoom() {
969
+ this.categorical?.resetZoom();
970
+ this.scatter?.resetZoom();
971
+ this.scatterBrush.set(null);
972
+ }
973
+ onScatterPointerDown(event, local) {
974
+ const fullX = this.scatter?.fullXDomain();
975
+ const fullY = this.scatter?.fullYDomain();
976
+ if (!this.scatter || !fullX || !fullY) {
977
+ return;
978
+ }
979
+ if (event.pointerType === 'touch' && this.scatter.hasZoom()) {
980
+ this.mode.set('scatter-pan');
981
+ this.scatterPan.set({
982
+ start: local,
983
+ xDomain: this.scatter.zoomXDomain() ?? fullX,
984
+ yDomain: this.scatter.zoomYDomain() ?? fullY,
985
+ });
986
+ return;
987
+ }
988
+ this.mode.set('scatter-brush');
989
+ this.scatterBrush.set({ start: local, current: local });
990
+ }
991
+ localPoint(event) {
992
+ const hitbox = this.hitbox()?.nativeElement;
993
+ const width = this.width();
994
+ const height = this.height();
995
+ if (!hitbox || width <= 0 || height <= 0) {
996
+ return null;
997
+ }
998
+ const rect = hitbox.getBoundingClientRect();
999
+ if (rect.width <= 0 || rect.height <= 0) {
1000
+ return null;
1001
+ }
1002
+ return {
1003
+ x: clamp(((event.clientX - rect.left) / rect.width) * width, 0, width),
1004
+ y: clamp(((event.clientY - rect.top) / rect.height) * height, 0, height),
1005
+ };
1006
+ }
1007
+ pointerAxis(local) {
1008
+ return this.cart?.orientation() === 'horizontal' ? local.y : local.x;
1009
+ }
1010
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartBrush, deps: [], target: i0.ɵɵFactoryTarget.Component });
1011
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ChartBrush, isStandalone: true, selector: "svg:g[ui-chart-brush]", host: { listeners: { "window:pointermove": "onPointerMove($event)", "window:pointerup": "onPointerUp($event)", "window:pointercancel": "onPointerCancel($event)" }, classAttribute: "chart-brush" }, viewQueries: [{ propertyName: "hitbox", first: true, predicate: ["hitbox"], descendants: true, isSignal: true }], ngImport: i0, template: `
1012
+ <svg:rect
1013
+ #hitbox
1014
+ class="fill-transparent touch-none"
1015
+ x="0"
1016
+ y="0"
1017
+ [attr.width]="width()"
1018
+ [attr.height]="height()"
1019
+ (pointerdown)="onPointerDown($event)"
1020
+ (pointermove)="onPointerMove($event)"
1021
+ (pointerup)="onPointerUp($event)"
1022
+ (pointercancel)="onPointerCancel($event)"
1023
+ (wheel)="onWheel($event)"
1024
+ (dblclick)="resetZoom()" />
1025
+
1026
+ @if (categoryPreview(); as rect) {
1027
+ <svg:rect
1028
+ class="fill-foreground/10 stroke-foreground/30"
1029
+ [attr.x]="rect.x"
1030
+ [attr.y]="rect.y"
1031
+ [attr.width]="rect.width"
1032
+ [attr.height]="rect.height"
1033
+ stroke-dasharray="4 3" />
1034
+ }
1035
+
1036
+ @if (scatterPreviewRect(); as rect) {
1037
+ <svg:rect
1038
+ class="fill-foreground/10 stroke-foreground/30"
1039
+ [attr.x]="rect.x"
1040
+ [attr.y]="rect.y"
1041
+ [attr.width]="rect.width"
1042
+ [attr.height]="rect.height"
1043
+ stroke-dasharray="4 3" />
1044
+ }
1045
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1046
+ }
1047
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartBrush, decorators: [{
1048
+ type: Component,
1049
+ args: [{
1050
+ selector: 'svg:g[ui-chart-brush]',
1051
+ changeDetection: ChangeDetectionStrategy.OnPush,
1052
+ host: {
1053
+ class: 'chart-brush',
1054
+ '(window:pointermove)': 'onPointerMove($event)',
1055
+ '(window:pointerup)': 'onPointerUp($event)',
1056
+ '(window:pointercancel)': 'onPointerCancel($event)',
1057
+ },
1058
+ template: `
1059
+ <svg:rect
1060
+ #hitbox
1061
+ class="fill-transparent touch-none"
1062
+ x="0"
1063
+ y="0"
1064
+ [attr.width]="width()"
1065
+ [attr.height]="height()"
1066
+ (pointerdown)="onPointerDown($event)"
1067
+ (pointermove)="onPointerMove($event)"
1068
+ (pointerup)="onPointerUp($event)"
1069
+ (pointercancel)="onPointerCancel($event)"
1070
+ (wheel)="onWheel($event)"
1071
+ (dblclick)="resetZoom()" />
1072
+
1073
+ @if (categoryPreview(); as rect) {
1074
+ <svg:rect
1075
+ class="fill-foreground/10 stroke-foreground/30"
1076
+ [attr.x]="rect.x"
1077
+ [attr.y]="rect.y"
1078
+ [attr.width]="rect.width"
1079
+ [attr.height]="rect.height"
1080
+ stroke-dasharray="4 3" />
1081
+ }
1082
+
1083
+ @if (scatterPreviewRect(); as rect) {
1084
+ <svg:rect
1085
+ class="fill-foreground/10 stroke-foreground/30"
1086
+ [attr.x]="rect.x"
1087
+ [attr.y]="rect.y"
1088
+ [attr.width]="rect.width"
1089
+ [attr.height]="rect.height"
1090
+ stroke-dasharray="4 3" />
1091
+ }
1092
+ `,
1093
+ }]
1094
+ }], propDecorators: { hitbox: [{ type: i0.ViewChild, args: ['hitbox', { isSignal: true }] }] } });
1095
+
1096
+ /**
1097
+ * Attach to a chart's `<svg:svg>` to publish pointer-driven active-point
1098
+ * state into the surrounding `ChartContext`.
1099
+ *
1100
+ * Computes the local (x, y) of the pointer relative to the inner plotting
1101
+ * area, resolves the nearest category, and updates
1102
+ * `ChartContext.activePoint`. Clears it on `pointerleave`.
1103
+ */
1104
+ class ChartPointerTracker {
1105
+ root = inject(ChartContext);
1106
+ cart = inject(CartesianContext);
1107
+ viewport = inject(CategoricalViewportContext, { optional: true });
1108
+ onMove(event) {
1109
+ const target = event.currentTarget;
1110
+ if (!target)
1111
+ return;
1112
+ const rect = target.getBoundingClientRect();
1113
+ if (rect.width === 0 || rect.height === 0)
1114
+ return;
1115
+ const { width, height } = this.root.dimensions();
1116
+ // Map client coords → viewBox coords (SVG uses `0 0 width height`,
1117
+ // preserveAspectRatio="none" so axes scale independently).
1118
+ const scaleX = width / rect.width;
1119
+ const scaleY = height / rect.height;
1120
+ const viewX = (event.clientX - rect.left) * scaleX;
1121
+ const viewY = (event.clientY - rect.top) * scaleY;
1122
+ const margin = this.cart.margin();
1123
+ const localX = viewX - margin.left;
1124
+ const localY = viewY - margin.top;
1125
+ const index = nearestCategoryIndex({
1126
+ categoryScale: this.cart.categoryScale,
1127
+ categories: this.cart.categories,
1128
+ orientation: this.cart.orientation,
1129
+ }, localX, localY);
1130
+ if (index < 0) {
1131
+ this.root.activePoint.set(null);
1132
+ return;
1133
+ }
1134
+ this.root.activePoint.set({
1135
+ index,
1136
+ datumIndex: (this.viewport?.zoomRange()?.startIndex ?? 0) + index,
1137
+ clientX: event.clientX,
1138
+ clientY: event.clientY,
1139
+ });
1140
+ }
1141
+ onLeave() {
1142
+ this.root.activePoint.set(null);
1143
+ }
1144
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartPointerTracker, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1145
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: ChartPointerTracker, isStandalone: true, selector: "svg:svg[uiChartPointerTracker]", host: { listeners: { "pointermove": "onMove($event)", "pointerleave": "onLeave()" } }, ngImport: i0 });
1146
+ }
1147
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartPointerTracker, decorators: [{
1148
+ type: Directive,
1149
+ args: [{
1150
+ selector: 'svg:svg[uiChartPointerTracker]',
1151
+ host: {
1152
+ '(pointermove)': 'onMove($event)',
1153
+ '(pointerleave)': 'onLeave()',
1154
+ },
1155
+ }]
1156
+ }] });
1157
+
1158
+ /** Locate the chart container DOM in order to position the tooltip. */
1159
+ function containerRect(el) {
1160
+ // Climb to the nearest `[data-chart]` (the ChartContainer host).
1161
+ let node = el;
1162
+ while (node && !node.hasAttribute('data-chart')) {
1163
+ node = node.parentElement;
1164
+ }
1165
+ return node?.getBoundingClientRect() ?? null;
1166
+ }
1167
+ /**
1168
+ * Tooltip overlay — renders the default tooltip card (or a user-supplied
1169
+ * template) anchored to the currently active data point.
1170
+ *
1171
+ * Shadcn-compatible knobs:
1172
+ * - `indicator="line"` / `"none"` / `"dashed"` — swap the row glyph
1173
+ * - `hideLabel` — drop the header row
1174
+ * - `label="Activities"` — override the header text
1175
+ * - `labelKey="activities"` — resolve the header from `config[labelKey].label`
1176
+ * - `labelFormatter` — transform the header (e.g. ISO date → long date)
1177
+ * - `formatter` — format per-row values (e.g. `"380 kcal"`)
1178
+ * - `valueKey` — for pie/radial/radar, read row value from a single field
1179
+ * - Icons are picked up automatically from `config[key].icon`.
1180
+ */
1181
+ class ChartTooltip {
1182
+ root = inject(ChartContext);
1183
+ host = inject((ElementRef));
1184
+ /** Data key on each datum whose value is the category label (x-axis). */
1185
+ xKey = input(null, ...(ngDevMode ? [{ debugName: "xKey" }] : /* istanbul ignore next */ []));
1186
+ /** Data source (optional — if omitted tooltip reads from chart data via activePoint.datumIndex). */
1187
+ data = input(null, ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
1188
+ /**
1189
+ * Optional key for per-datum value lookup (pie / radial / radar datasets
1190
+ * store values on a single field like `visitors` instead of one field per
1191
+ * series). When set and `activePoint.seriesKey` is active, the tooltip row
1192
+ * reads `datum[valueKey]` for its value.
1193
+ */
1194
+ valueKey = input(null, ...(ngDevMode ? [{ debugName: "valueKey" }] : /* istanbul ignore next */ []));
1195
+ /** Indicator variant next to each row. Default `'dot'`. */
1196
+ indicator = input('dot', ...(ngDevMode ? [{ debugName: "indicator" }] : /* istanbul ignore next */ []));
1197
+ /** Hide the header label entirely. */
1198
+ hideLabel = input(false, ...(ngDevMode ? [{ debugName: "hideLabel" }] : /* istanbul ignore next */ []));
1199
+ /** Override the header label with a fixed string. Takes precedence over `labelKey`. */
1200
+ label = input(null, ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1201
+ /**
1202
+ * Resolve the header label from `config[labelKey].label`. Useful when the
1203
+ * header should come from a config entry rather than the datum itself
1204
+ * (shadcn's "Custom label" variant).
1205
+ */
1206
+ labelKey = input(null, ...(ngDevMode ? [{ debugName: "labelKey" }] : /* istanbul ignore next */ []));
1207
+ /** Transform the final header string (e.g. ISO date → long date). */
1208
+ labelFormatter = input(null, ...(ngDevMode ? [{ debugName: "labelFormatter" }] : /* istanbul ignore next */ []));
1209
+ /** Format each row's value (return string — HTML is not interpreted). */
1210
+ formatter = input(null, ...(ngDevMode ? [{ debugName: "formatter" }] : /* istanbul ignore next */ []));
1211
+ customTpl = contentChild((TemplateRef), ...(ngDevMode ? [{ debugName: "customTpl" }] : /* istanbul ignore next */ []));
1212
+ visible = computed(() => this.root.activePoint() !== null, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
1213
+ payload = computed(() => {
1214
+ const active = this.root.activePoint();
1215
+ const rows = this.data();
1216
+ if (!active || !rows)
1217
+ return null;
1218
+ const dataIndex = active.datumIndex != null && active.datumIndex < rows.length ? active.datumIndex : active.index;
1219
+ if (dataIndex < 0 || dataIndex >= rows.length)
1220
+ return null;
1221
+ const cfg = this.root.config();
1222
+ const visibleKeys = this.root.visibleSeriesKeys();
1223
+ const datum = rows[dataIndex];
1224
+ const xKey = this.xKey();
1225
+ const category = xKey && xKey in datum ? String(datum[xKey]) : String(active.index);
1226
+ // When the active point targets a single series (pie/radial/radar hover),
1227
+ // collapse the tooltip to just that row.
1228
+ const activeSeriesKey = active.seriesKey && visibleKeys.includes(active.seriesKey) ? active.seriesKey : null;
1229
+ const keys = activeSeriesKey ? [activeSeriesKey] : visibleKeys;
1230
+ const valueKey = this.valueKey();
1231
+ const tooltipRows = keys.map((k) => {
1232
+ // For single-slice hover on pie/radial/radar the value lives on a
1233
+ // shared `valueKey` field, not on a per-series column.
1234
+ const rawValue = valueKey != null && activeSeriesKey === k ? datum[valueKey] : datum[k];
1235
+ return {
1236
+ seriesKey: k,
1237
+ label: cfg[k]?.label ?? k,
1238
+ value: rawValue,
1239
+ color: seriesColorVar(k),
1240
+ icon: cfg[k]?.icon,
1241
+ };
1242
+ });
1243
+ return { category, datum, rows: tooltipRows };
1244
+ }, ...(ngDevMode ? [{ debugName: "payload" }] : /* istanbul ignore next */ []));
1245
+ /** Resolve the header string honoring `label`, `labelKey`, then `labelFormatter`. */
1246
+ headerText(p) {
1247
+ if (this.hideLabel())
1248
+ return null;
1249
+ const override = this.label();
1250
+ const labelKey = this.labelKey();
1251
+ const cfg = this.root.config();
1252
+ const fromKey = labelKey ? (cfg[labelKey]?.label ?? labelKey) : null;
1253
+ const raw = override ?? fromKey ?? p.category;
1254
+ const fmt = this.labelFormatter();
1255
+ return fmt ? fmt(raw, p) : raw;
1256
+ }
1257
+ /** Apply per-row value formatter (or stringify). */
1258
+ formatRow(row, p) {
1259
+ const fmt = this.formatter();
1260
+ if (fmt)
1261
+ return fmt(row.value, row, p);
1262
+ return row.value == null ? '' : String(row.value);
1263
+ }
1264
+ position = computed(() => {
1265
+ const active = this.root.activePoint();
1266
+ if (!active || active.clientX == null || active.clientY == null) {
1267
+ return { x: 0, y: 0 };
1268
+ }
1269
+ // Map client coords → offset within the chart container (the element
1270
+ // marked with `data-chart`, which is our positioning ancestor).
1271
+ const rect = containerRect(this.host.nativeElement);
1272
+ if (!rect)
1273
+ return { x: 0, y: 0 };
1274
+ const tooltip = this.host.nativeElement.querySelector('[role="tooltip"]');
1275
+ const tooltipWidth = tooltip?.offsetWidth ?? 128;
1276
+ const tooltipHeight = tooltip?.offsetHeight ?? 0;
1277
+ const padding = 8;
1278
+ const minX = padding + tooltipWidth / 2;
1279
+ const maxX = Math.max(minX, rect.width - padding - tooltipWidth / 2);
1280
+ const x = Math.min(maxX, Math.max(minX, active.clientX - rect.left));
1281
+ // Y is the tooltip's bottom edge because the card is translated upward.
1282
+ const minY = padding + tooltipHeight;
1283
+ const y = Math.max(minY, active.clientY - rect.top - padding);
1284
+ return { x, y };
1285
+ }, ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
1286
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartTooltip, deps: [], target: i0.ɵɵFactoryTarget.Component });
1287
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ChartTooltip, isStandalone: true, selector: "ui-chart-tooltip", inputs: { xKey: { classPropertyName: "xKey", publicName: "xKey", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, valueKey: { classPropertyName: "valueKey", publicName: "valueKey", isSignal: true, isRequired: false, transformFunction: null }, indicator: { classPropertyName: "indicator", publicName: "indicator", isSignal: true, isRequired: false, transformFunction: null }, hideLabel: { classPropertyName: "hideLabel", publicName: "hideLabel", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, labelKey: { classPropertyName: "labelKey", publicName: "labelKey", isSignal: true, isRequired: false, transformFunction: null }, labelFormatter: { classPropertyName: "labelFormatter", publicName: "labelFormatter", isSignal: true, isRequired: false, transformFunction: null }, formatter: { classPropertyName: "formatter", publicName: "formatter", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.aria-hidden": "!visible()" }, classAttribute: "pointer-events-none absolute inset-0 z-10" }, queries: [{ propertyName: "customTpl", first: true, predicate: (TemplateRef), descendants: true, isSignal: true }], ngImport: i0, template: `
1288
+ @if (payload(); as p) {
1289
+ <div
1290
+ role="tooltip"
1291
+ class="pointer-events-none absolute grid min-w-32 max-w-72 -translate-x-1/2 -translate-y-full gap-1.5 rounded-lg border border-border/60 bg-background px-3 py-1.5 text-xs shadow-md"
1292
+ [style.left.px]="position().x"
1293
+ [style.top.px]="position().y">
1294
+ @if (customTpl(); as tpl) {
1295
+ <ng-container *ngTemplateOutlet="tpl; context: { $implicit: p }" />
1296
+ } @else {
1297
+ @if (!hideLabel() && headerText(p); as header) {
1298
+ <div class="font-medium">{{ header }}</div>
1299
+ }
1300
+ <ul class="grid gap-1.5">
1301
+ @for (row of p.rows; track row.seriesKey) {
1302
+ <li class="flex w-full flex-wrap items-stretch gap-2">
1303
+ <span class="flex flex-1 items-center gap-1.5">
1304
+ @switch (indicator()) {
1305
+ @case ('dot') {
1306
+ <span
1307
+ class="h-2.5 w-2.5 shrink-0 rounded-sm"
1308
+ [style.background]="row.color"
1309
+ [style.borderColor]="row.color"></span>
1310
+ }
1311
+ @case ('line') {
1312
+ <span class="h-full min-h-4 w-1 shrink-0 rounded-sm" [style.background]="row.color"></span>
1313
+ }
1314
+ @case ('dashed') {
1315
+ <span
1316
+ class="h-0 w-3 shrink-0 self-center border-t-2 border-dashed"
1317
+ [style.borderColor]="row.color"></span>
1318
+ }
1319
+ }
1320
+ @if (row.icon; as icon) {
1321
+ <span class="mr-1 inline-flex items-center text-muted-foreground">
1322
+ <ng-container *ngComponentOutlet="icon" />
1323
+ </span>
1324
+ }
1325
+ <span class="text-muted-foreground">{{ row.label }}</span>
1326
+ </span>
1327
+ <span class="font-mono font-medium tabular-nums text-foreground">
1328
+ {{ formatRow(row, p) }}
1329
+ </span>
1330
+ </li>
1331
+ }
1332
+ </ul>
1333
+ }
1334
+ </div>
1335
+ }
1336
+
1337
+ <!-- Live region — announces category changes to AT. -->
1338
+ <div class="sr-only" aria-live="polite" aria-atomic="true">
1339
+ @if (payload(); as p) {
1340
+ {{ p.category }}
1341
+ }
1342
+ </div>
1343
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1344
+ }
1345
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartTooltip, decorators: [{
1346
+ type: Component,
1347
+ args: [{
1348
+ selector: 'ui-chart-tooltip',
1349
+ changeDetection: ChangeDetectionStrategy.OnPush,
1350
+ imports: [NgTemplateOutlet, NgComponentOutlet],
1351
+ host: {
1352
+ class: 'pointer-events-none absolute inset-0 z-10',
1353
+ '[attr.aria-hidden]': '!visible()',
1354
+ },
1355
+ template: `
1356
+ @if (payload(); as p) {
1357
+ <div
1358
+ role="tooltip"
1359
+ class="pointer-events-none absolute grid min-w-32 max-w-72 -translate-x-1/2 -translate-y-full gap-1.5 rounded-lg border border-border/60 bg-background px-3 py-1.5 text-xs shadow-md"
1360
+ [style.left.px]="position().x"
1361
+ [style.top.px]="position().y">
1362
+ @if (customTpl(); as tpl) {
1363
+ <ng-container *ngTemplateOutlet="tpl; context: { $implicit: p }" />
1364
+ } @else {
1365
+ @if (!hideLabel() && headerText(p); as header) {
1366
+ <div class="font-medium">{{ header }}</div>
1367
+ }
1368
+ <ul class="grid gap-1.5">
1369
+ @for (row of p.rows; track row.seriesKey) {
1370
+ <li class="flex w-full flex-wrap items-stretch gap-2">
1371
+ <span class="flex flex-1 items-center gap-1.5">
1372
+ @switch (indicator()) {
1373
+ @case ('dot') {
1374
+ <span
1375
+ class="h-2.5 w-2.5 shrink-0 rounded-sm"
1376
+ [style.background]="row.color"
1377
+ [style.borderColor]="row.color"></span>
1378
+ }
1379
+ @case ('line') {
1380
+ <span class="h-full min-h-4 w-1 shrink-0 rounded-sm" [style.background]="row.color"></span>
1381
+ }
1382
+ @case ('dashed') {
1383
+ <span
1384
+ class="h-0 w-3 shrink-0 self-center border-t-2 border-dashed"
1385
+ [style.borderColor]="row.color"></span>
1386
+ }
1387
+ }
1388
+ @if (row.icon; as icon) {
1389
+ <span class="mr-1 inline-flex items-center text-muted-foreground">
1390
+ <ng-container *ngComponentOutlet="icon" />
1391
+ </span>
1392
+ }
1393
+ <span class="text-muted-foreground">{{ row.label }}</span>
1394
+ </span>
1395
+ <span class="font-mono font-medium tabular-nums text-foreground">
1396
+ {{ formatRow(row, p) }}
1397
+ </span>
1398
+ </li>
1399
+ }
1400
+ </ul>
1401
+ }
1402
+ </div>
1403
+ }
1404
+
1405
+ <!-- Live region — announces category changes to AT. -->
1406
+ <div class="sr-only" aria-live="polite" aria-atomic="true">
1407
+ @if (payload(); as p) {
1408
+ {{ p.category }}
1409
+ }
1410
+ </div>
1411
+ `,
1412
+ }]
1413
+ }], propDecorators: { xKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "xKey", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], valueKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueKey", required: false }] }], indicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "indicator", required: false }] }], hideLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideLabel", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], labelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelKey", required: false }] }], labelFormatter: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelFormatter", required: false }] }], formatter: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatter", required: false }] }], customTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
1414
+
1415
+ /**
1416
+ * Legend — renders a list of series swatches. Clicking an item toggles
1417
+ * visibility via `ChartContext.toggleSeries`.
1418
+ *
1419
+ * Place as a child of `<ui-chart-container>`:
1420
+ * ```html
1421
+ * <ui-chart-container [config]="cfg">
1422
+ * <ui-bar-chart ... />
1423
+ * <ui-chart-legend />
1424
+ * </ui-chart-container>
1425
+ * ```
1426
+ */
1427
+ class ChartLegend {
1428
+ root = inject(ChartContext);
1429
+ items = computed(() => {
1430
+ const cfg = this.root.config();
1431
+ const hidden = this.root.hiddenSeries();
1432
+ return this.root.seriesKeys().map((key) => ({
1433
+ seriesKey: key,
1434
+ label: cfg[key]?.label ?? key,
1435
+ color: seriesColorVar(key),
1436
+ hidden: hidden.has(key),
1437
+ }));
1438
+ }, ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1439
+ toggle(key) {
1440
+ this.root.toggleSeries(key);
1441
+ }
1442
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartLegend, deps: [], target: i0.ɵɵFactoryTarget.Component });
1443
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ChartLegend, isStandalone: true, selector: "ui-chart-legend", host: { classAttribute: "block pt-3" }, ngImport: i0, template: `
1444
+ <ul class="flex flex-wrap items-center justify-center gap-3 text-xs" aria-label="Toggle chart series visibility">
1445
+ @for (item of items(); track item.seriesKey) {
1446
+ <li>
1447
+ <button
1448
+ type="button"
1449
+ class="inline-flex min-h-11 items-center gap-2 rounded-md px-2.5 py-1.5 outline-none transition-opacity focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
1450
+ [class.opacity-50]="item.hidden"
1451
+ [attr.aria-pressed]="!item.hidden"
1452
+ [attr.aria-label]="(item.hidden ? 'Show ' : 'Hide ') + item.label"
1453
+ (click)="toggle(item.seriesKey)">
1454
+ <span class="inline-block h-2.5 w-2.5 rounded-sm" [style.background]="item.color"></span>
1455
+ <span class="text-muted-foreground">{{ item.label }}</span>
1456
+ </button>
1457
+ </li>
1458
+ }
1459
+ </ul>
1460
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1461
+ }
1462
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartLegend, decorators: [{
1463
+ type: Component,
1464
+ args: [{
1465
+ selector: 'ui-chart-legend',
1466
+ changeDetection: ChangeDetectionStrategy.OnPush,
1467
+ host: { class: 'block pt-3' },
1468
+ template: `
1469
+ <ul class="flex flex-wrap items-center justify-center gap-3 text-xs" aria-label="Toggle chart series visibility">
1470
+ @for (item of items(); track item.seriesKey) {
1471
+ <li>
1472
+ <button
1473
+ type="button"
1474
+ class="inline-flex min-h-11 items-center gap-2 rounded-md px-2.5 py-1.5 outline-none transition-opacity focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
1475
+ [class.opacity-50]="item.hidden"
1476
+ [attr.aria-pressed]="!item.hidden"
1477
+ [attr.aria-label]="(item.hidden ? 'Show ' : 'Hide ') + item.label"
1478
+ (click)="toggle(item.seriesKey)">
1479
+ <span class="inline-block h-2.5 w-2.5 rounded-sm" [style.background]="item.color"></span>
1480
+ <span class="text-muted-foreground">{{ item.label }}</span>
1481
+ </button>
1482
+ </li>
1483
+ }
1484
+ </ul>
1485
+ `,
1486
+ }]
1487
+ }] });
1488
+
1489
+ function formatNumber(value) {
1490
+ return Math.abs(value) >= 10 ? value.toFixed(0) : value.toFixed(1);
1491
+ }
1492
+ class ChartZoomControls {
1493
+ categorical = inject(CategoricalViewportContext, { optional: true });
1494
+ scatter = inject(ScatterViewportContext, { optional: true });
1495
+ status = computed(() => {
1496
+ if (this.categorical?.hasZoom()) {
1497
+ const range = this.categorical.zoomRange();
1498
+ const count = this.categorical.dataCount();
1499
+ if (!range) {
1500
+ return null;
1501
+ }
1502
+ return `Showing ${range.startIndex + 1}-${range.endIndex + 1} of ${count}`;
1503
+ }
1504
+ if (this.scatter?.hasZoom()) {
1505
+ const xDomain = this.scatter.zoomXDomain() ?? this.scatter.fullXDomain();
1506
+ const yDomain = this.scatter.zoomYDomain() ?? this.scatter.fullYDomain();
1507
+ if (!xDomain || !yDomain) {
1508
+ return null;
1509
+ }
1510
+ return `X ${formatNumber(xDomain[0])}-${formatNumber(xDomain[1])} · Y ${formatNumber(yDomain[0])}-${formatNumber(yDomain[1])}`;
1511
+ }
1512
+ return null;
1513
+ }, ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
1514
+ hint = computed(() => {
1515
+ if (this.scatter) {
1516
+ return 'Drag to brush a region. Use the wheel to zoom and one-finger touch drag to pan while zoomed.';
1517
+ }
1518
+ return 'Drag to select a category range. Use the wheel to zoom and one-finger touch drag to pan while zoomed.';
1519
+ }, ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
1520
+ resetZoom() {
1521
+ this.categorical?.resetZoom();
1522
+ this.scatter?.resetZoom();
1523
+ }
1524
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartZoomControls, deps: [], target: i0.ɵɵFactoryTarget.Component });
1525
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ChartZoomControls, isStandalone: true, selector: "ui-chart-zoom-controls", host: { classAttribute: "mt-3 flex flex-wrap items-center justify-between gap-3 text-xs text-muted-foreground" }, ngImport: i0, template: `
1526
+ <p>{{ hint() }}</p>
1527
+
1528
+ @if (status(); as currentStatus) {
1529
+ <div class="flex flex-wrap items-center gap-2">
1530
+ <span class="rounded-full border border-border bg-background px-2.5 py-1 font-medium text-foreground">
1531
+ {{ currentStatus }}
1532
+ </span>
1533
+ <button
1534
+ type="button"
1535
+ class="inline-flex min-h-11 items-center rounded-md border border-border bg-background px-3 text-foreground transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
1536
+ (click)="resetZoom()">
1537
+ Reset zoom
1538
+ </button>
1539
+ </div>
1540
+ }
1541
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1542
+ }
1543
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ChartZoomControls, decorators: [{
1544
+ type: Component,
1545
+ args: [{
1546
+ selector: 'ui-chart-zoom-controls',
1547
+ changeDetection: ChangeDetectionStrategy.OnPush,
1548
+ host: { class: 'mt-3 flex flex-wrap items-center justify-between gap-3 text-xs text-muted-foreground' },
1549
+ template: `
1550
+ <p>{{ hint() }}</p>
1551
+
1552
+ @if (status(); as currentStatus) {
1553
+ <div class="flex flex-wrap items-center gap-2">
1554
+ <span class="rounded-full border border-border bg-background px-2.5 py-1 font-medium text-foreground">
1555
+ {{ currentStatus }}
1556
+ </span>
1557
+ <button
1558
+ type="button"
1559
+ class="inline-flex min-h-11 items-center rounded-md border border-border bg-background px-3 text-foreground transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
1560
+ (click)="resetZoom()">
1561
+ Reset zoom
1562
+ </button>
1563
+ </div>
1564
+ }
1565
+ `,
1566
+ }]
1567
+ }] });
1568
+
1569
+ class PieCenter {
1570
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PieCenter, deps: [], target: i0.ɵɵFactoryTarget.Component });
1571
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: PieCenter, isStandalone: true, selector: "ui-pie-center", host: { classAttribute: "flex max-w-[10rem] flex-col items-center justify-center text-center" }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1572
+ }
1573
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PieCenter, decorators: [{
1574
+ type: Component,
1575
+ args: [{
1576
+ selector: 'ui-pie-center',
1577
+ changeDetection: ChangeDetectionStrategy.OnPush,
1578
+ host: {
1579
+ class: 'flex max-w-[10rem] flex-col items-center justify-center text-center',
1580
+ },
1581
+ template: `<ng-content />`,
1582
+ }]
1583
+ }] });
1584
+
1585
+ class RadialCenter {
1586
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RadialCenter, deps: [], target: i0.ɵɵFactoryTarget.Component });
1587
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.9", type: RadialCenter, isStandalone: true, selector: "ui-radial-center", host: { classAttribute: "flex max-w-[10rem] flex-col items-center justify-center text-center" }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1588
+ }
1589
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RadialCenter, decorators: [{
1590
+ type: Component,
1591
+ args: [{
1592
+ selector: 'ui-radial-center',
1593
+ changeDetection: ChangeDetectionStrategy.OnPush,
1594
+ host: {
1595
+ class: 'flex max-w-[10rem] flex-col items-center justify-center text-center',
1596
+ },
1597
+ template: `<ng-content />`,
1598
+ }]
1599
+ }] });
1600
+
1601
+ /** Read a numeric value from a datum, tolerating strings. */
1602
+ function readNumber$1(datum, key) {
1603
+ const raw = datum[key];
1604
+ if (typeof raw === 'number' && Number.isFinite(raw)) {
1605
+ return raw;
1606
+ }
1607
+ if (typeof raw === 'string') {
1608
+ const n = Number(raw);
1609
+ return Number.isFinite(n) ? n : 0;
1610
+ }
1611
+ return 0;
1612
+ }
1613
+ function resolveBarColor(datum, seriesKey, colorKey) {
1614
+ if (!colorKey) {
1615
+ return seriesColorVar(seriesKey);
1616
+ }
1617
+ const raw = datum[colorKey];
1618
+ if (typeof raw !== 'string' || raw.length === 0) {
1619
+ return seriesColorVar(seriesKey);
1620
+ }
1621
+ if (raw.startsWith('var(') ||
1622
+ raw.startsWith('#') ||
1623
+ raw.startsWith('rgb') ||
1624
+ raw.startsWith('hsl') ||
1625
+ raw.includes('(')) {
1626
+ return raw;
1627
+ }
1628
+ return seriesColorVar(raw);
1629
+ }
1630
+ function isActiveBar(datum, activeKey, activeValue) {
1631
+ if (!activeKey || activeValue === undefined) {
1632
+ return false;
1633
+ }
1634
+ const value = datum[activeKey];
1635
+ if (typeof value === 'number' && typeof activeValue === 'number') {
1636
+ return value === activeValue;
1637
+ }
1638
+ return String(value ?? '') === String(activeValue);
1639
+ }
1640
+ /** Build all bar rectangles for the given config. */
1641
+ function computeBarLayout(input) {
1642
+ const { data, xKey, seriesKeys, variant, orientation, innerWidth, innerHeight, bandPadding, groupPadding, colorKey, activeKey, activeValue, } = input;
1643
+ const categories = data.map((d) => String(d[xKey] ?? ''));
1644
+ const isVertical = orientation === 'vertical';
1645
+ const categoryScale = scaleBand()
1646
+ .domain(categories)
1647
+ .range(isVertical ? [0, innerWidth] : [0, innerHeight])
1648
+ .padding(bandPadding);
1649
+ const valueScale = scaleLinear();
1650
+ if (variant === 'stacked' && seriesKeys.length > 0) {
1651
+ return stackedLayout({
1652
+ data,
1653
+ xKey,
1654
+ seriesKeys,
1655
+ orientation,
1656
+ innerWidth,
1657
+ innerHeight,
1658
+ categoryScale,
1659
+ valueScale,
1660
+ categories,
1661
+ colorKey,
1662
+ activeKey,
1663
+ activeValue,
1664
+ });
1665
+ }
1666
+ return groupedLayout({
1667
+ data,
1668
+ xKey,
1669
+ seriesKeys,
1670
+ orientation,
1671
+ innerWidth,
1672
+ innerHeight,
1673
+ categoryScale,
1674
+ valueScale,
1675
+ groupPadding,
1676
+ categories,
1677
+ colorKey,
1678
+ activeKey,
1679
+ activeValue,
1680
+ });
1681
+ }
1682
+ function groupedLayout(input) {
1683
+ const { data, seriesKeys, orientation, innerWidth, innerHeight, categoryScale, valueScale, groupPadding, categories, colorKey, activeKey, activeValue, } = input;
1684
+ const isVertical = orientation === 'vertical';
1685
+ const minValue = min(data, (d) => min(seriesKeys, (k) => readNumber$1(d, k)) ?? 0) ?? 0;
1686
+ const maxValue = max(data, (d) => max(seriesKeys, (k) => readNumber$1(d, k)) ?? 0) ?? 0;
1687
+ const domainMin = Math.min(0, minValue);
1688
+ const domainMax = Math.max(0, maxValue, domainMin === 0 ? 1 : 0);
1689
+ valueScale
1690
+ .domain([domainMin, domainMax])
1691
+ .nice()
1692
+ .range(isVertical ? [innerHeight, 0] : [0, innerWidth]);
1693
+ const baseline = valueScale(0);
1694
+ const subScale = scaleBand()
1695
+ .domain(seriesKeys)
1696
+ .range([0, categoryScale.bandwidth()])
1697
+ .padding(groupPadding);
1698
+ const bars = [];
1699
+ data.forEach((datum, datumIndex) => {
1700
+ const category = categories[datumIndex];
1701
+ const bandStart = categoryScale(category) ?? 0;
1702
+ seriesKeys.forEach((seriesKey) => {
1703
+ const value = readNumber$1(datum, seriesKey);
1704
+ const sub = subScale(seriesKey) ?? 0;
1705
+ const scaledValue = valueScale(value);
1706
+ const color = resolveBarColor(datum, seriesKey, colorKey);
1707
+ const active = isActiveBar(datum, activeKey, activeValue);
1708
+ const rect = isVertical
1709
+ ? {
1710
+ key: `${datumIndex}-${seriesKey}`,
1711
+ seriesKey,
1712
+ datumIndex,
1713
+ category,
1714
+ value,
1715
+ x: bandStart + sub,
1716
+ y: Math.min(scaledValue, baseline),
1717
+ width: subScale.bandwidth(),
1718
+ height: Math.abs(baseline - scaledValue),
1719
+ color,
1720
+ active,
1721
+ }
1722
+ : {
1723
+ key: `${datumIndex}-${seriesKey}`,
1724
+ seriesKey,
1725
+ datumIndex,
1726
+ category,
1727
+ value,
1728
+ x: Math.min(scaledValue, baseline),
1729
+ y: bandStart + sub,
1730
+ width: Math.abs(baseline - scaledValue),
1731
+ height: subScale.bandwidth(),
1732
+ color,
1733
+ active,
1734
+ };
1735
+ bars.push(rect);
1736
+ });
1737
+ });
1738
+ return { bars, categoryScale, valueScale, categories };
1739
+ }
1740
+ function stackedLayout(input) {
1741
+ const { data, seriesKeys, orientation, innerWidth, innerHeight, categoryScale, valueScale, categories, colorKey, activeKey, activeValue, } = input;
1742
+ const isVertical = orientation === 'vertical';
1743
+ const normalized = data.map((d) => {
1744
+ const out = {};
1745
+ for (const k of seriesKeys) {
1746
+ out[k] = readNumber$1(d, k);
1747
+ }
1748
+ return out;
1749
+ });
1750
+ const series = stack().keys(seriesKeys)(normalized);
1751
+ const maxTotal = max(series[series.length - 1] ?? [], (p) => p[1]) ?? 0;
1752
+ valueScale
1753
+ .domain([0, maxTotal === 0 ? 1 : maxTotal])
1754
+ .nice()
1755
+ .range(isVertical ? [innerHeight, 0] : [0, innerWidth]);
1756
+ const bars = [];
1757
+ series.forEach((layer) => {
1758
+ const seriesKey = layer.key;
1759
+ layer.forEach((point, datumIndex) => {
1760
+ const [lower, upper] = point;
1761
+ const value = upper - lower;
1762
+ const category = categories[datumIndex];
1763
+ const bandStart = categoryScale(category) ?? 0;
1764
+ const color = resolveBarColor(data[datumIndex], seriesKey, colorKey);
1765
+ const active = isActiveBar(data[datumIndex], activeKey, activeValue);
1766
+ const rect = isVertical
1767
+ ? {
1768
+ key: `${datumIndex}-${seriesKey}`,
1769
+ seriesKey,
1770
+ datumIndex,
1771
+ category,
1772
+ value,
1773
+ x: bandStart,
1774
+ y: valueScale(upper),
1775
+ width: categoryScale.bandwidth(),
1776
+ height: valueScale(lower) - valueScale(upper),
1777
+ color,
1778
+ active,
1779
+ }
1780
+ : {
1781
+ key: `${datumIndex}-${seriesKey}`,
1782
+ seriesKey,
1783
+ datumIndex,
1784
+ category,
1785
+ value,
1786
+ x: valueScale(lower),
1787
+ y: bandStart,
1788
+ width: valueScale(upper) - valueScale(lower),
1789
+ height: categoryScale.bandwidth(),
1790
+ color,
1791
+ active,
1792
+ };
1793
+ bars.push(rect);
1794
+ });
1795
+ });
1796
+ return { bars, categoryScale, valueScale, categories };
1797
+ }
1798
+
1799
+ const DEFAULT_MARGIN$6 = { top: 8, right: 8, bottom: 24, left: 40 };
1800
+ const defaultBarValueFormatter = (value) => `${value}`;
1801
+ /**
1802
+ * Bar chart — composable within `<ui-chart-container>`.
1803
+ *
1804
+ * Layout variants (via inputs):
1805
+ * - `orientation`: `'vertical'` (default) or `'horizontal'`
1806
+ * - `variant`: `'grouped'` (default) or `'stacked'`
1807
+ */
1808
+ class BarChart {
1809
+ root = inject(ChartContext);
1810
+ cart = inject(CartesianContext);
1811
+ data = input.required(...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
1812
+ xKey = input.required(...(ngDevMode ? [{ debugName: "xKey" }] : /* istanbul ignore next */ []));
1813
+ orientation = input('vertical', ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
1814
+ variant = input('grouped', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
1815
+ margin = input(DEFAULT_MARGIN$6, ...(ngDevMode ? [{ debugName: "margin" }] : /* istanbul ignore next */ []));
1816
+ bandPadding = input(0.2, ...(ngDevMode ? [{ debugName: "bandPadding" }] : /* istanbul ignore next */ []));
1817
+ groupPadding = input(0.05, ...(ngDevMode ? [{ debugName: "groupPadding" }] : /* istanbul ignore next */ []));
1818
+ cornerRadius = input(4, ...(ngDevMode ? [{ debugName: "cornerRadius" }] : /* istanbul ignore next */ []));
1819
+ colorKey = input(undefined, ...(ngDevMode ? [{ debugName: "colorKey" }] : /* istanbul ignore next */ []));
1820
+ activeKey = input(undefined, ...(ngDevMode ? [{ debugName: "activeKey" }] : /* istanbul ignore next */ []));
1821
+ activeValue = input(undefined, ...(ngDevMode ? [{ debugName: "activeValue" }] : /* istanbul ignore next */ []));
1822
+ showValueLabels = input(false, ...(ngDevMode ? [{ debugName: "showValueLabels" }] : /* istanbul ignore next */ []));
1823
+ valueLabelFormat = input(defaultBarValueFormatter, ...(ngDevMode ? [{ debugName: "valueLabelFormat" }] : /* istanbul ignore next */ []));
1824
+ barClick = output();
1825
+ innerWidth = computed(() => Math.max(0, this.root.dimensions().width - this.margin().left - this.margin().right), ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
1826
+ innerHeight = computed(() => Math.max(0, this.root.dimensions().height - this.margin().top - this.margin().bottom), ...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
1827
+ layout = computed(() => computeBarLayout({
1828
+ data: this.data(),
1829
+ xKey: this.xKey(),
1830
+ seriesKeys: this.root.visibleSeriesKeys(),
1831
+ variant: this.variant(),
1832
+ orientation: this.orientation(),
1833
+ innerWidth: this.innerWidth(),
1834
+ innerHeight: this.innerHeight(),
1835
+ bandPadding: this.bandPadding(),
1836
+ groupPadding: this.groupPadding(),
1837
+ colorKey: this.colorKey(),
1838
+ activeKey: this.activeKey(),
1839
+ activeValue: this.activeValue(),
1840
+ }), ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
1841
+ bars = computed(() => this.layout().bars, ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
1842
+ viewBox = computed(() => {
1843
+ const { width, height } = this.root.dimensions();
1844
+ return `0 0 ${Math.max(0, width)} ${Math.max(0, height)}`;
1845
+ }, ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
1846
+ innerTransform = computed(() => `translate(${this.margin().left},${this.margin().top})`, ...(ngDevMode ? [{ debugName: "innerTransform" }] : /* istanbul ignore next */ []));
1847
+ ariaSummary = computed(() => {
1848
+ const keys = this.root.visibleSeriesKeys();
1849
+ const n = this.data().length;
1850
+ return `Bar chart, ${n} categories, ${keys.length} series: ${keys.join(', ')}.`;
1851
+ }, ...(ngDevMode ? [{ debugName: "ariaSummary" }] : /* istanbul ignore next */ []));
1852
+ constructor() {
1853
+ effect(() => {
1854
+ const layout = this.layout();
1855
+ this.cart.orientation.set(this.orientation());
1856
+ this.cart.margin.set(this.margin());
1857
+ this.cart.innerWidth.set(this.innerWidth());
1858
+ this.cart.innerHeight.set(this.innerHeight());
1859
+ this.cart.categoryScale.set(layout.categoryScale);
1860
+ this.cart.valueScale.set(layout.valueScale);
1861
+ this.cart.categories.set(layout.categories);
1862
+ });
1863
+ }
1864
+ emitClick(bar) {
1865
+ this.barClick.emit({
1866
+ seriesKey: bar.seriesKey,
1867
+ datumIndex: bar.datumIndex,
1868
+ category: bar.category,
1869
+ value: bar.value,
1870
+ datum: this.data()[bar.datumIndex],
1871
+ });
1872
+ }
1873
+ setActivePoint(event, bar) {
1874
+ const center = elementClientCenter(event.currentTarget);
1875
+ this.root.activePoint.set({
1876
+ index: bar.datumIndex,
1877
+ seriesKey: bar.seriesKey,
1878
+ clientX: center?.clientX,
1879
+ clientY: center?.clientY,
1880
+ });
1881
+ }
1882
+ clearActivePoint() {
1883
+ this.root.activePoint.set(null);
1884
+ }
1885
+ barOpacity(bar) {
1886
+ return this.activeKey() && this.activeValue() !== undefined ? (bar.active ? 1 : 0.42) : 1;
1887
+ }
1888
+ formatValueLabel(bar) {
1889
+ return this.valueLabelFormat()(bar.value);
1890
+ }
1891
+ barLabelX(bar) {
1892
+ if (this.orientation() === 'vertical') {
1893
+ return bar.x + bar.width / 2;
1894
+ }
1895
+ return bar.value >= 0 ? bar.x + bar.width + 6 : bar.x - 6;
1896
+ }
1897
+ barLabelY(bar) {
1898
+ if (this.orientation() === 'vertical') {
1899
+ return bar.value >= 0 ? bar.y - 8 : bar.y + bar.height + 10;
1900
+ }
1901
+ return bar.y + bar.height / 2;
1902
+ }
1903
+ barLabelAnchor(bar) {
1904
+ if (this.orientation() === 'vertical') {
1905
+ return 'middle';
1906
+ }
1907
+ return bar.value >= 0 ? 'start' : 'end';
1908
+ }
1909
+ barAriaLabel(bar) {
1910
+ const label = this.root.config()[bar.seriesKey]?.label ?? bar.seriesKey;
1911
+ return `${label} in ${bar.category}: ${bar.value}`;
1912
+ }
1913
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: BarChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
1914
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: BarChart, isStandalone: true, selector: "ui-bar-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, xKey: { classPropertyName: "xKey", publicName: "xKey", isSignal: true, isRequired: true, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, margin: { classPropertyName: "margin", publicName: "margin", isSignal: true, isRequired: false, transformFunction: null }, bandPadding: { classPropertyName: "bandPadding", publicName: "bandPadding", isSignal: true, isRequired: false, transformFunction: null }, groupPadding: { classPropertyName: "groupPadding", publicName: "groupPadding", isSignal: true, isRequired: false, transformFunction: null }, cornerRadius: { classPropertyName: "cornerRadius", publicName: "cornerRadius", isSignal: true, isRequired: false, transformFunction: null }, colorKey: { classPropertyName: "colorKey", publicName: "colorKey", isSignal: true, isRequired: false, transformFunction: null }, activeKey: { classPropertyName: "activeKey", publicName: "activeKey", isSignal: true, isRequired: false, transformFunction: null }, activeValue: { classPropertyName: "activeValue", publicName: "activeValue", isSignal: true, isRequired: false, transformFunction: null }, showValueLabels: { classPropertyName: "showValueLabels", publicName: "showValueLabels", isSignal: true, isRequired: false, transformFunction: null }, valueLabelFormat: { classPropertyName: "valueLabelFormat", publicName: "valueLabelFormat", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { barClick: "barClick" }, host: { classAttribute: "relative block h-full w-full" }, providers: [CartesianContext], ngImport: i0, template: `
1915
+ <svg:svg
1916
+ uiChartPointerTracker
1917
+ class="block h-full w-full overflow-visible"
1918
+ [attr.viewBox]="viewBox()"
1919
+ preserveAspectRatio="none"
1920
+ role="img"
1921
+ [attr.aria-label]="ariaSummary()">
1922
+ <svg:g [attr.transform]="innerTransform()">
1923
+ <ng-content select="svg\\:g[ui-chart-grid]" />
1924
+ <svg:g class="chart-bars">
1925
+ @for (bar of bars(); track bar.key) {
1926
+ <svg:rect
1927
+ class="chart-bar cursor-pointer outline-none transition-opacity hover:opacity-80"
1928
+ [attr.x]="bar.x"
1929
+ [attr.y]="bar.y"
1930
+ [attr.width]="bar.width"
1931
+ [attr.height]="bar.height"
1932
+ [attr.rx]="cornerRadius()"
1933
+ [attr.ry]="cornerRadius()"
1934
+ [attr.fill]="bar.color"
1935
+ [attr.opacity]="barOpacity(bar)"
1936
+ [attr.stroke]="bar.active ? 'hsl(var(--foreground))' : null"
1937
+ [attr.stroke-width]="bar.active ? 1.5 : null"
1938
+ [attr.aria-label]="barAriaLabel(bar)"
1939
+ tabindex="0"
1940
+ (focus)="setActivePoint($event, bar)"
1941
+ (blur)="clearActivePoint()"
1942
+ (click)="emitClick(bar)"
1943
+ (keydown.enter)="emitClick(bar)"
1944
+ (keydown.space)="emitClick(bar); $event.preventDefault()" />
1945
+ @if (showValueLabels() && bar.height > 0 && bar.width > 0) {
1946
+ <svg:text
1947
+ class="chart-bar-value pointer-events-none fill-muted-foreground text-[10px]"
1948
+ [attr.x]="barLabelX(bar)"
1949
+ [attr.y]="barLabelY(bar)"
1950
+ [attr.text-anchor]="barLabelAnchor(bar)"
1951
+ dominant-baseline="middle">
1952
+ {{ formatValueLabel(bar) }}
1953
+ </svg:text>
1954
+ }
1955
+ }
1956
+ </svg:g>
1957
+ <ng-content select="svg\\:g[ui-chart-axis-x]" />
1958
+ <ng-content select="svg\\:g[ui-chart-axis-y]" />
1959
+ <ng-content select="svg\\:g[ui-chart-crosshair]" />
1960
+ <ng-content />
1961
+ </svg:g>
1962
+ </svg:svg>
1963
+ <ng-content select="ui-chart-tooltip" />
1964
+ <ng-content select="ui-chart-legend" />
1965
+ `, isInline: true, dependencies: [{ kind: "directive", type: ChartPointerTracker, selector: "svg:svg[uiChartPointerTracker]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1966
+ }
1967
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: BarChart, decorators: [{
1968
+ type: Component,
1969
+ args: [{
1970
+ selector: 'ui-bar-chart',
1971
+ changeDetection: ChangeDetectionStrategy.OnPush,
1972
+ providers: [CartesianContext],
1973
+ imports: [ChartPointerTracker],
1974
+ host: { class: 'relative block h-full w-full' },
1975
+ template: `
1976
+ <svg:svg
1977
+ uiChartPointerTracker
1978
+ class="block h-full w-full overflow-visible"
1979
+ [attr.viewBox]="viewBox()"
1980
+ preserveAspectRatio="none"
1981
+ role="img"
1982
+ [attr.aria-label]="ariaSummary()">
1983
+ <svg:g [attr.transform]="innerTransform()">
1984
+ <ng-content select="svg\\:g[ui-chart-grid]" />
1985
+ <svg:g class="chart-bars">
1986
+ @for (bar of bars(); track bar.key) {
1987
+ <svg:rect
1988
+ class="chart-bar cursor-pointer outline-none transition-opacity hover:opacity-80"
1989
+ [attr.x]="bar.x"
1990
+ [attr.y]="bar.y"
1991
+ [attr.width]="bar.width"
1992
+ [attr.height]="bar.height"
1993
+ [attr.rx]="cornerRadius()"
1994
+ [attr.ry]="cornerRadius()"
1995
+ [attr.fill]="bar.color"
1996
+ [attr.opacity]="barOpacity(bar)"
1997
+ [attr.stroke]="bar.active ? 'hsl(var(--foreground))' : null"
1998
+ [attr.stroke-width]="bar.active ? 1.5 : null"
1999
+ [attr.aria-label]="barAriaLabel(bar)"
2000
+ tabindex="0"
2001
+ (focus)="setActivePoint($event, bar)"
2002
+ (blur)="clearActivePoint()"
2003
+ (click)="emitClick(bar)"
2004
+ (keydown.enter)="emitClick(bar)"
2005
+ (keydown.space)="emitClick(bar); $event.preventDefault()" />
2006
+ @if (showValueLabels() && bar.height > 0 && bar.width > 0) {
2007
+ <svg:text
2008
+ class="chart-bar-value pointer-events-none fill-muted-foreground text-[10px]"
2009
+ [attr.x]="barLabelX(bar)"
2010
+ [attr.y]="barLabelY(bar)"
2011
+ [attr.text-anchor]="barLabelAnchor(bar)"
2012
+ dominant-baseline="middle">
2013
+ {{ formatValueLabel(bar) }}
2014
+ </svg:text>
2015
+ }
2016
+ }
2017
+ </svg:g>
2018
+ <ng-content select="svg\\:g[ui-chart-axis-x]" />
2019
+ <ng-content select="svg\\:g[ui-chart-axis-y]" />
2020
+ <ng-content select="svg\\:g[ui-chart-crosshair]" />
2021
+ <ng-content />
2022
+ </svg:g>
2023
+ </svg:svg>
2024
+ <ng-content select="ui-chart-tooltip" />
2025
+ <ng-content select="ui-chart-legend" />
2026
+ `,
2027
+ }]
2028
+ }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], xKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "xKey", required: true }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], margin: [{ type: i0.Input, args: [{ isSignal: true, alias: "margin", required: false }] }], bandPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "bandPadding", required: false }] }], groupPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupPadding", required: false }] }], cornerRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "cornerRadius", required: false }] }], colorKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "colorKey", required: false }] }], activeKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeKey", required: false }] }], activeValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeValue", required: false }] }], showValueLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showValueLabels", required: false }] }], valueLabelFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueLabelFormat", required: false }] }], barClick: [{ type: i0.Output, args: ["barClick"] }] } });
2029
+
2030
+ function curveFor(curve) {
2031
+ switch (curve) {
2032
+ case 'monotone':
2033
+ return curveMonotoneX;
2034
+ case 'step':
2035
+ return curveStep;
2036
+ case 'linear':
2037
+ default:
2038
+ return curveLinear;
2039
+ }
2040
+ }
2041
+ function readNumber(datum, key) {
2042
+ const raw = datum[key];
2043
+ if (typeof raw === 'number' && Number.isFinite(raw))
2044
+ return raw;
2045
+ if (typeof raw === 'string') {
2046
+ const n = Number(raw);
2047
+ return Number.isFinite(n) ? n : 0;
2048
+ }
2049
+ return 0;
2050
+ }
2051
+ /** Build category + value scales for point-based charts (line / area). */
2052
+ function buildCartesianScales(input) {
2053
+ const { data, xKey, seriesKeys, orientation, innerWidth, innerHeight } = input;
2054
+ const isVertical = orientation === 'vertical';
2055
+ const categories = data.map((d) => String(d[xKey] ?? ''));
2056
+ const categoryScale = scalePoint()
2057
+ .domain(categories)
2058
+ .range(isVertical ? [0, innerWidth] : [0, innerHeight])
2059
+ .padding(0.5);
2060
+ const maxValue = max(data, (d) => max(seriesKeys, (k) => readNumber(d, k)) ?? 0) ?? 0;
2061
+ const valueScale = scaleLinear()
2062
+ .domain([0, maxValue === 0 ? 1 : maxValue])
2063
+ .nice()
2064
+ .range(isVertical ? [innerHeight, 0] : [0, innerWidth]);
2065
+ return { categories, categoryScale, valueScale };
2066
+ }
2067
+ /** Compute line-chart geometry. */
2068
+ function computeLineLayout(input) {
2069
+ const { data, seriesKeys, orientation, curve } = input;
2070
+ const { categories, categoryScale, valueScale } = buildCartesianScales(input);
2071
+ const isVertical = orientation === 'vertical';
2072
+ const allPoints = [];
2073
+ const series = [];
2074
+ for (const seriesKey of seriesKeys) {
2075
+ const points = data.map((d, datumIndex) => {
2076
+ const value = readNumber(d, seriesKey);
2077
+ const category = categories[datumIndex];
2078
+ const c = categoryScale(category) ?? 0;
2079
+ const v = valueScale(value);
2080
+ return {
2081
+ seriesKey,
2082
+ datumIndex,
2083
+ category,
2084
+ value,
2085
+ x: isVertical ? c : v,
2086
+ y: isVertical ? v : c,
2087
+ };
2088
+ });
2089
+ allPoints.push(...points);
2090
+ const generator = line()
2091
+ .x((p) => p.x)
2092
+ .y((p) => p.y)
2093
+ .curve(curveFor(curve));
2094
+ series.push({
2095
+ seriesKey,
2096
+ color: seriesColorVar(seriesKey),
2097
+ linePath: generator(points) ?? '',
2098
+ points,
2099
+ });
2100
+ }
2101
+ return {
2102
+ series,
2103
+ points: allPoints,
2104
+ categoryScale,
2105
+ valueScale,
2106
+ categories,
2107
+ };
2108
+ }
2109
+ /** Compute area-chart geometry (single or stacked). */
2110
+ function computeAreaLayout(input) {
2111
+ if (input.stacked && input.seriesKeys.length > 0) {
2112
+ return computeStackedArea(input);
2113
+ }
2114
+ return computeSingleArea(input);
2115
+ }
2116
+ function computeSingleArea(input) {
2117
+ const { data, seriesKeys, orientation, curve } = input;
2118
+ const { categories, categoryScale, valueScale } = buildCartesianScales(input);
2119
+ const isVertical = orientation === 'vertical';
2120
+ const baseline = isVertical ? valueScale(0) : valueScale(0);
2121
+ const allPoints = [];
2122
+ const series = [];
2123
+ for (const seriesKey of seriesKeys) {
2124
+ const points = data.map((d, datumIndex) => {
2125
+ const value = readNumber(d, seriesKey);
2126
+ const category = categories[datumIndex];
2127
+ const c = categoryScale(category) ?? 0;
2128
+ const v = valueScale(value);
2129
+ return {
2130
+ seriesKey,
2131
+ datumIndex,
2132
+ category,
2133
+ value,
2134
+ x: isVertical ? c : v,
2135
+ y: isVertical ? v : c,
2136
+ };
2137
+ });
2138
+ allPoints.push(...points);
2139
+ const lineGen = line()
2140
+ .x((p) => p.x)
2141
+ .y((p) => p.y)
2142
+ .curve(curveFor(curve));
2143
+ const areaGen = isVertical
2144
+ ? area()
2145
+ .x((p) => p.x)
2146
+ .y0(baseline)
2147
+ .y1((p) => p.y)
2148
+ .curve(curveFor(curve))
2149
+ : area()
2150
+ .y((p) => p.y)
2151
+ .x0(baseline)
2152
+ .x1((p) => p.x)
2153
+ .curve(curveFor(curve));
2154
+ series.push({
2155
+ seriesKey,
2156
+ color: seriesColorVar(seriesKey),
2157
+ linePath: lineGen(points) ?? '',
2158
+ areaPath: areaGen(points) ?? '',
2159
+ points,
2160
+ });
2161
+ }
2162
+ return {
2163
+ series,
2164
+ points: allPoints,
2165
+ categoryScale,
2166
+ valueScale,
2167
+ categories,
2168
+ stacked: false,
2169
+ };
2170
+ }
2171
+ function computeStackedArea(input) {
2172
+ const { data, seriesKeys, orientation, innerWidth, innerHeight, xKey, curve, expanded } = input;
2173
+ const isVertical = orientation === 'vertical';
2174
+ const categories = data.map((d) => String(d[xKey] ?? ''));
2175
+ const categoryScale = scalePoint()
2176
+ .domain(categories)
2177
+ .range(isVertical ? [0, innerWidth] : [0, innerHeight])
2178
+ .padding(0.5);
2179
+ const normalized = data.map((d) => {
2180
+ const out = {};
2181
+ for (const k of seriesKeys)
2182
+ out[k] = readNumber(d, k);
2183
+ return out;
2184
+ });
2185
+ const stackGenerator = stack().keys(seriesKeys);
2186
+ if (expanded) {
2187
+ stackGenerator.offset(stackOffsetExpand);
2188
+ }
2189
+ const stackSeries = stackGenerator(normalized);
2190
+ const maxTotal = expanded ? 1 : (max(stackSeries[stackSeries.length - 1] ?? [], (p) => p[1]) ?? 0);
2191
+ const valueScale = scaleLinear()
2192
+ .domain([0, maxTotal === 0 ? 1 : maxTotal])
2193
+ .range(isVertical ? [innerHeight, 0] : [0, innerWidth]);
2194
+ if (!expanded) {
2195
+ valueScale.nice();
2196
+ }
2197
+ const allPoints = [];
2198
+ const series = stackSeries.map((layer) => {
2199
+ const seriesKey = layer.key;
2200
+ const points = layer.map((p, datumIndex) => {
2201
+ const category = categories[datumIndex];
2202
+ const c = categoryScale(category) ?? 0;
2203
+ const upper = valueScale(p[1]);
2204
+ return {
2205
+ seriesKey,
2206
+ datumIndex,
2207
+ category,
2208
+ value: p[1] - p[0],
2209
+ x: isVertical ? c : upper,
2210
+ y: isVertical ? upper : c,
2211
+ };
2212
+ });
2213
+ allPoints.push(...points);
2214
+ const lineGen = line()
2215
+ .x(([x]) => x)
2216
+ .y(([, y]) => y)
2217
+ .curve(curveFor(curve));
2218
+ const areaGen = isVertical
2219
+ ? area()
2220
+ .x(([x]) => x)
2221
+ .y0(([, , y0]) => y0)
2222
+ .y1(([, y1]) => y1)
2223
+ .curve(curveFor(curve))
2224
+ : area()
2225
+ .y(([x]) => x)
2226
+ .x0(([, , x0]) => x0)
2227
+ .x1(([, x1]) => x1)
2228
+ .curve(curveFor(curve));
2229
+ const tuples = layer.map((p, i) => {
2230
+ const c = categoryScale(categories[i]) ?? 0;
2231
+ return [c, valueScale(p[1]), valueScale(p[0])];
2232
+ });
2233
+ return {
2234
+ seriesKey,
2235
+ color: seriesColorVar(seriesKey),
2236
+ linePath: lineGen(tuples) ?? '',
2237
+ areaPath: areaGen(tuples) ?? '',
2238
+ points,
2239
+ };
2240
+ });
2241
+ return {
2242
+ series,
2243
+ points: allPoints,
2244
+ categoryScale,
2245
+ valueScale,
2246
+ categories,
2247
+ stacked: true,
2248
+ };
2249
+ }
2250
+
2251
+ /**
2252
+ * Publish a `scalePoint` from line/area layouts to the `CartesianContext`,
2253
+ * which expects a `scaleBand`. We adapt by building a band with zero padding
2254
+ * centered on the same positions, so axis ticks render at the correct
2255
+ * offsets.
2256
+ *
2257
+ * This keeps axes/grid primitives agnostic to whether the underlying chart
2258
+ * uses `scaleBand` (bar) or `scalePoint` (line/area).
2259
+ */
2260
+ function pointToBandAdapter(pointScale, range) {
2261
+ return scaleBand().domain(pointScale.domain()).range(range).padding(0);
2262
+ }
2263
+ /** Recreate a linear scale with the same domain/range (handy for effects). */
2264
+ function cloneLinear(scale) {
2265
+ return scaleLinear().domain(scale.domain()).range(scale.range());
2266
+ }
2267
+ function provideCartesianFromLineLayout(ctx, layout, orientation, innerWidth, innerHeight) {
2268
+ const range = orientation === 'vertical' ? [0, innerWidth] : [0, innerHeight];
2269
+ ctx.orientation.set(orientation);
2270
+ ctx.innerWidth.set(innerWidth);
2271
+ ctx.innerHeight.set(innerHeight);
2272
+ ctx.categoryScale.set(pointToBandAdapter(layout.categoryScale, range));
2273
+ ctx.valueScale.set(cloneLinear(layout.valueScale));
2274
+ ctx.categories.set(layout.categories);
2275
+ }
2276
+
2277
+ const DEFAULT_MARGIN$5 = { top: 8, right: 8, bottom: 24, left: 40 };
2278
+ const defaultLineValueFormatter = (value) => `${value}`;
2279
+ class LineChart {
2280
+ root = inject(ChartContext);
2281
+ cart = inject(CartesianContext);
2282
+ viewport = inject(CategoricalViewportContext);
2283
+ data = input.required(...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
2284
+ xKey = input.required(...(ngDevMode ? [{ debugName: "xKey" }] : /* istanbul ignore next */ []));
2285
+ orientation = input('vertical', ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
2286
+ curve = input('monotone', ...(ngDevMode ? [{ debugName: "curve" }] : /* istanbul ignore next */ []));
2287
+ margin = input(DEFAULT_MARGIN$5, ...(ngDevMode ? [{ debugName: "margin" }] : /* istanbul ignore next */ []));
2288
+ strokeWidth = input(2, ...(ngDevMode ? [{ debugName: "strokeWidth" }] : /* istanbul ignore next */ []));
2289
+ showDots = input(true, ...(ngDevMode ? [{ debugName: "showDots" }] : /* istanbul ignore next */ []));
2290
+ dotRadius = input(3, ...(ngDevMode ? [{ debugName: "dotRadius" }] : /* istanbul ignore next */ []));
2291
+ dotColorKey = input(undefined, ...(ngDevMode ? [{ debugName: "dotColorKey" }] : /* istanbul ignore next */ []));
2292
+ dotStrokeColor = input(undefined, ...(ngDevMode ? [{ debugName: "dotStrokeColor" }] : /* istanbul ignore next */ []));
2293
+ dotStrokeWidth = input(0, ...(ngDevMode ? [{ debugName: "dotStrokeWidth" }] : /* istanbul ignore next */ []));
2294
+ showValueLabels = input(false, ...(ngDevMode ? [{ debugName: "showValueLabels" }] : /* istanbul ignore next */ []));
2295
+ valueLabelFormat = input(defaultLineValueFormatter, ...(ngDevMode ? [{ debugName: "valueLabelFormat" }] : /* istanbul ignore next */ []));
2296
+ pointClick = output();
2297
+ innerWidth = computed(() => Math.max(0, this.root.dimensions().width - this.margin().left - this.margin().right), ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
2298
+ innerHeight = computed(() => Math.max(0, this.root.dimensions().height - this.margin().top - this.margin().bottom), ...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
2299
+ visibleStartIndex = computed(() => this.viewport.zoomRange()?.startIndex ?? 0, ...(ngDevMode ? [{ debugName: "visibleStartIndex" }] : /* istanbul ignore next */ []));
2300
+ visibleData = computed(() => sliceByIndexRange(this.data(), this.viewport.zoomRange()), ...(ngDevMode ? [{ debugName: "visibleData" }] : /* istanbul ignore next */ []));
2301
+ layout = computed(() => computeLineLayout({
2302
+ data: this.visibleData(),
2303
+ xKey: this.xKey(),
2304
+ seriesKeys: this.root.visibleSeriesKeys(),
2305
+ orientation: this.orientation(),
2306
+ innerWidth: this.innerWidth(),
2307
+ innerHeight: this.innerHeight(),
2308
+ curve: this.curve(),
2309
+ }), ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
2310
+ series = computed(() => this.layout().series, ...(ngDevMode ? [{ debugName: "series" }] : /* istanbul ignore next */ []));
2311
+ viewBox = computed(() => {
2312
+ const { width, height } = this.root.dimensions();
2313
+ return `0 0 ${Math.max(0, width)} ${Math.max(0, height)}`;
2314
+ }, ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
2315
+ innerTransform = computed(() => `translate(${this.margin().left},${this.margin().top})`, ...(ngDevMode ? [{ debugName: "innerTransform" }] : /* istanbul ignore next */ []));
2316
+ ariaSummary = computed(() => {
2317
+ const keys = this.root.visibleSeriesKeys();
2318
+ return `Line chart, ${this.visibleData().length} points, ${keys.length} series: ${keys.join(', ')}.`;
2319
+ }, ...(ngDevMode ? [{ debugName: "ariaSummary" }] : /* istanbul ignore next */ []));
2320
+ constructor() {
2321
+ effect(() => {
2322
+ const layout = this.layout();
2323
+ this.viewport.dataCount.set(this.data().length);
2324
+ provideCartesianFromLineLayout(this.cart, layout, this.orientation(), this.innerWidth(), this.innerHeight());
2325
+ this.cart.margin.set(this.margin());
2326
+ });
2327
+ }
2328
+ emitClick(p) {
2329
+ const datumIndex = this.visibleStartIndex() + p.datumIndex;
2330
+ this.pointClick.emit({
2331
+ seriesKey: p.seriesKey,
2332
+ datumIndex,
2333
+ category: p.category,
2334
+ value: p.value,
2335
+ datum: this.data()[datumIndex],
2336
+ });
2337
+ }
2338
+ setActivePoint(event, p) {
2339
+ const center = elementClientCenter(event.currentTarget);
2340
+ const datumIndex = this.visibleStartIndex() + p.datumIndex;
2341
+ this.root.activePoint.set({
2342
+ index: p.datumIndex,
2343
+ datumIndex,
2344
+ seriesKey: p.seriesKey,
2345
+ clientX: center?.clientX,
2346
+ clientY: center?.clientY,
2347
+ });
2348
+ }
2349
+ clearActivePoint() {
2350
+ this.root.activePoint.set(null);
2351
+ }
2352
+ dotFill(point, fallbackColor) {
2353
+ const colorKey = this.dotColorKey();
2354
+ if (!colorKey) {
2355
+ return fallbackColor;
2356
+ }
2357
+ const datum = this.visibleData()[point.datumIndex];
2358
+ const raw = datum?.[colorKey];
2359
+ if (typeof raw !== 'string' || raw.length === 0) {
2360
+ return fallbackColor;
2361
+ }
2362
+ if (raw.startsWith('var(') ||
2363
+ raw.startsWith('#') ||
2364
+ raw.startsWith('rgb') ||
2365
+ raw.startsWith('hsl') ||
2366
+ raw.includes('(')) {
2367
+ return raw;
2368
+ }
2369
+ return `var(--color-${raw.replace(/[^a-zA-Z0-9_-]/g, '_')})`;
2370
+ }
2371
+ formatValueLabel(point) {
2372
+ return this.valueLabelFormat()(point.value);
2373
+ }
2374
+ labelX(point) {
2375
+ return this.orientation() === 'vertical' ? point.x : point.x + this.dotRadius() + 6;
2376
+ }
2377
+ labelY(point) {
2378
+ return this.orientation() === 'vertical' ? point.y - this.dotRadius() - 8 : point.y;
2379
+ }
2380
+ labelAnchor() {
2381
+ return this.orientation() === 'vertical' ? 'middle' : 'start';
2382
+ }
2383
+ pointAriaLabel(p) {
2384
+ const label = this.root.config()[p.seriesKey]?.label ?? p.seriesKey;
2385
+ return `${label} at ${p.category}: ${p.value}`;
2386
+ }
2387
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LineChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
2388
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: LineChart, isStandalone: true, selector: "ui-line-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, xKey: { classPropertyName: "xKey", publicName: "xKey", isSignal: true, isRequired: true, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, curve: { classPropertyName: "curve", publicName: "curve", isSignal: true, isRequired: false, transformFunction: null }, margin: { classPropertyName: "margin", publicName: "margin", isSignal: true, isRequired: false, transformFunction: null }, strokeWidth: { classPropertyName: "strokeWidth", publicName: "strokeWidth", isSignal: true, isRequired: false, transformFunction: null }, showDots: { classPropertyName: "showDots", publicName: "showDots", isSignal: true, isRequired: false, transformFunction: null }, dotRadius: { classPropertyName: "dotRadius", publicName: "dotRadius", isSignal: true, isRequired: false, transformFunction: null }, dotColorKey: { classPropertyName: "dotColorKey", publicName: "dotColorKey", isSignal: true, isRequired: false, transformFunction: null }, dotStrokeColor: { classPropertyName: "dotStrokeColor", publicName: "dotStrokeColor", isSignal: true, isRequired: false, transformFunction: null }, dotStrokeWidth: { classPropertyName: "dotStrokeWidth", publicName: "dotStrokeWidth", isSignal: true, isRequired: false, transformFunction: null }, showValueLabels: { classPropertyName: "showValueLabels", publicName: "showValueLabels", isSignal: true, isRequired: false, transformFunction: null }, valueLabelFormat: { classPropertyName: "valueLabelFormat", publicName: "valueLabelFormat", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick" }, host: { classAttribute: "relative block h-full w-full" }, providers: [CartesianContext, CategoricalViewportContext], ngImport: i0, template: `
2389
+ <svg:svg
2390
+ uiChartPointerTracker
2391
+ class="block h-full w-full overflow-visible"
2392
+ [attr.viewBox]="viewBox()"
2393
+ preserveAspectRatio="none"
2394
+ role="img"
2395
+ [attr.aria-label]="ariaSummary()">
2396
+ <svg:g [attr.transform]="innerTransform()">
2397
+ <ng-content select="svg\\:g[ui-chart-grid]" />
2398
+ <svg:g class="chart-lines">
2399
+ @for (s of series(); track s.seriesKey) {
2400
+ <svg:path
2401
+ class="chart-line"
2402
+ fill="none"
2403
+ stroke-linecap="round"
2404
+ stroke-linejoin="round"
2405
+ [attr.stroke]="s.color"
2406
+ [attr.stroke-width]="strokeWidth()"
2407
+ [attr.d]="s.linePath" />
2408
+ @if (showDots()) {
2409
+ @for (p of s.points; track p.datumIndex) {
2410
+ <svg:circle
2411
+ class="chart-dot cursor-pointer outline-none"
2412
+ [attr.cx]="p.x"
2413
+ [attr.cy]="p.y"
2414
+ [attr.r]="dotRadius()"
2415
+ [attr.fill]="dotFill(p, s.color)"
2416
+ [attr.stroke]="dotStrokeColor()"
2417
+ [attr.stroke-width]="dotStrokeWidth() || null"
2418
+ [attr.aria-label]="pointAriaLabel(p)"
2419
+ tabindex="0"
2420
+ (focus)="setActivePoint($event, p)"
2421
+ (blur)="clearActivePoint()"
2422
+ (click)="emitClick(p)"
2423
+ (keydown.enter)="emitClick(p)"
2424
+ (keydown.space)="emitClick(p); $event.preventDefault()" />
2425
+ @if (showValueLabels()) {
2426
+ <svg:text
2427
+ class="chart-line-value pointer-events-none fill-muted-foreground text-[10px]"
2428
+ [attr.x]="labelX(p)"
2429
+ [attr.y]="labelY(p)"
2430
+ [attr.text-anchor]="labelAnchor()"
2431
+ dominant-baseline="middle">
2432
+ {{ formatValueLabel(p) }}
2433
+ </svg:text>
2434
+ }
2435
+ }
2436
+ }
2437
+ }
2438
+ </svg:g>
2439
+ <ng-content select="svg\\:g[ui-chart-axis-x]" />
2440
+ <ng-content select="svg\\:g[ui-chart-axis-y]" />
2441
+ <ng-content select="svg\\:g[ui-chart-crosshair]" />
2442
+ <ng-content select="svg:g[ui-chart-brush]" />
2443
+ </svg:g>
2444
+ </svg:svg>
2445
+ <ng-content select="ui-chart-tooltip" />
2446
+ <ng-content select="ui-chart-legend" />
2447
+ <ng-content select="ui-chart-zoom-controls" />
2448
+ `, isInline: true, dependencies: [{ kind: "directive", type: ChartPointerTracker, selector: "svg:svg[uiChartPointerTracker]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2449
+ }
2450
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LineChart, decorators: [{
2451
+ type: Component,
2452
+ args: [{
2453
+ selector: 'ui-line-chart',
2454
+ changeDetection: ChangeDetectionStrategy.OnPush,
2455
+ providers: [CartesianContext, CategoricalViewportContext],
2456
+ imports: [ChartPointerTracker],
2457
+ host: { class: 'relative block h-full w-full' },
2458
+ template: `
2459
+ <svg:svg
2460
+ uiChartPointerTracker
2461
+ class="block h-full w-full overflow-visible"
2462
+ [attr.viewBox]="viewBox()"
2463
+ preserveAspectRatio="none"
2464
+ role="img"
2465
+ [attr.aria-label]="ariaSummary()">
2466
+ <svg:g [attr.transform]="innerTransform()">
2467
+ <ng-content select="svg\\:g[ui-chart-grid]" />
2468
+ <svg:g class="chart-lines">
2469
+ @for (s of series(); track s.seriesKey) {
2470
+ <svg:path
2471
+ class="chart-line"
2472
+ fill="none"
2473
+ stroke-linecap="round"
2474
+ stroke-linejoin="round"
2475
+ [attr.stroke]="s.color"
2476
+ [attr.stroke-width]="strokeWidth()"
2477
+ [attr.d]="s.linePath" />
2478
+ @if (showDots()) {
2479
+ @for (p of s.points; track p.datumIndex) {
2480
+ <svg:circle
2481
+ class="chart-dot cursor-pointer outline-none"
2482
+ [attr.cx]="p.x"
2483
+ [attr.cy]="p.y"
2484
+ [attr.r]="dotRadius()"
2485
+ [attr.fill]="dotFill(p, s.color)"
2486
+ [attr.stroke]="dotStrokeColor()"
2487
+ [attr.stroke-width]="dotStrokeWidth() || null"
2488
+ [attr.aria-label]="pointAriaLabel(p)"
2489
+ tabindex="0"
2490
+ (focus)="setActivePoint($event, p)"
2491
+ (blur)="clearActivePoint()"
2492
+ (click)="emitClick(p)"
2493
+ (keydown.enter)="emitClick(p)"
2494
+ (keydown.space)="emitClick(p); $event.preventDefault()" />
2495
+ @if (showValueLabels()) {
2496
+ <svg:text
2497
+ class="chart-line-value pointer-events-none fill-muted-foreground text-[10px]"
2498
+ [attr.x]="labelX(p)"
2499
+ [attr.y]="labelY(p)"
2500
+ [attr.text-anchor]="labelAnchor()"
2501
+ dominant-baseline="middle">
2502
+ {{ formatValueLabel(p) }}
2503
+ </svg:text>
2504
+ }
2505
+ }
2506
+ }
2507
+ }
2508
+ </svg:g>
2509
+ <ng-content select="svg\\:g[ui-chart-axis-x]" />
2510
+ <ng-content select="svg\\:g[ui-chart-axis-y]" />
2511
+ <ng-content select="svg\\:g[ui-chart-crosshair]" />
2512
+ <ng-content select="svg:g[ui-chart-brush]" />
2513
+ </svg:g>
2514
+ </svg:svg>
2515
+ <ng-content select="ui-chart-tooltip" />
2516
+ <ng-content select="ui-chart-legend" />
2517
+ <ng-content select="ui-chart-zoom-controls" />
2518
+ `,
2519
+ }]
2520
+ }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], xKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "xKey", required: true }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], curve: [{ type: i0.Input, args: [{ isSignal: true, alias: "curve", required: false }] }], margin: [{ type: i0.Input, args: [{ isSignal: true, alias: "margin", required: false }] }], strokeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "strokeWidth", required: false }] }], showDots: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDots", required: false }] }], dotRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "dotRadius", required: false }] }], dotColorKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dotColorKey", required: false }] }], dotStrokeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "dotStrokeColor", required: false }] }], dotStrokeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "dotStrokeWidth", required: false }] }], showValueLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showValueLabels", required: false }] }], valueLabelFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueLabelFormat", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }] } });
2521
+
2522
+ const DEFAULT_MARGIN$4 = { top: 8, right: 8, bottom: 24, left: 40 };
2523
+ /**
2524
+ * Area chart — composable within `<ui-chart-container>`.
2525
+ *
2526
+ * - `stacked=true` stacks series along the value axis.
2527
+ * - `gradient=true` fills each area with a vertical linear-gradient from
2528
+ * the series color (top) to transparent (bottom). The gradient is
2529
+ * emitted as `<defs><linearGradient>` per series, unique per chart id.
2530
+ */
2531
+ class AreaChart {
2532
+ root = inject(ChartContext);
2533
+ cart = inject(CartesianContext);
2534
+ viewport = inject(CategoricalViewportContext);
2535
+ data = input.required(...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
2536
+ xKey = input.required(...(ngDevMode ? [{ debugName: "xKey" }] : /* istanbul ignore next */ []));
2537
+ orientation = input('vertical', ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
2538
+ stacked = input(false, ...(ngDevMode ? [{ debugName: "stacked" }] : /* istanbul ignore next */ []));
2539
+ expanded = input(false, ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
2540
+ gradient = input(true, ...(ngDevMode ? [{ debugName: "gradient" }] : /* istanbul ignore next */ []));
2541
+ curve = input('monotone', ...(ngDevMode ? [{ debugName: "curve" }] : /* istanbul ignore next */ []));
2542
+ margin = input(DEFAULT_MARGIN$4, ...(ngDevMode ? [{ debugName: "margin" }] : /* istanbul ignore next */ []));
2543
+ strokeWidth = input(2, ...(ngDevMode ? [{ debugName: "strokeWidth" }] : /* istanbul ignore next */ []));
2544
+ showDots = input(false, ...(ngDevMode ? [{ debugName: "showDots" }] : /* istanbul ignore next */ []));
2545
+ dotRadius = input(3, ...(ngDevMode ? [{ debugName: "dotRadius" }] : /* istanbul ignore next */ []));
2546
+ pointClick = output();
2547
+ innerWidth = computed(() => Math.max(0, this.root.dimensions().width - this.margin().left - this.margin().right), ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
2548
+ innerHeight = computed(() => Math.max(0, this.root.dimensions().height - this.margin().top - this.margin().bottom), ...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
2549
+ visibleStartIndex = computed(() => this.viewport.zoomRange()?.startIndex ?? 0, ...(ngDevMode ? [{ debugName: "visibleStartIndex" }] : /* istanbul ignore next */ []));
2550
+ visibleData = computed(() => sliceByIndexRange(this.data(), this.viewport.zoomRange()), ...(ngDevMode ? [{ debugName: "visibleData" }] : /* istanbul ignore next */ []));
2551
+ layout = computed(() => computeAreaLayout({
2552
+ data: this.visibleData(),
2553
+ xKey: this.xKey(),
2554
+ seriesKeys: this.root.visibleSeriesKeys(),
2555
+ orientation: this.orientation(),
2556
+ innerWidth: this.innerWidth(),
2557
+ innerHeight: this.innerHeight(),
2558
+ curve: this.curve(),
2559
+ stacked: this.stacked(),
2560
+ expanded: this.expanded(),
2561
+ }), ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
2562
+ series = computed(() => this.layout().series, ...(ngDevMode ? [{ debugName: "series" }] : /* istanbul ignore next */ []));
2563
+ viewBox = computed(() => {
2564
+ const { width, height } = this.root.dimensions();
2565
+ return `0 0 ${Math.max(0, width)} ${Math.max(0, height)}`;
2566
+ }, ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
2567
+ innerTransform = computed(() => `translate(${this.margin().left},${this.margin().top})`, ...(ngDevMode ? [{ debugName: "innerTransform" }] : /* istanbul ignore next */ []));
2568
+ ariaSummary = computed(() => {
2569
+ const keys = this.root.visibleSeriesKeys();
2570
+ return `Area chart, ${this.visibleData().length} points, ${keys.length} series: ${keys.join(', ')}.`;
2571
+ }, ...(ngDevMode ? [{ debugName: "ariaSummary" }] : /* istanbul ignore next */ []));
2572
+ constructor() {
2573
+ effect(() => {
2574
+ const layout = this.layout();
2575
+ this.viewport.dataCount.set(this.data().length);
2576
+ provideCartesianFromLineLayout(this.cart, layout, this.orientation(), this.innerWidth(), this.innerHeight());
2577
+ this.cart.margin.set(this.margin());
2578
+ });
2579
+ }
2580
+ gradientId(seriesKey) {
2581
+ return `${this.root.id()}-grad-${seriesKey.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
2582
+ }
2583
+ areaFill(seriesKey, color) {
2584
+ return this.gradient() ? `url(#${this.gradientId(seriesKey)})` : color;
2585
+ }
2586
+ emitClick(p) {
2587
+ const datumIndex = this.visibleStartIndex() + p.datumIndex;
2588
+ this.pointClick.emit({
2589
+ seriesKey: p.seriesKey,
2590
+ datumIndex,
2591
+ category: p.category,
2592
+ value: p.value,
2593
+ datum: this.data()[datumIndex],
2594
+ });
2595
+ }
2596
+ setActivePoint(event, p) {
2597
+ const center = elementClientCenter(event.currentTarget);
2598
+ const datumIndex = this.visibleStartIndex() + p.datumIndex;
2599
+ this.root.activePoint.set({
2600
+ index: p.datumIndex,
2601
+ datumIndex,
2602
+ seriesKey: p.seriesKey,
2603
+ clientX: center?.clientX,
2604
+ clientY: center?.clientY,
2605
+ });
2606
+ }
2607
+ clearActivePoint() {
2608
+ this.root.activePoint.set(null);
2609
+ }
2610
+ pointAriaLabel(p) {
2611
+ const label = this.root.config()[p.seriesKey]?.label ?? p.seriesKey;
2612
+ return `${label} at ${p.category}: ${p.value}`;
2613
+ }
2614
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AreaChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
2615
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AreaChart, isStandalone: true, selector: "ui-area-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, xKey: { classPropertyName: "xKey", publicName: "xKey", isSignal: true, isRequired: true, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, stacked: { classPropertyName: "stacked", publicName: "stacked", isSignal: true, isRequired: false, transformFunction: null }, expanded: { classPropertyName: "expanded", publicName: "expanded", isSignal: true, isRequired: false, transformFunction: null }, gradient: { classPropertyName: "gradient", publicName: "gradient", isSignal: true, isRequired: false, transformFunction: null }, curve: { classPropertyName: "curve", publicName: "curve", isSignal: true, isRequired: false, transformFunction: null }, margin: { classPropertyName: "margin", publicName: "margin", isSignal: true, isRequired: false, transformFunction: null }, strokeWidth: { classPropertyName: "strokeWidth", publicName: "strokeWidth", isSignal: true, isRequired: false, transformFunction: null }, showDots: { classPropertyName: "showDots", publicName: "showDots", isSignal: true, isRequired: false, transformFunction: null }, dotRadius: { classPropertyName: "dotRadius", publicName: "dotRadius", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick" }, host: { classAttribute: "relative block h-full w-full" }, providers: [CartesianContext, CategoricalViewportContext], ngImport: i0, template: `
2616
+ <svg:svg
2617
+ uiChartPointerTracker
2618
+ class="block h-full w-full overflow-visible"
2619
+ [attr.viewBox]="viewBox()"
2620
+ preserveAspectRatio="none"
2621
+ role="img"
2622
+ [attr.aria-label]="ariaSummary()">
2623
+ @if (gradient()) {
2624
+ <svg:defs>
2625
+ @for (s of series(); track s.seriesKey) {
2626
+ <svg:linearGradient [attr.id]="gradientId(s.seriesKey)" x1="0" y1="0" x2="0" y2="1">
2627
+ <svg:stop offset="0%" [attr.stop-color]="s.color" stop-opacity="0.4" />
2628
+ <svg:stop offset="100%" [attr.stop-color]="s.color" stop-opacity="0" />
2629
+ </svg:linearGradient>
2630
+ }
2631
+ </svg:defs>
2632
+ }
2633
+ <svg:g [attr.transform]="innerTransform()">
2634
+ <ng-content select="svg\\:g[ui-chart-grid]" />
2635
+ <svg:g class="chart-areas">
2636
+ @for (s of series(); track s.seriesKey) {
2637
+ <svg:path
2638
+ class="chart-area"
2639
+ [attr.d]="s.areaPath"
2640
+ [attr.fill]="areaFill(s.seriesKey, s.color)"
2641
+ stroke="none" />
2642
+ <svg:path
2643
+ class="chart-area-stroke"
2644
+ [attr.d]="s.linePath"
2645
+ [attr.stroke]="s.color"
2646
+ [attr.stroke-width]="strokeWidth()"
2647
+ fill="none"
2648
+ stroke-linecap="round"
2649
+ stroke-linejoin="round" />
2650
+ @if (showDots()) {
2651
+ @for (p of s.points; track p.datumIndex) {
2652
+ <svg:circle
2653
+ class="chart-dot cursor-pointer outline-none"
2654
+ [attr.cx]="p.x"
2655
+ [attr.cy]="p.y"
2656
+ [attr.r]="dotRadius()"
2657
+ [attr.fill]="s.color"
2658
+ [attr.aria-label]="pointAriaLabel(p)"
2659
+ tabindex="0"
2660
+ (focus)="setActivePoint($event, p)"
2661
+ (blur)="clearActivePoint()"
2662
+ (click)="emitClick(p)"
2663
+ (keydown.enter)="emitClick(p)"
2664
+ (keydown.space)="emitClick(p); $event.preventDefault()" />
2665
+ }
2666
+ }
2667
+ }
2668
+ </svg:g>
2669
+ <ng-content select="svg\\:g[ui-chart-axis-x]" />
2670
+ <ng-content select="svg\\:g[ui-chart-axis-y]" />
2671
+ <ng-content select="svg\\:g[ui-chart-crosshair]" />
2672
+ <ng-content select="svg:g[ui-chart-brush]" />
2673
+ </svg:g>
2674
+ </svg:svg>
2675
+ <ng-content select="ui-chart-tooltip" />
2676
+ <ng-content select="ui-chart-legend" />
2677
+ <ng-content select="ui-chart-zoom-controls" />
2678
+ `, isInline: true, dependencies: [{ kind: "directive", type: ChartPointerTracker, selector: "svg:svg[uiChartPointerTracker]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2679
+ }
2680
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AreaChart, decorators: [{
2681
+ type: Component,
2682
+ args: [{
2683
+ selector: 'ui-area-chart',
2684
+ changeDetection: ChangeDetectionStrategy.OnPush,
2685
+ providers: [CartesianContext, CategoricalViewportContext],
2686
+ imports: [ChartPointerTracker],
2687
+ host: { class: 'relative block h-full w-full' },
2688
+ template: `
2689
+ <svg:svg
2690
+ uiChartPointerTracker
2691
+ class="block h-full w-full overflow-visible"
2692
+ [attr.viewBox]="viewBox()"
2693
+ preserveAspectRatio="none"
2694
+ role="img"
2695
+ [attr.aria-label]="ariaSummary()">
2696
+ @if (gradient()) {
2697
+ <svg:defs>
2698
+ @for (s of series(); track s.seriesKey) {
2699
+ <svg:linearGradient [attr.id]="gradientId(s.seriesKey)" x1="0" y1="0" x2="0" y2="1">
2700
+ <svg:stop offset="0%" [attr.stop-color]="s.color" stop-opacity="0.4" />
2701
+ <svg:stop offset="100%" [attr.stop-color]="s.color" stop-opacity="0" />
2702
+ </svg:linearGradient>
2703
+ }
2704
+ </svg:defs>
2705
+ }
2706
+ <svg:g [attr.transform]="innerTransform()">
2707
+ <ng-content select="svg\\:g[ui-chart-grid]" />
2708
+ <svg:g class="chart-areas">
2709
+ @for (s of series(); track s.seriesKey) {
2710
+ <svg:path
2711
+ class="chart-area"
2712
+ [attr.d]="s.areaPath"
2713
+ [attr.fill]="areaFill(s.seriesKey, s.color)"
2714
+ stroke="none" />
2715
+ <svg:path
2716
+ class="chart-area-stroke"
2717
+ [attr.d]="s.linePath"
2718
+ [attr.stroke]="s.color"
2719
+ [attr.stroke-width]="strokeWidth()"
2720
+ fill="none"
2721
+ stroke-linecap="round"
2722
+ stroke-linejoin="round" />
2723
+ @if (showDots()) {
2724
+ @for (p of s.points; track p.datumIndex) {
2725
+ <svg:circle
2726
+ class="chart-dot cursor-pointer outline-none"
2727
+ [attr.cx]="p.x"
2728
+ [attr.cy]="p.y"
2729
+ [attr.r]="dotRadius()"
2730
+ [attr.fill]="s.color"
2731
+ [attr.aria-label]="pointAriaLabel(p)"
2732
+ tabindex="0"
2733
+ (focus)="setActivePoint($event, p)"
2734
+ (blur)="clearActivePoint()"
2735
+ (click)="emitClick(p)"
2736
+ (keydown.enter)="emitClick(p)"
2737
+ (keydown.space)="emitClick(p); $event.preventDefault()" />
2738
+ }
2739
+ }
2740
+ }
2741
+ </svg:g>
2742
+ <ng-content select="svg\\:g[ui-chart-axis-x]" />
2743
+ <ng-content select="svg\\:g[ui-chart-axis-y]" />
2744
+ <ng-content select="svg\\:g[ui-chart-crosshair]" />
2745
+ <ng-content select="svg:g[ui-chart-brush]" />
2746
+ </svg:g>
2747
+ </svg:svg>
2748
+ <ng-content select="ui-chart-tooltip" />
2749
+ <ng-content select="ui-chart-legend" />
2750
+ <ng-content select="ui-chart-zoom-controls" />
2751
+ `,
2752
+ }]
2753
+ }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], xKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "xKey", required: true }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], stacked: [{ type: i0.Input, args: [{ isSignal: true, alias: "stacked", required: false }] }], expanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }], gradient: [{ type: i0.Input, args: [{ isSignal: true, alias: "gradient", required: false }] }], curve: [{ type: i0.Input, args: [{ isSignal: true, alias: "curve", required: false }] }], margin: [{ type: i0.Input, args: [{ isSignal: true, alias: "margin", required: false }] }], strokeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "strokeWidth", required: false }] }], showDots: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDots", required: false }] }], dotRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "dotRadius", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }] } });
2754
+
2755
+ function computePieLayout(input) {
2756
+ const { data, valueKey, nameKey, seriesKeys, innerWidth, innerHeight, innerRadius, padAngle, cornerRadius, startAngle, endAngle, activeIndex, activeOffset = 12, } = input;
2757
+ const outerRadius = Math.max(0, Math.min(innerWidth, innerHeight) / 2);
2758
+ const centerX = innerWidth / 2;
2759
+ const centerY = innerHeight / 2;
2760
+ if (data.length === 0 || outerRadius === 0) {
2761
+ return { slices: [], centerX, centerY, outerRadius };
2762
+ }
2763
+ const pieGen = pie()
2764
+ .value((d) => Number(d[valueKey] ?? 0))
2765
+ .sort(null)
2766
+ .padAngle(padAngle)
2767
+ .startAngle(startAngle)
2768
+ .endAngle(endAngle);
2769
+ const arcGen = arc()
2770
+ .innerRadius(innerRadius)
2771
+ .outerRadius(outerRadius)
2772
+ .cornerRadius(cornerRadius);
2773
+ const arcs = pieGen(data);
2774
+ const slices = arcs.map((a, i) => {
2775
+ const d = a.data;
2776
+ const key = seriesKeys?.[i] ?? String(d[nameKey] ?? i);
2777
+ const centroid = arcGen.centroid(a);
2778
+ const magnitude = Math.hypot(centroid[0], centroid[1]) || 1;
2779
+ const isActive = i === activeIndex;
2780
+ return {
2781
+ seriesKey: key,
2782
+ name: String(d[nameKey] ?? key),
2783
+ value: Number(d[valueKey] ?? 0),
2784
+ datumIndex: i,
2785
+ color: seriesColorVar(key),
2786
+ arcPath: arcGen(a) ?? '',
2787
+ centroid,
2788
+ startAngle: a.startAngle,
2789
+ endAngle: a.endAngle,
2790
+ translateX: isActive ? (centroid[0] / magnitude) * activeOffset : 0,
2791
+ translateY: isActive ? (centroid[1] / magnitude) * activeOffset : 0,
2792
+ };
2793
+ });
2794
+ return { slices, centerX, centerY, outerRadius };
2795
+ }
2796
+
2797
+ const DEFAULT_MARGIN$3 = { top: 8, right: 8, bottom: 8, left: 8 };
2798
+ class PieChart {
2799
+ root = inject(ChartContext);
2800
+ data = input.required(...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
2801
+ valueKey = input.required(...(ngDevMode ? [{ debugName: "valueKey" }] : /* istanbul ignore next */ []));
2802
+ nameKey = input.required(...(ngDevMode ? [{ debugName: "nameKey" }] : /* istanbul ignore next */ []));
2803
+ seriesKeys = input(undefined, ...(ngDevMode ? [{ debugName: "seriesKeys" }] : /* istanbul ignore next */ []));
2804
+ margin = input(DEFAULT_MARGIN$3, ...(ngDevMode ? [{ debugName: "margin" }] : /* istanbul ignore next */ []));
2805
+ innerRadius = input(0, ...(ngDevMode ? [{ debugName: "innerRadius" }] : /* istanbul ignore next */ []));
2806
+ padAngle = input(0.01, ...(ngDevMode ? [{ debugName: "padAngle" }] : /* istanbul ignore next */ []));
2807
+ cornerRadius = input(0, ...(ngDevMode ? [{ debugName: "cornerRadius" }] : /* istanbul ignore next */ []));
2808
+ startAngle = input(-Math.PI / 2, ...(ngDevMode ? [{ debugName: "startAngle" }] : /* istanbul ignore next */ []));
2809
+ endAngle = input((3 * Math.PI) / 2, ...(ngDevMode ? [{ debugName: "endAngle" }] : /* istanbul ignore next */ []));
2810
+ showLabels = input(false, ...(ngDevMode ? [{ debugName: "showLabels" }] : /* istanbul ignore next */ []));
2811
+ activeIndex = input(undefined, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
2812
+ activeOffset = input(12, ...(ngDevMode ? [{ debugName: "activeOffset" }] : /* istanbul ignore next */ []));
2813
+ sliceClick = output();
2814
+ innerWidth = computed(() => Math.max(0, this.root.dimensions().width - this.margin().left - this.margin().right), ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
2815
+ innerHeight = computed(() => Math.max(0, this.root.dimensions().height - this.margin().top - this.margin().bottom), ...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
2816
+ layout = computed(() => computePieLayout({
2817
+ data: this.data(),
2818
+ valueKey: this.valueKey(),
2819
+ nameKey: this.nameKey(),
2820
+ seriesKeys: this.seriesKeys(),
2821
+ innerWidth: this.innerWidth(),
2822
+ innerHeight: this.innerHeight(),
2823
+ innerRadius: this.innerRadius(),
2824
+ padAngle: this.padAngle(),
2825
+ cornerRadius: this.cornerRadius(),
2826
+ startAngle: this.startAngle(),
2827
+ endAngle: this.endAngle(),
2828
+ activeIndex: this.activeIndex(),
2829
+ activeOffset: this.activeOffset(),
2830
+ }), ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
2831
+ viewBox = computed(() => {
2832
+ const { width, height } = this.root.dimensions();
2833
+ return `0 0 ${Math.max(0, width)} ${Math.max(0, height)}`;
2834
+ }, ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
2835
+ innerTransform = computed(() => `translate(${this.margin().left},${this.margin().top})`, ...(ngDevMode ? [{ debugName: "innerTransform" }] : /* istanbul ignore next */ []));
2836
+ ariaSummary = computed(() => `Pie chart, ${this.data().length} slices.`, ...(ngDevMode ? [{ debugName: "ariaSummary" }] : /* istanbul ignore next */ []));
2837
+ sliceAriaLabel(s) {
2838
+ return `${s.name}: ${s.value}`;
2839
+ }
2840
+ sliceTransform(s) {
2841
+ return s.translateX || s.translateY ? `translate(${s.translateX}, ${s.translateY})` : null;
2842
+ }
2843
+ emitClick(s) {
2844
+ this.sliceClick.emit({
2845
+ seriesKey: s.seriesKey,
2846
+ name: s.name,
2847
+ value: s.value,
2848
+ datumIndex: s.datumIndex,
2849
+ datum: this.data()[s.datumIndex],
2850
+ });
2851
+ }
2852
+ setActive(event, s) {
2853
+ const clientX = event instanceof PointerEvent ? event.clientX : event.target.getBoundingClientRect().left;
2854
+ const clientY = event instanceof PointerEvent ? event.clientY : event.target.getBoundingClientRect().top;
2855
+ this.root.activePoint.set({
2856
+ index: s.datumIndex,
2857
+ datumIndex: s.datumIndex,
2858
+ seriesKey: s.seriesKey,
2859
+ clientX,
2860
+ clientY,
2861
+ });
2862
+ }
2863
+ clearActive() {
2864
+ this.root.activePoint.set(null);
2865
+ }
2866
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PieChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
2867
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: PieChart, isStandalone: true, selector: "ui-pie-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, valueKey: { classPropertyName: "valueKey", publicName: "valueKey", isSignal: true, isRequired: true, transformFunction: null }, nameKey: { classPropertyName: "nameKey", publicName: "nameKey", isSignal: true, isRequired: true, transformFunction: null }, seriesKeys: { classPropertyName: "seriesKeys", publicName: "seriesKeys", isSignal: true, isRequired: false, transformFunction: null }, margin: { classPropertyName: "margin", publicName: "margin", isSignal: true, isRequired: false, transformFunction: null }, innerRadius: { classPropertyName: "innerRadius", publicName: "innerRadius", isSignal: true, isRequired: false, transformFunction: null }, padAngle: { classPropertyName: "padAngle", publicName: "padAngle", isSignal: true, isRequired: false, transformFunction: null }, cornerRadius: { classPropertyName: "cornerRadius", publicName: "cornerRadius", isSignal: true, isRequired: false, transformFunction: null }, startAngle: { classPropertyName: "startAngle", publicName: "startAngle", isSignal: true, isRequired: false, transformFunction: null }, endAngle: { classPropertyName: "endAngle", publicName: "endAngle", isSignal: true, isRequired: false, transformFunction: null }, showLabels: { classPropertyName: "showLabels", publicName: "showLabels", isSignal: true, isRequired: false, transformFunction: null }, activeIndex: { classPropertyName: "activeIndex", publicName: "activeIndex", isSignal: true, isRequired: false, transformFunction: null }, activeOffset: { classPropertyName: "activeOffset", publicName: "activeOffset", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sliceClick: "sliceClick" }, host: { classAttribute: "relative block h-full w-full" }, ngImport: i0, template: `
2868
+ <svg:svg
2869
+ class="block h-full w-full overflow-visible"
2870
+ [attr.viewBox]="viewBox()"
2871
+ preserveAspectRatio="xMidYMid meet"
2872
+ role="img"
2873
+ [attr.aria-label]="ariaSummary()">
2874
+ <svg:g [attr.transform]="innerTransform()">
2875
+ <svg:g [attr.transform]="'translate(' + layout().centerX + ',' + layout().centerY + ')'">
2876
+ @for (s of layout().slices; track s.seriesKey) {
2877
+ <svg:path
2878
+ class="chart-slice cursor-pointer transition-opacity hover:opacity-80"
2879
+ [attr.d]="s.arcPath"
2880
+ [attr.fill]="s.color"
2881
+ [attr.transform]="sliceTransform(s)"
2882
+ [attr.aria-label]="sliceAriaLabel(s)"
2883
+ tabindex="0"
2884
+ (click)="emitClick(s)"
2885
+ (keydown.enter)="emitClick(s)"
2886
+ (keydown.space)="emitClick(s); $event.preventDefault()"
2887
+ (pointerenter)="setActive($event, s)"
2888
+ (pointermove)="setActive($event, s)"
2889
+ (pointerleave)="clearActive()"
2890
+ (focus)="setActive($event, s)"
2891
+ (blur)="clearActive()" />
2892
+ }
2893
+ @if (showLabels()) {
2894
+ @for (s of layout().slices; track s.seriesKey) {
2895
+ <svg:text
2896
+ class="chart-slice-label fill-foreground text-[10px]"
2897
+ text-anchor="middle"
2898
+ dominant-baseline="middle"
2899
+ [attr.x]="s.centroid[0] + s.translateX"
2900
+ [attr.y]="s.centroid[1] + s.translateY">
2901
+ {{ s.value }}
2902
+ </svg:text>
2903
+ }
2904
+ }
2905
+ </svg:g>
2906
+ </svg:g>
2907
+ </svg:svg>
2908
+ <div class="pointer-events-none absolute inset-0 flex items-center justify-center">
2909
+ <ng-content select="ui-pie-center" />
2910
+ </div>
2911
+ <ng-content select="ui-chart-tooltip" />
2912
+ <ng-content select="ui-chart-legend" />
2913
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
2914
+ }
2915
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: PieChart, decorators: [{
2916
+ type: Component,
2917
+ args: [{
2918
+ selector: 'ui-pie-chart',
2919
+ changeDetection: ChangeDetectionStrategy.OnPush,
2920
+ host: { class: 'relative block h-full w-full' },
2921
+ template: `
2922
+ <svg:svg
2923
+ class="block h-full w-full overflow-visible"
2924
+ [attr.viewBox]="viewBox()"
2925
+ preserveAspectRatio="xMidYMid meet"
2926
+ role="img"
2927
+ [attr.aria-label]="ariaSummary()">
2928
+ <svg:g [attr.transform]="innerTransform()">
2929
+ <svg:g [attr.transform]="'translate(' + layout().centerX + ',' + layout().centerY + ')'">
2930
+ @for (s of layout().slices; track s.seriesKey) {
2931
+ <svg:path
2932
+ class="chart-slice cursor-pointer transition-opacity hover:opacity-80"
2933
+ [attr.d]="s.arcPath"
2934
+ [attr.fill]="s.color"
2935
+ [attr.transform]="sliceTransform(s)"
2936
+ [attr.aria-label]="sliceAriaLabel(s)"
2937
+ tabindex="0"
2938
+ (click)="emitClick(s)"
2939
+ (keydown.enter)="emitClick(s)"
2940
+ (keydown.space)="emitClick(s); $event.preventDefault()"
2941
+ (pointerenter)="setActive($event, s)"
2942
+ (pointermove)="setActive($event, s)"
2943
+ (pointerleave)="clearActive()"
2944
+ (focus)="setActive($event, s)"
2945
+ (blur)="clearActive()" />
2946
+ }
2947
+ @if (showLabels()) {
2948
+ @for (s of layout().slices; track s.seriesKey) {
2949
+ <svg:text
2950
+ class="chart-slice-label fill-foreground text-[10px]"
2951
+ text-anchor="middle"
2952
+ dominant-baseline="middle"
2953
+ [attr.x]="s.centroid[0] + s.translateX"
2954
+ [attr.y]="s.centroid[1] + s.translateY">
2955
+ {{ s.value }}
2956
+ </svg:text>
2957
+ }
2958
+ }
2959
+ </svg:g>
2960
+ </svg:g>
2961
+ </svg:svg>
2962
+ <div class="pointer-events-none absolute inset-0 flex items-center justify-center">
2963
+ <ng-content select="ui-pie-center" />
2964
+ </div>
2965
+ <ng-content select="ui-chart-tooltip" />
2966
+ <ng-content select="ui-chart-legend" />
2967
+ `,
2968
+ }]
2969
+ }], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], valueKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueKey", required: true }] }], nameKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "nameKey", required: true }] }], seriesKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "seriesKeys", required: false }] }], margin: [{ type: i0.Input, args: [{ isSignal: true, alias: "margin", required: false }] }], innerRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "innerRadius", required: false }] }], padAngle: [{ type: i0.Input, args: [{ isSignal: true, alias: "padAngle", required: false }] }], cornerRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "cornerRadius", required: false }] }], startAngle: [{ type: i0.Input, args: [{ isSignal: true, alias: "startAngle", required: false }] }], endAngle: [{ type: i0.Input, args: [{ isSignal: true, alias: "endAngle", required: false }] }], showLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLabels", required: false }] }], activeIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeIndex", required: false }] }], activeOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeOffset", required: false }] }], sliceClick: [{ type: i0.Output, args: ["sliceClick"] }] } });
2970
+
2971
+ function computeRadarLayout(input) {
2972
+ const { data, axisKey, seriesKeys, innerWidth, innerHeight, levels, curve, grid } = input;
2973
+ const radius = Math.max(0, Math.min(innerWidth, innerHeight) / 2);
2974
+ const centerX = innerWidth / 2;
2975
+ const centerY = innerHeight / 2;
2976
+ if (data.length === 0 || seriesKeys.length === 0 || radius === 0) {
2977
+ return { centerX, centerY, radius, axes: [], levels: [], maxValue: 0, series: [], grid };
2978
+ }
2979
+ const maxValue = input.maxValue ?? max(data, (d) => max(seriesKeys, (k) => Number(d[k] ?? 0)) ?? 0) ?? 1;
2980
+ const angleStep = (2 * Math.PI) / data.length;
2981
+ const angleFor = (i) => i * angleStep - Math.PI / 2;
2982
+ const axes = data.map((d, i) => {
2983
+ const angle = angleFor(i);
2984
+ return {
2985
+ name: String(d[axisKey] ?? i),
2986
+ angle,
2987
+ x: Math.cos(angle) * radius,
2988
+ y: Math.sin(angle) * radius,
2989
+ };
2990
+ });
2991
+ const levelValues = Array.from({ length: levels }, (_, index) => {
2992
+ const value = ((index + 1) / levels) * maxValue;
2993
+ const levelRadius = ((index + 1) / levels) * radius;
2994
+ return {
2995
+ value,
2996
+ radius: levelRadius,
2997
+ path: polygonPath(axes.map((axis) => ({
2998
+ x: Math.cos(axis.angle) * levelRadius,
2999
+ y: Math.sin(axis.angle) * levelRadius,
3000
+ }))),
3001
+ };
3002
+ });
3003
+ const curveFactory = curve === 'cardinal' ? curveCardinalClosed : curveLinearClosed;
3004
+ const radial = lineRadial()
3005
+ .angle((point) => point.angle + Math.PI / 2)
3006
+ .radius((point) => (point.value / maxValue) * radius)
3007
+ .curve(curveFactory);
3008
+ const series = seriesKeys.map((seriesKey) => {
3009
+ const points = data.map((datum, index) => {
3010
+ const value = Number(datum[seriesKey] ?? 0);
3011
+ const angle = angleFor(index);
3012
+ const pointRadius = (value / maxValue) * radius;
3013
+ return {
3014
+ axis: String(datum[axisKey] ?? index),
3015
+ value,
3016
+ x: Math.cos(angle) * pointRadius,
3017
+ y: Math.sin(angle) * pointRadius,
3018
+ };
3019
+ });
3020
+ return {
3021
+ seriesKey,
3022
+ color: seriesColorVar(seriesKey),
3023
+ path: radial(data.map((datum, index) => ({ angle: angleFor(index), value: Number(datum[seriesKey] ?? 0) }))) ?? '',
3024
+ points,
3025
+ };
3026
+ });
3027
+ return { centerX, centerY, radius, axes, levels: levelValues, maxValue, series, grid };
3028
+ }
3029
+ function polygonPath(points) {
3030
+ if (points.length === 0) {
3031
+ return '';
3032
+ }
3033
+ const [first, ...rest] = points;
3034
+ return `M ${first.x} ${first.y} ${rest.map((point) => `L ${point.x} ${point.y}`).join(' ')} Z`;
3035
+ }
3036
+
3037
+ const DEFAULT_MARGIN$2 = { top: 24, right: 24, bottom: 24, left: 24 };
3038
+ class RadarChart {
3039
+ root = inject(ChartContext);
3040
+ data = input.required(...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
3041
+ axisKey = input.required(...(ngDevMode ? [{ debugName: "axisKey" }] : /* istanbul ignore next */ []));
3042
+ margin = input(DEFAULT_MARGIN$2, ...(ngDevMode ? [{ debugName: "margin" }] : /* istanbul ignore next */ []));
3043
+ curve = input('linear', ...(ngDevMode ? [{ debugName: "curve" }] : /* istanbul ignore next */ []));
3044
+ levels = input(4, ...(ngDevMode ? [{ debugName: "levels" }] : /* istanbul ignore next */ []));
3045
+ maxValue = input(undefined, ...(ngDevMode ? [{ debugName: "maxValue" }] : /* istanbul ignore next */ []));
3046
+ strokeWidth = input(2, ...(ngDevMode ? [{ debugName: "strokeWidth" }] : /* istanbul ignore next */ []));
3047
+ fillOpacity = input(0.2, ...(ngDevMode ? [{ debugName: "fillOpacity" }] : /* istanbul ignore next */ []));
3048
+ showLabels = input(true, ...(ngDevMode ? [{ debugName: "showLabels" }] : /* istanbul ignore next */ []));
3049
+ showDots = input(true, ...(ngDevMode ? [{ debugName: "showDots" }] : /* istanbul ignore next */ []));
3050
+ dotRadius = input(3, ...(ngDevMode ? [{ debugName: "dotRadius" }] : /* istanbul ignore next */ []));
3051
+ grid = input('circle', ...(ngDevMode ? [{ debugName: "grid" }] : /* istanbul ignore next */ []));
3052
+ gridFilled = input(false, ...(ngDevMode ? [{ debugName: "gridFilled" }] : /* istanbul ignore next */ []));
3053
+ showAxes = input(true, ...(ngDevMode ? [{ debugName: "showAxes" }] : /* istanbul ignore next */ []));
3054
+ linesOnly = input(false, ...(ngDevMode ? [{ debugName: "linesOnly" }] : /* istanbul ignore next */ []));
3055
+ innerWidth = computed(() => Math.max(0, this.root.dimensions().width - this.margin().left - this.margin().right), ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
3056
+ innerHeight = computed(() => Math.max(0, this.root.dimensions().height - this.margin().top - this.margin().bottom), ...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
3057
+ layout = computed(() => computeRadarLayout({
3058
+ data: this.data(),
3059
+ axisKey: this.axisKey(),
3060
+ seriesKeys: this.root.visibleSeriesKeys(),
3061
+ innerWidth: this.innerWidth(),
3062
+ innerHeight: this.innerHeight(),
3063
+ maxValue: this.maxValue(),
3064
+ levels: this.levels(),
3065
+ curve: this.curve(),
3066
+ grid: this.grid(),
3067
+ }), ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
3068
+ viewBox = computed(() => {
3069
+ const { width, height } = this.root.dimensions();
3070
+ return `0 0 ${Math.max(0, width)} ${Math.max(0, height)}`;
3071
+ }, ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
3072
+ innerTransform = computed(() => `translate(${this.margin().left},${this.margin().top})`, ...(ngDevMode ? [{ debugName: "innerTransform" }] : /* istanbul ignore next */ []));
3073
+ ariaSummary = computed(() => {
3074
+ const keys = this.root.visibleSeriesKeys();
3075
+ return `Radar chart, ${this.data().length} axes, ${keys.length} series.`;
3076
+ }, ...(ngDevMode ? [{ debugName: "ariaSummary" }] : /* istanbul ignore next */ []));
3077
+ gridFill() {
3078
+ return this.gridFilled() ? 'hsl(var(--muted))' : 'none';
3079
+ }
3080
+ gridFillOpacity(levelIndex) {
3081
+ return this.gridFilled() ? Math.max(0.06, 0.18 - levelIndex * 0.02) : null;
3082
+ }
3083
+ dotAriaLabel(seriesKey, p) {
3084
+ const label = this.root.config()[seriesKey]?.label ?? seriesKey;
3085
+ return `${label} at ${p.axis}: ${p.value}`;
3086
+ }
3087
+ setActive(event, seriesKey, index) {
3088
+ const clientX = event instanceof PointerEvent ? event.clientX : event.target.getBoundingClientRect().left;
3089
+ const clientY = event instanceof PointerEvent ? event.clientY : event.target.getBoundingClientRect().top;
3090
+ this.root.activePoint.set({ index, datumIndex: index, seriesKey, clientX, clientY });
3091
+ }
3092
+ clearActive() {
3093
+ this.root.activePoint.set(null);
3094
+ }
3095
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RadarChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
3096
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: RadarChart, isStandalone: true, selector: "ui-radar-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, axisKey: { classPropertyName: "axisKey", publicName: "axisKey", isSignal: true, isRequired: true, transformFunction: null }, margin: { classPropertyName: "margin", publicName: "margin", isSignal: true, isRequired: false, transformFunction: null }, curve: { classPropertyName: "curve", publicName: "curve", isSignal: true, isRequired: false, transformFunction: null }, levels: { classPropertyName: "levels", publicName: "levels", isSignal: true, isRequired: false, transformFunction: null }, maxValue: { classPropertyName: "maxValue", publicName: "maxValue", isSignal: true, isRequired: false, transformFunction: null }, strokeWidth: { classPropertyName: "strokeWidth", publicName: "strokeWidth", isSignal: true, isRequired: false, transformFunction: null }, fillOpacity: { classPropertyName: "fillOpacity", publicName: "fillOpacity", isSignal: true, isRequired: false, transformFunction: null }, showLabels: { classPropertyName: "showLabels", publicName: "showLabels", isSignal: true, isRequired: false, transformFunction: null }, showDots: { classPropertyName: "showDots", publicName: "showDots", isSignal: true, isRequired: false, transformFunction: null }, dotRadius: { classPropertyName: "dotRadius", publicName: "dotRadius", isSignal: true, isRequired: false, transformFunction: null }, grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null }, gridFilled: { classPropertyName: "gridFilled", publicName: "gridFilled", isSignal: true, isRequired: false, transformFunction: null }, showAxes: { classPropertyName: "showAxes", publicName: "showAxes", isSignal: true, isRequired: false, transformFunction: null }, linesOnly: { classPropertyName: "linesOnly", publicName: "linesOnly", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "relative block h-full w-full" }, ngImport: i0, template: `
3097
+ <svg:svg
3098
+ class="block h-full w-full overflow-visible"
3099
+ [attr.viewBox]="viewBox()"
3100
+ preserveAspectRatio="xMidYMid meet"
3101
+ role="img"
3102
+ [attr.aria-label]="ariaSummary()">
3103
+ <svg:g [attr.transform]="innerTransform()">
3104
+ <svg:g class="chart-radar" [attr.transform]="'translate(' + layout().centerX + ',' + layout().centerY + ')'">
3105
+ @if (layout().grid !== 'none') {
3106
+ @for (level of layout().levels; track level.value; let levelIndex = $index) {
3107
+ @if (layout().grid === 'circle') {
3108
+ <svg:circle
3109
+ class="chart-radar-level stroke-border"
3110
+ [attr.fill]="gridFill()"
3111
+ [attr.fill-opacity]="gridFillOpacity(levelIndex)"
3112
+ stroke-dasharray="2 2"
3113
+ cx="0"
3114
+ cy="0"
3115
+ [attr.r]="level.radius" />
3116
+ } @else {
3117
+ <svg:path
3118
+ class="chart-radar-level stroke-border"
3119
+ [attr.d]="level.path"
3120
+ [attr.fill]="gridFill()"
3121
+ [attr.fill-opacity]="gridFillOpacity(levelIndex)"
3122
+ stroke-dasharray="2 2" />
3123
+ }
3124
+ }
3125
+ }
3126
+ @if (showAxes()) {
3127
+ @for (axis of layout().axes; track axis.name) {
3128
+ <svg:line class="chart-radar-axis stroke-border" x1="0" y1="0" [attr.x2]="axis.x" [attr.y2]="axis.y" />
3129
+ @if (showLabels()) {
3130
+ <svg:text
3131
+ class="chart-radar-axis-label fill-muted-foreground text-[10px]"
3132
+ text-anchor="middle"
3133
+ dominant-baseline="middle"
3134
+ [attr.x]="axis.x * 1.12"
3135
+ [attr.y]="axis.y * 1.12">
3136
+ {{ axis.name }}
3137
+ </svg:text>
3138
+ }
3139
+ }
3140
+ }
3141
+ @for (s of layout().series; track s.seriesKey) {
3142
+ <svg:path
3143
+ class="chart-radar-series"
3144
+ [attr.d]="s.path"
3145
+ [attr.stroke]="s.color"
3146
+ [attr.fill]="linesOnly() ? 'none' : s.color"
3147
+ [attr.fill-opacity]="linesOnly() ? null : fillOpacity()"
3148
+ [attr.stroke-width]="strokeWidth()"
3149
+ stroke-linejoin="round" />
3150
+ @if (showDots()) {
3151
+ @for (p of s.points; track p.axis; let i = $index) {
3152
+ <svg:circle
3153
+ class="chart-radar-dot cursor-pointer"
3154
+ [attr.cx]="p.x"
3155
+ [attr.cy]="p.y"
3156
+ [attr.r]="dotRadius()"
3157
+ [attr.fill]="s.color"
3158
+ tabindex="0"
3159
+ [attr.aria-label]="dotAriaLabel(s.seriesKey, p)"
3160
+ (pointerenter)="setActive($event, s.seriesKey, i)"
3161
+ (pointermove)="setActive($event, s.seriesKey, i)"
3162
+ (pointerleave)="clearActive()"
3163
+ (focus)="setActive($event, s.seriesKey, i)"
3164
+ (blur)="clearActive()" />
3165
+ }
3166
+ }
3167
+ }
3168
+ </svg:g>
3169
+ <ng-content />
3170
+ </svg:g>
3171
+ </svg:svg>
3172
+ <ng-content select="ui-chart-tooltip" />
3173
+ <ng-content select="ui-chart-legend" />
3174
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
3175
+ }
3176
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RadarChart, decorators: [{
3177
+ type: Component,
3178
+ args: [{
3179
+ selector: 'ui-radar-chart',
3180
+ changeDetection: ChangeDetectionStrategy.OnPush,
3181
+ host: { class: 'relative block h-full w-full' },
3182
+ template: `
3183
+ <svg:svg
3184
+ class="block h-full w-full overflow-visible"
3185
+ [attr.viewBox]="viewBox()"
3186
+ preserveAspectRatio="xMidYMid meet"
3187
+ role="img"
3188
+ [attr.aria-label]="ariaSummary()">
3189
+ <svg:g [attr.transform]="innerTransform()">
3190
+ <svg:g class="chart-radar" [attr.transform]="'translate(' + layout().centerX + ',' + layout().centerY + ')'">
3191
+ @if (layout().grid !== 'none') {
3192
+ @for (level of layout().levels; track level.value; let levelIndex = $index) {
3193
+ @if (layout().grid === 'circle') {
3194
+ <svg:circle
3195
+ class="chart-radar-level stroke-border"
3196
+ [attr.fill]="gridFill()"
3197
+ [attr.fill-opacity]="gridFillOpacity(levelIndex)"
3198
+ stroke-dasharray="2 2"
3199
+ cx="0"
3200
+ cy="0"
3201
+ [attr.r]="level.radius" />
3202
+ } @else {
3203
+ <svg:path
3204
+ class="chart-radar-level stroke-border"
3205
+ [attr.d]="level.path"
3206
+ [attr.fill]="gridFill()"
3207
+ [attr.fill-opacity]="gridFillOpacity(levelIndex)"
3208
+ stroke-dasharray="2 2" />
3209
+ }
3210
+ }
3211
+ }
3212
+ @if (showAxes()) {
3213
+ @for (axis of layout().axes; track axis.name) {
3214
+ <svg:line class="chart-radar-axis stroke-border" x1="0" y1="0" [attr.x2]="axis.x" [attr.y2]="axis.y" />
3215
+ @if (showLabels()) {
3216
+ <svg:text
3217
+ class="chart-radar-axis-label fill-muted-foreground text-[10px]"
3218
+ text-anchor="middle"
3219
+ dominant-baseline="middle"
3220
+ [attr.x]="axis.x * 1.12"
3221
+ [attr.y]="axis.y * 1.12">
3222
+ {{ axis.name }}
3223
+ </svg:text>
3224
+ }
3225
+ }
3226
+ }
3227
+ @for (s of layout().series; track s.seriesKey) {
3228
+ <svg:path
3229
+ class="chart-radar-series"
3230
+ [attr.d]="s.path"
3231
+ [attr.stroke]="s.color"
3232
+ [attr.fill]="linesOnly() ? 'none' : s.color"
3233
+ [attr.fill-opacity]="linesOnly() ? null : fillOpacity()"
3234
+ [attr.stroke-width]="strokeWidth()"
3235
+ stroke-linejoin="round" />
3236
+ @if (showDots()) {
3237
+ @for (p of s.points; track p.axis; let i = $index) {
3238
+ <svg:circle
3239
+ class="chart-radar-dot cursor-pointer"
3240
+ [attr.cx]="p.x"
3241
+ [attr.cy]="p.y"
3242
+ [attr.r]="dotRadius()"
3243
+ [attr.fill]="s.color"
3244
+ tabindex="0"
3245
+ [attr.aria-label]="dotAriaLabel(s.seriesKey, p)"
3246
+ (pointerenter)="setActive($event, s.seriesKey, i)"
3247
+ (pointermove)="setActive($event, s.seriesKey, i)"
3248
+ (pointerleave)="clearActive()"
3249
+ (focus)="setActive($event, s.seriesKey, i)"
3250
+ (blur)="clearActive()" />
3251
+ }
3252
+ }
3253
+ }
3254
+ </svg:g>
3255
+ <ng-content />
3256
+ </svg:g>
3257
+ </svg:svg>
3258
+ <ng-content select="ui-chart-tooltip" />
3259
+ <ng-content select="ui-chart-legend" />
3260
+ `,
3261
+ }]
3262
+ }], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], axisKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisKey", required: true }] }], margin: [{ type: i0.Input, args: [{ isSignal: true, alias: "margin", required: false }] }], curve: [{ type: i0.Input, args: [{ isSignal: true, alias: "curve", required: false }] }], levels: [{ type: i0.Input, args: [{ isSignal: true, alias: "levels", required: false }] }], maxValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxValue", required: false }] }], strokeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "strokeWidth", required: false }] }], fillOpacity: [{ type: i0.Input, args: [{ isSignal: true, alias: "fillOpacity", required: false }] }], showLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLabels", required: false }] }], showDots: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDots", required: false }] }], dotRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "dotRadius", required: false }] }], grid: [{ type: i0.Input, args: [{ isSignal: true, alias: "grid", required: false }] }], gridFilled: [{ type: i0.Input, args: [{ isSignal: true, alias: "gridFilled", required: false }] }], showAxes: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAxes", required: false }] }], linesOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "linesOnly", required: false }] }] } });
3263
+
3264
+ function computeRadialLayout(input) {
3265
+ const { data, nameKey, valueKey, innerWidth, innerHeight, seriesKeys, trackPadding, startAngle, endAngle, cornerRadius, } = input;
3266
+ const outerRadius = Math.max(0, Math.min(innerWidth, innerHeight) / 2);
3267
+ const centerX = innerWidth / 2;
3268
+ const centerY = innerHeight / 2;
3269
+ if (data.length === 0 || outerRadius === 0) {
3270
+ return { centerX, centerY, outerRadius, bars: [], maxValue: 0 };
3271
+ }
3272
+ const maxValue = input.maxValue ?? max(data, (d) => Number(d[valueKey] ?? 0)) ?? 1;
3273
+ const trackCount = data.length;
3274
+ const availableRadius = outerRadius - (trackCount - 1) * trackPadding;
3275
+ const trackThickness = availableRadius / trackCount;
3276
+ const bars = data.map((d, i) => {
3277
+ const inner = i * (trackThickness + trackPadding);
3278
+ const outer = inner + trackThickness;
3279
+ const value = Number(d[valueKey] ?? 0);
3280
+ const pct = maxValue === 0 ? 0 : value / maxValue;
3281
+ const sweep = (endAngle - startAngle) * pct;
3282
+ const valueEndAngle = startAngle + sweep;
3283
+ const key = seriesKeys?.[i] ?? String(d[nameKey] ?? i);
3284
+ const arcGen = arc().innerRadius(inner).outerRadius(outer).cornerRadius(cornerRadius);
3285
+ return {
3286
+ seriesKey: key,
3287
+ name: String(d[nameKey] ?? key),
3288
+ value,
3289
+ datumIndex: i,
3290
+ color: seriesColorVar(key),
3291
+ arcPath: arcGen.startAngle(startAngle).endAngle(valueEndAngle)(null) ?? '',
3292
+ backgroundPath: arcGen.startAngle(startAngle).endAngle(endAngle)(null) ?? '',
3293
+ innerRadius: inner,
3294
+ outerRadius: outer,
3295
+ endAngle: valueEndAngle,
3296
+ };
3297
+ });
3298
+ return { centerX, centerY, outerRadius, bars, maxValue };
3299
+ }
3300
+
3301
+ const DEFAULT_MARGIN$1 = { top: 8, right: 8, bottom: 8, left: 8 };
3302
+ const defaultRadialValueFormatter = (value) => `${value}`;
3303
+ class RadialChart {
3304
+ root = inject(ChartContext);
3305
+ data = input.required(...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
3306
+ nameKey = input.required(...(ngDevMode ? [{ debugName: "nameKey" }] : /* istanbul ignore next */ []));
3307
+ valueKey = input.required(...(ngDevMode ? [{ debugName: "valueKey" }] : /* istanbul ignore next */ []));
3308
+ seriesKeys = input(undefined, ...(ngDevMode ? [{ debugName: "seriesKeys" }] : /* istanbul ignore next */ []));
3309
+ margin = input(DEFAULT_MARGIN$1, ...(ngDevMode ? [{ debugName: "margin" }] : /* istanbul ignore next */ []));
3310
+ trackPadding = input(4, ...(ngDevMode ? [{ debugName: "trackPadding" }] : /* istanbul ignore next */ []));
3311
+ cornerRadius = input(8, ...(ngDevMode ? [{ debugName: "cornerRadius" }] : /* istanbul ignore next */ []));
3312
+ startAngle = input(-Math.PI / 2, ...(ngDevMode ? [{ debugName: "startAngle" }] : /* istanbul ignore next */ []));
3313
+ endAngle = input((3 * Math.PI) / 2, ...(ngDevMode ? [{ debugName: "endAngle" }] : /* istanbul ignore next */ []));
3314
+ maxValue = input(undefined, ...(ngDevMode ? [{ debugName: "maxValue" }] : /* istanbul ignore next */ []));
3315
+ showTrack = input(true, ...(ngDevMode ? [{ debugName: "showTrack" }] : /* istanbul ignore next */ []));
3316
+ showValueLabels = input(false, ...(ngDevMode ? [{ debugName: "showValueLabels" }] : /* istanbul ignore next */ []));
3317
+ valueLabelFormat = input(defaultRadialValueFormatter, ...(ngDevMode ? [{ debugName: "valueLabelFormat" }] : /* istanbul ignore next */ []));
3318
+ barClick = output();
3319
+ innerWidth = computed(() => Math.max(0, this.root.dimensions().width - this.margin().left - this.margin().right), ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
3320
+ innerHeight = computed(() => Math.max(0, this.root.dimensions().height - this.margin().top - this.margin().bottom), ...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
3321
+ layout = computed(() => computeRadialLayout({
3322
+ data: this.data(),
3323
+ nameKey: this.nameKey(),
3324
+ valueKey: this.valueKey(),
3325
+ seriesKeys: this.seriesKeys(),
3326
+ innerWidth: this.innerWidth(),
3327
+ innerHeight: this.innerHeight(),
3328
+ trackPadding: this.trackPadding(),
3329
+ startAngle: this.startAngle(),
3330
+ endAngle: this.endAngle(),
3331
+ cornerRadius: this.cornerRadius(),
3332
+ maxValue: this.maxValue(),
3333
+ }), ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
3334
+ viewBox = computed(() => {
3335
+ const { width, height } = this.root.dimensions();
3336
+ return `0 0 ${Math.max(0, width)} ${Math.max(0, height)}`;
3337
+ }, ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
3338
+ innerTransform = computed(() => `translate(${this.margin().left},${this.margin().top})`, ...(ngDevMode ? [{ debugName: "innerTransform" }] : /* istanbul ignore next */ []));
3339
+ ariaSummary = computed(() => `Radial bar chart, ${this.data().length} tracks.`, ...(ngDevMode ? [{ debugName: "ariaSummary" }] : /* istanbul ignore next */ []));
3340
+ barAriaLabel(b) {
3341
+ return `${b.name}: ${b.value}`;
3342
+ }
3343
+ formatValueLabel(b) {
3344
+ return this.valueLabelFormat()(b.value, b.name);
3345
+ }
3346
+ barLabelX(b) {
3347
+ const radius = (b.innerRadius + b.outerRadius) / 2 + 10;
3348
+ return Math.sin(b.endAngle) * radius;
3349
+ }
3350
+ barLabelY(b) {
3351
+ const radius = (b.innerRadius + b.outerRadius) / 2 + 10;
3352
+ return -Math.cos(b.endAngle) * radius;
3353
+ }
3354
+ barLabelAnchor(b) {
3355
+ return this.barLabelX(b) >= 0 ? 'start' : 'end';
3356
+ }
3357
+ emitClick(b) {
3358
+ this.barClick.emit({
3359
+ seriesKey: b.seriesKey,
3360
+ name: b.name,
3361
+ value: b.value,
3362
+ datumIndex: b.datumIndex,
3363
+ datum: this.data()[b.datumIndex],
3364
+ });
3365
+ }
3366
+ setActive(event, b) {
3367
+ const clientX = event instanceof PointerEvent ? event.clientX : event.target.getBoundingClientRect().left;
3368
+ const clientY = event instanceof PointerEvent ? event.clientY : event.target.getBoundingClientRect().top;
3369
+ this.root.activePoint.set({
3370
+ index: b.datumIndex,
3371
+ datumIndex: b.datumIndex,
3372
+ seriesKey: b.seriesKey,
3373
+ clientX,
3374
+ clientY,
3375
+ });
3376
+ }
3377
+ clearActive() {
3378
+ this.root.activePoint.set(null);
3379
+ }
3380
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RadialChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
3381
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: RadialChart, isStandalone: true, selector: "ui-radial-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, nameKey: { classPropertyName: "nameKey", publicName: "nameKey", isSignal: true, isRequired: true, transformFunction: null }, valueKey: { classPropertyName: "valueKey", publicName: "valueKey", isSignal: true, isRequired: true, transformFunction: null }, seriesKeys: { classPropertyName: "seriesKeys", publicName: "seriesKeys", isSignal: true, isRequired: false, transformFunction: null }, margin: { classPropertyName: "margin", publicName: "margin", isSignal: true, isRequired: false, transformFunction: null }, trackPadding: { classPropertyName: "trackPadding", publicName: "trackPadding", isSignal: true, isRequired: false, transformFunction: null }, cornerRadius: { classPropertyName: "cornerRadius", publicName: "cornerRadius", isSignal: true, isRequired: false, transformFunction: null }, startAngle: { classPropertyName: "startAngle", publicName: "startAngle", isSignal: true, isRequired: false, transformFunction: null }, endAngle: { classPropertyName: "endAngle", publicName: "endAngle", isSignal: true, isRequired: false, transformFunction: null }, maxValue: { classPropertyName: "maxValue", publicName: "maxValue", isSignal: true, isRequired: false, transformFunction: null }, showTrack: { classPropertyName: "showTrack", publicName: "showTrack", isSignal: true, isRequired: false, transformFunction: null }, showValueLabels: { classPropertyName: "showValueLabels", publicName: "showValueLabels", isSignal: true, isRequired: false, transformFunction: null }, valueLabelFormat: { classPropertyName: "valueLabelFormat", publicName: "valueLabelFormat", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { barClick: "barClick" }, host: { classAttribute: "relative block h-full w-full" }, ngImport: i0, template: `
3382
+ <svg:svg
3383
+ class="block h-full w-full overflow-visible"
3384
+ [attr.viewBox]="viewBox()"
3385
+ preserveAspectRatio="xMidYMid meet"
3386
+ role="img"
3387
+ [attr.aria-label]="ariaSummary()">
3388
+ <svg:g [attr.transform]="innerTransform()">
3389
+ <svg:g [attr.transform]="'translate(' + layout().centerX + ',' + layout().centerY + ')'">
3390
+ @for (b of layout().bars; track b.seriesKey) {
3391
+ @if (showTrack()) {
3392
+ <svg:path class="chart-radial-track fill-muted" [attr.d]="b.backgroundPath" />
3393
+ }
3394
+ <svg:path
3395
+ class="chart-radial-bar cursor-pointer transition-opacity hover:opacity-80"
3396
+ [attr.d]="b.arcPath"
3397
+ [attr.fill]="b.color"
3398
+ [attr.aria-label]="barAriaLabel(b)"
3399
+ tabindex="0"
3400
+ (click)="emitClick(b)"
3401
+ (keydown.enter)="emitClick(b)"
3402
+ (keydown.space)="emitClick(b); $event.preventDefault()"
3403
+ (pointerenter)="setActive($event, b)"
3404
+ (pointermove)="setActive($event, b)"
3405
+ (pointerleave)="clearActive()"
3406
+ (focus)="setActive($event, b)"
3407
+ (blur)="clearActive()" />
3408
+ @if (showValueLabels()) {
3409
+ <svg:text
3410
+ class="chart-radial-value pointer-events-none fill-muted-foreground text-[10px]"
3411
+ [attr.x]="barLabelX(b)"
3412
+ [attr.y]="barLabelY(b)"
3413
+ [attr.text-anchor]="barLabelAnchor(b)"
3414
+ dominant-baseline="middle">
3415
+ {{ formatValueLabel(b) }}
3416
+ </svg:text>
3417
+ }
3418
+ }
3419
+ </svg:g>
3420
+ </svg:g>
3421
+ </svg:svg>
3422
+ <div class="pointer-events-none absolute inset-0 flex items-center justify-center">
3423
+ <ng-content select="ui-radial-center" />
3424
+ </div>
3425
+ <ng-content select="ui-chart-tooltip" />
3426
+ <ng-content select="ui-chart-legend" />
3427
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
3428
+ }
3429
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RadialChart, decorators: [{
3430
+ type: Component,
3431
+ args: [{
3432
+ selector: 'ui-radial-chart',
3433
+ changeDetection: ChangeDetectionStrategy.OnPush,
3434
+ host: { class: 'relative block h-full w-full' },
3435
+ template: `
3436
+ <svg:svg
3437
+ class="block h-full w-full overflow-visible"
3438
+ [attr.viewBox]="viewBox()"
3439
+ preserveAspectRatio="xMidYMid meet"
3440
+ role="img"
3441
+ [attr.aria-label]="ariaSummary()">
3442
+ <svg:g [attr.transform]="innerTransform()">
3443
+ <svg:g [attr.transform]="'translate(' + layout().centerX + ',' + layout().centerY + ')'">
3444
+ @for (b of layout().bars; track b.seriesKey) {
3445
+ @if (showTrack()) {
3446
+ <svg:path class="chart-radial-track fill-muted" [attr.d]="b.backgroundPath" />
3447
+ }
3448
+ <svg:path
3449
+ class="chart-radial-bar cursor-pointer transition-opacity hover:opacity-80"
3450
+ [attr.d]="b.arcPath"
3451
+ [attr.fill]="b.color"
3452
+ [attr.aria-label]="barAriaLabel(b)"
3453
+ tabindex="0"
3454
+ (click)="emitClick(b)"
3455
+ (keydown.enter)="emitClick(b)"
3456
+ (keydown.space)="emitClick(b); $event.preventDefault()"
3457
+ (pointerenter)="setActive($event, b)"
3458
+ (pointermove)="setActive($event, b)"
3459
+ (pointerleave)="clearActive()"
3460
+ (focus)="setActive($event, b)"
3461
+ (blur)="clearActive()" />
3462
+ @if (showValueLabels()) {
3463
+ <svg:text
3464
+ class="chart-radial-value pointer-events-none fill-muted-foreground text-[10px]"
3465
+ [attr.x]="barLabelX(b)"
3466
+ [attr.y]="barLabelY(b)"
3467
+ [attr.text-anchor]="barLabelAnchor(b)"
3468
+ dominant-baseline="middle">
3469
+ {{ formatValueLabel(b) }}
3470
+ </svg:text>
3471
+ }
3472
+ }
3473
+ </svg:g>
3474
+ </svg:g>
3475
+ </svg:svg>
3476
+ <div class="pointer-events-none absolute inset-0 flex items-center justify-center">
3477
+ <ng-content select="ui-radial-center" />
3478
+ </div>
3479
+ <ng-content select="ui-chart-tooltip" />
3480
+ <ng-content select="ui-chart-legend" />
3481
+ `,
3482
+ }]
3483
+ }], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], nameKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "nameKey", required: true }] }], valueKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueKey", required: true }] }], seriesKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "seriesKeys", required: false }] }], margin: [{ type: i0.Input, args: [{ isSignal: true, alias: "margin", required: false }] }], trackPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackPadding", required: false }] }], cornerRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "cornerRadius", required: false }] }], startAngle: [{ type: i0.Input, args: [{ isSignal: true, alias: "startAngle", required: false }] }], endAngle: [{ type: i0.Input, args: [{ isSignal: true, alias: "endAngle", required: false }] }], maxValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxValue", required: false }] }], showTrack: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTrack", required: false }] }], showValueLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showValueLabels", required: false }] }], valueLabelFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueLabelFormat", required: false }] }], barClick: [{ type: i0.Output, args: ["barClick"] }] } });
3484
+
3485
+ function nice(extent) {
3486
+ const [lo, hi] = extent;
3487
+ if (lo === undefined || hi === undefined)
3488
+ return [0, 1];
3489
+ if (lo === hi)
3490
+ return [lo - 1, hi + 1];
3491
+ return [lo, hi];
3492
+ }
3493
+ function resolveScatterDomains(input) {
3494
+ const xValues = input.data.map((d) => Number(d[input.xKey] ?? 0));
3495
+ const yValues = input.data.map((d) => Number(d[input.yKey] ?? 0));
3496
+ return {
3497
+ xDomain: (input.xDomain ?? nice(extent(xValues))),
3498
+ yDomain: (input.yDomain ?? nice(extent(yValues))),
3499
+ };
3500
+ }
3501
+ function computeScatterLayout(input) {
3502
+ const { data, xKey, yKey, sizeKey, seriesKey, seriesKeys, innerWidth, innerHeight, minPointRadius, maxPointRadius } = input;
3503
+ const xValues = data.map((d) => Number(d[xKey] ?? 0));
3504
+ const yValues = data.map((d) => Number(d[yKey] ?? 0));
3505
+ const sizeValues = sizeKey ? data.map((d) => Number(d[sizeKey] ?? 0)) : [];
3506
+ const { xDomain, yDomain } = resolveScatterDomains(input);
3507
+ const xScale = scaleLinear()
3508
+ .domain(xDomain)
3509
+ .range([0, innerWidth]);
3510
+ const yScale = scaleLinear()
3511
+ .domain(yDomain)
3512
+ .range([innerHeight, 0]);
3513
+ let sizeScale = null;
3514
+ if (sizeKey && sizeValues.length > 0) {
3515
+ const [sLo, sHi] = extent(sizeValues);
3516
+ sizeScale = scaleLinear()
3517
+ .domain([sLo ?? 0, sHi ?? 1])
3518
+ .range([minPointRadius, maxPointRadius]);
3519
+ }
3520
+ const fallbackKey = seriesKeys[0] ?? 'value';
3521
+ const points = data.flatMap((d, i) => {
3522
+ const key = seriesKey ? String(d[seriesKey] ?? fallbackKey) : fallbackKey;
3523
+ const sz = sizeKey ? Number(d[sizeKey] ?? 0) : 0;
3524
+ const rawX = xValues[i];
3525
+ const rawY = yValues[i];
3526
+ if (rawX < xDomain[0] || rawX > xDomain[1] || rawY < yDomain[0] || rawY > yDomain[1]) {
3527
+ return [];
3528
+ }
3529
+ return [
3530
+ {
3531
+ seriesKey: key,
3532
+ color: seriesColorVar(key),
3533
+ x: xScale(rawX),
3534
+ y: yScale(rawY),
3535
+ radius: sizeScale ? sizeScale(sz) : minPointRadius,
3536
+ datumIndex: i,
3537
+ rawX,
3538
+ rawY,
3539
+ rawSize: sz,
3540
+ },
3541
+ ];
3542
+ });
3543
+ return { points, xScale, yScale, xDomain, yDomain };
3544
+ }
3545
+
3546
+ const DEFAULT_MARGIN = { top: 8, right: 8, bottom: 24, left: 40 };
3547
+ /**
3548
+ * Scatter chart — one dot per datum. Both axes are linear; color can
3549
+ * come from a fixed series key or per-row via `seriesKey` field. Point
3550
+ * radius optionally scales with `sizeKey`.
3551
+ */
3552
+ class ScatterChart {
3553
+ root = inject(ChartContext);
3554
+ viewport = inject(ScatterViewportContext);
3555
+ data = input.required(...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
3556
+ xKey = input.required(...(ngDevMode ? [{ debugName: "xKey" }] : /* istanbul ignore next */ []));
3557
+ yKey = input.required(...(ngDevMode ? [{ debugName: "yKey" }] : /* istanbul ignore next */ []));
3558
+ /** Optional numeric field to drive point radius. */
3559
+ sizeKey = input(undefined, ...(ngDevMode ? [{ debugName: "sizeKey" }] : /* istanbul ignore next */ []));
3560
+ /** Optional field on each datum used as color key. */
3561
+ seriesKey = input(undefined, ...(ngDevMode ? [{ debugName: "seriesKey" }] : /* istanbul ignore next */ []));
3562
+ margin = input(DEFAULT_MARGIN, ...(ngDevMode ? [{ debugName: "margin" }] : /* istanbul ignore next */ []));
3563
+ minPointRadius = input(3, ...(ngDevMode ? [{ debugName: "minPointRadius" }] : /* istanbul ignore next */ []));
3564
+ maxPointRadius = input(12, ...(ngDevMode ? [{ debugName: "maxPointRadius" }] : /* istanbul ignore next */ []));
3565
+ xDomain = input(undefined, ...(ngDevMode ? [{ debugName: "xDomain" }] : /* istanbul ignore next */ []));
3566
+ yDomain = input(undefined, ...(ngDevMode ? [{ debugName: "yDomain" }] : /* istanbul ignore next */ []));
3567
+ pointClick = output();
3568
+ innerWidth = computed(() => Math.max(0, this.root.dimensions().width - this.margin().left - this.margin().right), ...(ngDevMode ? [{ debugName: "innerWidth" }] : /* istanbul ignore next */ []));
3569
+ innerHeight = computed(() => Math.max(0, this.root.dimensions().height - this.margin().top - this.margin().bottom), ...(ngDevMode ? [{ debugName: "innerHeight" }] : /* istanbul ignore next */ []));
3570
+ resolvedDomains = computed(() => resolveScatterDomains({
3571
+ data: this.data(),
3572
+ xKey: this.xKey(),
3573
+ yKey: this.yKey(),
3574
+ xDomain: this.xDomain(),
3575
+ yDomain: this.yDomain(),
3576
+ }), ...(ngDevMode ? [{ debugName: "resolvedDomains" }] : /* istanbul ignore next */ []));
3577
+ currentXDomain = computed(() => this.viewport.zoomXDomain() ?? this.resolvedDomains().xDomain, ...(ngDevMode ? [{ debugName: "currentXDomain" }] : /* istanbul ignore next */ []));
3578
+ currentYDomain = computed(() => this.viewport.zoomYDomain() ?? this.resolvedDomains().yDomain, ...(ngDevMode ? [{ debugName: "currentYDomain" }] : /* istanbul ignore next */ []));
3579
+ layout = computed(() => computeScatterLayout({
3580
+ data: this.data(),
3581
+ xKey: this.xKey(),
3582
+ yKey: this.yKey(),
3583
+ sizeKey: this.sizeKey(),
3584
+ seriesKey: this.seriesKey(),
3585
+ seriesKeys: this.root.visibleSeriesKeys(),
3586
+ innerWidth: this.innerWidth(),
3587
+ innerHeight: this.innerHeight(),
3588
+ minPointRadius: this.minPointRadius(),
3589
+ maxPointRadius: this.maxPointRadius(),
3590
+ xDomain: this.currentXDomain(),
3591
+ yDomain: this.currentYDomain(),
3592
+ }), ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
3593
+ viewBox = computed(() => {
3594
+ const { width, height } = this.root.dimensions();
3595
+ return `0 0 ${Math.max(0, width)} ${Math.max(0, height)}`;
3596
+ }, ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
3597
+ innerTransform = computed(() => `translate(${this.margin().left},${this.margin().top})`, ...(ngDevMode ? [{ debugName: "innerTransform" }] : /* istanbul ignore next */ []));
3598
+ clipPathId = computed(() => `${this.root.id()}-scatter-clip`, ...(ngDevMode ? [{ debugName: "clipPathId" }] : /* istanbul ignore next */ []));
3599
+ ariaSummary = computed(() => `Scatter chart, ${this.data().length} points.`, ...(ngDevMode ? [{ debugName: "ariaSummary" }] : /* istanbul ignore next */ []));
3600
+ constructor() {
3601
+ effect(() => {
3602
+ const domains = this.resolvedDomains();
3603
+ const layout = this.layout();
3604
+ this.viewport.innerWidth.set(this.innerWidth());
3605
+ this.viewport.innerHeight.set(this.innerHeight());
3606
+ this.viewport.fullXDomain.set(domains.xDomain);
3607
+ this.viewport.fullYDomain.set(domains.yDomain);
3608
+ this.viewport.xScale.set(layout.xScale);
3609
+ this.viewport.yScale.set(layout.yScale);
3610
+ });
3611
+ }
3612
+ pointAriaLabel(p) {
3613
+ return `${p.seriesKey}: x=${p.rawX}, y=${p.rawY}`;
3614
+ }
3615
+ emitClick(p) {
3616
+ this.pointClick.emit({
3617
+ seriesKey: p.seriesKey,
3618
+ datumIndex: p.datumIndex,
3619
+ x: p.rawX,
3620
+ y: p.rawY,
3621
+ datum: this.data()[p.datumIndex],
3622
+ });
3623
+ }
3624
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ScatterChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
3625
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: ScatterChart, isStandalone: true, selector: "ui-scatter-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, xKey: { classPropertyName: "xKey", publicName: "xKey", isSignal: true, isRequired: true, transformFunction: null }, yKey: { classPropertyName: "yKey", publicName: "yKey", isSignal: true, isRequired: true, transformFunction: null }, sizeKey: { classPropertyName: "sizeKey", publicName: "sizeKey", isSignal: true, isRequired: false, transformFunction: null }, seriesKey: { classPropertyName: "seriesKey", publicName: "seriesKey", isSignal: true, isRequired: false, transformFunction: null }, margin: { classPropertyName: "margin", publicName: "margin", isSignal: true, isRequired: false, transformFunction: null }, minPointRadius: { classPropertyName: "minPointRadius", publicName: "minPointRadius", isSignal: true, isRequired: false, transformFunction: null }, maxPointRadius: { classPropertyName: "maxPointRadius", publicName: "maxPointRadius", isSignal: true, isRequired: false, transformFunction: null }, xDomain: { classPropertyName: "xDomain", publicName: "xDomain", isSignal: true, isRequired: false, transformFunction: null }, yDomain: { classPropertyName: "yDomain", publicName: "yDomain", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pointClick: "pointClick" }, host: { classAttribute: "relative block h-full w-full" }, providers: [ScatterViewportContext], ngImport: i0, template: `
3626
+ <svg:svg
3627
+ class="block h-full w-full overflow-visible"
3628
+ [attr.viewBox]="viewBox()"
3629
+ preserveAspectRatio="none"
3630
+ role="img"
3631
+ [attr.aria-label]="ariaSummary()">
3632
+ <svg:defs>
3633
+ <svg:clipPath [attr.id]="clipPathId()">
3634
+ <svg:rect x="0" y="0" [attr.width]="innerWidth()" [attr.height]="innerHeight()" />
3635
+ </svg:clipPath>
3636
+ </svg:defs>
3637
+ <svg:g [attr.transform]="innerTransform()">
3638
+ <svg:g class="chart-scatter" [attr.clip-path]="'url(#' + clipPathId() + ')'">
3639
+ @for (p of layout().points; track p.datumIndex) {
3640
+ <svg:circle
3641
+ class="chart-scatter-point cursor-pointer transition-opacity hover:opacity-80"
3642
+ [attr.cx]="p.x"
3643
+ [attr.cy]="p.y"
3644
+ [attr.r]="p.radius"
3645
+ [attr.fill]="p.color"
3646
+ [attr.aria-label]="pointAriaLabel(p)"
3647
+ tabindex="0"
3648
+ (click)="emitClick(p)"
3649
+ (keydown.enter)="emitClick(p)"
3650
+ (keydown.space)="emitClick(p); $event.preventDefault()" />
3651
+ }
3652
+ </svg:g>
3653
+ <ng-content select="svg:g[ui-chart-brush]" />
3654
+ </svg:g>
3655
+ </svg:svg>
3656
+ <ng-content select="ui-chart-legend" />
3657
+ <ng-content select="ui-chart-zoom-controls" />
3658
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
3659
+ }
3660
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ScatterChart, decorators: [{
3661
+ type: Component,
3662
+ args: [{
3663
+ selector: 'ui-scatter-chart',
3664
+ changeDetection: ChangeDetectionStrategy.OnPush,
3665
+ providers: [ScatterViewportContext],
3666
+ host: { class: 'relative block h-full w-full' },
3667
+ template: `
3668
+ <svg:svg
3669
+ class="block h-full w-full overflow-visible"
3670
+ [attr.viewBox]="viewBox()"
3671
+ preserveAspectRatio="none"
3672
+ role="img"
3673
+ [attr.aria-label]="ariaSummary()">
3674
+ <svg:defs>
3675
+ <svg:clipPath [attr.id]="clipPathId()">
3676
+ <svg:rect x="0" y="0" [attr.width]="innerWidth()" [attr.height]="innerHeight()" />
3677
+ </svg:clipPath>
3678
+ </svg:defs>
3679
+ <svg:g [attr.transform]="innerTransform()">
3680
+ <svg:g class="chart-scatter" [attr.clip-path]="'url(#' + clipPathId() + ')'">
3681
+ @for (p of layout().points; track p.datumIndex) {
3682
+ <svg:circle
3683
+ class="chart-scatter-point cursor-pointer transition-opacity hover:opacity-80"
3684
+ [attr.cx]="p.x"
3685
+ [attr.cy]="p.y"
3686
+ [attr.r]="p.radius"
3687
+ [attr.fill]="p.color"
3688
+ [attr.aria-label]="pointAriaLabel(p)"
3689
+ tabindex="0"
3690
+ (click)="emitClick(p)"
3691
+ (keydown.enter)="emitClick(p)"
3692
+ (keydown.space)="emitClick(p); $event.preventDefault()" />
3693
+ }
3694
+ </svg:g>
3695
+ <ng-content select="svg:g[ui-chart-brush]" />
3696
+ </svg:g>
3697
+ </svg:svg>
3698
+ <ng-content select="ui-chart-legend" />
3699
+ <ng-content select="ui-chart-zoom-controls" />
3700
+ `,
3701
+ }]
3702
+ }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], xKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "xKey", required: true }] }], yKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "yKey", required: true }] }], sizeKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "sizeKey", required: false }] }], seriesKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "seriesKey", required: false }] }], margin: [{ type: i0.Input, args: [{ isSignal: true, alias: "margin", required: false }] }], minPointRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "minPointRadius", required: false }] }], maxPointRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxPointRadius", required: false }] }], xDomain: [{ type: i0.Input, args: [{ isSignal: true, alias: "xDomain", required: false }] }], yDomain: [{ type: i0.Input, args: [{ isSignal: true, alias: "yDomain", required: false }] }], pointClick: [{ type: i0.Output, args: ["pointClick"] }] } });
3703
+
3704
+ /*
3705
+ * Public API Surface of @ojiepermana/angular/chart
3706
+ */
3707
+ // Core
3708
+
3709
+ /**
3710
+ * Generated bundle index. Do not edit.
3711
+ */
3712
+
3713
+ export { AreaChart, BarChart, CHART_DATA_ATTRIBUTE, CHART_THEMES, CartesianContext, ChartAxisX, ChartAxisY, ChartBrush, ChartContainer, ChartContext, ChartCrosshair, ChartGrid, ChartLegend, ChartPointerTracker, ChartStyle, ChartTooltip, ChartZoomControls, LineChart, PieCenter, PieChart, RadarChart, RadialCenter, RadialChart, ScatterChart, bandTicks, buildCartesianScales, buildChartCss, cloneLinear, computeAreaLayout, computeBarLayout, computeLineLayout, computePieLayout, computeRadarLayout, computeRadialLayout, computeScatterLayout, effectiveIndexRange, indexRangeSize, linearTicks, normalizeIndexRange, normalizeNumericDomain, panIndexRange, panNumericDomain, pointToBandAdapter, provideCartesianFromLineLayout, resolveScatterDomains, seriesColorVar, sliceByIndexRange, xScale, yScale, zoomIndexRange, zoomNumericDomain };
3714
+ //# sourceMappingURL=ojiepermana-angular-chart.mjs.map