@ojiepermana/angular-chart 22.0.27

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 (32) hide show
  1. package/README.md +249 -0
  2. package/fesm2022/ojiepermana-angular-chart-area.mjs +266 -0
  3. package/fesm2022/ojiepermana-angular-chart-area.mjs.map +1 -0
  4. package/fesm2022/ojiepermana-angular-chart-bar.mjs +674 -0
  5. package/fesm2022/ojiepermana-angular-chart-bar.mjs.map +1 -0
  6. package/fesm2022/ojiepermana-angular-chart-core.mjs +764 -0
  7. package/fesm2022/ojiepermana-angular-chart-core.mjs.map +1 -0
  8. package/fesm2022/ojiepermana-angular-chart-line.mjs +281 -0
  9. package/fesm2022/ojiepermana-angular-chart-line.mjs.map +1 -0
  10. package/fesm2022/ojiepermana-angular-chart-pie.mjs +248 -0
  11. package/fesm2022/ojiepermana-angular-chart-pie.mjs.map +1 -0
  12. package/fesm2022/ojiepermana-angular-chart-primitives.mjs +1186 -0
  13. package/fesm2022/ojiepermana-angular-chart-primitives.mjs.map +1 -0
  14. package/fesm2022/ojiepermana-angular-chart-radar.mjs +329 -0
  15. package/fesm2022/ojiepermana-angular-chart-radar.mjs.map +1 -0
  16. package/fesm2022/ojiepermana-angular-chart-radial.mjs +255 -0
  17. package/fesm2022/ojiepermana-angular-chart-radial.mjs.map +1 -0
  18. package/fesm2022/ojiepermana-angular-chart-scatter.mjs +253 -0
  19. package/fesm2022/ojiepermana-angular-chart-scatter.mjs.map +1 -0
  20. package/fesm2022/ojiepermana-angular-chart.mjs +20 -0
  21. package/fesm2022/ojiepermana-angular-chart.mjs.map +1 -0
  22. package/package.json +76 -0
  23. package/types/ojiepermana-angular-chart-area.d.ts +58 -0
  24. package/types/ojiepermana-angular-chart-bar.d.ts +171 -0
  25. package/types/ojiepermana-angular-chart-core.d.ts +369 -0
  26. package/types/ojiepermana-angular-chart-line.d.ts +57 -0
  27. package/types/ojiepermana-angular-chart-pie.d.ts +93 -0
  28. package/types/ojiepermana-angular-chart-primitives.d.ts +265 -0
  29. package/types/ojiepermana-angular-chart-radar.d.ts +89 -0
  30. package/types/ojiepermana-angular-chart-radial.d.ts +86 -0
  31. package/types/ojiepermana-angular-chart-scatter.d.ts +95 -0
  32. package/types/ojiepermana-angular-chart.d.ts +2 -0
