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"; @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(); } }); } 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])); 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} to ${end}ms`; }); } 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(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; }