feat: show cumulative maximum score probability in timeline tooltip
Publish GitHub Pages output / publish (push) Successful in 56s

This commit is contained in:
2026-09-10 14:40:07 +09:00
parent b1b99e6cc0
commit 1fa15cbe08
5 changed files with 88 additions and 13 deletions
+16 -7
View File
@@ -2,6 +2,7 @@
import type { NoteScorePoint } from '$lib/engine/index.ts';
export let notes: readonly NoteScorePoint[];
export let maximumScoreProbabilities: readonly number[] = [];
export let durationSeconds: number;
export let minimumWidth = 720;
export let timeTicks: readonly number[] = [];
@@ -10,6 +11,7 @@
timeSeconds: number;
averageScore: number;
maximumScore: number;
maximumProbability?: number;
};
const HEIGHT = 220;
@@ -25,7 +27,7 @@
$: viewWidth = Math.max(320, chartWidth);
$: plotWidth = viewWidth - LEFT - RIGHT;
$: points = buildPoints(notes);
$: points = buildPoints(notes, maximumScoreProbabilities);
$: finalMaximumScore = Math.max(1, points.at(-1)?.maximumScore ?? 0);
$: chartMaximumScore = Math.max(500_000, Math.ceil(finalMaximumScore / 500_000) * 500_000);
$: averagePath = linePath(points, 'averageScore', durationSeconds, chartMaximumScore, viewWidth);
@@ -41,14 +43,14 @@
tooltipPercent = 0;
}
function buildPoints(source: readonly NoteScorePoint[]): ScorePoint[] {
function buildPoints(source: readonly NoteScorePoint[], probabilities: readonly number[]): ScorePoint[] {
let averageScore = 0;
let maximumScore = 0;
const result: ScorePoint[] = [{ timeSeconds: 0, averageScore: 0, maximumScore: 0 }];
for (const note of source) {
const result: ScorePoint[] = [{ timeSeconds: 0, averageScore: 0, maximumScore: 0, maximumProbability: 1 }];
for (const [index, note] of source.entries()) {
averageScore += note.expectedScore;
maximumScore += note.theoreticalMaxScore;
result.push({ timeSeconds: note.timeSeconds, averageScore, maximumScore });
result.push({ timeSeconds: note.timeSeconds, averageScore, maximumScore, maximumProbability: probabilities[index] });
}
return result;
}
@@ -114,6 +116,12 @@
return Math.round(score).toLocaleString('ko-KR');
}
function formatProbability(probability: number | undefined): string {
if (probability === undefined) return '—';
if (probability > 0 && probability < 0.000001) return '<0.0001%';
return `${(probability * 100).toLocaleString('ko-KR', { maximumFractionDigits: probability < 0.0001 ? 4 : 2 })}%`;
}
function formatAxisScore(score: number): string {
return score === 0 ? '0' : `${Math.round(score / 10_000).toLocaleString('ko-KR')}만`;
}
@@ -147,7 +155,7 @@
aria-valuemin="0"
aria-valuemax={Math.ceil(durationSeconds)}
aria-valuenow={Math.round(hoverTime ?? 0)}
aria-valuetext={hoveredPoint && hoverTime !== null ? `${formatTime(hoverTime)}, 평균 점수 ${formatScore(hoveredPoint.averageScore)}, 최고 점수 ${formatScore(hoveredPoint.maximumScore)}` : '방향키로 시각을 선택하세요'}
aria-valuetext={hoveredPoint && hoverTime !== null ? `${formatTime(hoverTime)}, 평균 점수 ${formatScore(hoveredPoint.averageScore)}, 최고 점수 ${formatScore(hoveredPoint.maximumScore)}, 이 시간까지 최고점 유지 확률 ${formatProbability(hoveredPoint.maximumProbability)}` : '방향키로 시각을 선택하세요'}
tabindex="0"
on:pointermove={onPointerMove}
on:pointerleave={() => hoverTime = null}
@@ -176,10 +184,11 @@
</svg>
{#if hoveredPoint && hoverTime !== null}
<div class:align-right={tooltipPercent > 68} class="tooltip" style={`left: ${tooltipPercent}%;`} role="status">
<div class:align-right={tooltipPercent > 50} class="tooltip" style={`left: ${tooltipPercent}%;`} role="status">
<strong>{formatTime(hoverTime)}</strong>
<span><i class="average"></i>평균 점수 <b>{formatScore(hoveredPoint.averageScore)}</b></span>
<span><i class="maximum"></i>최고 점수 <b>{formatScore(hoveredPoint.maximumScore)}</b></span>
<span>이 시간까지 최고점 유지 확률 <b>{formatProbability(hoveredPoint.maximumProbability)}</b></span>
</div>
{/if}
</div>