// (C) Copyright 2015 Moodle Pty Ltd. // // 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. import { Component, Input, OnDestroy, OnInit, ElementRef, OnChanges, ViewChild, SimpleChange } from '@angular/core'; import { CoreFilter } from '@features/filter/services/filter'; import { CoreFilterHelper } from '@features/filter/services/filter-helper'; import { CoreUtils } from '@services/utils/utils'; import { Chart, ChartLegendLabelItem, ChartLegendOptions } from 'chart.js'; /** * This component shows a chart using chart.js. * Documentation can be found at http://www.chartjs.org/docs/. * It only supports changes on these properties: data and labels. * * Example usage: * */ @Component({ selector: 'core-chart', templateUrl: 'core-chart.html', styleUrls: ['chart.scss'], }) export class CoreChartComponent implements OnDestroy, OnInit, OnChanges { // The first 6 colors will be the app colors, the following will be randomly generated. // It will use the same colors in the whole session. protected static backgroundColors = [ 'rgba(0,100,210, 0.6)', 'rgba(203,61,77, 0.6)', 'rgba(0,121,130, 0.6)', 'rgba(249,128,18, 0.6)', 'rgba(94,129,0, 0.6)', 'rgba(251,173,26, 0.6)', ]; @Input() data: number[] = []; // Chart data. @Input() labels: string[] = []; // Labels of the data. @Input() type?: string; // Type of chart. @Input() legend?: ChartLegendOptions; // Legend options. @Input() height = 300; // Height of the chart element. @Input() filter?: boolean | string; // Whether to filter labels. If not defined, true if contextLevel and instanceId are set. @Input() contextLevel?: string; // The context level of the text. @Input() contextInstanceId?: number; // The instance ID related to the context. @Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters. @Input() wsNotFiltered?: boolean | string; // If true it means the WS didn't filter the labels for some reason. @ViewChild('canvas') canvas?: ElementRef; chart?: ChartWithLegend; legendItems: ChartLegendLabelItem[] = []; /** * @inheritdoc */ async ngOnInit(): Promise { let legend: ChartLegendOptions = {}; if (typeof this.legend == 'undefined') { legend = { display: false, labels: { generateLabels: (chart: Chart): ChartLegendLabelItem[] => { const data = chart.data; if (data.labels?.length) { const datasets = data.datasets![0]; return data.labels.map((label, i) => ({ text: label + ': ' + datasets.data![i], fillStyle: datasets.backgroundColor![i], })); } return []; }, }, }; } else { legend = Object.assign({}, this.legend); } if (this.type == 'bar' && this.data.length >= 5) { this.type = 'horizontalBar'; } // Format labels if needed. await this.formatLabels(); const context = this.canvas!.nativeElement.getContext('2d')!; this.chart = new Chart(context, { type: this.type, data: { labels: this.labels, datasets: [{ data: this.data, backgroundColor: this.getRandomColors(this.data.length), }], }, options: { legend }, }); this.updateLegendItems(); } /** * @inheritdoc */ async ngOnChanges(changes: Record): Promise { if (!this.chart || !changes.labels || !changes.data) { return; } if (changes.labels) { // Format labels if needed. await this.formatLabels(); } this.chart.data.datasets![0] = { data: this.data, backgroundColor: this.getRandomColors(this.data.length), }; this.chart.data.labels = this.labels; this.chart.update(); this.updateLegendItems(); } /** * Format labels if needed. * * @return Promise resolved when done. */ protected async formatLabels(): Promise { if (!this.contextLevel || !this.contextInstanceId || this.filter === false) { return; } const options = { clean: true, singleLine: true, courseId: this.courseId, wsNotFiltered: CoreUtils.isTrueOrOne(this.wsNotFiltered), }; const filters = await CoreFilterHelper.getFilters(this.contextLevel, this.contextInstanceId, options); await Promise.all(this.labels.map(async (label, i) => { this.labels[i] = await CoreFilter.formatText(label, options, filters); })); } /** * Generate random colors if needed. * * @param n Number of colors needed. * @return Array with the number of background colors requested. */ protected getRandomColors(n: number): string[] { while (CoreChartComponent.backgroundColors.length < n) { const red = Math.floor(Math.random() * 255); const green = Math.floor(Math.random() * 255); const blue = Math.floor(Math.random() * 255); CoreChartComponent.backgroundColors.push('rgba(' + red + ', ' + green + ', ' + blue + ', 0.6)'); } return CoreChartComponent.backgroundColors.slice(0, n); } /** * @inheritdoc */ ngOnDestroy(): void { if (this.chart) { this.chart.destroy(); this.chart = undefined; } } /** * Recompute legendItems property. */ protected updateLegendItems(): void { this.legendItems = (this.chart?.legend?.legendItems ?? []).filter(item => !!item); } } // For some reason the legend property isn't defined in TS, define it ourselves. type ChartWithLegend = Chart & { legend?: { legendItems?: ChartLegendLabelItem[]; }; };