Activity detail: layout refactor + GPS-derived speed for map coloring
Layout: map + charts stacked left, stats panel (2-col) on the right. Cadence moved to last stat. Charts sit directly below the map. Speed coloring: most FIT files don't record per-second speed, leaving timeseries speed_kmh all-null and the hover link dead. Fix: derive speed from consecutive GPS coordinates (haversine + 5-pt moving average) when the device didn't record it. Add --backfill-speed render flag to retrofit existing timeseries files.
This commit is contained in:
@@ -235,11 +235,11 @@
|
||||
stat('Max speed', formatSpeed(activity.max_speed_kmh), 'speed'),
|
||||
stat('Avg HR', activity.avg_hr_bpm ? `${activity.avg_hr_bpm} bpm` : '—', 'heart_rate'),
|
||||
stat('Max HR', activity.max_hr_bpm ? `${activity.max_hr_bpm} bpm` : '—', 'heart_rate'),
|
||||
stat('Cadence', activity.avg_cadence_rpm ? `${activity.avg_cadence_rpm} rpm` : '—', 'cadence'),
|
||||
...(activity.avg_power_w != null ? [
|
||||
stat('Avg power', `${activity.avg_power_w} W`, 'power'),
|
||||
stat('NP', npPower != null ? `${npPower} W` : '—', 'power'),
|
||||
] : []),
|
||||
stat('Cadence', activity.avg_cadence_rpm ? `${activity.avg_cadence_rpm} rpm` : '—', 'cadence'),
|
||||
].filter(s => !s.key || !hiddenStats.has(s.key));
|
||||
</script>
|
||||
|
||||
@@ -375,37 +375,50 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Map + Stats split -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-4 mb-4">
|
||||
<!-- Map -->
|
||||
<div class="relative h-[400px] lg:h-[420px] rounded-xl overflow-hidden bg-zinc-800">
|
||||
{#if trackUrl}
|
||||
<ActivityMap
|
||||
{trackUrl}
|
||||
{timeseries}
|
||||
bbox={detail?.bbox ?? null}
|
||||
initialCoords={activity.preview_coords}
|
||||
accentColor={color}
|
||||
{colorMode}
|
||||
bind:hoveredIdx
|
||||
/>
|
||||
{#if legendInfo}
|
||||
<div class="absolute bottom-3 left-3 flex items-center gap-2 bg-zinc-900/80 backdrop-blur-sm rounded-lg px-2.5 py-1.5 text-xs pointer-events-none select-none">
|
||||
<span class="text-blue-400 tabular-nums">{legendInfo.min}</span>
|
||||
<div class="w-14 h-1.5 rounded-full flex-shrink-0" style="background:linear-gradient(to right,#3b82f6,#4ade80,#facc15,#ef4444)"></div>
|
||||
<span class="text-red-400 tabular-nums">{legendInfo.max}</span>
|
||||
<span class="text-zinc-500 ml-0.5">{legendInfo.label}</span>
|
||||
<!-- Map + Charts (left) / Stats (right) -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-4 mb-4 items-start">
|
||||
|
||||
<!-- Left column: map stacked above charts -->
|
||||
<div class="flex flex-col gap-4 min-w-0">
|
||||
<div class="relative h-[360px] rounded-xl overflow-hidden bg-zinc-800">
|
||||
{#if trackUrl}
|
||||
<ActivityMap
|
||||
{trackUrl}
|
||||
{timeseries}
|
||||
bbox={detail?.bbox ?? null}
|
||||
initialCoords={activity.preview_coords}
|
||||
accentColor={color}
|
||||
{colorMode}
|
||||
bind:hoveredIdx
|
||||
/>
|
||||
{#if legendInfo}
|
||||
<div class="absolute bottom-3 left-3 flex items-center gap-2 bg-zinc-900/80 backdrop-blur-sm rounded-lg px-2.5 py-1.5 text-xs pointer-events-none select-none">
|
||||
<span class="text-blue-400 tabular-nums">{legendInfo.min}</span>
|
||||
<div class="w-14 h-1.5 rounded-full flex-shrink-0" style="background:linear-gradient(to right,#3b82f6,#4ade80,#facc15,#ef4444)"></div>
|
||||
<span class="text-red-400 tabular-nums">{legendInfo.max}</span>
|
||||
<span class="text-zinc-500 ml-0.5">{legendInfo.label}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="w-full h-full flex items-center justify-center text-zinc-600 text-sm">
|
||||
No GPS track
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="w-full h-full flex items-center justify-center text-zinc-600 text-sm">
|
||||
No GPS track
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="text-red-400 text-sm">{error}</p>
|
||||
{:else if timeseries && timeseries.t.length > 0}
|
||||
<div class="bg-zinc-900 rounded-xl border border-zinc-800 p-4">
|
||||
<ActivityCharts {timeseries} bind:hoveredIdx {athlete} />
|
||||
</div>
|
||||
{:else if !detail || timeseriesLoading}
|
||||
<div class="bg-zinc-900 rounded-xl border border-zinc-800 p-4 h-32 animate-pulse"></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Stats panel -->
|
||||
<div class="grid grid-cols-2 lg:grid-cols-1 gap-px bg-zinc-800 rounded-xl overflow-hidden">
|
||||
<!-- Right column: stats summary -->
|
||||
<div class="grid grid-cols-2 gap-px bg-zinc-800 rounded-xl overflow-hidden">
|
||||
{#each stats as s}
|
||||
{@const cm =
|
||||
s.key === 'speed' && hasSpeedTrack ? 'speed' :
|
||||
@@ -426,24 +439,14 @@
|
||||
</div>
|
||||
{/each}
|
||||
{#if detail?.gear}
|
||||
<div class="bg-zinc-900 px-4 py-3 col-span-2 lg:col-span-1">
|
||||
<div class="bg-zinc-900 px-4 py-3 col-span-2">
|
||||
<p class="text-sm font-medium text-zinc-300">{detail.gear}</p>
|
||||
<p class="text-xs text-zinc-500">Gear</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts -->
|
||||
{#if error}
|
||||
<p class="text-red-400 text-sm mt-4">{error}</p>
|
||||
{:else if timeseries && timeseries.t.length > 0}
|
||||
<div class="bg-zinc-900 rounded-xl border border-zinc-800 p-4">
|
||||
<ActivityCharts {timeseries} bind:hoveredIdx {athlete} />
|
||||
</div>
|
||||
{:else if !detail || timeseriesLoading}
|
||||
<div class="bg-zinc-900 rounded-xl border border-zinc-800 p-4 h-32 animate-pulse"></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Laps -->
|
||||
{#if detail?.laps?.length}
|
||||
|
||||
Reference in New Issue
Block a user