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:
+23
-34
@@ -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", "vam_curve", "records", "best_climbs"}
|
||||
_COMPUTED = {"bas_version", "generated_at", "power_curve", "records", "best_climbs"}
|
||||
for user_dir in targets:
|
||||
index_path = user_dir / "index.json"
|
||||
if not index_path.exists():
|
||||
@@ -377,10 +377,12 @@ def _link_data(site: Path, data: Path) -> None:
|
||||
console.print(f"Linked data: [cyan]{target}[/cyan] → [cyan]{public_data}[/cyan]")
|
||||
|
||||
|
||||
def _recompute_vam(data: Path, handle: str | None = None) -> None:
|
||||
"""Recompute climbing_vam_mh and vam_curve for all activities from stored timeseries."""
|
||||
def _backfill_vam_summary(data: Path, handle: str | None = None) -> None:
|
||||
"""Copy climbing_vam_mh from detail JSONs into index.json summaries.
|
||||
|
||||
Needed once after the vam_curve→climbing_vam_mh-in-summary migration.
|
||||
"""
|
||||
import json
|
||||
from bincio.extract.metrics import compute_vam_from_timeseries
|
||||
|
||||
targets = [data / handle] if handle else _user_dirs(data)
|
||||
for user_dir in targets:
|
||||
@@ -394,31 +396,18 @@ def _recompute_vam(data: Path, handle: str | None = None) -> None:
|
||||
continue
|
||||
|
||||
updated = 0
|
||||
for act_path in acts_dir.glob("*.json"):
|
||||
if act_path.stem.endswith((".timeseries", ".geojson")):
|
||||
continue
|
||||
ts_path = acts_dir / f"{act_path.stem}.timeseries.json"
|
||||
if not ts_path.exists():
|
||||
for s in index_data.get("activities", []):
|
||||
if "climbing_vam_mh" in s:
|
||||
continue # already backfilled
|
||||
act_path = acts_dir / f"{s['id']}.json"
|
||||
if not act_path.exists():
|
||||
continue
|
||||
try:
|
||||
detail = json.loads(act_path.read_text(encoding="utf-8"))
|
||||
sport = detail.get("sport", "other")
|
||||
ts = json.loads(ts_path.read_text(encoding="utf-8"))
|
||||
new_vam, new_curve = compute_vam_from_timeseries(ts, sport)
|
||||
if (new_vam == detail.get("climbing_vam_mh")
|
||||
and new_curve == detail.get("vam_curve")):
|
||||
continue
|
||||
detail["climbing_vam_mh"] = new_vam
|
||||
detail["vam_curve"] = new_curve
|
||||
act_path.write_text(
|
||||
json.dumps(detail, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
act_id = act_path.stem
|
||||
for s in index_data.get("activities", []):
|
||||
if s.get("id") == act_id:
|
||||
s["vam_curve"] = new_curve
|
||||
break
|
||||
updated += 1
|
||||
vam = detail.get("climbing_vam_mh")
|
||||
if vam is not None:
|
||||
s["climbing_vam_mh"] = vam
|
||||
updated += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -426,7 +415,7 @@ def _recompute_vam(data: Path, handle: str | None = None) -> None:
|
||||
index_path.write_text(
|
||||
json.dumps(index_data, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
console.print(f" [cyan]{user_dir.name}[/cyan]: {updated} activity(ies) updated")
|
||||
console.print(f" [cyan]{user_dir.name}[/cyan]: {updated} summary(ies) updated")
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -452,9 +441,9 @@ def _recompute_vam(data: Path, handle: str | None = None) -> None:
|
||||
@click.option("--recompute-elevation", "recompute_elevation", is_flag=True,
|
||||
help="Recompute elevation_gain_m/loss_m for all activities from stored timeseries "
|
||||
"(run once after upgrading the dropout-skip fix).")
|
||||
@click.option("--recompute-vam", "recompute_vam", is_flag=True,
|
||||
help="Recompute climbing_vam_mh and vam_curve for all activities from stored "
|
||||
"timeseries (run once after adding VAM support).")
|
||||
@click.option("--backfill-vam-summary", "backfill_vam_summary", is_flag=True,
|
||||
help="Copy climbing_vam_mh from detail JSONs into index.json summaries "
|
||||
"(run once after the VAM curve → summary migration).")
|
||||
def render(
|
||||
config_path: Optional[str],
|
||||
data_dir: Optional[str],
|
||||
@@ -466,7 +455,7 @@ def render(
|
||||
no_build: bool,
|
||||
recompute_climbs: bool,
|
||||
recompute_elevation: bool,
|
||||
recompute_vam: bool,
|
||||
backfill_vam_summary: bool,
|
||||
) -> None:
|
||||
"""Build (or serve) the BincioActivity static site from a BAS data store."""
|
||||
|
||||
@@ -484,9 +473,9 @@ def render(
|
||||
console.print("Recomputing elevation gain/loss from timeseries…")
|
||||
_recompute_elevation(data, handle=handle)
|
||||
|
||||
if recompute_vam:
|
||||
console.print("Recomputing VAM from timeseries…")
|
||||
_recompute_vam(data, handle=handle)
|
||||
if backfill_vam_summary:
|
||||
console.print("Backfilling climbing_vam_mh into summaries…")
|
||||
_backfill_vam_summary(data, handle=handle)
|
||||
|
||||
_merge_edits(data, handle=handle)
|
||||
_rebuild_athlete_json(data, handle=handle)
|
||||
|
||||
Reference in New Issue
Block a user