MOBILE-3639 core: Migrate chart component
parent
4e7c9eb6e8
commit
18757924bb
|
@ -3749,6 +3749,14 @@
|
||||||
"@babel/types": "^7.3.0"
|
"@babel/types": "^7.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/chart.js": {
|
||||||
|
"version": "2.9.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.31.tgz",
|
||||||
|
"integrity": "sha512-hzS6phN/kx3jClk3iYqEHNnYIRSi4RZrIGJ8CDLjgatpHoftCezvC44uqB3o3OUm9ftU1m7sHG8+RLyPTlACrA==",
|
||||||
|
"requires": {
|
||||||
|
"moment": "^2.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/cordova": {
|
"@types/cordova": {
|
||||||
"version": "0.0.34",
|
"version": "0.0.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz",
|
||||||
|
@ -5930,6 +5938,47 @@
|
||||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||||
},
|
},
|
||||||
|
"chart.js": {
|
||||||
|
"version": "2.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
|
||||||
|
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
|
||||||
|
"requires": {
|
||||||
|
"chartjs-color": "^2.1.0",
|
||||||
|
"moment": "^2.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chartjs-color": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
|
||||||
|
"requires": {
|
||||||
|
"chartjs-color-string": "^0.6.0",
|
||||||
|
"color-convert": "^1.9.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": {
|
||||||
|
"version": "1.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
|
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||||
|
"requires": {
|
||||||
|
"color-name": "1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-name": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||||
|
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chartjs-color-string": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
|
||||||
|
"requires": {
|
||||||
|
"color-name": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"check-es-compat": {
|
"check-es-compat": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/check-es-compat/-/check-es-compat-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/check-es-compat/-/check-es-compat-1.1.1.tgz",
|
||||||
|
|
|
@ -68,9 +68,11 @@
|
||||||
"@ionic/angular": "^5.6.3",
|
"@ionic/angular": "^5.6.3",
|
||||||
"@ngx-translate/core": "^13.0.0",
|
"@ngx-translate/core": "^13.0.0",
|
||||||
"@ngx-translate/http-loader": "^6.0.0",
|
"@ngx-translate/http-loader": "^6.0.0",
|
||||||
|
"@types/chart.js": "^2.9.31",
|
||||||
"@types/cordova": "0.0.34",
|
"@types/cordova": "0.0.34",
|
||||||
"@types/cordova-plugin-file-transfer": "^1.6.2",
|
"@types/cordova-plugin-file-transfer": "^1.6.2",
|
||||||
"@types/dom-mediacapture-record": "^1.0.7",
|
"@types/dom-mediacapture-record": "^1.0.7",
|
||||||
|
"chart.js": "^2.9.4",
|
||||||
"com-darryncampbell-cordova-plugin-intent": "^1.3.0",
|
"com-darryncampbell-cordova-plugin-intent": "^1.3.0",
|
||||||
"cordova": "^10.0.0",
|
"cordova": "^10.0.0",
|
||||||
"cordova-android": "^8.1.0",
|
"cordova-android": "^8.1.0",
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
// (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:
|
||||||
|
* <core-chart [data]="data" [labels]="labels" [type]="type" [legend]="legend"></core-chart>
|
||||||
|
*/
|
||||||
|
@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<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
chart?: ChartWithLegend;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnChanges(changes: Record<string, SimpleChange>): Promise<void> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format labels if needed.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async formatLabels(): Promise<void> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// For some reason the legend property isn't defined in TS, define it ourselves.
|
||||||
|
type ChartWithLegend = Chart & {
|
||||||
|
legend?: {
|
||||||
|
legendItems?: ChartLegendLabelItem[];
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
<canvas #canvas [attr.height]="height"></canvas>
|
||||||
|
|
||||||
|
<ion-list *ngIf="chart" inset="true">
|
||||||
|
<ion-item *ngFor="let data of chart.legend!.legendItems">
|
||||||
|
<ion-icon name="square" slot="start" [style.color]="data.fillStyle"></ion-icon>
|
||||||
|
<ion-label>{{data.text}}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
|
@ -53,6 +53,7 @@ import { CoreFilesComponent } from './files/files';
|
||||||
import { CoreLocalFileComponent } from './local-file/local-file';
|
import { CoreLocalFileComponent } from './local-file/local-file';
|
||||||
import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
|
import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
|
||||||
import { CoreSitePickerComponent } from './site-picker/site-picker';
|
import { CoreSitePickerComponent } from './site-picker/site-picker';
|
||||||
|
import { CoreChartComponent } from './chart/chart';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -88,6 +89,7 @@ import { CoreSitePickerComponent } from './site-picker/site-picker';
|
||||||
CoreLocalFileComponent,
|
CoreLocalFileComponent,
|
||||||
CoreBSTooltipComponent,
|
CoreBSTooltipComponent,
|
||||||
CoreSitePickerComponent,
|
CoreSitePickerComponent,
|
||||||
|
CoreChartComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -130,6 +132,7 @@ import { CoreSitePickerComponent } from './site-picker/site-picker';
|
||||||
CoreLocalFileComponent,
|
CoreLocalFileComponent,
|
||||||
CoreBSTooltipComponent,
|
CoreBSTooltipComponent,
|
||||||
CoreSitePickerComponent,
|
CoreSitePickerComponent,
|
||||||
|
CoreChartComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreComponentsModule {}
|
export class CoreComponentsModule {}
|
||||||
|
|
Loading…
Reference in New Issue