@acorex/charts 21.0.0-next.13 → 21.0.0-next.15

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.
@@ -1,7 +1,7 @@
1
- import { AXChartComponent, getEasingFunction, computeTooltipPosition } from '@acorex/charts';
1
+ import { AXChartComponent, getEasingFunction, computeTooltipPosition, formatLargeNumber } from '@acorex/charts';
2
2
  import { AXChartTooltipComponent } from '@acorex/charts/chart-tooltip';
3
3
  import * as i0 from '@angular/core';
4
- import { InjectionToken, inject, ChangeDetectorRef, input, viewChild, signal, computed, afterNextRender, effect, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core';
4
+ import { InjectionToken, input, viewChild, signal, inject, computed, afterNextRender, effect, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core';
5
5
 
6
6
  const AXGaugeChartDefaultConfig = {
7
7
  minValue: 0,
@@ -35,8 +35,6 @@ function gaugeChartConfig(config = {}) {
35
35
  * Renders a semi-circular gauge chart with animated needle and thresholds
36
36
  */
37
37
  class AXGaugeChartComponent extends AXChartComponent {
38
- // Inject ChangeDetectorRef
39
- cdr = inject(ChangeDetectorRef);
40
38
  // Inputs
41
39
  /** Chart value input */
42
40
  value = input(0, ...(ngDevMode ? [{ debugName: "value" }] : []));
@@ -54,6 +52,8 @@ class AXGaugeChartComponent extends AXChartComponent {
54
52
  _prevValue = signal(null, ...(ngDevMode ? [{ debugName: "_prevValue" }] : []));
55
53
  // Track previous options to detect changes
56
54
  _prevOptionsString = '';
55
+ // Cache for container dimensions to avoid repeated DOM reads
56
+ _lastContainerRect = null;
57
57
  // Tooltip signals
58
58
  _tooltipVisible = signal(false, ...(ngDevMode ? [{ debugName: "_tooltipVisible" }] : []));
59
59
  _tooltipPosition = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "_tooltipPosition" }] : []));
@@ -74,8 +74,11 @@ class AXGaugeChartComponent extends AXChartComponent {
74
74
  ...this.options(),
75
75
  };
76
76
  }, ...(ngDevMode ? [{ debugName: "effectiveOptions" }] : []));
77
- // Tooltip gap in pixels for precise spacing
77
+ // Constants
78
78
  TOOLTIP_GAP = 10;
