@acorex/charts 19.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/bar-chart/README.md +3 -0
- package/bar-chart/index.d.ts +3 -0
- package/bar-chart/lib/bar-chart.component.d.ts +123 -0
- package/bar-chart/lib/bar-chart.config.d.ts +6 -0
- package/bar-chart/lib/bar-chart.type.d.ts +44 -0
- package/chart-tooltip/README.md +3 -0
- package/chart-tooltip/index.d.ts +2 -0
- package/chart-tooltip/lib/chart-tooltip.component.d.ts +43 -0
- package/chart-tooltip/lib/chart-tooltip.type.d.ts +7 -0
- package/donut-chart/README.md +3 -0
- package/donut-chart/index.d.ts +3 -0
- package/donut-chart/lib/donut-chart.component.d.ts +143 -0
- package/donut-chart/lib/donut-chart.config.d.ts +6 -0
- package/donut-chart/lib/donut-chart.type.d.ts +25 -0
- package/fesm2022/acorex-charts-bar-chart.mjs +563 -0
- package/fesm2022/acorex-charts-bar-chart.mjs.map +1 -0
- package/fesm2022/acorex-charts-chart-tooltip.mjs +75 -0
- package/fesm2022/acorex-charts-chart-tooltip.mjs.map +1 -0
- package/fesm2022/acorex-charts-donut-chart.mjs +616 -0
- package/fesm2022/acorex-charts-donut-chart.mjs.map +1 -0
- package/fesm2022/acorex-charts-gauge-chart.mjs +548 -0
- package/fesm2022/acorex-charts-gauge-chart.mjs.map +1 -0
- package/fesm2022/acorex-charts-hierarchy-chart.mjs +652 -0
- package/fesm2022/acorex-charts-hierarchy-chart.mjs.map +1 -0
- package/fesm2022/acorex-charts-line-chart.mjs +738 -0
- package/fesm2022/acorex-charts-line-chart.mjs.map +1 -0
- package/fesm2022/acorex-charts.mjs +8 -0
- package/fesm2022/acorex-charts.mjs.map +1 -0
- package/gauge-chart/README.md +3 -0
- package/gauge-chart/index.d.ts +3 -0
- package/gauge-chart/lib/gauge-chart.component.d.ts +110 -0
- package/gauge-chart/lib/gauge-chart.config.d.ts +6 -0
- package/gauge-chart/lib/gauge-chart.type.d.ts +37 -0
- package/hierarchy-chart/README.md +61 -0
- package/hierarchy-chart/index.d.ts +3 -0
- package/hierarchy-chart/lib/hierarchy-chart.component.d.ts +99 -0
- package/hierarchy-chart/lib/hierarchy-chart.config.d.ts +6 -0
- package/hierarchy-chart/lib/hierarchy-chart.type.d.ts +227 -0
- package/index.d.ts +1 -0
- package/line-chart/README.md +3 -0
- package/line-chart/index.d.ts +3 -0
- package/line-chart/lib/line-chart.component.d.ts +96 -0
- package/line-chart/lib/line-chart.config.d.ts +6 -0
- package/line-chart/lib/line-chart.type.d.ts +61 -0
- package/package.json +48 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { AXChartTooltipComponent } from '@acorex/charts/chart-tooltip';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import * as i0 from '@angular/core';
|
|
4
|
+
import { InjectionToken, inject, ChangeDetectorRef, input, output, viewChild, signal, computed, afterNextRender, effect, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
5
|
+
import { AX_GLOBAL_CONFIG } from '@acorex/core/config';
|
|
6
|
+
import { set } from 'lodash-es';
|
|
7
|
+
|
|
8
|
+
const AXDonutChartDefaultConfig = {
|
|
9
|
+
showTooltip: true,
|
|
10
|
+
donutWidth: 35,
|
|
11
|
+
cornerRadius: 4,
|
|
12
|
+
animationDuration: 800,
|
|
13
|
+
animationEasing: 'cubic-out',
|
|
14
|
+
};
|
|
15
|
+
const AX_DONUT_CHART_CONFIG = new InjectionToken('AX_DONUT_CHART_CONFIG', {
|
|
16
|
+
providedIn: 'root',
|
|
17
|
+
factory: () => {
|
|
18
|
+
const global = inject(AX_GLOBAL_CONFIG);
|
|
19
|
+
set(global, 'chart.donutChart', AXDonutChartDefaultConfig);
|
|
20
|
+
return AXDonutChartDefaultConfig;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
function donutChartConfig(config = {}) {
|
|
24
|
+
const result = {
|
|
25
|
+
...AXDonutChartDefaultConfig,
|
|
26
|
+
...config,
|
|
27
|
+
};
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const AXDonutChartColors = {
|
|
32
|
+
// Modern color palette suitable for data visualization
|
|
33
|
+
defaultColors: [
|
|
34
|
+
'#4361ee', // Blue
|
|
35
|
+
'#3a0ca3', // Purple
|
|
36
|
+
'#7209b7', // Violet
|
|
37
|
+
'#f72585', // Pink
|
|
38
|
+
'#4cc9f0', // Light Blue
|
|
39
|
+
'#4895ef', // Sky Blue
|
|
40
|
+
'#560bad', // Deep Purple
|
|
41
|
+
'#f15bb5', // Light Pink
|
|
42
|
+
'#00bbf9', // Cyan
|
|
43
|
+
'#00f5d4', // Teal
|
|
44
|
+
],
|
|
45
|
+
// Get a color from the palette by index with wraparound
|
|
46
|
+
getColor: (index, customPalette) => {
|
|
47
|
+
const palette = customPalette || AXDonutChartColors.defaultColors;
|
|
48
|
+
return palette[index % palette.length];
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Donut Chart Component
|
|
53
|
+
* Displays data in a circular donut chart with interactive segments
|
|
54
|
+
*/
|
|
55
|
+
class AXDonutChartComponent {
|
|
56
|
+
// Dependency Injection
|
|
57
|
+
cdr = inject(ChangeDetectorRef);
|
|
58
|
+
// Inputs
|
|
59
|
+
/** Chart data input */
|
|
60
|
+
data = input([]);
|
|
61
|
+
/** Chart options input */
|
|
62
|
+
options = input({});
|
|
63
|
+
// Outputs
|
|
64
|
+
/** Emitted when a segment is clicked */
|
|
65
|
+
segmentClick = output();
|
|
66
|
+
/** Emitted when a segment has mouse hover
|
|
67
|
+
* Can be used by external elements to highlight this segment
|
|
68
|
+
*/
|
|
69
|
+
segmentHover = output();
|
|
70
|
+
// Chart container reference
|
|
71
|
+
chartContainerEl = viewChild.required('chartContainer');
|
|
72
|
+
// D3 reference - loaded asynchronously
|
|
73
|
+
d3;
|
|
74
|
+
// Chart SVG reference
|
|
75
|
+
svg;
|
|
76
|
+
pieData = [];
|
|
77
|
+
// State management
|
|
78
|
+
hiddenSegments = new Set();
|
|
79
|
+
_initialized = signal(false);
|
|
80
|
+
_rendered = signal(false);
|
|
81
|
+
// Tooltip state
|
|
82
|
+
_tooltipVisible = signal(false);
|
|
83
|
+
_tooltipPosition = signal({ x: 0, y: 0 });
|
|
84
|
+
_tooltipData = signal({
|
|
85
|
+
title: '',
|
|
86
|
+
value: 0,
|
|
87
|
+
percentage: '0%',
|
|
88
|
+
color: '',
|
|
89
|
+
});
|
|
90
|
+
// Public computed properties for the template
|
|
91
|
+
tooltipVisible = this._tooltipVisible.asReadonly();
|
|
92
|
+
tooltipPosition = this._tooltipPosition.asReadonly();
|
|
93
|
+
tooltipData = this._tooltipData.asReadonly();
|
|
94
|
+
// Inject configuration
|
|
95
|
+
configToken = inject(AX_DONUT_CHART_CONFIG);
|
|
96
|
+
// Computed configuration options
|
|
97
|
+
effectiveOptions = computed(() => {
|
|
98
|
+
return {
|
|
99
|
+
...this.configToken,
|
|
100
|
+
...this.options(),
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
// Data accessor for handling different incoming data formats
|
|
104
|
+
chartDataArray = computed(() => {
|
|
105
|
+
return this.data() || [];
|
|
106
|
+
});
|
|
107
|
+
// Color accessor method
|
|
108
|
+
getColor(index) {
|
|
109
|
+
return AXDonutChartColors.getColor(index);
|
|
110
|
+
}
|
|
111
|
+
// Segment visibility check
|
|
112
|
+
isSegmentHidden(id) {
|
|
113
|
+
return this.hiddenSegments.has(id);
|
|
114
|
+
}
|
|
115
|
+
constructor() {
|
|
116
|
+
// Dynamically load D3 and initialize the chart when the component is ready
|
|
117
|
+
afterNextRender(() => {
|
|
118
|
+
this._initialized.set(true);
|
|
119
|
+
this.loadD3();
|
|
120
|
+
});
|
|
121
|
+
// Watch for changes to redraw the chart
|
|
122
|
+
effect(() => {
|
|
123
|
+
// Access inputs to track them
|
|
124
|
+
this.data();
|
|
125
|
+
this.effectiveOptions();
|
|
126
|
+
// Only update if already rendered
|
|
127
|
+
if (this._rendered()) {
|
|
128
|
+
this.updateChart();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Highlights a specific segment by ID
|
|
134
|
+
* @param id The segment ID to highlight, or null to clear highlight
|
|
135
|
+
*/
|
|
136
|
+
highlightSegment(id) {
|
|
137
|
+
if (!this.svg)
|
|
138
|
+
return;
|
|
139
|
+
// Reset all segments first
|
|
140
|
+
this.svg
|
|
141
|
+
.selectAll('path')
|
|
142
|
+
.classed('ax-donut-chart-highlighted', false)
|
|
143
|
+
.classed('ax-donut-chart-dimmed', false)
|
|
144
|
+
.attr('transform', 'scale(1)');
|
|
145
|
+
if (id !== null) {
|
|
146
|
+
// Highlight the target segment
|
|
147
|
+
this.svg
|
|
148
|
+
.selectAll('path')
|
|
149
|
+
.filter((d) => d?.data?.id === id)
|
|
150
|
+
.classed('ax-donut-chart-highlighted', true)
|
|
151
|
+
.attr('transform', 'scale(1.02)');
|
|
152
|
+
// Dim other segments
|
|
153
|
+
this.svg
|
|
154
|
+
.selectAll('path')
|
|
155
|
+
.filter((d) => d?.data?.id !== id)
|
|
156
|
+
.classed('ax-donut-chart-dimmed', true);
|
|
157
|
+
}
|
|
158
|
+
this.cdr.detectChanges();
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Toggles visibility of a segment by ID
|
|
162
|
+
* @param id Segment ID to toggle
|
|
163
|
+
* @returns New visibility state (true = visible, false = hidden)
|
|
164
|
+
*/
|
|
165
|
+
toggleSegment(id) {
|
|
166
|
+
this.toggleSegmentVisibility(id);
|
|
167
|
+
return !this.isSegmentHidden(id);
|
|
168
|
+
}
|
|
169
|
+
ngOnDestroy() {
|
|
170
|
+
this.cleanupChart();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Loads D3.js dynamically
|
|
174
|
+
*/
|
|
175
|
+
async loadD3() {
|
|
176
|
+
try {
|
|
177
|
+
this.d3 = await import('d3');
|
|
178
|
+
// If container is ready, create chart
|
|
179
|
+
if (this._initialized() && this.chartContainerEl()) {
|
|
180
|
+
this.createChart();
|
|
181
|
+
this._rendered.set(true);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
console.error('Failed to load D3.js:', error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
onSegmentClick(item) {
|
|
189
|
+
this.toggleSegmentVisibility(item.id);
|
|
190
|
+
this.segmentClick.emit(item);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Creates the donut chart
|
|
194
|
+
*/
|
|
195
|
+
createChart() {
|
|
196
|
+
if (!this.d3 || !this.chartContainerEl()?.nativeElement)
|
|
197
|
+
return;
|
|
198
|
+
try {
|
|
199
|
+
const containerElement = this.chartContainerEl().nativeElement;
|
|
200
|
+
this.clearChart(containerElement);
|
|
201
|
+
const data = this.chartDataArray();
|
|
202
|
+
if (!data || data.length === 0) {
|
|
203
|
+
this.showNoDataMessage(containerElement);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Filter out hidden segments
|
|
207
|
+
const visibleData = data.filter((item) => !this.hiddenSegments.has(item.id));
|
|
208
|
+
// If all segments are hidden, show message
|
|
209
|
+
if (visibleData.length === 0) {
|
|
210
|
+
this.showAllSegmentsHiddenMessage(containerElement);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const options = this.effectiveOptions();
|
|
214
|
+
const { width, height } = this.setupDimensions(containerElement, options);
|
|
215
|
+
this.renderDonutChart(containerElement, width, height, visibleData);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.error('Error creating donut chart:', error);
|
|
219
|
+
this.handleChartError();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Updates the chart with new data
|
|
224
|
+
*/
|
|
225
|
+
updateChart() {
|
|
226
|
+
this.createChart(); // Recreate the chart with updated data
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Clears the chart container
|
|
230
|
+
*/
|
|
231
|
+
clearChart(container) {
|
|
232
|
+
this.d3.select(container).selectAll('svg').remove();
|
|
233
|
+
this._tooltipVisible.set(false);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Shows a message when no data is available
|
|
237
|
+
*/
|
|
238
|
+
showNoDataMessage(container) {
|
|
239
|
+
const messageContainer = this.d3
|
|
240
|
+
.select(container)
|
|
241
|
+
.append('div')
|
|
242
|
+
.attr('class', 'ax-donut-chart-no-data-message')
|
|
243
|
+
.style('width', 'auto')
|
|
244
|
+
.style('text-align', 'center');
|
|
245
|
+
// Add an icon
|
|
246
|
+
messageContainer
|
|
247
|
+
.append('div')
|
|
248
|
+
.attr('class', 'ax-donut-chart-no-data-icon')
|
|
249
|
+
.html('<i class="fa-light fa-chart-pie-simple fa-2x"></i>');
|
|
250
|
+
// Add text message and help text
|
|
251
|
+
messageContainer.append('div').attr('class', 'ax-donut-chart-no-data-text').text('No data available');
|
|
252
|
+
messageContainer
|
|
253
|
+
.append('div')
|
|
254
|
+
.attr('class', 'ax-donut-chart-no-data-help')
|
|
255
|
+
.text('Please provide data in the correct format');
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Shows a message when all segments are hidden
|
|
259
|
+
*/
|
|
260
|
+
showAllSegmentsHiddenMessage(container) {
|
|
261
|
+
this.clearChart(container);
|
|
262
|
+
// Add a simple div to ensure proper positioning
|
|
263
|
+
const wrapper = this.d3
|
|
264
|
+
.select(container)
|
|
265
|
+
.append('div')
|
|
266
|
+
.style('position', 'relative')
|
|
267
|
+
.style('width', '100%')
|
|
268
|
+
.style('height', '100%');
|
|
269
|
+
const messageContainer = wrapper
|
|
270
|
+
.append('div')
|
|
271
|
+
.attr('class', 'ax-donut-chart-no-data-message')
|
|
272
|
+
.style('position', 'absolute')
|
|
273
|
+
.style('left', '50%')
|
|
274
|
+
.style('top', '50%')
|
|
275
|
+
.style('transform', 'translate(-50%, -50%)')
|
|
276
|
+
.style('text-align', 'center')
|
|
277
|
+
.style('z-index', '10')
|
|
278
|
+
.style('background-color', 'rgba(255, 255, 255, 0.95)')
|
|
279
|
+
.style('padding', '1.5rem')
|
|
280
|
+
.style('border-radius', '0.5rem')
|
|
281
|
+
.style('box-shadow', '0 2px 12px rgba(0, 0, 0, 0.08)')
|
|
282
|
+
.style('width', '80%')
|
|
283
|
+
.style('max-width', '300px');
|
|
284
|
+
// Add an icon
|
|
285
|
+
messageContainer
|
|
286
|
+
.append('div')
|
|
287
|
+
.attr('class', 'ax-donut-chart-no-data-icon')
|
|
288
|
+
.style('color', 'var(--ax-text-muted, #999)')
|
|
289
|
+
.style('margin-bottom', '0.75rem')
|
|
290
|
+
.html('<i class="fa-light fa-eye-slash fa-2x"></i>');
|
|
291
|
+
// Add text message and help text
|
|
292
|
+
messageContainer
|
|
293
|
+
.append('div')
|
|
294
|
+
.attr('class', 'ax-donut-chart-no-data-text')
|
|
295
|
+
.style('font-size', '1rem')
|
|
296
|
+
.style('font-weight', '600')
|
|
297
|
+
.style('color', 'var(--ax-text-color, #333)')
|
|
298
|
+
.style('margin-bottom', '0.5rem')
|
|
299
|
+
.text('All segments are hidden');
|
|
300
|
+
messageContainer
|
|
301
|
+
.append('div')
|
|
302
|
+
.attr('class', 'ax-donut-chart-no-data-help')
|
|
303
|
+
.style('font-size', '0.8rem')
|
|
304
|
+
.style('color', 'var(--ax-text-muted, #999)')
|
|
305
|
+
.text('Click on a segment to show data');
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Setups chart dimensions based on container and options
|
|
309
|
+
*/
|
|
310
|
+
setupDimensions(container, options) {
|
|
311
|
+
// Get container dimensions or use defaults
|
|
312
|
+
const containerWidth = container.clientWidth || 400;
|
|
313
|
+
const containerHeight = container.clientHeight || 400;
|
|
314
|
+
// Ensure minimum dimensions for the chart
|
|
315
|
+
const minDim = 200;
|
|
316
|
+
const width = Math.max(options?.width || containerWidth, minDim);
|
|
317
|
+
const height = Math.max(options?.height || containerHeight, minDim);
|
|
318
|
+
return { width, height };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Renders the donut chart with visible data
|
|
322
|
+
*/
|
|
323
|
+
renderDonutChart(container, width, height, visibleData) {
|
|
324
|
+
const total = visibleData.reduce((sum, item) => sum + item.value, 0);
|
|
325
|
+
// Create SVG container with filters
|
|
326
|
+
const svg = this.createSvgWithFilters(container, width, height);
|
|
327
|
+
// Create main chart group
|
|
328
|
+
this.svg = svg.append('g').attr('transform', `translate(${width / 2}, ${height / 2})`);
|
|
329
|
+
// Create donut segments
|
|
330
|
+
this.createDonutSegments(width, height, visibleData, total);
|
|
331
|
+
// Add total in center
|
|
332
|
+
this.addCenterDisplay(total);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Create SVG element with filter definitions for shadows
|
|
336
|
+
*/
|
|
337
|
+
createSvgWithFilters(container, width, height) {
|
|
338
|
+
const svg = this.d3
|
|
339
|
+
.select(container)
|
|
340
|
+
.append('svg')
|
|
341
|
+
.attr('width', '100%')
|
|
342
|
+
.attr('height', '100%')
|
|
343
|
+
.attr('viewBox', `0 0 ${width} ${height}`)
|
|
344
|
+
.attr('preserveAspectRatio', 'xMidYMid meet');
|
|
345
|
+
// Add drop shadow filter
|
|
346
|
+
const defs = svg.append('defs');
|
|
347
|
+
const filter = defs.append('filter').attr('id', 'ax-donut-chart-segment-shadow').attr('height', '130%');
|
|
348
|
+
filter.append('feGaussianBlur').attr('in', 'SourceAlpha').attr('stdDeviation', 2).attr('result', 'blur');
|
|
349
|
+
filter.append('feOffset').attr('in', 'blur').attr('dx', 1).attr('dy', 1).attr('result', 'offsetBlur');
|
|
350
|
+
const feComponentTransfer = filter
|
|
351
|
+
.append('feComponentTransfer')
|
|
352
|
+
.attr('in', 'offsetBlur')
|
|
353
|
+
.attr('result', 'offsetBlur');
|
|
354
|
+
feComponentTransfer.append('feFuncA').attr('type', 'linear').attr('slope', 0.3);
|
|
355
|
+
const feMerge = filter.append('feMerge');
|
|
356
|
+
feMerge.append('feMergeNode').attr('in', 'offsetBlur');
|
|
357
|
+
feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
|
|
358
|
+
return svg;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Create and render the donut segments with animations
|
|
362
|
+
*/
|
|
363
|
+
createDonutSegments(chartWidth, chartHeight, data, total) {
|
|
364
|
+
// Create pie layout
|
|
365
|
+
const pie = this.d3
|
|
366
|
+
.pie()
|
|
367
|
+
.value((d) => d.value)
|
|
368
|
+
.sort(null)
|
|
369
|
+
.padAngle(0.02);
|
|
370
|
+
// Calculate the radius of the donut chart
|
|
371
|
+
const radius = (Math.min(chartWidth, chartHeight) / 2) * 0.85;
|
|
372
|
+
// Calculate inner radius based on donutWidth percentage
|
|
373
|
+
const donutWidthPercent = this.effectiveOptions().donutWidth / 100;
|
|
374
|
+
const innerRadius = radius * (1 - donutWidthPercent);
|
|
375
|
+
// Create arc generator with the configured radius and corner radius
|
|
376
|
+
const arc = this.d3
|
|
377
|
+
.arc()
|
|
378
|
+
.innerRadius(innerRadius)
|
|
379
|
+
.outerRadius(radius * 0.95)
|
|
380
|
+
.cornerRadius(this.effectiveOptions().cornerRadius);
|
|
381
|
+
// Create hover arc for animation
|
|
382
|
+
const hoverArc = this.d3
|
|
383
|
+
.arc()
|
|
384
|
+
.innerRadius(innerRadius)
|
|
385
|
+
.outerRadius(radius + 10)
|
|
386
|
+
.cornerRadius(this.effectiveOptions().cornerRadius);
|
|
387
|
+
// Get animation options
|
|
388
|
+
const animationDuration = this.effectiveOptions().animationDuration;
|
|
389
|
+
const animationEasing = this.getEasingFunction(this.effectiveOptions().animationEasing);
|
|
390
|
+
// Generate pie data
|
|
391
|
+
this.pieData = pie(data);
|
|
392
|
+
// Add segments with animation
|
|
393
|
+
const segments = this.svg
|
|
394
|
+
.selectAll('path')
|
|
395
|
+
.data(this.pieData)
|
|
396
|
+
.enter()
|
|
397
|
+
.append('path')
|
|
398
|
+
.attr('class', 'ax-donut-chart-segment')
|
|
399
|
+
.attr('fill', (d, i) => this.getColor(i))
|
|
400
|
+
.style('opacity', 0)
|
|
401
|
+
.style('cursor', 'pointer') // Add cursor pointer to segments
|
|
402
|
+
.on('mouseenter', (event, d) => {
|
|
403
|
+
if (!this.effectiveOptions().showTooltip)
|
|
404
|
+
return;
|
|
405
|
+
this.handleSegmentHover(event, d, hoverArc, arc, total);
|
|
406
|
+
})
|
|
407
|
+
.on('mouseleave', (event, d) => {
|
|
408
|
+
this.handleSegmentLeave(event, d, arc);
|
|
409
|
+
})
|
|
410
|
+
.on('mousemove', (event) => {
|
|
411
|
+
if (this._tooltipVisible()) {
|
|
412
|
+
this.updateTooltipPosition(event);
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
.on('click', (event, d) => {
|
|
416
|
+
this.onSegmentClick(d.data);
|
|
417
|
+
});
|
|
418
|
+
// Animate segments
|
|
419
|
+
segments
|
|
420
|
+
.transition()
|
|
421
|
+
.duration(animationDuration)
|
|
422
|
+
.ease(animationEasing)
|
|
423
|
+
.delay((d, i) => i * 50)
|
|
424
|
+
.style('opacity', 1)
|
|
425
|
+
.attrTween('d', (d) => {
|
|
426
|
+
const interpolate = this.d3.interpolate({ startAngle: d.startAngle, endAngle: d.startAngle }, d);
|
|
427
|
+
return (t) => arc(interpolate(t));
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Gets the appropriate D3 easing function based on the option string
|
|
432
|
+
*/
|
|
433
|
+
getEasingFunction(easing) {
|
|
434
|
+
switch (easing) {
|
|
435
|
+
case 'linear':
|
|
436
|
+
return this.d3.easeLinear;
|
|
437
|
+
case 'ease':
|
|
438
|
+
return this.d3.easePolyInOut;
|
|
439
|
+
case 'ease-in':
|
|
440
|
+
return this.d3.easePolyIn;
|
|
441
|
+
case 'ease-out':
|
|
442
|
+
return this.d3.easePolyOut;
|
|
443
|
+
case 'ease-in-out':
|
|
444
|
+
return this.d3.easePolyInOut;
|
|
445
|
+
case 'cubic':
|
|
446
|
+
return this.d3.easeCubic;
|
|
447
|
+
case 'cubic-in':
|
|
448
|
+
return this.d3.easeCubicIn;
|
|
449
|
+
case 'cubic-out':
|
|
450
|
+
return this.d3.easeCubicOut;
|
|
451
|
+
case 'cubic-in-out':
|
|
452
|
+
return this.d3.easeCubicInOut;
|
|
453
|
+
default:
|
|
454
|
+
return this.d3.easeCubicOut; // Default easing
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Handle hover effects on a segment
|
|
459
|
+
*/
|
|
460
|
+
handleSegmentHover(event, d, hoverArc, normalArc, total) {
|
|
461
|
+
const segmentData = d.data;
|
|
462
|
+
const percentage = ((segmentData.value / total) * 100).toFixed(1);
|
|
463
|
+
const segmentColor = this.d3.select(event.currentTarget).attr('fill');
|
|
464
|
+
// Update tooltip data
|
|
465
|
+
this._tooltipData.set({
|
|
466
|
+
title: segmentData.name,
|
|
467
|
+
value: segmentData.value,
|
|
468
|
+
percentage: `${percentage}%`,
|
|
469
|
+
color: segmentColor,
|
|
470
|
+
});
|
|
471
|
+
// Show tooltip
|
|
472
|
+
this._tooltipVisible.set(true);
|
|
473
|
+
this.updateTooltipPosition(event);
|
|
474
|
+
this.cdr.detectChanges();
|
|
475
|
+
// Emit segment hover event
|
|
476
|
+
this.segmentHover.emit(segmentData);
|
|
477
|
+
// Apply hover effect
|
|
478
|
+
this.d3
|
|
479
|
+
.select(event.currentTarget)
|
|
480
|
+
.transition()
|
|
481
|
+
.duration(200)
|
|
482
|
+
.attr('d', (d) => hoverArc(d));
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Handles mouse leave from segments
|
|
486
|
+
*/
|
|
487
|
+
handleSegmentLeave(event, d, normalArc) {
|
|
488
|
+
// Hide tooltip
|
|
489
|
+
this._tooltipVisible.set(false);
|
|
490
|
+
this.cdr.detectChanges();
|
|
491
|
+
// Emit null to indicate no segment is hovered
|
|
492
|
+
this.segmentHover.emit(null);
|
|
493
|
+
// Remove hover effect
|
|
494
|
+
this.d3
|
|
495
|
+
.select(event.currentTarget)
|
|
496
|
+
.transition()
|
|
497
|
+
.duration(200)
|
|
498
|
+
.attr('d', (d) => normalArc(d));
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Updates tooltip position
|
|
502
|
+
* Ensures the tooltip is visible by adjusting position when near edges
|
|
503
|
+
*/
|
|
504
|
+
updateTooltipPosition(event) {
|
|
505
|
+
const container = this.chartContainerEl().nativeElement.getBoundingClientRect();
|
|
506
|
+
const x = event.clientX - container.left;
|
|
507
|
+
const y = event.clientY - container.top;
|
|
508
|
+
// Get container dimensions to check if we're near the edge
|
|
509
|
+
const containerWidth = container.width;
|
|
510
|
+
const containerHeight = container.height;
|
|
511
|
+
// Tooltip dimensions approximation (can't get exact dimensions without rendering)
|
|
512
|
+
const tooltipWidth = 150; // Approximate width of tooltip
|
|
513
|
+
const tooltipHeight = 100; // Approximate height of tooltip
|
|
514
|
+
// Calculate position with edge detection
|
|
515
|
+
let tooltipX = x;
|
|
516
|
+
let tooltipY = y;
|
|
517
|
+
// Check if we're too close to the right edge
|
|
518
|
+
const rightEdgeDistance = containerWidth - x;
|
|
519
|
+
if (rightEdgeDistance < tooltipWidth + 20) {
|
|
520
|
+
// Place tooltip to the left of the cursor with no gap
|
|
521
|
+
tooltipX = x - tooltipWidth + 20; // Overlap more with cursor position
|
|
522
|
+
}
|
|
523
|
+
// Check if we're too close to the bottom edge
|
|
524
|
+
const bottomEdgeDistance = containerHeight - y;
|
|
525
|
+
if (bottomEdgeDistance < tooltipHeight + 10) {
|
|
526
|
+
// Move tooltip up if near bottom edge
|
|
527
|
+
tooltipY = y - tooltipHeight;
|
|
528
|
+
}
|
|
529
|
+
this._tooltipPosition.set({
|
|
530
|
+
x: tooltipX,
|
|
531
|
+
y: tooltipY,
|
|
532
|
+
});
|
|
533
|
+
this.cdr.detectChanges();
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Toggles the visibility of a segment
|
|
537
|
+
*/
|
|
538
|
+
toggleSegmentVisibility(id) {
|
|
539
|
+
if (this.hiddenSegments.has(id)) {
|
|
540
|
+
this.hiddenSegments.delete(id);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
this.hiddenSegments.add(id);
|
|
544
|
+
}
|
|
545
|
+
// Hide tooltip when toggling segments
|
|
546
|
+
this._tooltipVisible.set(false);
|
|
547
|
+
this.updateChart();
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Adds center display with total value
|
|
551
|
+
*/
|
|
552
|
+
addCenterDisplay(total) {
|
|
553
|
+
if (!this.svg)
|
|
554
|
+
return;
|
|
555
|
+
// Calculate appropriate font sizes based on chart dimensions
|
|
556
|
+
const chartContainerWidth = this.chartContainerEl().nativeElement.clientWidth;
|
|
557
|
+
const baseFontSize = Math.max(1.4, Math.min(2.4, chartContainerWidth / 200)); // Scale between 1.4rem and 2.4rem
|
|
558
|
+
const subTextFontSize = baseFontSize * 0.5;
|
|
559
|
+
// Create group for the total display
|
|
560
|
+
const totalDisplay = this.svg
|
|
561
|
+
.append('g')
|
|
562
|
+
.attr('class', 'ax-donut-chart-total-display')
|
|
563
|
+
.attr('text-anchor', 'middle');
|
|
564
|
+
// Add total value
|
|
565
|
+
totalDisplay
|
|
566
|
+
.append('text')
|
|
567
|
+
.attr('class', 'ax-donut-chart-total-value')
|
|
568
|
+
.style('font-size', `${baseFontSize}rem`)
|
|
569
|
+
.style('font-weight', '600')
|
|
570
|
+
.style('fill', 'currentColor')
|
|
571
|
+
.text(total.toLocaleString());
|
|
572
|
+
// Add label
|
|
573
|
+
totalDisplay
|
|
574
|
+
.append('text')
|
|
575
|
+
.attr('class', 'ax-donut-chart-total-label')
|
|
576
|
+
.attr('dy', '1.4em')
|
|
577
|
+
.style('font-size', `${subTextFontSize}rem`)
|
|
578
|
+
.style('fill', 'currentColor')
|
|
579
|
+
.style('fill-opacity', '0.8')
|
|
580
|
+
.text('Total');
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Handles chart rendering errors
|
|
584
|
+
*/
|
|
585
|
+
handleChartError() {
|
|
586
|
+
const container = this.chartContainerEl()?.nativeElement;
|
|
587
|
+
if (container) {
|
|
588
|
+
this.showNoDataMessage(container);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Cleans up chart resources
|
|
593
|
+
*/
|
|
594
|
+
cleanupChart() {
|
|
595
|
+
if (this.svg) {
|
|
596
|
+
this.d3.select(this.chartContainerEl()?.nativeElement).selectAll('svg').remove();
|
|
597
|
+
this.svg = null;
|
|
598
|
+
this.pieData = [];
|
|
599
|
+
}
|
|
600
|
+
this.hiddenSegments.clear();
|
|
601
|
+
this._tooltipVisible.set(false);
|
|
602
|
+
}
|
|
603
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: AXDonutChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
604
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.2.9", 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>\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: [":host{display:block;width:100%;height:100%}.ax-donut-chart{width:100%;height:100%;position:relative;display:flex;align-items:center;justify-content:center;border-radius:.5rem;overflow:hidden}.ax-donut-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}.ax-donut-chart-no-data-message{position:absolute;text-align:center;transform:translate(-50%,-50%);font-family:var(--ax-font-family, system-ui, sans-serif);background-color:#ffffffe6;padding:1.5rem;border-radius:.5rem;box-shadow:0 2px 12px #00000014;width:80%;max-width:300px}.ax-donut-chart-no-data-message .ax-donut-chart-no-data-icon{color:var(--ax-text-muted, #999);margin-bottom:.75rem}.ax-donut-chart-no-data-message .ax-donut-chart-no-data-text{font-size:1rem;font-weight:600;color:var(--ax-text-color, #333);margin-bottom:.5rem}.ax-donut-chart-no-data-message .ax-donut-chart-no-data-help{font-size:.8rem;color:var(--ax-text-muted, #999)}.ax-donut-chart-segment{cursor:pointer;transition:all .3s cubic-bezier(.25,.8,.25,1);stroke:#fff;stroke-width:1.5px;filter:drop-shadow(0px 1px 2px rgba(0,0,0,.1))}.ax-donut-chart-segment:hover{opacity:.92;filter:drop-shadow(0px 3px 5px rgba(0,0,0,.15));transform:scale(1.01)}.ax-donut-chart-highlighted{opacity:1;filter:brightness(1.05) drop-shadow(0px 3px 5px rgba(0,0,0,.15));transform:scale(1.02)}.ax-donut-chart-dimmed{opacity:.4}.ax-donut-chart-total-display{pointer-events:none}.ax-donut-chart-total-value{fill:currentColor}.ax-donut-chart-total-label{fill:currentColor;opacity:.8}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: AXChartTooltipComponent, selector: "ax-chart-tooltip", inputs: ["data", "position", "visible", "showPercentage", "style"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
605
|
+
}
|
|
606
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.9", ngImport: i0, type: AXDonutChartComponent, decorators: [{
|
|
607
|
+
type: Component,
|
|
608
|
+
args: [{ selector: 'ax-donut-chart', standalone: true, imports: [CommonModule, AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-donut-chart\" #chartContainer>\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: [":host{display:block;width:100%;height:100%}.ax-donut-chart{width:100%;height:100%;position:relative;display:flex;align-items:center;justify-content:center;border-radius:.5rem;overflow:hidden}.ax-donut-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}.ax-donut-chart-no-data-message{position:absolute;text-align:center;transform:translate(-50%,-50%);font-family:var(--ax-font-family, system-ui, sans-serif);background-color:#ffffffe6;padding:1.5rem;border-radius:.5rem;box-shadow:0 2px 12px #00000014;width:80%;max-width:300px}.ax-donut-chart-no-data-message .ax-donut-chart-no-data-icon{color:var(--ax-text-muted, #999);margin-bottom:.75rem}.ax-donut-chart-no-data-message .ax-donut-chart-no-data-text{font-size:1rem;font-weight:600;color:var(--ax-text-color, #333);margin-bottom:.5rem}.ax-donut-chart-no-data-message .ax-donut-chart-no-data-help{font-size:.8rem;color:var(--ax-text-muted, #999)}.ax-donut-chart-segment{cursor:pointer;transition:all .3s cubic-bezier(.25,.8,.25,1);stroke:#fff;stroke-width:1.5px;filter:drop-shadow(0px 1px 2px rgba(0,0,0,.1))}.ax-donut-chart-segment:hover{opacity:.92;filter:drop-shadow(0px 3px 5px rgba(0,0,0,.15));transform:scale(1.01)}.ax-donut-chart-highlighted{opacity:1;filter:brightness(1.05) drop-shadow(0px 3px 5px rgba(0,0,0,.15));transform:scale(1.02)}.ax-donut-chart-dimmed{opacity:.4}.ax-donut-chart-total-display{pointer-events:none}.ax-donut-chart-total-value{fill:currentColor}.ax-donut-chart-total-label{fill:currentColor;opacity:.8}\n"] }]
|
|
609
|
+
}], ctorParameters: () => [] });
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Generated bundle index. Do not edit.
|
|
613
|
+
*/
|
|
614
|
+
|
|
615
|
+
export { AXDonutChartColors, AXDonutChartComponent, AXDonutChartDefaultConfig, AX_DONUT_CHART_CONFIG, donutChartConfig };
|
|
616
|
+
//# sourceMappingURL=acorex-charts-donut-chart.mjs.map
|