@@ -0,0 +1,1186 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, input, computed, ChangeDetectionStrategy, Component, viewChild, signal, Directive, ElementRef, contentChild, TemplateRef } from '@angular/core';
3
+ import { CartesianContext, linearTicks, bandTicks, ChartContext, CategoricalViewportContext, ScatterViewportContext, nearestCategoryIndex, normalizeIndexRange, panIndexRange, panNumericDomain, indexRangeSize, normalizeNumericDomain, zoomNumericDomain, zoomIndexRange, seriesColorVar } from '@ojiepermana/angular-chart/core';
4
+ import { NgTemplateOutlet, NgComponentOutlet } from '@angular/common';
5
+
6
+ /**
7
+ * X axis for a cartesian chart. Reads scales from `CartesianContext`.
8
+ *
9
+ * Renders as `<svg:g>` — must be placed inside the owning chart's SVG.
10
+ */
11
+ class ChartAxisX {
12
+ ctx = inject(CartesianContext);
13
+ /** Approximate tick count for linear (value) scales. */
14
+ tickCount = input(5, /* @ts-ignore */
15
+ ...(ngDevMode ? [{ debugName: "tickCount" }] : /* istanbul ignore next */ []));
16
+ /** Show 6-px tick marks between the axis line and the labels. */
17
+ tickLine = input(true, /* @ts-ignore */
18
+ ...(ngDevMode ? [{ debugName: "tickLine" }] : /* istanbul ignore next */ []));
19
+ /** Formatter for numeric tick labels. */
20
+ tickFormat = input((v) => String(v), /* @ts-ignore */
21
+ ...(ngDevMode ? [{ debugName: "tickFormat" }] : /* istanbul ignore next */ []));
22
+ innerWidth = this.ctx.innerWidth;
23
+ transform = computed(() => `translate(0,${this.ctx.innerHeight()})`, /* @ts-ignore */
24
+ ...(ngDevMode ? [{ debugName: "transform" }] : /* istanbul ignore next */ []));
25
+ ticks = computed(() => {
26
+ const horizontal = this.ctx.orientation() === 'horizontal';
27
+ if (horizontal) {
28
+ const scale = this.ctx.valueScale();
29
+ return scale ? linearTicks(scale, this.tickCount(), this.tickFormat()) : [];
30
+ }
31
+ const scale = this.ctx.categoryScale();
32
+ return scale ? bandTicks(scale) : [];
33
+ }, /* @ts-ignore */
34
+ ...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
35
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartAxisX, deps: [], target: i0.ɵɵFactoryTarget.Component });
36
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ChartAxisX, isStandalone: true, selector: "svg:g[ChartAxisX]", 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: `
37
+ <svg:line class="stroke-border" [attr.x1]="0" [attr.x2]="innerWidth()" y1="0" y2="0" />
38
+ @for (t of ticks(); track t.value) {
39
+ <svg:g [attr.transform]="'translate(' + t.offset + ',0)'">
40
+ @if (tickLine()) {
41
+ <svg:line class="stroke-border" y1="0" y2="6" />
42
+ }
43
+ <svg:text
44
+ class="fill-current"
45
+ y="18"
46
+ text-anchor="middle"
47
+ style="font-size: var(--text-xs); font-family: var(--font-sans)">
48
+ {{ t.label }}
49
+ </svg:text>
50
+ </svg:g>
51
+ }
52
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
53
+ }
54
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartAxisX, decorators: [{
55
+ type: Component,
56
+ args: [{
57
+ selector: 'svg:g[ChartAxisX]',
58
+ changeDetection: ChangeDetectionStrategy.OnPush,
59
+ host: {
60
+ class: 'chart-axis chart-axis-x text-muted-foreground',
61
+ '[attr.transform]': 'transform()',
62
+ },
63
+ template: `
64
+ <svg:line class="stroke-border" [attr.x1]="0" [attr.x2]="innerWidth()" y1="0" y2="0" />
65
+ @for (t of ticks(); track t.value) {
66
+ <svg:g [attr.transform]="'translate(' + t.offset + ',0)'">
67
+ @if (tickLine()) {
68
+ <svg:line class="stroke-border" y1="0" y2="6" />
69
+ }
70
+ <svg:text
71
+ class="fill-current"
72
+ y="18"
73
+ text-anchor="middle"
74
+ style="font-size: var(--text-xs); font-family: var(--font-sans)">
75
+ {{ t.label }}
76
+ </svg:text>
77
+ </svg:g>
78
+ }
79
+ `,
80
+ }]
81
+ }], 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 }] }] } });
82
+
83
+ /**
84
+ * Y axis for a cartesian chart. Reads scales from `CartesianContext`.
85
+ *
86
+ * Renders as `<svg:g>` — must be placed inside the owning chart's SVG.
87
+ */
88
+ class ChartAxisY {
89
+ ctx = inject(CartesianContext);
90
+ tickCount = input(5, /* @ts-ignore */
91
+ ...(ngDevMode ? [{ debugName: "tickCount" }] : /* istanbul ignore next */ []));
92
+ tickLine = input(true, /* @ts-ignore */
93
+ ...(ngDevMode ? [{ debugName: "tickLine" }] : /* istanbul ignore next */ []));
94
+ tickFormat = input((v) => String(v), /* @ts-ignore */
95
+ ...(ngDevMode ? [{ debugName: "tickFormat" }] : /* istanbul ignore next */ []));
96
+ innerHeight = this.ctx.innerHeight;
97
+ ticks = computed(() => {
98
+ const horizontal = this.ctx.orientation() === 'horizontal';
99
+ if (horizontal) {
100
+ const scale = this.ctx.categoryScale();
101
+ return scale ? bandTicks(scale) : [];
102
+ }
103
+ const scale = this.ctx.valueScale();
104
+ return scale ? linearTicks(scale, this.tickCount(), this.tickFormat()) : [];
105
+ }, /* @ts-ignore */
106
+ ...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
107
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartAxisY, deps: [], target: i0.ɵɵFactoryTarget.Component });
108
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ChartAxisY, isStandalone: true, selector: "svg:g[ChartAxisY]", 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: `
109
+ <svg:line class="stroke-border" x1="0" x2="0" [attr.y1]="0" [attr.y2]="innerHeight()" />
110
+ @for (t of ticks(); track t.value) {
111
+ <svg:g [attr.transform]="'translate(0,' + t.offset + ')'">
112
+ @if (tickLine()) {
113
+ <svg:line class="stroke-border" x1="-6" x2="0" />
114
+ }
115
+ <svg:text
116
+ class="fill-current"
117
+ x="-8"
118
+ dy="0.32em"
119
+ text-anchor="end"
120
+ style="font-size: var(--text-xs); font-family: var(--font-sans)">
121
+ {{ t.label }}
122
+ </svg:text>
123
+ </svg:g>
124
+ }
125
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
126
+ }
127
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartAxisY, decorators: [{
128
+ type: Component,
129
+ args: [{
130
+ selector: 'svg:g[ChartAxisY]',
131
+ changeDetection: ChangeDetectionStrategy.OnPush,
132
+ host: {
133
+ class: 'chart-axis chart-axis-y text-muted-foreground',
134
+ },
135
+ template: `
136
+ <svg:line class="stroke-border" x1="0" x2="0" [attr.y1]="0" [attr.y2]="innerHeight()" />
137
+ @for (t of ticks(); track t.value) {
138
+ <svg:g [attr.transform]="'translate(0,' + t.offset + ')'">
139
+ @if (tickLine()) {
140
+ <svg:line class="stroke-border" x1="-6" x2="0" />
141
+ }
142
+ <svg:text
143
+ class="fill-current"
144
+ x="-8"
145
+ dy="0.32em"
146
+ text-anchor="end"
147
+ style="font-size: var(--text-xs); font-family: var(--font-sans)">
148
+ {{ t.label }}
149
+ </svg:text>
150
+ </svg:g>
151
+ }
152
+ `,
153
+ }]
154
+ }], 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 }] }] } });
155
+
156
+ /**
157
+ * Horizontal / vertical grid lines for the cartesian plotting area.
158
+ *
159
+ * Reads tick positions from `CartesianContext.valueScale`. Direction of the
160
+ * grid lines follows `orientation`:
161
+ * - vertical → horizontal grid lines (one per y-tick)
162
+ * - horizontal → vertical grid lines (one per x-tick)
163
+ */
164
+ class ChartGrid {
165
+ ctx = inject(CartesianContext);
166
+ tickCount = input(5, /* @ts-ignore */
167
+ ...(ngDevMode ? [{ debugName: "tickCount" }] : /* istanbul ignore next */ []));
168
+ ticks = computed(() => {
169
+ const scale = this.ctx.valueScale();
170
+ return scale ? linearTicks(scale, this.tickCount()) : [];
171
+ }, /* @ts-ignore */
172
+ ...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
173
+ line = (offset) => {
174
+ if (this.ctx.orientation() === 'vertical') {
175
+ return { x1: 0, x2: this.ctx.innerWidth(), y1: offset, y2: offset };
176
+ }
177
+ return { x1: offset, x2: offset, y1: 0, y2: this.ctx.innerHeight() };
178
+ };
179
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartGrid, deps: [], target: i0.ɵɵFactoryTarget.Component });
180
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ChartGrid, isStandalone: true, selector: "svg:g[ChartGrid]", inputs: { tickCount: { classPropertyName: "tickCount", publicName: "tickCount", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "chart-grid text-border" }, ngImport: i0, template: `
181
+ @for (t of ticks(); track t.value) {
182
+ <svg:line
183
+ class="stroke-border"
184
+ stroke-dasharray="3 3"
185
+ [attr.x1]="line(t.offset).x1"
186
+ [attr.x2]="line(t.offset).x2"
187
+ [attr.y1]="line(t.offset).y1"
188
+ [attr.y2]="line(t.offset).y2" />
189
+ }
190
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
191
+ }
192
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartGrid, decorators: [{
193
+ type: Component,
194
+ args: [{
195
+ selector: 'svg:g[ChartGrid]',
196
+ changeDetection: ChangeDetectionStrategy.OnPush,
197
+ host: {
198
+ class: 'chart-grid text-border',
199
+ },
200
+ template: `
201
+ @for (t of ticks(); track t.value) {
202
+ <svg:line
203
+ class="stroke-border"
204
+ stroke-dasharray="3 3"
205
+ [attr.x1]="line(t.offset).x1"
206
+ [attr.x2]="line(t.offset).x2"
207
+ [attr.y1]="line(t.offset).y1"
208
+ [attr.y2]="line(t.offset).y2" />
209
+ }
210
+ `,
211
+ }]
212
+ }], propDecorators: { tickCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "tickCount", required: false }] }] } });
213
+
214
+ /**
215
+ * Crosshair primitive — a line drawn through the currently active data point
216
+ * perpendicular to the categorical axis. Reads `activePoint` from
217
+ * `ChartContext` and the scales from `CartesianContext`.
218
+ *
219
+ * Place inside a cartesian chart's SVG inner group.
220
+ */
221
+ class ChartCrosshair {
222
+ root = inject(ChartContext);
223
+ cart = inject(CartesianContext);
224
+ line = computed(() => {
225
+ const active = this.root.activePoint();
226
+ const scale = this.cart.categoryScale();
227
+ const categories = this.cart.categories();
228
+ if (!active || !scale || active.index < 0 || active.index >= categories.length) {
229
+ return null;
230
+ }
231
+ const base = scale(categories[active.index]) ?? 0;
232
+ const pos = base + scale.bandwidth() / 2;
233
+ if (this.cart.orientation() === 'vertical') {
234
+ return { x1: pos, x2: pos, y1: 0, y2: this.cart.innerHeight() };
235
+ }
236
+ return { x1: 0, x2: this.cart.innerWidth(), y1: pos, y2: pos };
237
+ }, /* @ts-ignore */
238
+ ...(ngDevMode ? [{ debugName: "line" }] : /* istanbul ignore next */ []));
239
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartCrosshair, deps: [], target: i0.ɵɵFactoryTarget.Component });
240
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ChartCrosshair, isStandalone: true, selector: "svg:g[ChartCrosshair]", host: { classAttribute: "chart-crosshair" }, ngImport: i0, template: `
241
+ @if (line(); as l) {
242
+ <svg:line
243
+ class="stroke-border"
244
+ stroke-dasharray="3 3"
245
+ [attr.x1]="l.x1"
246
+ [attr.x2]="l.x2"
247
+ [attr.y1]="l.y1"
248
+ [attr.y2]="l.y2" />
249
+ }
250
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
251
+ }
252
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartCrosshair, decorators: [{
253
+ type: Component,
254
+ args: [{
255
+ selector: 'svg:g[ChartCrosshair]',
256
+ changeDetection: ChangeDetectionStrategy.OnPush,
257
+ host: { class: 'chart-crosshair' },
258
+ template: `
259
+ @if (line(); as l) {
260
+ <svg:line
261
+ class="stroke-border"
262
+ stroke-dasharray="3 3"
263
+ [attr.x1]="l.x1"
264
+ [attr.x2]="l.x2"
265
+ [attr.y1]="l.y1"
266
+ [attr.y2]="l.y2" />
267
+ }
268
+ `,
269
+ }]
270
+ }] });
271
+
272
+ function clamp(value, min, max) {
273
+ return Math.min(max, Math.max(min, value));
274
+ }
275
+ function sameDomain(a, b) {
276
+ return Math.abs(a[0] - b[0]) < 1e-9 && Math.abs(a[1] - b[1]) < 1e-9;
277
+ }
278
+ class ChartBrush {
279
+ hitbox = viewChild.required('hitbox');
280
+ cart = inject(CartesianContext, { optional: true });
281
+ categorical = inject(CategoricalViewportContext, { optional: true });
282
+ scatter = inject(ScatterViewportContext, { optional: true });
283
+ mode = signal(null, /* @ts-ignore */
284
+ ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
285
+ activePointerId = signal(null, /* @ts-ignore */
286
+ ...(ngDevMode ? [{ debugName: "activePointerId" }] : /* istanbul ignore next */ []));
287
+ categoryStartIndex = signal(null, /* @ts-ignore */
288
+ ...(ngDevMode ? [{ debugName: "categoryStartIndex" }] : /* istanbul ignore next */ []));
289
+ categoryPanStart = signal(null, /* @ts-ignore */
290
+ ...(ngDevMode ? [{ debugName: "categoryPanStart" }] : /* istanbul ignore next */ []));
291
+ scatterBrush = signal(null, /* @ts-ignore */
292
+ ...(ngDevMode ? [{ debugName: "scatterBrush" }] : /* istanbul ignore next */ []));
293
+ scatterPan = signal(null, /* @ts-ignore */
294
+ ...(ngDevMode ? [{ debugName: "scatterPan" }] : /* istanbul ignore next */ []));
295
+ width = computed(() => this.scatter?.innerWidth() ?? this.cart?.innerWidth() ?? 0, /* @ts-ignore */
296
+ ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
297
+ height = computed(() => this.scatter?.innerHeight() ?? this.cart?.innerHeight() ?? 0, /* @ts-ignore */
298
+ ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
299
+ categoryPreview = computed(() => {
300
+ const cart = this.cart;
301
+ const viewport = this.categorical;
302
+ const range = viewport?.brushRange();
303
+ const scale = cart?.categoryScale();
304
+ const categories = cart?.categories() ?? [];
305
+ if (!cart || !viewport || !range || !scale || categories.length === 0) {
306
+ return null;
307
+ }
308
+ const visibleStart = viewport.zoomRange()?.startIndex ?? 0;
309
+ const startVisible = range.startIndex - visibleStart;
310
+ const endVisible = range.endIndex - visibleStart;
311
+ if (startVisible < 0 || endVisible >= categories.length) {
312
+ return null;
313
+ }
314
+ const first = scale(categories[startVisible]) ?? 0;
315
+ const last = (scale(categories[endVisible]) ?? 0) + scale.bandwidth();
316
+ if (cart.orientation() === 'vertical') {
317
+ return { x: Math.min(first, last), y: 0, width: Math.abs(last - first), height: cart.innerHeight() };
318
+ }
319
+ return { x: 0, y: Math.min(first, last), width: cart.innerWidth(), height: Math.abs(last - first) };
320
+ }, /* @ts-ignore */
321
+ ...(ngDevMode ? [{ debugName: "categoryPreview" }] : /* istanbul ignore next */ []));
322
+ scatterPreviewRect = computed(() => {
323
+ const preview = this.scatterBrush();
324
+ if (!preview) {
325
+ return null;
326
+ }
327
+ return {
328
+ x: Math.min(preview.start.x, preview.current.x),
329
+ y: Math.min(preview.start.y, preview.current.y),
330
+ width: Math.abs(preview.current.x - preview.start.x),
331
+ height: Math.abs(preview.current.y - preview.start.y),
332
+ };
333
+ }, /* @ts-ignore */
334
+ ...(ngDevMode ? [{ debugName: "scatterPreviewRect" }] : /* istanbul ignore next */ []));
335
+ onPointerDown(event) {
336
+ if (this.activePointerId() != null) {
337
+ return;
338
+ }
339
+ const local = this.localPoint(event);
340
+ if (!local) {
341
+ return;
342
+ }
343
+ if (this.scatter) {
344
+ if (!this.scatter.fullXDomain() || !this.scatter.fullYDomain()) {
345
+ return;
346
+ }
347
+ event.preventDefault();
348
+ this.activePointerId.set(event.pointerId);
349
+ this.hitbox().nativeElement.setPointerCapture(event.pointerId);
350
+ this.onScatterPointerDown(event, local);
351
+ return;
352
+ }
353
+ if (!this.cart || !this.categorical) {
354
+ return;
355
+ }
356
+ const index = nearestCategoryIndex({
357
+ categoryScale: this.cart.categoryScale,
358
+ categories: this.cart.categories,
359
+ orientation: this.cart.orientation,
360
+ }, local.x, local.y);
361
+ if (index < 0) {
362
+ return;
363
+ }
364
+ event.preventDefault();
365
+ this.activePointerId.set(event.pointerId);
366
+ this.hitbox().nativeElement.setPointerCapture(event.pointerId);
367
+ const visibleStart = this.categorical.zoomRange()?.startIndex ?? 0;
368
+ const absoluteIndex = visibleStart + index;
369
+ if (event.pointerType === 'touch' && this.categorical.hasZoom()) {
370
+ this.mode.set('category-pan');
371
+ this.categoryPanStart.set({ coord: this.pointerAxis(local), range: this.categorical.zoomRange() });
372
+ return;
373
+ }
374
+ this.mode.set('category-brush');
375
+ this.categoryStartIndex.set(absoluteIndex);
376
+ this.categorical.brushRange.set({ startIndex: absoluteIndex, endIndex: absoluteIndex });
377
+ }
378
+ onPointerMove(event) {
379
+ if (this.activePointerId() !== event.pointerId) {
380
+ return;
381
+ }
382
+ const local = this.localPoint(event);
383
+ if (!local) {
384
+ return;
385
+ }
386
+ if (this.mode() === 'category-brush' && this.cart && this.categorical) {
387
+ const startIndex = this.categoryStartIndex();
388
+ if (startIndex == null) {
389
+ return;
390
+ }
391
+ const index = nearestCategoryIndex({
392
+ categoryScale: this.cart.categoryScale,
393
+ categories: this.cart.categories,
394
+ orientation: this.cart.orientation,
395
+ }, local.x, local.y);
396
+ if (index < 0) {
397
+ return;
398
+ }
399
+ const visibleStart = this.categorical.zoomRange()?.startIndex ?? 0;
400
+ this.categorical.brushRange.set(normalizeIndexRange(startIndex, visibleStart + index, this.categorical.dataCount()));
401
+ return;
402
+ }
403
+ if (this.mode() === 'category-pan' && this.cart && this.categorical) {
404
+ const pan = this.categoryPanStart();
405
+ if (!pan) {
406
+ return;
407
+ }
408
+ const scale = this.cart.categoryScale();
409
+ const step = scale?.step?.() ?? scale?.bandwidth() ?? 0;
410
+ if (step <= 0) {
411
+ return;
412
+ }
413
+ const deltaSteps = Math.round((pan.coord - this.pointerAxis(local)) / step);
414
+ this.categorical.zoomRange.set(panIndexRange(pan.range, this.categorical.dataCount(), deltaSteps));
415
+ return;
416
+ }
417
+ if (this.mode() === 'scatter-brush') {
418
+ const preview = this.scatterBrush();
419
+ if (!preview) {
420
+ return;
421
+ }
422
+ this.scatterBrush.set({ start: preview.start, current: local });
423
+ return;
424
+ }
425
+ if (this.mode() === 'scatter-pan' && this.scatter) {
426
+ const pan = this.scatterPan();
427
+ const xScale = this.scatter.xScale();
428
+ const yScale = this.scatter.yScale();
429
+ const fullX = this.scatter.fullXDomain();
430
+ const fullY = this.scatter.fullYDomain();
431
+ if (!pan || !xScale || !yScale || !fullX || !fullY) {
432
+ return;
433
+ }
434
+ const deltaX = xScale.invert(pan.start.x) - xScale.invert(local.x);
435
+ const deltaY = yScale.invert(pan.start.y) - yScale.invert(local.y);
436
+ this.scatter.zoomXDomain.set(panNumericDomain(pan.xDomain, fullX, deltaX));
437
+ this.scatter.zoomYDomain.set(panNumericDomain(pan.yDomain, fullY, deltaY));
438
+ }
439
+ }
440
+ onPointerUp(event) {
441
+ if (event && this.activePointerId() !== event.pointerId) {
442
+ return;
443
+ }
444
+ if (this.mode() === 'category-brush' && this.categorical) {
445
+ const range = this.categorical.brushRange();
446
+ if (range && indexRangeSize(range, this.categorical.dataCount()) > 1) {
447
+ this.categorical.zoomRange.set(range);
448
+ }
449
+ this.categorical.brushRange.set(null);
450
+ }
451
+ if (this.mode() === 'scatter-brush' && this.scatter) {
452
+ const preview = this.scatterBrush();
453
+ const xScale = this.scatter.xScale();
454
+ const yScale = this.scatter.yScale();
455
+ const fullX = this.scatter.fullXDomain();
456
+ const fullY = this.scatter.fullYDomain();
457
+ if (preview && xScale && yScale && fullX && fullY) {
458
+ const width = Math.abs(preview.current.x - preview.start.x);
459
+ const height = Math.abs(preview.current.y - preview.start.y);
460
+ if (width >= 6 && height >= 6) {
461
+ const nextX = normalizeNumericDomain(xScale.invert(preview.start.x), xScale.invert(preview.current.x));
462
+ const nextY = normalizeNumericDomain(yScale.invert(preview.start.y), yScale.invert(preview.current.y));
463
+ this.scatter.zoomXDomain.set(sameDomain(nextX, fullX) ? null : nextX);
464
+ this.scatter.zoomYDomain.set(sameDomain(nextY, fullY) ? null : nextY);
465
+ }
466
+ }
467
+ this.scatterBrush.set(null);
468
+ }
469
+ this.mode.set(null);
470
+ this.categoryStartIndex.set(null);
471
+ this.categoryPanStart.set(null);
472
+ this.scatterPan.set(null);
473
+ if (event && this.hitbox().nativeElement.hasPointerCapture(event.pointerId)) {
474
+ this.hitbox().nativeElement.releasePointerCapture(event.pointerId);
475
+ }
476
+ this.activePointerId.set(null);
477
+ }
478
+ onPointerCancel(event) {
479
+ if (this.activePointerId() !== event.pointerId) {
480
+ return;
481
+ }
482
+ if (this.hitbox().nativeElement.hasPointerCapture(event.pointerId)) {
483
+ this.hitbox().nativeElement.releasePointerCapture(event.pointerId);
484
+ }
485
+ this.mode.set(null);
486
+ this.categorical?.brushRange.set(null);
487
+ this.scatterBrush.set(null);
488
+ this.categoryStartIndex.set(null);
489
+ this.categoryPanStart.set(null);
490
+ this.scatterPan.set(null);
491
+ this.activePointerId.set(null);
492
+ }
493
+ onWheel(event) {
494
+ const local = this.localPoint(event);
495
+ if (!local) {
496
+ return;
497
+ }
498
+ event.preventDefault();
499
+ if (this.scatter) {
500
+ const xScale = this.scatter.xScale();
501
+ const yScale = this.scatter.yScale();
502
+ const fullX = this.scatter.fullXDomain();
503
+ const fullY = this.scatter.fullYDomain();
504
+ if (!xScale || !yScale || !fullX || !fullY) {
505
+ return;
506
+ }
507
+ const factor = event.deltaY < 0 ? 0.8 : 1.25;
508
+ const currentX = this.scatter.zoomXDomain() ?? fullX;
509
+ const currentY = this.scatter.zoomYDomain() ?? fullY;
510
+ const nextX = zoomNumericDomain(currentX, fullX, xScale.invert(local.x), factor);
511
+ const nextY = zoomNumericDomain(currentY, fullY, yScale.invert(local.y), factor);
512
+ this.scatter.zoomXDomain.set(sameDomain(nextX, fullX) ? null : nextX);
513
+ this.scatter.zoomYDomain.set(sameDomain(nextY, fullY) ? null : nextY);
514
+ return;
515
+ }
516
+ if (!this.cart || !this.categorical) {
517
+ return;
518
+ }
519
+ const visibleIndex = nearestCategoryIndex({
520
+ categoryScale: this.cart.categoryScale,
521
+ categories: this.cart.categories,
522
+ orientation: this.cart.orientation,
523
+ }, local.x, local.y);
524
+ if (visibleIndex < 0) {
525
+ return;
526
+ }
527
+ const anchor = (this.categorical.zoomRange()?.startIndex ?? 0) + visibleIndex;
528
+ const factor = event.deltaY < 0 ? 0.75 : 1.25;
529
+ this.categorical.zoomRange.set(zoomIndexRange(this.categorical.zoomRange(), this.categorical.dataCount(), anchor, factor));
530
+ this.categorical.brushRange.set(null);
531
+ }
532
+ resetZoom() {
533
+ this.categorical?.resetZoom();
534
+ this.scatter?.resetZoom();
535
+ this.scatterBrush.set(null);
536
+ }
537
+ onScatterPointerDown(event, local) {
538
+ const fullX = this.scatter?.fullXDomain();
539
+ const fullY = this.scatter?.fullYDomain();
540
+ if (!this.scatter || !fullX || !fullY) {
541
+ return;
542
+ }
543
+ if (event.pointerType === 'touch' && this.scatter.hasZoom()) {
544
+ this.mode.set('scatter-pan');
545
+ this.scatterPan.set({
546
+ start: local,
547
+ xDomain: this.scatter.zoomXDomain() ?? fullX,
548
+ yDomain: this.scatter.zoomYDomain() ?? fullY,
549
+ });
550
+ return;
551
+ }
552
+ this.mode.set('scatter-brush');
553
+ this.scatterBrush.set({ start: local, current: local });
554
+ }
555
+ localPoint(event) {
556
+ const hitbox = this.hitbox()?.nativeElement;
557
+ const width = this.width();
558
+ const height = this.height();
559
+ if (!hitbox || width <= 0 || height <= 0) {
560
+ return null;
561
+ }
562
+ const rect = hitbox.getBoundingClientRect();
563
+ if (rect.width <= 0 || rect.height <= 0) {
564
+ return null;
565
+ }
566
+ return {
567
+ x: clamp(((event.clientX - rect.left) / rect.width) * width, 0, width),
568
+ y: clamp(((event.clientY - rect.top) / rect.height) * height, 0, height),
569
+ };
570
+ }
571
+ pointerAxis(local) {
572
+ return this.cart?.orientation() === 'horizontal' ? local.y : local.x;
573
+ }
574
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartBrush, deps: [], target: i0.ɵɵFactoryTarget.Component });
575
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ChartBrush, isStandalone: true, selector: "svg:g[ChartBrush]", 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: `
576
+ <svg:rect
577
+ #hitbox
578
+ class="fill-transparent touch-none"
579
+ x="0"
580
+ y="0"
581
+ [attr.width]="width()"
582
+ [attr.height]="height()"
583
+ (pointerdown)="onPointerDown($event)"
584
+ (pointermove)="onPointerMove($event)"
585
+ (pointerup)="onPointerUp($event)"
586
+ (pointercancel)="onPointerCancel($event)"
587
+ (wheel)="onWheel($event)"
588
+ (dblclick)="resetZoom()" />
589
+
590
+ @if (categoryPreview(); as rect) {
591
+ <svg:rect
592
+ class="fill-foreground/10 stroke-foreground/30"
593
+ [attr.x]="rect.x"
594
+ [attr.y]="rect.y"
595
+ [attr.width]="rect.width"
596
+ [attr.height]="rect.height"
597
+ stroke-dasharray="4 3" />
598
+ }
599
+
600
+ @if (scatterPreviewRect(); as rect) {
601
+ <svg:rect
602
+ class="fill-foreground/10 stroke-foreground/30"
603
+ [attr.x]="rect.x"
604
+ [attr.y]="rect.y"
605
+ [attr.width]="rect.width"
606
+ [attr.height]="rect.height"
607
+ stroke-dasharray="4 3" />
608
+ }
609
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
610
+ }
611
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartBrush, decorators: [{
612
+ type: Component,
613
+ args: [{
614
+ selector: 'svg:g[ChartBrush]',
615
+ changeDetection: ChangeDetectionStrategy.OnPush,
616
+ host: {
617
+ class: 'chart-brush',
618
+ '(window:pointermove)': 'onPointerMove($event)',
619
+ '(window:pointerup)': 'onPointerUp($event)',
620
+ '(window:pointercancel)': 'onPointerCancel($event)',
621
+ },
622
+ template: `
623
+ <svg:rect
624
+ #hitbox
625
+ class="fill-transparent touch-none"
626
+ x="0"
627
+ y="0"
628
+ [attr.width]="width()"
629
+ [attr.height]="height()"
630
+ (pointerdown)="onPointerDown($event)"
631
+ (pointermove)="onPointerMove($event)"
632
+ (pointerup)="onPointerUp($event)"
633
+ (pointercancel)="onPointerCancel($event)"
634
+ (wheel)="onWheel($event)"
635
+ (dblclick)="resetZoom()" />
636
+
637
+ @if (categoryPreview(); as rect) {
638
+ <svg:rect
639
+ class="fill-foreground/10 stroke-foreground/30"
640
+ [attr.x]="rect.x"
641
+ [attr.y]="rect.y"
642
+ [attr.width]="rect.width"
643
+ [attr.height]="rect.height"
644
+ stroke-dasharray="4 3" />
645
+ }
646
+
647
+ @if (scatterPreviewRect(); as rect) {
648
+ <svg:rect
649
+ class="fill-foreground/10 stroke-foreground/30"
650
+ [attr.x]="rect.x"
651
+ [attr.y]="rect.y"
652
+ [attr.width]="rect.width"
653
+ [attr.height]="rect.height"
654
+ stroke-dasharray="4 3" />
655
+ }
656
+ `,
657
+ }]
658
+ }], propDecorators: { hitbox: [{ type: i0.ViewChild, args: ['hitbox', { isSignal: true }] }] } });
659
+
660
+ /**
661
+ * Attach to a chart's `<svg:svg>` to publish pointer-driven active-point
662
+ * state into the surrounding `ChartContext`.
663
+ *
664
+ * Computes the local (x, y) of the pointer relative to the inner plotting
665
+ * area, resolves the nearest category, and updates
666
+ * `ChartContext.activePoint`. Clears it on `pointerleave`.
667
+ */
668
+ class ChartPointerTracker {
669
+ root = inject(ChartContext);
670
+ cart = inject(CartesianContext);
671
+ viewport = inject(CategoricalViewportContext, { optional: true });
672
+ onMove(event) {
673
+ const target = event.currentTarget;
674
+ if (!target)
675
+ return;
676
+ const rect = target.getBoundingClientRect();
677
+ if (rect.width === 0 || rect.height === 0)
678
+ return;
679
+ const { width, height } = this.root.dimensions();
680
+ // Map client coords → viewBox coords (SVG uses `0 0 width height`,
681
+ // preserveAspectRatio="none" so axes scale independently).
682
+ const scaleX = width / rect.width;
683
+ const scaleY = height / rect.height;
684
+ const viewX = (event.clientX - rect.left) * scaleX;
685
+ const viewY = (event.clientY - rect.top) * scaleY;
686
+ const margin = this.cart.margin();
687
+ const localX = viewX - margin.left;
688
+ const localY = viewY - margin.top;
689
+ const index = nearestCategoryIndex({
690
+ categoryScale: this.cart.categoryScale,
691
+ categories: this.cart.categories,
692
+ orientation: this.cart.orientation,
693
+ }, localX, localY);
694
+ if (index < 0) {
695
+ this.root.activePoint.set(null);
696
+ return;
697
+ }
698
+ this.root.activePoint.set({
699
+ index,
700
+ datumIndex: (this.viewport?.zoomRange()?.startIndex ?? 0) + index,
701
+ clientX: event.clientX,
702
+ clientY: event.clientY,
703
+ });
704
+ }
705
+ onLeave() {
706
+ this.root.activePoint.set(null);
707
+ }
708
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartPointerTracker, deps: [], target: i0.ɵɵFactoryTarget.Directive });
709
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ChartPointerTracker, isStandalone: true, selector: "svg:svg[ChartPointerTracker]", host: { listeners: { "pointermove": "onMove($event)", "pointerleave": "onLeave()" } }, ngImport: i0 });
710
+ }
711
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartPointerTracker, decorators: [{
712
+ type: Directive,
713
+ args: [{
714
+ selector: 'svg:svg[ChartPointerTracker]',
715
+ host: {
716
+ '(pointermove)': 'onMove($event)',
717
+ '(pointerleave)': 'onLeave()',
718
+ },
719
+ }]
720
+ }] });
721
+
722
+ /** Locate the chart container DOM in order to position the tooltip. */
723
+ function containerRect(el) {
724
+ // Climb to the nearest `[data-chart]` (the ChartContainer host).
725
+ let node = el;
726
+ while (node && !node.hasAttribute('data-chart')) {
727
+ node = node.parentElement;
728
+ }
729
+ return node?.getBoundingClientRect() ?? null;
730
+ }
731
+ /**
732
+ * Tooltip overlay — renders the default tooltip card (or a user-supplied
733
+ * template) anchored to the currently active data point.
734
+ *
735
+ * Shadcn-compatible knobs:
736
+ * - `indicator="line"` / `"none"` / `"dashed"` — swap the row glyph
737
+ * - `hideLabel` — drop the header row
738
+ * - `label="Activities"` — override the header text
739
+ * - `labelKey="activities"` — resolve the header from `config[labelKey].label`
740
+ * - `labelFormatter` — transform the header (e.g. ISO date → long date)
741
+ * - `formatter` — format per-row values (e.g. `"380 kcal"`)
742
+ * - `valueKey` — for pie/radial/radar, read row value from a single field
743
+ * - Icons are picked up automatically from `config[key].icon`.
744
+ */
745
+ class ChartTooltip {
746
+ root = inject(ChartContext);
747
+ host = inject((ElementRef));
748
+ /** Data key on each datum whose value is the category label (x-axis). */
749
+ xKey = input(null, /* @ts-ignore */
750
+ ...(ngDevMode ? [{ debugName: "xKey" }] : /* istanbul ignore next */ []));
751
+ /** Data source (optional — if omitted tooltip reads from chart data via activePoint.datumIndex). */
752
+ data = input(null, /* @ts-ignore */
753
+ ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
754
+ /**
755
+ * Optional key for per-datum value lookup (pie / radial / radar datasets
756
+ * store values on a single field like `visitors` instead of one field per
757
+ * series). When set and `activePoint.seriesKey` is active, the tooltip row
758
+ * reads `datum[valueKey]` for its value.
759
+ */
760
+ valueKey = input(null, /* @ts-ignore */
761
+ ...(ngDevMode ? [{ debugName: "valueKey" }] : /* istanbul ignore next */ []));
762
+ /** Indicator variant next to each row. Default `'dot'`. */
763
+ indicator = input('dot', /* @ts-ignore */
764
+ ...(ngDevMode ? [{ debugName: "indicator" }] : /* istanbul ignore next */ []));
765
+ /** Hide the header label entirely. */
766
+ hideLabel = input(false, /* @ts-ignore */
767
+ ...(ngDevMode ? [{ debugName: "hideLabel" }] : /* istanbul ignore next */ []));
768
+ /** Override the header label with a fixed string. Takes precedence over `labelKey`. */
769
+ label = input(null, /* @ts-ignore */
770
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
771
+ /**
772
+ * Resolve the header label from `config[labelKey].label`. Useful when the
773
+ * header should come from a config entry rather than the datum itself
774
+ * (shadcn's "Custom label" variant).
775
+ */
776
+ labelKey = input(null, /* @ts-ignore */
777
+ ...(ngDevMode ? [{ debugName: "labelKey" }] : /* istanbul ignore next */ []));
778
+ /** Transform the final header string (e.g. ISO date → long date). */
779
+ labelFormatter = input(null, /* @ts-ignore */
780
+ ...(ngDevMode ? [{ debugName: "labelFormatter" }] : /* istanbul ignore next */ []));
781
+ /** Format each row's value (return string — HTML is not interpreted). */
782
+ formatter = input(null, /* @ts-ignore */
783
+ ...(ngDevMode ? [{ debugName: "formatter" }] : /* istanbul ignore next */ []));
784
+ customTpl = contentChild((TemplateRef), /* @ts-ignore */
785
+ ...(ngDevMode ? [{ debugName: "customTpl" }] : /* istanbul ignore next */ []));
786
+ visible = computed(() => this.root.activePoint() !== null, /* @ts-ignore */
787
+ ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
788
+ payload = computed(() => {
789
+ const active = this.root.activePoint();
790
+ const rows = this.data();
791
+ if (!active || !rows)
792
+ return null;
793
+ const dataIndex = active.datumIndex != null && active.datumIndex < rows.length ? active.datumIndex : active.index;
794
+ if (dataIndex < 0 || dataIndex >= rows.length)
795
+ return null;
796
+ const cfg = this.root.config();
797
+ const visibleKeys = this.root.visibleSeriesKeys();
798
+ const datum = rows[dataIndex];
799
+ const xKey = this.xKey();
800
+ const category = xKey && xKey in datum ? String(datum[xKey]) : String(active.index);
801
+ // When the active point targets a single series (pie/radial/radar hover),
802
+ // collapse the tooltip to just that row.
803
+ const activeSeriesKey = active.seriesKey && visibleKeys.includes(active.seriesKey) ? active.seriesKey : null;
804
+ const keys = activeSeriesKey ? [activeSeriesKey] : visibleKeys;
805
+ const valueKey = this.valueKey();
806
+ const tooltipRows = keys.map((k) => {
807
+ // For single-slice hover on pie/radial/radar the value lives on a
808
+ // shared `valueKey` field, not on a per-series column.
809
+ const rawValue = valueKey != null && activeSeriesKey === k ? datum[valueKey] : datum[k];
810
+ return {
811
+ seriesKey: k,
812
+ label: cfg[k]?.label ?? k,
813
+ value: rawValue,
814
+ color: seriesColorVar(k),
815
+ icon: cfg[k]?.icon,
816
+ };
817
+ });
818
+ return { category, datum, rows: tooltipRows };
819
+ }, /* @ts-ignore */
820
+ ...(ngDevMode ? [{ debugName: "payload" }] : /* istanbul ignore next */ []));
821
+ /** Resolve the header string honoring `label`, `labelKey`, then `labelFormatter`. */
822
+ headerText(p) {
823
+ if (this.hideLabel())
824
+ return null;
825
+ const override = this.label();
826
+ const labelKey = this.labelKey();
827
+ const cfg = this.root.config();
828
+ const fromKey = labelKey ? (cfg[labelKey]?.label ?? labelKey) : null;
829
+ const raw = override ?? fromKey ?? p.category;
830
+ const fmt = this.labelFormatter();
831
+ return fmt ? fmt(raw, p) : raw;
832
+ }
833
+ /** Apply per-row value formatter (or stringify). */
834
+ formatRow(row, p) {
835
+ const fmt = this.formatter();
836
+ if (fmt)
837
+ return fmt(row.value, row, p);
838
+ return row.value == null ? '' : String(row.value);
839
+ }
840
+ position = computed(() => {
841
+ const active = this.root.activePoint();
842
+ if (!active || active.clientX == null || active.clientY == null) {
843
+ return { x: 0, y: 0 };
844
+ }
845
+ // Map client coords → offset within the chart container (the element
846
+ // marked with `data-chart`, which is our positioning ancestor).
847
+ const rect = containerRect(this.host.nativeElement);
848
+ if (!rect)
849
+ return { x: 0, y: 0 };
850
+ const tooltip = this.host.nativeElement.querySelector('[role="tooltip"]');
851
+ const tooltipWidth = tooltip?.offsetWidth ?? 128;
852
+ const tooltipHeight = tooltip?.offsetHeight ?? 0;
853
+ const padding = 8;
854
+ const minX = padding + tooltipWidth / 2;
855
+ const maxX = Math.max(minX, rect.width - padding - tooltipWidth / 2);
856
+ const x = Math.min(maxX, Math.max(minX, active.clientX - rect.left));
857
+ // Y is the tooltip's bottom edge because the card is translated upward.
858
+ const minY = padding + tooltipHeight;
859
+ const y = Math.max(minY, active.clientY - rect.top - padding);
860
+ return { x, y };
861
+ }, /* @ts-ignore */
862
+ ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
863
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartTooltip, deps: [], target: i0.ɵɵFactoryTarget.Component });
864
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ChartTooltip, isStandalone: true, selector: "ChartTooltip", 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: `
865
+ @if (payload(); as p) {
866
+ <div
867
+ role="tooltip"
868
+ 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-[hsl(var(--border)/var(--opacity-60))] bg-background px-3 py-1.5 text-xs shadow-md"
869
+ [style.left.px]="position().x"
870
+ [style.top.px]="position().y">
871
+ @if (customTpl(); as tpl) {
872
+ <ng-container *ngTemplateOutlet="tpl; context: { $implicit: p }" />
873
+ } @else {
874
+ @if (!hideLabel() && headerText(p); as header) {
875
+ <div class="font-medium">{{ header }}</div>
876
+ }
877
+ <ul class="grid gap-1.5">
878
+ @for (row of p.rows; track row.seriesKey) {
879
+ <li class="flex w-full flex-wrap items-stretch gap-2">
880
+ <span class="flex flex-1 items-center gap-1.5">
881
+ @switch (indicator()) {
882
+ @case ('dot') {
883
+ <span
884
+ class="h-2.5 w-2.5 shrink-0 rounded-sm"
885
+ [style.background]="row.color"
886
+ [style.borderColor]="row.color"></span>
887
+ }
888
+ @case ('line') {
889
+ <span class="h-full min-h-4 w-1 shrink-0 rounded-sm" [style.background]="row.color"></span>
890
+ }
891
+ @case ('dashed') {
892
+ <span
893
+ class="h-0 w-3 shrink-0 self-center border-t-2 border-dashed"
894
+ [style.borderColor]="row.color"></span>
895
+ }
896
+ }
897
+ @if (row.icon; as icon) {
898
+ <span class="mr-1 inline-flex items-center text-muted-foreground">
899
+ <ng-container *ngComponentOutlet="icon" />
900
+ </span>
901
+ }
902
+ <span class="text-muted-foreground">{{ row.label }}</span>
903
+ </span>
904
+ <span class="font-mono font-medium tabular-nums text-foreground">
905
+ {{ formatRow(row, p) }}
906
+ </span>
907
+ </li>
908
+ }
909
+ </ul>
910
+ }
911
+ </div>
912
+ }
913
+
914
+ <!-- Live region — announces category changes to AT. -->
915
+ <div class="sr-only" aria-live="polite" aria-atomic="true">
916
+ @if (payload(); as p) {
917
+ {{ p.category }}
918
+ }
919
+ </div>
920
+ `, 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 });
921
+ }
922
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartTooltip, decorators: [{
923
+ type: Component,
924
+ args: [{
925
+ selector: 'ChartTooltip',
926
+ changeDetection: ChangeDetectionStrategy.OnPush,
927
+ imports: [NgTemplateOutlet, NgComponentOutlet],
928
+ host: {
929
+ class: 'pointer-events-none absolute inset-0 z-10',
930
+ '[attr.aria-hidden]': '!visible()',
931
+ },
932
+ template: `
933
+ @if (payload(); as p) {
934
+ <div
935
+ role="tooltip"
936
+ 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-[hsl(var(--border)/var(--opacity-60))] bg-background px-3 py-1.5 text-xs shadow-md"
937
+ [style.left.px]="position().x"
938
+ [style.top.px]="position().y">
939
+ @if (customTpl(); as tpl) {
940
+ <ng-container *ngTemplateOutlet="tpl; context: { $implicit: p }" />
941
+ } @else {
942
+ @if (!hideLabel() && headerText(p); as header) {
943
+ <div class="font-medium">{{ header }}</div>
944
+ }
945
+ <ul class="grid gap-1.5">
946
+ @for (row of p.rows; track row.seriesKey) {
947
+ <li class="flex w-full flex-wrap items-stretch gap-2">
948
+ <span class="flex flex-1 items-center gap-1.5">
949
+ @switch (indicator()) {
950
+ @case ('dot') {
951
+ <span
952
+ class="h-2.5 w-2.5 shrink-0 rounded-sm"
953
+ [style.background]="row.color"
954
+ [style.borderColor]="row.color"></span>
955
+ }
956
+ @case ('line') {
957
+ <span class="h-full min-h-4 w-1 shrink-0 rounded-sm" [style.background]="row.color"></span>
958
+ }
959
+ @case ('dashed') {
960
+ <span
961
+ class="h-0 w-3 shrink-0 self-center border-t-2 border-dashed"
962
+ [style.borderColor]="row.color"></span>
963
+ }
964
+ }
965
+ @if (row.icon; as icon) {
966
+ <span class="mr-1 inline-flex items-center text-muted-foreground">
967
+ <ng-container *ngComponentOutlet="icon" />
968
+ </span>
969
+ }
970
+ <span class="text-muted-foreground">{{ row.label }}</span>
971
+ </span>
972
+ <span class="font-mono font-medium tabular-nums text-foreground">
973
+ {{ formatRow(row, p) }}
974
+ </span>
975
+ </li>
976
+ }
977
+ </ul>
978
+ }
979
+ </div>
980
+ }
981
+
982
+ <!-- Live region — announces category changes to AT. -->
983
+ <div class="sr-only" aria-live="polite" aria-atomic="true">
984
+ @if (payload(); as p) {
985
+ {{ p.category }}
986
+ }
987
+ </div>
988
+ `,
989
+ }]
990
+ }], 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 }] }] } });
991
+
992
+ /**
993
+ * Legend — renders a list of series swatches. Clicking an item toggles
994
+ * visibility via `ChartContext.toggleSeries`.
995
+ *
996
+ * Place as a child of `<Chart>`:
997
+ * ```html
998
+ * <Chart [config]="cfg">
999
+ * <ChartBar ... />
1000
+ * <ChartLegend />
1001
+ * </Chart>
1002
+ * ```
1003
+ */
1004
+ class ChartLegend {
1005
+ root = inject(ChartContext);
1006
+ items = computed(() => {
1007
+ const cfg = this.root.config();
1008
+ const hidden = this.root.hiddenSeries();
1009
+ return this.root.seriesKeys().map((key) => ({
1010
+ seriesKey: key,
1011
+ label: cfg[key]?.label ?? key,
1012
+ color: seriesColorVar(key),
1013
+ hidden: hidden.has(key),
1014
+ }));
1015
+ }, /* @ts-ignore */
1016
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1017
+ toggle(key) {
1018
+ this.root.toggleSeries(key);
1019
+ }
1020
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartLegend, deps: [], target: i0.ɵɵFactoryTarget.Component });
1021
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ChartLegend, isStandalone: true, selector: "ChartLegend", host: { classAttribute: "block pt-3" }, ngImport: i0, template: `
1022
+ <ul class="flex flex-wrap items-center justify-center gap-3 text-xs" aria-label="Toggle chart series visibility">
1023
+ @for (item of items(); track item.seriesKey) {
1024
+ <li>
1025
+ <button
1026
+ type="button"
1027
+ 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"
1028
+ [class.opacity-50]="item.hidden"
1029
+ [attr.aria-pressed]="!item.hidden"
1030
+ [attr.aria-label]="(item.hidden ? 'Show ' : 'Hide ') + item.label"
1031
+ (click)="toggle(item.seriesKey)">
1032
+ <span class="inline-block h-2.5 w-2.5 rounded-sm" [style.background]="item.color"></span>
1033
+ <span class="text-muted-foreground">{{ item.label }}</span>
1034
+ </button>
1035
+ </li>
1036
+ }
1037
+ </ul>
1038
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1039
+ }
1040
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartLegend, decorators: [{
1041
+ type: Component,
1042
+ args: [{
1043
+ selector: 'ChartLegend',
1044
+ changeDetection: ChangeDetectionStrategy.OnPush,
1045
+ host: { class: 'block pt-3' },
1046
+ template: `
1047
+ <ul class="flex flex-wrap items-center justify-center gap-3 text-xs" aria-label="Toggle chart series visibility">
1048
+ @for (item of items(); track item.seriesKey) {
1049
+ <li>
1050
+ <button
1051
+ type="button"
1052
+ 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"
1053
+ [class.opacity-50]="item.hidden"
1054
+ [attr.aria-pressed]="!item.hidden"
1055
+ [attr.aria-label]="(item.hidden ? 'Show ' : 'Hide ') + item.label"
1056
+ (click)="toggle(item.seriesKey)">
1057
+ <span class="inline-block h-2.5 w-2.5 rounded-sm" [style.background]="item.color"></span>
1058
+ <span class="text-muted-foreground">{{ item.label }}</span>
1059
+ </button>
1060
+ </li>
1061
+ }
1062
+ </ul>
1063
+ `,
1064
+ }]
1065
+ }] });
1066
+
1067
+ function formatNumber(value) {
1068
+ return Math.abs(value) >= 10 ? value.toFixed(0) : value.toFixed(1);
1069
+ }
1070
+ class ChartZoomControls {
1071
+ categorical = inject(CategoricalViewportContext, { optional: true });
1072
+ scatter = inject(ScatterViewportContext, { optional: true });
1073
+ status = computed(() => {
1074
+ if (this.categorical?.hasZoom()) {
1075
+ const range = this.categorical.zoomRange();
1076
+ const count = this.categorical.dataCount();
1077
+ if (!range) {
1078
+ return null;
1079
+ }
1080
+ return `Showing ${range.startIndex + 1}-${range.endIndex + 1} of ${count}`;
1081
+ }
1082
+ if (this.scatter?.hasZoom()) {
1083
+ const xDomain = this.scatter.zoomXDomain() ?? this.scatter.fullXDomain();
1084
+ const yDomain = this.scatter.zoomYDomain() ?? this.scatter.fullYDomain();
1085
+ if (!xDomain || !yDomain) {
1086
+ return null;
1087
+ }
1088
+ return `X ${formatNumber(xDomain[0])}-${formatNumber(xDomain[1])} · Y ${formatNumber(yDomain[0])}-${formatNumber(yDomain[1])}`;
1089
+ }
1090
+ return null;
1091
+ }, /* @ts-ignore */
1092
+ ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
1093
+ hint = computed(() => {
1094
+ if (this.scatter) {
1095
+ return 'Drag to brush a region. Use the wheel to zoom and one-finger touch drag to pan while zoomed.';
1096
+ }
1097
+ return 'Drag to select a category range. Use the wheel to zoom and one-finger touch drag to pan while zoomed.';
1098
+ }, /* @ts-ignore */
1099
+ ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
1100
+ resetZoom() {
1101
+ this.categorical?.resetZoom();
1102
+ this.scatter?.resetZoom();
1103
+ }
1104
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartZoomControls, deps: [], target: i0.ɵɵFactoryTarget.Component });
1105
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ChartZoomControls, isStandalone: true, selector: "ChartZoomControls", host: { classAttribute: "mt-3 flex flex-wrap items-center justify-between gap-3 text-xs text-muted-foreground" }, ngImport: i0, template: `
1106
+ <p>{{ hint() }}</p>
1107
+
1108
+ @if (status(); as currentStatus) {
1109
+ <div class="flex flex-wrap items-center gap-2">
1110
+ <span class="rounded-full border border-border bg-background px-2.5 py-1 font-medium text-foreground">
1111
+ {{ currentStatus }}
1112
+ </span>
1113
+ <button
1114
+ type="button"
1115
+ 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"
1116
+ (click)="resetZoom()">
1117
+ Reset zoom
1118
+ </button>
1119
+ </div>
1120
+ }
1121
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1122
+ }
1123
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ChartZoomControls, decorators: [{
1124
+ type: Component,
1125
+ args: [{
1126
+ selector: 'ChartZoomControls',
1127
+ changeDetection: ChangeDetectionStrategy.OnPush,
1128
+ host: { class: 'mt-3 flex flex-wrap items-center justify-between gap-3 text-xs text-muted-foreground' },
1129
+ template: `
1130
+ <p>{{ hint() }}</p>
1131
+
1132
+ @if (status(); as currentStatus) {
1133
+ <div class="flex flex-wrap items-center gap-2">
1134
+ <span class="rounded-full border border-border bg-background px-2.5 py-1 font-medium text-foreground">
1135
+ {{ currentStatus }}
1136
+ </span>
1137
+ <button
1138
+ type="button"
1139
+ 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"
1140
+ (click)="resetZoom()">
1141
+ Reset zoom
1142
+ </button>
1143
+ </div>
1144
+ }
1145
+ `,
1146
+ }]
1147
+ }] });
1148
+
1149
+ class PieCenter {
1150
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: PieCenter, deps: [], target: i0.ɵɵFactoryTarget.Component });
1151
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.2", type: PieCenter, isStandalone: true, selector: "ChartPieCenter", 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 });
1152
+ }
1153
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: PieCenter, decorators: [{
1154
+ type: Component,
1155
+ args: [{
1156
+ selector: 'ChartPieCenter',
1157
+ changeDetection: ChangeDetectionStrategy.OnPush,
1158
+ host: {
1159
+ class: 'flex max-w-[10rem] flex-col items-center justify-center text-center',
1160
+ },
1161
+ template: `<ng-content />`,
1162
+ }]
1163
+ }] });
1164
+
1165
+ class RadialCenter {
1166
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RadialCenter, deps: [], target: i0.ɵɵFactoryTarget.Component });
1167
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.2", type: RadialCenter, isStandalone: true, selector: "ChartRadialCenter", 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 });
1168
+ }
1169
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RadialCenter, decorators: [{
1170
+ type: Component,
1171
+ args: [{
1172
+ selector: 'ChartRadialCenter',
1173
+ changeDetection: ChangeDetectionStrategy.OnPush,
1174
+ host: {
1175
+ class: 'flex max-w-[10rem] flex-col items-center justify-center text-center',
1176
+ },
1177
+ template: `<ng-content />`,
1178
+ }]
1179
+ }] });
1180
+
1181
+ /**
1182
+ * Generated bundle index. Do not edit.
1183
+ */
1184
+
1185
+ export { ChartAxisX, ChartAxisY, ChartBrush, ChartCrosshair, ChartGrid, ChartLegend, ChartPointerTracker, ChartTooltip, ChartZoomControls, PieCenter, RadialCenter };
1186
+ //# sourceMappingURL=ojiepermana-angular-chart-primitives.mjs.map