personal records tab into athlete page
This commit is contained in:
+77
-17
@@ -69,6 +69,8 @@ def write_activity(
|
||||
"start_latlng": list(metrics.start_latlng) if metrics.start_latlng else None,
|
||||
"end_latlng": list(metrics.end_latlng) if metrics.end_latlng else None,
|
||||
"mmp": metrics.mmp,
|
||||
"best_efforts": metrics.best_efforts,
|
||||
"best_climb_m": metrics.best_climb_m,
|
||||
"laps": [_serialise_lap(lap) for lap in activity.laps],
|
||||
"timeseries": build_timeseries(activity.points, activity.started_at, privacy),
|
||||
"source": source,
|
||||
@@ -117,6 +119,8 @@ def build_summary(
|
||||
"avg_cadence_rpm": metrics.avg_cadence_rpm,
|
||||
"avg_power_w": metrics.avg_power_w,
|
||||
"mmp": metrics.mmp,
|
||||
"best_efforts": metrics.best_efforts,
|
||||
"best_climb_m": metrics.best_climb_m,
|
||||
"source": _infer_source(activity),
|
||||
"privacy": privacy,
|
||||
"detail_url": f"activities/{activity_id}.json",
|
||||
@@ -127,16 +131,7 @@ def build_summary(
|
||||
|
||||
|
||||
def write_athlete_json(summaries: list[dict], output_dir: Path, athlete_config: dict) -> None:
|
||||
"""Aggregate per-activity MMP curves into athlete.json.
|
||||
|
||||
Computes element-wise max MMP for:
|
||||
- all_time
|
||||
- last_365d
|
||||
- last_90d
|
||||
|
||||
The site reads this single file for the athlete/power-curve page.
|
||||
Per-activity mmp is already in each summary for client-side season filtering.
|
||||
"""
|
||||
"""Aggregate per-activity MMP curves and personal records into athlete.json."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -148,8 +143,9 @@ def write_athlete_json(summaries: list[dict], output_dir: Path, athlete_config:
|
||||
cutoff_365 = _cutoff_iso(365)
|
||||
cutoff_90 = _cutoff_iso(90)
|
||||
|
||||
# ── MMP aggregation ───────────────────────────────────────────────────────
|
||||
|
||||
def _merge_mmps(activity_mmps: list[list[list[int]]]) -> list[list[int]]:
|
||||
"""Element-wise max across a list of mmp arrays."""
|
||||
best: dict[int, int] = {}
|
||||
for mmp in activity_mmps:
|
||||
for d, w in mmp:
|
||||
@@ -157,18 +153,82 @@ def write_athlete_json(summaries: list[dict], output_dir: Path, athlete_config:
|
||||
best[d] = w
|
||||
return [[d, w] for d, w in sorted(best.items())]
|
||||
|
||||
all_mmps = [s["mmp"] for s in summaries if s.get("mmp")]
|
||||
mmps_365 = [s["mmp"] for s in summaries if s.get("mmp") and s["started_at"] >= cutoff_365]
|
||||
mmps_90 = [s["mmp"] for s in summaries if s.get("mmp") and s["started_at"] >= cutoff_90]
|
||||
all_mmps = [s["mmp"] for s in summaries if s.get("mmp")]
|
||||
mmps_365 = [s["mmp"] for s in summaries if s.get("mmp") and s["started_at"] >= cutoff_365]
|
||||
mmps_90 = [s["mmp"] for s in summaries if s.get("mmp") 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}
|
||||
|
||||
SPORTS = ["running", "cycling", "swimming", "hiking", "walking", "skiing", "other"]
|
||||
records: dict[str, dict[float, dict]] = {s: {} for s in SPORTS}
|
||||
best_climb: list[dict] = [] # top 10 best climbs for cycling
|
||||
|
||||
for s in summaries:
|
||||
sport = s.get("sport", "other")
|
||||
act_id = s.get("id", "")
|
||||
started = s.get("started_at", "")
|
||||
title = s.get("title", "")
|
||||
|
||||
# Distance-based best efforts
|
||||
for d_km, t_s in (s.get("best_efforts") or []):
|
||||
bucket = records.get(sport, {})
|
||||
existing = bucket.get(d_km)
|
||||
if existing is None or t_s < existing["time_s"]:
|
||||
bucket[d_km] = {
|
||||
"time_s": t_s,
|
||||
"activity_id": act_id,
|
||||
"started_at": started,
|
||||
"title": title,
|
||||
}
|
||||
records[sport] = bucket
|
||||
|
||||
# Best climb (cycling only) — collect all, trim to top 10 after loop
|
||||
climb = s.get("best_climb_m")
|
||||
if climb and sport == "cycling":
|
||||
best_climb.append({
|
||||
"climb_m": climb,
|
||||
"activity_id": act_id,
|
||||
"started_at": started,
|
||||
"title": title,
|
||||
})
|
||||
|
||||
# Hiking / walking: track longest distance and most elevation from summaries
|
||||
if sport in ("hiking", "walking"):
|
||||
dist = s.get("distance_m") or 0
|
||||
elev = s.get("elevation_gain_m") or 0
|
||||
for metric, key, val in [("longest_m", "distance_m", dist),
|
||||
("most_elevation_m", "elevation_gain_m", elev)]:
|
||||
bucket = records[sport]
|
||||
existing = bucket.get(metric)
|
||||
if val and (existing is None or val > existing.get("value", 0)):
|
||||
bucket[metric] = {
|
||||
"value": val,
|
||||
"activity_id": act_id,
|
||||
"started_at": started,
|
||||
"title": title,
|
||||
}
|
||||
records[sport] = bucket
|
||||
|
||||
# Serialise records: convert float keys to strings for JSON
|
||||
def _serialise_sport_records(bucket: dict) -> dict:
|
||||
return {str(k): v for k, v in bucket.items()}
|
||||
|
||||
athlete = {
|
||||
"bas_version": "1.0",
|
||||
"generated_at": now.isoformat(),
|
||||
"power_curve": {
|
||||
"all_time": _merge_mmps(all_mmps) if all_mmps else None,
|
||||
"last_365d": _merge_mmps(mmps_365) if mmps_365 else None,
|
||||
"last_90d": _merge_mmps(mmps_90) if mmps_90 else None,
|
||||
"all_time": _merge_mmps(all_mmps) if all_mmps else None,
|
||||
"last_365d": _merge_mmps(mmps_365) if mmps_365 else None,
|
||||
"last_90d": _merge_mmps(mmps_90) if mmps_90 else None,
|
||||
},
|
||||
"records": {
|
||||
sport: _serialise_sport_records(records[sport])
|
||||
for sport in SPORTS
|
||||
if records[sport]
|
||||
},
|
||||
"best_climbs": sorted(best_climb, key=lambda x: x["climb_m"], reverse=True)[:10],
|
||||
**athlete_config,
|
||||
}
|
||||
(output_dir / "athlete.json").write_text(
|
||||
|
||||
Reference in New Issue
Block a user