@acorex/charts 0.0.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.
@@ -0,0 +1,686 @@
1
+ import { AXChartTooltipComponent } from '@acorex/charts/chart-tooltip';
2
+ import * as i0 from '@angular/core';
3
+ import { InjectionToken, inject, ChangeDetectorRef, input, viewChild, signal, computed, afterNextRender, effect, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core';
4
+ import { AX_GLOBAL_CONFIG } from '@acorex/core/config';
5
+ import { set } from 'lodash-es';
6
+
7
+ const AXGaugeChartDefaultConfig = {
8
+ minValue: 0,
9
+ maxValue: 100,
10
+ gaugeWidth: 22,
11
+ cornerRadius: 5,
12
+ showValue: true,
13
+ showTooltip: true,
14
+ animationDuration: 750,
15
+ animationEasing: 'cubic-out',
16
+ };
17
+ const AX_GAUGE_CHART_CONFIG = new InjectionToken('AX_GAUGE_CHART_CONFIG', {
18
+ providedIn: 'root',
19
+ factory: () => {
20
+ const global = inject(AX_GLOBAL_CONFIG);
21
+ set(global, 'chart.gaugeChart', AXGaugeChartDefaultConfig);
22
+ return AXGaugeChartDefaultConfig;
23
+ },
24
+ });
25
+ function gaugeChartConfig(config = {}) {
26
+ const result = {
27
+ ...AXGaugeChartDefaultConfig,
28
+ ...config,
29
+ };
30
+ return result;
31
+ }
32
+
33
+ /**
34
+ * Gauge Chart Component
35
+ * Renders a semi-circular gauge chart with animated needle and thresholds
36
+ */
37
+ class AXGaugeChartComponent {
38
+ // Inject ChangeDetectorRef
39
+ cdr = inject(ChangeDetectorRef);
40
+ // Inputs
41
+ /** Chart value input */
42
+ value = input(0);
43
+ /** Chart options input */
44
+ options = input({});
45
+ // Chart container reference
46
+ chartContainerEl = viewChild.required('chartContainer');
47
+ // SVG element reference (created in the code)
48
+ svgElement = null;
49
+ // D3 reference - loaded asynchronously
50
+ d3;
51
+ // Signals for component state
52
+ _initialized = signal(false);
53
+ _rendered = signal(false);
54
+ _prevValue = signal(null);
55
+ // Track previous options to detect changes
56
+ _prevOptionsString = '';
57
+ // Tooltip signals
58
+ _tooltipVisible = signal(false);
59
+ _tooltipPosition = signal({ x: 0, y: 0 });
60
+ _tooltipData = signal({
61
+ title: '',
62
+ value: '',
63
+ });
64
+ // Expose tooltip signals as read-only for the template
65
+ tooltipVisible = this._tooltipVisible.asReadonly();
66
+ tooltipPosition = this._tooltipPosition.asReadonly();
67
+ tooltipData = this._tooltipData.asReadonly();
68
+ // Inject configuration
69
+ configToken = inject(AX_GAUGE_CHART_CONFIG);
70
+ // Computed configuration options
71
+ effectiveOptions = computed(() => {
72
+ return {
73
+ ...this.configToken,
74
+ ...this.options(),
75
+ };
76
+ });
77
+ constructor() {
78
+ // Dynamically load D3 and initialize the chart when the component is ready
79
+ afterNextRender(() => {
80
+ this._initialized.set(true);
81
+ this.loadD3();
82
+ });
83
+ // Watch for changes to redraw the chart
84
+ effect(() => {
85
+ // Access inputs to track them
86
+ const currentValue = this.value();
87
+ const options = this.effectiveOptions();
88
+ // Update previous value
89
+ this._prevValue.set(currentValue);
90
+ // Only update if already rendered
91
+ if (this._rendered()) {
92
+ // If only the value changed, just update the value display and dial
93
+ const optionsString = JSON.stringify(options);
94
+ if (this._prevOptionsString === optionsString) {
95
+ this.updateValueAndDial(currentValue);
96
+ }
97
+ else {
98
+ // If options changed, recreate the whole chart
99
+ this._prevOptionsString = optionsString;
100
+ this.updateChart();
101
+ }
102
+ }
103
+ });
104
+ }
105
+ ngOnDestroy() {
106
+ this.cleanupChart();
107
+ }
108
+ /**
109
+ * Loads D3.js dynamically
110
+ */
111
+ async loadD3() {
112
+ try {
113
+ this.d3 = await import('d3');
114
+ // If container is ready, create chart
115
+ if (this._initialized() && this.chartContainerEl()) {
116
+ this.createChart();
117
+ this._rendered.set(true);
118
+ }
119
+ }
120
+ catch (error) {
121
+ console.error('Failed to load D3.js:', error);
122
+ }
123
+ }
124
+ /**
125
+ * Creates the gauge chart with all elements
126
+ */
127
+ createChart() {
128
+ // Clear container and create SVG
129
+ if (this.svgElement) {
130
+ this.svgElement.remove();
131
+ }
132
+ // Create SVG element
133
+ this.svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
134
+ this.chartContainerEl().nativeElement.appendChild(this.svgElement);
135
+ // Get current options
136
+ const options = this.effectiveOptions();
137
+ // Store options string for comparison
138
+ this._prevOptionsString = JSON.stringify(options);
139
+ // Use design tokens instead of removed properties
140
+ const showValue = options.showValue;
141
+ const minValue = options.minValue;
142
+ const maxValue = options.maxValue;
143
+ const thresholds = options.thresholds ?? [];
144
+ const label = options.label ?? '';
145
+ const gaugeWidth = options.gaugeWidth;
146
+ const cornerRadius = options.cornerRadius;
147
+ const animationDuration = options.animationDuration;
148
+ // Calculate responsive dimensions
149
+ const containerElement = this.chartContainerEl().nativeElement;
150
+ const containerWidth = containerElement.clientWidth;
151
+ const containerHeight = containerElement.clientHeight;
152
+ // Determine chart dimensions - use options if provided, otherwise use container dimensions
153
+ const width = options.width || containerWidth;
154
+ const height = options.height || containerHeight;
155
+ // Ensure minimum dimensions
156
+ const effectiveWidth = Math.max(width, 100);
157
+ const effectiveHeight = Math.max(height, 50);
158
+ // For a semi-circular gauge, width should be approximately twice the height
159
+ const size = Math.min(effectiveWidth, effectiveHeight * 2);
160
+ const margin = size * 0.1;
161
+ // Set up SVG with responsive viewBox
162
+ const svg = this.d3
163
+ .select(this.svgElement)
164
+ .attr('width', '100%')
165
+ .attr('height', '100%')
166
+ .attr('viewBox', `0 0 ${size} ${size / 2}`)
167
+ .attr('preserveAspectRatio', 'xMidYMid meet');
168
+ // Create a group for the chart with margin and move it to show only the top half
169
+ const chartGroup = svg.append('g').attr('transform', `translate(${size / 2}, ${size / 2 - margin})`);
170
+ // Define gauge parameters
171
+ const radius = size / 2 - margin;
172
+ const innerRadius = radius - gaugeWidth;
173
+ const outerRadius = radius;
174
+ // Create gradient definitions
175
+ this.createGradients(svg, thresholds);
176
+ // Draw the background arc
177
+ this.drawBackgroundArc(chartGroup, innerRadius, outerRadius, cornerRadius);
178
+ // Draw the threshold arcs if thresholds exist
179
+ if (thresholds.length > 0) {
180
+ this.drawThresholds(chartGroup, innerRadius, outerRadius, minValue, maxValue, thresholds, cornerRadius);
181
+ }
182
+ // Draw tick marks
183
+ this.drawTicks(chartGroup, outerRadius, minValue, maxValue);
184
+ // Draw the dial/needle with animation
185
+ this.drawDial(chartGroup, radius, this.value(), minValue, maxValue, animationDuration);
186
+ // Draw the value display (after the dial so it's on top)
187
+ if (showValue) {
188
+ this.drawValueDisplay(chartGroup, this.value(), label, radius);
189
+ }
190
+ // Hide tooltip initially
191
+ this._tooltipVisible.set(false);
192
+ }
193
+ /**
194
+ * Updates the chart when options change
195
+ */
196
+ updateChart() {
197
+ this.createChart();
198
+ }
199
+ /**
200
+ * Updates only the value display and dial position without recreating the chart
201
+ */
202
+ updateValueAndDial(value) {
203
+ if (!this.svgElement)
204
+ return;
205
+ const options = this.effectiveOptions();
206
+ const minValue = options.minValue;
207
+ const maxValue = options.maxValue;
208
+ const animationDuration = options.animationDuration;
209
+ const easingFunction = this.getEasingFunction(options.animationEasing);
210
+ const currentValue = value;
211
+ const svg = this.d3.select(this.svgElement);
212
+ // Get chart size from SVG viewBox
213
+ const viewBoxValues = svg.attr('viewBox')?.split(' ').map(Number) || [0, 0, 300, 150];
214
+ const size = viewBoxValues[2];
215
+ const margin = size * 0.1;
216
+ const radius = size / 2 - margin;
217
+ // Update value text
218
+ svg.select('.gauge-value').text(currentValue.toLocaleString());
219
+ // Update needle position
220
+ const angle = this.scaleValueToAngle(currentValue, minValue, maxValue);
221
+ const angleInDegrees = this.radiansToDegrees(angle - Math.PI / 2);
222
+ svg
223
+ .select('.gauge-needle')
224
+ .attr('fill', 'rgb(var(--ax-comp-gauge-chart-needle-color))')
225
+ .transition()
226
+ .duration(animationDuration)
227
+ .ease(easingFunction)
228
+ .attr('transform', `rotate(${angleInDegrees})`);
229
+ }
230
+ /**
231
+ * Draw the background arc
232
+ */
233
+ drawBackgroundArc(chartGroup, innerRadius, outerRadius, cornerRadius) {
234
+ // Create arc for the background
235
+ const backgroundArc = this.d3
236
+ .arc()
237
+ .innerRadius(innerRadius)
238
+ .outerRadius(outerRadius)
239
+ .startAngle(-Math.PI / 2) // Start from bottom (-90 degrees)
240
+ .endAngle(Math.PI / 2) // End at top (90 degrees)
241
+ .cornerRadius(cornerRadius);
242
+ // Draw background arc
243
+ const backgroundPath = chartGroup
244
+ .append('path')
245
+ .attr('d', backgroundArc)
246
+ .attr('class', 'gauge-arc gauge-base')
247
+ .attr('fill', 'url(#gauge-bg-gradient)')
248
+ .attr('filter', 'drop-shadow(0px 2px 3px rgba(0,0,0,0.1))');
249
+ // Add tooltip for single arc if no thresholds
250
+ if (!this.effectiveOptions().thresholds || this.effectiveOptions().thresholds.length === 0) {
251
+ backgroundPath
252
+ .style('cursor', 'pointer')
253
+ .on('mouseenter', (event) => {
254
+ if (!this.effectiveOptions().showTooltip)
255
+ return;
256
+ this.showSingleRangeTooltip(event);
257
+ })
258
+ .on('mousemove', (event) => {
259
+ if (this._tooltipVisible()) {
260
+ this.updateTooltipPosition(event);
261
+ }
262
+ })
263
+ .on('mouseleave', () => {
264
+ this.hideTooltip();
265
+ });
266
+ }
267
+ }
268
+ /**
269
+ * Draw the thresholds arcs
270
+ */
271
+ drawThresholds(chartGroup, innerRadius, outerRadius, minValue, maxValue, thresholds, cornerRadius) {
272
+ // Create arc generator
273
+ const arc = this.d3
274
+ .arc()
275
+ .innerRadius(innerRadius)
276
+ .outerRadius(outerRadius * 0.98)
277
+ .cornerRadius(cornerRadius);
278
+ // Sort thresholds by value in ascending order
279
+ const sortedThresholds = [...thresholds].sort((a, b) => a.value - b.value);
280
+ // Calculate all angles first using the color angle calculation
281
+ const angles = sortedThresholds.map((t) => this.scaleValueToColorAngle(t.value, minValue, maxValue));
282
+ // Start from the minimum value angle
283
+ let previousEndAngle = this.scaleValueToColorAngle(minValue, minValue, maxValue);
284
+ sortedThresholds.forEach((threshold, i) => {
285
+ const endAngle = angles[i];
286
+ // Skip if end angle is not greater than start angle
287
+ if (endAngle <= previousEndAngle)
288
+ return;
289
+ const arcPath = chartGroup
290
+ .append('path')
291
+ .attr('d', arc({
292
+ innerRadius,
293
+ outerRadius,
294
+ startAngle: previousEndAngle,
295
+ endAngle,
296
+ }))
297
+ .attr('fill', threshold.color || `url(#threshold-gradient-${i})`)
298
+ .attr('class', 'gauge-arc threshold-arc')
299
+ .attr('data-value', threshold.value)
300
+ .style('cursor', 'pointer');
301
+ // Add tooltip interaction
302
+ if (this.effectiveOptions().showTooltip) {
303
+ // Convert angles back to values for tooltip
304
+ const startValue = this.angleToValue(previousEndAngle, minValue, maxValue);
305
+ const endValue = threshold.value;
306
+ arcPath
307
+ .on('mouseenter', (event) => {
308
+ this.showThresholdTooltip(event, startValue, endValue, threshold.color, threshold.label);
309
+ })
310
+ .on('mousemove', (event) => {
311
+ if (this._tooltipVisible()) {
312
+ this.updateTooltipPosition(event);
313
+ }
314
+ })
315
+ .on('mouseleave', () => {
316
+ this.hideTooltip();
317
+ });
318
+ }
319
+ // Update the previous end angle for the next threshold
320
+ previousEndAngle = endAngle;
321
+ });
322
+ // Draw the last segment if the last threshold is less than the max value
323
+ if (previousEndAngle < Math.PI / 2) {
324
+ const lastArc = chartGroup
325
+ .append('path')
326
+ .attr('d', arc({
327
+ innerRadius,
328
+ outerRadius,
329
+ startAngle: previousEndAngle,
330
+ endAngle: Math.PI / 2,
331
+ }))
332
+ .attr('fill', 'url(#gauge-bg-gradient)')
333
+ .attr('class', 'gauge-arc threshold-arc')
334
+ .style('cursor', 'pointer');
335
+ // Add tooltip for the last segment
336
+ if (this.effectiveOptions().showTooltip) {
337
+ const startValue = this.angleToValue(previousEndAngle, minValue, maxValue);
338
+ const endValue = maxValue;
339
+ const lastThreshold = thresholds[thresholds.length - 1];
340
+ lastArc
341
+ .on('mouseenter', (event) => {
342
+ this.showThresholdTooltip(event, startValue, endValue, lastThreshold.color, lastThreshold.label);
343
+ })
344
+ .on('mousemove', (event) => {
345
+ if (this._tooltipVisible()) {
346
+ this.updateTooltipPosition(event);
347
+ }
348
+ })
349
+ .on('mouseleave', () => {
350
+ this.hideTooltip();
351
+ });
352
+ }
353
+ }
354
+ }
355
+ /**
356
+ * Shows tooltip for a threshold arc
357
+ */
358
+ showThresholdTooltip(event, startValue, endValue, color, label) {
359
+ // Create tooltip data
360
+ this._tooltipData.set({
361
+ title: label || 'Range',
362
+ value: `${startValue.toLocaleString()} - ${endValue.toLocaleString()}`,
363
+ color: color,
364
+ });
365
+ // Show tooltip
366
+ this._tooltipVisible.set(true);
367
+ this.updateTooltipPosition(event);
368
+ this.cdr.detectChanges();
369
+ }
370
+ /**
371
+ * Shows tooltip for the entire range when no thresholds are defined
372
+ */
373
+ showSingleRangeTooltip(event) {
374
+ const options = this.effectiveOptions();
375
+ if (!options.showTooltip)
376
+ return;
377
+ this.updateTooltipPosition(event);
378
+ this._tooltipData.set({
379
+ title: options.label || 'Range',
380
+ value: `${options.minValue.toLocaleString()} - ${options.maxValue.toLocaleString()}`,
381
+ color: 'rgb(var(--ax-comp-gauge-chart-track-color))',
382
+ });
383
+ this._tooltipVisible.set(true);
384
+ this.cdr.detectChanges();
385
+ }
386
+ /**
387
+ * Updates the tooltip position based on the mouse event
388
+ */
389
+ updateTooltipPosition(event) {
390
+ const container = this.chartContainerEl().nativeElement.getBoundingClientRect();
391
+ const x = event.clientX - container.left;
392
+ const y = event.clientY - container.top;
393
+ // Get container dimensions to check if we're near the edge
394
+ const containerWidth = container.width;
395
+ const containerHeight = container.height;
396
+ // Tooltip dimensions approximation (can't get exact dimensions without rendering)
397
+ const tooltipWidth = 150; // Approximate width of tooltip
398
+ const tooltipHeight = 80; // Approximate height of tooltip
399
+ // Calculate position with edge detection
400
+ let tooltipX = x + 8; // fix overlap with cursor
401
+ const tooltipY = y - 12; // fix overlap with cursor
402
+ // Check if we're too close to the right edge
403
+ const rightEdgeDistance = containerWidth - x;
404
+ if (rightEdgeDistance < tooltipWidth) {
405
+ // Place tooltip to the left of the cursor with no gap
406
+ tooltipX = x - tooltipWidth + 70; // Overlap more with cursor position
407
+ }
408
+ // Check if we're too close to the bottom edge
409
+ // const bottomEdgeDistance = containerHeight - y;
410
+ // if (bottomEdgeDistance < tooltipHeight + 10) {
411
+ // // Move tooltip up if near bottom edge
412
+ // tooltipY = y - tooltipHeight;
413
+ // }
414
+ this._tooltipPosition.set({
415
+ x: tooltipX,
416
+ y: tooltipY,
417
+ });
418
+ this.cdr.detectChanges();
419
+ }
420
+ /**
421
+ * Hides the tooltip
422
+ */
423
+ hideTooltip() {
424
+ this._tooltipVisible.set(false);
425
+ this.cdr.detectChanges();
426
+ }
427
+ /**
428
+ * Cleans up chart resources
429
+ */
430
+ cleanupChart() {
431
+ if (this.svgElement) {
432
+ this.svgElement.remove();
433
+ this.svgElement = null;
434
+ }
435
+ this._tooltipVisible.set(false);
436
+ }
437
+ /**
438
+ * Creates gradient definitions for thresholds
439
+ */
440
+ createGradients(svg, thresholds) {
441
+ const defs = svg.append('defs');
442
+ // Create a radial gradient for the background
443
+ const bgGradient = defs
444
+ .append('radialGradient')
445
+ .attr('id', 'gauge-bg-gradient')
446
+ .attr('cx', '50%')
447
+ .attr('cy', '50%')
448
+ .attr('r', '50%')
449
+ .attr('gradientUnits', 'userSpaceOnUse');
450
+ // Get CSS variable for track color
451
+ if (thresholds.length === 0) {
452
+ // Default gradient when no thresholds are provided
453
+ bgGradient.append('stop').attr('offset', '0%').attr('stop-color', 'rgb(var(--ax-comp-gauge-chart-track-color))');
454
+ bgGradient
455
+ .append('stop')
456
+ .attr('offset', '100%')
457
+ .attr('stop-color', 'rgba(var(--ax-comp-gauge-chart-track-color), 0.8)');
458
+ }
459
+ else {
460
+ bgGradient.append('stop').attr('offset', '0%').attr('stop-color', 'rgb(var(--ax-comp-gauge-chart-track-color))');
461
+ bgGradient
462
+ .append('stop')
463
+ .attr('offset', '100%')
464
+ .attr('stop-color', 'rgba(var(--ax-comp-gauge-chart-track-color), 0.8)');
465
+ // Create gradients for each threshold
466
+ thresholds.forEach((threshold, i) => {
467
+ const gradient = defs
468
+ .append('linearGradient')
469
+ .attr('id', `threshold-gradient-${i}`)
470
+ .attr('gradientUnits', 'userSpaceOnUse')
471
+ .attr('x1', '-1')
472
+ .attr('y1', '0')
473
+ .attr('x2', '1')
474
+ .attr('y2', '0');
475
+ gradient
476
+ .append('stop')
477
+ .attr('offset', '0%')
478
+ .attr('stop-color', this.d3.color(threshold.color)?.brighter(0.5).toString() || threshold.color);
479
+ gradient.append('stop').attr('offset', '100%').attr('stop-color', threshold.color);
480
+ });
481
+ }
482
+ }
483
+ /**
484
+ * Draws tick marks and labels around the gauge
485
+ */
486
+ drawTicks(chartGroup, radius, minValue, maxValue) {
487
+ const tickCount = 5; // Number of ticks including min and max
488
+ const tickLength = radius * 0.1;
489
+ const labelOffset = radius * 0.15;
490
+ // Create a group for the ticks
491
+ const tickGroup = chartGroup.append('g').attr('class', 'ticks');
492
+ // Generate tick values
493
+ const tickValues = [];
494
+ const step = (maxValue - minValue) / (tickCount - 1);
495
+ for (let i = 0; i < tickCount; i++) {
496
+ tickValues.push(minValue + i * step);
497
+ }
498
+ // Create ticks and labels
499
+ tickValues.forEach((tick) => {
500
+ // Calculate angle for this tick
501
+ const angle = this.scaleValueToAngle(tick, minValue, maxValue);
502
+ const radians = angle - Math.PI / 2; // Adjust for starting at bottom (-90 degrees)
503
+ // Calculate positions
504
+ const x1 = Math.cos(radians) * (radius + 5);
505
+ const y1 = Math.sin(radians) * (radius + 5);
506
+ const x2 = Math.cos(radians) * (radius + tickLength);
507
+ const y2 = Math.sin(radians) * (radius + tickLength);
508
+ // Calculate label position
509
+ const labelX = Math.cos(radians) * (radius + labelOffset);
510
+ const labelY = Math.sin(radians) * (radius + labelOffset);
511
+ // Draw tick line
512
+ tickGroup
513
+ .append('line')
514
+ .attr('x1', x1)
515
+ .attr('y1', y1)
516
+ .attr('x2', x2)
517
+ .attr('y2', y2)
518
+ .attr('stroke', 'rgba(var(--ax-comp-gauge-chart-text-color), 0.5)')
519
+ .attr('stroke-width', 2);
520
+ // Add tick label
521
+ tickGroup
522
+ .append('text')
523
+ .attr('x', labelX)
524
+ .attr('y', labelY)
525
+ .attr('text-anchor', 'middle')
526
+ .attr('dominant-baseline', 'middle')
527
+ .attr('fill', 'rgba(var(--ax-comp-gauge-chart-text-color), 0.7)')
528
+ .style('font-size', '12px')
529
+ .text(tick.toLocaleString());
530
+ });
531
+ }
532
+ /**
533
+ * Draws the value and label text in the center
534
+ */
535
+ drawValueDisplay(chartGroup, value, label, radius) {
536
+ // Calculate appropriate font sizes based on chart dimensions
537
+ const chartContainerWidth = this.chartContainerEl().nativeElement.clientWidth;
538
+ const baseFontSize = Math.max(1.4, Math.min(2.4, chartContainerWidth / 200)); // Scale between 1.4rem and 2.4rem
539
+ const subTextFontSize = baseFontSize * 0.5;
540
+ // Create group for the value display
541
+ const valueGroup = chartGroup.append('g').attr('class', 'gauge-value-display').attr('text-anchor', 'middle');
542
+ // Add value display
543
+ valueGroup
544
+ .append('text')
545
+ .attr('class', 'gauge-value')
546
+ .attr('x', 0)
547
+ .attr('y', radius * 0.25)
548
+ .style('font-size', `${baseFontSize}rem`)
549
+ .style('font-weight', '600')
550
+ .style('fill', 'rgb(var(--ax-comp-gauge-chart-text-color))')
551
+ .text(value.toLocaleString());
552
+ // Add label display (if provided)
553
+ if (label) {
554
+ valueGroup
555
+ .append('text')
556
+ .attr('class', 'gauge-label')
557
+ .attr('x', 0)
558
+ .attr('y', radius * 0.35)
559
+ .style('font-size', `${subTextFontSize}rem`)
560
+ .style('fill', 'rgb(var(--ax-comp-gauge-chart-text-color))')
561
+ .style('opacity', '0.8')
562
+ .text(label);
563
+ }
564
+ }
565
+ /**
566
+ * Draws the dial/needle pointing to the current value with animation
567
+ */
568
+ drawDial(chartGroup, radius, value, minValue, maxValue, animationDuration) {
569
+ // Clamp value to min/max range
570
+ const clampedValue = Math.max(minValue, Math.min(maxValue, value));
571
+ // Calculate angle for needle based on value
572
+ const angle = this.scaleValueToAngle(clampedValue, minValue, maxValue);
573
+ const angleInDegrees = this.radiansToDegrees(angle - Math.PI / 2);
574
+ // Create a group for the needle
575
+ const dialGroup = chartGroup.append('g').attr('class', 'dial');
576
+ // Draw the needle with initial position at minimum value
577
+ const needlePath = dialGroup
578
+ .append('path')
579
+ .attr('d', this.createNeedlePath(radius))
580
+ .attr('class', 'gauge-needle')
581
+ .attr('fill', 'rgb(var(--ax-comp-gauge-chart-needle-color))')
582
+ .attr('transform', `rotate(${this.radiansToDegrees(-Math.PI)})`); // Start at -180 degrees (left)
583
+ // Add a center circle
584
+ dialGroup
585
+ .append('circle')
586
+ .attr('cx', 0)
587
+ .attr('cy', 0)
588
+ .attr('r', radius * 0.08)
589
+ .attr('fill', 'rgb(var(--ax-comp-gauge-chart-needle-color))')
590
+ .attr('stroke', '#fff')
591
+ .attr('stroke-width', 2);
592
+ // Add a smaller white center
593
+ dialGroup
594
+ .append('circle')
595
+ .attr('cx', 0)
596
+ .attr('cy', 0)
597
+ .attr('r', radius * 0.03)
598
+ .attr('fill', '#fff');
599
+ // Get easing function
600
+ const easingFunction = this.getEasingFunction(this.options()?.animationEasing);
601
+ // Animate the needle from min to current value
602
+ needlePath
603
+ .transition()
604
+ .duration(animationDuration)
605
+ .ease(easingFunction)
606
+ .attr('transform', `rotate(${angleInDegrees})`);
607
+ }
608
+ /**
609
+ * Gets the appropriate D3 easing function based on the option string
610
+ */
611
+ getEasingFunction(easing) {
612
+ switch (easing) {
613
+ case 'linear':
614
+ return this.d3.easeLinear;
615
+ case 'ease':
616
+ return this.d3.easePolyInOut;
617
+ case 'ease-in':
618
+ return this.d3.easePolyIn;
619
+ case 'ease-out':
620
+ return this.d3.easePolyOut;
621
+ case 'ease-in-out':
622
+ return this.d3.easePolyInOut;
623
+ case 'cubic':
624
+ return this.d3.easeCubic;
625
+ case 'cubic-in':
626
+ return this.d3.easeCubicIn;
627
+ case 'cubic-out':
628
+ return this.d3.easeCubicOut;
629
+ case 'cubic-in-out':
630
+ return this.d3.easeCubicInOut;
631
+ default:
632
+ return this.d3.easeCubicOut; // Default easing
633
+ }
634
+ }
635
+ /**
636
+ * Creates a path string for the needle
637
+ */
638
+ createNeedlePath(radius) {
639
+ const needleLength = radius * 0.8;
640
+ const needleWidth = radius * 0.06;
641
+ return `M 0,${needleWidth / 2} L ${needleLength},0 L 0,${-needleWidth / 2} Z`;
642
+ }
643
+ /**
644
+ * Scales a value to an angle (in radians)
645
+ */
646
+ scaleValueToAngle(value, min, max) {
647
+ const valueRange = max - min;
648
+ const angleRange = Math.PI;
649
+ return ((value - min) / valueRange) * angleRange - Math.PI / 2;
650
+ }
651
+ /**
652
+ * Scales a value to an angle for color arcs (in radians)
653
+ */
654
+ scaleValueToColorAngle(value, min, max) {
655
+ const valueRange = max - min;
656
+ const angleRange = Math.PI; // 180 degrees in radians
657
+ return ((value - min) / valueRange) * angleRange - Math.PI / 2;
658
+ }
659
+ /**
660
+ * Converts an angle back to a value
661
+ */
662
+ angleToValue(angle, min, max) {
663
+ const valueRange = max - min;
664
+ const angleRange = Math.PI;
665
+ return min + ((angle + Math.PI / 2) / angleRange) * valueRange;
666
+ }
667
+ /**
668
+ * Converts radians to degrees
669
+ */
670
+ radiansToDegrees(radians) {
671
+ return radians * (180 / Math.PI);
672
+ }
673
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: AXGaugeChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
674
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.0.4", 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 }], ngImport: i0, template: "<div class=\"ax-gauge-chart\" #chartContainer></div>\n<ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\"></ax-chart-tooltip>\n", styles: ["ax-gauge-chart{display:block;width:100%;height:100%;min-height:200px;--ax-comp-gauge-chart-bg-color: var(--ax-sys-color-lightest-surface);--ax-comp-gauge-chart-text-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-gauge-chart-track-color: var(--ax-sys-color-dark-surface);--ax-comp-gauge-chart-needle-color: var(--ax-sys-color-primary-500)}ax-gauge-chart .ax-gauge-chart{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:visible;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgb(var(--ax-comp-gauge-chart-bg-color));border-radius:.5rem}ax-gauge-chart .ax-gauge-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}ax-gauge-chart .ax-gauge-chart-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 });
675
+ }
676
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: AXGaugeChartComponent, decorators: [{
677
+ type: Component,
678
+ args: [{ selector: 'ax-gauge-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-gauge-chart\" #chartContainer></div>\n<ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\"></ax-chart-tooltip>\n", styles: ["ax-gauge-chart{display:block;width:100%;height:100%;min-height:200px;--ax-comp-gauge-chart-bg-color: var(--ax-sys-color-lightest-surface);--ax-comp-gauge-chart-text-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-gauge-chart-track-color: var(--ax-sys-color-dark-surface);--ax-comp-gauge-chart-needle-color: var(--ax-sys-color-primary-500)}ax-gauge-chart .ax-gauge-chart{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:visible;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgb(var(--ax-comp-gauge-chart-bg-color));border-radius:.5rem}ax-gauge-chart .ax-gauge-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}ax-gauge-chart .ax-gauge-chart-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"] }]
679
+ }], ctorParameters: () => [] });
680
+
681
+ /**
682
+ * Generated bundle index. Do not edit.
683
+ */
684
+
685
+ export { AXGaugeChartComponent, AXGaugeChartDefaultConfig, AX_GAUGE_CHART_CONFIG, gaugeChartConfig };
686
+ //# sourceMappingURL=acorex-charts-gauge-chart.mjs.map