map now working
This commit is contained in:
@@ -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 (Mon–Sun)
|
||||
// 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}
|
||||
Reference in New Issue
Block a user