X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?p=it%2Fotf.git;a=blobdiff_plain;f=otf-frontend%2Fclient%2Fsrc%2Fapp%2Flayout%2Fcomponents%2Fstats%2Fmulti-line-chart%2Fmulti-line-chart.component.ts;fp=otf-frontend%2Fclient%2Fsrc%2Fapp%2Flayout%2Fcomponents%2Fstats%2Fmulti-line-chart%2Fmulti-line-chart.component.ts;h=e9782c49e406dcb8c3ad20e85c54b4c2c3e9117a;hp=0000000000000000000000000000000000000000;hb=14f6f95c84a4a1fa8774190db4a03fd0214ec55f;hpb=f49bd1efeaaddd4891c1f329b18d8cfb28b3e75b diff --git a/otf-frontend/client/src/app/layout/components/stats/multi-line-chart/multi-line-chart.component.ts b/otf-frontend/client/src/app/layout/components/stats/multi-line-chart/multi-line-chart.component.ts new file mode 100644 index 0000000..e9782c4 --- /dev/null +++ b/otf-frontend/client/src/app/layout/components/stats/multi-line-chart/multi-line-chart.component.ts @@ -0,0 +1,336 @@ +/* Copyright (c) 2019 AT&T Intellectual Property. # +# # +# 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, OnInit, Input, ViewChild, ElementRef } from '@angular/core'; +import { StatsService } from '../stats.service'; +import * as am4core from "@amcharts/amcharts4/core"; +import * as am4charts from "@amcharts/amcharts4/charts"; +import * as moment from 'moment'; + +//am4core.useTheme(am4themes_animated); + +@Component({ + selector: 'app-multi-line-chart', + templateUrl: './multi-line-chart.component.pug', + styleUrls: ['./multi-line-chart.component.scss'] +}) +export class MultiLineChartComponent implements OnInit { + + @ViewChild('multilinechartdiv') MultiLineChartDiv: ElementRef; + @Input() height: string; + + public chart: am4charts.XYChart; + public loadingIndicator; + public chartData; + protected stats: StatsService; + public dataIsEmpty = 0; + constructor(private statsService: StatsService) { + this.stats = statsService; + } + + ngOnInit() { + + this.stats.onDefaultDataCallStarted().subscribe(res => { + this.showLoadingIndicator(); + }) + + this.stats.onTIExecutionChangeStarted().subscribe(res => { + this.showLoadingIndicator(); + }) + + this.stats.onDefaultDataCallFinished().subscribe(res => { + this.renderChart(); + this.hideLoadingIndicator(); + }) + + this.stats.onTIExecutionChangeFinished().subscribe(res => { + this.renderChart(); + this.hideLoadingIndicator(); + }) + this.renderChart(); + + //Resize if screen size changes. + // this.stats.checkWindow().subscribe(res=>{ + // this.renderChart(); + // }) + } + + + // Rearrange the data to match the format needed for amcharts. Need to group by date. Each object has a date and a list of the lines and its count. + reformatData() { + var newData = []; + + //Names of the test instances. + var InstanceStrings = {}; + + //Go through the instances and add the names to the InstanceStrings Array. + for (var numberInstances = 0; numberInstances < this.chartData.length; numberInstances++) { + var instanceName = this.chartData[numberInstances].testInstanceName; + InstanceStrings[instanceName] = 0; + } + + // Iterate through the test instances. + for (var instanceLength = 0; instanceLength < this.chartData.length; instanceLength++) { + + //For each instance go through the dates. + for (var numDates = 0; numDates < this.chartData[instanceLength].dateData.length; numDates++) { + + //Check newData to see if date has been pushed. + var dateIndex = newData.findIndex((element) => { + return ( + this.chartData[instanceLength].dateData[numDates].date.getFullYear() === element.date.getFullYear() && + this.chartData[instanceLength].dateData[numDates].date.getMonth() === element.date.getMonth() && + this.chartData[instanceLength].dateData[numDates].date.getDate() === element.date.getDate() + ) + }); + + //If date is not present push the new date and the count for the test instance. + if (newData[dateIndex] == undefined) { + //date is not present + var newDate = { + date: new Date(this.chartData[instanceLength].dateData[numDates].date.getFullYear(), + this.chartData[instanceLength].dateData[numDates].date.getMonth(), + this.chartData[instanceLength].dateData[numDates].date.getDate()) + }; + newDate = Object.assign(newDate, InstanceStrings); + newDate[this.chartData[instanceLength].testInstanceName] = this.chartData[instanceLength].dateData[numDates].count; + newData.push(newDate); + } else { + + //If the date is present update the count for that test instance. + newData[dateIndex][this.chartData[instanceLength].testInstanceName] += this.chartData[instanceLength].dateData[numDates].count; + } + } + } + return newData; + } + + //fill in dates that have no data. If a specific date is not present, we need to fill in the date and set a count for 0. + async setupPoints(rawData): Promise { + let formattedData = []; + + //If the chart is supposed to be empty push in a line with a count of 0. + if (rawData.length == 0) { + return new Promise((resolve, reject) => { + + let days = this.daysDuration(this.stats.filters.startDate, this.stats.filters.endDate); + + if (!days) { + days = 62; + this.stats.filters.startDate = (moment().subtract(2, "months").toDate()); + } + + // Go through 62 days and push a count of 0/ + for (let day = 0; day < days; day++) { + let newDate = this.addDaysToDate(this.stats.filters.startDate, day); + formattedData.push({ + date: new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate()), + count: 0, + testInstancename: "empty" + }) + } + resolve(formattedData); + }) + + + + + + //Data is not empty. push in empty days for each instance. Some instances might not have executions for each day. + } else return new Promise((resolve, reject) => { + //get list of test instances. + var InstanceStrings = {}; + for (var numberInstances = 0; numberInstances < this.chartData.length; numberInstances++) { + var instanceName = this.chartData[numberInstances].testInstanceName; + InstanceStrings[instanceName] = 0; + } + + + //Go through the data + for (let i = 0; i < rawData.length; i++) { + + //for the first iteration, + if (i == 0) { + formattedData.push(rawData[0]); + + // if the date is before the startDate specified by the filter or two months be default. + if (formattedData[0].date > this.stats.filters.startDate) { + + // Go through the difference in days and push the date and count of 0. + let days = this.daysDuration(this.stats.filters.startDate, formattedData[0].date) + for (let k = days - 1; k >= 0; k--) { + let newDate = this.addDaysToDate(this.stats.filters.startDate, k); + var objectToAdd = { + date: new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate()), + }; + //push the new date and all the strings for the test instances. + objectToAdd = Object.assign(objectToAdd, InstanceStrings); + + //add date to the beginning of the array. + formattedData.unshift(objectToAdd) + + } + } + + //for all other iterations + } else { + + //get the difference in days. + let days = this.daysDuration(rawData[i].date, rawData[i - 1].date); + if (days > 1) { + //push the new dates. + for (let j = 1; j < days; j++) { + let newDate = this.addDaysToDate(rawData[i - 1].date, j); + var objectToAdd = { + date: new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate()), + }; + //push the new date and all the strings for the test instances. + objectToAdd = Object.assign(objectToAdd, InstanceStrings); + formattedData.push(objectToAdd); + } + } + formattedData.push(rawData[i]); + } + } + + if (rawData.length >= 1) { + var days = this.daysDuration(rawData[rawData.length - 1].date, this.stats.filters.endDate); + if (days >= 1) { + for (let j = 1; j < days; j++) { + let newDate = this.addDaysToDate(rawData[rawData.length - 1].date, j); + var objectToAdd = { + date: new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate()), + }; + objectToAdd = Object.assign(objectToAdd, InstanceStrings); + formattedData.push(objectToAdd); + } + } + } + + + resolve(formattedData); + }) + } + + daysDuration(date1, date2) { + return Math.ceil(Math.abs((date1 - date2) / (60 * 60 * 24 * 1000))); + } + + addDaysToDate(date, days) { + let newDate = new Date(date); + newDate.setDate(date.getDate() + days); + return newDate; + } + + //initialize loading indicator + showLoadingIndicator() { + + this.height = "380px"; + this.loadingIndicator = this.chart.tooltipContainer.createChild(am4core.Container); + this.loadingIndicator.background.fill = am4core.color("#fff"); + this.loadingIndicator.background.fillOpacity = 0.8; + this.loadingIndicator.width = am4core.percent(100); + this.loadingIndicator.height = am4core.percent(100); + + let indicatorLabel = this.loadingIndicator.createChild(am4core.Label); + indicatorLabel.text = "Loading.."; + indicatorLabel.align = "center"; + indicatorLabel.valign = "middle"; + indicatorLabel.fontSize = 18; + indicatorLabel.fontWeight= "bold"; + indicatorLabel.dy = 50; + + let loadingImage = this.loadingIndicator.createChild(am4core.Image); + //loadingImage.href = "https://img.devrant.com/devrant/rant/r_647810_4FeCH.gif"; + loadingImage.href = "https://loading.io/spinners/equalizer/lg.equalizer-bars-loader.gif"; + //loadingImage.dataSource = "/loading-pies.svg" + loadingImage.align = "center"; + loadingImage.valign = "middle"; + loadingImage.horizontalCenter = "middle"; + loadingImage.verticalCenter = "middle"; + loadingImage.scale = 3.0; + + } + + hideLoadingIndicator() { + this.loadingIndicator.hide(); + } + + async renderChart() { + //console.log("here") + + am4core.options.minPolylineStep = 5; + + this.chart = am4core.create(this.MultiLineChartDiv.nativeElement, am4charts.XYChart); + this.chart.hiddenState.properties.opacity = 0; // this creates initial fade-in + + this.chart.paddingRight = 20; + this.chartData = this.stats.getData("multiLineData"); + + //reformat the data to match the format needed for amcharts. + var formattedData = this.reformatData(); + + //sort the data. + formattedData.sort((a, b) => a.date - b.date); + + //fill in gaps in the data + await this.setupPoints(formattedData).then(res => { + formattedData = res; + }, err => console.log(err)); + + this.chart.data = formattedData; + + let dateAxis = this.chart.xAxes.push(new am4charts.DateAxis()); + dateAxis.title.text = "Date"; + + let valueAxis = this.chart.yAxes.push(new am4charts.ValueAxis()); + valueAxis.title.text = "Executions"; + + this.chart.cursor = new am4charts.XYCursor(); + this.chart.cursor.xAxis = dateAxis; + + //if the data is empty, push in a line and set the count to 0. + if (this.chartData.length == 0) { + this.chartData.push({ testInstanceName: "empty" }) + let newSeries = this.chart.series.push(new am4charts.LineSeries()); + newSeries.name = "empty"; + newSeries.dataFields.dateX = "date"; + newSeries.dataFields.valueY = "count"; + newSeries.tooltipText = "{valueY.value}"; + newSeries.sequencedInterpolation = true; + newSeries.defaultState.transitionDuration = 1000; + newSeries.strokeWidth = 3; + newSeries.tensionX = 0.8; + } else { + + //initialize the lines for the series + for (let index = 0; index < this.chartData.length; index++) { + let newSeries = this.chart.series.push(new am4charts.LineSeries()); + newSeries.name = this.chartData[index].testInstanceName; + newSeries.dataFields.dateX = "date"; + newSeries.dataFields.valueY = (this.chartData[index].testInstanceName).toString(); + newSeries.tooltipText = "{valueY.value}"; + newSeries.sequencedInterpolation = true; + newSeries.defaultState.transitionDuration = 1000; + newSeries.strokeWidth = 3; + newSeries.tensionX = 0.8; + } + this.chart.legend = new am4charts.Legend(); + this.chart.legend.labels.template.text = "[bold {color}]{name}"; + } + } + +} \ No newline at end of file