Add VAM (climbing velocity) metric and per-duration curve
Extract pipeline now computes two VAM metrics per activity (cycling, running, hiking, walking): - climbing_vam_mh: VAM on ascending segments only, using 30 s forward lookahead to classify climbing vs. flat/descent (stored in detail JSON) - vam_curve: [[duration_s, vam_mh], ...] best VAM per standard duration (60 s – 1 h), sliding window on 30 s smoothed elevation, only windows with ≥ 10 m net gain count (stored in summary + detail) Athlete JSON aggregates vam_curve across all activities (all_time, last_365d, last_90d), same structure as power_curve. Frontend: - ActivityDetail shows "Climbing VAM" stat (grouped with elevation) - AthleteView adds a "VAM Curve" tab that appears only when the athlete has climbing data; renders VamChart (new component, mirrors MmpChart) vam_curve stripped from combined global feed; kept in user year shards for season-based on-the-fly aggregation in VamChart. Requires bincio reextract to backfill existing activities.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
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';
|
||||
@@ -19,12 +20,13 @@
|
||||
|
||||
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' | 'records' | 'segments' | 'profile' | 'explore' | 'nerd';
|
||||
type Tab = 'power' | 'vam' | 'records' | 'segments' | 'profile' | 'explore' | 'nerd';
|
||||
let activeTab: Tab = 'power';
|
||||
let mounted = false;
|
||||
let isOwner = false;
|
||||
@@ -94,7 +96,7 @@
|
||||
isOwner = (e as CustomEvent<string>).detail === handle;
|
||||
}, { once: true });
|
||||
}
|
||||
const TABS: Tab[] = ['power', 'records', 'segments', 'profile', 'explore', 'nerd'];
|
||||
const TABS: Tab[] = ['power', 'vam', '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;
|
||||
@@ -131,6 +133,7 @@
|
||||
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 {
|
||||
@@ -156,15 +159,19 @@
|
||||
return hi >= 900 ? `${lo}+ bpm` : `${lo}–${hi} bpm`;
|
||||
}
|
||||
|
||||
const ALL_TABS: { key: Tab; label: string; ownerOnly?: boolean }[] = [
|
||||
const ALL_TABS: { key: Tab; label: string; ownerOnly?: boolean; requiresVam?: 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 },
|
||||
];
|
||||
$: TABS = ALL_TABS.filter(t => !t.ownerOnly || isOwner);
|
||||
$: TABS = ALL_TABS.filter(t =>
|
||||
(!t.ownerOnly || isOwner) &&
|
||||
(!t.requiresVam || athlete?.vam_curve?.all_time != null)
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
@@ -216,6 +223,16 @@
|
||||
<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} />
|
||||
|
||||
Reference in New Issue
Block a user