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
+15 -3
View File
@@ -24,6 +24,10 @@
const templates = [
['runs', 'Your recorded running activities'],
['laps', 'Recorded lap inputs from compact analysis'],
['segments', 'Five-minute moving segments from full-resolution recordings'],
['cohort "Easy" where heart_rate <= 145 bpm', 'First-match partitions for a stacked additive summary'],
['compare previous year', 'Same local-date interval in the preceding year'],
['during last 12 weeks', 'Rolling period ending today'],
['during last 6 months', 'Calendar months'],
['during last 30 days', 'Rolling days'],
@@ -41,7 +45,14 @@
['trend change', 'Difference from the preceding bucket'],
['trend percent_change', 'Percentage change from the preceding bucket'],
['plot speed vs heart_rate', 'Run-level scatter plot and Pearson correlation'],
['chart bar', 'Choose auto, line, bar or none'],
['chart bar', 'Choose auto, line, bar, area, histogram, box, heatmap or none'],
['display overlay', 'Plot multiple series together with axes by unit'],
['baseline previous 90 days', 'Earlier matched-run baseline before the query starts'],
['flag above baseline 1.5 sd min 3 runs sustained 2 buckets', 'Persistent descriptive deviation with minimum coverage'],
['color by elevation_per_km', 'Scatter color encodes this numeric metric'],
['size by distance', 'Scatter size encodes this numeric metric'],
['reference heart_rate = 150 bpm', 'Draw a reference line for a selected metric'],
['let hr_14d = rolling_mean(heart_rate, 14 days)', 'Custom rolling mean, sum or count before filtering'],
['by distance bins 5 km', 'Group runs into half-open distance ranges'],
['during 2026-07-01 to 2026-09-30', 'Inclusive local-date range'],
['ending 2026-10-02', 'Optional end date for a rolling period'],
@@ -71,7 +82,7 @@
const word = stream.current();
if (names.has(word) || availableMetrics().some(metric=>metric.name===word)) return 'variable-2';
if (language.functions.includes(word)) return 'builtin';
if (/^(runs|during|last|ending|where|and|or|between|summarize|by|compare|previous|period|none|has|missing|title|contains|sort|asc|desc|to|let|having|rank|take|limit|trend|rolling|cumulative|change|percent_change|plot|vs|chart|bins|all|time)$/.test(word)) return 'keyword';
if (/^(runs|during|last|ending|where|and|or|between|summarize|by|compare|previous|period|none|has|missing|title|contains|sort|asc|desc|to|let|having|rank|take|limit|trend|rolling|cumulative|change|percent_change|plot|vs|chart|bins|all|time|baseline|flag|sd|min|sustained|buckets|above|below|outside|display|overlay|separate|color|size|reference|as)$/.test(word)) return 'keyword';
if (/^(day|days|week|weeks|month|months|km|m|bpm|spm|W|ms|h|min|s|date)$/.test(word)) return 'atom';
return null;
}
@@ -89,7 +100,7 @@
} else if (/^\s*rank by (?:\w+|\w+\(\w*\))\s+\w*$/.test(line)) {
list=['asc','desc'].filter(text=>text.startsWith(token)).map(text=>({text,detail:'Group ranking order'}));
} else if (/^\s*chart\s+\w*$/.test(line)) {
list=['auto','line','bar','none'].filter(text=>text.startsWith(token)).map(text=>({text,detail:'Chart presentation'}));
list=['auto','line','bar','area','histogram','box','heatmap','stacked','none'].filter(text=>text.startsWith(token)).map(text=>({text,detail:'Chart presentation'}));
} else if (/^\s*trend\s+[\w ]*$/.test(line)) {
from={line:cursor.line,ch:line.indexOf('trend')+6};const prefix=line.slice(from.ch);
list=['cumulative','rolling 4 buckets','change','percent_change'].filter(text=>text.startsWith(prefix)).map(text=>({text,detail:'Transform displayed time buckets'}));
@@ -192,6 +203,7 @@
// Submit only the authoritative DSL. Helper controls never override it.
const url=new URL(form.action||location.pathname,location.origin);
url.search=new URLSearchParams({query:text});
const analysis=new URL(location.href).searchParams.get('analysis');if(analysis)url.searchParams.set('analysis',analysis);
location.assign(url);
});
document.getElementById('format-query').onclick=async()=>{