@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.
- package/README.md +249 -0
- package/fesm2022/ojiepermana-angular-chart-area.mjs +266 -0
- package/fesm2022/ojiepermana-angular-chart-area.mjs.map +1 -0
- package/fesm2022/ojiepermana-angular-chart-bar.mjs +674 -0
- package/fesm2022/ojiepermana-angular-chart-bar.mjs.map +1 -0
- package/fesm2022/ojiepermana-angular-chart-core.mjs +764 -0
- package/fesm2022/ojiepermana-angular-chart-core.mjs.map +1 -0
- package/fesm2022/ojiepermana-angular-chart-line.mjs +281 -0
- package/fesm2022/ojiepermana-angular-chart-line.mjs.map +1 -0
- package/fesm2022/ojiepermana-angular-chart-pie.mjs +248 -0
- package/fesm2022/ojiepermana-angular-chart-pie.mjs.map +1 -0
- package/fesm2022/ojiepermana-angular-chart-primitives.mjs +1186 -0
- package/fesm2022/ojiepermana-angular-chart-primitives.mjs.map +1 -0
- package/fesm2022/ojiepermana-angular-chart-radar.mjs +329 -0
- package/fesm2022/ojiepermana-angular-chart-radar.mjs.map +1 -0
- package/fesm2022/ojiepermana-angular-chart-radial.mjs +255 -0
- package/fesm2022/ojiepermana-angular-chart-radial.mjs.map +1 -0
- package/fesm2022/ojiepermana-angular-chart-scatter.mjs +253 -0
- package/fesm2022/ojiepermana-angular-chart-scatter.mjs.map +1 -0
- package/fesm2022/ojiepermana-angular-chart.mjs +20 -0
- package/fesm2022/ojiepermana-angular-chart.mjs.map +1 -0
- package/package.json +76 -0
- package/types/ojiepermana-angular-chart-area.d.ts +58 -0
- package/types/ojiepermana-angular-chart-bar.d.ts +171 -0
- package/types/ojiepermana-angular-chart-core.d.ts +369 -0
- package/types/ojiepermana-angular-chart-line.d.ts +57 -0
- package/types/ojiepermana-angular-chart-pie.d.ts +93 -0
- package/types/ojiepermana-angular-chart-primitives.d.ts +265 -0
- package/types/ojiepermana-angular-chart-radar.d.ts +89 -0
- package/types/ojiepermana-angular-chart-radial.d.ts +86 -0
- package/types/ojiepermana-angular-chart-scatter.d.ts +95 -0
- 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
|