import {Component, OnInit, ViewChild} from '@angular/core'; import {ChartConfiguration} from 'chart.js'; 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} from "../replays"; import {calculateAccuracy} from "../format"; import {Title} from "@angular/platform-browser"; import {OsuGradeComponent} from "../../corelib/components/osu-grade/osu-grade.component"; import {ChartComponent} from "../../corelib/components/chart/chart.component"; import {ReplayViewerComponent} from "../../corelib/components/replay-viewer/replay-viewer.component"; @Component({ selector: 'app-view-score', standalone: true, imports: [ NgChartsModule, JsonPipe, NgIf, DecimalPipe, NgForOf, NgOptimizedImage, RouterLink, OsuGradeComponent, ChartComponent, ReplayViewerComponent ], 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; } private loadScoreData(): void { this.isLoading = true; this.http.get(`${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; for (let i = 0; i < 4; i++) { this.barChartData.datasets[i].data = chartData.datasets[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); 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(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 calculateAccuracy = calculateAccuracy; }