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
+37
View File
@@ -0,0 +1,37 @@
<section class="panel analysis-workspace">
<div class="section-head"><div><span class="eyebrow">ACCOUNT LIBRARY</span><h2>Investigations</h2></div><button type="button" class="button secondary" id="new-analysis">New analysis</button></div>
<div id="analysis-library" class="analysis-library" aria-live="polite"><p class="field-help">Loading your saved analyses…</p></div>
</section>
<dialog id="save-analysis-dialog" class="analysis-dialog">
<form id="save-analysis-form">
<div class="studio-row"><h2 id="save-analysis-title">Save analysis</h2><button type="button" class="text-button" id="close-analysis-dialog" aria-label="Close">×</button></div>
<label>Name<input name="name" maxlength="100" required placeholder="HR at easy pace"></label>
<label>Investigation<input name="collection" maxlength="80" required value="My analyses" placeholder="Easy-run effort"></label>
<label>Notes<textarea name="notes" maxlength="4000" rows="3" placeholder="What are you investigating? What should you check next?"></textarea></label>
<div class="analysis-settings"><label>Dashboard display<select name="view"><option value="chart">Plot</option><option value="value">Summary values</option><option value="table">Grouped table</option></select></label><label>Widget width<select name="width"><option value="half">Half width</option><option value="full">Full width</option></select></label></div>
<label class="analysis-check"><input name="pinned" type="checkbox"> Pin to live dashboard</label>
<p id="save-analysis-error" class="studio-error" role="alert"></p>
<div class="studio-row bottom"><button type="button" class="button secondary" id="save-analysis-copy">Save as new</button><button class="button primary" type="submit" id="save-analysis-submit">Save analysis</button></div>
</form>
</dialog>
<details class="panel analysis-builder">
<summary>Build a comparable-effort investigation</summary>
<form id="matched-effort-form">
<div class="analysis-settings">
<label>Compare inputs<select name="source"><option value="runs">Whole runs</option><option value="segments">Steady moving segments</option></select></label>
<label>Pace from · min/km<input name="pace_low" value="5:20" pattern="[0-9]+:[0-5][0-9]" required></label>
<label>Pace to · min/km<input name="pace_high" value="5:40" pattern="[0-9]+:[0-5][0-9]" required></label>
<label>Max ascent per km · m/km<input name="hills" type="number" min="0" max="1000" value="10" required></label>
<label>Current window · weeks<input name="weeks" type="number" min="1" max="104" value="12" required></label>
<label>Earlier baseline · days<input name="baseline" type="number" min="7" max="732" value="90" required></label>
<label>Minimum usable inputs per week<input name="minimum" type="number" min="2" max="1000" value="3" required></label>
</div>
<p class="field-help">Segments also require ≤5% speed variation, ≥80% moving HR coverage and ≥4 minutes. Matching pace and hills narrows the cohort; conditions can still differ.</p>
<button class="button primary" type="submit">Explore matched heart rate</button>
</form>
</details>
<details class="panel analysis-preset-gallery">
<summary>Start with a question</summary>
<div class="analysis-presets">{% for preset in presets %}<a class="analysis-preset" href="{{ url_for('web.stats', query=preset.query) }}"><span class="eyebrow">{{ preset.category }}</span><h3>{{ preset.name }}</h3><p>{{ preset.description }}</p></a>{% endfor %}</div>
</details>
<script id="selected-analysis" type="application/json">{{ selected_analysis|tojson }}</script>
+18
View File
@@ -0,0 +1,18 @@
<section id="live-analyses" data-csrf="{{ csrf_token() }}" aria-label="Live analysis dashboard">
<div class="section-head"><div><span class="eyebrow">LIVE OBSERVATORY</span><h2>Your training signals</h2><p class="field-help">Account-saved analyses · refreshed on page load, focus and every five minutes</p></div><div class="query-actions"><button class="button secondary" type="button" id="refresh-analyses">Refresh</button><a class="button primary" href="{{ url_for('web.stats') }}">Explore a question</a></div></div>
<div id="live-analysis-grid" class="live-analysis-grid">
{% for analysis in analyses %}
<article class="panel live-analysis-widget {{ 'widget-full' if analysis.width == 'full' }}" data-id="{{ analysis.id }}" data-view="{{ analysis.view }}">
<div class="section-head"><div><span class="eyebrow">{{ analysis.collection }}</span><h3>{{ analysis.name }}</h3></div><a class="back-link" href="{{ url_for('web.stats', analysis=analysis.id) }}">Explore →</a></div>
<div class="live-widget-body"><p class="field-help">Loading live results…</p></div>
<div class="live-widget-insights"></div>
{% if analysis.notes %}<details class="widget-notes"><summary>Investigation notes</summary><p>{{ analysis.notes }}</p></details>{% endif %}
<div class="widget-actions"><button class="text-button" type="button" data-widget-action="up" aria-label="Move {{ analysis.name }} earlier">↑</button><button class="text-button" type="button" data-widget-action="down" aria-label="Move {{ analysis.name }} later">↓</button><button class="text-button" type="button" data-widget-action="width">{{ 'Half width' if analysis.width == 'full' else 'Full width' }}</button><label class="sr-only" for="widget-view-{{ analysis.id }}">Display {{ analysis.name }} as</label><select id="widget-view-{{ analysis.id }}" data-widget-action="view"><option value="chart" {{ 'selected' if analysis.view == 'chart' }}>Plot</option><option value="value" {{ 'selected' if analysis.view == 'value' }}>Values</option><option value="table" {{ 'selected' if analysis.view == 'table' }}>Table</option></select><button class="text-button" type="button" data-widget-action="unpin">Unpin</button></div>
<p class="widget-status field-help" role="status"></p>
</article>
{% else %}
<div class="panel live-empty"><h3>Keep watching a useful question</h3><p>Explore matched-pace heart rate, workload or consistency in Stats, then pin your analysis here.</p><a class="button secondary" href="{{ url_for('web.stats') }}">Open analysis studio</a></div>
{% endfor %}
</div>
<p id="live-analysis-status" class="field-help" role="status"></p>
</section>
+3 -2
View File
@@ -1,10 +1,11 @@
{% extends 'base.html' %}
{% block title %}{{ 'Overview' if personal else profile.display_name }} · Stride{% endblock %}
{% block breadcrumb %}{{ 'OVERVIEW' if personal else 'SHARED PROFILE' }}{% endblock %}
{% block head %}<link rel="stylesheet" href="{{ url_for('static',filename='vendor/leaflet.css') }}">{% endblock %}
{% block head %}<link rel="stylesheet" href="{{ url_for('static', filename='analysis.css') }}"><link rel="stylesheet" href="{{ url_for('static',filename='vendor/leaflet.css') }}">{% endblock %}
{% block content %}
{% if not personal %}<a class="back-link" href="{{ url_for('web.friends') }}">← Friends</a>{% endif %}
<div class="page-heading"><div><h1>{{ 'Your running' if personal else profile.display_name ~ '’s running' }}</h1><p>{{ 'Distance, effort, and your recent sessions.' if personal else 'Activities shared with friends.' }}</p></div>{% if not personal %}<span class="pill friends">Friends · Shared runs</span>{% endif %}</div>
{% if personal %}{% include '_live_analyses.html' %}{% endif %}
<section class="dashboard-route panel" aria-label="Latest running route">
<div class="dashboard-route-info"><span class="eyebrow">LATEST RUN / ROUTE REPLAY</span><h2>{{ mapped_run.title if mapped_run else 'Your next run starts here.' }}</h2>
{% if mapped_run %}<p>{{ mapped_run.started_at|localdate(mapped_run.owner.timezone) }} · {{ '%.2f'|format(mapped_run.distance_km) }} km</p><a class="back-link" href="{{ url_for('web.activity',activity_id=mapped_run.id) }}">View run {{ icon('arrow') }}</a>{% else %}<p>Import a run with GPS to see your route in motion.</p>{% endif %}
@@ -26,4 +27,4 @@
{% endif %}
<section class="panel activities-panel" id="history"><div class="section-head"><div><span class="eyebrow">SESSION HISTORY</span><h2>{{ 'Your activities' if personal else 'Shared activities' }}</h2></div><span class="subtle-label">{{ stats.count }} activities</span></div>{% include '_activities.html' %}</section>
{% endblock %}
{% block scripts %}<script src="{{ url_for('static',filename='vendor/leaflet.js') }}"></script><script src="{{ url_for('static',filename='vendor/chart.umd.js') }}"></script><script src="{{ url_for('static',filename='dashboard.js') }}"></script>{% endblock %}
{% block scripts %}<script src="{{ url_for('static',filename='vendor/leaflet.js') }}"></script><script src="{{ url_for('static',filename='vendor/chart.umd.js') }}"></script><script src="{{ url_for('static',filename='dashboard.js') }}"></script><script src="{{ url_for('static',filename='analysis-charts.js') }}"></script><script src="{{ url_for('static',filename='live-analyses.js') }}"></script>{% endblock %}
+21 -16
View File
@@ -1,17 +1,17 @@
{% extends 'base.html' %}
{% block title %}Stats studio · Stride{% endblock %}
{% block head %}<link rel="stylesheet" href="{{ url_for('static', filename='vendor/codemirror.css') }}"><link rel="stylesheet" href="{{ url_for('static', filename='vendor/show-hint.css') }}"><link rel="stylesheet" href="{{ url_for('static', filename='stats.css') }}">{% endblock %}
{% block head %}<link rel="stylesheet" href="{{ url_for('static', filename='analysis.css') }}"><link rel="stylesheet" href="{{ url_for('static', filename='vendor/codemirror.css') }}"><link rel="stylesheet" href="{{ url_for('static', filename='vendor/show-hint.css') }}"><link rel="stylesheet" href="{{ url_for('static', filename='stats.css') }}">{% endblock %}
{% block content %}
<div id="stats-studio" data-owner="{{ current_user.id }}" data-show-previous="{{ 'true' if results and results.show_previous else 'false' }}" data-query="{{ query_text }}" data-csrf="{{ csrf_token() }}" data-validate-url="{{ url_for('web.stats_validate') }}">
<div class="page-heading"><div><span class="eyebrow">YOUR RUNNING / STATS STUDIO</span><h1>Your runs. Your questions.</h1><p>Write a query, explore the pattern, and inspect the runs behind it.</p></div><button type="button" class="button secondary" id="save-view">☆ Save view</button></div>
<div id="saved-views" class="studio-presets" aria-label="Saved views"></div>
<div id="stats-studio" data-owner="{{ current_user.id }}" data-source="{{ query.source }}" data-show-previous="{{ 'true' if results and results.show_previous else 'false' }}" data-query="{{ query_text }}" data-csrf="{{ csrf_token() }}" data-validate-url="{{ url_for('web.stats_validate') }}">
<div class="page-heading"><div><span class="eyebrow">YOUR RUNNING / STATS STUDIO</span><h1>{{ selected_analysis.name if selected_analysis else "Your runs. Your questions." }}</h1><p>Write a query, explore the pattern, and inspect the runs behind it.</p></div><div class="query-actions"><button type="button" class="button secondary" id="save-view">Save analysis</button><button type="button" class="button primary" id="pin-analysis">Pin live analysis</button></div></div>
{% include '_analysis_workspace.html' %}
<script id="query-language" type="application/json">{{ language|tojson }}</script>
<form method="get" id="explorer-form" class="panel question-panel" novalidate>
<div class="studio-row"><div><span class="eyebrow">RUNNING QUERY</span><span class="query-caption">An analysis language for your running history.</span></div><div class="query-actions"><button type="button" class="button secondary" id="format-query">Format</button><button type="button" class="button secondary" id="complete-query">Suggest <kbd>Ctrl Space</kbd></button></div></div>
<div id="query-editor"><label class="sr-only" for="query-input">Running stats query</label><textarea id="query-input" name="query" spellcheck="false" maxlength="4000" rows="9">{{ submitted_query if submitted_query else query_text }}</textarea></div>
<div class="studio-row query-state"><span id="query-validation" role="status">Checking query...</span><span class="query-shortcut"><kbd>Ctrl / Cmd Enter</kbd> to run</span></div>
<div id="query-diagnostics" aria-live="polite">{% if error %}<p class="studio-error" role="alert">{{ error }}</p>{% endif %}</div>
<div class="studio-row studio-bottom"><span class="field-help">Whole-run metrics · {{ current_user.timezone }}<br>Lower pace values mean faster running.</span><button class="button primary" id="run-query" type="submit">Run query {{ icon('arrow') }}</button></div>
<div class="studio-row studio-bottom"><span class="field-help">{{ 'Whole-run metrics' if query.source == 'runs' else 'Recorded laps' if query.source == 'laps' else 'Five-minute moving segments' }} · {{ current_user.timezone }}<br>Lower pace values mean faster running.</span><button class="button primary" id="run-query" type="submit">Run query {{ icon('arrow') }}</button></div>
<details class="query-help"><summary>Language reference &amp; examples</summary><div class="query-reference"><div><h3>Filter and compare</h3><pre>during last 6 months
where distance between 8 and 20 km
and pace &lt;= 5:30 /km
@@ -23,7 +23,13 @@ rank by mean(distance) desc
limit 10
trend rolling 4 buckets
let efficiency = speed / heart_rate
plot pace vs heart_rate</pre><p class="field-help">Functions: sum, mean, weighted_mean, median, min, max, count, population stddev, and p10/p25/p75/p90/p95/p99. take limits ranked runs before grouping; limit limits groups. Trends also accept cumulative, change and percent_change. Arithmetic uses km, hours and seconds/km. Missing inputs and zero divisors give missing values.</p></div><div><h3>Metrics · select up to six</h3><div class="metric-reference">{% for key, info in metrics.items() %}<button type="button" class="metric-insert" data-metric="{{ key }}" title="{{ info[2] }}"><code>{{ key }}</code><span>{{ info[1] }}</span></button>{% endfor %}</div><p class="field-help">duration accepts h, min or s; distance accepts km or m. Use has / missing to select recorded sensor coverage.</p></div></div></details>
plot pace vs heart_rate
color by elevation_per_km
size by distance
baseline previous 90 days
flag above baseline 1.5 sd min 3 runs sustained 2 buckets
let hr_14d = rolling_mean(heart_rate, 14 days)
summarize mean(heart_rate) as "Matched HR"</pre><p class="field-help">Sources: runs, laps, segments. Charts: auto, line, bar, area, histogram, box, heatmap, stacked, none. Use display overlay to combine series. Functions: sum, mean, weighted_mean, median, min, max, count, population stddev, and p10/p25/p75/p90/p95/p99. take limits ranked runs before grouping; limit limits groups. Trends also accept cumulative, change and percent_change. Arithmetic uses km, hours and seconds/km. Missing inputs and zero divisors give missing values.</p></div><div><h3>Metrics · select up to six</h3><div class="metric-reference">{% for key, info in metrics.items() %}<button type="button" class="metric-insert" data-metric="{{ key }}" title="{{ info[2] }}"><code>{{ key }}</code><span>{{ info[1] }}</span></button>{% endfor %}</div><p class="field-help">duration accepts h, min or s; distance accepts km or m. Use has / missing to select recorded sensor coverage.</p></div></div></details>
<details id="guided-controls" class="query-help"><summary>Guided controls</summary><p class="field-help">Adjust the basic clauses without removing additional metrics and filters.</p>
<div class="studio-sentence">Show <label class="sr-only" for="metric">Primary metric</label><select id="metric" name="metric">{% if query.metric not in metrics %}<option value="{{ query.metric }}" selected>{{ query.metric }}</option>{% endif %}{% for key, info in metrics.items() %}<option value="{{ key }}" {{ 'selected' if query.metric == key }}>{{ info[0]|lower }}</option>{% endfor %}</select> and <label class="sr-only" for="secondary">Second metric</label><select id="secondary" name="secondary">{% if query.secondary not in metrics and query.secondary != 'none' %}<option value="{{ query.secondary }}" selected>{{ query.secondary }}</option>{% endif %}<option value="none" {{ 'selected' if query.secondary == 'none' }}>no second metric</option>{% for key, info in metrics.items() %}<option value="{{ key }}" {{ 'selected' if query.secondary == key }}>{{ info[0]|lower }}</option>{% endfor %}</select><br>
{% if query.all_time or query.start or query.days or query.months %}Change the period to the last{% else %}from runs during the last{% endif %} <label class="sr-only" for="weeks">Number of weeks</label><input id="weeks" name="weeks" type="number" min="1" max="104" value="{{ query.weeks }}" required> weeks, grouped by <label class="sr-only" for="group">Grouping</label><select id="group" name="group">{% if query.group == 'bin' %}<option value="bin" selected disabled>{{ query.bin.field }} bins</option>{% endif %}{% for grouping in language.groups %}<option value="{{ grouping }}" {{ 'selected' if query.group == grouping }}>{{ grouping }}</option>{% endfor %}</select>.</div>
@@ -37,19 +43,18 @@ plot pace vs heart_rate</pre><p class="field-help">Functions: sum, mean, weighte
</div>
</details>
</form>
<div class="studio-presets" aria-label="Starting questions"><span class="subtle-label">START WITH</span><a href="{{ url_for('web.stats',query='runs\n during all time\n summarize gap_since_previous_run, distance\n by run\n sort by gap_since_previous_run desc\n take 10\n compare none') }}">Biggest run gaps</a><a href="{{ url_for('web.stats',query='runs\n during last 24 months\n summarize count(), median(pace), sum(distance)\n by weekday\n having count() >= 3 runs\n compare none') }}">Weekday patterns</a><a href="{{ url_for('web.stats',query='runs\n during last 12 weeks\n summarize distance, count()\n by week\n trend rolling 4 buckets\n compare none') }}">Rolling volume</a><a href="{{ url_for('web.stats',metric='distance',secondary='duration') }}">Weekly volume</a><a href="{{ url_for('web.stats',metric='pace',secondary='heart_rate',minimum=10) }}">Long-run progression</a><a href="{{ url_for('web.stats',metric='pace',secondary='heart_rate',hr_min=140,hr_max=150) }}">Pace at similar heart rate</a><a href="{{ url_for('web.stats',metric='cadence',secondary='ground_contact') }}">Running form</a></div>
{% if results %}
<div class="studio-row result-heading"><div><h2>{{ results.count }} matching {{ 'run' if results.count == 1 else 'runs' }}</h2><p>{{ results.start }} – {{ results.end }} · {{ '%.1f'|format(results.distance) }} km{% if query.compare %}<br>Compared with {{ results.previous_start }} – {{ results.previous_end }} · {{ results.prior_count }} runs{% endif %}</p></div></div>
<div class="studio-row result-heading"><div><h2>{{ results.count }} matching {{ query.source }}</h2><p>{{ results.start }} – {{ results.end }} · {{ '%.1f'|format(results.distance) }} km{% if query.compare %}<br>Compared with {{ results.previous_start }} – {{ results.previous_end }} · {{ results.prior_count }} runs{% endif %}</p></div></div>
{% if not results.count %}<div class="panel studio-empty"><h2>No runs match this question.</h2><p>Try a longer period or widen the distance and heart rate filters.</p></div>{% else %}
<div class="studio-metrics">{% for card in results.cards %}<article class="stat-card"><span class="eyebrow">{{ card.label }}</span><div class="stat-value">{{ card.value }}<small>{{ card.unit }}</small></div>{% if query.compare %}<p>Previous: {{ card.previous }} {{ card.unit }}{% if card.delta %} · {{ card.delta }} {{ 'sec/km' if card.unit == '/km' else card.unit }}{% endif %}</p>{% endif %}<p>{{ card.coverage }} of {{ results.count }} runs with usable data</p><details><summary>How this is calculated</summary><p>{{ card.definition }}{% if card.key in ('cadence','power','ground_contact','stride_length') %} Coverage counts runs, not sensor recording time.{% endif %}</p></details></article>{% endfor %}</div>
{% if query.chart != 'none' %}<section class="panel"><div class="section-head"><div><span class="eyebrow">TRENDS / {{ query.group|upper }}</span><h2>{{ 'Your question over time' if query.group in ('day','week','month','year') else 'Your question by ' ~ query.group }}</h2></div><span class="subtle-label">Solid: current · Dashed: previous</span></div><div class="studio-charts">{% for series in results.chart.series %}<div class="studio-chart"><h3>{{ series.label }} <small>{{ series.unit }}</small></h3><div><canvas id="stats-chart-{{ loop.index0 }}" role="img" aria-label="{{ series.label }} grouped by {{ query.group }}"></canvas></div></div>{% endfor %}</div><p class="studio-chart-help">Select a current-period point to inspect its runs. {% if query.trend %}Trend: {{ query.trend }} · Summary cards and the grouped table show the untransformed values.{% else %}Missing data leaves a gap.{% endif %} Time comparisons align by elapsed days; categorical comparisons use the same categories.</p></section>{% endif %}
<div class="studio-metrics">{% for card in results.cards %}<article class="stat-card"><span class="eyebrow">{{ card.label }}</span><div class="stat-value">{{ card.value }}<small>{{ card.unit }}</small></div>{% if query.compare %}<p>Previous: {{ card.previous }} {{ card.unit }}{% if card.delta %} · {{ card.delta }} {{ 'sec/km' if card.unit == '/km' else card.unit }}{% endif %}</p>{% endif %}<p>{{ card.coverage }} of {{ results.count }} {{ query.source }} with usable data</p><details><summary>How this is calculated</summary><p>{{ card.definition }}{% if card.key in ('cadence','power','ground_contact','stride_length') %} Coverage counts runs, not sensor recording time.{% endif %}</p></details></article>{% endfor %}</div>
<section class="panel analysis-results"><div class="section-head"><div><span class="eyebrow">EXPLORE / VISUALIZE</span><h2>Your question, visualized</h2></div><div class="query-actions"><label>Chart <select data-presentation="chart" aria-label="Chart type">{% for type in ('auto','line','bar','area','histogram','box','heatmap','stacked','none') %}<option value="{{ type }}" {{ 'selected' if query.chart == type }}>{{ type|capitalize }}</option>{% endfor %}</select></label><label>Series <select data-presentation="display" aria-label="Series layout"><option value="separate" {{ 'selected' if query.display == 'separate' }}>Separate</option><option value="overlay" {{ 'selected' if query.display == 'overlay' }}>Overlay</option></select></label></div></div><div id="analysis-plots" class="analysis-plots"></div><p class="studio-chart-help">Select a point, box or calendar day to inspect contributing runs. Distribution charts show individual run inputs. Solid: current; dashed: previous. {% if query.trend %}Trend: {{ query.trend }}; cards and tables retain untransformed values.{% endif %}</p></section>
<section id="analysis-insights" class="analysis-insights" aria-label="Baseline and deviations"></section>
{% endif %}
{% if results.chart.scatter %}<section class="panel studio-scatter"><div class="section-head"><div><span class="eyebrow">RELATIONSHIP / RUN AVERAGES</span><h2>{{ results.chart.scatter.x[0] }} vs {{ results.chart.scatter.y[0] }}</h2></div><span class="subtle-label">{% if results.chart.scatter.correlation is not none %}Pearson r = {{ '%.3f'|format(results.chart.scatter.correlation) }}{% else %}Correlation unavailable{% endif %}</span></div><div class="scatter-wrap"><canvas id="stats-scatter" aria-label="Run metric scatter plot" role="img"></canvas></div><p class="studio-chart-help">Each point is one run. Select a point to open that run. Correlation describes association; it does not establish cause.</p></section>{% endif %}
<section class="panel studio-groups"><div class="section-head"><div><span class="eyebrow">ANALYSIS / GROUPED RESULTS</span><h2>{{ query.group|capitalize }} breakdown</h2></div><a class="button secondary" href="{{ url_for('web.stats_export',query=query_text,kind='groups') }}">Export CSV</a></div><div class="activity-table-wrap"><table class="activity-table"><thead><tr><th>GROUP</th><th>RUNS</th>{% for card in results.cards %}<th>{{ card.label|upper }} · {{ card.unit }}</th>{% endfor %}</tr></thead><tbody>{% for group in results.groups %}<tr><td><button type="button" class="group-drill" data-bucket="{{ group.label }}">{{ group.label }}</button></td><td>{{ group.count }}</td>{% for value in group['values'] %}<td>{{ value }}</td>{% endfor %}</tr>{% endfor %}</tbody></table></div></section>
<section class="panel studio-evidence"><div class="section-head"><div><span class="eyebrow">BEHIND THE NUMBERS</span><h2 id="evidence-heading">Contributing runs</h2></div><div class="studio-row"><a class="button secondary" href="{{ url_for('web.stats_export',query=query_text,kind='runs') }}">Export runs</a><label class="studio-period" for="evidence-period">Period <select id="evidence-period"><option value="">All results</option>{% for label in results.chart.labels %}<option>{{ label }}</option>{% endfor %}</select></label><button type="button" class="text-button" id="clear-selection" hidden>Show all results</button></div></div><div class="activity-table-wrap"><table class="activity-table"><thead><tr><th>RUN / LOCAL DATE</th><th>DISTANCE</th>{% for card in results.evidence_columns %}<th>{{ card.label|upper }} · {{ card.unit }}</th>{% endfor %}</tr></thead><tbody id="evidence-body">{% for row in results.rows %}<tr><td><a href="{{ url_for('web.activity',activity_id=row.id) }}"><strong>{{ row.title }}</strong><br><small>{{ row.date }}</small></a>{% if results.show_previous %}<small class="previous-run">Previous: {% if row.previous_run %}<a href="{{ url_for('web.activity',activity_id=row.previous_run.id) }}">{{ row.previous_run.date }} · {{ row.previous_run.title }}</a>{% else %}No earlier recorded run{% endif %}</small>{% endif %}</td><td>{{ row.distance }} km</td>{% for value in row['values'] %}<td>{{ value }}</td>{% endfor %}</tr>{% else %}<tr><td colspan="{{ results.cards|length + 2 }}">No matching runs.</td></tr>{% endfor %}</tbody></table></div><div class="studio-pagination"><span>Page {{ results.page }} of {{ results.pages }} · up to 25 runs per page</span>{% if results.page > 1 %}<a class="button secondary" href="{{ url_for('web.stats',query=query_text,page=results.page-1) }}">Previous</a>{% endif %}{% if results.page < results.pages %}<a class="button secondary" href="{{ url_for('web.stats',query=query_text,page=results.page+1) }}">Next</a>{% endif %}</div></section>
<script id="stats-data" type="application/json">{{ results.chart|tojson }}</script>
<section class="panel studio-groups"><div class="section-head"><div><span class="eyebrow">ANALYSIS / GROUPED RESULTS</span><h2>{{ query.group|capitalize }} breakdown</h2></div><a class="button secondary" href="{{ url_for('web.stats_export',query=query_text,kind='groups') }}">Export CSV</a></div><div class="activity-table-wrap"><table class="activity-table"><thead><tr><th>GROUP</th><th>{{ query.source|upper }}</th>{% for card in results.cards %}<th>{{ card.label|upper }} · {{ card.unit }}</th>{% endfor %}</tr></thead><tbody>{% for group in results.groups %}<tr><td><button type="button" class="group-drill" data-bucket="{{ group.label }}">{{ group.label }}</button></td><td>{{ group.count }}</td>{% for value in group['values'] %}<td>{{ value }}</td>{% endfor %}</tr>{% endfor %}</tbody></table></div></section>
<section class="panel studio-evidence" id="studio-evidence"><div class="section-head"><div><span class="eyebrow">BEHIND THE NUMBERS</span><h2 id="evidence-heading">Contributing {{ query.source }}</h2></div><div class="studio-row"><a class="button secondary" href="{{ url_for('web.stats_export',query=query_text,kind='runs') }}">Export runs</a><label class="studio-period" for="evidence-period">Period <select id="evidence-period"><option value="">All results</option>{% for label in results.chart.labels + results.chart.drill_labels %}<option>{{ label }}</option>{% endfor %}</select></label><button type="button" class="text-button" id="clear-selection" hidden>Show all results</button></div></div><div class="activity-table-wrap"><table class="activity-table"><thead><tr><th>{{ query.source|upper }} / LOCAL DATE</th><th>DISTANCE</th>{% for card in results.evidence_columns %}<th>{{ card.label|upper }} · {{ card.unit }}</th>{% endfor %}</tr></thead><tbody id="evidence-body">{% for row in results.rows %}<tr><td><a href="{{ url_for('web.activity',activity_id=row.id) }}"><strong>{{ row.title }}</strong><br><small>{{ row.date }}</small></a>{% if results.show_previous %}<small class="previous-run">Previous: {% if row.previous_run %}<a href="{{ url_for('web.activity',activity_id=row.previous_run.id) }}">{{ row.previous_run.date }} · {{ row.previous_run.title }}</a>{% else %}No earlier recorded run{% endif %}</small>{% endif %}</td><td>{{ row.distance }} km</td>{% for value in row['values'] %}<td>{{ value }}</td>{% endfor %}</tr>{% else %}<tr><td colspan="{{ results.cards|length + 2 }}">No matching runs.</td></tr>{% endfor %}</tbody></table></div><div class="studio-pagination"><span>Page {{ results.page }} of {{ results.pages }} · up to 25 {{ query.source }} per page</span>{% if results.page > 1 %}<a class="button secondary" href="{{ url_for('web.stats',query=query_text,page=results.page-1) }}">Previous</a>{% endif %}{% if results.page < results.pages %}<a class="button secondary" href="{{ url_for('web.stats',query=query_text,page=results.page+1) }}">Next</a>{% endif %}</div></section>
<script id="stats-data" type="application/json">{{ results.chart|tojson }}</script><script id="insights-data" type="application/json">{{ results.insights|tojson }}</script>
{% endif %}
<p class="analysis-semantics field-help">Gaps, rolling metrics and streaks use your complete history before filters. Arithmetic and filters apply per run; summaries apply per group. Contributing runs show the individual input values for each summary. Empty sums remain missing; count is zero. Group ranking/limits and having select the displayed cohort before chart trends.</p><div id="studio-status" role="status" class="field-help"></div>
<p class="analysis-semantics field-help">Sources: runs, recorded laps, or five-minute moving segments. Segments exclude recording gaps and pauses; partial segments remain queryable. Gaps, rolling metrics and streaks use complete run history before filters. Arithmetic and filters apply per run; summaries apply per group. Contributing runs show the individual input values for each summary. Empty sums remain missing; count is zero. Group ranking/limits and having select the displayed cohort before chart trends.</p><div id="studio-status" role="status" class="field-help"></div>
</div>
{% endblock %}
{% block scripts %}<script src="{{ url_for('static',filename='vendor/codemirror.js') }}"></script><script src="{{ url_for('static',filename='vendor/show-hint.js') }}"></script><script src="{{ url_for('static',filename='stats-editor.js') }}"></script><script src="{{ url_for('static',filename='vendor/chart.umd.js') }}"></script><script src="{{ url_for('static',filename='stats.js') }}"></script>{% endblock %}
{% block scripts %}<script src="{{ url_for('static',filename='vendor/codemirror.js') }}"></script><script src="{{ url_for('static',filename='vendor/show-hint.js') }}"></script><script src="{{ url_for('static',filename='stats-editor.js') }}"></script><script src="{{ url_for('static',filename='vendor/chart.umd.js') }}"></script><script src="{{ url_for('static',filename='analysis-charts.js') }}"></script><script src="{{ url_for('static',filename='stats.js') }}"></script><script src="{{ url_for('static',filename='analysis-library.js') }}"></script>{% endblock %}