opf/openproject

View on GitHub
frontend/src/app/shared/components/work-package-graphs/embedded/wp-embedded-graph.component.ts

Summary

Maintainability
C
1 day
Test Coverage
import { Component, Input, SimpleChanges } from '@angular/core';
import { WorkPackageTableConfiguration } from 'core-app/features/work-packages/components/wp-table/wp-table-configuration';
import { ChartOptions } from 'chart.js';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { GroupObject } from 'core-app/features/hal/resources/wp-collection-resource';
import DataLabelsPlugin from 'chartjs-plugin-datalabels';

export interface WorkPackageEmbeddedGraphDataset {
  label:string;
  queryProps:any;
  queryId?:number|string;
  groups?:GroupObject[];
}
interface ChartDataSet {
  label:string;
  data:number[];
}

@Component({
  selector: 'op-wp-embedded-graph',
  templateUrl: './wp-embedded-graph.html',
  styleUrls: ['./wp-embedded-graph.component.sass'],
})
export class WorkPackageEmbeddedGraphComponent {
  @Input() public datasets:WorkPackageEmbeddedGraphDataset[];

  @Input() public chartOptions:ChartOptions;

  @Input() chartType = 'bar';

  public configuration:WorkPackageTableConfiguration;

  public error:string|null = null;

  public chartHeight = '100%';

  public chartLabels:string[] = [];

  public chartData:ChartDataSet[] = [];

  public chartPlugins = [DataLabelsPlugin];

  public internalChartOptions:ChartOptions;

  public initialized = false;

  public text = {
    noResults: this.i18n.t('js.work_packages.no_results.title'),
  };

  constructor(readonly i18n:I18nService) {}

  ngOnChanges(changes:SimpleChanges) {
    if (changes.datasets) {
      this.setChartOptions();
      this.updateChartData();

      if (!changes.datasets.firstChange) {
        this.initialized = true;
      }
    } else if (changes.chartType) {
      this.setChartOptions();
    }
  }

  private updateChartData() {
    let uniqLabels = _.uniq(this.datasets.reduce((array, dataset) => {
      const groups = (dataset.groups || []).map((group) => group.value) as any;
      return array.concat(groups);
    }, [])) as string[];

    const labelCountMaps = this.datasets.map((dataset) => {
      const countMap = (dataset.groups || []).reduce((hash, group) => ({
        ...hash,
        [group.value]: group.count,
      }), {} as any);

      return {
        label: dataset.label,
        data: uniqLabels.map((label) => countMap[label] || 0),
      };
    });

    uniqLabels = uniqLabels.map((label) => {
      if (label === null) {
        return this.i18n.t('js.placeholders.default');
      }
      return label;
    });

    this.setHeight();

    // keep the array in order to update the labels
    this.chartLabels.length = 0;
    this.chartLabels.push(...uniqLabels);
    this.chartData.length = 0;
    this.chartData.push(...labelCountMaps);
  }

  protected setChartOptions() {
    const bodyFontColor= getComputedStyle(document.body).getPropertyValue('--body-font-color');
    const gridLineColor= getComputedStyle(document.body).getPropertyValue('--borderColor-default');
    const backdropColor= getComputedStyle(document.body).getPropertyValue('--overlay-backdrop-bgColor');

    const defaults:ChartOptions = {
      color: bodyFontColor,
      responsive: true,
      maintainAspectRatio: false,
      indexAxis: this.chartType === 'horizontalBar' ? 'y' : 'x',
      scales: {
        r: {
          angleLines: {
            color: this.isRadarChart() ? gridLineColor : 'transparent',
          },
          grid: {
            color: this.isRadarChart() ? gridLineColor : 'transparent',
          },
          pointLabels: {
            color: this.isRadarChart() ? bodyFontColor : 'transparent',
          },
          ticks: {
            color: this.isRadarChart() ? bodyFontColor : 'transparent',
            backdropColor: this.isRadarChart() ? backdropColor : 'transparent',
          },
        },
        y: {
          ticks: {
            color: this.isBarChart() ? bodyFontColor : 'transparent',
          },
          grid: {
            color: this.isBarChart() ? gridLineColor : 'transparent',
          },
          border: {
            color: this.isBarChart() ? bodyFontColor : 'transparent',
          },
        },
        x: {
          ticks: {
            color: this.isBarChart() ? bodyFontColor : 'transparent',
          },
          grid: {
            color: this.isBarChart() ? gridLineColor : 'transparent',
          },
          border: {
            color: this.isBarChart() ? bodyFontColor : 'transparent',
          },
        },
      },
      plugins: {
        legend: {
          // Only display legends if more than one dataset is provided.
          display: this.datasets.length > 1,
        },
        datalabels: {
          anchor: 'center',
          align: this.chartType === 'bar' ? 'top' : 'center',
          color: bodyFontColor,
        },
      },
    };

    this.internalChartOptions = {
      ...defaults,
      ...this.chartOptions,
    };
  }

  public get hasDataToDisplay() {
    return this.chartData.length > 0 && this.chartData.some((set) => set.data.length > 0);
  }

  public get mappedChartType():string {
    return this.chartType === 'horizontalBar' ? 'bar' : this.chartType;
  }

  public get chartDescription():string {
    const chartDataDescriptions = _.map(this.chartLabels, (label, index) => {
      if (this.chartData.length === 1) {
        const allCount = this.chartData[0].data[index];
        return `${allCount} ${label}`;
      }
      const labelCounts = _.map(this.chartData, (dataset) => `${dataset.data[index]} ${dataset.label}`);
      return `${label}: ${labelCounts.join(', ')}`;
    });

    return chartDataDescriptions.join('; ');
  }

  private setHeight() {
    if (this.chartType === 'horizontalBar' && this.datasets && this.datasets[0]) {
      const labels:string[] = [];
      this.datasets.forEach((d) => d.groups!.forEach((g) => {
        if (!labels.includes(g.value)) {
          labels.push(g.value);
        }
      }));
      let height = labels.length * 40;

      if (this.datasets.length > 1) {
        // make some more room for the legend
        height += 40;
      }

      // some minimum height e.g. for the labels
      height += 40;

      this.chartHeight = `${height}px`;
    } else {
      this.chartHeight = '100%';
    }
  }

  private isBarChart() {
    return this.chartType === 'bar' || this.chartType === 'horizontalBar' || this.chartType === 'line';
  }

  private isRadarChart() {
    return this.chartType === 'radar' || this.chartType === 'polarArea';
  }
}