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:
Davide Scaini
2026-05-16 21:34:06 +02:00
parent de602ff5d9
commit baf20b51ba
8 changed files with 369 additions and 6 deletions
+1 -1
View File
@@ -116,7 +116,7 @@ def _rebuild_athlete_json(data: Path, handle: str | None = None) -> None:
from bincio.render.merge import parse_sidecar, _apply_sidecar_summary
targets = [data / handle] if handle else _user_dirs(data)
_COMPUTED = {"bas_version", "generated_at", "power_curve", "records", "best_climbs"}
_COMPUTED = {"bas_version", "generated_at", "power_curve", "vam_curve", "records", "best_climbs"}
for user_dir in targets:
index_path = user_dir / "index.json"
if not index_path.exists():
+1 -1
View File
@@ -421,7 +421,7 @@ FEED_PAGE_SIZE = 50
# Extra fields stripped from the combined feed — preview_coords is the biggest
# contributor (~24% of shard size) but the feed cards need it for thumbnails,
# so we keep it. mmp is never displayed in feed cards.
_COMBINED_FEED_STRIP = _FEED_STRIP | {"mmp"}
_COMBINED_FEED_STRIP = _FEED_STRIP | {"mmp", "vam_curve"}
def write_combined_feed(data_dir: Path) -> int: