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

212 lines
6.7 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();
}
});
}
buildChartData(chart: ReplayDataChart): ChartData<"bar", DefaultDataPoint<"bar">, any> {
const sortedData = chart.data.sort((a, b) => a.first - b.first);
const minFirst = Math.floor(sortedData[0].first / 2) * 2; // Round down to nearest even number
const maxFirst = Math.ceil(sortedData[sortedData.length - 1].first / 2) * 2; // Round up to nearest even number
const groupRanges = Array.from({length: (maxFirst - minFirst) / 2 + 1}, (_, i) => minFirst + i * 2);
const 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 };
});
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 };
}
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;
}
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]));
this.barChartData.labels = this.generateLabelsFromEntries(sortedEntries);
for (let i = 0; i < 4; i++) {
this.barChartData.datasets[i].data = this.generateChartDataFromEntries(sortedEntries, i);
}
}
},
error: (error) => {
this.error = error;
}
});
}
private getChartRange(entries: [string, DistributionEntry][]): [number, number] {
const keys = entries.map(([key, _]) => parseInt(key));
const minKey = Math.min(...keys);
const maxKey = Math.max(...keys);
const absoluteMax = Math.max(Math.abs(minKey), Math.abs(maxKey));
return [absoluteMax * -1, absoluteMax];
}
private generateLabelsFromEntries(entries: [string, DistributionEntry][]): string[] {
const range = this.getChartRange(entries);
const entriesMap = new Map(entries.map(([key, value]) => [parseInt(key), value]));
const filledEntries = [];
for (let key = range[0]; key <= range[1]; key++) {
filledEntries.push([key.toString(), entriesMap.get(key) || 0]);
}
return filledEntries.map(([key, _]) => {
const start = parseInt(String(key));
const end = start + 2;
return `${start}ms to ${end}ms`;
2024-02-14 16:43:11 +00:00
});
}
private generateChartDataFromEntries(entries: [string, DistributionEntry][], i: number): number[] {
const range = this.getChartRange(entries);
const defaultPercentageValues: DistributionEntry = {
percentageMiss: 0,
percentage50: 0,
percentage100: 0,
percentage300: 0,
};
const entriesMap = new Map<number, DistributionEntry>(entries.map(([key, value]) => [parseInt(key), value]));
const filledEntries: [number, DistributionEntry][] = [];
for (let key = range[0]; key <= range[1]; key++) {
filledEntries.push([key, entriesMap.get(key) || { ...defaultPercentageValues }]);
}
const propertyMap = ['percentageMiss', 'percentage50', 'percentage100', 'percentage300'];
if (i >= 0 && i < propertyMap.length) {
return filledEntries.map(([, value]) => value[propertyMap[i] as keyof DistributionEntry]);
}
return [];
}
protected readonly Object = Object;
protected readonly calculateAccuracy = calculateAccuracy;
}