nise/nise-frontend/src/app/view-score/view-score.component.ts

259 lines
8.6 KiB
TypeScript
Raw Normal View History

2024-02-14 16:43:11 +00:00
import {Component, OnInit, ViewChild} from '@angular/core';
import {ChartConfiguration, ChartData, DefaultDataPoint} from 'chart.js';
2024-02-14 16:43:11 +00:00
import {BaseChartDirective, NgChartsModule} from 'ng2-charts';
import {HttpClient} from "@angular/common/http";
import {environment} from "../../environments/environment";
import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common";
import {ActivatedRoute, RouterLink} from "@angular/router";
import {catchError, throwError} from "rxjs";
import {DistributionEntry, ReplayData, ReplayDataChart} from "../replays";
2024-02-14 16:43:11 +00:00
import {calculateAccuracy} from "../format";
import {Title} from "@angular/platform-browser";
import {OsuGradeComponent} from "../../corelib/components/osu-grade/osu-grade.component";
@Component({
selector: 'app-view-score',
standalone: true,
imports: [
NgChartsModule,
JsonPipe,
NgIf,
DecimalPipe,
NgForOf,
NgOptimizedImage,
RouterLink,
OsuGradeComponent
],
templateUrl: './view-score.component.html',
styleUrl: './view-score.component.css'
})
export class ViewScoreComponent implements OnInit {
@ViewChild(BaseChartDirective)
public chart!: BaseChartDirective;
isLoading = false;
error: string | null = null;
replayData: ReplayData | 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,
private activatedRoute: ActivatedRoute,
private title: Title
) {}
ngOnInit(): void {
this.activatedRoute.params.subscribe(params => {
this.replayId = params['replayId'];
if (this.replayId) {
this.loadScoreData();
}
});
}
buildCircleguardUrl(): string {
if(!this.replayData) {
return "";
}
let url = "circleguard://m=" + this.replayData.beatmap_id + "&u=" + this.replayData.user_id;
if (this.replayData.mods.length > 0) {
url += "&m1=" + this.replayData.mods.join('');
}
return url;
}
calculateStatistics(data: Array<{ first: number, second: number }>): Array<{ name: string, value: number }> {
if (data.length === 0) return [];
const sortedData = data.sort((a, b) => a.first - b.first);
let mean = data.reduce((acc, curr) => acc + curr.first, 0) / data.length;
let median = data.length % 2 === 0 ? (sortedData[data.length / 2 - 1].first + sortedData[data.length / 2].first) / 2 : sortedData[Math.floor(data.length / 2)].first;
let variance = data.reduce((acc, curr) => acc + Math.pow(curr.first - mean, 2), 0) / (data.length - 1);
let stdDev = Math.sqrt(variance);
let min = sortedData[0].first;
let max = sortedData[sortedData.length - 1].first;
const statistics = {
'mean': mean,
'median': median,
'variance': variance,
'std. dev': stdDev,
'min': min,
'max': max
};
return Object.entries(statistics).map(([name, value]) => ({ name, value }));
}
buildChartData(chart: ReplayDataChart): ChartData<"bar", DefaultDataPoint<"bar">, any> {
let sortedData = chart.data.sort((a, b) => a.first - b.first);
const mean = sortedData.reduce((acc, curr) => acc + curr.first, 0) / sortedData.length;
const stdDev = Math.sqrt(sortedData.reduce((acc, curr) => acc + Math.pow(curr.first - mean, 2), 0) / sortedData.length);
sortedData = sortedData.filter(item => {
const zScore = (item.first - mean) / stdDev;
return Math.abs(zScore) <= 4;
});
const minFirst = Math.floor(sortedData[0].first / 2) * 2;
const maxFirst = Math.ceil(sortedData[sortedData.length - 1].first / 2) * 2;
const groupRanges = Array.from({length: (maxFirst - minFirst) / 2 + 1}, (_, i) => minFirst + i * 2);
let groupedData = groupRanges.map(rangeStart => {
const rangeEnd = rangeStart + 2;
const entriesInGroup = sortedData.filter(e => e.first >= rangeStart && e.first < rangeEnd);
const sumSecond = entriesInGroup.reduce((acc, curr) => acc + curr.second, 0);
return { first: rangeStart, second: sumSecond };
});
for (let i = groupedData.length - 1; i >= 0; i--) {
if (groupedData[i].second === 0 && (i === groupedData.length - 1 || groupedData[i + 1].second === 0)) {
groupedData.pop();
} else {
break;
}
}
const labels = groupedData.map(({ first }) => `${first}ms to ${first + 2}ms`);
const datasets = [{
data: groupedData.map(({ second }) => second),
label: chart.title + " (%)",
backgroundColor: 'rgba(0,255,41,0.66)',
borderRadius: 5
}];
return { labels, datasets };
}
2024-02-14 16:43:11 +00:00
private loadScoreData(): void {
this.isLoading = true;
this.http.get<ReplayData>(`${environment.apiUrl}/score/${this.replayId}`).pipe(
catchError(error => {
this.isLoading = false;
return throwError(() => new Error('An error occurred with the request: ' + error.message));
})
).subscribe({
next: (response) => {
this.isLoading = false;
this.replayData = response;
this.title.setTitle(
`${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;
2024-02-14 16:43:11 +00:00
for (let i = 0; i < 4; i++) {
this.barChartData.datasets[i].data = chartData.datasets[i];
2024-02-14 16:43:11 +00:00
}
}
},
error: (error) => {
this.error = error;
}
});
}
private getChartRange(entries: [string, DistributionEntry][]): [number, number] {
const keys = entries.map(([key, _]) => parseInt(key));
2024-02-14 16:43:11 +00:00
const minKey = Math.min(...keys);
const maxKey = Math.max(...keys);
return [minKey, maxKey];
2024-02-14 16:43:11 +00:00
}
private generateChartData(entries: [string, DistributionEntry][]): { labels: string[], datasets: number[][] } {
2024-02-14 16:43:11 +00:00
const range = this.getChartRange(entries);
const labels: string[] = [];
const datasets: number[][] = Array(4).fill(0).map(() => []);
2024-02-14 16:43:11 +00:00
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);
2024-02-14 16:43:11 +00:00
}
// 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);
}
2024-02-14 16:43:11 +00:00
return { labels, datasets };
2024-02-14 16:43:11 +00:00
}
protected readonly Object = Object;
protected readonly calculateAccuracy = calculateAccuracy;
2024-02-14 16:43:11 +00:00
}