Yrkki/cv-generator-fe

View on GitHub
src/app/services/chart/chart.service.ts

Summary

Maintainability
A
0 mins
Test Coverage
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2018 Georgi Marinov
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// eslint-disable-next-line no-redeclare
/*global globalThis*/
/* tslint:disable: no-non-null-assertion */
import { Injectable } from '@angular/core';
import {
  Chart, ChartConfiguration, TooltipOptions, PieController, BarController, ArcElement, BarElement,
  LinearScale, CategoryScale, Legend, Tooltip, ChartType
} from 'chart.js';
import { DeepPartial } from 'chart.js/dist/types/utils';
import { Indexable } from '../../interfaces/indexable';
import { ChartColorService } from '../../services/chart-color/chart-color.service';
import { ChartModel } from '../../model/chart/chart.model';
import { UiService } from '../../services/ui/ui.service';

/**
 * A chart diagram service.
 */
@Injectable({
  providedIn: 'root'
})
export class ChartService {
  /** Instances tracker. */
  private chartInstancesCache: Indexable<Chart> = {};

  /**
   * Constructs a chart.
   * ~constructor
   *
   * @param chartColorService The chart color service injected dependency.
   * @param chartModel The chart model injected dependency.
   * @param uiService The ui service injected dependency.
   */
  constructor(
    protected readonly chartColorService: ChartColorService,
    public readonly chartModel: ChartModel,
    public readonly uiService: UiService,
  ) {
    Chart.register(PieController, BarController, ArcElement, BarElement, LinearScale, CategoryScale, Legend, Tooltip);
  }

  /** Initializes the color scheme delegate. */
  public initColors() { this.chartColorService.initColors(); }

  /** Draws a chart.
   *
   * @param chartType The type of the chart.
   * @param chartConfiguration The chart configuration.
   */
  public drawChart(chartType: string, chartConfiguration: any) {
    // console.log('Debug: drawChart: chartType:', chartType);
    if (!this.chartModel.chartLoaded[chartType]) {
      const ctx = this.loadChartContext(this.uiService.chartName(chartType));
      // console.log('Debug: drawChart: ctx:', ctx);
      if (typeof ctx !== 'undefined') {
        // console.log('Debug: drawChart: chartConfiguration:', chartConfiguration);
        this.createChart(ctx, chartConfiguration);
        this.chartModel.chartLoaded[chartType] = true;
      }
    }
  }

  /**
   * Finds a chart graphics context for a specified id.
   *
   * @param canvasId The chart id to look up context for.
   *
   * @returns The chart graphics context if found.
   */
  private loadChartContext(canvasId: string): CanvasRenderingContext2D | undefined {
    const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
    // console.log('Debug: loadChartContext: canvas: ', canvas);
    if (typeof canvas === 'undefined' || canvas == null) { return undefined; }

    const ctx = canvas.getContext('2d');

    return ctx!;
  }

  /** Invokes redrawing the charts. */
  public refreshCharts() {
    this.chartModel.chartLoaded = {};
  }

  /**
   * Creates a chart.
   *
   * @param ctx The context to draw the chart in.
   * @param chartConfiguration The chart configuration.
   *
   * @returns A Chart object.
   */
  private createChart(ctx: CanvasRenderingContext2D, chartConfiguration: ChartConfiguration): Chart {
    try {
      if (this.chartInstancesCache[ctx.canvas.id] != null) {
        // console.log('Debug: ChartService: createChart: deleting: ctx.canvas.id: ', ctx.canvas.id);
        this.cleanUpChart(this.chartInstancesCache[ctx.canvas.id]);
        delete this.chartInstancesCache[ctx.canvas.id];
      }

      // console.log('Debug: ChartService: createChart: instantiating: chartConfiguration: ', chartConfiguration);
      const chart = new Chart(ctx, chartConfiguration);
      this.chartInstancesCache[ctx.canvas.id] = chart;
      return chart;
    } catch (err) { return this.chartInstancesCache[ctx.canvas.id]; }
  }

  /**
   * Cleans up a chart.
   *
   * @param chart The chart to clean up.
   */
  private cleanUpChart(chart: Chart) {
    if (chart != null) {
      chart.destroy();
      // delete chart;
    }
  }

