@acorex/charts 21.0.1-next.57 → 21.0.1-next.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/fesm2022/acorex-charts-bar-chart.mjs +15 -11
  2. package/fesm2022/acorex-charts-bar-chart.mjs.map +1 -1
  3. package/fesm2022/acorex-charts-chart-legend.mjs +5 -5
  4. package/fesm2022/acorex-charts-chart-legend.mjs.map +1 -1
  5. package/fesm2022/acorex-charts-chart-tooltip.mjs +4 -4
  6. package/fesm2022/acorex-charts-chart-tooltip.mjs.map +1 -1
  7. package/fesm2022/acorex-charts-donut-chart.mjs +16 -8
  8. package/fesm2022/acorex-charts-donut-chart.mjs.map +1 -1
  9. package/fesm2022/acorex-charts-funnel-chart.mjs +275 -0
  10. package/fesm2022/acorex-charts-funnel-chart.mjs.map +1 -0
  11. package/fesm2022/acorex-charts-gauge-chart.mjs +135 -73
  12. package/fesm2022/acorex-charts-gauge-chart.mjs.map +1 -1
  13. package/fesm2022/acorex-charts-heatmap-chart.mjs +281 -0
  14. package/fesm2022/acorex-charts-heatmap-chart.mjs.map +1 -0
  15. package/fesm2022/acorex-charts-hierarchy-chart.mjs +4 -4
  16. package/fesm2022/acorex-charts-hierarchy-chart.mjs.map +1 -1
  17. package/fesm2022/acorex-charts-line-chart.mjs +15 -17
  18. package/fesm2022/acorex-charts-line-chart.mjs.map +1 -1
  19. package/fesm2022/acorex-charts.mjs +3 -3
  20. package/fesm2022/acorex-charts.mjs.map +1 -1
  21. package/funnel-chart/README.md +3 -0
  22. package/heatmap-chart/README.md +3 -0
  23. package/package.json +18 -10
  24. package/{bar-chart/index.d.ts → types/acorex-charts-bar-chart.d.ts} +4 -1
  25. package/{donut-chart/index.d.ts → types/acorex-charts-donut-chart.d.ts} +3 -1
  26. package/types/acorex-charts-funnel-chart.d.ts +108 -0
  27. package/{gauge-chart/index.d.ts → types/acorex-charts-gauge-chart.d.ts} +14 -1
  28. package/types/acorex-charts-heatmap-chart.d.ts +111 -0
  29. package/{hierarchy-chart/index.d.ts → types/acorex-charts-hierarchy-chart.d.ts} +3 -2
  30. package/{line-chart/index.d.ts → types/acorex-charts-line-chart.d.ts} +3 -1
  31. /package/{chart-legend/index.d.ts → types/acorex-charts-chart-legend.d.ts} +0 -0
  32. /package/{chart-tooltip/index.d.ts → types/acorex-charts-chart-tooltip.d.ts} +0 -0
  33. /package/{index.d.ts → types/acorex-charts.d.ts} +0 -0
