All files / microchart-donut si-microchart-donut.component.ts

100% Statements 41/41
73.07% Branches 19/26
100% Functions 8/8
100% Lines 36/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138                                                                                  24x                       12x         12x   12x 12x         12x 12x 12x 12x     12x 12x     12x     12x 12x         12x 12x 12x 12x 12x   12x 12x 12x 12x   12x   15x 13x 13x 13x     12x                 13x               13x 13x   13x 13x         13x       26x      
/**
 * Copyright (c) Siemens 2016 - 2026
 * SPDX-License-Identifier: MIT
 */
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  input,
  numberAttribute
} from '@angular/core';
import { Coordinate, makeArc, valueToRelativeAngle } from '@siemens/native-charts-ng/utils';
 
/**
 * One series of the microchart donut.
 */
export interface MicrochartDonutSeries {
  /** value in percent */
  valuePercent: number;
  /**
   * Use a data-color. See: {@link https://element.siemens.io/fundamentals/colors/data-visualization-colors/#tokens}
   *
   * @example "element-data-10"
   */
  colorToken: string;
  /** ID exposed as `data-id` */
  id?: string;
}
 
interface InternalSeries {
  series: MicrochartDonutSeries;
  path: string;
  colorVar: string;
}
 
@Component({
  selector: 'si-microchart-donut',
  templateUrl: './si-microchart-donut.component.html',
  styleUrl: './si-microchart-donut.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SiMicrochartDonutComponent {
  /**
   * Microchart donut series. Can also be an array representing multiple series.
   * Each series, in case of multiple, forms section of the arc, in percentage
   * of the respective value.
   * Example series can be:
   * @example
   * ```ts
   * Series: MicrochartDonutSeries[] = [{ valuePercent: 40, colorToken: 'element-data-4' }];
   * ```
   * @defaultValue []
   */
  readonly series = input.required<MicrochartDonutSeries[]>();
  /**
   * Radius of donut. The radius is calculated from center of the donut to the mid point of the arc.
   * @defaultValue 7.5
   */
  readonly radius = input(7.5, { transform: numberAttribute });
 
  protected readonly arcThickness = computed(() => 0.7 * this.radius());
  protected readonly dim = computed(() => {
    // Keeping the viewbox width and height only enough to fit the donut.
    // The radius is calculated from center of the donut to the mid point of the arc.
    // Hence half the arc thicknes on both side needs to be added (equals full arc thickness)
    // The buffer space is added because the svg donut gets cropped on certain Os + browser configuration.
    const radius = this.radius();
    const arcThickness = this.arcThickness();
    const size = 2 * radius + arcThickness + this.bufferSpace;
    return { width: size, height: size };
  });
 
  protected readonly backgroundPath = computed(() =>
    makeArc(this.center(), this.radius(), this.startAngle, this.endAngle)
  );
 
  private readonly center = computed<Coordinate>(() => {
    // Once we have the viewbox dimensions, getting the donut at the center by
    // calculating correct coordinates
    const d = this.dim();
    return {
      x: d.width / 2,
      y: d.height / 2
    };
  });
  private readonly min = 0;
  private readonly max = 100;
  private readonly startAngle = 0;
  private readonly endAngle = 360;
  private readonly bufferSpace = 4;
 
  protected readonly internalSeries = computed(() => {
    const inputSeries = this.series();
    const result: InternalSeries[] = [];
    let nextPosAngle = 0;
 
    for (const series of inputSeries) {
      // Does not add series if percent is 0 or less
      if (series.valuePercent > 0) {
        const { nextAngle, internal } = this.toInternalSeries(nextPosAngle, series);
        result.push(internal);
        nextPosAngle = nextAngle;
      }
    }
    return result;
  });
 
  private toInternalSeries(
    startAngle: number,
    series: MicrochartDonutSeries,
    color?: string
  ): { nextAngle: number; internal: InternalSeries } {
    let nextAngle =
      valueToRelativeAngle(
        this.startAngle,
        this.endAngle,
        this.min,
        this.max,
        series.valuePercent
      ) + startAngle;
 
    startAngle = this.containAngle(startAngle);
    nextAngle = this.containAngle(nextAngle);
 
    color ??= series.colorToken;
    const internal: InternalSeries = {
      series,
      path: makeArc(this.center(), this.radius(), startAngle, nextAngle),
      colorVar: `var(--${color})`
    };
    return { nextAngle, internal };
  }
 
  private containAngle(angle: number): number {
    return Math.max(this.startAngle, Math.min(this.endAngle, angle));
  }
}