nise/nise-frontend/src/corelib/components/chart-hit-distribution/chart-hit-distribution.component.ts

320 lines
11 KiB
TypeScript
Raw Normal View History

2024-03-05 22:52:55 +00:00
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
2024-03-06 11:26:00 +00:00
let removedOutliers = ys.filter(y => y > lowerBound && y < upperBound);
if(removedOutliers.length > 0) {
ys = removedOutliers;
}
2024-03-05 22:52:55 +00:00
}
// 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
2024-03-06 10:51:53 +00:00
const percentiles = this.percentile(ys, [25, 75]);
const iqr = percentiles[1] - percentiles[0];
2024-03-05 22:52:55 +00:00
// Calculate bounds
2024-03-06 10:51:53 +00:00
const lowerBound = percentiles[0] - 1.5 * iqr;
const upperBound = percentiles[1] + 1.5 * iqr;
2024-03-05 22:52:55 +00:00
// 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];
}
});
2024-03-06 10:51:53 +00:00
if(Object.entries(filteredData).length === 0) {
return data;
}
2024-03-05 22:52:55 +00:00
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<number, DistributionEntry>(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) {
2024-03-06 11:26:00 +00:00
labels.push(`${key}ms to ${key + 2}ms`);
2024-03-05 22:52:55 +00:00
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
2024-03-06 11:26:00 +00:00
// if (range[1] % 2 !== range[0] % 2) {
// const lastEntry = entriesMap.get(range[1]) || { ...defaultValues };
// labels.push(`${range[1]}ms to ${range[1] + 1}ms`);
// console.log(`${range[1]}ms to ${range[1] + 1}ms`, range[1], lastEntry);
// 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);
// }
2024-03-05 22:52:55 +00:00
} 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 };
}
}