79
+ HALF_CIRCLE_RADIANS = Math.PI;
80
+ QUARTER_CIRCLE_RADIANS = Math.PI / 2;
81
+ DEGREES_PER_RADIAN = 180 / Math.PI;
79
82
  constructor() {
80
83
  super();
81
84
  // Dynamically load D3 and initialize the chart when the component is ready
@@ -112,6 +115,8 @@ class AXGaugeChartComponent extends AXChartComponent {
112
115
  * Loads D3.js dynamically
113
116
  */
114
117
  async loadD3() {
118
+ if (this.d3)
119
+ return; // Already loaded
115
120
  try {
116
121
  this.d3 = await import('d3');
117
122
  // If container is ready, create chart
@@ -139,37 +144,44 @@ class AXGaugeChartComponent extends AXChartComponent {
139
144
  const options = this.effectiveOptions();
140
145
  // Store options string for comparison
141
146
  this._prevOptionsString = JSON.stringify(options);
142
- // Use design tokens instead of removed properties
143
- const showValue = options.showValue;
144
- const minValue = options.minValue;
145
- const maxValue = options.maxValue;
147
+ const { minValue, maxValue, showValue, label = '', gaugeWidth, cornerRadius, animationDuration } = options;
146
148
  const thresholds = options.thresholds ?? [];
147
- const label = options.label ?? '';
148
- const gaugeWidth = options.gaugeWidth;
149
- const cornerRadius = options.cornerRadius;
150
- const animationDuration = options.animationDuration;
151
149
  // Calculate responsive dimensions
152
150
  const containerElement = this.chartContainerEl().nativeElement;
153
151
  const containerWidth = containerElement.clientWidth;
154
152
  const containerHeight = containerElement.clientHeight;
153
+ // Cache container dimensions
154
+ this._lastContainerRect = { width: containerWidth, height: containerHeight };
155
155
  // Determine chart dimensions - use options if provided, otherwise use container dimensions
156
- const width = options.width || containerWidth;
157
- const height = options.height || containerHeight;
156
+ const width = options.width ?? containerWidth;
157
+ const height = options.height ?? containerHeight;
158
158
  // Ensure minimum dimensions
159
159
  const effectiveWidth = Math.max(width, 100);
160
160
  const effectiveHeight = Math.max(height, 50);
161
161
  // For a semi-circular gauge, width should be approximately twice the height
162
162
  const size = Math.min(effectiveWidth, effectiveHeight * 2);
163
- const margin = Math.max(8, size * 0.08);
164
- // Set up SVG with responsive viewBox
163
+ // Calculate margin as percentage of size (5-8% with reasonable bounds)
164
+ const marginRatio = 0.08;
165
+ const margin = Math.max(5, Math.min(size * marginRatio, 30));
166
+ // Calculate space needed for ticks and labels (increased for larger font sizes)
167
+ const tickLabelSpace = Math.max(12, Math.min(size * 0.12, 40));
168
+ const totalVerticalSpace = size / 2 + tickLabelSpace;
169
+ // Calculate horizontal padding for tick labels (they extend beyond gauge edges)
170
+ // Estimate based on font size - tick labels are roughly 2-3 characters wide
171
+ const estimatedTickFontSize = Math.max(14, Math.min(18, Math.floor(size / 35)));
172
+ const horizontalPadding = Math.max(20, estimatedTickFontSize * 1.5);
173
+ const totalWidth = size + horizontalPadding * 2;
174
+ // Set up SVG with responsive viewBox that accounts for overflow
165
175
  const svg = this.d3
166
176
  .select(this.svgElement)
167
177
  .attr('width', '100%')
168
178
  .attr('height', '100%')
169
- .attr('viewBox', `0 0 ${size} ${size / 2}`)
179
+ .attr('viewBox', `0 0 ${totalWidth} ${totalVerticalSpace}`)
170
180
  .attr('preserveAspectRatio', 'xMidYMid meet');
171
- // Create a group for the chart with margin and move it to show only the top half
172
- const chartGroup = svg.append('g').attr('transform', `translate(${size / 2}, ${size / 2 - margin})`);
181
+ // Create a group for the chart centered horizontally with padding, positioned to show only the top half
182
+ const chartGroup = svg
183
+ .append('g')
184
+ .attr('transform', `translate(${size / 2 + horizontalPadding}, ${size / 2 - margin})`);
173
185
  // Define gauge parameters
174
186
  const radius = size / 2 - margin;
175
187
  const desiredGaugeWidth = typeof gaugeWidth === 'number' && !Number.isNaN(gaugeWidth) ? gaugeWidth : size * 0.12;
@@ -218,7 +230,7 @@ class AXGaugeChartComponent extends AXChartComponent {
218
230
  svg.select('.gauge-value').text(currentValue.toLocaleString());
219
231
  // Update needle position
220
232
  const angle = this.scaleValueToAngle(currentValue, minValue, maxValue);
221
- const angleInDegrees = this.radiansToDegrees(angle - Math.PI / 2);
233
+ const angleInDegrees = this.radiansToDegrees(angle - this.QUARTER_CIRCLE_RADIANS);
222
234
  svg
223
235
  .select('.gauge-needle')
224
236
  .attr('fill', 'rgb(var(--ax-comp-gauge-chart-needle-color))')
@@ -236,8 +248,8 @@ class AXGaugeChartComponent extends AXChartComponent {
236
248
  .arc()
237
249
  .innerRadius(innerRadius)
238
250
  .outerRadius(outerRadius)
239
- .startAngle(-Math.PI / 2) // Start from bottom (-90 degrees)
240
- .endAngle(Math.PI / 2) // End at top (90 degrees)
251
+ .startAngle(-this.QUARTER_CIRCLE_RADIANS) // Start from bottom (-90 degrees)
252
+ .endAngle(this.QUARTER_CIRCLE_RADIANS) // End at top (90 degrees)
241
253
  .cornerRadius(cornerRadius);
242
254
  // Draw background arc
243
255
  const backgroundPath = chartGroup
@@ -275,8 +287,8 @@ class AXGaugeChartComponent extends AXChartComponent {
275
287
  .innerRadius(innerRadius)
276
288
  .outerRadius(outerRadius * 0.98)
277
289
  .cornerRadius(cornerRadius);
278
- // Sort thresholds by value in ascending order
279
- const sortedThresholds = [...thresholds].sort((a, b) => a.value - b.value);
290
+ // Sort thresholds by value in ascending order (only if needed)
291
+ const sortedThresholds = thresholds.length > 1 ? [...thresholds].sort((a, b) => a.value - b.value) : thresholds;
280
292
  // Calculate all angles first using the color angle calculation
281
293
  const angles = sortedThresholds.map((t) => this.scaleValueToColorAngle(t.value, minValue, maxValue));
282
294
  // Start from the minimum value angle
@@ -320,14 +332,14 @@ class AXGaugeChartComponent extends AXChartComponent {
320
332
  previousEndAngle = endAngle;
321
333
  });
322
334
  // Draw the last segment if the last threshold is less than the max value
323
- if (previousEndAngle < Math.PI / 2) {
335
+ if (previousEndAngle < this.QUARTER_CIRCLE_RADIANS) {
324
336
  const lastArc = chartGroup
325
337
  .append('path')
326
338
  .attr('d', arc({
327
339
  innerRadius,
328
340
  outerRadius,
329
341
  startAngle: previousEndAngle,
330
- endAngle: Math.PI / 2,
342
+ endAngle: this.QUARTER_CIRCLE_RADIANS,
331
343
  }))
332
344
  .attr('fill', 'url(#gauge-bg-gradient)')
333
345
  .attr('class', 'gauge-arc threshold-arc')
@@ -365,7 +377,6 @@ class AXGaugeChartComponent extends AXChartComponent {
365
377
  // Show tooltip
366
378
  this._tooltipVisible.set(true);
367
379
  this.updateTooltipPosition(event);
368
- this.cdr.detectChanges();
369
380
  }
370
381
  /**
371
382
  * Shows tooltip for the entire range when no thresholds are defined
@@ -381,7 +392,6 @@ class AXGaugeChartComponent extends AXChartComponent {
381
392
  color: 'rgb(var(--ax-comp-gauge-chart-track-color))',
382
393
  });
383
394
  this._tooltipVisible.set(true);
384
- this.cdr.detectChanges();
385
395
  }
386
396
  /**
387
397
  * Updates the tooltip position based on the mouse event
@@ -393,22 +403,18 @@ class AXGaugeChartComponent extends AXChartComponent {
393
403
  return;
394
404
  const rect = containerEl.getBoundingClientRect();
395
405
  const tooltipEl = containerEl.querySelector('.chart-tooltip');
396
- const tooltipRect = tooltipEl ? tooltipEl.getBoundingClientRect() : null;
397
- // For gauge charts, the mouse event coordinates are already in the correct coordinate system
398
- // relative to the container, but we need to position the tooltip appropriately for the semi-circular layout
406
+ const tooltipRect = tooltipEl?.getBoundingClientRect() ?? null;
399
407
  // Position the tooltip slightly above and to the right of the cursor for better visibility
400
408
  const tooltipX = event.clientX + 10;
401
409
  const tooltipY = event.clientY - 10;
402
410
  const pos = computeTooltipPosition(rect, tooltipRect, tooltipX, tooltipY, this.TOOLTIP_GAP);
403
411
  this._tooltipPosition.set(pos);
404
- this.cdr.detectChanges();
405
412
  }
406
413
  /**
407
414
  * Hides the tooltip
408
415
  */
409
416
  hideTooltip() {
410
417
  this._tooltipVisible.set(false);
411
- this.cdr.detectChanges();
412
418
  }
413
419
  /**
414
420
  * Cleans up chart resources
@@ -419,6 +425,7 @@ class AXGaugeChartComponent extends AXChartComponent {
419
425
  this.svgElement = null;
420
426
  }
421
427
  this._tooltipVisible.set(false);
428
+ this._lastContainerRect = null;
422
429
  }
423
430
  /**
424
431
  * Creates gradient definitions for thresholds
@@ -470,16 +477,20 @@ class AXGaugeChartComponent extends AXChartComponent {
470
477
  * Draws tick marks and labels around the gauge
471
478
  */
472
479
  drawTicks(chartGroup, radius, minValue, maxValue, size) {
473
- // Dynamically choose tick count based on chart size
480
+ // Dynamically choose tick count based on chart size (reduced for better spacing)
474
481
  let tickCount = 5;
475
- if (size < 260)
476
- tickCount = 3;
477
- else if (size < 340)
482
+ if (size < 200)
483
+ tickCount = 3; // Very small: only show min, mid, max
484
+ else if (size < 300)
478
485
  tickCount = 4;
486
+ else if (size < 400)
487
+ tickCount = 5;
479
488
  else if (size > 520)
480
489
  tickCount = 7;
481
- const tickLength = Math.max(6, Math.min(radius * 0.12, 14));
482
- const labelOffset = Math.max(radius * 0.12, 22);
490
+ // Scale tick length and label offset proportionally to size
491
+ const tickLength = Math.max(4, Math.min(radius * 0.12, 14));
492
+ // Increased label offset to prevent overlap with tick lines and wide labels (like 100M)
493
+ const labelOffset = Math.max(22, Math.min(radius * 0.28, 45));
483
494
  // Create a group for the ticks
484
495
  const tickGroup = chartGroup.append('g').attr('class', 'ticks');
485
496
  // Generate tick values
@@ -492,7 +503,7 @@ class AXGaugeChartComponent extends AXChartComponent {
492
503
  tickValues.forEach((tick) => {
493
504
  // Calculate angle for this tick
494
505
  const angle = this.scaleValueToAngle(tick, minValue, maxValue);
495
- const radians = angle - Math.PI / 2; // Adjust for starting at bottom (-90 degrees)
506
+ const radians = angle - this.QUARTER_CIRCLE_RADIANS; // Adjust for starting at bottom (-90 degrees)
496
507
  // Calculate positions
497
508
  const x1 = Math.cos(radians) * (radius + 5);
498
509
  const y1 = Math.sin(radians) * (radius + 5);
@@ -510,8 +521,23 @@ class AXGaugeChartComponent extends AXChartComponent {
510
521
  .attr('y2', y2)
511
522
  .attr('stroke', 'rgba(var(--ax-comp-gauge-chart-text-color), 0.5)')
512
523
  .attr('stroke-width', 2);
513
- // Add tick label with dynamic font size
514
- const tickFontPx = Math.max(9, Math.min(12, Math.floor(size / 40)));
524
+ // Add tick label with dynamic font size (minimum 14px)
525
+ const tickFontPx = Math.max(14, Math.min(18, Math.floor(size / 35)));
526
+ // Smart formatting: preserve decimals for small ranges, round for large values
527
+ const valueRange = maxValue - minValue;
528
+ let formattedValue;
529
+ if (valueRange <= 10) {
530
+ // Small range: show up to 2 decimal places, remove trailing zeros
531
+ formattedValue = tick.toFixed(2).replace(/\.?0+$/, '');
532
+ }
533
+ else if (valueRange < 1000) {
534
+ // Medium range: show 1 decimal if needed
535
+ formattedValue = tick % 1 === 0 ? tick.toString() : tick.toFixed(1);
536
+ }
537
+ else {
538
+ // Large range: use abbreviated format (K, M, B)
539
+ formattedValue = formatLargeNumber(Math.round(tick));
540
+ }
515
541
  tickGroup
516
542
  .append('text')
517
543
  .attr('x', labelX)
@@ -520,17 +546,18 @@ class AXGaugeChartComponent extends AXChartComponent {
520
546
  .attr('dominant-baseline', 'middle')
521
547
  .attr('fill', 'rgba(var(--ax-comp-gauge-chart-text-color), 0.7)')
522
548
  .style('font-size', `${tickFontPx}px`)
523
- .text(tick.toFixed(0).toLocaleString());
549
+ .text(formattedValue);
524
550
  });
525
551
  }
526
552
  /**
527
553
  * Draws the value and label text in the center
528
554
  */
529
555
  drawValueDisplay(chartGroup, value, label, radius, size) {
530
- // Px-based scaling for robust small/large sizes
531
- const valueFontPx = Math.max(14, Math.min(32, Math.floor(size / 8)));
556
+ // Px-based scaling for robust small/large sizes with better small-size handling
557
+ const valueFontPx = Math.max(18, Math.min(32, Math.floor(size / 8)));
532
558
  const labelFontPx = Math.max(10, Math.min(16, Math.floor(valueFontPx * 0.55)));
533
- const verticalGap = Math.max(4, Math.min(10, Math.floor(size / 60)));
559
+ // More vertical gap for small sizes: use larger base value
560
+ const verticalGap = Math.max(6, Math.min(12, Math.floor(size / 40)));
534
561
  // Create group for the value display
535
562
  const valueGroup = chartGroup.append('g').attr('class', 'gauge-value-display').attr('text-anchor', 'middle');
536
563
  // Add value display
@@ -564,7 +591,7 @@ class AXGaugeChartComponent extends AXChartComponent {
564
591
  const clampedValue = Math.max(minValue, Math.min(maxValue, value));
565
592
  // Calculate angle for needle based on value
566
593
  const angle = this.scaleValueToAngle(clampedValue, minValue, maxValue);
567
- const angleInDegrees = this.radiansToDegrees(angle - Math.PI / 2);
594
+ const angleInDegrees = this.radiansToDegrees(angle - this.QUARTER_CIRCLE_RADIANS);
568
595
  // Create a group for the needle
569
596
  const dialGroup = chartGroup.append('g').attr('class', 'dial');
570
597
  // Draw the needle with initial position at minimum value
@@ -573,7 +600,7 @@ class AXGaugeChartComponent extends AXChartComponent {
573
600
  .attr('d', this.createNeedlePath(radius))
574
601
  .attr('class', 'gauge-needle')
575
602
  .attr('fill', 'rgb(var(--ax-comp-gauge-chart-needle-color))')
576
- .attr('transform', `rotate(${this.radiansToDegrees(-Math.PI)})`); // Start at -180 degrees (left)
603
+ .attr('transform', `rotate(${this.radiansToDegrees(-this.HALF_CIRCLE_RADIANS)})`); // Start at -180 degrees (left)
577
604
  // Add a center circle
578
605
  dialGroup
579
606
  .append('circle')
@@ -613,30 +640,27 @@ class AXGaugeChartComponent extends AXChartComponent {
613
640
  */
614
641
  scaleValueToAngle(value, min, max) {
615
642
  const valueRange = max - min;
616
- const angleRange = Math.PI;
617
- return ((value - min) / valueRange) * angleRange - Math.PI / 2;
643
+ return ((value - min) / valueRange) * this.HALF_CIRCLE_RADIANS - this.QUARTER_CIRCLE_RADIANS;
618
644
  }
619
645
  /**
620
646
  * Scales a value to an angle for color arcs (in radians)
621
647
  */
622
648
  scaleValueToColorAngle(value, min, max) {
623
649
  const valueRange = max - min;
624
- const angleRange = Math.PI; // 180 degrees in radians
625
- return ((value - min) / valueRange) * angleRange - Math.PI / 2;
650
+ return ((value - min) / valueRange) * this.HALF_CIRCLE_RADIANS - this.QUARTER_CIRCLE_RADIANS;
626
651
  }
627
652
  /**
628
653
  * Converts an angle back to a value
629
654
  */
630
655
  angleToValue(angle, min, max) {
631
656
  const valueRange = max - min;
632
- const angleRange = Math.PI;
633
- return min + ((angle + Math.PI / 2) / angleRange) * valueRange;
657
+ return min + ((angle + this.QUARTER_CIRCLE_RADIANS) / this.HALF_CIRCLE_RADIANS) * valueRange;
634
658
  }
635
659
  /**
636
660
  * Converts radians to degrees
637
661
  */
638
662
  radiansToDegrees(radians) {
639
- return radians * (180 / Math.PI);
663
+ return radians * this.DEGREES_PER_RADIAN;
640
664
  }
641
665
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.6", ngImport: i0, type: AXGaugeChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
642
666
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.6", type: AXGaugeChartComponent, isStandalone: true, selector: "ax-gauge-chart", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "chartContainerEl", first: true, predicate: ["chartContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"ax-gauge-chart\" role=\"img\" #chartContainer></div>\n<ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\"></ax-chart-tooltip>\n", styles: ["ax-gauge-chart{display:block;width:100%;height:100%;min-height:200px;--ax-comp-gauge-chart-bg-color: 0, 0, 0, 0;--ax-comp-gauge-chart-text-color: var(--ax-sys-color-on-lightest-surface);--ax-comp-gauge-chart-track-color: var(--ax-sys-color-dark-surface);--ax-comp-gauge-chart-needle-color: var(--ax-sys-color-primary-500)}ax-gauge-chart .ax-gauge-chart{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgba(var(--ax-comp-gauge-chart-bg-color));border-radius:.5rem}ax-gauge-chart .ax-gauge-chart svg{width:100%;height:100%;max-width:100%;max-height:100%;overflow:visible}ax-gauge-chart .ax-gauge-chart svg g:has(text){font-family:inherit}ax-gauge-chart .ax-gauge-chart-no-data-message{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:1rem;width:100%;height:100%;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgb(var(--ax-comp-gauge-chart-bg-color))}ax-gauge-chart .ax-gauge-chart-no-data-icon{margin-bottom:.75rem;color:rgba(var(--ax-comp-gauge-chart-text-color),.6)}ax-gauge-chart .ax-gauge-chart-no-data-text{font-weight:600;color:rgb(var(--ax-comp-gauge-chart-text-color))}\n"], dependencies: [{ kind: "component", type: AXChartTooltipComponent, selector: "ax-chart-tooltip", inputs: ["data", "position", "visible", "showPercentage", "style"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });