diff --git a/rowers/forms.py b/rowers/forms.py index 7387d8fe..ae17b596 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -722,7 +722,8 @@ class TrendFlexModalForm(forms.Form): label='Only Ranking Pieces', required=False) - + + # This form sets options for the summary stats page class StatsOptionsForm(forms.Form): includereststrokes = forms.BooleanField(initial=False,label='Include Rest Strokes',required=False) @@ -1181,10 +1182,21 @@ class FlexOptionsForm(forms.Form): ('line','Line Plot'), ('scatter','Scatter Plot'), ) - plottype = forms.ChoiceField(choices=plotchoices,initial='scatter', + plottype = forms.ChoiceField(choices=plotchoices,initial='line', label='Chart Type') - +class ForceCurveOptionsForm(forms.Form): + includereststrokes = forms.BooleanField(initial=False, required = False, + label='Include Rest Strokes') + plotchoices = ( + ('line','Force Curve Collection Plot'), + ('scatter','Peak Force Scatter Plot'), + ('none','Only aggregrate data') + ) + plottype = forms.ChoiceField(choices=plotchoices,initial='scatter', + label='Individual Stroke Chart Type') + + class FlexAxesForm(forms.Form): axchoices = list( (ax[0],ax[1]) for ax in axes if ax[0] not in ['cumdist','None'] diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 94a4548b..86217e08 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -17,6 +17,7 @@ from math import pi from django.utils import timezone from bokeh.palettes import Dark2_8 as palette +from bokeh.models.glyphs import MultiLine import itertools from bokeh.plotting import figure, ColumnDataSource, Figure,curdoc from bokeh.models import CustomJS,Slider, TextInput,BoxAnnotation @@ -64,6 +65,7 @@ activate(settings.TIME_ZONE) thetimezone = get_current_timezone() from scipy.stats import linregress,percentileofscore +from scipy.spatial import ConvexHull,Delaunay from scipy import optimize from scipy.signal import savgol_filter from scipy.interpolate import griddata @@ -373,7 +375,7 @@ def interactive_activitychart(workouts,startdate,enddate,stack='type'): script,div = components(p) return script,div -def interactive_forcecurve(theworkouts,workstrokesonly=False): +def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' ids = [int(w.id) for w in theworkouts] @@ -407,96 +409,222 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): # quick linear regression # peakforce = slope*peakforceangle + intercept slope, intercept, r,p,stderr = linregress(rowdata['peakforceangle'],rowdata['peakforce']) - theta = np.arctan(slope) - a = rowdata['peakforceangle']-rowdata['peakforceangle'].mean() - F = rowdata['peakforce']-rowdata['peakforce'].mean() + covariancematrix = np.cov(rowdata['peakforceangle'],y=rowdata['peakforce']) + eig_vals, eig_vecs = np.linalg.eig(covariancematrix) - R = np.array([[np.cos(theta),np.sin(theta)], - [-np.sin(theta),np.cos(theta)]]) - Rinv = np.array([[np.cos(theta),np.sin(theta)], - [-np.sin(theta),np.cos(theta)]]) + a = rowdata['peakforceangle']-rowdata['peakforceangle'].median() + F = rowdata['peakforce']-rowdata['peakforce'].median() + + Rinv = eig_vecs + R = np.linalg.inv(Rinv) x = R[0,0]*a+R[0,1]*F y = R[1,0]*a+R[1,1]*F - x05 = x.quantile(q=0.05) - x25 = x.quantile(q=0.25) - x75 = x.quantile(q=0.75) - x95 = x.quantile(q=0.95) + x05 = x.quantile(q=0.01) + x25 = x.quantile(q=0.15) + x75 = x.quantile(q=0.85) + x95 = x.quantile(q=0.99) + + y05 = y.quantile(q=0.01) + y25 = y.quantile(q=0.15) + y75 = y.quantile(q=0.85) + y95 = y.quantile(q=0.99) + + a25 = Rinv[0,0]*x25 + rowdata['peakforceangle'].median() + F25 = Rinv[1,0]*x25 + rowdata['peakforce'].median() + + a25b = Rinv[0,1]*y25 + rowdata['peakforceangle'].median() + F25b = Rinv[1,1]*y25 + rowdata['peakforce'].median() + + a75 = Rinv[0,0]*x75 + rowdata['peakforceangle'].median() + F75 = Rinv[1,0]*x75 + rowdata['peakforce'].median() + + a75b = Rinv[0,1]*y75 + rowdata['peakforceangle'].median() + F75b = Rinv[1,1]*y75 + rowdata['peakforce'].median() + + + a05 = Rinv[0,0]*x05 + rowdata['peakforceangle'].median() + F05 = Rinv[1,0]*x05 + rowdata['peakforce'].median() + + a05b = Rinv[0,1]*y05 + rowdata['peakforceangle'].median() + F05b = Rinv[1,1]*y05 + rowdata['peakforce'].median() + + a95 = Rinv[0,0]*x95 + rowdata['peakforceangle'].median() + F95 = Rinv[1,0]*x95 + rowdata['peakforce'].median() + + a95b = Rinv[0,1]*y95 + rowdata['peakforceangle'].median() + F95b = Rinv[1,1]*y95 + rowdata['peakforce'].median() + - y05 = y.quantile(q=0.05) - y25 = y.quantile(q=0.25) - y75 = y.quantile(q=0.75) - y95 = y.quantile(q=0.95) try: - catchav = rowdata['catch'].mean() + catchav = rowdata['catch'].median() + catch25 = rowdata['catch'].quantile(q=0.25) + catch75 = rowdata['catch'].quantile(q=0.75) + catch05 = rowdata['catch'].quantile(q=0.05) + catch95 = rowdata['catch'].quantile(q=0.95) except KeyError: catchav = 0 + catch25 = 0 + catch75 = 0 + catch05 = 0 + catch95 = 0 try: - finishav = rowdata['finish'].mean() + finishav = rowdata['finish'].median() + finish25 = rowdata['finish'].quantile(q=0.25) + finish75 = rowdata['finish'].quantile(q=0.75) + finish05 = rowdata['finish'].quantile(q=0.05) + finish95 = rowdata['finish'].quantile(q=0.95) except KeyError: finishav = 0 + finish25 = 0 + finish75 = 0 + finish05 = 0 + finish95 = 0 + try: - washav = rowdata['wash'].mean() + washav = (rowdata['finish']-rowdata['wash']).median() + wash25 = (rowdata['finish']-rowdata['wash']).quantile(q=0.25) + wash75 = (rowdata['finish']-rowdata['wash']).quantile(q=0.75) + wash05 = (rowdata['finish']-rowdata['wash']).quantile(q=0.05) + wash95 = (rowdata['finish']-rowdata['wash']).quantile(q=0.95) except KeyError: washav = 0 + wash25 = 0 + wash75 = 0 + wash05 = 0 + wash95 = 0 try: - slipav = rowdata['slip'].mean() + slipav = (rowdata['slip']+rowdata['catch']).median() + slip25 = (rowdata['slip']+rowdata['catch']).quantile(q=0.25) + slip75 = (rowdata['slip']+rowdata['catch']).quantile(q=0.75) + slip05 = (rowdata['slip']+rowdata['catch']).quantile(q=0.05) + slip95 = (rowdata['slip']+rowdata['catch']).quantile(q=0.95) except KeyError: slipav = 0 - + slip25 = 0 + slip75 = 0 + slip05 = 0 + slip95 = 0 + try: - peakforceav = rowdata['peakforce'].mean() + peakforceav = rowdata['peakforce'].median() + peakforce25 = rowdata['peakforce'].quantile(q=0.25) + peakforce75 = rowdata['peakforce'].quantile(q=0.75) + peakforce05 = rowdata['peakforce'].quantile(q=0.05) + peakforce95 = rowdata['peakforce'].quantile(q=0.95) except KeyError: peakforceav = 0 + peakforce25 = 0 + peakforce75 = 0 + peakforce05 = 0 + peakforce95 = 0 try: - averageforceav = rowdata['averageforce'].mean() + averageforceav = rowdata['averageforce'].median() except KeyError: averageforceav = 0 - + try: - peakforceangleav = rowdata['peakforceangle'].mean() + peakforceangleav = rowdata['peakforceangle'].median() + peakforceangle05 = rowdata['peakforceangle'].quantile(q=0.05) + peakforceangle25 = rowdata['peakforceangle'].quantile(q=0.25) + peakforceangle75 = rowdata['peakforceangle'].quantile(q=0.75) + peakforceangle95 = rowdata['peakforceangle'].quantile(q=0.95) except KeyError: peakforceangleav = 0 + peakforceangle25 = 0 + peakforceangle75 = 0 + peakforceangle05 = 0 + peakforceangle95 = 0 + + + #thresholdforce /= 4.45 # N to lbs + thresholdforce = 100 if 'x' in boattype else 200 + points2575 = [ + (catch25,0), #0 + (slip25,thresholdforce), #1 + (a75,F75),#4 + (a25b,F25b), #9 + (a25,F25), #2 + (wash75,thresholdforce), #5 + (finish75,0), #6 + (finish25,0), #7 + (wash25,thresholdforce), #8 + (a75b,F75b), #3 + (slip75,thresholdforce), #10 + (catch75,0), #11 + ] + + points0595 = [ + (catch05,0), #0 + (slip05,thresholdforce), #1 + (a95,F95),#4 + (a05b,F05b), #9 + (a05,F05), #2 + (wash95,thresholdforce), #5 + (finish95,0), #6 + (finish05,0), #7 + (wash05,thresholdforce), #8 + (a95b,F95b), #3 + (slip95,thresholdforce), #10 + (catch95,0), #11 + ] + + + hull2575 = ConvexHull(points2575) + + + angles2575 = [] + forces2575 = [] + + for x,y in points2575: + angles2575.append(x) + forces2575.append(y) + + + angles0595 = [] + forces0595 = [] + + for x,y in points0595: + angles0595.append(x) + forces0595.append(y) + + + + - peakforceangle05 = rowdata['peakforceangle'].quantile(q=0.05) - peakforce05 = rowdata['peakforce'].quantile(q=0.05) - - peakforceangle25 = rowdata['peakforceangle'].quantile(q=0.25) - peakforce25 = rowdata['peakforce'].quantile(q=0.25) - - peakforceangle75 = rowdata['peakforceangle'].quantile(q=0.75) - peakforce75 = rowdata['peakforce'].quantile(q=0.75) - - peakforceangle95 = rowdata['peakforceangle'].quantile(q=0.95) - peakforce95 = rowdata['peakforce'].quantile(q=0.95) - - x = [catchav, - catchav+slipav, + slipav, peakforceangleav, - finishav-washav, + washav, finishav] - thresholdforce = 100 if 'x' in boattype else 200 - #thresholdforce /= 4.45 # N to lbs y = [0,thresholdforce, peakforceav, thresholdforce,0] + + source = ColumnDataSource( data = dict( x = x, y = y, )) + sourceslipwash = ColumnDataSource( + data = dict( + xslip = [slipav,washav], + yslip = [thresholdforce,thresholdforce] + ) + ) + sourcetrend = ColumnDataSource( data = dict( x = [peakforceangle25,peakforceangle75], @@ -506,8 +634,8 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): sourcefit = ColumnDataSource( data = dict( - x = rowdata['peakforceangle'], - y = slope*rowdata['peakforceangle']+intercept + x = np.array([peakforceangle25,peakforceangle75]), + y = slope*np.array([peakforceangle25,peakforceangle75])+intercept ) ) @@ -515,10 +643,27 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): rowdata ) - sourcepoints = ColumnDataSource( + if plottype == 'scatter': + sourcepoints = ColumnDataSource( + data = dict( + peakforceangle = rowdata['peakforceangle'], + peakforce = rowdata['peakforce'] + ) + ) + else: + sourcepoints = ColumnDataSource( + data = dict( + peakforceangle = [], + peakforce = [] + )) + + + sourcerange = ColumnDataSource( data = dict( - peakforceangle = rowdata['peakforceangle'], - peakforce = rowdata['peakforce'] + x2575 = angles2575, + y2575 = forces2575, + x0595 = angles0595, + y0595 = forces0595, ) ) @@ -555,10 +700,57 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): avf = Span(location=averageforceav,dimension='width',line_color='blue', line_dash=[6,6],line_width=2) - plot.circle('peakforceangle','peakforce',source=sourcepoints,color='cyan') + plot.patch('x0595','y0595',source=sourcerange,color="red",alpha=0.05) + plot.patch('x2575','y2575',source=sourcerange,color="red",alpha=0.2) + plot.line('x','y',source=source,color="red") + plot.circle('xslip','yslip',source=sourceslipwash,color="red") + + plot.circle('peakforceangle','peakforce',source=sourcepoints,color='black',alpha=0.1) + + if plottype == 'line': + multilinedatax = [] + multilinedatay = [] + for i in range(len(rowdata)): + x = [ + rowdata['catch'].values[i], + rowdata['slip'].values[i]+rowdata['catch'].values[i], + rowdata['peakforceangle'].values[i], + rowdata['finish'].values[i]-rowdata['wash'].values[i], + rowdata['finish'].values[i] + ] + + + y = [ + 0, + thresholdforce, + rowdata['peakforce'].values[i], + thresholdforce, + 0] + + multilinedatax.append(x) + multilinedatay.append(y) + + sourcemultiline = ColumnDataSource(dict( + x=multilinedatax, + y=multilinedatay, + )) + + sourcemultiline2 = ColumnDataSource(dict( + x=multilinedatax, + y=multilinedatay, + )) + + glyph = MultiLine(xs='x',ys='y',line_color='black',line_alpha=0.05) + plot.add_glyph(sourcemultiline,glyph) + else: + sourcemultiline = ColumnDataSource(dict( + x=[],y=[])) + + sourcemultiline2 = ColumnDataSource(dict( + x=[],y=[])) + + plot.line('x','y',source=source,color="red") - plot.line('x','y',source=sourcetrend,color="blue") - plot.line('x','y',source=sourcefit,color="green") plot.add_layout(avf) @@ -598,14 +790,14 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): ) sliplabel = Label(x=370,y=280,x_units='screen',y_units='screen', - text="Slip: {slipav:6.2f}".format(slipav=slipav), + text="Slip: {slipav:6.2f}".format(slipav=slipav-catchav), background_fill_alpha=0.7, background_fill_color='white', text_color='red', ) washlabel = Label(x=360,y=250,x_units='screen',y_units='screen', - text="Wash: {washav:6.2f}".format(washav=washav), + text="Wash: {washav:6.2f}".format(washav=finishav-washav), background_fill_alpha=0.7, background_fill_color='white', text_color='red', @@ -650,6 +842,7 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): callback = CustomJS(args = dict( source=source, source2=source2, + sourceslipwash=sourceslipwash, sourcepoints=sourcepoints, avf=avf, avflabel=avflabel, @@ -661,13 +854,26 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): peakforceanglelabel=peakforceanglelabel, annolabel=annolabel, sliderlabel=sliderlabel, + plottype=plottype, + sourcemultiline=sourcemultiline, + sourcemultiline2=sourcemultiline2 ), code=""" var data = source.data var data2 = source2.data + var dataslipwash = sourceslipwash.data var datapoints = sourcepoints.data + var multilines = sourcemultiline.data + var multilines2 = sourcemultiline2.data + var plottype = plottype + + var multilinesx = multilines['xs'] + var multilinesy = multilines['ys'] var x = data['x'] var y = data['y'] + + var xslip = dataslipwash['xslip'] + var spm1 = data2['spm'] var distance1 = data2['distance'] var driveenergy1 = data2['driveenergy'] @@ -682,6 +888,9 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): var peakforce = data2['peakforce'] var averageforce = data2['averageforce'] + var peakforcepoints = datapoints['peakforce'] + var peakforceanglepoints = datapoints['peakforceangle'] + var annotation = annotation.value var minspm = minspm.value var maxspm = maxspm.value @@ -705,13 +914,23 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): datapoints['peakforceangle'] = [] datapoints['peakforce'] = [] + multilines['xs'] = [] + multilines['ys'] = [] for (i=0; i=minspm && spm1[i]<=maxspm) { if (distance1[i]>=mindist && distance1[i]<=maxdist) { if (driveenergy1[i]>=minwork && driveenergy1[i]<=maxwork) { - datapoints['peakforceangle'].push(peakforceangle[i]) - datapoints['peakforce'].push(peakforce[i]) + if (plottype=='scatter') { + datapoints['peakforceangle'].push(peakforceangle[i]) + datapoints['peakforce'].push(peakforce[i]) + } + if (plottype=='line') { + xs = [c[i],slip[i],peakforceangle[i],wash[i],finish[i]] + ys = [0,thresholdforce,peakforce[i],thresholdforce,finish[i]] + multilines['xs'].push(xs) + multilines['ys'].push(ys) + } catchav += c[i] finishav += finish[i] slipav += slip[i] @@ -736,6 +955,9 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): data['x'] = [catchav,catchav+slipav,peakforceangleav,finishav-washav,finishav] data['y'] = [0,thresholdforce,peakforceav,thresholdforce,0] + dataslipwash['xslip'] = [catchav+slipav,finishav-washav] + dataslipwash['yslip'] = [thresholdforce,thresholdforce] + avf.location = averageforceav avflabel.text = 'Favg: '+averageforceav.toFixed(2) catchlabel.text = 'Catch: '+catchav.toFixed(2) @@ -748,7 +970,9 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): // source.trigger('change'); source.change.emit(); + sourceslipwash.change.emit() sourcepoints.change.emit(); + sourcemultiline.change.emit(); """) annotation = TextInput(title="Type your plot notes here", value="", diff --git a/rowers/templates/forcecurve_single.html b/rowers/templates/forcecurve_single.html index 2cebb103..2326aa0a 100644 --- a/rowers/templates/forcecurve_single.html +++ b/rowers/templates/forcecurve_single.html @@ -22,27 +22,24 @@

