VAM: drop duration curve, show avg climbing VAM in Nerd Corner

Remove the per-duration VAM curve everywhere (metrics, summaries, detail
JSON, athlete.json, VamChart.svelte, AthleteView VAM tab). Keep only
climbing_vam_mh per activity. Add it to activity summaries so NerdCorner
can plot average climbing VAM per week/month year-over-year alongside
distance/elevation/time. Add --backfill-vam-summary flag to copy the
field from existing detail JSONs into index.json without re-extracting.
This commit is contained in:
Davide Scaini
2026-05-16 22:03:40 +02:00
parent 7cd8a6b030
commit 003b540481
8 changed files with 77 additions and 346 deletions
+6 -23
View File
@@ -2,7 +2,6 @@
import { onMount } from 'svelte';
import type { AthleteJson, BASIndex, ActivitySummary } from '../lib/types';
import MmpChart from './MmpChart.svelte';
import VamChart from './VamChart.svelte';
import RecordsView from './RecordsView.svelte';
import AthleteDrawer from './AthleteDrawer.svelte';
import Explore from './Explore.svelte';
@@ -20,13 +19,12 @@
let athlete: AthleteJson | null = null;
let activities: ActivitySummary[] = [];
let vamActivities: ActivitySummary[] = [];
let allActivities: ActivitySummary[] = [];
let loading = true;
let error: string | null = null;
let drawerOpen = false;
type Tab = 'power' | 'vam' | 'records' | 'segments' | 'profile' | 'explore' | 'nerd';
type Tab = 'power' | 'records' | 'segments' | 'profile' | 'explore' | 'nerd';
let activeTab: Tab = 'power';
let mounted = false;
let isOwner = false;
@@ -96,7 +94,7 @@
isOwner = (e as CustomEvent<string>).detail === handle;
}, { once: true });
}
const TABS: Tab[] = ['power', 'vam', 'records', 'segments', 'profile', 'explore', 'nerd'];
const TABS: Tab[] = ['power', 'records', 'segments', 'profile', 'explore', 'nerd'];
const rawTab = new URLSearchParams(window.location.search).get('tab');
const resolved = TABS.includes(rawTab as Tab) ? (rawTab as Tab) : 'power';
activeTab = (resolved === 'explore' && !isOwner) ? 'power' : resolved;
@@ -133,7 +131,6 @@
athlete = resolvedAthlete;
allActivities = index.activities.filter(a => !isUnlisted(a.privacy));
activities = allActivities.filter(a => a.mmp);
vamActivities = allActivities.filter(a => a.vam_curve);
} catch (e: any) {
error = e.message;
} finally {
@@ -159,19 +156,15 @@
return hi >= 900 ? `${lo}+ bpm` : `${lo}${hi} bpm`;
}
const ALL_TABS: { key: Tab; label: string; ownerOnly?: boolean; requiresVam?: boolean }[] = [
const ALL_TABS: { key: Tab; label: string; ownerOnly?: boolean }[] = [
{ key: 'power', label: 'Power Curve' },
{ key: 'vam', label: 'VAM Curve', requiresVam: true },
{ key: 'records', label: 'Records' },
{ key: 'segments', label: 'Segments' },
{ key: 'profile', label: 'Profile' },
{ key: 'explore', label: 'Explore', ownerOnly: true },
{ key: 'nerd', label: 'Nerd Corner', ownerOnly: true },
{ key: 'explore', label: 'Explore', ownerOnly: true },
{ key: 'nerd', label: 'Nerd Corner', ownerOnly: true },
];
$: TABS = ALL_TABS.filter(t =>
(!t.ownerOnly || isOwner) &&
(!t.requiresVam || athlete?.vam_curve?.all_time != null)
);
$: TABS = ALL_TABS.filter(t => !t.ownerOnly || isOwner);
</script>
{#if loading}
@@ -223,16 +216,6 @@
<p class="text-zinc-500 text-sm">No power data found. Make sure your activities include power meter data.</p>
{/if}
<!-- VAM Curve tab -->
{:else if activeTab === 'vam'}
{#if athlete.vam_curve?.all_time}
<div class="bg-zinc-900 rounded-xl p-4 border border-zinc-800">
<VamChart {athlete} activities={vamActivities} />
</div>
{:else}
<p class="text-zinc-500 text-sm">No climbing data found.</p>
{/if}
<!-- Records tab -->
{:else if activeTab === 'records'}
<RecordsView {athlete} {base} />