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