<label>Compare inputs<selectname="source"><optionvalue="intervals">Matched continuous windows · one point per run</option><optionvalue="runs">Whole runs</option></select></label>
<label>Pace from · min/km<inputname="pace_low"value="5:20"pattern="[0-9]+:[0-5][0-9]"required></label>
<label>Pace to · min/km<inputname="pace_high"value="5:40"pattern="[0-9]+:[0-5][0-9]"required></label>
<label>Max ascent per km · m/km<inputname="hills"type="number"min="0"max="1000"value="10"required></label>
<label>Whole-run max ascent · m/km<inputname="hills"type="number"min="0"max="1000"value="10"required></label>
<label>Window max absolute grade · %<inputname="grade"type="number"min="0"max="15"value="2"required></label>
<label>Exclude first elapsed minutes<inputname="warmup"type="number"min="0"max="60"value="10"required></label>
<label>Matched minutes per run<inputname="matched"type="number"min="1"max="180"value="3"required></label>
<label>Minimum usable inputs per week<inputname="minimum"type="number"min="2"max="1000"value="3"required></label>
<label>Minimum independent runs per week<inputname="minimum"type="number"min="2"max="1000"value="3"required></label>
<label><inputname="use_baseline"type="checkbox"> Add an earlier baseline and persistent weekly deviation rule</label>
</div>
<pclass="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>
<pclass="field-help">Continuous windows require ≤15% speed variation, ≤2 percentage points of grade variation, ≥90% HR coverage and ≥25 seconds. Each qualifying run contributes one observation after matching. An earlier baseline needs at least ten independent runs; it may be unavailable for a short history.</p>
<pclass="field-help">These observations stay private when you share a run. Numeric observations are available in Stats.</p><buttonclass="button primary">Save session context</button></form></section>{% endif %}
<pclass="field-help">Set two boundaries for your lower, middle and upper effort bands. Use your own chosen thresholds; these labels do not infer physiological zones.</p>
<pclass="field-help">Recorded HR at a boundary belongs to the higher band. Clear both fields to remove your bands. Stats exposes actual recorded minutes in each band.</p><buttonclass="button primary">Save bands</button></form></section>
{% if connection %}<sectionclass="panel settings-card"><spanclass="eyebrow">DAILY RECOVERY</span><h2>Recovery observations</h2><p>Import private resting HR, overnight HRV, sleep duration and daily stress from Garmin.</p><pclass="field-help">{{ connection.health_message }} Historical observations fill gradually during hourly sync. Missing watch observations remain missing.</p>
<divclass="detail-bottom"id="splits"><sectionclass="panel"><divclass="section-head"><h2>Laps & splits</h2><spanclass="subtle-label">{{ analysis.laps|length }} recorded laps</span></div><divclass="activity-table-wrap"><tableclass="splits-table"><thead><tr><th>LAP</th><th>DISTANCE</th><th>TIME</th><th>PACE</th><th>GAP EST.</th><th>HR</th></tr></thead><tbody>{% for lap in analysis.laps %}<tr><td>{{ loop.index }}</td><td>{{ '%.2f'|format(lap.distance / 1000) }} km</td><td>{{ lap.duration|duration }}</td><td>{{ pace_text(lap.pace) }}</td><td>{{ pace_text(lap.gap) }}</td><td>{{ lap.heart_rate|int if lap.heart_rate else '—' }}</td></tr>{% else %}<tr><tdcolspan="6">No laps recorded.</td></tr>{% endfor %}</tbody></table></div></section>
{% if owner_view %}<sectionclass="panel publishing-panel"><spanclass="eyebrow">YOUR RUN, YOUR CHOICE</span><h2>Sharing settings</h2><formmethod="post"action="{{ url_for('web.edit_activity',activity_id=activity.id) }}"class="stack"><inputtype="hidden"name="csrf_token"value="{{ csrf_token() }}"><label>Activity title<inputname="title"value="{{ activity.title }}"maxlength="160"required></label><label>Who can see this activity in Stride?<selectname="visibility"><optionvalue="private"{{'selected'ifactivity.visibility =='private'}}>Only me</option><optionvalue="friends"{{'selected'ifactivity.visibility =='friends'}}>Accepted friends</option></select></label><pclass="field-help">Accepted friends see the full route and measurements of shared activities. Public link access is separate: anyone with a copied link can view this run, including its full route and measurements, until you disable the link.</p><buttonclass="button primary">Save changes {{ icon('arrow') }}</button></form><formid="revoke-share"method="post"action="{{ url_for('web.revoke_activity_share',activity_id=activity.id) }}"{{'hidden'ifnotactivity.share_token}}><inputtype="hidden"name="csrf_token"value="{{ csrf_token() }}"><buttonclass="button secondary">Disable public link</button></form></section>{% endif %}</div>
<detailsclass="panel methodology"id="method"><summary>About these numbers <span>Recorded data vs. estimates +</span></summary><div><p><strong>GAP is an estimate, not a Garmin or Strava value.</strong> We use the <ahref="https://doi.org/10.1152/japplphysiol.01177.2001"target="_blank"rel="noopener noreferrer">Minetti running energy-cost model</a> to estimate an equivalent flat pace from the recorded slope. Elevation uses a five-sample median and a roughly 60 m distance window. Grades outside ±30%, missing data, and recording gaps over 10 seconds are excluded. GAP is only calculated for running.</p><p>Average GAP divides time by equivalent flat distance over eligible moving samples. Coverage indicates how much recorded distance contributed; lap GAP uses the same method. Stops and missing data can make its sample coverage differ from timer pace. Wind, surface, heat, fatigue and individual running economy are not modelled.</p><p><strong>Heart rate stays measured.</strong> A universal grade correction would imply precision we don't have. Terrain summaries show time-weighted recorded HR, without trying to remove the effect of hills. Cadence is converted from running cycles/min to steps/min. Missing measurements are shown as unavailable, never zero-filled.</p><p>Map colours use this activity’s 10th–90th percentile range, with extremes saturated. Direction arrows follow recording order; they are not navigation instructions. Routes are simplified for display, preserving segment endpoints and GPS gaps. Map measurements and chart traces are sampled for display; computations use all available records.</p></div></details>
<pclass="field-help">Disconnect deletes the server session and stops scheduled sync. It keeps imported activities and does not revoke other session copies.</p>
</div></section></div>
<divclass="settings-grid">{% include '_training_settings.html' %}</div>
<divid="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') }}">
<divid="stats-studio"data-owner="{{ current_user.id }}"data-source="{{ results.source if results else 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') }}">
<divclass="page-heading"><div><spanclass="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><divclass="query-actions"><buttontype="button"class="button secondary"id="save-view">Save analysis</button><buttontype="button"class="button primary"id="pin-analysis">Pin live analysis</button></div></div>
<divid="query-editor"><labelclass="sr-only"for="query-input">Running stats query</label><textareaid="query-input"name="query"spellcheck="false"maxlength="4000"rows="9">{{ submitted_query if submitted_query else query_text }}</textarea></div>
<divclass="studio-row query-state"><spanid="query-validation"role="status">Checking query...</span><spanclass="query-shortcut"><kbd>Ctrl / Cmd Enter</kbd> to run</span></div>
<divid="query-diagnostics"aria-live="polite">{% if error %}<pclass="studio-error"role="alert">{{ error }}</p>{% endif %}</div>
<divclass="studio-row studio-bottom"><spanclass="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><buttonclass="button primary"id="run-query"type="submit">Run query {{ icon('arrow') }}</button></div>
<divclass="studio-row studio-bottom"><spanclass="field-help">{{ query.source|capitalize }} · {{ current_user.timezone }}<br>Lower pace values mean faster running.</span><buttonclass="button primary"id="run-query"type="submit">Run query {{ icon('arrow') }}</button></div>
<detailsclass="query-help"><summary>Language reference & examples</summary><divclass="query-reference"><div><h3>Filter and compare</h3><pre>during last 6 months
where distance between 8 and 20 km
and pace <= 5:30 /km
@@ -29,7 +29,7 @@ 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><pclass="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><divclass="metric-reference">{% for key, info in metrics.items() %}<buttontype="button"class="metric-insert"data-metric="{{ key }}"title="{{ info[2] }}"><code>{{ key }}</code><span>{{ info[1] }}</span></button>{% endfor %}</div><pclass="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>
summarize mean(heart_rate) as "Matched HR"</pre><pclass="field-help">Sources: runs, laps, segments, intervals, efforts, days, health. 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><divclass="metric-reference">{% for key, info in metrics.items() %}<buttontype="button"class="metric-insert"data-metric="{{ key }}"title="{{ info[2] }}"><code>{{ key }}</code><span>{{ info[1] }}</span></button>{% endfor %}</div><pclass="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>
<detailsid="guided-controls"class="query-help"><summary>Guided controls</summary><pclass="field-help">Adjust the basic clauses without removing additional metrics and filters.</p>
<divclass="studio-sentence">Show <labelclass="sr-only"for="metric">Primary metric</label><selectid="metric"name="metric">{% if query.metric not in metrics %}<optionvalue="{{ query.metric }}"selected>{{ query.metric }}</option>{% endif %}{% for key, info in metrics.items() %}<optionvalue="{{ key }}"{{'selected'ifquery.metric ==key}}>{{ info[0]|lower }}</option>{% endfor %}</select> and <labelclass="sr-only"for="secondary">Second metric</label><selectid="secondary"name="secondary">{% if query.secondary not in metrics and query.secondary != 'none' %}<optionvalue="{{ query.secondary }}"selected>{{ query.secondary }}</option>{% endif %}<optionvalue="none"{{'selected'ifquery.secondary =='none'}}>no second metric</option>{% for key, info in metrics.items() %}<optionvalue="{{ key }}"{{'selected'ifquery.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 %} <labelclass="sr-only"for="weeks">Number of weeks</label><inputid="weeks"name="weeks"type="number"min="1"max="104"value="{{ query.weeks }}"required> weeks, grouped by <labelclass="sr-only"for="group">Grouping</label><selectid="group"name="group">{% if query.group == 'bin' %}<optionvalue="bin"selecteddisabled>{{ query.bin.field }} bins</option>{% endif %}{% for grouping in language.groups %}<optionvalue="{{ grouping }}"{{'selected'ifquery.group ==grouping}}>{{ grouping }}</option>{% endfor %}</select>.</div>
@@ -44,17 +44,17 @@ summarize mean(heart_rate) as "Matched HR"</pre><p class="field-help">Sources: r
{% if not results.count %}<divclass="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 %}
<divclass="studio-metrics">{% for card in results.cards %}<articleclass="stat-card"><spanclass="eyebrow">{{ card.label }}</span><divclass="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>
{% if results.coverage %}<pclass="field-help">Evidence: {{ results.coverage.sessions }} independent running sessions · {{ results.coverage.minutes }} recorded moving/timer minutes. Daily recovery observations are separate from running-session coverage.</p>{% endif %}<divclass="studio-metrics">{% for card in results.cards %}<articleclass="stat-card"><spanclass="eyebrow">{{ card.label }}</span><divclass="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 }} {{ results.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>
<sectionclass="panel analysis-results"><divclass="section-head"><div><spanclass="eyebrow">EXPLORE / VISUALIZE</span><h2>Your question, visualized</h2></div><divclass="query-actions"><label>Chart <selectdata-presentation="chart"aria-label="Chart type">{% for type in ('auto','line','bar','area','histogram','box','heatmap','stacked','none') %}<optionvalue="{{ type }}"{{'selected'ifquery.chart ==type}}>{{ type|capitalize }}</option>{% endfor %}</select></label><label>Series <selectdata-presentation="display"aria-label="Series layout"><optionvalue="separate"{{'selected'ifquery.display =='separate'}}>Separate</option><optionvalue="overlay"{{'selected'ifquery.display =='overlay'}}>Overlay</option></select></label></div></div><divid="analysis-plots"class="analysis-plots"></div><pclass="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>
<sectionid="analysis-insights"class="analysis-insights"aria-label="Baseline and deviations"></section>
{% endif %}
<sectionclass="panel studio-groups"><divclass="section-head"><div><spanclass="eyebrow">ANALYSIS / GROUPED RESULTS</span><h2>{{ query.group|capitalize }} breakdown</h2></div><aclass="button secondary"href="{{ url_for('web.stats_export',query=query_text,kind='groups') }}">Export CSV</a></div><divclass="activity-table-wrap"><tableclass="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><buttontype="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>
<sectionclass="panel studio-evidence"id="studio-evidence"><divclass="section-head"><div><spanclass="eyebrow">BEHIND THE NUMBERS</span><h2id="evidence-heading">Contributing {{ query.source }}</h2></div><divclass="studio-row"><aclass="button secondary"href="{{ url_for('web.stats_export',query=query_text,kind='runs') }}">Export runs</a><labelclass="studio-period"for="evidence-period">Period <selectid="evidence-period"><optionvalue="">All results</option>{% for label in results.chart.labels + results.chart.drill_labels %}<option>{{ label }}</option>{% endfor %}</select></label><buttontype="button"class="text-button"id="clear-selection"hidden>Show all results</button></div></div><divclass="activity-table-wrap"><tableclass="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><tbodyid="evidence-body">{% for row in results.rows %}<tr><td><ahref="{{ url_for('web.activity',activity_id=row.id) }}"><strong>{{ row.title }}</strong><br><small>{{ row.date }}</small></a>{% if results.show_previous %}<smallclass="previous-run">Previous: {% if row.previous_run %}<ahref="{{ 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><tdcolspan="{{ results.cards|length + 2 }}">No matching runs.</td></tr>{% endfor %}</tbody></table></div><divclass="studio-pagination"><span>Page {{ results.page }} of {{ results.pages }} · up to 25 {{ query.source }} per page</span>{% if results.page > 1 %}<aclass="button secondary"href="{{ url_for('web.stats',query=query_text,page=results.page-1) }}">Previous</a>{% endif %}{% if results.page <results.pages%}<aclass="button secondary"href="{{ url_for('web.stats',query=query_text,page=results.page+1) }}">Next</a>{% endif %}</div></section>
<sectionclass="panel studio-groups"><divclass="section-head"><div><spanclass="eyebrow">ANALYSIS / GROUPED RESULTS</span><h2>{{ query.group|capitalize }} breakdown</h2></div><aclass="button secondary"href="{{ url_for('web.stats_export',query=query_text,kind='groups') }}">Export CSV</a></div><divclass="activity-table-wrap"><tableclass="activity-table"><thead><tr><th>GROUP</th><th>{{ results.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><buttontype="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>
<sectionclass="panel studio-evidence"id="studio-evidence"><divclass="section-head"><div><spanclass="eyebrow">BEHIND THE NUMBERS</span><h2id="evidence-heading">Contributing {{ results.source }}</h2></div><divclass="studio-row"><aclass="button secondary"href="{{ url_for('web.stats_export',query=query_text,kind='runs') }}">Export inputs</a><labelclass="studio-period"for="evidence-period">Period <selectid="evidence-period"><optionvalue="">All results</option>{% for label in results.chart.labels + results.chart.drill_labels %}<option>{{ label }}</option>{% endfor %}</select></label><buttontype="button"class="text-button"id="clear-selection"hidden>Show all results</button></div></div><divclass="activity-table-wrap"><tableclass="activity-table"><thead><tr><th>{{ results.source|upper }} / LOCAL DATE</th><th>DISTANCE</th>{% for card in results.evidence_columns %}<th>{{ card.label|upper }} · {{ card.unit }}</th>{% endfor %}</tr></thead><tbodyid="evidence-body">{% for row in results.rows %}<tr><td>{% if row.id %}<ahref="{{ url_for('web.activity',activity_id=row.id) }}"><strong>{{ row.title }}</strong><br><small>{{ row.date }}</small></a>{% else %}<strong>{{ row.title }}</strong><br><small>{{ row.date }}</small>{% for id in row.activity_ids %}<ahref="{{ url_for('web.activity', activity_id=id) }}"> · Run #{{ id }}</a>{% endfor %}{% endif %}{% if results.show_previous %}<smallclass="previous-run">Previous: {% if row.previous_run %}<ahref="{{ 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><tdcolspan="{{ results.cards|length + 2 }}">No matching runs.</td></tr>{% endfor %}</tbody></table></div><divclass="studio-pagination"><span>Page {{ results.page }} of {{ results.pages }} · up to 25 {{ query.source }} per page</span>{% if results.page > 1 %}<aclass="button secondary"href="{{ url_for('web.stats',query=query_text,page=results.page-1) }}">Previous</a>{% endif %}{% if results.page <results.pages%}<aclass="button secondary"href="{{ url_for('web.stats',query=query_text,page=results.page+1) }}">Next</a>{% endif %}</div></section>
<pclass="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><divid="studio-status"role="status"class="field-help"></div>
<pclass="analysis-semantics field-help">Sources include runs, laps, moving segments, continuous windows, best efforts and daily training/recovery observations. Filters select inputs before optional per-run reduction; summaries then apply per group. Recording windows from the same session are dependent. Daily zeros mean no recorded running, rather than no other training; missing recovery and sensor observations stay missing. Run-history calculations use complete history before filters. Empty sums remain missing; count is zero.</p><divid="studio-status"role="status"class="field-help"></div>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.