(() => { 'use strict'; const root = document.getElementById('stats-studio'); if (!root) return; const form = document.getElementById('explorer-form'); const input = document.getElementById('query-input'); const status = document.getElementById('studio-status'); const value = name => form.elements.namedItem(name).value; const storageKey = `stride.stats.views.${root.dataset.owner}`; const views = document.getElementById('saved-views'); let saved = []; try { const parsed = JSON.parse(localStorage.getItem(storageKey) || '[]'); if (Array.isArray(parsed)) saved = parsed.filter(x => typeof x.name === 'string' && typeof x.query === 'string').slice(0, 12); } catch (_) { /* Storage may be unavailable. */ } function renderSaved() { views.replaceChildren(); saved.forEach((view, index) => { const wrapper = document.createElement('span'); const link = document.createElement('a'); link.textContent = view.name; link.href = '/stats?' + new URLSearchParams({query: view.query}); const remove = document.createElement('button'); remove.type = 'button'; remove.textContent = '×'; remove.setAttribute('aria-label', `Remove ${view.name}`); remove.onclick = () => { saved.splice(index, 1); persist(); renderSaved(); }; wrapper.append(link, remove); views.append(wrapper); }); } function persist() { try { localStorage.setItem(storageKey, JSON.stringify(saved)); status.textContent = 'Saved views are stored in this browser.'; } catch (_) { status.textContent = 'Browser storage is unavailable; bookmark this results page to save the question.'; } } document.getElementById('save-view').onclick = () => { const name = window.prompt('Name this view', `${value('metric').replaceAll('_', ' ')} · last ${value('weeks')} weeks`); if (!name || !name.trim()) return; const query = window.strideQueryEditor ? window.strideQueryEditor.getValue() : input.value; saved = [{name: name.trim().slice(0, 80), query}, ...saved.filter(v => v.query !== query)].slice(0, 12); persist(); renderSaved(); }; renderSaved(); const data = document.getElementById('stats-data'); if (!data || typeof Chart === 'undefined') return; const chartData = JSON.parse(data.textContent); const pace = n => { const s = Math.round(n); return `${Math.floor(s/60)}:${String(s%60).padStart(2,'0')}`; }; const clear = document.getElementById('clear-selection'); const tbody = document.getElementById('evidence-body'); const original = [...tbody.children].map(row => row.cloneNode(true)); const pagination = root.querySelector('.studio-pagination'); const period = document.getElementById('evidence-period'); const originalPagination = [...pagination.children].map(node => node.cloneNode(true)); let pending = null; clear.onclick = () => { pending?.abort(); period.value = ''; tbody.replaceChildren(...original.map(row => row.cloneNode(true))); pagination.replaceChildren(...originalPagination.map(node => node.cloneNode(true))); clear.hidden = true; document.getElementById('evidence-heading').textContent = 'Contributing runs'; status.textContent = ''; }; period.onchange = () => { if (period.value) loadBucket(period.value); else clear.click(); }; async function loadBucket(bucket, page = 1) { pending?.abort(); const controller = new AbortController(); pending = controller; const url = new URL('/stats/runs', location.origin); url.search = new URLSearchParams({query:root.dataset.query,bucket,page}); status.textContent = 'Loading contributing runs…'; try { const response = await fetch(url, {signal:controller.signal}); if (!response.ok) throw new Error(); const result = await response.json(); tbody.replaceChildren(); result.rows.forEach(row => { const tr = document.createElement('tr'); const td = document.createElement('td'); const a = document.createElement('a'); a.href = `/activities/${row.id}`; a.textContent = `${row.title} · ${row.date}`; td.append(a); tr.append(td); [`${row.distance} km`, ...row.values].forEach(text => { const cell = document.createElement('td'); cell.textContent = text; tr.append(cell); }); tbody.append(tr); }); document.getElementById('evidence-heading').textContent = `${bucket} · ${result.count} runs`; period.value = bucket; pagination.replaceChildren(); const label = document.createElement('span'); label.textContent = `Page ${result.page} of ${result.pages} · selected period`; pagination.append(label); [[result.page > 1, 'Previous', result.page - 1], [result.page < result.pages, 'Next', result.page + 1]].forEach(([show, name, target]) => { if (!show) return; const button = document.createElement('button'); button.type = 'button'; button.className = 'button secondary'; button.textContent = name; button.onclick = () => loadBucket(bucket, target); pagination.append(button); }); clear.hidden = false; status.textContent = 'Selected period loaded.'; } catch (error) { if (error.name !== 'AbortError') status.textContent = 'Could not load this period. Try again.'; } } chartData.series.forEach((series, index) => { const canvas = document.getElementById(`stats-chart-${index}`); if (!canvas) return; new Chart(canvas, { type: ['distance', 'duration', 'elevation'].includes(series.key) ? 'bar' : 'line', data: {labels: chartData.labels, datasets: [ {label:'Current', data:series.values, borderColor:'#f48459', backgroundColor:'#f4845940', borderWidth:2, pointRadius:3, tension:.2, spanGaps:false}, ...(series.previous.length ? [{label:'Previous (aligned)', data:series.previous, borderColor:'#9ba4a5', backgroundColor:'#9ba4a526', borderDash:[5,5], borderWidth:2, pointRadius:2, spanGaps:false}] : []) ]}, options: {responsive:true, maintainAspectRatio:false, animation:false, plugins:{legend:{labels:{color:'#bcc4c5'}}, tooltip:{callbacks:{label:c => `${c.dataset.label}: ${c.parsed.y == null ? '—' : series.key === 'pace' ? pace(c.parsed.y) : c.parsed.y.toFixed(1)} ${series.unit}`}}}, scales:{x:{ticks:{color:'#9ba4a5',maxTicksLimit:5},grid:{display:false}},y:{title:{display:true,text:series.unit,color:'#9ba4a5'},ticks:{color:'#9ba4a5',callback:n=>series.key==='pace'?pace(n):new Intl.NumberFormat(undefined,{maximumFractionDigits:2}).format(n)},grid:{color:'#303638'}}}, onClick:(_, elements) => { const point = elements.find(e => e.datasetIndex === 0); if (!point) return; const bucket = chartData.labels[point.index]; loadBucket(bucket); } } }); }); })();