add overing in stats page

This commit is contained in:
Davide Scaini
2026-03-29 11:20:48 +02:00
parent 7327861c4a
commit 5647e52865
2 changed files with 103 additions and 24 deletions
+78 -14
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { ActivitySummary, BASIndex, Sport } from '../lib/types';
import { formatDistance, sportIcon, sportColor, sportLabel } from '../lib/format';
import { formatDistance, formatDuration, sportIcon, sportColor, sportLabel } from '../lib/format';
let all: ActivitySummary[] = [];
let sport: Sport | 'all' = 'all';
@@ -16,6 +16,46 @@
$: activities = sport === 'all' ? all : all.filter(a => a.sport === sport);
// ── Tooltip ───────────────────────────────────────────────────────────────
$: activitiesByDate = (() => {
const m = new Map<string, ActivitySummary[]>();
for (const a of activities) {
const d = a.started_at.slice(0, 10);
if (!m.has(d)) m.set(d, []);
m.get(d)!.push(a);
}
return m;
})();
let hoveredDate: string | null = null;
let tooltipPos = { x: 0, y: 0 };
let hideTimer: ReturnType<typeof setTimeout> | null = null;
$: tooltipActivities = hoveredDate ? (activitiesByDate.get(hoveredDate) ?? []) : [];
function onCellEnter(date: string, e: MouseEvent) {
if (!date || !activitiesByDate.has(date)) return;
if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
hoveredDate = date;
const vw = window.innerWidth;
const vh = window.innerHeight;
tooltipPos = {
x: e.clientX > vw - 310 ? e.clientX - 305 : e.clientX + 14,
y: Math.min(e.clientY - 8, vh - 260),
};
}
function onCellLeave() {
hideTimer = setTimeout(() => { hoveredDate = null; }, 120);
}
function onTooltipEnter() {
if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
}
function onTooltipLeave() {
hoveredDate = null;
}
// ── Heatmap data ─────────────────────────────────────────────────────────
// byDateBySport: date → sport → total distance (m)
$: byDateBySport = (() => {
@@ -133,16 +173,6 @@
const DOW = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
function cellTitle(date: string): string {
if (!date) return '';
const sportMap = byDateBySport.get(date);
if (!sportMap) return date;
const parts = [...sportMap.entries()]
.sort((a, b) => b[1] - a[1])
.map(([sp, dist]) => `${sportIcon(sp as Sport)} ${formatDistance(dist)}`);
return `${date}\n${parts.join(' ')}`;
}
const sports: Array<{ value: Sport | 'all'; label: string }> = [
{ value: 'all', label: 'All' },
{ value: 'cycling', label: '🚴 Cycling' },
@@ -198,7 +228,7 @@
{@const weeks = getWeeks(year)}
{@const yt = totalsByYear.get(year)}
{#if yt}
<div class="mb-8">
<div class="mb-8" role="presentation">
<div class="flex items-baseline gap-3 mb-2">
<h2 class="text-lg font-semibold text-white">{year}</h2>
<span class="text-sm text-zinc-400">
@@ -232,9 +262,10 @@
</div>
{#each week as date}
<div
class="w-[10px] h-[10px] rounded-[2px]"
class="w-[10px] h-[10px] rounded-[2px] {date && activitiesByDate.has(date) ? 'cursor-pointer' : ''}"
style="background:{cellColors.get(date) ?? '#27272a'}"
title={cellTitle(date)}
on:mouseenter={e => onCellEnter(date, e)}
on:mouseleave={onCellLeave}
/>
{/each}
</div>
@@ -267,3 +298,36 @@
{/each}
{/if}
<!-- Day tooltip -->
{#if hoveredDate && tooltipActivities.length > 0}
<div
class="fixed z-50 bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl p-3 w-[280px]"
style="left:{tooltipPos.x}px; top:{tooltipPos.y}px"
on:mouseenter={onTooltipEnter}
on:mouseleave={onTooltipLeave}
>
<p class="text-xs font-medium text-zinc-400 mb-2">
{new Date(hoveredDate + 'T12:00:00').toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })}
</p>
<div class="flex flex-col gap-1">
{#each tooltipActivities as a}
<a
href="{import.meta.env.BASE_URL}activity/{a.id}/"
class="flex flex-col gap-0.5 rounded-lg px-2 py-1.5 hover:bg-zinc-800 transition-colors"
>
<span class="text-sm font-medium text-white truncate">
{sportIcon(a.sport)} {a.title}
</span>
<span class="text-xs text-zinc-400">
{formatDistance(a.distance_m)}
{#if a.moving_time_s ?? a.duration_s}
· {formatDuration(a.moving_time_s ?? a.duration_s)}
{/if}
<span class="ml-1" style="color:{sportColor(a.sport)}">{sportLabel(a.sport, a.sub_sport)}</span>
</span>
</a>
{/each}
</div>
</div>
{/if}