diff --git a/UPDATES.md b/UPDATES.md
index 8002b0f..5b5f212 100644
--- a/UPDATES.md
+++ b/UPDATES.md
@@ -4,6 +4,10 @@ updatedAt: 2026-09-07T14:17:02+09:00
## 2026년 9월 7일
+### 그래프 시간축 갱신 수정
+
+`FIXED` 다른 곡의 상세 배치를 열 때 이전 시간 눈금과 격자가 남던 문제를 수정했습니다. 곡 변경 시 시간축·점수축·커서를 갱신하고, 모바일에서도 점수 그래프와 스킬 구간의 너비를 맞춥니다.
+
### 모든 악곡 난이도 지원
`NEW` 모든 악곡의 Easy·Normal 채보를 추가했습니다. 배치 분석과 조합 탐색에서 네 난이도를 선택할 수 있으며, 분석 기록 저장·복원과 리더 레이팅 곡 추천에도 모든 난이도를 반영합니다.
diff --git a/src/lib/components/ScoreTimelineChart.svelte b/src/lib/components/ScoreTimelineChart.svelte
index 0164de4..2497414 100644
--- a/src/lib/components/ScoreTimelineChart.svelte
+++ b/src/lib/components/ScoreTimelineChart.svelte
@@ -33,6 +33,14 @@
$: yTicks = Array.from({ length: chartMaximumScore / 500_000 + 1 }, (_, index) => index * 500_000);
$: hoveredPoint = hoverTime === null ? null : pointAtTime(points, hoverTime);
+ // A different chart invalidates the previous pointer/keyboard selection.
+ $: resetHover(notes, durationSeconds);
+
+ function resetHover(_notes: readonly NoteScorePoint[], _duration: number): void {
+ hoverTime = null;
+ tooltipPercent = 0;
+ }
+
function buildPoints(source: readonly NoteScorePoint[]): ScorePoint[] {
let averageScore = 0;
let maximumScore = 0;
@@ -45,27 +53,27 @@
return result;
}
- function x(timeSeconds: number, width = viewWidth): number {
- return LEFT + Math.max(0, Math.min(1, timeSeconds / Math.max(1, durationSeconds))) * (width - LEFT - RIGHT);
+ function x(timeSeconds: number, width: number, duration: number): number {
+ return LEFT + Math.max(0, Math.min(1, timeSeconds / Math.max(1, duration))) * (width - LEFT - RIGHT);
}
- function y(score: number, scaleMaximum = chartMaximumScore): number {
+ function y(score: number, scaleMaximum: number): number {
return TOP + (1 - score / scaleMaximum) * PLOT_HEIGHT;
}
function linePath(source: readonly ScorePoint[], field: 'averageScore' | 'maximumScore', duration: number, scaleMaximum: number, width: number): string {
if (source.length === 0) return '';
- const commands = [`M ${x(source[0].timeSeconds, width).toFixed(2)} ${y(source[0][field], scaleMaximum).toFixed(2)}`];
+ const commands = [`M ${x(source[0].timeSeconds, width, duration).toFixed(2)} ${y(source[0][field], scaleMaximum).toFixed(2)}`];
for (let index = 1; index < source.length; index += 1) {
const previous = source[index - 1];
const point = source[index];
commands.push(
- `L ${x(point.timeSeconds, width).toFixed(2)} ${y(previous[field], scaleMaximum).toFixed(2)}`,
- `L ${x(point.timeSeconds, width).toFixed(2)} ${y(point[field], scaleMaximum).toFixed(2)}`
+ `L ${x(point.timeSeconds, width, duration).toFixed(2)} ${y(previous[field], scaleMaximum).toFixed(2)}`,
+ `L ${x(point.timeSeconds, width, duration).toFixed(2)} ${y(point[field], scaleMaximum).toFixed(2)}`
);
}
const last = source.at(-1)!;
- commands.push(`L ${x(duration, width).toFixed(2)} ${y(last[field], scaleMaximum).toFixed(2)}`);
+ commands.push(`L ${x(duration, width, duration).toFixed(2)} ${y(last[field], scaleMaximum).toFixed(2)}`);
return commands.join(' ');
}
@@ -127,7 +135,7 @@
{#each yTicks as tick}
- {formatAxisScore(tick)}
+ {formatAxisScore(tick)}
{/each}
{#each timeTicks as tick}
-
+
{/each}
{#each yTicks as tick}
-
+
{/each}
{#if hoveredPoint && hoverTime !== null}
-
-
-
+
+
+
{/if}
@@ -184,7 +192,7 @@
.legend { display: flex; gap: 9px; color: #aab2bf; font-size: 10px; font-weight: 700; }
.legend span, .tooltip span { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.axis-value { position: absolute; right: 7px; color: #747f90; font-size: 10px; font-weight: 650; transform: translateY(-50%); white-space: nowrap; }
- .score-track { position: relative; flex: 1 0 auto; height: 220px; overflow: hidden; border-bottom: 1px solid #465062; background: #10141a; }
+ .score-track { position: relative; flex: 1 0 0; height: 220px; overflow: hidden; border-bottom: 1px solid #465062; background: #10141a; }
i { display: inline-block; flex: none; width: 8px; height: 8px; border-radius: 50%; }
i.average { background: #f08db8; }
i.maximum { background: #64cbe4; }
diff --git a/src/routes/analysis/+page.svelte b/src/routes/analysis/+page.svelte
index f667e48..447c7c2 100644
--- a/src/routes/analysis/+page.svelte
+++ b/src/routes/analysis/+page.svelte
@@ -842,16 +842,16 @@
return Array.from({ length: Math.floor(durationSeconds / interval) + 1 }, (_, index) => index * interval);
}
- function timelinePosition(seconds: number): number {
- return timelineDuration > 0 ? Math.max(0, Math.min(100, seconds / timelineDuration * 100)) : 0;
+ function timelinePosition(seconds: number, duration: number): number {
+ return duration > 0 ? Math.max(0, Math.min(100, seconds / duration * 100)) : 0;
}
- function skillWindowStyle(startSeconds: number, endSeconds: number): string {
- return `left: ${timelinePosition(startSeconds)}%; width: ${Math.max(0, timelinePosition(endSeconds) - timelinePosition(startSeconds))}%;`;
+ function skillWindowStyle(startSeconds: number, endSeconds: number, duration: number): string {
+ return `left: ${timelinePosition(startSeconds, duration)}%; width: ${Math.max(0, timelinePosition(endSeconds, duration) - timelinePosition(startSeconds, duration))}%;`;
}
- function timelineTrackStyle(): string {
- return `min-width: ${timelineWidth}px; --time-grid-step: ${timelinePosition(10)}%;`;
+ function timelineTrackStyle(width: number, duration: number): string {
+ return `min-width: ${width}px; --time-grid-step: ${timelinePosition(10, duration)}%;`;
}
function memberColor(cardId: string): string {
@@ -859,8 +859,8 @@
return MEMBER_COLORS[index >= 0 ? index % MEMBER_COLORS.length : 0];
}
- function coloredWindowStyle(window: SkillWindow): string {
- return `${skillWindowStyle(window.startSeconds, window.endSeconds)} --member-color: ${memberColor(window.cardId)};`;
+ function coloredWindowStyle(window: SkillWindow, duration: number): string {
+ return `${skillWindowStyle(window.startSeconds, window.endSeconds, duration)} --member-color: ${memberColor(window.cardId)};`;
}
function mergeEffectiveActiveWindows(windows: readonly EffectiveSkillWindow[]): DisplayEffectiveWindow[] {
@@ -1113,11 +1113,11 @@
{#each result.order as cardId, slot}
{slot + 1}{resultCardName(cardId)}
-
+
{#each result.activeWindows.filter((window) => window.cardId === cardId) as window}
최종 액티브
-
+
{#each displayedEffectiveActiveWindows as window}
1 ? ` · 최대 ${window.maxOverlapCount}명 중 가장 높은 효과` : ''}`}
role="img"
aria-label={`${resultCardName(window.cardId)} 액티브가 ${formatSeconds(window.startSeconds)}부터 ${formatSeconds(window.endSeconds)}까지 적용`}
@@ -1142,11 +1142,11 @@
스페셜
-
+
{#each result.specialWindows as window}
-
- {#each timelineTicks as second}
{second}초{/each}
+
+ {#each timelineTicks as second}{second}초{/each}