2024-02-19 14:59:52 +00:00
|
|
|
import {ChangeDetectionStrategy, Component, Input, OnChanges} from '@angular/core';
|
2024-02-18 16:02:32 +00:00
|
|
|
import {DecimalPipe, NgForOf} from "@angular/common";
|
|
|
|
|
import {ChartConfiguration, ChartData, DefaultDataPoint} from "chart.js";
|
|
|
|
|
import {NgChartsModule} from "ng2-charts";
|
|
|
|
|
import {FormsModule} from "@angular/forms";
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
selector: 'app-chart',
|
|
|
|
|
standalone: true,
|
|
|
|
|
imports: [
|
|
|
|
|
DecimalPipe,
|
|
|
|
|
NgChartsModule,
|
|
|
|
|
NgForOf,
|
|
|
|
|
FormsModule
|
|
|
|
|
],
|
|
|
|
|
templateUrl: './chart.component.html',
|
|
|
|
|
styleUrl: './chart.component.css',
|
|
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush
|
|
|
|
|
})
|
2024-02-18 17:45:19 +00:00
|
|
|
export class ChartComponent implements OnChanges {
|
|
|
|
|
|
|
|
|
|
barChartOptions: ChartConfiguration<'bar'>['options'];
|
|
|
|
|
|
|
|
|
|
ngOnChanges() {
|
|
|
|
|
const showPercentages = this.showPercentages;
|
|
|
|
|
this.barChartOptions = {
|
|
|
|
|
responsive: true,
|
|
|
|
|
//@ts-ignore
|
|
|
|
|
animations: false,
|
|
|
|
|
scales: {
|
|
|
|
|
y: {
|
|
|
|
|
ticks: {
|
|
|
|
|
callback: function(value, index, ticks) {
|
|
|
|
|
return showPercentages ? value + '%' : value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-18 16:02:32 +00:00
|
|
|
}
|
2024-02-18 17:45:19 +00:00
|
|
|
};
|
|
|
|
|
}
|
2024-02-18 16:02:32 +00:00
|
|
|
|
|
|
|
|
@Input() title!: string;
|
|
|
|
|
@Input() data!: number[];
|
|
|
|
|
|
|
|
|
|
removeOutliers = true;
|
2024-11-15 22:33:32 +00:00
|
|
|
groupData = false;
|
|
|
|
|
showPercentages = false;
|
2024-02-18 16:02:32 +00:00
|
|
|
|
|
|
|
|
calculateStatistics(): Array<{ name: string, value: number }> {
|
|
|
|
|
if (this.data.length === 0) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let chartData = this.data;
|
|
|
|
|
|
|
|
|
|
if(this.removeOutliers) {
|
|
|
|
|
chartData = this.removeOutliersZScore(this.data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Assuming data is already without outliers and sorted if necessary
|
|
|
|
|
let sortedData = chartData.slice().sort((a, b) => a - b);
|
|
|
|
|
|
|
|
|
|
let mean = sortedData.reduce((acc, curr) => acc + curr, 0) / sortedData.length;
|
|
|
|
|
let median = sortedData.length % 2 === 0 ? (sortedData[sortedData.length / 2 - 1] + sortedData[sortedData.length / 2]) / 2 : sortedData[Math.floor(sortedData.length / 2)];
|
|
|
|
|
let variance = sortedData.reduce((acc, curr) => acc + Math.pow(curr - mean, 2), 0) / (sortedData.length - 1);
|
|
|
|
|
let stdDev = Math.sqrt(variance);
|
|
|
|
|
let min = sortedData[0];
|
|
|
|
|
let max = sortedData[sortedData.length - 1];
|
|
|
|
|
|
|
|
|
|
const statistics = {
|
|
|
|
|
'mean': mean,
|
|
|
|
|
'median': median,
|
|
|
|
|
'std. dev': stdDev,
|
|
|
|
|
'min': min,
|
|
|
|
|
'max': max
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return Object.entries(statistics).map(([name, value]) => ({ name, value }));
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-19 14:59:52 +00:00
|
|
|
/**
|
|
|
|
|
* Removes zeros from the start and end of the array, used for charts.
|
|
|
|
|
*/
|
|
|
|
|
trimEdgeZeros(dataPoints: { value: number, count: number }[]): { value: number, count: number }[] {
|
|
|
|
|
let startIndex = 0;
|
|
|
|
|
while (startIndex < dataPoints.length && dataPoints[startIndex].count === 0) {
|
|
|
|
|
startIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let endIndex = dataPoints.length - 1;
|
|
|
|
|
while (endIndex >= 0 && dataPoints[endIndex].count === 0) {
|
|
|
|
|
endIndex--;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dataPoints.slice(startIndex, endIndex + 1);
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-18 16:02:32 +00:00
|
|
|
buildChartData(): ChartData<"bar", DefaultDataPoint<"bar">, any> {
|
|
|
|
|
let chartData = this.data;
|
|
|
|
|
|
|
|
|
|
if(this.removeOutliers) {
|
|
|
|
|
chartData = this.removeOutliersZScore(this.data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let sortedData = chartData.slice().sort((a, b) => a - b);
|
|
|
|
|
const minData = Math.floor(sortedData[0]);
|
|
|
|
|
const maxData = Math.ceil(sortedData[sortedData.length - 1]);
|
|
|
|
|
let dataPoints: { value: number; count: number }[] = [];
|
|
|
|
|
|
|
|
|
|
if (this.groupData) {
|
|
|
|
|
const rangeSize = 2;
|
|
|
|
|
const groupRanges = Array.from({ length: (maxData - minData) / rangeSize + 1 }, (_, i) => minData + i * rangeSize);
|
|
|
|
|
|
|
|
|
|
dataPoints = groupRanges.map(rangeStart => {
|
|
|
|
|
const rangeEnd = rangeStart + rangeSize;
|
|
|
|
|
const countInGroup = sortedData.filter(value => value >= rangeStart && value < rangeEnd).length;
|
|
|
|
|
return { value: rangeStart, count: countInGroup };
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// Not grouping data, fill with zeros where needed
|
|
|
|
|
for (let i = minData; i <= maxData; i++) {
|
|
|
|
|
const countInGroup = sortedData.filter(value => Math.round(value) === i).length;
|
|
|
|
|
dataPoints.push({ value: i, count: countInGroup });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-19 14:59:52 +00:00
|
|
|
dataPoints = this.trimEdgeZeros(dataPoints);
|
|
|
|
|
|
2024-02-18 16:02:32 +00:00
|
|
|
const total = dataPoints.reduce((acc, curr) => acc + curr.count, 0);
|
|
|
|
|
|
|
|
|
|
const labels = this.groupData ? dataPoints.map(({ value }) => `${value}ms to ${value + 2}ms`) : dataPoints.map(({ value }) => `${value}ms`);
|
|
|
|
|
const datasets = [{
|
|
|
|
|
data: dataPoints.map(({ count }) => this.showPercentages ? (count / total * 100) : count),
|
|
|
|
|
label: this.showPercentages ? this.title + " (%)": this.title,
|
|
|
|
|
backgroundColor: 'rgba(0,255,41,0.66)',
|
2024-02-18 17:45:19 +00:00
|
|
|
borderRadius: 5,
|
2024-02-18 16:02:32 +00:00
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
return { labels, datasets };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private removeOutliersZScore(data: number[]): number[] {
|
|
|
|
|
const mean = data.reduce((acc, curr) => acc + curr, 0) / data.length;
|
|
|
|
|
const stdDev = Math.sqrt(data.reduce((acc, curr) => acc + Math.pow(curr - mean, 2), 0) / data.length);
|
|
|
|
|
|
|
|
|
|
return data.filter(value => {
|
|
|
|
|
const zScore = (value - mean) / stdDev;
|
|
|
|
|
return Math.abs(zScore) <= 4;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|