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
+1 -26
View File
@@ -102,7 +102,6 @@ def write_activity(
"best_efforts": metrics.best_efforts,
"best_climb_m": metrics.best_climb_m,
"climbing_vam_mh": metrics.climbing_vam_mh,
"vam_curve": metrics.vam_curve,
"laps": [_serialise_lap(lap) for lap in activity.laps],
"timeseries_url": f"activities/{activity_id}.timeseries.json" if timeseries else None,
"source": source,
@@ -259,7 +258,7 @@ def build_summary(
"mmp": metrics.mmp,
"best_efforts": metrics.best_efforts,
"best_climb_m": metrics.best_climb_m,
"vam_curve": metrics.vam_curve,
"climbing_vam_mh": metrics.climbing_vam_mh,
"source": _infer_source(activity),
"privacy": privacy,
"detail_url": f"activities/{activity_id}.json",
@@ -303,25 +302,6 @@ def write_athlete_json(summaries: list[dict], output_dir: Path, athlete_config:
mmps_365 = [s["mmp"] for s in summaries if s.get("mmp") and _is_outdoor(s) and s["started_at"] >= cutoff_365]
mmps_90 = [s["mmp"] for s in summaries if s.get("mmp") and _is_outdoor(s) and s["started_at"] >= cutoff_90]
# ── VAM curve aggregation ─────────────────────────────────────────────────
def _merge_vam_curves(vam_lists: list[list[list[int]]]) -> list[list[int]]:
best: dict[int, int] = {}
for vc in vam_lists:
for d, v in vc:
if d not in best or v > best[d]:
best[d] = v
return [[d, v] for d, v in sorted(best.items())]
_VAM_SPORTS = {"cycling", "running", "hiking", "walking"}
def _has_vam(s: dict) -> bool:
return bool(s.get("vam_curve")) and s.get("sport") in _VAM_SPORTS and _is_outdoor(s)
all_vams = [s["vam_curve"] for s in summaries if _has_vam(s)]
vams_365 = [s["vam_curve"] for s in summaries if _has_vam(s) and s["started_at"] >= cutoff_365]
vams_90 = [s["vam_curve"] for s in summaries if _has_vam(s) and s["started_at"] >= cutoff_90]
# ── Personal records aggregation ──────────────────────────────────────────
# records[sport][distance_km] = {time_s, activity_id, started_at, title}
# best_climb[activity_id] = {climb_m, started_at, title}
@@ -390,11 +370,6 @@ def write_athlete_json(summaries: list[dict], output_dir: Path, athlete_config:
"last_365d": _merge_mmps(mmps_365) if mmps_365 else None,
"last_90d": _merge_mmps(mmps_90) if mmps_90 else None,
},
"vam_curve": {
"all_time": _merge_vam_curves(all_vams) if all_vams else None,
"last_365d": _merge_vam_curves(vams_365) if vams_365 else None,
"last_90d": _merge_vam_curves(vams_90) if vams_90 else None,
},
"records": {
sport: _serialise_sport_records(records[sport])
for sport in SPORTS