diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt index 312a036..b592947 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt @@ -235,8 +235,8 @@ data class ReplayData( } data class DistributionEntry( - val percentageMiss: Double, - val percentage300: Double, - val percentage100: Double, - val percentage50: Double + val countMiss: Double, + val count300: Double, + val count100: Double, + val count50: Double ) \ No newline at end of file diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt index ff774e5..2f31e0f 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt @@ -538,7 +538,7 @@ class ScoreService( var totalHits = 0 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 errorDistribution.getOrPut(error) { mutableMapOf("MISS" to 0, "THREE_HUNDRED" to 0, "ONE_HUNDRED" to 0, "FIFTY" to 0) } .apply { @@ -550,10 +550,10 @@ class ScoreService( return errorDistribution.mapValues { (_, judgementCounts) -> judgementCounts.values.sum() DistributionEntry( - percentageMiss = (judgementCounts.getOrDefault("MISS", 0).toDouble() / totalHits) * 100, - percentage300 = (judgementCounts.getOrDefault("THREE_HUNDRED", 0).toDouble() / totalHits) * 100, - percentage100 = (judgementCounts.getOrDefault("ONE_HUNDRED", 0).toDouble() / totalHits) * 100, - percentage50 = (judgementCounts.getOrDefault("FIFTY", 0).toDouble() / totalHits) * 100 + countMiss = judgementCounts.getOrDefault("MISS", 0).toDouble(), + count300 = judgementCounts.getOrDefault("THREE_HUNDRED", 0).toDouble(), + count100 = judgementCounts.getOrDefault("ONE_HUNDRED", 0).toDouble(), + count50 = judgementCounts.getOrDefault("FIFTY", 0).toDouble() ) } } @@ -567,7 +567,7 @@ class ScoreService( var totalHits = 0 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 errorDistribution.getOrPut(error) { mutableMapOf("Miss" to 0, "300" to 0, "100" to 0, "50" to 0) } .apply { @@ -579,10 +579,10 @@ class ScoreService( return errorDistribution.mapValues { (_, judgementCounts) -> judgementCounts.values.sum() DistributionEntry( - percentageMiss = (judgementCounts.getOrDefault("Miss", 0).toDouble() / totalHits) * 100, - percentage300 = (judgementCounts.getOrDefault("300", 0).toDouble() / totalHits) * 100, - percentage100 = (judgementCounts.getOrDefault("100", 0).toDouble() / totalHits) * 100, - percentage50 = (judgementCounts.getOrDefault("50", 0).toDouble() / totalHits) * 100 + countMiss = judgementCounts.getOrDefault("MISS", 0).toDouble(), + count300 = judgementCounts.getOrDefault("THREE_HUNDRED", 0).toDouble(), + count100 = judgementCounts.getOrDefault("ONE_HUNDRED", 0).toDouble(), + count50 = judgementCounts.getOrDefault("FIFTY", 0).toDouble() ) } } diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserScoreService.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserScoreService.kt index 6ad5837..93d225e 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserScoreService.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserScoreService.kt @@ -207,10 +207,10 @@ class UserScoreService( return errorDistribution.mapValues { (_, judgementCounts) -> judgementCounts.values.sum() DistributionEntry( - percentageMiss = (judgementCounts.getOrDefault("MISS", 0).toDouble() / totalHits) * 100, - percentage300 = (judgementCounts.getOrDefault("THREE_HUNDRED", 0).toDouble() / totalHits) * 100, - percentage100 = (judgementCounts.getOrDefault("ONE_HUNDRED", 0).toDouble() / totalHits) * 100, - percentage50 = (judgementCounts.getOrDefault("FIFTY", 0).toDouble() / totalHits) * 100 + countMiss = judgementCounts.getOrDefault("MISS", 0).toDouble(), + count300 = judgementCounts.getOrDefault("THREE_HUNDRED", 0).toDouble(), + count100 = judgementCounts.getOrDefault("ONE_HUNDRED", 0).toDouble(), + count50 = judgementCounts.getOrDefault("FIFTY", 0).toDouble() ) } } diff --git a/nise-frontend/src/app/replays.ts b/nise-frontend/src/app/replays.ts index 1c88de4..d0528d8 100644 --- a/nise-frontend/src/app/replays.ts +++ b/nise-frontend/src/app/replays.ts @@ -168,10 +168,10 @@ export interface ReplayData { } export interface DistributionEntry { - percentageMiss: number; - percentage300: number; - percentage100: number; - percentage50: number; + countMiss: number; + count300: number; + count100: number; + count50: number; } export interface ErrorDistribution { diff --git a/nise-frontend/src/app/view-score/view-score.component.html b/nise-frontend/src/app/view-score/view-score.component.html index 869623d..0cb5f93 100644 --- a/nise-frontend/src/app/view-score/view-score.component.html +++ b/nise-frontend/src/app/view-score/view-score.component.html @@ -222,15 +222,7 @@ -
-

# hit distribution

- - -
+ + + diff --git a/nise-frontend/src/app/view-score/view-score.component.ts b/nise-frontend/src/app/view-score/view-score.component.ts index 10c4952..2f222ff 100644 --- a/nise-frontend/src/app/view-score/view-score.component.ts +++ b/nise-frontend/src/app/view-score/view-score.component.ts @@ -11,6 +11,9 @@ 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 { + ChartHitDistributionComponent +} from "../../corelib/components/chart-hit-distribution/chart-hit-distribution.component"; @Component({ selector: 'app-view-score', @@ -24,7 +27,8 @@ import {ChartComponent} from "../../corelib/components/chart/chart.component"; NgOptimizedImage, RouterLink, OsuGradeComponent, - ChartComponent + ChartComponent, + ChartHitDistributionComponent ], templateUrl: './view-score.component.html', styleUrl: './view-score.component.css' @@ -39,31 +43,6 @@ export class ViewScoreComponent implements OnInit { 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 @@ -111,20 +90,6 @@ export class ViewScoreComponent implements OnInit { 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; @@ -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(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; diff --git a/nise-frontend/src/app/view-user-score/view-user-score.component.ts b/nise-frontend/src/app/view-user-score/view-user-score.component.ts index e3c35b6..e3203f9 100644 --- a/nise-frontend/src/app/view-user-score/view-user-score.component.ts +++ b/nise-frontend/src/app/view-user-score/view-user-score.component.ts @@ -126,10 +126,10 @@ export class ViewUserScoreComponent implements OnInit { const datasets: number[][] = Array(4).fill(0).map(() => []); const defaultPercentageValues: DistributionEntry = { - percentageMiss: 0, - percentage50: 0, - percentage100: 0, - percentage300: 0, + countMiss: 0, + count50: 0, + count100: 0, + count300: 0, }; const entriesMap = new Map(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 sumEntry: DistributionEntry = { - percentageMiss: currentEntry.percentageMiss + nextEntry.percentageMiss, - percentage50: currentEntry.percentage50 + nextEntry.percentage50, - percentage100: currentEntry.percentage100 + nextEntry.percentage100, - percentage300: currentEntry.percentage300 + nextEntry.percentage300, + countMiss: currentEntry.countMiss + nextEntry.countMiss, + count50: currentEntry.count50 + nextEntry.count50, + count100: currentEntry.count100 + nextEntry.count100, + count300: currentEntry.count300 + nextEntry.count300, }; - datasets[0].push(sumEntry.percentageMiss); - datasets[1].push(sumEntry.percentage50); - datasets[2].push(sumEntry.percentage100); - datasets[3].push(sumEntry.percentage300); + datasets[0].push(sumEntry.countMiss); + datasets[1].push(sumEntry.count50); + datasets[2].push(sumEntry.count100); + datasets[3].push(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]) || { ...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); + datasets[0].push(lastEntry.countMiss); + datasets[1].push(lastEntry.count50); + datasets[2].push(lastEntry.count100); + datasets[3].push(lastEntry.count300); } return { labels, datasets }; diff --git a/nise-frontend/src/corelib/components/chart-hit-distribution/chart-hit-distribution.component.css b/nise-frontend/src/corelib/components/chart-hit-distribution/chart-hit-distribution.component.css new file mode 100644 index 0000000..0c9372a --- /dev/null +++ b/nise-frontend/src/corelib/components/chart-hit-distribution/chart-hit-distribution.component.css @@ -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; +} diff --git a/nise-frontend/src/corelib/components/chart-hit-distribution/chart-hit-distribution.component.html b/nise-frontend/src/corelib/components/chart-hit-distribution/chart-hit-distribution.component.html new file mode 100644 index 0000000..2196677 --- /dev/null +++ b/nise-frontend/src/corelib/components/chart-hit-distribution/chart-hit-distribution.component.html @@ -0,0 +1,21 @@ +
+

# hit distribution

+
+ remove outliers + group data + show percentages +
+
    +
  • + {{ stat.name }}: {{ stat.value | number: '1.2-2' }} +
  • +
+ + +
diff --git a/nise-frontend/src/corelib/components/chart-hit-distribution/chart-hit-distribution.component.ts b/nise-frontend/src/corelib/components/chart-hit-distribution/chart-hit-distribution.component.ts new file mode 100644 index 0000000..b797f9d --- /dev/null +++ b/nise-frontend/src/corelib/components/chart-hit-distribution/chart-hit-distribution.component.ts @@ -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(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 }; + } + + +}