import {Component, Input, OnChanges, OnInit} from '@angular/core'; import {ChartConfiguration, ChartData, DefaultDataPoint} from "chart.js"; import {NgChartsModule} from "ng2-charts"; import {DistributionEntry, ErrorDistribution} from "../../../app/replays"; import {FormsModule} from "@angular/forms"; import {DecimalPipe, NgForOf} from "@angular/common"; @Component({ selector: 'app-chart-hit-distribution', standalone: true, imports: [ NgChartsModule, FormsModule, DecimalPipe, NgForOf ], templateUrl: './chart-hit-distribution.component.html', styleUrl: './chart-hit-distribution.component.css' }) export class ChartHitDistributionComponent implements OnInit, OnChanges { @Input() errorDistribution!: ErrorDistribution; @Input() mods!: string[]; removeOutliers = true; groupData = true; showPercentages = true; public barChartLegend = true; public barChartPlugins = []; public barChartOptions: ChartConfiguration<'bar'>['options'] = { responsive: true, scales: { x: { stacked: true, }, y: { stacked: true } } }; ngOnInit(): void { } ngOnChanges(): void { const showPercentages = this.showPercentages; this.barChartOptions = { responsive: true, //@ts-ignore animations: false, scales: { x: { stacked: true, }, y: { stacked: true, ticks: { callback: function(value, index, ticks) { return showPercentages ? value + '%' : value; } } } } }; } calculateStatistics(): Array<{ name: string, value: number }> { let { ys} = this.calculateData(this.errorDistribution); if(this.removeOutliers) { // Calculate IQR const percentiles = this.percentile(ys, [25, 75]); const iqr = percentiles[1] - percentiles[0]; // Calculate bounds const lowerBound = percentiles[0] - 1.5 * iqr; const upperBound = percentiles[1] + 1.5 * iqr; // Filter outliers ys = ys.filter(y => y > lowerBound && y < upperBound); } // Assuming data is already without outliers and sorted if necessary let sortedData = ys.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); let stdDev = Math.sqrt(variance); let min = sortedData[0]; let max = sortedData[sortedData.length - 1]; let ur = stdDev * 10; if(this.mods.includes('DT') || this.mods.includes('NC')) { ur /= 1.5; } if(this.mods.includes('HT')) { ur /= 0.75; } const statistics = { 'mean': mean, 'median': median, 'std. dev': stdDev, 'unstable rate': ur, 'min': min, 'max': max }; return Object.entries(statistics).map(([name, value]) => ({ name, value })); } percentile(data: number[], percentiles: number[]): number[] { // 1. Sort the data const sortedData = data.slice().sort((a, b) => a - b); // 2. Calculate indices for percentiles const indices = percentiles.map(p => (p / 100) * (sortedData.length - 1)); // 3. Extract values, handling potential fractional indices const results = indices.map(index => { const lower = Math.floor(index); const upper = Math.ceil(index); const fraction = index - lower; // Basic linear interpolation if index is fractional if (fraction > 0) { return sortedData[lower] + (sortedData[upper] - sortedData[lower]) * fraction; } else { return sortedData[lower]; } }); return results; } private calculateTotalErrors(distribution: { countMiss: number; count50: number; count100: number; count300: number }): number { return distribution.countMiss + distribution.count50 + distribution.count100 + distribution.count300; } doRemoveOutliers(data: ErrorDistribution): ErrorDistribution { let {errorDetails, ys} = this.calculateData(data); // Calculate IQR const q1 = ys[Math.floor((ys.length / 4))]; const q3 = ys[Math.floor((ys.length * 3) / 4)]; const iqr = q3 - q1; // Calculate bounds const lowerBound = q1 - 1.5 * iqr; const upperBound = q3 + 1.5 * iqr; // Filter outliers const filteredDetails = errorDetails.filter(detail => detail.x > lowerBound && detail.x < upperBound); // Convert back to ErrorDistribution format const filteredData: ErrorDistribution = {}; filteredDetails.forEach(detail => { //@ts-ignore if (data[detail.x]) { //@ts-ignore filteredData[detail.x] = data[detail.x]; } }); return filteredData; } private calculateData(data: ErrorDistribution) { const errorDetails = Object.entries(data).map(([key, distribution]) => ({ x: Number.parseInt(key), y: this.calculateTotalErrors(distribution), })); let ys = []; for (let key in data) { let distributionEntry = data[key]; for (let i = 0; i < distributionEntry.countMiss; i++) { ys.push(Number.parseInt(key)); } for (let i = 0; i < distributionEntry.count50; i++) { ys.push(Number.parseInt(key)); } for (let i = 0; i < distributionEntry.count100; i++) { ys.push(Number.parseInt(key)); } for (let i = 0; i < distributionEntry.count300; i++) { ys.push(Number.parseInt(key)); } } return {errorDetails, ys}; } buildChartData(): ChartData<"bar", DefaultDataPoint<"bar">, any> { let errorDistribution= this.removeOutliers ? this.doRemoveOutliers(this.errorDistribution) : this.errorDistribution; let errorDistributionArray = Object.entries(errorDistribution); let barChartData: ChartConfiguration<'bar'>['data'] = { labels: [], datasets: [ { data: [], label: 'Miss' + (this.showPercentages ? ' (%)' : ''), backgroundColor: 'rgba(255,0,0,0.66)', borderRadius: 5 }, { data: [], label: '50' + (this.showPercentages ? ' (%)' : ''), backgroundColor: 'rgba(187,129,33,0.66)', borderRadius: 5 }, { data: [], label: '100' + (this.showPercentages ? ' (%)' : ''), backgroundColor: 'rgba(219,255,0,0.8)', borderRadius: 5 }, { data: [], label: '300' + (this.showPercentages ? ' (%)' : ''), backgroundColor: 'rgba(0,255,41,0.66)', borderRadius: 5 } ], } if (errorDistributionArray.length >= 1) { const sortedEntries = errorDistributionArray .sort((a, b) => parseInt(a[0]) - parseInt(b[0])); const chartData = this.generateChartData(sortedEntries); barChartData.labels = chartData.labels; for (let i = 0; i < 4; i++) { barChartData.datasets[i].data = chartData.datasets[i]; } } return barChartData; } private getChartRange(entries: [string, DistributionEntry][]): [number, number] { const keys = entries.map(([key, _]) => parseInt(key)); const minKey = Math.min(...keys); const maxKey = Math.max(...keys); return [minKey, maxKey]; } private generateChartData(entries: [string, DistributionEntry][]): { labels: string[], datasets: number[][] } { const range = this.getChartRange(entries); const labels: string[] = []; const datasets: number[][] = Array(4).fill(0).map(() => []); const defaultValues: DistributionEntry = { countMiss: 0, count50: 0, count100: 0, count300: 0, }; const entriesMap = new Map(entries.map(([key, value]) => [parseInt(key), value])); if(this.groupData) { let totalHits = 0; for (let key = range[0]; key <= range[1]; key++) { const entry = entriesMap.get(key) || { ...defaultValues }; totalHits += entry.countMiss + entry.count50 + entry.count100 + entry.count300; } for (let key = range[0]; key <= range[1]; key += 2) { const endKey = key + 2 <= range[1] ? key + 2 : key + 1; labels.push(`${key}ms to ${endKey}ms`); const currentEntry = entriesMap.get(key) || { ...defaultValues }; const nextEntry = key + 1 <= range[1] ? (entriesMap.get(key + 1) || { ...defaultValues }) : defaultValues; const sumEntry: DistributionEntry = { countMiss: currentEntry.countMiss + nextEntry.countMiss, count50: currentEntry.count50 + nextEntry.count50, count100: currentEntry.count100 + nextEntry.count100, count300: currentEntry.count300 + nextEntry.count300, }; datasets[0].push(this.showPercentages ? ((sumEntry.countMiss / totalHits) * 100) : sumEntry.countMiss); datasets[1].push(this.showPercentages ? ((sumEntry.count50 / totalHits) * 100) : sumEntry.count50); datasets[2].push(this.showPercentages ? ((sumEntry.count100 / totalHits) * 100) : sumEntry.count100); datasets[3].push(this.showPercentages ? ((sumEntry.count300 / totalHits) * 100) : sumEntry.count300); } // Handling the case for an odd last key if needed if (range[1] % 2 !== range[0] % 2) { const lastEntry = entriesMap.get(range[1]) || { ...defaultValues }; labels.push(`${range[1]}ms to ${range[1] + 1}ms`); datasets[0].push(this.showPercentages ? ((lastEntry.countMiss / totalHits) * 100) : lastEntry.countMiss); datasets[1].push(this.showPercentages ? ((lastEntry.count50 / totalHits) * 100) : lastEntry.count50); datasets[2].push(this.showPercentages ? ((lastEntry.count100 / totalHits) * 100) : lastEntry.count100); datasets[3].push(this.showPercentages ? ((lastEntry.count300 / totalHits) * 100) : lastEntry.count300); } } else { // Calculate totalHits as the sum of all counts across entriesMap before the loop let totalHits = 0; for (let key = range[0]; key <= range[1]; key++) { const entry = entriesMap.get(key) || { ...defaultValues }; totalHits += entry.countMiss + entry.count50 + entry.count100 + entry.count300; } // Then iterate through the range to populate your datasets for (let key = range[0]; key <= range[1]; key++) { labels.push(`${key}ms`); const currentEntry = entriesMap.get(key) || { ...defaultValues }; // Calculate percentage based on totalHits for the entire entriesMap, then push data datasets[0].push(this.showPercentages ? ((currentEntry.countMiss / totalHits) * 100) : currentEntry.countMiss); datasets[1].push(this.showPercentages ? ((currentEntry.count50 / totalHits) * 100) : currentEntry.count50); datasets[2].push(this.showPercentages ? ((currentEntry.count100 / totalHits) * 100) : currentEntry.count100); datasets[3].push(this.showPercentages ? ((currentEntry.count300 / totalHits) * 100) : currentEntry.count300); } } return { labels, datasets }; } }