@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.
- package/fesm2022/acorex-charts-bar-chart.mjs +15 -11
- package/fesm2022/acorex-charts-bar-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-chart-legend.mjs +5 -5
- package/fesm2022/acorex-charts-chart-legend.mjs.map +1 -1
- package/fesm2022/acorex-charts-chart-tooltip.mjs +4 -4
- package/fesm2022/acorex-charts-chart-tooltip.mjs.map +1 -1
- package/fesm2022/acorex-charts-donut-chart.mjs +16 -8
- package/fesm2022/acorex-charts-donut-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-funnel-chart.mjs +275 -0
- package/fesm2022/acorex-charts-funnel-chart.mjs.map +1 -0
- package/fesm2022/acorex-charts-gauge-chart.mjs +135 -73
- package/fesm2022/acorex-charts-gauge-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-heatmap-chart.mjs +281 -0
- package/fesm2022/acorex-charts-heatmap-chart.mjs.map +1 -0
- package/fesm2022/acorex-charts-hierarchy-chart.mjs +4 -4
- package/fesm2022/acorex-charts-hierarchy-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-line-chart.mjs +15 -17
- package/fesm2022/acorex-charts-line-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts.mjs +3 -3
- package/fesm2022/acorex-charts.mjs.map +1 -1
- package/funnel-chart/README.md +3 -0
- package/heatmap-chart/README.md +3 -0
- package/package.json +18 -10
- package/{bar-chart/index.d.ts → types/acorex-charts-bar-chart.d.ts} +4 -1
- package/{donut-chart/index.d.ts → types/acorex-charts-donut-chart.d.ts} +3 -1
- package/types/acorex-charts-funnel-chart.d.ts +108 -0
- package/{gauge-chart/index.d.ts → types/acorex-charts-gauge-chart.d.ts} +14 -1
- package/types/acorex-charts-heatmap-chart.d.ts +111 -0
- package/{hierarchy-chart/index.d.ts → types/acorex-charts-hierarchy-chart.d.ts} +3 -2
- package/{line-chart/index.d.ts → types/acorex-charts-line-chart.d.ts} +3 -1
- /package/{chart-legend/index.d.ts → types/acorex-charts-chart-legend.d.ts} +0 -0
- /package/{chart-tooltip/index.d.ts → types/acorex-charts-chart-tooltip.d.ts} +0 -0
- /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
|
-
|
|
169
|
-
const
|
|
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
|
-
//
|
|
172
|
-
|
|
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 ${
|
|
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 :
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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',
|
|
528
|
-
.attr('y1',
|
|
529
|
-
.attr('x2',
|
|
530
|
-
.attr('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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
547
|
-
//
|
|
548
|
-
formattedValue = formatLargeNumber(Math.round(tick));
|
|
550
|
+
catch {
|
|
551
|
+
// Keep fallback estimates.
|
|
549
552
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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: "
|
|
675
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "
|
|
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: "
|
|
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:
|
|
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.
|