diff --git a/rowers/dataprepnodjango.py b/rowers/dataprepnodjango.py index 7bb0c4cb..d2f3c84a 100644 --- a/rowers/dataprepnodjango.py +++ b/rowers/dataprepnodjango.py @@ -1007,7 +1007,10 @@ def dataprep(rowdatadf,id=0,bands=True,barchart=True,otwpower=True, f = rowdatadf['TimeStamp (sec)'].diff().mean() if f != 0: - windowsize = 2*(int(10./(f)))+1 + try: + windowsize = 2*(int(10./(f)))+1 + except ValueError: + windowsize = 1 else: windowsize = 1 if windowsize <= 3: diff --git a/rowers/forms.py b/rowers/forms.py index d0608bb1..30c6836b 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -25,6 +25,12 @@ import datetime from django.forms import formset_factory from rowers.utils import landingpages from rowers.metrics import axes +from rowers.metrics import axlabels + +formaxlabels = axlabels.copy() +formaxlabels.pop('None') +parchoices = list(sorted(formaxlabels.items(), key = lambda x:x[1])) + class FlexibleDecimalField(forms.DecimalField): @@ -710,6 +716,12 @@ class DataFrameColumnsForm(forms.Form): cols = forms.MultipleChoiceField(choices=colchoices, label='Table Columns') +class HistoForm(forms.Form): + includereststrokes = forms.BooleanField(initial=False,label='Include Rest Strokes',required=False) + histoparam = forms.ChoiceField(choices=parchoices,initial='power', + label='Metric') + + # form to select modality and boat type for trend flex class TrendFlexModalForm(forms.Form): modality = forms.ChoiceField(choices=workouttypes, @@ -722,7 +734,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) @@ -796,12 +809,6 @@ class PlannedSessionMultipleCloneForm(forms.Form): label='Planned Sessions' ) -from rowers.metrics import axlabels - -formaxlabels = axlabels.copy() -formaxlabels.pop('None') -parchoices = list(sorted(formaxlabels.items(), key = lambda x:x[1])) - class BoxPlotChoiceForm(forms.Form): yparam = forms.ChoiceField(choices=parchoices,initial='spm', @@ -1181,24 +1188,39 @@ 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='line', + label='Individual Stroke Chart Type') + + class FlexAxesForm(forms.Form): - axchoices = ( + axchoices = list( (ax[0],ax[1]) for ax in axes if ax[0] not in ['cumdist','None'] ) + axchoices = dict((x,y) for x,y in axchoices) + axchoices = list(sorted(axchoices.items(), key = lambda x:x[1])) - yaxchoices = ( - (ax[0], ax[1]) for ax in axes if ax[0] not in ['cumdist','distance','time'] - ) + yaxchoices = list((ax[0],ax[1]) for ax in axes if ax[0] not in ['cumdist','distance','time']) + yaxchoices = dict((x,y) for x,y in yaxchoices) + yaxchoices = list(sorted(yaxchoices.items(), key = lambda x:x[1])) - yaxchoices2 = ( - (ax[0], ax[1]) for ax in axes if ax[0] not in ['cumdist','distance','time'] - ) + yaxchoices2 = list( + (ax[0],ax[1]) for ax in axes if ax[0] not in ['cumdist','distance','time'] + ) + yaxchoices2 = dict((x,y) for x,y in yaxchoices2) + yaxchoices2 = list(sorted(yaxchoices2.items(), key = lambda x:x[1])) xaxis = forms.ChoiceField( choices=axchoices,label='X-Axis',required=True) diff --git a/rowers/imports.py b/rowers/imports.py index 6114ad79..fe1a85e6 100644 --- a/rowers/imports.py +++ b/rowers/imports.py @@ -43,7 +43,7 @@ from django.contrib.auth.decorators import login_required # from .models import Profile from rowingdata import rowingdata, make_cumvalues import pandas as pd -from rowers.models import Rower,Workout,checkworkoutuser +from rowers.models import Rower,Workout,checkworkoutuser,TombStone import rowers.mytypes as mytypes from rowsandall_app.settings import ( C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 2112a245..141bc9f0 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 @@ -164,7 +166,8 @@ def tailwind(bearing,vwind,winddir): from rowers.dataprep import nicepaceformat,niceformat from rowers.dataprep import timedeltaconv -def interactive_boxchart(datadf,fieldname,extratitle=''): +def interactive_boxchart(datadf,fieldname,extratitle='', + spmmin=0,spmmax=0,workmin=0,workmax=0): if datadf.empty: return '','It looks like there are no data matching your filter' @@ -186,15 +189,6 @@ def interactive_boxchart(datadf,fieldname,extratitle=''): plot = hv.render(boxwhiskers) - #plot = BoxPlot(datadf, values=fieldname, label='date', - # legend=False, - # title=axlabels[fieldname]+' '+extratitle, - # outliers=False, - # tools=TOOLS, - # toolbar_location="above", - # toolbar_sticky=False, - # x_mapper_type='datetime',plot_width=920) - yrange1 = Range1d(start=yaxminima[fieldname],end=yaxmaxima[fieldname]) plot.y_range = yrange1 plot.sizing_mode = 'scale_width' @@ -221,6 +215,18 @@ def interactive_boxchart(datadf,fieldname,extratitle=''): plot.plot_width=920 plot.plot_height=600 + slidertext = 'SPM: {:.0f}-{:.0f}, WpS: {:.0f}-{:.0f}'.format( + spmmin,spmmax,workmin,workmax + ) + sliderlabel = Label(x=50,y=20,x_units='screen',y_units='screen', + text=slidertext, + background_fill_alpha=0.7, + background_fill_color='white', + text_color='black',text_font_size='10pt', + ) + + plot.add_layout(sliderlabel) + script, div = components(plot) return script,div @@ -331,45 +337,12 @@ def interactive_activitychart(workouts,startdate,enddate,stack='type'): p.toolbar_location = None p.sizing_mode = 'scale_width' -# p = hv.Bars(df,values='duration', -# label = CatAttr(columns=['date'], sort=False), -# xlabel='Date', -# ylabel='Time', -# title='Activity {d1} to {d2}'.format( -# d1 = startdate.strftime("%Y-%m-%d"), -# d2 = enddate.strftime("%Y-%m-%d"), -# ), -# stack=stack, -# plot_width=350, -# plot_height=250, -# toolbar_location = None, -# ) - - - - - - # for legend in p.legend: - # new_items = [] - # for legend_item in legend.items: - # it = legend_item.label['value'] - # tot = df[df[stack]==it].duration.sum() - # if tot != 0: - # new_items.append(legend_item) - # legend.items = new_items - # p.legend.location = "top_left" - # p.legend.background_fill_alpha = 0.7 - #p.sizing_mode = 'scale_width' - #p.sizing_mode = 'stretch_both' - - #p.yaxis.axis_label = 'Minutes' - 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] @@ -399,63 +372,296 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): if rowdata.empty: return "","No Valid Data Available","","" + + # quick linear regression + # peakforce = slope*peakforceangle + intercept try: - catchav = rowdata['catch'].mean() + slope, intercept, r,p,stderr = linregress(rowdata['peakforceangle'],rowdata['peakforce']) + except KeyError: + slope = 0 + intercept = 0 + + try: + covariancematrix = np.cov(rowdata['peakforceangle'],y=rowdata['peakforce']) + eig_vals, eig_vecs = np.linalg.eig(covariancematrix) + + 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.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() + except KeyError: + a25 = 0 + F25 = 0 + + a25b = 0 + F25b = 0 + + a75 = 0 + F75 = 0 + + a75b = 0 + F75b = 0 + + + a05 = 0 + F05 = 0 + + a05b = 0 + F05b = 0 + + a95 = 0 + F95 = 0 + + a95b = 0 + F95b = 0 + + + + try: + 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 + ] + + + + 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) + + + + + 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], + y = [peakforce25,peakforce75] + ) + ) + + sourcefit = ColumnDataSource( + data = dict( + x = np.array([peakforceangle25,peakforceangle75]), + y = slope*np.array([peakforceangle25,peakforceangle75])+intercept + ) + ) source2 = ColumnDataSource( rowdata ) + if plottype == 'scatter': + sourcepoints = ColumnDataSource( + data = dict( + peakforceangle = rowdata['peakforceangle'], + peakforce = rowdata['peakforce'] + ) + ) + else: + sourcepoints = ColumnDataSource( + data = dict( + peakforceangle = [], + peakforce = [] + )) + + + sourcerange = ColumnDataSource( + data = dict( + x2575 = angles2575, + y2575 = forces2575, + x0595 = angles0595, + y0595 = forces0595, + ) + ) + plot = Figure(tools=TOOLS, toolbar_sticky=False,toolbar_location="above") @@ -489,59 +695,142 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): avf = Span(location=averageforceav,dimension='width',line_color='blue', line_dash=[6,6],line_width=2) + 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)): + try: + 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] + except KeyError: + x = [0,0] + y = [0,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.add_layout(avf) - peakflabel = Label(x=355,y=430,x_units='screen',y_units='screen', + peakflabel = Label(x=410,y=460,x_units='screen',y_units='screen', text="Fpeak: {peakforceav:6.2f}".format(peakforceav=peakforceav), background_fill_alpha=.7, background_fill_color='white', text_color='blue', ) - avflabel = Label(x=365,y=400,x_units='screen',y_units='screen', + avflabel = Label(x=420,y=430,x_units='screen',y_units='screen', text="Favg: {averageforceav:6.2f}".format(averageforceav=averageforceav), background_fill_alpha=.7, background_fill_color='white', text_color='blue', ) - catchlabel = Label(x=360,y=370,x_units='screen',y_units='screen', + catchlabel = Label(x=415,y=400,x_units='screen',y_units='screen', text="Catch: {catchav:6.2f}".format(catchav=catchav), background_fill_alpha=0.7, background_fill_color='white', text_color='red', ) - peakforceanglelabel = Label(x=320,y=340,x_units='screen',y_units='screen', + peakforceanglelabel = Label(x=375,y=370,x_units='screen',y_units='screen', text="Peak angle: {peakforceangleav:6.2f}".format(peakforceangleav=peakforceangleav), background_fill_alpha=0.7, background_fill_color='white', text_color='red', ) - finishlabel = Label(x=355,y=310,x_units='screen',y_units='screen', + finishlabel = Label(x=410,y=340,x_units='screen',y_units='screen', text="Finish: {finishav:6.2f}".format(finishav=finishav), background_fill_alpha=0.7, background_fill_color='white', text_color='red', ) - sliplabel = Label(x=370,y=280,x_units='screen',y_units='screen', - text="Slip: {slipav:6.2f}".format(slipav=slipav), + sliplabel = Label(x=425,y=310,x_units='screen',y_units='screen', + 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), + washlabel = Label(x=415,y=280,x_units='screen',y_units='screen', + text="Wash: {washav:6.2f}".format(washav=finishav-washav), background_fill_alpha=0.7, background_fill_color='white', text_color='red', ) + lengthlabel = Label(x=405,y=250, x_units='screen',y_units='screen', + text="Length: {length:6.2f}".format(length=finishav-catchav), + background_fill_alpha=0.7, + background_fill_color='white', + text_color='green' + ) + + efflengthlabel = Label(x=340,y=220, x_units='screen',y_units='screen', + text="Effective Length: {length:6.2f}".format(length=washav-slipav), + background_fill_alpha=0.7, + background_fill_color='white', + text_color='green' + ) + + annolabel = Label(x=50,y=450,x_units='screen',y_units='screen', + text='', + background_fill_alpha=0.7, + background_fill_color='white', + text_color='black', + ) + + sliderlabel = Label(x=10,y=470,x_units='screen',y_units='screen', + text='', + background_fill_alpha=0.7, + background_fill_color='white', + text_color='black',text_font_size='10pt', + ) + + plot.add_layout(peakflabel) plot.add_layout(peakforceanglelabel) plot.add_layout(avflabel) @@ -549,6 +838,10 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): plot.add_layout(sliplabel) plot.add_layout(washlabel) plot.add_layout(finishlabel) + plot.add_layout(annolabel) + plot.add_layout(sliderlabel) + plot.add_layout(lengthlabel) + plot.add_layout(efflengthlabel) plot.xaxis.axis_label = "Angle" plot.yaxis.axis_label = "Force (N)" @@ -564,6 +857,8 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): callback = CustomJS(args = dict( source=source, source2=source2, + sourceslipwash=sourceslipwash, + sourcepoints=sourcepoints, avf=avf, avflabel=avflabel, catchlabel=catchlabel, @@ -572,12 +867,30 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): washlabel=washlabel, peakflabel=peakflabel, peakforceanglelabel=peakforceanglelabel, + annolabel=annolabel, + sliderlabel=sliderlabel, + lengthlabel=lengthlabel, + efflengthlabel=efflengthlabel, + 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 = multilines2['x'] + var multilinesy = multilines2['y'] var x = data['x'] var y = data['y'] + + var xslip = dataslipwash['xslip'] + var spm1 = data2['spm'] var distance1 = data2['distance'] var driveenergy1 = data2['driveenergy'] @@ -592,6 +905,10 @@ 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 var mindist = mindist.value @@ -599,6 +916,10 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): var minwork = minwork.value var maxwork = maxwork.value + sliderlabel.text = 'SPM: '+minspm.toFixed(0)+'-'+maxspm.toFixed(0) + sliderlabel.text += ', Dist: '+mindist.toFixed(0)+'-'+maxdist.toFixed(0) + sliderlabel.text += ', WpS: '+minwork.toFixed(0)+'-'+maxwork.toFixed(0) + var catchav = 0 var finishav = 0 var slipav = 0 @@ -608,11 +929,23 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): var peakforceav = 0 var count = 0 + datapoints['peakforceangle'] = [] + datapoints['peakforce'] = [] + multilines['x'] = [] + multilines['y'] = [] for (i=0; i=minspm && spm1[i]<=maxspm) { if (distance1[i]>=mindist && distance1[i]<=maxdist) { if (driveenergy1[i]>=minwork && driveenergy1[i]<=maxwork) { + if (plottype=='scatter') { + datapoints['peakforceangle'].push(peakforceangle[i]) + datapoints['peakforce'].push(peakforce[i]) + } + if (plottype=='line') { + multilines['x'].push(multilinesx[i]) + multilines['y'].push(multilinesy[i]) + } catchav += c[i] finishav += finish[i] slipav += slip[i] @@ -637,6 +970,12 @@ 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] + + var length = finishav-catchav + var efflength = length-slipav-washav + avf.location = averageforceav avflabel.text = 'Favg: '+averageforceav.toFixed(2) catchlabel.text = 'Catch: '+catchav.toFixed(2) @@ -645,11 +984,25 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): washlabel.text = 'Wash: '+washav.toFixed(2) peakflabel.text = 'Fpeak: '+peakforceav.toFixed(2) peakforceanglelabel.text = 'Peak angle: '+peakforceangleav.toFixed(2) + annolabel.text = annotation + lengthlabel.text = 'Length: '+length.toFixed(2) + efflengthlabel.text = 'Effective Length: '+efflength.toFixed(2) + + console.log(count); + console.log(multilines['x'].length); + console.log(multilines['y'].length); // source.trigger('change'); source.change.emit(); + sourceslipwash.change.emit() + sourcepoints.change.emit(); + sourcemultiline.change.emit(); """) + annotation = TextInput(title="Type your plot notes here", value="", + callback=callback) + callback.args["annotation"] = annotation + slider_spm_min = Slider(start=15.0, end=55,value=15.0, step=.1, title="Min SPM",callback=callback) callback.args["minspm"] = slider_spm_min @@ -670,16 +1023,17 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): distmax = 100+100*int(rowdata['distance'].max()/100.) - slider_dist_min = Slider(start=0,end=distmax,value=0,step=1, + slider_dist_min = Slider(start=0,end=distmax,value=0,step=50, title="Min Distance",callback=callback) callback.args["mindist"] = slider_dist_min slider_dist_max = Slider(start=0,end=distmax,value=distmax, - step=1, + step=50, title="Max Distance",callback=callback) callback.args["maxdist"] = slider_dist_max - layout = layoutrow([layoutcolumn([slider_spm_min, + layout = layoutrow([layoutcolumn([annotation, + slider_spm_min, slider_spm_max, slider_dist_min, slider_dist_max, @@ -860,12 +1214,13 @@ def fitnessmetric_chart(fitnessmetrics,user,workoutmode='rower',startdate=None, return [script,div] -def interactive_histoall(theworkouts): +def interactive_histoall(theworkouts,histoparam,includereststrokes): TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' ids = [int(w.id) for w in theworkouts] - rowdata = dataprep.getsmallrowdata_db(['power'],ids=ids,doclean=True) + workstrokesonly = not includereststrokes + rowdata = dataprep.getsmallrowdata_db([histoparam],ids=ids,doclean=True,workstrokesonly=workstrokesonly) rowdata.dropna(axis=0,how='any',inplace=True) @@ -873,16 +1228,16 @@ def interactive_histoall(theworkouts): return "","No Valid Data Available","","" try: - histopwr = rowdata['power'].values + histopwr = rowdata[histoparam].values except KeyError: - return "","No power data","","" + return "","No data","","" if len(histopwr) == 0: return "","No valid data available","","" # throw out nans histopwr = histopwr[~np.isinf(histopwr)] - histopwr = histopwr[histopwr > 25] - histopwr = histopwr[histopwr < 1000] + histopwr = histopwr[histopwr > yaxminima[histoparam]] + histopwr = histopwr[histopwr < yaxmaxima[histoparam]] plot = Figure(tools=TOOLS,plot_width=900, toolbar_sticky=False, @@ -935,7 +1290,7 @@ def interactive_histoall(theworkouts): # plot.quad(top='hist_norm',bottom=0,left=edges[:-1],right=edges[1:]) plot.quad(top='hist_norm',bottom=0,left='left',right='right',source=source) - plot.xaxis.axis_label = "Power (W)" + plot.xaxis.axis_label = axlabels[histoparam] plot.yaxis.axis_label = "% of strokes" plot.y_range = Range1d(0,1.05*max(hist_norm)) @@ -943,7 +1298,7 @@ def interactive_histoall(theworkouts): hover = plot.select(dict(type=HoverTool)) hover.tooltips = OrderedDict([ - ('Power(W)','@left{int}'), + (axlabels[histoparam],'@left{int}'), ('% of strokes','@hist_norm'), ('Cumulative %','@histsum{int}'), ]) @@ -957,12 +1312,36 @@ def interactive_histoall(theworkouts): axis_label="Cumulative % of strokes"),'right') plot.sizing_mode = 'scale_width' + + annolabel = Label(x=50,y=450,x_units='screen',y_units='screen', + text='', + background_fill_alpha=0.7, + background_fill_color='white', + text_color='black', + ) + + plot.add_layout(annolabel) + + callback = CustomJS(args = dict( + annolabel=annolabel, + ), code=""" + var annotation = annotation.value + annolabel.text = annotation + """) + + annotation = TextInput(title="Type your plot notes here", value="", + callback=callback) + callback.args["annotation"] = annotation + + layout = layoutcolumn([annotation,plot]) + try: - script, div = components(plot) + script, div = components(layout) except ValueError: script = '' div = '' - + + return [script,div] def course_map(course): @@ -2370,7 +2749,8 @@ def interactive_chart(id=0,promember=0,intervaldata = {}): def interactive_multiflex(datadf,xparam,yparam,groupby,extratitle='', ploterrorbars=False, - title=None,binsize=1,colorlegend=[]): + title=None,binsize=1,colorlegend=[], + spmmin=0,spmmax=0,workmin=0,workmax=0): if datadf.empty: return ['','

No non-zero data in selection

'] @@ -2515,8 +2895,8 @@ def interactive_multiflex(datadf,xparam,yparam,groupby,extratitle='', for nr, gvalue, color in colorlegend: - box = BoxAnnotation(bottom=125+20*nr,left=100,top=145+20*nr, - right=120, + box = BoxAnnotation(bottom=75+20*nr,left=50,top=95+20*nr, + right=70, bottom_units='screen', top_units='screen', left_units='screen', @@ -2524,7 +2904,7 @@ def interactive_multiflex(datadf,xparam,yparam,groupby,extratitle='', fill_color=color, fill_alpha=1.0, line_color=color) - legendlabel = Label(x=121,y=128+20*nr,x_units='screen', + legendlabel = Label(x=71,y=78+20*nr,x_units='screen', y_units='screen', text = "{gvalue:3.0f}".format(gvalue=gvalue), background_fill_alpha=1.0, @@ -2534,7 +2914,7 @@ def interactive_multiflex(datadf,xparam,yparam,groupby,extratitle='', plot.add_layout(legendlabel) if colorlegend: - legendlabel = Label(x=372,y=300,x_units='screen', + legendlabel = Label(x=322,y=250,x_units='screen', y_units='screen', text = 'group legend', text_color='black', @@ -2552,15 +2932,27 @@ def interactive_multiflex(datadf,xparam,yparam,groupby,extratitle='', else: plot.yaxis.axis_label = axlabels[yparam] - binlabel = Label(x=100,y=100,x_units='screen', + binlabel = Label(x=50,y=50,x_units='screen', y_units='screen', text="Bin size {binsize:3.1f}".format(binsize=binsize), background_fill_alpha=0.7, background_fill_color='white', - text_color='black', + text_color='black',text_font_size='10pt', ) + slidertext = 'SPM: {:.0f}-{:.0f}, WpS: {:.0f}-{:.0f}'.format( + spmmin,spmmax,workmin,workmax + ) + sliderlabel = Label(x=50,y=20,x_units='screen',y_units='screen', + text=slidertext, + background_fill_alpha=0.7, + background_fill_color='white', + text_color='black',text_font_size='10pt', + ) + + plot.add_layout(binlabel) + plot.add_layout(sliderlabel) yrange1 = Range1d(start=yaxmin,end=yaxmax) plot.y_range = yrange1 @@ -2946,12 +3338,12 @@ def interactive_cum_flex_chart2(theworkouts,promember=0, except KeyError: distmax = 1000. - slider_dist_min = Slider(start=0,end=distmax,value=0,step=1, + slider_dist_min = Slider(start=0,end=distmax,value=0,step=50, title="Min Distance",callback=callback) callback.args["mindist"] = slider_dist_min slider_dist_max = Slider(start=0,end=distmax,value=distmax, - step=1, + step=50, title="Max Distance",callback=callback) callback.args["maxdist"] = slider_dist_max @@ -3522,7 +3914,7 @@ def interactive_flex_chart2(id=0,promember=0, """) annotation = TextInput(title="Type your plot notes here", value="", - callback=callback) + callback=callback) callback.args["annotation"] = annotation slider_spm_min = Slider(start=15.0, end=55,value=15.0, step=.1, @@ -3548,12 +3940,12 @@ def interactive_flex_chart2(id=0,promember=0, except (KeyError,ValueError): distmax = 100 - slider_dist_min = Slider(start=0,end=distmax,value=0,step=1, + slider_dist_min = Slider(start=0,end=distmax,value=0,step=50, title="Min Distance",callback=callback) callback.args["mindist"] = slider_dist_min slider_dist_max = Slider(start=0,end=distmax,value=distmax, - step=1, + step=50, title="Max Distance",callback=callback) callback.args["maxdist"] = slider_dist_max diff --git a/rowers/mailprocessing.py b/rowers/mailprocessing.py index 32e7f3b8..a7cc453a 100644 --- a/rowers/mailprocessing.py +++ b/rowers/mailprocessing.py @@ -106,7 +106,8 @@ def make_new_workout_from_email(rower, datafile, name, cntr=0,testing=False): if testing: print('Fileformat = ',fileformat) - + + if fileformat == 'unknown': # extension = datafilename[-4:].lower() # fcopy = "media/"+datafilename[:-4]+"_copy"+extension diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py index cc2e01a2..ac493684 100644 --- a/rowers/management/commands/processemail.py +++ b/rowers/management/commands/processemail.py @@ -234,6 +234,8 @@ class Command(BaseCommand): ] except IOError: rowers = [] + except Message.DoesNotExist: + attachment.delete() for rower in rowers: if extension == 'zip': try: diff --git a/rowers/metrics.py b/rowers/metrics.py index 2f5c12b9..1e7d55b1 100644 --- a/rowers/metrics.py +++ b/rowers/metrics.py @@ -285,7 +285,7 @@ axesnew = [ (name,d['verbose_name'],d['ax_min'],d['ax_max'],d['type']) for name,d in rowingmetrics ] -axes = tuple(axesnew+[('None','None',0,1,'basic')]) +axes = tuple(axesnew+[('None','',0,1,'basic')]) axlabels = {ax[0]:ax[1] for ax in axes} diff --git a/rowers/tasks.py b/rowers/tasks.py index 6a8ad981..04c65fec 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -97,6 +97,12 @@ import arrow from rowers.utils import get_strava_stream +def safetimedelta(x): + try: + return timedelta(seconds=x) + except ValueError: + return timedelta(seconds=0) + siteurl = SITE_URL # testing task @@ -292,7 +298,7 @@ def handle_check_race_course(self, rowdata = rowdata[rowdata['time']>splitsecond] # we may want to expand the time (interpolate) rowdata['dt'] = rowdata['time'].apply( - lambda x: timedelta(seconds=x) + lambda x: safetimedelta(seconds=x) ) rowdata = rowdata.resample('100ms',on='dt').mean() rowdata = rowdata.interpolate() @@ -618,7 +624,7 @@ def handle_calctrimp(id, df2['time'] = df2[' ElapsedTime (sec)'] df2['time'] = df2['time'].apply( - lambda x:timedelta(seconds=x) + lambda x:safetimedelta(seconds=x) ) duration = df['TimeStamp (sec)'].max()-df['TimeStamp (sec)'].min() @@ -1659,11 +1665,10 @@ def handle_makeplot(f1, f2, t, hrdata, plotnr, imagename, try: haspower = row.df[' Power (watts)'].mean() > 50 - except TypeError: - haspower = True - except KeyError: + except (TypeError, KeyError): haspower = False + nr_rows = len(row.df) if (plotnr in [1, 2, 4, 5, 8, 11, 9, 12]) and (nr_rows > 1200): bin = int(nr_rows / 1200.) @@ -1700,14 +1705,13 @@ def handle_makeplot(f1, f2, t, hrdata, plotnr, imagename, t += ' - Power Distribution' fig1 = row.get_power_piechart(t) - canvas = FigureCanvas(fig1) + if fig1: + canvas = FigureCanvas(fig1) - # plt.savefig('static/plots/'+imagename,format='png') - canvas.print_figure('static/plots/' + imagename) - # plt.imsave(fname='static/plots/'+imagename) - plt.close(fig1) - fig1.clf() - gc.collect() + canvas.print_figure('static/plots/' + imagename) + plt.close(fig1) + fig1.clf() + gc.collect() return imagename diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html index ee0d3664..aca799ec 100644 --- a/rowers/templates/analysis.html +++ b/rowers/templates/analysis.html @@ -11,18 +11,6 @@ diff --git a/rowers/templates/forcecurve_single.html b/rowers/templates/forcecurve_single.html index 652e563c..2326aa0a 100644 --- a/rowers/templates/forcecurve_single.html +++ b/rowers/templates/forcecurve_single.html @@ -23,23 +23,22 @@ diff --git a/rowers/templates/histo.html b/rowers/templates/histo.html index b7b2d7d2..4e200c2d 100644 --- a/rowers/templates/histo.html +++ b/rowers/templates/histo.html @@ -75,6 +75,8 @@ +

Histogram View

+ diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 40ed76d4..7a44188d 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -20,11 +20,17 @@ def histo(request,theuser=0, 'workouttypes':[i[0] for i in mytypes.workouttypes], 'waterboattype':mytypes.waterboattype, 'rankingonly': False, + 'histoparam':'power' }): r = getrequestrower(request,userid=theuser) theuser = r.user - + + if 'histoparam' in request.session: + histoparam = request.session['histoparam'] + else: + histoparam = 'power' + if 'waterboattype' in request.session: waterboattype = request.session['waterboattype'] else: @@ -81,6 +87,7 @@ def histo(request,theuser=0, if request.method == 'POST': form = DateRangeForm(request.POST) modalityform = TrendFlexModalForm(request.POST) + histoform = HistoForm(request.POST) if form.is_valid(): startdate = form.cleaned_data['startdate'] enddate = form.cleaned_data['enddate'] @@ -110,6 +117,11 @@ def histo(request,theuser=0, 'startdate': startdate, 'enddate': enddate, }) + if histoform.is_valid(): + includereststrokes = histoform.cleaned_data['includereststrokes'] + histoparam = histoform.cleaned_data['histoparam'] + request.session['histoparam'] = histoparam + request.session['includereststrokes'] = includereststrokes else: form = DateRangeForm(initial={ 'startdate': startdate, @@ -125,6 +137,10 @@ def histo(request,theuser=0, 'rankingonly':rankingonly, } ) + histoform = HistoForm(initial={ + 'includereststrokes':False, + 'histoparam':histoparam + }) negtypes = [] for b in mytypes.boattypes: @@ -149,6 +165,7 @@ def histo(request,theuser=0, 'enddatestring':enddatestring, 'rankingonly':rankingonly, 'includereststrokes':includereststrokes, + 'histoparam':histoparam, } request.session['options'] = options @@ -163,9 +180,21 @@ def histo(request,theuser=0, request.session['options'] = options + breadcrumbs = [ + { + 'url':'/rowers/analysis', + 'name':'Analysis' + }, + { + 'url':reverse('histo'), + 'name': 'Histogram' + } + ] + return render(request, 'histo.html', {'interactiveplot':script, 'the_div':div, + 'breadcrumbs':breadcrumbs, 'id':theuser, 'active':'nav-analysis', 'theuser':theuser, @@ -174,6 +203,7 @@ def histo(request,theuser=0, 'enddate':enddate, 'form':form, 'optionsform':modalityform, + 'histoform':histoform, 'teams':get_my_teams(request.user), }) @@ -295,6 +325,7 @@ def histo_data( 'enddatestring':timezone.now().strftime("%Y-%m-%d"), 'startdatestring':(timezone.now()-datetime.timedelta(days=30)).strftime("%Y-%m-%d"), 'deltadays':-1, + 'histoparam':'power', }): def_options = options @@ -311,6 +342,7 @@ def histo_data( theuser = keyvalue_get_default('theuser',options,def_options) startdatestring = keyvalue_get_default('startdatestring',options,def_options) enddatestring = keyvalue_get_default('enddatestring',options,def_options) + histoparam = keyvalue_get_default('histoparam',options,def_options) if modality == 'all': modalities = [m[0] for m in mytypes.workouttypes] @@ -358,7 +390,7 @@ def histo_data( rankingpiece__in=rankingpiece) if allworkouts: - res = interactive_histoall(allworkouts) + res = interactive_histoall(allworkouts,histoparam,includereststrokes) script = res[0] div = res[1] else: @@ -2593,7 +2625,9 @@ def multiflex_data(request,userid=0, extratitle=extratitle, ploterrorbars=ploterrorbars, binsize=binsize, - colorlegend=colorlegend) + colorlegend=colorlegend, + spmmin=spmmin,spmmax=spmmax, + workmin=workmin,workmax=workmax) scripta= script.split('\n')[2:-1] script = ''.join(scripta) @@ -3085,7 +3119,8 @@ def boxplot_view_data(request,userid=0, script,div = interactive_boxchart(datadf,plotfield, - extratitle=extratitle) + extratitle=extratitle, + spmmin=spmmin,spmmax=spmmax,workmin=workmin,workmax=workmax) scripta = script.split('\n')[2:-1] script = ''.join(scripta) diff --git a/rowers/views/statements.py b/rowers/views/statements.py index c6e5054d..a4d400c0 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,HistoForm, LoginForm,DocumentsForm,UploadOptionsForm,ImageForm,CourseForm, TeamUploadOptionsForm,WorkFlowLeftPanelForm,WorkFlowMiddlePanelForm, WorkFlowLeftPanelElement,WorkFlowMiddlePanelElement, diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 51efce24..bd7b6ba9 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), }) @@ -102,7 +111,7 @@ def workout_histo_view(request,id=0): if not promember: return HttpResponseRedirect("/rowers/about/") - res = interactive_histoall([w]) + res = interactive_histoall([w],'power',False) script = res[0] div = res[1] @@ -4834,8 +4843,8 @@ def workout_summary_edit_view(request,id,message="",successmessage="" s = cd["intervalstring"] try: rowdata.updateinterval_string(s) - except (ParseException,err): - messages.error(request,'Parsing error in column '+str(err.col)) + except: + messages.error(request,'Parsing error') intervalstats = rowdata.allstats() itime,idist,itype = rowdata.intervalstats_values() nrintervals = len(idist) diff --git a/static/css/styles2.css b/static/css/styles2.css index 2a8c9896..b20f26bd 100644 --- a/static/css/styles2.css +++ b/static/css/styles2.css @@ -608,7 +608,7 @@ #theplot .bk-plot-layout { position: auto; - width: 100%; + width: 90%; height: auto; display: inline; }