Improved hit distribution chart
This commit is contained in:
parent
c6c4b5bb6d
commit
63ccc5eeeb
@ -235,8 +235,8 @@ data class ReplayData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class DistributionEntry(
|
data class DistributionEntry(
|
||||||
val percentageMiss: Double,
|
val countMiss: Double,
|
||||||
val percentage300: Double,
|
val count300: Double,
|
||||||
val percentage100: Double,
|
val count100: Double,
|
||||||
val percentage50: Double
|
val count50: Double
|
||||||
)
|
)
|
||||||
@ -538,7 +538,7 @@ class ScoreService(
|
|||||||
var totalHits = 0
|
var totalHits = 0
|
||||||
|
|
||||||
judgements.forEach { hit ->
|
judgements.forEach { hit ->
|
||||||
val error = (hit.error.roundToInt() / 2) * 2
|
val error = hit.error.roundToInt()
|
||||||
val judgementType = hit.type // Assuming this is how you get the judgement type
|
val judgementType = hit.type // Assuming this is how you get the judgement type
|
||||||
errorDistribution.getOrPut(error) { mutableMapOf("MISS" to 0, "THREE_HUNDRED" to 0, "ONE_HUNDRED" to 0, "FIFTY" to 0) }
|
errorDistribution.getOrPut(error) { mutableMapOf("MISS" to 0, "THREE_HUNDRED" to 0, "ONE_HUNDRED" to 0, "FIFTY" to 0) }
|
||||||
.apply {
|
.apply {
|
||||||
@ -550,10 +550,10 @@ class ScoreService(
|
|||||||
return errorDistribution.mapValues { (_, judgementCounts) ->
|
return errorDistribution.mapValues { (_, judgementCounts) ->
|
||||||
judgementCounts.values.sum()
|
judgementCounts.values.sum()
|
||||||
DistributionEntry(
|
DistributionEntry(
|
||||||
percentageMiss = (judgementCounts.getOrDefault("MISS", 0).toDouble() / totalHits) * 100,
|
countMiss = judgementCounts.getOrDefault("MISS", 0).toDouble(),
|
||||||
percentage300 = (judgementCounts.getOrDefault("THREE_HUNDRED", 0).toDouble() / totalHits) * 100,
|
count300 = judgementCounts.getOrDefault("THREE_HUNDRED", 0).toDouble(),
|
||||||
percentage100 = (judgementCounts.getOrDefault("ONE_HUNDRED", 0).toDouble() / totalHits) * 100,
|
count100 = judgementCounts.getOrDefault("ONE_HUNDRED", 0).toDouble(),
|
||||||
percentage50 = (judgementCounts.getOrDefault("FIFTY", 0).toDouble() / totalHits) * 100
|
count50 = judgementCounts.getOrDefault("FIFTY", 0).toDouble()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -567,7 +567,7 @@ class ScoreService(
|
|||||||
var totalHits = 0
|
var totalHits = 0
|
||||||
|
|
||||||
judgements.forEach { hit ->
|
judgements.forEach { hit ->
|
||||||
val error = (hit.error!!.roundToInt() / 2) * 2
|
val error = hit.error!!.roundToInt()
|
||||||
val judgementType = hit.type // Assuming this is how you get the judgement type
|
val judgementType = hit.type // Assuming this is how you get the judgement type
|
||||||
errorDistribution.getOrPut(error) { mutableMapOf("Miss" to 0, "300" to 0, "100" to 0, "50" to 0) }
|
errorDistribution.getOrPut(error) { mutableMapOf("Miss" to 0, "300" to 0, "100" to 0, "50" to 0) }
|
||||||
.apply {
|
.apply {
|
||||||
@ -579,10 +579,10 @@ class ScoreService(
|
|||||||
return errorDistribution.mapValues { (_, judgementCounts) ->
|
return errorDistribution.mapValues { (_, judgementCounts) ->
|
||||||
judgementCounts.values.sum()
|
judgementCounts.values.sum()
|
||||||
DistributionEntry(
|
DistributionEntry(
|
||||||
percentageMiss = (judgementCounts.getOrDefault("Miss", 0).toDouble() / totalHits) * 100,
|
countMiss = judgementCounts.getOrDefault("MISS", 0).toDouble(),
|
||||||
percentage300 = (judgementCounts.getOrDefault("300", 0).toDouble() / totalHits) * 100,
|
count300 = judgementCounts.getOrDefault("THREE_HUNDRED", 0).toDouble(),
|
||||||
percentage100 = (judgementCounts.getOrDefault("100", 0).toDouble() / totalHits) * 100,
|
count100 = judgementCounts.getOrDefault("ONE_HUNDRED", 0).toDouble(),
|
||||||
percentage50 = (judgementCounts.getOrDefault("50", 0).toDouble() / totalHits) * 100
|
count50 = judgementCounts.getOrDefault("FIFTY", 0).toDouble()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -207,10 +207,10 @@ class UserScoreService(
|
|||||||
return errorDistribution.mapValues { (_, judgementCounts) ->
|
return errorDistribution.mapValues { (_, judgementCounts) ->
|
||||||
judgementCounts.values.sum()
|
judgementCounts.values.sum()
|
||||||
DistributionEntry(
|
DistributionEntry(
|
||||||
percentageMiss = (judgementCounts.getOrDefault("MISS", 0).toDouble() / totalHits) * 100,
|
countMiss = judgementCounts.getOrDefault("MISS", 0).toDouble(),
|
||||||
percentage300 = (judgementCounts.getOrDefault("THREE_HUNDRED", 0).toDouble() / totalHits) * 100,
|
count300 = judgementCounts.getOrDefault("THREE_HUNDRED", 0).toDouble(),
|
||||||
percentage100 = (judgementCounts.getOrDefault("ONE_HUNDRED", 0).toDouble() / totalHits) * 100,
|
count100 = judgementCounts.getOrDefault("ONE_HUNDRED", 0).toDouble(),
|
||||||
percentage50 = (judgementCounts.getOrDefault("FIFTY", 0).toDouble() / totalHits) * 100
|
count50 = judgementCounts.getOrDefault("FIFTY", 0).toDouble()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -168,10 +168,10 @@ export interface ReplayData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DistributionEntry {
|
export interface DistributionEntry {
|
||||||
percentageMiss: number;
|
countMiss: number;
|
||||||
percentage300: number;
|
count300: number;
|
||||||
percentage100: number;
|
count100: number;
|
||||||
percentage50: number;
|
count50: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorDistribution {
|
export interface ErrorDistribution {
|
||||||
|
|||||||
@ -222,15 +222,7 @@
|
|||||||
<app-chart [title]="chart.title" [data]="chart.data"></app-chart>
|
<app-chart [title]="chart.title" [data]="chart.data"></app-chart>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div class="main term mb-2" *ngIf="this.replayData.error_distribution && Object.keys(this.replayData.error_distribution).length > 0">
|
<ng-container *ngIf="hasReplay()">
|
||||||
<h1># hit distribution</h1>
|
<app-chart-hit-distribution [errorDistribution]="replayData!!.error_distribution" [mods]="replayData!!.mods"></app-chart-hit-distribution>
|
||||||
<canvas baseChart
|
</ng-container>
|
||||||
[data]="barChartData"
|
|
||||||
[options]="barChartOptions"
|
|
||||||
[plugins]="barChartPlugins"
|
|
||||||
[legend]="barChartLegend"
|
|
||||||
[type]="'bar'"
|
|
||||||
class="chart">
|
|
||||||
</canvas>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@ -11,6 +11,9 @@ import {calculateAccuracy} from "../format";
|
|||||||
import {Title} from "@angular/platform-browser";
|
import {Title} from "@angular/platform-browser";
|
||||||
import {OsuGradeComponent} from "../../corelib/components/osu-grade/osu-grade.component";
|
import {OsuGradeComponent} from "../../corelib/components/osu-grade/osu-grade.component";
|
||||||
import {ChartComponent} from "../../corelib/components/chart/chart.component";
|
import {ChartComponent} from "../../corelib/components/chart/chart.component";
|
||||||
|
import {
|
||||||
|
ChartHitDistributionComponent
|
||||||
|
} from "../../corelib/components/chart-hit-distribution/chart-hit-distribution.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-view-score',
|
selector: 'app-view-score',
|
||||||
@ -24,7 +27,8 @@ import {ChartComponent} from "../../corelib/components/chart/chart.component";
|
|||||||
NgOptimizedImage,
|
NgOptimizedImage,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
OsuGradeComponent,
|
OsuGradeComponent,
|
||||||
ChartComponent
|
ChartComponent,
|
||||||
|
ChartHitDistributionComponent
|
||||||
],
|
],
|
||||||
templateUrl: './view-score.component.html',
|
templateUrl: './view-score.component.html',
|
||||||
styleUrl: './view-score.component.css'
|
styleUrl: './view-score.component.css'
|
||||||
@ -39,31 +43,6 @@ export class ViewScoreComponent implements OnInit {
|
|||||||
replayData: ReplayData | null = null;
|
replayData: ReplayData | null = null;
|
||||||
replayId: number | null = null;
|
replayId: number | null = null;
|
||||||
|
|
||||||
public barChartLegend = true;
|
|
||||||
public barChartPlugins = [];
|
|
||||||
|
|
||||||
public barChartData: ChartConfiguration<'bar'>['data'] = {
|
|
||||||
labels: [],
|
|
||||||
datasets: [
|
|
||||||
{ data: [], label: 'Miss (%)', backgroundColor: 'rgba(255,0,0,0.66)', borderRadius: 5 },
|
|
||||||
{ data: [], label: '50 (%)', backgroundColor: 'rgba(187,129,33,0.66)', borderRadius: 5 },
|
|
||||||
{ data: [], label: '100 (%)', backgroundColor: 'rgba(219,255,0,0.8)', borderRadius: 5 },
|
|
||||||
{ data: [], label: '300 (%)', backgroundColor: 'rgba(0,255,41,0.66)', borderRadius: 5 }
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
public barChartOptions: ChartConfiguration<'bar'>['options'] = {
|
|
||||||
responsive: true,
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
stacked: true,
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
stacked: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(private http: HttpClient,
|
constructor(private http: HttpClient,
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private title: Title
|
private title: Title
|
||||||
@ -111,20 +90,6 @@ export class ViewScoreComponent implements OnInit {
|
|||||||
this.title.setTitle(
|
this.title.setTitle(
|
||||||
`${this.replayData.username} on ${this.replayData.beatmap_title} (${this.replayData.beatmap_version})`
|
`${this.replayData.username} on ${this.replayData.beatmap_title} (${this.replayData.beatmap_version})`
|
||||||
)
|
)
|
||||||
|
|
||||||
let errorDistribution = Object.entries(this.replayData.error_distribution);
|
|
||||||
|
|
||||||
if (errorDistribution.length >= 1) {
|
|
||||||
const sortedEntries = errorDistribution
|
|
||||||
.sort((a, b) => parseInt(a[0]) - parseInt(b[0]));
|
|
||||||
|
|
||||||
const chartData = this.generateChartData(sortedEntries);
|
|
||||||
this.barChartData.labels = chartData.labels;
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
this.barChartData.datasets[i].data = chartData.datasets[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.error = error;
|
this.error = error;
|
||||||
@ -132,61 +97,6 @@ export class ViewScoreComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 defaultPercentageValues: DistributionEntry = {
|
|
||||||
percentageMiss: 0,
|
|
||||||
percentage50: 0,
|
|
||||||
percentage100: 0,
|
|
||||||
percentage300: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const entriesMap = new Map<number, DistributionEntry>(entries.map(([key, value]) => [parseInt(key), value]));
|
|
||||||
|
|
||||||
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) || { ...defaultPercentageValues };
|
|
||||||
const nextEntry = key + 1 <= range[1] ? (entriesMap.get(key + 1) || { ...defaultPercentageValues }) : defaultPercentageValues;
|
|
||||||
|
|
||||||
const sumEntry: DistributionEntry = {
|
|
||||||
percentageMiss: currentEntry.percentageMiss + nextEntry.percentageMiss,
|
|
||||||
percentage50: currentEntry.percentage50 + nextEntry.percentage50,
|
|
||||||
percentage100: currentEntry.percentage100 + nextEntry.percentage100,
|
|
||||||
percentage300: currentEntry.percentage300 + nextEntry.percentage300,
|
|
||||||
};
|
|
||||||
|
|
||||||
datasets[0].push(sumEntry.percentageMiss);
|
|
||||||
datasets[1].push(sumEntry.percentage50);
|
|
||||||
datasets[2].push(sumEntry.percentage100);
|
|
||||||
datasets[3].push(sumEntry.percentage300);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handling the case for an odd last key if needed
|
|
||||||
if (range[1] % 2 !== range[0] % 2) {
|
|
||||||
const lastEntry = entriesMap.get(range[1]) || { ...defaultPercentageValues };
|
|
||||||
labels.push(`${range[1]}ms to ${range[1] + 1}ms`);
|
|
||||||
datasets[0].push(lastEntry.percentageMiss);
|
|
||||||
datasets[1].push(lastEntry.percentage50);
|
|
||||||
datasets[2].push(lastEntry.percentage100);
|
|
||||||
datasets[3].push(lastEntry.percentage300);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { labels, datasets };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly Object = Object;
|
protected readonly Object = Object;
|
||||||
protected readonly calculateAccuracy = calculateAccuracy;
|
protected readonly calculateAccuracy = calculateAccuracy;
|
||||||
|
|||||||
@ -126,10 +126,10 @@ export class ViewUserScoreComponent implements OnInit {
|
|||||||
const datasets: number[][] = Array(4).fill(0).map(() => []);
|
const datasets: number[][] = Array(4).fill(0).map(() => []);
|
||||||
|
|
||||||
const defaultPercentageValues: DistributionEntry = {
|
const defaultPercentageValues: DistributionEntry = {
|
||||||
percentageMiss: 0,
|
countMiss: 0,
|
||||||
percentage50: 0,
|
count50: 0,
|
||||||
percentage100: 0,
|
count100: 0,
|
||||||
percentage300: 0,
|
count300: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const entriesMap = new Map<number, DistributionEntry>(entries.map(([key, value]) => [parseInt(key), value]));
|
const entriesMap = new Map<number, DistributionEntry>(entries.map(([key, value]) => [parseInt(key), value]));
|
||||||
@ -142,26 +142,26 @@ export class ViewUserScoreComponent implements OnInit {
|
|||||||
const nextEntry = key + 1 <= range[1] ? (entriesMap.get(key + 1) || { ...defaultPercentageValues }) : defaultPercentageValues;
|
const nextEntry = key + 1 <= range[1] ? (entriesMap.get(key + 1) || { ...defaultPercentageValues }) : defaultPercentageValues;
|
||||||
|
|
||||||
const sumEntry: DistributionEntry = {
|
const sumEntry: DistributionEntry = {
|
||||||
percentageMiss: currentEntry.percentageMiss + nextEntry.percentageMiss,
|
countMiss: currentEntry.countMiss + nextEntry.countMiss,
|
||||||
percentage50: currentEntry.percentage50 + nextEntry.percentage50,
|
count50: currentEntry.count50 + nextEntry.count50,
|
||||||
percentage100: currentEntry.percentage100 + nextEntry.percentage100,
|
count100: currentEntry.count100 + nextEntry.count100,
|
||||||
percentage300: currentEntry.percentage300 + nextEntry.percentage300,
|
count300: currentEntry.count300 + nextEntry.count300,
|
||||||
};
|
};
|
||||||
|
|
||||||
datasets[0].push(sumEntry.percentageMiss);
|
datasets[0].push(sumEntry.countMiss);
|
||||||
datasets[1].push(sumEntry.percentage50);
|
datasets[1].push(sumEntry.count50);
|
||||||
datasets[2].push(sumEntry.percentage100);
|
datasets[2].push(sumEntry.count100);
|
||||||
datasets[3].push(sumEntry.percentage300);
|
datasets[3].push(sumEntry.count300);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handling the case for an odd last key if needed
|
// Handling the case for an odd last key if needed
|
||||||
if (range[1] % 2 !== range[0] % 2) {
|
if (range[1] % 2 !== range[0] % 2) {
|
||||||
const lastEntry = entriesMap.get(range[1]) || { ...defaultPercentageValues };
|
const lastEntry = entriesMap.get(range[1]) || { ...defaultPercentageValues };
|
||||||
labels.push(`${range[1]}ms to ${range[1] + 1}ms`);
|
labels.push(`${range[1]}ms to ${range[1] + 1}ms`);
|
||||||
datasets[0].push(lastEntry.percentageMiss);
|
datasets[0].push(lastEntry.countMiss);
|
||||||
datasets[1].push(lastEntry.percentage50);
|
datasets[1].push(lastEntry.count50);
|
||||||
datasets[2].push(lastEntry.percentage100);
|
datasets[2].push(lastEntry.count100);
|
||||||
datasets[3].push(lastEntry.percentage300);
|
datasets[3].push(lastEntry.count300);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { labels, datasets };
|
return { labels, datasets };
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
.chart-statistics {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
list-style-type: none;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-statistics li {
|
||||||
|
display: flex;
|
||||||
|
margin-right: 120px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
<div class="main term mb-2">
|
||||||
|
<h1># hit distribution</h1>
|
||||||
|
<fieldset class="text-center">
|
||||||
|
<input type="checkbox" checked="checked" [(ngModel)]="this.removeOutliers"> remove outliers
|
||||||
|
<input type="checkbox" checked="checked" [(ngModel)]="this.groupData"> group data
|
||||||
|
<input type="checkbox" checked="checked" [(ngModel)]="this.showPercentages" (ngModelChange)="this.ngOnChanges()"> show percentages
|
||||||
|
</fieldset>
|
||||||
|
<ul class="chart-statistics">
|
||||||
|
<li *ngFor="let stat of this.calculateStatistics()">
|
||||||
|
{{ stat.name }}: <strong style="margin-left: 4px">{{ stat.value | number: '1.2-2' }}</strong>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<canvas baseChart
|
||||||
|
[data]="this.buildChartData()"
|
||||||
|
[options]="barChartOptions"
|
||||||
|
[plugins]="barChartPlugins"
|
||||||
|
[legend]="barChartLegend"
|
||||||
|
[type]="'bar'"
|
||||||
|
class="chart">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,313 @@
|
|||||||
|
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<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) {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user