@acorex/charts 20.2.0-next.9 → 20.2.1
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/bar-chart/index.d.ts +74 -13
- package/chart-legend/index.d.ts +1 -1
- package/chart-tooltip/index.d.ts +11 -4
- package/donut-chart/index.d.ts +39 -6
- package/fesm2022/acorex-charts-bar-chart.mjs +672 -360
- package/fesm2022/acorex-charts-bar-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-chart-legend.mjs +12 -15
- package/fesm2022/acorex-charts-chart-legend.mjs.map +1 -1
- package/fesm2022/acorex-charts-chart-tooltip.mjs +38 -13
- package/fesm2022/acorex-charts-chart-tooltip.mjs.map +1 -1
- package/fesm2022/acorex-charts-donut-chart.mjs +205 -228
- package/fesm2022/acorex-charts-donut-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-gauge-chart.mjs +53 -86
- package/fesm2022/acorex-charts-gauge-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-hierarchy-chart.mjs +29 -9
- package/fesm2022/acorex-charts-hierarchy-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts-line-chart.mjs +67 -60
- package/fesm2022/acorex-charts-line-chart.mjs.map +1 -1
- package/fesm2022/acorex-charts.mjs +72 -1
- package/fesm2022/acorex-charts.mjs.map +1 -1
- package/gauge-chart/index.d.ts +20 -9
- package/hierarchy-chart/index.d.ts +27 -4
- package/index.d.ts +29 -1
- package/line-chart/index.d.ts +34 -5
- package/package.json +3 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AX_CHART_COLOR_PALETTE, getChartColor } from '@acorex/charts';
|
|
1
|
+
import { AX_CHART_COLOR_PALETTE, getChartColor, getEasingFunction, computeTooltipPosition } from '@acorex/charts';
|
|
2
2
|
import { AXChartTooltipComponent } from '@acorex/charts/chart-tooltip';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
4
|
import { InjectionToken, inject, ChangeDetectorRef, input, output, viewChild, signal, computed, afterNextRender, effect, untracked, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core';
|
|
@@ -12,6 +12,14 @@ const AXDonutChartDefaultConfig = {
|
|
|
12
12
|
cornerRadius: 4,
|
|
13
13
|
animationDuration: 800,
|
|
14
14
|
animationEasing: 'cubic-out',
|
|
15
|
+
messages: {
|
|
16
|
+
noData: 'No data available',
|
|
17
|
+
noDataHelp: 'Please provide data to display the chart',
|
|
18
|
+
allHidden: 'All segments are hidden',
|
|
19
|
+
allHiddenHelp: 'Please toggle visibility of at least one segment',
|
|
20
|
+
noDataIcon: 'fa-light fa-chart-pie',
|
|
21
|
+
allHiddenIcon: 'fa-light fa-eye-slash',
|
|
22
|
+
},
|
|
15
23
|
};
|
|
16
24
|
const AX_DONUT_CHART_CONFIG = new InjectionToken('AX_DONUT_CHART_CONFIG', {
|
|
17
25
|
providedIn: 'root',
|
|
@@ -60,6 +68,7 @@ class AXDonutChartComponent {
|
|
|
60
68
|
_initialized = signal(false, ...(ngDevMode ? [{ debugName: "_initialized" }] : []));
|
|
61
69
|
_rendered = signal(false, ...(ngDevMode ? [{ debugName: "_rendered" }] : []));
|
|
62
70
|
_isInitialAnimating = signal(false, ...(ngDevMode ? [{ debugName: "_isInitialAnimating" }] : []));
|
|
71
|
+
_currentlyHighlightedSegment = signal(null, ...(ngDevMode ? [{ debugName: "_currentlyHighlightedSegment" }] : []));
|
|
63
72
|
// Tooltip state
|
|
64
73
|
_tooltipVisible = signal(false, ...(ngDevMode ? [{ debugName: "_tooltipVisible" }] : []));
|
|
65
74
|
_tooltipPosition = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "_tooltipPosition" }] : []));
|
|
@@ -84,6 +93,23 @@ class AXDonutChartComponent {
|
|
|
84
93
|
...this.options(),
|
|
85
94
|
};
|
|
86
95
|
}, ...(ngDevMode ? [{ debugName: "effectiveOptions" }] : []));
|
|
96
|
+
// Tooltip positioning gap in pixels
|
|
97
|
+
TOOLTIP_GAP = 10;
|
|
98
|
+
// Messages with defaults
|
|
99
|
+
effectiveMessages = computed(() => {
|
|
100
|
+
const defaultMessages = {
|
|
101
|
+
noData: 'No data available',
|
|
102
|
+
noDataHelp: 'Please provide data to display the chart',
|
|
103
|
+
allHidden: 'All segments are hidden',
|
|
104
|
+
allHiddenHelp: 'Please toggle visibility of at least one segment',
|
|
105
|
+
noDataIcon: 'fa-light fa-chart-pie',
|
|
106
|
+
allHiddenIcon: 'fa-light fa-eye-slash',
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
...defaultMessages,
|
|
110
|
+
...this.effectiveOptions().messages,
|
|
111
|
+
};
|
|
112
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveMessages" }] : []));
|
|
87
113
|
// Data accessor for handling different incoming data formats
|
|
88
114
|
chartDataArray = computed(() => {
|
|
89
115
|
return this.data() || [];
|
|
@@ -106,7 +132,8 @@ class AXDonutChartComponent {
|
|
|
106
132
|
#eff = effect(() => {
|
|
107
133
|
// Access inputs to track them
|
|
108
134
|
this.data();
|
|
109
|
-
//
|
|
135
|
+
// Track options changes to trigger re-render on updates
|
|
136
|
+
this.effectiveOptions();
|
|
110
137
|
// Only update if already rendered
|
|
111
138
|
untracked(() => {
|
|
112
139
|
if (this._rendered()) {
|
|
@@ -123,60 +150,61 @@ class AXDonutChartComponent {
|
|
|
123
150
|
return;
|
|
124
151
|
if (!this.svg)
|
|
125
152
|
return;
|
|
153
|
+
const currentlyHighlighted = this._currentlyHighlightedSegment();
|
|
154
|
+
// If the same segment is already highlighted, do nothing
|
|
155
|
+
if (currentlyHighlighted === id) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Update the currently highlighted segment state
|
|
159
|
+
this._currentlyHighlightedSegment.set(id);
|
|
126
160
|
const transitionDuration = 150;
|
|
127
|
-
// Helper to
|
|
128
|
-
const
|
|
161
|
+
// Helper to get full data for the segment ID
|
|
162
|
+
const getSegmentDataById = (segmentId) => {
|
|
163
|
+
return untracked(() => this.chartDataArray()).find((d) => d.id === segmentId) || null;
|
|
164
|
+
};
|
|
165
|
+
// Always interrupt any ongoing transitions first
|
|
166
|
+
this.svg.selectAll('path').interrupt();
|
|
167
|
+
if (id === null) {
|
|
168
|
+
// Clear all highlights
|
|
129
169
|
this.svg
|
|
130
170
|
.selectAll('path')
|
|
131
171
|
.classed('ax-donut-chart-highlighted', false)
|
|
132
172
|
.classed('ax-donut-chart-dimmed', false)
|
|
133
173
|
.transition()
|
|
134
174
|
.duration(transitionDuration)
|
|
135
|
-
.attr('transform',
|
|
175
|
+
.attr('transform', null)
|
|
136
176
|
.style('filter', null)
|
|
137
177
|
.style('opacity', 1);
|
|
138
178
|
this.segmentHover.emit(null);
|
|
139
|
-
};
|
|
140
|
-
// Helper to get full data for the segment ID
|
|
141
|
-
const getSegmentDataById = (segmentId) => {
|
|
142
|
-
return untracked(() => this.chartDataArray()).find((d) => d.id === segmentId) || null;
|
|
143
|
-
};
|
|
144
|
-
if (id === null) {
|
|
145
|
-
unhighlightAllSegments();
|
|
146
179
|
return;
|
|
147
180
|
}
|
|
148
181
|
const targetSegment = this.svg.selectAll('path').filter((d) => d?.data?.id === id);
|
|
149
182
|
if (targetSegment.empty()) {
|
|
150
|
-
|
|
183
|
+
// Target not found, clear highlights
|
|
184
|
+
this._currentlyHighlightedSegment.set(null);
|
|
185
|
+
this.highlightSegment(null);
|
|
151
186
|
return;
|
|
152
187
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.transition()
|
|
174
|
-
.duration(transitionDuration)
|
|
175
|
-
.attr('transform', 'scale(1.02)')
|
|
176
|
-
.style('filter', 'url(#ax-donut-chart-segment-shadow)') // Apply shadow
|
|
177
|
-
.style('opacity', 1); // Ensure full opacity
|
|
178
|
-
this.segmentHover.emit(getSegmentDataById(id));
|
|
179
|
-
}
|
|
188
|
+
// Dim all segments that are NOT the target
|
|
189
|
+
this.svg
|
|
190
|
+
.selectAll('path')
|
|
191
|
+
.filter((d) => d?.data?.id !== id)
|
|
192
|
+
.classed('ax-donut-chart-highlighted', false)
|
|
193
|
+
.classed('ax-donut-chart-dimmed', true)
|
|
194
|
+
.transition()
|
|
195
|
+
.duration(transitionDuration)
|
|
196
|
+
.attr('transform', null)
|
|
197
|
+
.style('filter', null)
|
|
198
|
+
.style('opacity', 0.5);
|
|
199
|
+
// Highlight the target segment
|
|
200
|
+
targetSegment
|
|
201
|
+
.classed('ax-donut-chart-dimmed', false)
|
|
202
|
+
.classed('ax-donut-chart-highlighted', true)
|
|
203
|
+
.transition()
|
|
204
|
+
.duration(transitionDuration)
|
|
205
|
+
.style('filter', 'url(#ax-donut-chart-segment-shadow)')
|
|
206
|
+
.style('opacity', 1);
|
|
207
|
+
this.segmentHover.emit(getSegmentDataById(id));
|
|
180
208
|
}
|
|
181
209
|
/**
|
|
182
210
|
* Toggles visibility of a segment by ID
|
|
@@ -192,8 +220,11 @@ class AXDonutChartComponent {
|
|
|
192
220
|
if (wasAllHidden) {
|
|
193
221
|
// Force complete DOM cleanup and redraw
|
|
194
222
|
if (this.chartContainerEl()?.nativeElement) {
|
|
195
|
-
// Clear
|
|
196
|
-
this.d3
|
|
223
|
+
// Clear only chart SVG and message containers (preserve tooltip component)
|
|
224
|
+
this.d3
|
|
225
|
+
?.select(this.chartContainerEl().nativeElement)
|
|
226
|
+
.selectAll('svg, .ax-chart-message-container, .ax-donut-chart-no-data-message')
|
|
227
|
+
.remove();
|
|
197
228
|
// Force full redraw
|
|
198
229
|
setTimeout(() => {
|
|
199
230
|
this.createChart();
|
|
@@ -268,13 +299,15 @@ class AXDonutChartComponent {
|
|
|
268
299
|
* Updates the chart with new data
|
|
269
300
|
*/
|
|
270
301
|
updateChart() {
|
|
302
|
+
// Reset highlighted segment state when updating
|
|
303
|
+
this._currentlyHighlightedSegment.set(null);
|
|
271
304
|
this.createChart(); // Recreate the chart with updated data
|
|
272
305
|
}
|
|
273
306
|
/**
|
|
274
307
|
* Clears the chart container
|
|
275
308
|
*/
|
|
276
309
|
clearChart(container) {
|
|
277
|
-
this.d3.select(container).selectAll('svg').remove();
|
|
310
|
+
this.d3.select(container).selectAll('svg, .ax-chart-message-container, .ax-donut-chart-no-data-message').remove();
|
|
278
311
|
this._tooltipVisible.set(false);
|
|
279
312
|
}
|
|
280
313
|
/**
|
|
@@ -284,27 +317,7 @@ class AXDonutChartComponent {
|
|
|
284
317
|
if (!this.chartContainerEl()?.nativeElement)
|
|
285
318
|
return;
|
|
286
319
|
const container = this.chartContainerEl().nativeElement;
|
|
287
|
-
this.
|
|
288
|
-
const messageContainer = this.d3
|
|
289
|
-
.select(container)
|
|
290
|
-
.append('div')
|
|
291
|
-
// Apply generic container class and specific background class
|
|
292
|
-
.attr('class', 'ax-donut-chart-no-data-message');
|
|
293
|
-
messageContainer
|
|
294
|
-
.append('div')
|
|
295
|
-
// Apply generic icon class and specific donut chart icon class
|
|
296
|
-
.attr('class', 'ax-chart-message-icon ax-donut-chart-no-data-icon')
|
|
297
|
-
.html('<i class="fa-light fa-chart-pie fa-2x"></i>'); // Donut chart icon
|
|
298
|
-
messageContainer
|
|
299
|
-
.append('div')
|
|
300
|
-
// Apply generic text class and specific donut chart text class
|
|
301
|
-
.attr('class', 'ax-chart-message-text ax-donut-chart-no-data-text')
|
|
302
|
-
.text('No data available');
|
|
303
|
-
messageContainer
|
|
304
|
-
.append('div')
|
|
305
|
-
// Apply generic help class and specific donut chart help class
|
|
306
|
-
.attr('class', 'ax-chart-message-help ax-donut-chart-no-data-help')
|
|
307
|
-
.text('Please provide data to display the chart');
|
|
320
|
+
this.renderMessage(container, this.effectiveMessages().noDataIcon, this.effectiveMessages().noData, this.effectiveMessages().noDataHelp, 'ax-donut-chart-no-data-message');
|
|
308
321
|
}
|
|
309
322
|
/**
|
|
310
323
|
* Shows a message when all segments are hidden
|
|
@@ -313,27 +326,7 @@ class AXDonutChartComponent {
|
|
|
313
326
|
if (!this.chartContainerEl()?.nativeElement)
|
|
314
327
|
return;
|
|
315
328
|
const container = this.chartContainerEl().nativeElement;
|
|
316
|
-
this.
|
|
317
|
-
const messageContainer = this.d3
|
|
318
|
-
.select(container)
|
|
319
|
-
.append('div')
|
|
320
|
-
// Apply generic container class and specific background class
|
|
321
|
-
.attr('class', 'ax-donut-chart-no-data-message');
|
|
322
|
-
messageContainer
|
|
323
|
-
.append('div')
|
|
324
|
-
// Apply generic icon class and specific donut chart icon class if any distinction is needed later
|
|
325
|
-
.attr('class', 'ax-chart-message-icon ax-donut-chart-no-data-icon')
|
|
326
|
-
.html('<i class="fa-light fa-eye-slash fa-2x"></i>');
|
|
327
|
-
messageContainer
|
|
328
|
-
.append('div')
|
|
329
|
-
// Apply generic text class and specific donut chart text class if needed
|
|
330
|
-
.attr('class', 'ax-chart-message-text ax-donut-chart-no-data-text')
|
|
331
|
-
.text('All segments are hidden');
|
|
332
|
-
messageContainer
|
|
333
|
-
.append('div')
|
|
334
|
-
// Apply generic help class and specific donut chart help class if needed
|
|
335
|
-
.attr('class', 'ax-chart-message-help ax-donut-chart-no-data-help')
|
|
336
|
-
.text('Please toggle visibility of at least one segment');
|
|
329
|
+
this.renderMessage(container, this.effectiveMessages().allHiddenIcon, this.effectiveMessages().allHidden, this.effectiveMessages().allHiddenHelp, 'ax-donut-chart-no-data-message');
|
|
337
330
|
}
|
|
338
331
|
/**
|
|
339
332
|
* Setups chart dimensions based on container and options
|
|
@@ -410,12 +403,6 @@ class AXDonutChartComponent {
|
|
|
410
403
|
.innerRadius(innerRadius)
|
|
411
404
|
.outerRadius(radius * 0.95)
|
|
412
405
|
.cornerRadius(this.effectiveOptions().cornerRadius);
|
|
413
|
-
// Create hover arc for animation
|
|
414
|
-
const hoverArc = this.d3
|
|
415
|
-
.arc()
|
|
416
|
-
.innerRadius(innerRadius)
|
|
417
|
-
.outerRadius(radius + 10)
|
|
418
|
-
.cornerRadius(this.effectiveOptions().cornerRadius);
|
|
419
406
|
// Create label arc generator for placing labels (middle of the donut ring)
|
|
420
407
|
const labelArc = this.d3
|
|
421
408
|
.arc()
|
|
@@ -423,7 +410,7 @@ class AXDonutChartComponent {
|
|
|
423
410
|
.outerRadius(innerRadius + (radius * 0.95 - innerRadius) / 2);
|
|
424
411
|
// Get animation options
|
|
425
412
|
const animationDuration = this.effectiveOptions().animationDuration;
|
|
426
|
-
const animationEasing =
|
|
413
|
+
const animationEasing = getEasingFunction(this.d3, this.effectiveOptions().animationEasing);
|
|
427
414
|
// Generate pie data
|
|
428
415
|
this.pieData = pie(data);
|
|
429
416
|
// Add segments with animation
|
|
@@ -451,8 +438,8 @@ class AXDonutChartComponent {
|
|
|
451
438
|
return;
|
|
452
439
|
this.handleSliceHover(event, d.data);
|
|
453
440
|
})
|
|
454
|
-
.on('mouseleave', (
|
|
455
|
-
this.handleSegmentLeave(
|
|
441
|
+
.on('mouseleave', () => {
|
|
442
|
+
this.handleSegmentLeave();
|
|
456
443
|
})
|
|
457
444
|
.on('mousemove', (event) => {
|
|
458
445
|
if (this._tooltipVisible()) {
|
|
@@ -463,6 +450,8 @@ class AXDonutChartComponent {
|
|
|
463
450
|
this.onSegmentClick(d.data);
|
|
464
451
|
});
|
|
465
452
|
// Animate segments
|
|
453
|
+
const numSegments = this.pieData.length;
|
|
454
|
+
let completed = 0;
|
|
466
455
|
segments
|
|
467
456
|
.transition()
|
|
468
457
|
.duration(animationDuration)
|
|
@@ -472,112 +461,107 @@ class AXDonutChartComponent {
|
|
|
472
461
|
.attrTween('d', (d) => {
|
|
473
462
|
const interpolate = this.d3.interpolate({ startAngle: d.startAngle, endAngle: d.startAngle }, d);
|
|
474
463
|
return (t) => arc(interpolate(t));
|
|
464
|
+
})
|
|
465
|
+
.on('end', () => {
|
|
466
|
+
completed++;
|
|
467
|
+
if (completed >= numSegments) {
|
|
468
|
+
this._isInitialAnimating.set(false);
|
|
469
|
+
this.cdr.detectChanges();
|
|
470
|
+
}
|
|
471
|
+
})
|
|
472
|
+
.on('interrupt', () => {
|
|
473
|
+
// Treat interrupts as completion to avoid getting stuck in animating state
|
|
474
|
+
completed++;
|
|
475
|
+
if (completed >= numSegments) {
|
|
476
|
+
this._isInitialAnimating.set(false);
|
|
477
|
+
this.cdr.detectChanges();
|
|
478
|
+
}
|
|
475
479
|
});
|
|
476
|
-
// Add data labels if enabled
|
|
480
|
+
// Add data labels if enabled and chart size is sufficient
|
|
477
481
|
if (this.effectiveOptions().showDataLabels) {
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
482
|
+
const minChartDimension = Math.min(chartWidth, chartHeight);
|
|
483
|
+
const isTooSmallChartForLabels = minChartDimension < 260; // Hide labels entirely on small charts
|
|
484
|
+
if (!isTooSmallChartForLabels) {
|
|
485
|
+
// Calculate optimal font size based on segment size and chart dimensions
|
|
486
|
+
const calculateFontSize = (d) => {
|
|
487
|
+
const angleSize = d.endAngle - d.startAngle; // radians
|
|
488
|
+
const segmentPercentage = (d.data.value / total) * 100;
|
|
489
|
+
// Drop labels for very small segments
|
|
490
|
+
if (segmentPercentage < 2)
|
|
491
|
+
return 0;
|
|
492
|
+
// Minimum readable font size; avoid tiny unreadable text
|
|
493
|
+
const minReadable = segmentPercentage < 8 ? 0 : 9;
|
|
494
|
+
// Base size scaled by arc angle and radius, capped
|
|
495
|
+
const scaled = (angleSize * radius) / 9;
|
|
496
|
+
return Math.min(Math.max(minReadable, scaled), 13);
|
|
497
|
+
};
|
|
498
|
+
const formatPercentage = (value) => {
|
|
499
|
+
if (value < 1)
|
|
500
|
+
return '<1%';
|
|
501
|
+
if (value < 10)
|
|
502
|
+
return `${value.toFixed(1)}%`;
|
|
503
|
+
return `${Math.round(value)}%`;
|
|
504
|
+
};
|
|
505
|
+
const labelRadius = innerRadius + (radius * 0.95 - innerRadius) / 2;
|
|
506
|
+
const approximateTextWidth = (text, fontSize) => {
|
|
507
|
+
// Approximate width: 0.55em per char
|
|
508
|
+
return text.length * fontSize * 0.55;
|
|
509
|
+
};
|
|
510
|
+
const arcLength = (d) => (d.endAngle - d.startAngle) * labelRadius;
|
|
511
|
+
const buildLabelText = (d) => {
|
|
512
|
+
const percentage = (d.data.value / total) * 100;
|
|
513
|
+
if (percentage < 1)
|
|
514
|
+
return '';
|
|
515
|
+
const label = d.data.label || '';
|
|
516
|
+
const percentageText = formatPercentage(percentage);
|
|
517
|
+
return label ? `${label} (${percentageText})` : percentageText;
|
|
518
|
+
};
|
|
519
|
+
const dataThatFits = this.pieData.filter((d) => {
|
|
520
|
+
const fontSize = calculateFontSize(d);
|
|
521
|
+
if (!fontSize)
|
|
522
|
+
return false;
|
|
523
|
+
const text = buildLabelText(d);
|
|
524
|
+
if (!text)
|
|
525
|
+
return false;
|
|
526
|
+
const available = arcLength(d);
|
|
527
|
+
const needed = approximateTextWidth(text, fontSize) + 6; // small padding
|
|
528
|
+
return needed <= available;
|
|
529
|
+
});
|
|
530
|
+
const labels = this.svg
|
|
531
|
+
.selectAll('.ax-donut-chart-data-label')
|
|
532
|
+
.data(dataThatFits)
|
|
533
|
+
.enter()
|
|
534
|
+
.append('text')
|
|
535
|
+
.attr('class', 'ax-donut-chart-data-label')
|
|
536
|
+
.style('opacity', 0)
|
|
537
|
+
.style('pointer-events', 'none')
|
|
538
|
+
.style('fill', 'rgb(var(--ax-comp-donut-chart-data-labels-color))')
|
|
539
|
+
.attr('transform', (d) => {
|
|
540
|
+
const centroid = labelArc.centroid(d);
|
|
541
|
+
return `translate(${centroid[0]}, ${centroid[1]})`;
|
|
542
|
+
})
|
|
543
|
+
.attr('text-anchor', 'middle')
|
|
544
|
+
.attr('dominant-baseline', 'middle')
|
|
545
|
+
.style('font-size', (d) => `${calculateFontSize(d)}px`)
|
|
546
|
+
.style('font-weight', (d) => ((d.data.value / total) * 100 >= 15 ? '600' : '500'))
|
|
547
|
+
.text((d) => buildLabelText(d));
|
|
548
|
+
labels
|
|
549
|
+
.transition()
|
|
550
|
+
.duration(animationDuration)
|
|
551
|
+
.delay((d, i) => i * 50 + 200)
|
|
552
|
+
.style('opacity', 1);
|
|
546
553
|
}
|
|
547
554
|
}
|
|
548
|
-
|
|
549
|
-
|
|
555
|
+
// Determine when all segment animations are complete (handle empty case)
|
|
556
|
+
if (numSegments === 0) {
|
|
550
557
|
this._isInitialAnimating.set(false);
|
|
551
558
|
this.cdr.detectChanges();
|
|
552
|
-
}
|
|
559
|
+
}
|
|
553
560
|
}
|
|
554
561
|
/**
|
|
555
562
|
* Gets the appropriate D3 easing function based on the option string
|
|
556
563
|
*/
|
|
557
|
-
getEasingFunction
|
|
558
|
-
switch (easing) {
|
|
559
|
-
case 'linear':
|
|
560
|
-
return this.d3.easeLinear;
|
|
561
|
-
case 'ease':
|
|
562
|
-
return this.d3.easePolyInOut;
|
|
563
|
-
case 'ease-in':
|
|
564
|
-
return this.d3.easePolyIn;
|
|
565
|
-
case 'ease-out':
|
|
566
|
-
return this.d3.easePolyOut;
|
|
567
|
-
case 'ease-in-out':
|
|
568
|
-
return this.d3.easePolyInOut;
|
|
569
|
-
case 'cubic':
|
|
570
|
-
return this.d3.easeCubic;
|
|
571
|
-
case 'cubic-in':
|
|
572
|
-
return this.d3.easeCubicIn;
|
|
573
|
-
case 'cubic-out':
|
|
574
|
-
return this.d3.easeCubicOut;
|
|
575
|
-
case 'cubic-in-out':
|
|
576
|
-
return this.d3.easeCubicInOut;
|
|
577
|
-
default:
|
|
578
|
-
return this.d3.easeCubicOut; // Default easing
|
|
579
|
-
}
|
|
580
|
-
}
|
|
564
|
+
// getEasingFunction moved to shared chart utils
|
|
581
565
|
/**
|
|
582
566
|
* Handle hover effects on a segment
|
|
583
567
|
*/
|
|
@@ -606,52 +590,28 @@ class AXDonutChartComponent {
|
|
|
606
590
|
/**
|
|
607
591
|
* Handles mouse leave from segments
|
|
608
592
|
*/
|
|
609
|
-
handleSegmentLeave(
|
|
593
|
+
handleSegmentLeave() {
|
|
610
594
|
if (this._isInitialAnimating())
|
|
611
595
|
return;
|
|
612
596
|
// Hide tooltip
|
|
613
597
|
this._tooltipVisible.set(false);
|
|
614
598
|
this.cdr.detectChanges();
|
|
615
|
-
//
|
|
616
|
-
|
|
617
|
-
// Remove hover effect (visual style related to classes and scale)
|
|
618
|
-
// Calling highlightSegment with the ID of the segment being left.
|
|
619
|
-
// If it was highlighted, this will toggle it off and emit null for segmentHover.
|
|
620
|
-
this.highlightSegment(d.data.id);
|
|
621
|
-
// Animate the arc back to normal for the specific segment being left.
|
|
622
|
-
this.d3
|
|
623
|
-
.select(event.currentTarget)
|
|
624
|
-
.transition()
|
|
625
|
-
.duration(200)
|
|
626
|
-
.attr('d', (arcData) => normalArc(arcData)); // Ensure arcData is used if 'd' is shadowed
|
|
599
|
+
// Clear all highlights to ensure segments return to normal state
|
|
600
|
+
this.highlightSegment(null);
|
|
627
601
|
}
|
|
628
602
|
/**
|
|
629
603
|
* Updates tooltip position
|
|
630
604
|
* Ensures the tooltip is visible by adjusting position when near edges
|
|
631
605
|
*/
|
|
632
606
|
updateTooltipPosition(event) {
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
if (!tooltipEl) {
|
|
636
|
-
const x = event.clientX - container.left;
|
|
637
|
-
const y = event.clientY - container.top;
|
|
638
|
-
this._tooltipPosition.set({ x, y });
|
|
607
|
+
const containerEl = this.chartContainerEl()?.nativeElement;
|
|
608
|
+
if (!containerEl)
|
|
639
609
|
return;
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
const
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
// Position tooltip to the right by default
|
|
646
|
-
let x = cursorX + gap / 3;
|
|
647
|
-
// If tooltip would go off the right edge, position it to the left
|
|
648
|
-
if (x + tooltipRect.width > container.width) {
|
|
649
|
-
x = cursorX - tooltipRect.width - gap;
|
|
650
|
-
}
|
|
651
|
-
// Keep tooltip within container bounds
|
|
652
|
-
x = Math.max(gap, Math.min(x, container.width - tooltipRect.width - gap));
|
|
653
|
-
const y = Math.max(gap, Math.min(cursorY, container.height - tooltipRect.height - gap));
|
|
654
|
-
this._tooltipPosition.set({ x, y });
|
|
610
|
+
const containerRect = containerEl.getBoundingClientRect();
|
|
611
|
+
const tooltipEl = containerEl.querySelector('.chart-tooltip');
|
|
612
|
+
const tooltipRect = tooltipEl ? tooltipEl.getBoundingClientRect() : null;
|
|
613
|
+
const pos = computeTooltipPosition(containerRect, tooltipRect, event.clientX, event.clientY, this.TOOLTIP_GAP);
|
|
614
|
+
this._tooltipPosition.set(pos);
|
|
655
615
|
}
|
|
656
616
|
/**
|
|
657
617
|
* Adds center display with total value
|
|
@@ -706,7 +666,24 @@ class AXDonutChartComponent {
|
|
|
706
666
|
}
|
|
707
667
|
this.hiddenSegments.clear();
|
|
708
668
|
this._tooltipVisible.set(false);
|
|
669
|
+
this._currentlyHighlightedSegment.set(null);
|
|
670
|
+
}
|
|
671
|
+
// ===== Helper utilities for modularity and maintainability =====
|
|
672
|
+
renderMessage(container, iconClass, messageText, helpText, specificClass) {
|
|
673
|
+
// Clear container area first (preserve tooltip component)
|
|
674
|
+
this.d3.select(container).selectAll('svg, .ax-chart-message-container, .ax-donut-chart-no-data-message').remove();
|
|
675
|
+
const messageContainer = this.d3
|
|
676
|
+
.select(container)
|
|
677
|
+
.append('div')
|
|
678
|
+
.attr('class', `ax-chart-message-container ax-donut-chart-message-background ${specificClass}`);
|
|
679
|
+
messageContainer
|
|
680
|
+
.append('div')
|
|
681
|
+
.attr('class', 'ax-chart-message-icon ax-donut-chart-no-data-icon')
|
|
682
|
+
.html(`<i class="${iconClass} fa-2x"></i>`);
|
|
683
|
+
messageContainer.append('div').attr('class', 'ax-chart-message-text ax-donut-chart-no-data-text').text(messageText);
|
|
684
|
+
messageContainer.append('div').attr('class', 'ax-chart-message-help ax-donut-chart-no-data-help').text(helpText);
|
|
709
685
|
}
|
|
686
|
+
// computeTooltipPosition moved to shared chart utils
|
|
710
687
|
/**
|
|
711
688
|
* Gets an accessibility label describing the donut chart for screen readers
|
|
712
689
|
*/
|
|
@@ -742,12 +719,12 @@ class AXDonutChartComponent {
|
|
|
742
719
|
};
|
|
743
720
|
});
|
|
744
721
|
}
|
|
745
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
746
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.
|
|
722
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXDonutChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
723
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.3", type: AXDonutChartComponent, isStandalone: true, selector: "ax-donut-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", segmentHover: "segmentHover" }, viewQueries: [{ propertyName: "chartContainerEl", first: true, predicate: ["chartContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"ax-donut-chart\" #chartContainer role=\"img\" [attr.aria-label]=\"getAccessibilityLabel()\">\n <!-- Tooltip component -->\n <ax-chart-tooltip\n [visible]=\"tooltipVisible()\"\n [position]=\"tooltipPosition()\"\n [data]=\"tooltipData()\"\n [showPercentage]=\"true\"\n ></ax-chart-tooltip>\n</div>\n", styles: ["ax-donut-chart{display:block;width:100%;height:100%;--ax-comp-donut-chart-bg-color: 0, 0, 0, 0;--ax-comp-donut-chart-value-color: var(--ax-sys-color-on-lightest-surface)}ax-donut-chart .ax-donut-chart{width:100%;height:100%;position:relative;display:flex;align-items:center;justify-content:center;border-radius:.5rem;overflow:hidden;background-color:rgba(var(--ax-comp-donut-chart-bg-color))}ax-donut-chart .ax-donut-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}ax-donut-chart .ax-donut-chart svg g:has(text){font-family:inherit}ax-donut-chart .ax-donut-chart-no-data-message{text-align:center;background-color:rgb(var(--ax-comp-donut-chart-bg-color));border:1px solid rgba(var(--ax-sys-color-surface));padding:1.5rem;border-radius:.5rem;width:80%;max-width:300px}ax-donut-chart .ax-donut-chart-no-data-message .ax-donut-chart-no-data-icon{opacity:.6;margin-bottom:.75rem}ax-donut-chart .ax-donut-chart-no-data-message .ax-donut-chart-no-data-text{font-size:1rem;font-weight:600;margin-bottom:.5rem}ax-donut-chart .ax-donut-chart-no-data-message .ax-donut-chart-no-data-help{font-size:.8rem;opacity:.6}\n"], dependencies: [{ kind: "component", type: AXChartTooltipComponent, selector: "ax-chart-tooltip", inputs: ["data", "position", "visible", "showPercentage", "style"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
747
724
|
}
|
|
748
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
725
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXDonutChartComponent, decorators: [{
|
|
749
726
|
type: Component,
|
|
750
|
-
args: [{ selector: 'ax-donut-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-donut-chart\" #chartContainer role=\"img\" [attr.aria-label]=\"getAccessibilityLabel()\"
|
|
727
|
+
args: [{ selector: 'ax-donut-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-donut-chart\" #chartContainer role=\"img\" [attr.aria-label]=\"getAccessibilityLabel()\">\n <!-- Tooltip component -->\n <ax-chart-tooltip\n [visible]=\"tooltipVisible()\"\n [position]=\"tooltipPosition()\"\n [data]=\"tooltipData()\"\n [showPercentage]=\"true\"\n ></ax-chart-tooltip>\n</div>\n", styles: ["ax-donut-chart{display:block;width:100%;height:100%;--ax-comp-donut-chart-bg-color: 0, 0, 0, 0;--ax-comp-donut-chart-value-color: var(--ax-sys-color-on-lightest-surface)}ax-donut-chart .ax-donut-chart{width:100%;height:100%;position:relative;display:flex;align-items:center;justify-content:center;border-radius:.5rem;overflow:hidden;background-color:rgba(var(--ax-comp-donut-chart-bg-color))}ax-donut-chart .ax-donut-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}ax-donut-chart .ax-donut-chart svg g:has(text){font-family:inherit}ax-donut-chart .ax-donut-chart-no-data-message{text-align:center;background-color:rgb(var(--ax-comp-donut-chart-bg-color));border:1px solid rgba(var(--ax-sys-color-surface));padding:1.5rem;border-radius:.5rem;width:80%;max-width:300px}ax-donut-chart .ax-donut-chart-no-data-message .ax-donut-chart-no-data-icon{opacity:.6;margin-bottom:.75rem}ax-donut-chart .ax-donut-chart-no-data-message .ax-donut-chart-no-data-text{font-size:1rem;font-weight:600;margin-bottom:.5rem}ax-donut-chart .ax-donut-chart-no-data-message .ax-donut-chart-no-data-help{font-size:.8rem;opacity:.6}\n"] }]
|
|
751
728
|
}], ctorParameters: () => [] });
|
|
752
729
|
|
|
753
730
|
/**
|