Add saved analysis workspace and live dashboard statistics

This commit is contained in:
Peter Stockings
2026-10-03 12:27:20 +10:00
parent 5ede1a01d2
commit e6e2f22479
31 changed files with 1986 additions and 131 deletions
+54 -13
View File
@@ -61,6 +61,16 @@ DEFAULT = {
"derived": [],
"aggregates": {},
"filter_groups": [],
"display": "separate",
"baseline": None,
"flag": None,
"color": None,
"size": None,
"labels": {},
"reference": None,
"source": "runs",
"cohorts": [],
"comparison": "period",
}
@@ -121,7 +131,7 @@ def query_text(q):
period = f"during last {q['days']} days"
else:
period = f"during last {q['weeks']} weeks"
lines = ["runs", " " + period]
lines = [q["source"], " " + period]
for spec in q["derived"]:
lines.append(f" let {spec['name']} = {spec['expression']}")
if q["end"] and not q["start"]:
@@ -166,7 +176,11 @@ def query_text(q):
)
for condition in q["filters"]:
lines.append(clause(condition))
metrics = ", ".join(query_metrics(q))
metrics = ", ".join(
key
+ (f" as {json.dumps(q['labels'][key], ensure_ascii=False)}" if key in q["labels"] else "")
for key in query_metrics(q)
)
lines.extend(
[
f" summarize {metrics}",
@@ -175,7 +189,14 @@ def query_text(q):
if q["bin"]
else f" by {q['group']}"
),
" compare " + ("previous period" if q["compare"] else "none"),
" compare "
+ (
"previous year"
if q["comparison"] == "year"
else "previous period"
if q["compare"]
else "none"
),
]
)
if q["sort"] != ["date", "desc"]:
@@ -188,7 +209,7 @@ def query_text(q):
"runs"
if spec and spec["function"] == "count"
else "sec/km"
if spec and field == "pace" and spec["function"] in {"sum", "stddev"}
if spec and field in {"pace", "gap"} and spec["function"] in {"sum", "stddev"}
else next(iter(units[field]))
)
lines.append(f" having {key} {condition['op']} {number_text(condition['value'])} {unit}")
@@ -203,6 +224,25 @@ def query_text(q):
lines.append(f" plot {q['plot'][0]} vs {q['plot'][1]}")
if q["chart"] != "auto":
lines.append(f" chart {q['chart']}")
if q["display"] != "separate":
lines.append(f" display {q['display']}")
if q["baseline"]:
lines.append(f" baseline previous {q['baseline']} days")
if q["flag"]:
flag = q["flag"]
lines.append(
f" flag {flag['direction']} baseline {number_text(flag['sd'])} sd min {flag['minimum']} runs sustained {flag['sustained']} buckets"
)
for key in ("color", "size"):
if q[key]:
lines.append(f" {key} by {q[key]}")
if q["reference"]:
ref = q["reference"]
lines.append(f" reference {ref['field']} = {ref['raw']} {ref['unit']}")
for cohort in q["cohorts"]:
lines.append(
f" cohort {json.dumps(cohort['name'], ensure_ascii=False)} where {cohort['source']}"
)
return "\n".join(lines)
@@ -254,16 +294,17 @@ def metric_value(row, key):
if key == "heart_rate":
return row.avg_hr
if key in {"distance", "duration", "elevation"}:
return (
getattr(
row,
{"distance": "distance_m", "duration": "duration_s", "elevation": "elevation_m"}[
key
],
)
/ {"distance": 1000, "duration": 3600, "elevation": 1}[key]
raw = getattr(
row,
{"distance": "distance_m", "duration": "duration_s", "elevation": "elevation_m"}[key],
)
return (row.analysis_summary or {}).get(key)
return (
raw / {"distance": 1000, "duration": 3600, "elevation": 1}[key]
if isinstance(raw, (int, float)) and math.isfinite(raw)
else None
)
summary = row.analysis_summary or {}
return summary.get("summary", summary).get(key)
def aggregate(rows, key):