  /**
   * Adds a chart of frequency objects.
   *
   * @param frequencies Array of frequency data items for the chart.
   * @param responsive Whether the chart is responsive/resizeable.
   *
   * @returns A ChartConfiguration object.
   */
  // tslint:disable-next-line: variable-name
  public addChart(frequencies: any[], responsive = false): DeepPartial<ChartConfiguration> {
    if (!frequencies) { return {}; }

    const chartConfiguration = this.chartConfiguration;
    chartConfiguration.options!.plugins!.tooltip = this.tooltip<'pie'>(frequencies);

    chartConfiguration.data = this.datasetsSettings(frequencies);
    chartConfiguration.data.datasets?.forEach((ds) => ds.data = frequencies.map((_: any) => _[1].Count));
    chartConfiguration.data.labels = frequencies.map((_: any) => _[1].ShortLabel);

    chartConfiguration.options!.responsive = responsive;
    if (responsive && chartConfiguration.data.labels.length > 100) {
      chartConfiguration.options!.plugins!.legend!.position = 'chartArea';
    }

    return chartConfiguration;
  }

  /**
   * Adds a language chart.
   *
   * @param languages The array of languages to show.
   * @param responsive Whether the chart is responsive/resizeable.
   *
   * @returns A ChartConfiguration object.
   */
  public addLanguageChart(languages: any[], responsive = false): DeepPartial<ChartConfiguration> {
    if (!languages) { return {}; }

    const chartConfiguration = this.chartConfiguration;
    chartConfiguration.options!.plugins!.legend!.position = 'right';

    chartConfiguration.options!.layout!.padding = 0;
    chartConfiguration.options!.responsive = responsive;

    chartConfiguration.options!.plugins!.tooltip!.callbacks!.label = (tooltipItem) => {
      return chartConfiguration.data!.labels?.[tooltipItem.dataIndex] as string;
    };

    chartConfiguration.data = this.datasetsSettings(languages);
    chartConfiguration.data.datasets?.forEach((ds) => ds.data = languages.map((_: any) => _.Share));
    chartConfiguration.data.labels = languages.map((_: any) => _.Language + ': ' + _.Level + ' (' + _.Share + '%)');

    return chartConfiguration;
  }

  /**
   * Adds a chart of frequency objects.
   *
   * @param frequencies Array of frequency data items for the chart.
   *
   * @returns A ChartConfiguration object.
   */
  protected get chartConfiguration(): DeepPartial<ChartConfiguration> {
    const ttype: ChartType = 'pie';
    return {
      type: ttype,
      options: {
        responsive: true, maintainAspectRatio: false,
        plugins: {
          legend: {
            labels: {
              font: {
                // family: 'Century Gothic',
                family: 'Arial, Helvetica, sans-serif',
                size: 14
              }, color: '#101010',
              boxWidth: 14,
              padding: 10
            }, fullSize: true,
            display: true,
            position: 'left'
          }, tooltip: this.tooltip<typeof ttype>()
        }, layout: { padding: 10 },
        devicePixelRatio: globalThis.devicePixelRatio * 2
      }
    };
  }

  /**
   * The tooltip.
   *
   * @returns A tooltip object.
   */
  protected tooltip<TType extends ChartType>(frequencies?: any[]): DeepPartial<DeepPartial<TooltipOptions<TType>>> {
    return {
      mode: 'nearest',
      position: 'average',
      padding: 6,
      bodyFont: { size: 14 },
      bodySpacing: 2,
      caretSize: 10,
      displayColors: false,
      backgroundColor: 'rgba(255,255,255,0.7)',
      bodyColor: '#fff',
      callbacks: {
        // tslint:disable-next-line: variable-name
        label: (tooltipItem) => {
          if (typeof tooltipItem.dataIndex === 'undefined' || typeof frequencies === 'undefined') { return ''; }
          return ((frequencies.map((_: any) => _[1].Label)[tooltipItem.dataIndex] as string).split('\n'));
        },
        // tslint:disable-next-line: variable-name
        labelTextColor: (_tooltipItem) => '#000000'
      }
    };
  }

  /**
   * The background color.
   *
   * @param color The default background color.
   *
   * @returns A background color object.
   */
  protected backgroundColor(color: string = '#00000000') {
    return {
      backgroundColor: color,
      hoverBackgroundColor: color,
    };
  }

  /**
   * The border color.
   *
   * @param color The default border color.
   *
   * @returns A border color object.
   */
  protected borderColor(color: string = '#E8E8E8') {
    return {
      borderColor: color,
      hoverBorderColor: color,
    };
  }

  /**
   * The datasets.
   *
   * @param items The items to process.
   *
   * @returns A datasets object.
   */
  private datasetsSettings(items: any[]) {
    return {
      datasets: [{
        backgroundColor: items.map((_: any) => this.chartColorService.nextBackgroundColor()),
        hoverBackgroundColor: items.map((_: any) => this.chartColorService.nextHoverBackgroundColor()),
        ...this.borderColor(),
        borderWidth: 2
      }]
    };
  }
}