@@ -0,0 +1,275 @@
1
+ import { AXChartComponent, getEasingFunction, computeTooltipPosition } from '@acorex/charts';
2
+ import { AXChartTooltipComponent } from '@acorex/charts/chart-tooltip';
3
+ import { AXPlatform } from '@acorex/core/platform';
4
+ import * as i0 from '@angular/core';
5
+ import { InjectionToken, input, output, viewChild, signal, inject, computed, afterNextRender, effect, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core';
6
+ import { map } from 'rxjs';
7
+
8
+ const AXFunnelChartDefaultConfig = {
9
+ margin: { top: 20, right: 160, bottom: 20, left: 160 },
10
+ neckWidth: 0.3,
11
+ showLabels: true,
12
+ labelOffset: 24,
13
+ showTooltip: true,
14
+ animationDuration: 1000,
15
+ animationEasing: 'cubic-out',
16
+ startColor: 'rgb(var(--ax-sys-color-primary-50))',
17
+ endColor: 'rgb(var(--ax-sys-color-primary-950))',
18
+ messages: {
19
+ noData: 'No funnel data available',
20
+ noDataIcon: 'fa-light fa-filter-list',
21
+ },
22
+ };
23
+ const AX_FUNNEL_CHART_CONFIG = new InjectionToken('AX_FUNNEL_CHART_CONFIG', {
24
+ providedIn: 'root',
25
+ factory: () => AXFunnelChartDefaultConfig,
26
+ });
27
+
28
+ class AXFunnelChartComponent extends AXChartComponent {
29
+ // Inputs
30
+ data = input([], ...(ngDevMode ? [{ debugName: "data" }] : []));
31
+ options = input({}, ...(ngDevMode ? [{ debugName: "options" }] : []));
32
+ // Outputs
33
+ /** Emitted when a funnel segment is clicked */
34
+ segmentClick = output();
35
+ chartContainerEl = viewChild.required('chartContainer');
36
+ // SVG State
37
+ svgElement = null;
38
+ d3;
39
+ _initialized = signal(false, ...(ngDevMode ? [{ debugName: "_initialized" }] : []));
40
+ _rendered = signal(false, ...(ngDevMode ? [{ debugName: "_rendered" }] : []));
41
+ platformService = inject(AXPlatform);
42
+ isRtl = signal(this.platformService.isRtl(), ...(ngDevMode ? [{ debugName: "isRtl" }] : []));
43
+ directionSub;
44
+ // Tooltip Signals
45
+ _tooltipVisible = signal(false, ...(ngDevMode ? [{ debugName: "_tooltipVisible" }] : []));
46
+ _tooltipPosition = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "_tooltipPosition" }] : []));
47
+ _tooltipData = signal({ title: '', value: '' }, ...(ngDevMode ? [{ debugName: "_tooltipData" }] : []));
48
+ _tooltipRafId = null;
49
+ tooltipVisible = this._tooltipVisible.asReadonly();
50
+ tooltipPosition = this._tooltipPosition.asReadonly();
51
+ tooltipData = this._tooltipData.asReadonly();
52
+ configToken = inject(AX_FUNNEL_CHART_CONFIG);
53
+ effectiveOptions = computed(() => ({
54
+ ...this.configToken,
55
+ ...this.options(),
56
+ }), ...(ngDevMode ? [{ debugName: "effectiveOptions" }] : []));
57
+ constructor() {
58
+ super();
59
+ afterNextRender(() => {
60
+ this._initialized.set(true);
61
+ this.loadD3();
62
+ this.directionSub = this.platformService.directionChange
63
+ .pipe(map((i) => i.data === 'rtl'))
64
+ .subscribe((isRtl) => {
65
+ this.isRtl.set(isRtl);
66
+ if (this._rendered()) {
67
+ this.updateChart();
68
+ }
69
+ });
70
+ });
71
+ effect(() => {
72
+ // Trigger update on data or option change
73
+ this.data();
74
+ this.effectiveOptions();
75
+ if (this._rendered()) {
76
+ this.updateChart();
77
+ }
78
+ });
79
+ }
80
+ ngOnDestroy() {
81
+ this.directionSub?.unsubscribe();
82
+ this.cleanupChart();
83
+ }
84
+ async loadD3() {
85
+ if (this.d3)
86
+ return;
87
+ try {
88
+ this.d3 = await import('d3');
89
+ if (this._initialized() && this.chartContainerEl()) {
90
+ this.createChart();
91
+ this._rendered.set(true);
92
+ }
93
+ }
94
+ catch (error) {
95
+ console.error('AXFunnelChart: Failed to load D3.js', error);
96
+ }
97
+ }
98
+ createChart() {
99
+ if (this.svgElement)
100
+ this.svgElement.remove();
101
+ const data = [...this.data()].sort((a, b) => b.value - a.value);
102
+ if (!data.length) {
103
+ this.hideTooltip();
104
+ return;
105
+ }
106
+ const container = this.chartContainerEl().nativeElement;
107
+ const width = container.clientWidth;
108
+ const height = container.clientHeight;
109
+ const opt = this.effectiveOptions();
110
+ const margin = opt.margin;
111
+ const isRtl = this.isRtl();
112
+ const labelOffset = opt.labelOffset ?? 24;
113
+ this.svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
114
+ const svg = this.d3
115
+ .select(this.svgElement)
116
+ .attr('width', '100%')
117
+ .attr('height', '100%')
118
+ .attr('viewBox', `0 0 ${width} ${height}`)
119
+ .attr('preserveAspectRatio', 'xMidYMid meet');
120
+ container.appendChild(this.svgElement);
121
+ const innerWidth = width - margin.left - margin.right;
122
+ const innerHeight = height - margin.top - margin.bottom;
123
+ const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);
124
+ const sliceHeight = innerHeight / data.length;
125
+ const maxValue = data[0].value;
126
+ const minValue = data[data.length - 1]?.value ?? maxValue;
127
+ const easing = getEasingFunction(this.d3, opt.animationEasing);
128
+ data.forEach((d, i) => {
129
+ const topVal = d.value;
130
+ const bottomVal = data[i + 1]?.value ?? d.value * opt.neckWidth;
131
+ const topW = (topVal / maxValue) * innerWidth;
132
+ const bottomW = (bottomVal / maxValue) * innerWidth;
133
+ const xTop = (innerWidth - topW) / 2;
134
+ const xBottom = (innerWidth - bottomW) / 2;
135
+ const yTop = i * sliceHeight;
136
+ const yBottom = (i + 1) * sliceHeight;
137
+ const pathData = `M ${xTop},${yTop} L ${xTop + topW},${yTop} L ${xBottom + bottomW},${yBottom} L ${xBottom},${yBottom} Z`;
138
+ const sliceGroup = g.append('g').attr('class', 'funnel-slice-container');
139
+ const computedColor = this.resolveSliceColor(d, i, opt, minValue, maxValue);
140
+ const path = sliceGroup
141
+ .append('path')
142
+ .attr('class', 'funnel-slice')
143
+ .attr('d', pathData)
144
+ .attr('fill', computedColor)
145
+ .style('opacity', 0)
146
+ .on('mouseenter', (event) => {
147
+ svg.classed('is-dimmed', true);
148
+ this.d3.select(event.currentTarget).classed('is-active', true);
149
+ this.showTooltip(event, d, computedColor);
150
+ })
151
+ .on('mousemove', (event) => this.updateTooltipPosition(event))
152
+ .on('click', () => this.segmentClick.emit(d))
153
+ .on('mouseleave', (event) => {
154
+ svg.classed('is-dimmed', false);
155
+ this.d3.select(event.currentTarget).classed('is-active', false);
156
+ this.hideTooltip();
157
+ });
158
+ path
159
+ .transition()
160
+ .duration(opt.animationDuration)
161
+ .delay(i * 80)
162
+ .ease(easing)
163
+ .style('opacity', 1);
164
+ if (opt.showLabels) {
165
+ const labelGroup = sliceGroup.append('g').attr('class', 'funnel-label-group').style('opacity', 0);
166
+ const labelX = isRtl ? innerWidth / 2 - topW / 2 - labelOffset : innerWidth / 2 + topW / 2 + labelOffset;
167
+ // In RTL documents, SVG `text-anchor: end` can expand *into* the plot area because "end"
168
+ // becomes the logical left edge. Using `start` makes the label expand away from the slice
169
+ // on both LTR (to the right) and RTL (to the left).
170
+ const anchor = 'start';
171
+ labelGroup
172
+ .append('text')
173
+ .attr('class', 'funnel-label-name')
174
+ .attr('x', labelX)
175
+ .attr('y', yTop + sliceHeight / 2 - 5)
176
+ .attr('text-anchor', anchor)
177
+ .text(d.name);
178
+ labelGroup
179
+ .append('text')
180
+ .attr('class', 'funnel-label-value')
181
+ .attr('x', labelX)
182
+ .attr('y', yTop + sliceHeight / 2 + 15)
183
+ .attr('text-anchor', anchor)
184
+ .text(d.value.toLocaleString());
185
+ labelGroup
186
+ .transition()
187
+ .duration(600)
188
+ .delay(400 + i * 80)
189
+ .style('opacity', 1);
190
+ }
191
+ });
192
+ }
193
+ resolveSliceColor(item, index, opt, minValue, maxValue) {
194
+ if (item.color)
195
+ return item.color;
196
+ const palette = opt.colors?.filter(Boolean) ?? [];
197
+ if (palette.length > 0) {
198
+ const key = String(item.id ?? item.name ?? index);
199
+ const idx = this.hashStringToUint32(key) % palette.length;
200
+ return palette[idx] ?? opt.startColor ?? '#1e1b4b';
201
+ }
202
+ const startColor = opt.startColor ?? '#1e1b4b';
203
+ const endColor = opt.endColor ?? '#818cf8';
204
+ const resolvedStartColor = this.resolveCssColor(startColor);
205
+ const resolvedEndColor = this.resolveCssColor(endColor);
206
+ const range = maxValue - minValue;
207
+ const t = range === 0 ? 1 : (item.value - minValue) / range;
208
+ const clamped = Math.max(0, Math.min(1, t));
209
+ return this.d3.interpolateRgb(resolvedStartColor, resolvedEndColor)(clamped);
210
+ }
211
+ hashStringToUint32(input) {
212
+ let hash = 5381;
213
+ for (let i = 0; i < input.length; i++) {
214
+ hash = (hash * 33) ^ input.charCodeAt(i);
215
+ }
216
+ return hash >>> 0;
217
+ }
218
+ resolveCssColor(color) {
219
+ const container = this.chartContainerEl().nativeElement;
220
+ const probe = document.createElement('span');
221
+ probe.style.color = color;
222
+ probe.style.position = 'absolute';
223
+ probe.style.left = '-9999px';
224
+ probe.style.top = '-9999px';
225
+ container.appendChild(probe);
226
+ const computed = getComputedStyle(probe).color;
227
+ probe.remove();
228
+ return computed || color;
229
+ }
230
+ updateChart() {
231
+ this.createChart();
232
+ }
233
+ showTooltip(event, item, color) {
234
+ if (!this.effectiveOptions().showTooltip)
235
+ return;
236
+ this._tooltipData.set({ title: item.name, value: item.value.toLocaleString(), color });
237
+ this._tooltipVisible.set(true);
238
+ this.updateTooltipPosition(event);
239
+ }
240
+ updateTooltipPosition(event) {
241
+ if (this._tooltipRafId)
242
+ cancelAnimationFrame(this._tooltipRafId);
243
+ this._tooltipRafId = requestAnimationFrame(() => {
244
+ const containerEl = this.chartContainerEl().nativeElement;
245
+ const rect = containerEl.getBoundingClientRect();
246
+ const tooltipEl = containerEl.querySelector('.chart-tooltip');
247
+ const tooltipRect = tooltipEl?.getBoundingClientRect() ?? null;
248
+ this._tooltipPosition.set(computeTooltipPosition(rect, tooltipRect, event.clientX + 10, event.clientY - 10, 10));
249
+ });
250
+ }
251
+ hideTooltip() {
252
+ this._tooltipVisible.set(false);
253
+ }
254
+ cleanupChart() {
255
+ if (this._tooltipRafId)
256
+ cancelAnimationFrame(this._tooltipRafId);
257
+ if (this.svgElement) {
258
+ this.svgElement.remove();
259
+ this.svgElement = null;
260
+ }
261
+ }
262
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXFunnelChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
263
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: AXFunnelChartComponent, isStandalone: true, selector: "ax-funnel-chart", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { segmentClick: "segmentClick" }, viewQueries: [{ propertyName: "chartContainerEl", first: true, predicate: ["chartContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"ax-funnel-chart-container\" role=\"img\" #chartContainer>\n @if (data()?.length === 0) {\n <div class=\"ax-funnel-no-data\">\n <i [class]=\"effectiveOptions().messages?.noDataIcon\"></i>\n <p class=\"ax-funnel-no-data-text\">{{ effectiveOptions().messages?.noData }}</p>\n </div>\n }\n</div>\n\n<ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\">\n</ax-chart-tooltip>\n", styles: ["ax-funnel-chart{display:block;width:100%;height:100%;min-height:350px;--ax-comp-funnel-bg: 0, 0, 0, 0;--ax-comp-funnel-text: var(--ax-sys-color-on-surface);--ax-comp-funnel-label-secondary: var(--ax-sys-color-on-surface-variant);--ax-comp-funnel-slice-opacity: .9;--ax-comp-funnel-dim-opacity: .25}ax-funnel-chart .ax-funnel-chart-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:rgba(var(--ax-comp-funnel-bg));padding:1rem}ax-funnel-chart .ax-funnel-chart-container svg{display:block;width:100%;height:100%;overflow:visible}ax-funnel-chart .ax-funnel-chart-container svg .funnel-slice{cursor:pointer;opacity:var(--ax-comp-funnel-slice-opacity);transition:opacity .3s cubic-bezier(.4,0,.2,1)}ax-funnel-chart .ax-funnel-chart-container svg .funnel-slice.is-active{opacity:1;filter:saturate(1.2)}ax-funnel-chart .ax-funnel-chart-container svg .funnel-label-group{pointer-events:none}ax-funnel-chart .ax-funnel-chart-container svg .funnel-label-group .funnel-label-name{font-size:12px;font-weight:700;fill:rgb(var(--ax-comp-funnel-text));text-transform:uppercase;letter-spacing:.05em}ax-funnel-chart .ax-funnel-chart-container svg .funnel-label-group .funnel-label-value{font-size:13px;fill:rgb(var(--ax-comp-funnel-label-secondary));font-variant-numeric:tabular-nums;font-weight:500}ax-funnel-chart .ax-funnel-chart-container svg.is-dimmed .funnel-slice:not(.is-active){opacity:var(--ax-comp-funnel-dim-opacity)}ax-funnel-chart .ax-funnel-no-data{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:rgba(var(--ax-comp-funnel-text),.6)}ax-funnel-chart .ax-funnel-no-data i{font-size:2rem;margin-bottom:.5rem}\n"], dependencies: [{ kind: "component", type: AXChartTooltipComponent, selector: "ax-chart-tooltip", inputs: ["data", "position", "visible", "showPercentage", "style"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
264
+ }
265
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXFunnelChartComponent, decorators: [{
266
+ type: Component,
267
+ args: [{ selector: 'ax-funnel-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-funnel-chart-container\" role=\"img\" #chartContainer>\n @if (data()?.length === 0) {\n <div class=\"ax-funnel-no-data\">\n <i [class]=\"effectiveOptions().messages?.noDataIcon\"></i>\n <p class=\"ax-funnel-no-data-text\">{{ effectiveOptions().messages?.noData }}</p>\n </div>\n }\n</div>\n\n<ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\">\n</ax-chart-tooltip>\n", styles: ["ax-funnel-chart{display:block;width:100%;height:100%;min-height:350px;--ax-comp-funnel-bg: 0, 0, 0, 0;--ax-comp-funnel-text: var(--ax-sys-color-on-surface);--ax-comp-funnel-label-secondary: var(--ax-sys-color-on-surface-variant);--ax-comp-funnel-slice-opacity: .9;--ax-comp-funnel-dim-opacity: .25}ax-funnel-chart .ax-funnel-chart-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:rgba(var(--ax-comp-funnel-bg));padding:1rem}ax-funnel-chart .ax-funnel-chart-container svg{display:block;width:100%;height:100%;overflow:visible}ax-funnel-chart .ax-funnel-chart-container svg .funnel-slice{cursor:pointer;opacity:var(--ax-comp-funnel-slice-opacity);transition:opacity .3s cubic-bezier(.4,0,.2,1)}ax-funnel-chart .ax-funnel-chart-container svg .funnel-slice.is-active{opacity:1;filter:saturate(1.2)}ax-funnel-chart .ax-funnel-chart-container svg .funnel-label-group{pointer-events:none}ax-funnel-chart .ax-funnel-chart-container svg .funnel-label-group .funnel-label-name{font-size:12px;font-weight:700;fill:rgb(var(--ax-comp-funnel-text));text-transform:uppercase;letter-spacing:.05em}ax-funnel-chart .ax-funnel-chart-container svg .funnel-label-group .funnel-label-value{font-size:13px;fill:rgb(var(--ax-comp-funnel-label-secondary));font-variant-numeric:tabular-nums;font-weight:500}ax-funnel-chart .ax-funnel-chart-container svg.is-dimmed .funnel-slice:not(.is-active){opacity:var(--ax-comp-funnel-dim-opacity)}ax-funnel-chart .ax-funnel-no-data{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:rgba(var(--ax-comp-funnel-text),.6)}ax-funnel-chart .ax-funnel-no-data i{font-size:2rem;margin-bottom:.5rem}\n"] }]
268
+ }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], segmentClick: [{ type: i0.Output, args: ["segmentClick"] }], chartContainerEl: [{ type: i0.ViewChild, args: ['chartContainer', { isSignal: true }] }] } });
269
+
270
+ /**
271
+ * Generated bundle index. Do not edit.
272
+ */
273
+
274
+ export { AXFunnelChartComponent, AXFunnelChartDefaultConfig, AX_FUNNEL_CHART_CONFIG };
275
+ //# sourceMappingURL=acorex-charts-funnel-chart.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acorex-charts-funnel-chart.mjs","sources":["../../../../packages/charts/funnel-chart/src/lib/funnel-chart.config.ts","../../../../packages/charts/funnel-chart/src/lib/funnel-chart.component.ts","../../../../packages/charts/funnel-chart/src/lib/funnel-chart.component.html","../../../../packages/charts/funnel-chart/src/acorex-charts-funnel-chart.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport { AXFunnelChartOption } from './funnel-chart.type';\n\nexport const AXFunnelChartDefaultConfig: AXFunnelChartOption = {\n margin: { top: 20, right: 160, bottom: 20, left: 160 },\n neckWidth: 0.3,\n showLabels: true,\n labelOffset: 24,\n showTooltip: true,\n animationDuration: 1000,\n animationEasing: 'cubic-out',\n startColor: 'rgb(var(--ax-sys-color-primary-50))',\n endColor: 'rgb(var(--ax-sys-color-primary-950))',\n messages: {\n noData: 'No funnel data available',\n noDataIcon: 'fa-light fa-filter-list',\n },\n};\n\nexport const AX_FUNNEL_CHART_CONFIG = new InjectionToken<AXFunnelChartOption>('AX_FUNNEL_CHART_CONFIG', {\n providedIn: 'root',\n factory: () => AXFunnelChartDefaultConfig,\n});\n","import { AXChartComponent, AXChartComponentBase, computeTooltipPosition, getEasingFunction } from '@acorex/charts';\nimport { AXChartTooltipComponent, AXChartTooltipData } from '@acorex/charts/chart-tooltip';\nimport { AXPlatform } from '@acorex/core/platform';\nimport {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n OnDestroy,\n ViewEncapsulation,\n afterNextRender,\n computed,\n effect,\n inject,\n input,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport { map, Subscription } from 'rxjs';\nimport { AX_FUNNEL_CHART_CONFIG } from './funnel-chart.config';\nimport { AXFunnelChartOption, AXFunnelData } from './funnel-chart.type';\n\n@Component({\n selector: 'ax-funnel-chart',\n templateUrl: './funnel-chart.component.html',\n styleUrls: ['./funnel-chart.component.scss'],\n encapsulation: ViewEncapsulation.None,\n imports: [AXChartTooltipComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class AXFunnelChartComponent extends AXChartComponent implements OnDestroy, AXChartComponentBase {\n // Inputs\n data = input<AXFunnelData[]>([]);\n options = input<AXFunnelChartOption>({});\n\n // Outputs\n /** Emitted when a funnel segment is clicked */\n segmentClick = output<AXFunnelData>();\n\n private readonly chartContainerEl = viewChild.required<ElementRef<HTMLDivElement>>('chartContainer');\n\n // SVG State\n private svgElement: SVGSVGElement | null = null;\n protected d3!: typeof import('d3');\n private _initialized = signal(false);\n private _rendered = signal(false);\n private platformService = inject(AXPlatform);\n protected isRtl = signal(this.platformService.isRtl());\n private directionSub?: Subscription;\n\n // Tooltip Signals\n private _tooltipVisible = signal(false);\n private _tooltipPosition = signal({ x: 0, y: 0 });\n private _tooltipData = signal<AXChartTooltipData>({ title: '', value: '' });\n private _tooltipRafId: number | null = null;\n\n protected tooltipVisible = this._tooltipVisible.asReadonly();\n protected tooltipPosition = this._tooltipPosition.asReadonly();\n protected tooltipData = this._tooltipData.asReadonly();\n\n private configToken = inject(AX_FUNNEL_CHART_CONFIG);\n\n protected effectiveOptions = computed(() => ({\n ...this.configToken,\n ...this.options(),\n }));\n\n constructor() {\n super();\n\n afterNextRender(() => {\n this._initialized.set(true);\n this.loadD3();\n this.directionSub = this.platformService.directionChange\n .pipe(map((i) => i.data === 'rtl'))\n .subscribe((isRtl) => {\n this.isRtl.set(isRtl);\n if (this._rendered()) {\n this.updateChart();\n }\n });\n });\n\n effect(() => {\n // Trigger update on data or option change\n this.data();\n this.effectiveOptions();\n if (this._rendered()) {\n this.updateChart();\n }\n });\n }\n\n ngOnDestroy(): void {\n this.directionSub?.unsubscribe();\n this.cleanupChart();\n }\n\n protected async loadD3(): Promise<void> {\n if (this.d3) return;\n try {\n this.d3 = await import('d3');\n if (this._initialized() && this.chartContainerEl()) {\n this.createChart();\n this._rendered.set(true);\n }\n } catch (error) {\n console.error('AXFunnelChart: Failed to load D3.js', error);\n }\n }\n\n public createChart(): void {\n if (this.svgElement) this.svgElement.remove();\n\n const data = [...this.data()].sort((a, b) => b.value - a.value);\n if (!data.length) {\n this.hideTooltip();\n return;\n }\n\n const container = this.chartContainerEl().nativeElement;\n const width = container.clientWidth;\n const height = container.clientHeight;\n const opt = this.effectiveOptions();\n const margin = opt.margin;\n const isRtl = this.isRtl();\n const labelOffset = opt.labelOffset ?? 24;\n\n this.svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n const svg = this.d3\n .select(this.svgElement)\n .attr('width', '100%')\n .attr('height', '100%')\n .attr('viewBox', `0 0 ${width} ${height}`)\n .attr('preserveAspectRatio', 'xMidYMid meet');\n\n container.appendChild(this.svgElement);\n\n const innerWidth = width - margin.left - margin.right;\n const innerHeight = height - margin.top - margin.bottom;\n\n const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);\n\n const sliceHeight = innerHeight / data.length;\n const maxValue = data[0].value;\n const minValue = data[data.length - 1]?.value ?? maxValue;\n const easing = getEasingFunction(this.d3, opt.animationEasing);\n\n data.forEach((d, i) => {\n const topVal = d.value;\n const bottomVal = data[i + 1]?.value ?? d.value * opt.neckWidth;\n\n const topW = (topVal / maxValue) * innerWidth;\n const bottomW = (bottomVal / maxValue) * innerWidth;\n\n const xTop = (innerWidth - topW) / 2;\n const xBottom = (innerWidth - bottomW) / 2;\n const yTop = i * sliceHeight;\n const yBottom = (i + 1) * sliceHeight;\n\n const pathData = `M ${xTop},${yTop} L ${xTop + topW},${yTop} L ${xBottom + bottomW},${yBottom} L ${xBottom},${yBottom} Z`;\n\n const sliceGroup = g.append('g').attr('class', 'funnel-slice-container');\n\n const computedColor = this.resolveSliceColor(d, i, opt, minValue, maxValue);\n const path = sliceGroup\n .append('path')\n .attr('class', 'funnel-slice')\n .attr('d', pathData)\n .attr('fill', computedColor)\n .style('opacity', 0)\n .on('mouseenter', (event) => {\n svg.classed('is-dimmed', true);\n this.d3.select(event.currentTarget).classed('is-active', true);\n this.showTooltip(event, d, computedColor);\n })\n .on('mousemove', (event) => this.updateTooltipPosition(event))\n .on('click', () => this.segmentClick.emit(d))\n .on('mouseleave', (event) => {\n svg.classed('is-dimmed', false);\n this.d3.select(event.currentTarget).classed('is-active', false);\n this.hideTooltip();\n });\n\n path\n .transition()\n .duration(opt.animationDuration)\n .delay(i * 80)\n .ease(easing)\n .style('opacity', 1);\n\n if (opt.showLabels) {\n const labelGroup = sliceGroup.append('g').attr('class', 'funnel-label-group').style('opacity', 0);\n const labelX = isRtl ? innerWidth / 2 - topW / 2 - labelOffset : innerWidth / 2 + topW / 2 + labelOffset;\n // In RTL documents, SVG `text-anchor: end` can expand *into* the plot area because \"end\"\n // becomes the logical left edge. Using `start` makes the label expand away from the slice\n // on both LTR (to the right) and RTL (to the left).\n const anchor = 'start';\n\n labelGroup\n .append('text')\n .attr('class', 'funnel-label-name')\n .attr('x', labelX)\n .attr('y', yTop + sliceHeight / 2 - 5)\n .attr('text-anchor', anchor)\n .text(d.name);\n\n labelGroup\n .append('text')\n .attr('class', 'funnel-label-value')\n .attr('x', labelX)\n .attr('y', yTop + sliceHeight / 2 + 15)\n .attr('text-anchor', anchor)\n .text(d.value.toLocaleString());\n\n labelGroup\n .transition()\n .duration(600)\n .delay(400 + i * 80)\n .style('opacity', 1);\n }\n });\n }\n\n private resolveSliceColor(\n item: AXFunnelData,\n index: number,\n opt: AXFunnelChartOption,\n minValue: number,\n maxValue: number,\n ): string {\n if (item.color) return item.color;\n\n const palette = opt.colors?.filter(Boolean) ?? [];\n if (palette.length > 0) {\n const key = String(item.id ?? item.name ?? index);\n const idx = this.hashStringToUint32(key) % palette.length;\n return palette[idx] ?? opt.startColor ?? '#1e1b4b';\n }\n\n const startColor = opt.startColor ?? '#1e1b4b';\n const endColor = opt.endColor ?? '#818cf8';\n const resolvedStartColor = this.resolveCssColor(startColor);\n const resolvedEndColor = this.resolveCssColor(endColor);\n const range = maxValue - minValue;\n const t = range === 0 ? 1 : (item.value - minValue) / range;\n const clamped = Math.max(0, Math.min(1, t));\n return this.d3.interpolateRgb(resolvedStartColor, resolvedEndColor)(clamped);\n }\n\n private hashStringToUint32(input: string): number {\n let hash = 5381;\n for (let i = 0; i < input.length; i++) {\n hash = (hash * 33) ^ input.charCodeAt(i);\n }\n return hash >>> 0;\n }\n\n private resolveCssColor(color: string): string {\n const container = this.chartContainerEl().nativeElement;\n const probe = document.createElement('span');\n probe.style.color = color;\n probe.style.position = 'absolute';\n probe.style.left = '-9999px';\n probe.style.top = '-9999px';\n container.appendChild(probe);\n const computed = getComputedStyle(probe).color;\n probe.remove();\n return computed || color;\n }\n\n public updateChart(): void {\n this.createChart();\n }\n\n private showTooltip(event: MouseEvent, item: AXFunnelData, color: string): void {\n if (!this.effectiveOptions().showTooltip) return;\n this._tooltipData.set({ title: item.name, value: item.value.toLocaleString(), color });\n this._tooltipVisible.set(true);\n this.updateTooltipPosition(event);\n }\n\n private updateTooltipPosition(event: MouseEvent): void {\n if (this._tooltipRafId) cancelAnimationFrame(this._tooltipRafId);\n this._tooltipRafId = requestAnimationFrame(() => {\n const containerEl = this.chartContainerEl().nativeElement;\n const rect = containerEl.getBoundingClientRect();\n const tooltipEl = containerEl.querySelector('.chart-tooltip') as HTMLElement;\n const tooltipRect = tooltipEl?.getBoundingClientRect() ?? null;\n this._tooltipPosition.set(computeTooltipPosition(rect, tooltipRect, event.clientX + 10, event.clientY - 10, 10));\n });\n }\n\n private hideTooltip(): void {\n this._tooltipVisible.set(false);\n }\n\n public cleanupChart(): void {\n if (this._tooltipRafId) cancelAnimationFrame(this._tooltipRafId);\n if (this.svgElement) {\n this.svgElement.remove();\n this.svgElement = null;\n }\n }\n\n\n}\n","<div class=\"ax-funnel-chart-container\" role=\"img\" #chartContainer>\n @if (data()?.length === 0) {\n <div class=\"ax-funnel-no-data\">\n <i [class]=\"effectiveOptions().messages?.noDataIcon\"></i>\n <p class=\"ax-funnel-no-data-text\">{{ effectiveOptions().messages?.noData }}</p>\n </div>\n }\n</div>\n\n<ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\">\n</ax-chart-tooltip>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -81,6 +81,10 @@ class AXGaugeChartComponent extends AXChartComponent {
81
81
  HALF_CIRCLE_RADIANS = Math.PI;
82
82
  QUARTER_CIRCLE_RADIANS = Math.PI / 2;
83
83
  DEGREES_PER_RADIAN = 180 / Math.PI;
84
+ CHART_EDGE_PADDING = 12;
85
+ TICK_LABEL_CHAR_WIDTH_RATIO = 0.62;
86
+ TICK_LABEL_SIDE_PADDING = 6;
87
+ LABEL_TICK_CLEARANCE = 4;
84
88
  constructor() {
85
89
  super();
86
90
  // Dynamically load D3 and initialize the chart when the component is ready
@@ -165,29 +169,36 @@ class AXGaugeChartComponent extends AXChartComponent {
165
169
  // Calculate margin as percentage of size (5-8% with reasonable bounds)
166
170
  const marginRatio = 0.08;
167
171
  const margin = Math.max(5, Math.min(size * marginRatio, 30));
168
- // Calculate space needed for ticks and labels (increased for larger font sizes)
169
- const tickLabelSpace = Math.max(12, Math.min(size * 0.12, 40));
172
+ const tickCount = this.getTickCount(size);
173
+ const tickFontPx = this.getTickFontSize(size);
174
+ const maxTickLabelWidth = this.estimateMaxTickLabelWidth(tickCount, minValue, maxValue, tickFontPx);
175
+ const tickLength = Math.max(4, Math.min((size / 2) * 0.12, 14));
176
+ // Keep labels closer to the gauge arc while preserving readability.
177
+ const labelGap = Math.max(6, Math.min((size / 2) * 0.06, 14));
178
+ const labelOffsetFromTickEnd = labelGap + tickFontPx * 0.35;
179
+ const tickLabelSpace = tickLength + labelOffsetFromTickEnd + tickFontPx;
170
180
  const totalVerticalSpace = size / 2 + tickLabelSpace;
171
- // Calculate horizontal padding for tick labels (they extend beyond gauge edges)
172
- // Estimate based on font size - tick labels are roughly 2-3 characters wide
173
- const estimatedTickFontSize = Math.max(14, Math.min(18, Math.floor(size / 35)));
174
- const horizontalPadding = Math.max(20, estimatedTickFontSize * 1.5);
181
+ // Labels at edge ticks use start/end anchors, so they can extend by near full width.
182
+ const horizontalPadding = Math.max(18, maxTickLabelWidth + 8);
175
183
  const totalWidth = size + horizontalPadding * 2;
184
+ const viewBoxWidth = totalWidth + this.CHART_EDGE_PADDING * 2;
185
+ const viewBoxHeight = totalVerticalSpace + this.CHART_EDGE_PADDING * 2;
176
186
  // Set up SVG with responsive viewBox that accounts for overflow
177
187
  const svg = this.d3
178
188
  .select(this.svgElement)
179
189
  .attr('width', '100%')
180
190
  .attr('height', '100%')
181
- .attr('viewBox', `0 0 ${totalWidth} ${totalVerticalSpace}`)
191
+ .attr('viewBox', `0 0 ${viewBoxWidth} ${viewBoxHeight}`)
182
192
  .attr('preserveAspectRatio', 'xMidYMid meet');
183
193
  // Create a group for the chart centered horizontally with padding, positioned to show only the top half
184
194
  const chartGroup = svg
185
195
  .append('g')
186
- .attr('transform', `translate(${size / 2 + horizontalPadding}, ${size / 2 - margin})`);
196
+ .attr('transform', `translate(${size / 2 + horizontalPadding + this.CHART_EDGE_PADDING}, ${size / 2 - margin + this.CHART_EDGE_PADDING})`);
187
197
  // Define gauge parameters
188
198
  const radius = size / 2 - margin;
189
- const desiredGaugeWidth = typeof gaugeWidth === 'number' && !Number.isNaN(gaugeWidth) ? gaugeWidth : size * 0.12;
190
- const clampedGaugeWidth = Math.max(size * 0.06, Math.min(desiredGaugeWidth, size * 0.2));
199
+ const desiredGaugeWidth = typeof gaugeWidth === 'number' && !Number.isNaN(gaugeWidth) ? gaugeWidth : 30;
200
+ // Respect explicit gaugeWidth in px with sane absolute bounds.
201
+ const clampedGaugeWidth = Math.max(6, Math.min(desiredGaugeWidth, radius * 0.7));
191
202
  const innerRadius = radius - clampedGaugeWidth;
192
203
  const outerRadius = radius;
193
204
  // Create gradient definitions
@@ -199,7 +210,7 @@ class AXGaugeChartComponent extends AXChartComponent {
199
210
  this.drawThresholds(chartGroup, innerRadius, outerRadius, minValue, maxValue, thresholds, cornerRadius);
200
211
  }
201
212
  // Draw tick marks
202
- this.drawTicks(chartGroup, outerRadius, minValue, maxValue, size);
213
+ this.drawTicks(chartGroup, outerRadius, minValue, maxValue, size, tickCount, tickFontPx);
203
214
  // Draw the dial/needle with animation
204
215
  this.drawDial(chartGroup, radius, this.value(), minValue, maxValue, animationDuration);
205
216
  // Draw the value display (after the dial so it's on top)
@@ -485,79 +496,130 @@ class AXGaugeChartComponent extends AXChartComponent {
485
496
  /**
486
497
  * Draws tick marks and labels around the gauge
487
498
  */
488
- drawTicks(chartGroup, radius, minValue, maxValue, size) {
489
- // Dynamically choose tick count based on chart size (reduced for better spacing)
490
- let tickCount = 5;
491
- if (size < 200)
492
- tickCount = 3; // Very small: only show min, mid, max
493
- else if (size < 300)
494
- tickCount = 4;
495
- else if (size < 400)
496
- tickCount = 5;
497
- else if (size > 520)
498
- tickCount = 7;
499
- // Scale tick length and label offset proportionally to size
499
+ drawTicks(chartGroup, radius, minValue, maxValue, size, tickCount, tickFontPx) {
500
500
  const tickLength = Math.max(4, Math.min(radius * 0.12, 14));
501
- // Increased label offset to prevent overlap with tick lines and wide labels (like 100M)
502
- const labelOffset = Math.max(22, Math.min(radius * 0.28, 45));
503
- // Create a group for the ticks
501
+ const baseLabelOffset = this.getBaseLabelOffset(radius, tickLength, tickFontPx);
504
502
  const tickGroup = chartGroup.append('g').attr('class', 'ticks');
505
- // Generate tick values
506
503
  const tickValues = [];
507
504
  const step = (maxValue - minValue) / (tickCount - 1);
508
505
  for (let i = 0; i < tickCount; i++) {
509
506
  tickValues.push(minValue + i * step);
510
507
  }
511
- // Create ticks and labels
512
- tickValues.forEach((tick) => {
513
- // Calculate angle for this tick
508
+ const tickMetrics = tickValues.map((tick) => {
514
509
  const angle = this.scaleValueToAngle(tick, minValue, maxValue);
515
- const radians = angle - this.QUARTER_CIRCLE_RADIANS; // Adjust for starting at bottom (-90 degrees)
516
- // Calculate positions
517
- const x1 = Math.cos(radians) * (radius + 5);
518
- const y1 = Math.sin(radians) * (radius + 5);
519
- const x2 = Math.cos(radians) * (radius + tickLength);
520
- const y2 = Math.sin(radians) * (radius + tickLength);
521
- // Calculate label position
522
- const labelX = Math.cos(radians) * (radius + labelOffset);
523
- const labelY = Math.sin(radians) * (radius + labelOffset);
524
- // Draw tick line
510
+ const radians = angle - this.QUARTER_CIRCLE_RADIANS;
511
+ return {
512
+ radians,
513
+ cos: Math.cos(radians),
514
+ sin: Math.sin(radians),
515
+ label: this.formatTickValue(tick, minValue, maxValue),
516
+ };
517
+ });
518
+ tickMetrics.forEach((metric) => {
525
519
  tickGroup
526
520
  .append('line')
527
- .attr('x1', x1)
528
- .attr('y1', y1)
529
- .attr('x2', x2)
530
- .attr('y2', y2)
521
+ .attr('x1', metric.cos * (radius + 5))
522
+ .attr('y1', metric.sin * (radius + 5))
523
+ .attr('x2', metric.cos * (radius + tickLength))
524
+ .attr('y2', metric.sin * (radius + tickLength))
531
525
  .attr('stroke', 'rgba(var(--ax-comp-gauge-chart-text-color), 0.5)')
532
526
  .attr('stroke-width', 2);
533
- // Add tick label with dynamic font size (minimum 14px)
534
- const tickFontPx = Math.max(14, Math.min(18, Math.floor(size / 35)));
535
- // Smart formatting: preserve decimals for small ranges, round for large values
536
- const valueRange = maxValue - minValue;
537
- let formattedValue;
538
- if (valueRange <= 10) {
539
- // Small range: show up to 2 decimal places, remove trailing zeros
540
- formattedValue = tick.toFixed(2).replace(/\.?0+$/, '');
541
- }
542
- else if (valueRange < 1000) {
543
- // Medium range: show 1 decimal if needed
544
- formattedValue = tick % 1 === 0 ? tick.toString() : tick.toFixed(1);
527
+ });
528
+ const textSelection = tickGroup
529
+ .selectAll('text.gauge-tick-label')
530
+ .data(tickMetrics)
531
+ .enter()
532
+ .append('text')
533
+ .attr('class', 'gauge-tick-label')
534
+ .attr('x', 0)
535
+ .attr('y', 0)
536
+ .attr('fill', 'rgba(var(--ax-comp-gauge-chart-text-color), 0.7)')
537
+ .style('font-size', `${tickFontPx}px`)
538
+ .text((d) => d.label);
539
+ textSelection.each((d, index, nodes) => {
540
+ const node = nodes[index];
541
+ let bboxWidth = this.estimateLabelWidth(d.label, tickFontPx);
542
+ let bboxHeight = tickFontPx;
543
+ try {
544
+ const bbox = node.getBBox();
545
+ if (bbox.width > 0)
546
+ bboxWidth = bbox.width;
547
+ if (bbox.height > 0)
548
+ bboxHeight = bbox.height;
545
549
  }
546
- else {
547
- // Large range: use abbreviated format (K, M, B)
548
- formattedValue = formatLargeNumber(Math.round(tick));
550
+ catch {
551
+ // Keep fallback estimates.
549
552
  }
550
- tickGroup
551
- .append('text')
552
- .attr('x', labelX)
553
- .attr('y', labelY)
554
- .attr('text-anchor', 'middle')
555
- .attr('dominant-baseline', 'middle')
556
- .attr('fill', 'rgba(var(--ax-comp-gauge-chart-text-color), 0.7)')
557
- .style('font-size', `${tickFontPx}px`)
558
- .text(formattedValue);
553
+ const anchor = this.getTickLabelAnchor(d.cos);
554
+ const nearEdgeExtent = this.getNearEdgeProjection(anchor, d.cos, d.sin, bboxWidth, bboxHeight);
555
+ const requiredOffset = tickLength + this.LABEL_TICK_CLEARANCE + nearEdgeExtent;
556
+ const effectiveLabelOffset = Math.max(baseLabelOffset, requiredOffset);
557
+ const nudge = tickFontPx * 0.1;
558
+ const x = d.cos * (radius + effectiveLabelOffset) + (anchor === 'start' ? nudge : anchor === 'end' ? -nudge : 0);
559
+ const y = d.sin * (radius + effectiveLabelOffset);
560
+ this.d3.select(node).attr('x', x).attr('y', y).attr('text-anchor', anchor).attr('dominant-baseline', 'middle');
559
561
  });
560
562
  }
563
+ getTickCount(size) {
564
+ if (size < 200)
565
+ return 3;
566
+ if (size < 300)
567
+ return 4;
568
+ if (size < 400)
569
+ return 5;
570
+ if (size > 520)
571
+ return 7;
572
+ return 5;
573
+ }
574
+ getTickFontSize(size) {
575
+ return Math.max(14, Math.min(18, Math.floor(size / 35)));
576
+ }
577
+ getBaseLabelOffset(radius, tickLength, tickFontPx) {
578
+ const labelGap = Math.max(6, Math.min(radius * 0.06, 14));
579
+ return tickLength + labelGap + tickFontPx * 0.2;
580
+ }
581
+ getTickLabelAnchor(cosValue) {
582
+ if (cosValue > 0.35)
583
+ return 'start';
584
+ if (cosValue < -0.35)
585
+ return 'end';
586
+ return 'middle';
587
+ }
588
+ getNearEdgeProjection(anchor, cosValue, sinValue, bboxWidth, bboxHeight) {
589
+ const absCos = Math.abs(cosValue);
590
+ const absSin = Math.abs(sinValue);
591
+ const halfHeightProjection = absSin * (bboxHeight / 2);
592
+ // For start/end anchored labels on edge ticks, the nearest side is close to the anchor point.
593
+ if (anchor === 'start' || anchor === 'end') {
594
+ return halfHeightProjection + absCos * this.TICK_LABEL_SIDE_PADDING;
595
+ }
596
+ // For centered labels, nearest side includes half text width projection.
597
+ return absCos * (bboxWidth / 2) + halfHeightProjection;
598
+ }
599
+ formatTickValue(tick, minValue, maxValue) {
600
+ const valueRange = maxValue - minValue;
601
+ if (valueRange <= 10) {
602
+ return tick.toFixed(2).replace(/\.?0+$/, '');
603
+ }
604
+ if (valueRange < 1000) {
605
+ return tick % 1 === 0 ? tick.toString() : tick.toFixed(1);
606
+ }
607
+ return formatLargeNumber(Math.round(tick));
608
+ }
609
+ estimateLabelWidth(label, tickFontPx) {
610
+ return label.length * tickFontPx * this.TICK_LABEL_CHAR_WIDTH_RATIO + this.TICK_LABEL_SIDE_PADDING;
611
+ }
612
+ estimateMaxTickLabelWidth(tickCount, minValue, maxValue, tickFontPx) {
613
+ if (tickCount <= 1)
614
+ return tickFontPx;
615
+ const step = (maxValue - minValue) / (tickCount - 1);
616
+ let maxChars = 0;
617
+ for (let i = 0; i < tickCount; i++) {
618
+ const label = this.formatTickValue(minValue + i * step, minValue, maxValue);
619
+ maxChars = Math.max(maxChars, label.length);
620
+ }
621
+ return maxChars * tickFontPx * this.TICK_LABEL_CHAR_WIDTH_RATIO + this.TICK_LABEL_SIDE_PADDING;
622
+ }
561
623
  /**
562
624
  * Draws the value and label text in the center
563
625
  */
@@ -671,13 +733,13 @@ class AXGaugeChartComponent extends AXChartComponent {
671
733
  radiansToDegrees(radians) {
672
734
  return radians * this.DEGREES_PER_RADIAN;
673
735
  }
674
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXGaugeChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
675
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.3", type: AXGaugeChartComponent, isStandalone: true, selector: "ax-gauge-chart", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "chartContainerEl", first: true, predicate: ["chartContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"ax-gauge-chart\" role=\"img\" #chartContainer></div>\n<ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\"></ax-chart-tooltip>\n", styles: ["ax-gauge-chart{display:block;width:100%;height:100%;min-height:200px;--ax-comp-gauge-chart-bg-color: 0, 0, 0, 0;--ax-comp-gauge-chart-text-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-gauge-chart-track-color: var(--ax-sys-color-dark-surface);--ax-comp-gauge-chart-needle-color: var(--ax-sys-color-primary-500)}ax-gauge-chart .ax-gauge-chart{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgba(var(--ax-comp-gauge-chart-bg-color));border-radius:.5rem}ax-gauge-chart .ax-gauge-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}ax-gauge-chart .ax-gauge-chart svg g:has(text){font-family:inherit}ax-gauge-chart .ax-gauge-chart-no-data-message{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:1rem;width:100%;height:100%;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgb(var(--ax-comp-gauge-chart-bg-color))}ax-gauge-chart .ax-gauge-chart-no-data-icon{margin-bottom:.75rem;color:rgba(var(--ax-comp-gauge-chart-text-color),.6)}ax-gauge-chart .ax-gauge-chart-no-data-text{font-weight:600;color:rgb(var(--ax-comp-gauge-chart-text-color))}\n"], dependencies: [{ kind: "component", type: AXChartTooltipComponent, selector: "ax-chart-tooltip", inputs: ["data", "position", "visible", "showPercentage", "style"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
736
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXGaugeChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
737
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.3", type: AXGaugeChartComponent, isStandalone: true, selector: "ax-gauge-chart", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "chartContainerEl", first: true, predicate: ["chartContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"ax-gauge-chart\" role=\"img\" #chartContainer></div>\n<ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\"></ax-chart-tooltip>\n", styles: ["ax-gauge-chart{display:block;width:100%;height:100%;min-height:clamp(220px,38vw,360px);--ax-comp-gauge-chart-bg-color: 0, 0, 0, 0;--ax-comp-gauge-chart-text-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-gauge-chart-track-color: var(--ax-sys-color-dark-surface);--ax-comp-gauge-chart-needle-color: var(--ax-sys-color-primary-500)}ax-gauge-chart .ax-gauge-chart{position:relative;width:100%;height:100%;box-sizing:border-box;padding:clamp(.5rem,1.2vw,.875rem);overflow:hidden;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgba(var(--ax-comp-gauge-chart-bg-color));border-radius:.5rem}ax-gauge-chart .ax-gauge-chart svg{display:block;width:100%;height:100%;max-width:100%;max-height:100%;overflow:hidden}ax-gauge-chart .ax-gauge-chart svg g:has(text){font-family:inherit}ax-gauge-chart .ax-gauge-chart-no-data-message{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:1rem;width:100%;height:100%;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgb(var(--ax-comp-gauge-chart-bg-color))}ax-gauge-chart .ax-gauge-chart-no-data-icon{margin-bottom:.75rem;color:rgba(var(--ax-comp-gauge-chart-text-color),.6)}ax-gauge-chart .ax-gauge-chart-no-data-text{font-weight:600;color:rgb(var(--ax-comp-gauge-chart-text-color))}\n"], dependencies: [{ kind: "component", type: AXChartTooltipComponent, selector: "ax-chart-tooltip", inputs: ["data", "position", "visible", "showPercentage", "style"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
676
738
  }
677
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXGaugeChartComponent, decorators: [{
739
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXGaugeChartComponent, decorators: [{
678
740
  type: Component,
679
- args: [{ selector: 'ax-gauge-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-gauge-chart\" role=\"img\" #chartContainer></div>\n<ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\"></ax-chart-tooltip>\n", styles: ["ax-gauge-chart{display:block;width:100%;height:100%;min-height:200px;--ax-comp-gauge-chart-bg-color: 0, 0, 0, 0;--ax-comp-gauge-chart-text-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-gauge-chart-track-color: var(--ax-sys-color-dark-surface);--ax-comp-gauge-chart-needle-color: var(--ax-sys-color-primary-500)}ax-gauge-chart .ax-gauge-chart{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgba(var(--ax-comp-gauge-chart-bg-color));border-radius:.5rem}ax-gauge-chart .ax-gauge-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}ax-gauge-chart .ax-gauge-chart svg g:has(text){font-family:inherit}ax-gauge-chart .ax-gauge-chart-no-data-message{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:1rem;width:100%;height:100%;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgb(var(--ax-comp-gauge-chart-bg-color))}ax-gauge-chart .ax-gauge-chart-no-data-icon{margin-bottom:.75rem;color:rgba(var(--ax-comp-gauge-chart-text-color),.6)}ax-gauge-chart .ax-gauge-chart-no-data-text{font-weight:600;color:rgb(var(--ax-comp-gauge-chart-text-color))}\n"] }]
680
- }], ctorParameters: () => [] });
741
+ args: [{ selector: 'ax-gauge-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-gauge-chart\" role=\"img\" #chartContainer></div>\n<ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\"></ax-chart-tooltip>\n", styles: ["ax-gauge-chart{display:block;width:100%;height:100%;min-height:clamp(220px,38vw,360px);--ax-comp-gauge-chart-bg-color: 0, 0, 0, 0;--ax-comp-gauge-chart-text-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-gauge-chart-track-color: var(--ax-sys-color-dark-surface);--ax-comp-gauge-chart-needle-color: var(--ax-sys-color-primary-500)}ax-gauge-chart .ax-gauge-chart{position:relative;width:100%;height:100%;box-sizing:border-box;padding:clamp(.5rem,1.2vw,.875rem);overflow:hidden;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgba(var(--ax-comp-gauge-chart-bg-color));border-radius:.5rem}ax-gauge-chart .ax-gauge-chart svg{display:block;width:100%;height:100%;max-width:100%;max-height:100%;overflow:hidden}ax-gauge-chart .ax-gauge-chart svg g:has(text){font-family:inherit}ax-gauge-chart .ax-gauge-chart-no-data-message{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:1rem;width:100%;height:100%;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgb(var(--ax-comp-gauge-chart-bg-color))}ax-gauge-chart .ax-gauge-chart-no-data-icon{margin-bottom:.75rem;color:rgba(var(--ax-comp-gauge-chart-text-color),.6)}ax-gauge-chart .ax-gauge-chart-no-data-text{font-weight:600;color:rgb(var(--ax-comp-gauge-chart-text-color))}\n"] }]
742
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], chartContainerEl: [{ type: i0.ViewChild, args: ['chartContainer', { isSignal: true }] }] } });
681
743
 
682
744
  /**
683
745
  * Generated bundle index. Do not edit.