Empower Force Curve

    -
  • - {% if user.is_authenticated and mayedit %} -
    - {% csrf_token %} - {% if workstrokesonly %} - - - {% else %} - - -
    - {% endif %} - If your data source allows, this will show or hide strokes taken during rest intervals. - {% endif %} -
  • {{ the_div|safe }}
  • +
  • +
    + {% csrf_token %} + + {{ form.as_table }} +
    +

    + +

    +
    +
diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 46cdd11e..d29dbd37 100644 Binary files a/rowers/tests/testdata/testdata.csv.gz and b/rowers/tests/testdata/testdata.csv.gz differ diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 76691ab1..2179bebb 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -48,6 +48,7 @@ from django.http import ( ) from django.contrib.auth import authenticate, login, logout from rowers.forms import ( + ForceCurveOptionsForm, LoginForm,DocumentsForm,UploadOptionsForm,ImageForm,CourseForm, TeamUploadOptionsForm,WorkFlowLeftPanelForm,WorkFlowMiddlePanelForm, WorkFlowLeftPanelElement,WorkFlowMiddlePanelElement, diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index c976e6f7..8b3ac052 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -26,15 +26,24 @@ def workout_forcecurve_view(request,id=0,workstrokesonly=False): if not promember: return HttpResponseRedirect("/rowers/about/") - if request.method == 'POST' and 'workstrokesonly' in request.POST: - workstrokesonly = request.POST['workstrokesonly'] - if workstrokesonly == 'True': - workstrokesonly = True + if request.method == 'POST': + form = ForceCurveOptionsForm(request.POST) + if form.is_valid(): + includereststrokes = form.cleaned_data['includereststrokes'] + plottype = form.cleaned_data['plottype'] + workstrokesonly = not includereststrokes else: - workstrokesonly = False + workstrokesonly = True + plottype = 'line' + else: + form = ForceCurveOptionsForm() + plottype = 'line' - script,div,js_resources,css_resources = interactive_forcecurve([row], - workstrokesonly=workstrokesonly) + script,div,js_resources,css_resources = interactive_forcecurve( + [row], + workstrokesonly=workstrokesonly, + plottype=plottype, + ) breadcrumbs = [ { @@ -53,12 +62,13 @@ def workout_forcecurve_view(request,id=0,workstrokesonly=False): ] r = getrower(request.user) - + return render(request, 'forcecurve_single.html', { 'the_script':script, 'rower':r, + 'form':form, 'workout':row, 'breadcrumbs':breadcrumbs, 'active':'nav-workouts', @@ -67,7 +77,6 @@ def workout_forcecurve_view(request,id=0,workstrokesonly=False): 'css_res':css_resources, 'id':id, 'mayedit':mayedit, - 'workstrokesonly': not workstrokesonly, 'teams':get_my_teams(request.user), })