@acorex/charts 20.9.2 → 20.9.4

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.
@@ -10,6 +10,7 @@ const AXGaugeChartDefaultConfig = {
10
10
  cornerRadius: 5,
11
11
  showValue: true,
12
12
  showTooltip: true,
13
+ showDataLabels: true,
13
14
  animationDuration: 750,
14
15
  animationEasing: 'cubic-out',
15
16
  messages: {
@@ -83,6 +84,10 @@ class AXGaugeChartComponent extends AXChartComponent {
83
84
  TICK_LABEL_CHAR_WIDTH_RATIO = 0.62;
84
85
  TICK_LABEL_SIDE_PADDING = 6;
85
86
  LABEL_TICK_CLEARANCE = 4;
87
+ MIN_SEGMENT_CHORD_FOR_LABEL = 28;
88
+ MIN_SEGMENT_RING_FOR_LABEL = 14;
89
+ MIN_SEGMENT_LABEL_FONT = 10;
90
+ MAX_SEGMENT_LABEL_FONT = 16;
86
91
  instanceId = createChartInstanceId('ax-gauge');
87
92
  get bgGradientId() {
88
93
  return `${this.instanceId}-bg-gradient`;
@@ -324,23 +329,28 @@ class AXGaugeChartComponent extends AXChartComponent {
324
329
  const angles = sortedThresholds.map((t) => this.scaleValueToColorAngle(t.value, minValue, maxValue));
325
330
  // Start from the minimum value angle
326
331
  let previousEndAngle = this.scaleValueToColorAngle(minValue, minValue, maxValue);
332
+ const labeledSegments = [];
327
333
  sortedThresholds.forEach((threshold, i) => {
328
334
  const endAngle = angles[i];
329
335
  // Skip if end angle is not greater than start angle
330
336
  if (endAngle <= previousEndAngle)
331
337
  return;
338
+ const startAngle = previousEndAngle;
332
339
  const arcPath = chartGroup
333
340
  .append('path')
334
341
  .attr('d', arc({
335
342
  innerRadius,
336
343
  outerRadius,
337
- startAngle: previousEndAngle,
344
+ startAngle,
338
345
  endAngle,
339
346
  }))
340
347
  .attr('fill', threshold.color || `url(#${this.thresholdGradientId(i)})`)
341
348
  .attr('class', 'gauge-arc threshold-arc')
342
349
  .attr('data-value', threshold.value)
343
350
  .style('cursor', 'pointer');
351
+ if (threshold.label) {
352
+ labeledSegments.push({ startAngle, endAngle, label: threshold.label });
353
+ }
344
354
  // Add tooltip interaction
345
355
  if (this.effectiveOptions().showTooltip) {
346
356
  // Convert angles back to values for tooltip
@@ -394,6 +404,88 @@ class AXGaugeChartComponent extends AXChartComponent {
394
404
  });
395
405
  }
396
406
  }
407
+ if (this.effectiveOptions().showDataLabels !== false && labeledSegments.length > 0) {
408
+ this.drawSegmentLabels(chartGroup, labeledSegments, innerRadius, outerRadius);
409
+ }
410
+ }
411
+ /**
412
+ * Draws threshold labels centered on each segment arc when space allows.
413
+ */
414
+ drawSegmentLabels(chartGroup, segments, innerRadius, outerRadius) {
415
+ const labelArc = this.d3
416
+ .arc()
417
+ .innerRadius(innerRadius)
418
+ .outerRadius(outerRadius * 0.98);
419
+ const labelGroup = chartGroup.append('g').attr('class', 'gauge-segment-labels');
420
+ const ringWidth = outerRadius - innerRadius;
421
+ const labelRadius = innerRadius + ringWidth / 2;
422
+ const animationDuration = this.effectiveOptions().animationDuration ?? 750;
423
+ segments.forEach((segment) => {
424
+ const angle = segment.endAngle - segment.startAngle;
425
+ const chordLength = 2 * labelRadius * Math.sin(angle / 2);
426
+ const fontSize = this.calculateSegmentLabelFontSize(angle, ringWidth, chordLength);
427
+ if (fontSize <= 0)
428
+ return;
429
+ const availableWidth = chordLength * 0.8;
430
+ const text = this.getFittedSegmentLabel(segment.label, fontSize, availableWidth);
431
+ if (!text)
432
+ return;
433
+ const centroid = labelArc.centroid({
434
+ innerRadius,
435
+ outerRadius: outerRadius * 0.98,
436
+ startAngle: segment.startAngle,
437
+ endAngle: segment.endAngle,
438
+ });
439
+ labelGroup
440
+ .append('text')
441
+ .attr('class', 'gauge-segment-label')
442
+ .attr('transform', `translate(${centroid[0]}, ${centroid[1]})`)
443
+ .attr('text-anchor', 'middle')
444
+ .attr('dominant-baseline', 'middle')
445
+ .style('pointer-events', 'none')
446
+ .style('font-size', `${fontSize}px`)
447
+ .style('font-weight', '600')
448
+ .style('fill', 'rgb(var(--ax-comp-gauge-chart-data-labels-color))')
449
+ .style('opacity', 0)
450
+ .text(text)
451
+ .transition()
452
+ .duration(animationDuration)
453
+ .style('opacity', 1);
454
+ });
455
+ }
456
+ calculateSegmentLabelFontSize(angle, ringWidth, chordLength) {
457
+ if (chordLength < this.MIN_SEGMENT_CHORD_FOR_LABEL || ringWidth < this.MIN_SEGMENT_RING_FOR_LABEL) {
458
+ return 0;
459
+ }
460
+ const ringBasedSize = ringWidth * 0.45;
461
+ const angleScale = Math.min(1, 0.7 + 0.3 * (angle / (this.HALF_CIRCLE_RADIANS / 3)));
462
+ const maxByChord = chordLength / (2 * 0.65);
463
+ const fontSize = Math.min(ringBasedSize * angleScale, maxByChord);
464
+ if (fontSize < this.MIN_SEGMENT_LABEL_FONT)
465
+ return 0;
466
+ return Math.round(Math.max(this.MIN_SEGMENT_LABEL_FONT, Math.min(fontSize, this.MAX_SEGMENT_LABEL_FONT)) * 2) / 2;
467
+ }
468
+ getFittedSegmentLabel(label, fontSize, availableWidth) {
469
+ if (!label)
470
+ return '';
471
+ if (this.doesSegmentLabelFit(label, fontSize, availableWidth))
472
+ return label;
473
+ return this.truncateSegmentLabel(label, fontSize, availableWidth);
474
+ }
475
+ doesSegmentLabelFit(text, fontSize, availableWidth) {
476
+ return text.length * fontSize * 0.65 <= availableWidth;
477
+ }
478
+ truncateSegmentLabel(text, fontSize, availableWidth) {
479
+ const charWidth = fontSize * (fontSize <= 10 ? 0.7 : 0.65);
480
+ const availableForText = availableWidth - fontSize * 0.7;
481
+ if (availableForText <= 0)
482
+ return '';
483
+ const maxChars = Math.floor(availableForText / charWidth);
484
+ if (maxChars <= 0)
485
+ return '';
486
+ if (text.length <= maxChars)
487
+ return text;
488
+ return `${text.substring(0, Math.max(1, maxChars))}…`;
397
489
  }
398
490
  /**
399
491
  * Shows tooltip for a threshold arc
@@ -416,28 +508,39 @@ class AXGaugeChartComponent extends AXChartComponent {
416
508
  const options = this.effectiveOptions();
417
509
  if (!options.showTooltip)
418
510
  return;
419
- this.updateTooltipPosition(event);
420
511
  this._tooltipData.set({
421
512
  title: options.label || 'Range',
422
513
  value: `${options.minValue.toLocaleString()} - ${options.maxValue.toLocaleString()}`,
423
514
  color: 'rgb(var(--ax-comp-gauge-chart-track-color))',
424
515
  });
425
516
  this._tooltipVisible.set(true);
517
+ this.updateTooltipPosition(event);
426
518
  }
427
519
  updateTooltipPosition(event) {
428
520
  this._pendingTooltipCoords = { x: event.clientX, y: event.clientY };
429
521
  if (this._tooltipRafId != null)
430
522
  return;
523
+ this.scheduleTooltipPosition(0);
524
+ }
525
+ scheduleTooltipPosition(attempt) {
431
526
  this._tooltipRafId = requestAnimationFrame(() => {
432
- this._tooltipRafId = null;
433
527
  const coords = this._pendingTooltipCoords;
434
- if (!coords)
528
+ if (!coords) {
529
+ this._tooltipRafId = null;
435
530
  return;
531
+ }
436
532
  const containerEl = this.chartContainerEl()?.nativeElement;
437
- if (!containerEl)
533
+ if (!containerEl) {
534
+ this._tooltipRafId = null;
438
535
  return;
439
- const rect = containerEl.getBoundingClientRect();
536
+ }
440
537
  const tooltipEl = containerEl.querySelector('.chart-tooltip');
538
+ if (!tooltipEl && this._tooltipVisible() && attempt < 3) {
539
+ this.scheduleTooltipPosition(attempt + 1);
540
+ return;
541
+ }
542
+ this._tooltipRafId = null;
543
+ const rect = containerEl.getBoundingClientRect();
441
544
  const tooltipRect = tooltipEl?.getBoundingClientRect() ?? null;
442
545
  const pos = computeTooltipPosition(rect, tooltipRect, coords.x + 10, coords.y - 10, this.TOOLTIP_GAP);
443
546
  this._tooltipPosition.set(pos);
@@ -753,11 +856,11 @@ class AXGaugeChartComponent extends AXChartComponent {
753
856
  return radians * this.DEGREES_PER_RADIAN;
754
857
  }
755
858
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXGaugeChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
756
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.3", type: AXGaugeChartComponent, isStandalone: true, selector: "ax-gauge-chart", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "chartContainerEl", first: true, predicate: ["chartContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"ax-gauge-chart\" role=\"img\" #chartContainer>\n <ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\"></ax-chart-tooltip>\n</div>\n", styles: [".ax-chart-empty{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;display:flex;align-items:center;justify-content:center;box-sizing:border-box;padding:1rem;pointer-events:none}.ax-chart-empty__card{text-align:center;padding:1.5rem;border-radius:.5rem;border:1px solid rgba(var(--ax-sys-color-surface));width:80%;max-width:300px;box-sizing:border-box}.ax-chart-empty__icon{opacity:.6;margin-bottom:.75rem}.ax-chart-empty__title{font-size:1rem;font-weight:600;margin-bottom:.5rem}.ax-chart-empty__help{font-size:.8rem;opacity:.6}ax-gauge-chart{display:block;width:100%;height:100%;min-height:clamp(220px,38vw,360px);box-sizing:border-box;--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%;min-height:0;box-sizing:border-box;padding:clamp(.5rem,1.2vw,.875rem);overflow:hidden;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgba(var(--ax-comp-gauge-chart-bg-color));border-radius:.5rem}ax-gauge-chart .ax-gauge-chart svg{display:block;width:100%;height:100%;max-width:100%;max-height:100%;overflow:hidden}ax-gauge-chart .ax-gauge-chart svg g:has(text){font-family:inherit}\n"], dependencies: [{ kind: "component", type: AXChartTooltipComponent, selector: "ax-chart-tooltip", inputs: ["data", "position", "visible", "showPercentage", "style"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
859
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.3.3", type: AXGaugeChartComponent, isStandalone: true, selector: "ax-gauge-chart", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "chartContainerEl", first: true, predicate: ["chartContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"ax-gauge-chart\" role=\"img\" #chartContainer>\n <ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\"></ax-chart-tooltip>\n</div>\n", styles: [".ax-chart-empty{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;display:flex;align-items:center;justify-content:center;box-sizing:border-box;padding:1rem;pointer-events:none}.ax-chart-empty__card{text-align:center;padding:1.5rem;border-radius:.5rem;border:1px solid rgba(var(--ax-sys-color-surface));width:80%;max-width:300px;box-sizing:border-box}.ax-chart-empty__icon{opacity:.6;margin-bottom:.75rem}.ax-chart-empty__title{font-size:1rem;font-weight:600;margin-bottom:.5rem}.ax-chart-empty__help{font-size:.8rem;opacity:.6}ax-gauge-chart{display:block;width:100%;height:100%;min-height:clamp(220px,38vw,360px);box-sizing:border-box;--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-data-labels-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%;min-height:0;box-sizing:border-box;padding:clamp(.5rem,1.2vw,.875rem);overflow:hidden;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgba(var(--ax-comp-gauge-chart-bg-color));border-radius:.5rem}ax-gauge-chart .ax-gauge-chart svg{display:block;width:100%;height:100%;max-width:100%;max-height:100%;overflow:hidden}ax-gauge-chart .ax-gauge-chart svg g:has(text){font-family:inherit}\n"], dependencies: [{ kind: "component", type: AXChartTooltipComponent, selector: "ax-chart-tooltip", inputs: ["data", "position", "visible", "showPercentage", "style"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
757
860
  }
758
861
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXGaugeChartComponent, decorators: [{
759
862
  type: Component,
760
- args: [{ selector: 'ax-gauge-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-gauge-chart\" role=\"img\" #chartContainer>\n <ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\"></ax-chart-tooltip>\n</div>\n", styles: [".ax-chart-empty{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;display:flex;align-items:center;justify-content:center;box-sizing:border-box;padding:1rem;pointer-events:none}.ax-chart-empty__card{text-align:center;padding:1.5rem;border-radius:.5rem;border:1px solid rgba(var(--ax-sys-color-surface));width:80%;max-width:300px;box-sizing:border-box}.ax-chart-empty__icon{opacity:.6;margin-bottom:.75rem}.ax-chart-empty__title{font-size:1rem;font-weight:600;margin-bottom:.5rem}.ax-chart-empty__help{font-size:.8rem;opacity:.6}ax-gauge-chart{display:block;width:100%;height:100%;min-height:clamp(220px,38vw,360px);box-sizing:border-box;--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%;min-height:0;box-sizing:border-box;padding:clamp(.5rem,1.2vw,.875rem);overflow:hidden;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgba(var(--ax-comp-gauge-chart-bg-color));border-radius:.5rem}ax-gauge-chart .ax-gauge-chart svg{display:block;width:100%;height:100%;max-width:100%;max-height:100%;overflow:hidden}ax-gauge-chart .ax-gauge-chart svg g:has(text){font-family:inherit}\n"] }]
863
+ args: [{ selector: 'ax-gauge-chart', encapsulation: ViewEncapsulation.None, imports: [AXChartTooltipComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ax-gauge-chart\" role=\"img\" #chartContainer>\n <ax-chart-tooltip [data]=\"tooltipData()\" [position]=\"tooltipPosition()\" [visible]=\"tooltipVisible()\"></ax-chart-tooltip>\n</div>\n", styles: [".ax-chart-empty{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;display:flex;align-items:center;justify-content:center;box-sizing:border-box;padding:1rem;pointer-events:none}.ax-chart-empty__card{text-align:center;padding:1.5rem;border-radius:.5rem;border:1px solid rgba(var(--ax-sys-color-surface));width:80%;max-width:300px;box-sizing:border-box}.ax-chart-empty__icon{opacity:.6;margin-bottom:.75rem}.ax-chart-empty__title{font-size:1rem;font-weight:600;margin-bottom:.5rem}.ax-chart-empty__help{font-size:.8rem;opacity:.6}ax-gauge-chart{display:block;width:100%;height:100%;min-height:clamp(220px,38vw,360px);box-sizing:border-box;--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-data-labels-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%;min-height:0;box-sizing:border-box;padding:clamp(.5rem,1.2vw,.875rem);overflow:hidden;color:rgb(var(--ax-comp-gauge-chart-text-color));background-color:rgba(var(--ax-comp-gauge-chart-bg-color));border-radius:.5rem}ax-gauge-chart .ax-gauge-chart svg{display:block;width:100%;height:100%;max-width:100%;max-height:100%;overflow:hidden}ax-gauge-chart .ax-gauge-chart svg g:has(text){font-family:inherit}\n"] }]
761
864
  }], ctorParameters: () => [] });
762
865
 
763
866
  /**