map now working

This commit is contained in:
Davide Scaini
2026-03-28 19:34:22 +01:00
parent 5d58126d2f
commit 3441079913
18 changed files with 1489 additions and 10 deletions
+179
View File
@@ -0,0 +1,179 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { ActivitySummary, BASIndex, Sport } from '../lib/types';
import { formatDistance, sportIcon } from '../lib/format';
let activities: ActivitySummary[] = [];
let loading = true;
onMount(async () => {
const res = await fetch(`${import.meta.env.BASE_URL}data/index.json`);
const index: BASIndex = await res.json();
activities = index.activities.filter(a => a.privacy !== 'private' && a.distance_m);
loading = false;
});
// ── Heatmap ───────────────────────────────────────────────────────────────
// Build a map: dateString → total distance (m)
$: byDate = (() => {
const m = new Map<string, number>();
for (const a of activities) {
const d = a.started_at.slice(0, 10); // YYYY-MM-DD
m.set(d, (m.get(d) ?? 0) + (a.distance_m ?? 0));
}
return m;
})();
// Current year and prior 3 years to show
const now = new Date();
const years = [now.getFullYear(), now.getFullYear()-1, now.getFullYear()-2, now.getFullYear()-3];
function getWeeks(year: number): string[][] {
// Returns array of weeks, each week is array of 7 date strings (MonSun)
// Pad with empty strings at start/end
const jan1 = new Date(year, 0, 1);
const dec31 = new Date(year, 11, 31);
// Align to Monday
const start = new Date(jan1);
start.setDate(jan1.getDate() - ((jan1.getDay() + 6) % 7));
const end = new Date(dec31);
end.setDate(dec31.getDate() + (6 - (dec31.getDay() + 6) % 7));
const weeks: string[][] = [];
let cur = new Date(start);
while (cur <= end) {
const week: string[] = [];
for (let d = 0; d < 7; d++) {
const iso = cur.toISOString().slice(0, 10);
week.push(cur.getFullYear() === year ? iso : '');
cur.setDate(cur.getDate() + 1);
}
weeks.push(week);
}
return weeks;
}
function cellColor(date: string): string {
if (!date) return 'transparent';
const km = (byDate.get(date) ?? 0) / 1000;
if (km === 0) return '#27272a'; // zinc-800
if (km < 20) return '#0e4c5a';
if (km < 50) return '#0a6e82';
if (km < 80) return '#0891b2'; // cyan-600
if (km < 120) return '#06b6d4'; // cyan-500
return '#00c8ff';
}
// ── Totals ────────────────────────────────────────────────────────────────
$: totalsByYear = (() => {
const m = new Map<number, { dist: number; count: number }>();
for (const a of activities) {
const y = new Date(a.started_at).getFullYear();
const cur = m.get(y) ?? { dist: 0, count: 0 };
cur.dist += a.distance_m ?? 0;
cur.count += 1;
m.set(y, cur);
}
return m;
})();
$: allYears = [...totalsByYear.keys()].sort((a, b) => b - a);
const DOW = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
function monthLabels(weeks: string[][]): Array<{month:string;col:number}> {
const seen = new Set<string>();
return weeks.flatMap((week, i) => {
const day = week.find(d => d);
if (!day) return [];
const m = MONTHS[parseInt(day.slice(5, 7)) - 1];
if (seen.has(m)) return [];
seen.add(m);
return [{ month: m, col: i }];
});
}
</script>
{#if loading}
<div class="h-64 rounded-xl bg-zinc-800 animate-pulse mb-6" />
{:else}
<!-- Year totals -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8">
{#each allYears.slice(0, 4) as year}
{@const t = totalsByYear.get(year)}
<div class="bg-zinc-900 rounded-xl border border-zinc-800 p-4">
<p class="text-xs text-zinc-500 mb-1">{year}</p>
<p class="text-2xl font-bold text-white">{formatDistance(t?.dist ?? 0)}</p>
<p class="text-sm text-zinc-400">{t?.count ?? 0} activities</p>
</div>
{/each}
</div>
<!-- Heatmaps per year -->
{#each years as year}
{@const weeks = getWeeks(year)}
{@const labels = monthLabels(weeks)}
{@const yt = totalsByYear.get(year)}
{#if yt}
<div class="mb-8">
<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">
{formatDistance(yt.dist)} · {yt.count} activities
</span>
</div>
<div class="overflow-x-auto">
<div class="inline-block">
<!-- Month labels -->
<div class="flex mb-1 ml-6">
{#each labels as { month, col }}
<span
class="text-xs text-zinc-500 absolute"
style="left: calc({col} * 13px)"
>{month}</span>
{/each}
<!-- spacer to set width -->
<div style="width:{weeks.length * 13}px" />
</div>
<!-- Grid -->
<div class="flex gap-[3px]">
<!-- Day-of-week labels -->
<div class="flex flex-col gap-[3px] mr-1">
{#each DOW as d, i}
<span class="text-[9px] text-zinc-600 h-[10px] leading-[10px] w-3 text-right">
{i % 2 === 1 ? d : ''}
</span>
{/each}
</div>
<!-- Weeks -->
{#each weeks as week}
<div class="flex flex-col gap-[3px]">
{#each week as date}
<div
class="w-[10px] h-[10px] rounded-[2px]"
style="background:{cellColor(date)}"
title={date ? `${date}: ${formatDistance(byDate.get(date) ?? 0)}` : ''}
/>
{/each}
</div>
{/each}
</div>
</div>
</div>
<!-- Legend -->
<div class="flex items-center gap-1 mt-2">
<span class="text-xs text-zinc-500 mr-1">Less</span>
{#each ['#27272a','#0e4c5a','#0a6e82','#0891b2','#06b6d4','#00c8ff'] as c}
<div class="w-[10px] h-[10px] rounded-[2px]" style="background:{c}" />
{/each}
<span class="text-xs text-zinc-500 ml-1">More</span>
</div>
</div>
{/if}
{/each}
{/if}