Files
Stride/app/static/analysis-library.js
T

59 lines
5.9 KiB
JavaScript

(() => {
'use strict';
const root=document.getElementById('stats-studio');if(!root)return;
const status=document.getElementById('studio-status'),library=document.getElementById('analysis-library');
const dialog=document.getElementById('save-analysis-dialog'),form=document.getElementById('save-analysis-form');
const initial=JSON.parse(document.getElementById('selected-analysis').textContent);
let selected=initial,editing=null,analyses=[];
const el=window.StrideCharts.element;
const query=()=>window.strideQueryEditor?.getValue()||document.getElementById('query-input').value;
async function api(path='',method='GET',body){
const response=await fetch('/stats/analyses'+path,{method,headers:{'Content-Type':'application/json','X-CSRFToken':root.dataset.csrf},...(body?{body:JSON.stringify(body)}:{})});
const payload=response.status===204?{}:await response.json();if(!response.ok)throw new Error(payload.error||'Could not save analysis. Try again.');return payload;
}
function button(text,callback){const b=el('button','text-button',text);b.type='button';b.onclick=callback;return b;}
async function load(){try{analyses=(await api()).analyses;if(selected)selected=analyses.find(a=>a.id===selected.id)||null;render();}catch(error){library.textContent=error.message;}}
function render(){
library.replaceChildren();
if(!analyses.length){library.append(el('p','field-help','Save a question to build an investigation. Your library and pinned widgets follow your account.'));return;}
const collections=[...new Set(analyses.map(a=>a.collection))];
collections.forEach(collection=>{
const group=el('details','analysis-collection');
group.append(el('summary','',`${collection} · ${analyses.filter(a=>a.collection===collection).length} analyses`));
const grid=el('div','analysis-library-grid');
analyses.filter(a=>a.collection===collection).forEach(a=>{
const card=el('article','analysis-library-item'+(selected?.id===a.id?' is-current':''));const link=el('a','analysis-library-name',a.name);link.href='/stats?'+new URLSearchParams({analysis:a.id});card.append(link);
if(a.pinned)card.append(el('span','pill','Pinned'));
if(a.notes)card.append(el('p','field-help',a.notes));
const actions=el('div','query-actions');
actions.append(button(a.pinned?'Unpin':'Pin',async()=>{try{await api('/'+a.id,'PATCH',{pinned:!a.pinned});await load();status.textContent=a.pinned?'Removed from dashboard.':'Pinned to your live dashboard.';}catch(e){status.textContent=e.message;}}),button('Duplicate',()=>open(a,true)),button('Delete',async()=>{if(!confirm(`Delete “${a.name}” and its dashboard widget?`))return;try{await api('/'+a.id,'DELETE');if(selected?.id===a.id)selected=null;await load();status.textContent='Analysis deleted.';}catch(e){status.textContent=e.message;}}));
card.append(actions);grid.append(card);
});group.append(grid);library.append(group);
});
}
function open(a=selected,copy=false,pin=false){
editing=copy?null:a;form.reset();form.elements.name.value=a?(copy?`${a.name} · copy`:a.name):'New running analysis';form.elements.collection.value=a?.collection||'My analyses';form.elements.notes.value=a?.notes||'';form.elements.view.value=a?.view||'chart';form.elements.width.value=a?.width||'half';form.elements.pinned.checked=pin||a?.pinned||false;document.getElementById('save-analysis-title').textContent=editing?'Update analysis':'Save analysis';document.getElementById('save-analysis-error').textContent='';dialog.dataset.source=copy&&a?a.query:'';dialog.showModal();
}
document.getElementById('save-view').onclick=()=>open();
document.getElementById('pin-analysis').onclick=()=>open(selected,false,true);
document.getElementById('close-analysis-dialog').onclick=()=>dialog.close();
document.getElementById('save-analysis-copy').onclick=()=>{editing=null;document.getElementById('save-analysis-title').textContent='Save new analysis';};
form.onsubmit=async event=>{
event.preventDefault();const submit=document.getElementById('save-analysis-submit');submit.disabled=true;
const payload=Object.fromEntries(new FormData(form));payload.pinned=form.elements.pinned.checked;payload.query=dialog.dataset.source||query();
try{selected=(await api(editing?'/'+editing.id:'',editing?'PATCH':'POST',payload)).analysis;dialog.close();location.assign('/stats?'+new URLSearchParams({analysis:selected.id}));}
catch(error){document.getElementById('save-analysis-error').textContent=error.message;}
finally{submit.disabled=false;}
};
document.getElementById('new-analysis').onclick=()=>location.assign('/stats');
document.getElementById('matched-effort-form').onsubmit=event=>{
event.preventDefault();const fields=Object.fromEntries(new FormData(event.target));
const text=`${fields.source}\n during last ${fields.weeks} weeks\n where pace between ${fields.pace_low} and ${fields.pace_high} /km\n and elevation_per_km <= ${fields.hills} m/km\n and has heart_rate\n${fields.source==='segments'?' and pace_variability <= 5 %\n and sensor_coverage >= 80 %\n and duration >= 4 min\n':''} summarize weighted_mean(heart_rate) as "Matched-pace HR"\n by week\n compare none\n baseline previous ${fields.baseline} days\n flag above baseline 1.5 sd min ${fields.minimum} runs sustained 2 buckets\n chart line`;
location.assign('/stats?'+new URLSearchParams({query:text}));
};
document.querySelectorAll('[data-presentation]').forEach(control=>control.onchange=()=>{
const clause=control.dataset.presentation,text=`${clause} ${control.value}`;const lines=query().split('\n');const i=lines.findIndex(line=>line.trim().startsWith(clause+' '));if(i<0)lines.push(' '+text);else lines[i]=' '+text;if(clause==='chart'&&control.value==='heatmap'){const g=lines.findIndex(line=>line.trim().startsWith('by '));if(g<0)lines.push(' by day');else lines[g]=' by day';}window.strideQueryEditor.setValue(lines.join('\n'));document.getElementById('explorer-form').requestSubmit();
});
load();
})();