import {ChangeDetectionStrategy, Component, Input, OnChanges} from '@angular/core'; 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 }) 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; } } } } }; } @Input() title!: string; @Input() data!: number[]; removeOutliers = true; groupData = false; showPercentages = false; 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 })); } /** * 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); } 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 }); } } dataPoints = this.trimEdgeZeros(dataPoints); 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)', borderRadius: 5, }]; 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; }); } }