All files / util pie-chart.util.ts

10.34% Statements 3/29
0% Branches 0/4
0% Functions 0/8
10.71% Lines 3/28

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                  7x                                               7x                                               7x                                                                                                                                  
import type { ElementRef } from '@angular/core';
import * as d3 from 'd3';
 
import type { IPieChartDataNode, IPieChartOptions } from '../interfaces/pie-chart.interface';
import { generateConfiguration } from './configuration.util';
 
/**
 * The pie chart default configuration.
 */
export const defaultPieChartConfig: IPieChartOptions = Object.freeze({
  chartTitle: '',
  width: 600,
  height: 600,
  margin: {
    top: 20,
    right: 20,
    bottom: 20,
    left: 20,
  },
  innerRadius: 0, // increase inner radius to render a donut chart
  showLabels: true,
  labelRadiusModifier: 50,
  labelTextWrapWidth: 60,
  transitionDuration: 1000,
  color: d3.scaleOrdinal(d3.schemeCategory10),
} as IPieChartOptions);
 
/**
 * Creates a container for the pie chart.
 * @param container the chart container
 * @param config the chart configuration
 * @returns the object with the svg element and the g element
 */
const createContainer = (container: ElementRef<HTMLDivElement>, config: IPieChartOptions) => {
  const id = container.nativeElement.id ?? 'pie-0';
 
  d3.select(`#${id}`).select('svg').remove();
  const svg = d3
    .select(`#${id}`)
    .append('svg')
    .attr('width', config.width + config.margin.left + config.margin.right)
    .attr('height', config.height + config.margin.top + config.margin.bottom)
    .attr('class', id);
  const g = svg
    .append('g')
    .attr('transform', `translate(${config.width / 2 + config.margin.left},${config.height / 2 + config.margin.top})`);
 
  return { svg, g };
};
 
/**
 * Draws the pie chart.
 * @param container the chart container
 * @param data the chart data
 * @param options the chart options
 * @returns the chart configuration
 */
export const drawPieChart = (container: ElementRef<HTMLDivElement>, data: IPieChartDataNode[], options?: Partial<IPieChartOptions>) => {
  const config: IPieChartOptions = generateConfiguration<IPieChartOptions>(defaultPieChartConfig, options, {});
 
  const { g } = createContainer(container, config);
 
  const pie = d3.pie<IPieChartDataNode>().value(datum => datum.y);
 
  const radius = Math.min(config.width, config.height) / 2;
 
  const arc = d3.arc<d3.PieArcDatum<IPieChartDataNode>>().innerRadius(config.innerRadius).outerRadius(radius);
 
  const arcs = g
    .selectAll('arc')
    .data(pie(data))
    .enter()
    .append('g')
    .attr('class', 'arc')
    .on('mouseover', function (this, event: MouseEvent, d) {
      this.style.opacity = '0.8';
 
      const tooltipText = `${d.data.key}: ${d.data.y}`;
 
      g.append('text')
        .attr('class', 'chart-tooltip')
        .style('opacity', 0)
        .attr('dx', -config.width / (2 * 2 * 2))
        .attr('dy', config.height / 2 + config.margin.top)
        .text(tooltipText)
        .transition()
        .duration(config.transitionDuration)
        .style('opacity', 1);
    })
    .on('mouseout', function (this, event, d) {
      this.style.opacity = 'unset';
      d3.selectAll('.chart-tooltip')
        .transition()
        .duration(config.transitionDuration / 2)
        .style('opacity', 0)
        .remove();
    });
 
  arcs
    .append('path')
    .attr('fill', (d, i) => config.color(i.toString()))
    .attr('d', arc);
 
  if (config.showLabels) {
    const label = d3
      .arc<d3.PieArcDatum<IPieChartDataNode>>()
      .innerRadius(radius)
      .outerRadius(radius + config.labelRadiusModifier);
 
    const textDy = 5;
    arcs
      .append('text')
      .attr('class', 'legend')
      .attr('text-anchor', 'middle')
      .attr('dy', textDy)
      .attr('transform', d => `translate(${label.centroid(d)})`)
      .style('font-size', '12px')
      .text(d => d.data.y);
  }
 
  return config;
};