From b654a1f1a985286c3cff5627ceda2208180c60df Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 31 Mar 2019 16:32:44 +0200 Subject: [PATCH 01/24] cum flex chart slider annotations --- rowers/interactiveplots.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 1152682a..2112a245 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -2752,11 +2752,18 @@ def interactive_cum_flex_chart2(theworkouts,promember=0, text_color='green', ) + 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(x1means) plot.add_layout(xlabel) - plot.add_layout(y1means) + plot.add_layout(sliderlabel) y1label = Label(x=50,y=50,x_units='screen',y_units='screen', text=axlabels[yparam1]+": {y1mean:6.2f}".format(y1mean=y1mean), @@ -2820,6 +2827,7 @@ def interactive_cum_flex_chart2(theworkouts,promember=0, y1label=y1label, y2label=y2label, xlabel=xlabel, + sliderlabel=sliderlabel, y2means=y2means), code=""" var data = source.data var data2 = source2.data @@ -2844,6 +2852,11 @@ def interactive_cum_flex_chart2(theworkouts,promember=0, var maxdist = maxdist.value 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 xm = 0 var ym1 = 0 var ym2 = 0 From b97990b905b93d48299c136573a20f2b6199b513 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 31 Mar 2019 17:02:14 +0200 Subject: [PATCH 02/24] adding slider annotation to trend flex --- rowers/interactiveplots.py | 27 ++++++++++++++++++++------- rowers/views/analysisviews.py | 4 +++- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 2112a245..7fec35b5 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -2370,7 +2370,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 +2516,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 +2525,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 +2535,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 +2553,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 diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 86a21740..895e7d77 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -2593,7 +2593,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) From 64cb556730980d9f5f922d965b50b9b5f4f17ce5 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 31 Mar 2019 17:07:44 +0200 Subject: [PATCH 03/24] adding slider annotation to box chart --- rowers/interactiveplots.py | 24 ++++++++++++++---------- rowers/views/analysisviews.py | 3 ++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 7fec35b5..4ddceb58 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -164,7 +164,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 +187,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 +213,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 diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 895e7d77..a5f815e2 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -3087,7 +3087,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) From 60da5c233100dc01c97c22173993c80aea19ac09 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 31 Mar 2019 20:25:39 +0200 Subject: [PATCH 04/24] annotations & slider annotations at empower force chart --- rowers/interactiveplots.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 4ddceb58..d697eba6 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -546,6 +546,21 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): text_color='red', ) + 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) @@ -553,6 +568,8 @@ 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.xaxis.axis_label = "Angle" plot.yaxis.axis_label = "Force (N)" @@ -576,6 +593,8 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): washlabel=washlabel, peakflabel=peakflabel, peakforceanglelabel=peakforceanglelabel, + annolabel=annolabel, + sliderlabel=sliderlabel, ), code=""" var data = source.data var data2 = source2.data @@ -596,6 +615,7 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): var peakforce = data2['peakforce'] var averageforce = data2['averageforce'] + var annotation = annotation.value var minspm = minspm.value var maxspm = maxspm.value var mindist = mindist.value @@ -603,6 +623,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 @@ -649,11 +673,16 @@ 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 // source.trigger('change'); source.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 @@ -683,7 +712,8 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): 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, From f0a4cf56394a7743cee845b45775f1db8289efa8 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 31 Mar 2019 20:33:31 +0200 Subject: [PATCH 05/24] style changes force curve --- rowers/templates/forcecurve_single.html | 2 ++ static/css/styles2.css | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/rowers/templates/forcecurve_single.html b/rowers/templates/forcecurve_single.html index 652e563c..2cebb103 100644 --- a/rowers/templates/forcecurve_single.html +++ b/rowers/templates/forcecurve_single.html @@ -38,7 +38,9 @@ {% endif %}
  • +
    {{ the_div|safe }} +
  • 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; } From 3a530fc897dd1aecfd48cd111ccaab8584c77302 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 31 Mar 2019 21:06:15 +0200 Subject: [PATCH 06/24] alphabetical x selectors for flex chart --- rowers/forms.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index d0608bb1..7387d8fe 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -1186,19 +1186,23 @@ class FlexOptionsForm(forms.Form): 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) From e3564fd309f7faa2dbad4b20c4faaf50e11c156e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 31 Mar 2019 21:10:30 +0200 Subject: [PATCH 07/24] adding left and right carets --- rowers/metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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} From 8462e086dd73db779f272b112bca314118c758fc Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 1 Apr 2019 13:51:01 +0200 Subject: [PATCH 08/24] force curve chart with 5-95 and 25-75 percentile areas --- rowers/interactiveplots.py | 137 ++++++++++++++++++++++++-- rowers/tests/testdata/testdata.csv.gz | Bin 12534 -> 12557 bytes 2 files changed, 128 insertions(+), 9 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index d697eba6..3e5a8bf9 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -405,42 +405,91 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): try: catchav = rowdata['catch'].mean() + catch25 = rowdata['catch'].quantile(q=0.25) + catch75 = rowdata['catch'].quantile(q=0.75) + catch01 = rowdata['catch'].quantile(q=0.05) + catch99 = rowdata['catch'].quantile(q=0.95) except KeyError: catchav = 0 + catch25 = 0 + catch75 = 0 + catch01 = 0 + catch99 = 0 try: finishav = rowdata['finish'].mean() + finish25 = rowdata['finish'].quantile(q=0.25) + finish75 = rowdata['finish'].quantile(q=0.75) + finish01 = rowdata['finish'].quantile(q=0.05) + finish99 = rowdata['finish'].quantile(q=0.95) except KeyError: finishav = 0 + finish25 = 0 + finish75 = 0 + finish01 = 0 + finish99 = 0 + try: - washav = rowdata['wash'].mean() + washav = (rowdata['finish']-rowdata['wash']).mean() + wash25 = (rowdata['finish']-rowdata['wash']).quantile(q=0.25) + wash75 = (rowdata['finish']-rowdata['wash']).quantile(q=0.75) + wash01 = (rowdata['finish']-rowdata['wash']).quantile(q=0.05) + wash99 = (rowdata['finish']-rowdata['wash']).quantile(q=0.95) except KeyError: washav = 0 + wash25 = 0 + wash75 = 0 + wash01 = 0 + wash99 = 0 try: - slipav = rowdata['slip'].mean() + slipav = (rowdata['slip']+rowdata['catch']).mean() + slip25 = (rowdata['slip']+rowdata['catch']).quantile(q=0.25) + slip75 = (rowdata['slip']+rowdata['catch']).quantile(q=0.75) + slip01 = (rowdata['slip']+rowdata['catch']).quantile(q=0.05) + slip99 = (rowdata['slip']+rowdata['catch']).quantile(q=0.95) except KeyError: slipav = 0 - + slip25 = 0 + slip75 = 0 + slip01 = 0 + slip99 = 0 + try: peakforceav = rowdata['peakforce'].mean() + peakforce25 = rowdata['peakforce'].quantile(q=0.25) + peakforce75 = rowdata['peakforce'].quantile(q=0.75) + peakforce01 = rowdata['peakforce'].quantile(q=0.05) + peakforce99 = rowdata['peakforce'].quantile(q=0.95) except KeyError: peakforceav = 0 + peakforce25 = 0 + peakforce75 = 0 + peakforce01 = 0 + peakforce99 = 0 try: averageforceav = rowdata['averageforce'].mean() except KeyError: averageforceav = 0 - + try: peakforceangleav = rowdata['peakforceangle'].mean() + peakforceangle25 = rowdata['peakforceangle'].quantile(q=0.25) + peakforceangle75 = rowdata['peakforceangle'].quantile(q=0.75) + peakforceangle01 = rowdata['peakforceangle'].quantile(q=0.05) + peakforceangle99 = rowdata['peakforceangle'].quantile(q=0.95) except KeyError: peakforceangleav = 0 - + peakforceangle25 = 0 + peakforceangle75 = 0 + peakforceangle01 = 0 + peakforceangle99 = 0 + x = [catchav, - catchav+slipav, + slipav, peakforceangleav, - finishav-washav, + washav, finishav] thresholdforce = 100 if 'x' in boattype else 200 @@ -449,12 +498,70 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): peakforceav, thresholdforce,0] + x2575 = [catch25, + slip25, + peakforceangleav, + wash75, + finish75, + finish25, + wash25, + peakforceangleav, + slip75] + + y2575 = [0, + thresholdforce, + peakforce75, + thresholdforce, + 0, + 0, + thresholdforce, + peakforce25, + 0] + + + + x0199 = [catch01, + slip01, + peakforceangleav, + wash99, + finish99, + finish01, + wash01, + peakforceangleav, + slip99] + + y0199 = [0, + thresholdforce, + peakforce99, + thresholdforce, + 0, + 0, + thresholdforce, + peakforce01, + 0] + + source = ColumnDataSource( data = dict( x = x, y = y, )) + sourcerange = ColumnDataSource( + data = dict( + x2575=x2575, + y2575=y2575, + x0199=x0199, + y0199=y0199, + )) + + sourceslipwash = ColumnDataSource( + data = dict( + xslip = [slipav,washav], + yslip = [thresholdforce,thresholdforce] + ) + ) + source2 = ColumnDataSource( rowdata @@ -493,7 +600,10 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): avf = Span(location=averageforceav,dimension='width',line_color='blue', line_dash=[6,6],line_width=2) + plot.patch('x0199','y0199',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.add_layout(avf) @@ -533,14 +643,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', @@ -585,6 +695,7 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): callback = CustomJS(args = dict( source=source, source2=source2, + sourceslipwash=sourceslipwash, avf=avf, avflabel=avflabel, catchlabel=catchlabel, @@ -598,9 +709,13 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): ), code=""" var data = source.data var data2 = source2.data + var dataslipwash = sourceslipwash.data var x = data['x'] var y = data['y'] + + var xslip = dataslipwash['xslip'] + var spm1 = data2['spm'] var distance1 = data2['distance'] var driveenergy1 = data2['driveenergy'] @@ -665,6 +780,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) @@ -677,6 +795,7 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): // source.trigger('change'); source.change.emit(); + sourceslipwash.change.emit() """) annotation = TextInput(title="Type your plot notes here", value="", diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 46cdd11e466d4cdd6331711c9be59a2876cac02b..d29dbd37b0f2a98501d6e2578e4ac49c589b1007 100644 GIT binary patch literal 12557 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 Date: Tue, 2 Apr 2019 09:29:45 +0200 Subject: [PATCH 09/24] plotting some help lines and data points force curve --- rowers/interactiveplots.py | 72 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index d697eba6..94a4548b 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -403,6 +403,34 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): if rowdata.empty: return "","No Valid Data Available","","" + + # 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() + + 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)]]) + + 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) + + 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() except KeyError: @@ -427,6 +455,7 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): except KeyError: peakforceav = 0 + try: averageforceav = rowdata['averageforce'].mean() except KeyError: @@ -436,6 +465,19 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): peakforceangleav = rowdata['peakforceangle'].mean() except KeyError: peakforceangleav = 0 + + 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, @@ -455,11 +497,31 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): y = y, )) + sourcetrend = ColumnDataSource( + data = dict( + x = [peakforceangle25,peakforceangle75], + y = [peakforce25,peakforce75] + ) + ) + + sourcefit = ColumnDataSource( + data = dict( + x = rowdata['peakforceangle'], + y = slope*rowdata['peakforceangle']+intercept + ) + ) source2 = ColumnDataSource( rowdata ) + sourcepoints = ColumnDataSource( + data = dict( + peakforceangle = rowdata['peakforceangle'], + peakforce = rowdata['peakforce'] + ) + ) + plot = Figure(tools=TOOLS, toolbar_sticky=False,toolbar_location="above") @@ -493,7 +555,10 @@ 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.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) @@ -585,6 +650,7 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): callback = CustomJS(args = dict( source=source, source2=source2, + sourcepoints=sourcepoints, avf=avf, avflabel=avflabel, catchlabel=catchlabel, @@ -598,6 +664,7 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): ), code=""" var data = source.data var data2 = source2.data + var datapoints = sourcepoints.data var x = data['x'] var y = data['y'] @@ -636,11 +703,15 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): var peakforceav = 0 var count = 0 + datapoints['peakforceangle'] = [] + datapoints['peakforce'] = [] 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]) catchav += c[i] finishav += finish[i] slipav += slip[i] @@ -677,6 +748,7 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): // source.trigger('change'); source.change.emit(); + sourcepoints.change.emit(); """) annotation = TextInput(title="Type your plot notes here", value="", From 2f69b34bc11905a47d451fc5022ee01b34d8d8bb Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 2 Apr 2019 13:41:56 +0200 Subject: [PATCH 10/24] force curve improvements --- rowers/interactiveplots.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 3e5a8bf9..a960987c 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -499,8 +499,10 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): thresholdforce,0] x2575 = [catch25, - slip25, - peakforceangleav, + slip25, + peakforceangle25, +# peakforceangleav, + peakforceangle75, wash75, finish75, finish25, @@ -510,7 +512,9 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): y2575 = [0, thresholdforce, - peakforce75, + peakforce25, +# peakforceav, + peakforce75, thresholdforce, 0, 0, From e735d98dcfa74eb0b642ff4ffccbefa6c452d857 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 2 Apr 2019 17:31:45 +0200 Subject: [PATCH 11/24] working force plot --- rowers/interactiveplots.py | 227 +++++++++++++++++++++---------------- 1 file changed, 131 insertions(+), 96 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index c339fc82..2c499cee 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -64,6 +64,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 @@ -407,172 +408,207 @@ 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) - catch99 = rowdata['catch'].quantile(q=0.95) + catch95 = rowdata['catch'].quantile(q=0.95) except KeyError: catchav = 0 catch25 = 0 catch75 = 0 catch05 = 0 - catch99 = 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) - finish99 = rowdata['finish'].quantile(q=0.95) + finish95 = rowdata['finish'].quantile(q=0.95) except KeyError: finishav = 0 finish25 = 0 finish75 = 0 finish05 = 0 - finish99 = 0 + finish95 = 0 try: - washav = (rowdata['finish']-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) - wash99 = (rowdata['finish']-rowdata['wash']).quantile(q=0.95) + wash95 = (rowdata['finish']-rowdata['wash']).quantile(q=0.95) except KeyError: washav = 0 wash25 = 0 wash75 = 0 wash05 = 0 - wash99 = 0 + wash95 = 0 try: - slipav = (rowdata['slip']+rowdata['catch']).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) - slip99 = (rowdata['slip']+rowdata['catch']).quantile(q=0.95) + slip95 = (rowdata['slip']+rowdata['catch']).quantile(q=0.95) except KeyError: slipav = 0 slip25 = 0 slip75 = 0 slip05 = 0 - slip99 = 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) - peakforce99 = rowdata['peakforce'].quantile(q=0.95) + peakforce95 = rowdata['peakforce'].quantile(q=0.95) except KeyError: peakforceav = 0 peakforce25 = 0 peakforce75 = 0 peakforce05 = 0 - peakforce99 = 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) - peakforceangle99 = rowdata['peakforceangle'].quantile(q=0.95) + peakforceangle95 = rowdata['peakforceangle'].quantile(q=0.95) except KeyError: peakforceangleav = 0 peakforceangle25 = 0 peakforceangle75 = 0 peakforceangle05 = 0 - peakforceangle99 = 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) + + + + + x = [catchav, slipav, peakforceangleav, washav, finishav] - thresholdforce = 100 if 'x' in boattype else 200 - #thresholdforce /= 4.45 # N to lbs y = [0,thresholdforce, peakforceav, thresholdforce,0] - x2575 = [catch25, - slip25, - peakforceangle25, -# peakforceangleav, - peakforceangle75, - wash75, - finish75, - finish25, - wash25, - peakforceangleav, - slip75] - - y2575 = [0, - thresholdforce, - peakforce25, -# peakforceav, - peakforce75, - thresholdforce, - 0, - 0, - thresholdforce, - peakforce25, - 0] - - - - x0599 = [catch05, - slip05, - peakforceangleav, - wash99, - finish99, - finish05, - wash05, - peakforceangleav, - slip99] - - y0599 = [0, - thresholdforce, - peakforce99, - thresholdforce, - 0, - 0, - thresholdforce, - peakforce05, - 0] source = ColumnDataSource( @@ -581,21 +617,13 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): y = y, )) - sourcerange = ColumnDataSource( - data = dict( - x2575=x2575, - y2575=y2575, - x0599=x0599, - y0599=y0599, - )) - sourceslipwash = ColumnDataSource( data = dict( xslip = [slipav,washav], yslip = [thresholdforce,thresholdforce] ) ) - + sourcetrend = ColumnDataSource( data = dict( x = [peakforceangle25,peakforceangle75], @@ -605,8 +633,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 ) ) @@ -621,6 +649,15 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): ) ) + sourcerange = ColumnDataSource( + data = dict( + x2575 = angles2575, + y2575 = forces2575, + x0595 = angles0595, + y0595 = forces0595, + ) + ) + plot = Figure(tools=TOOLS, toolbar_sticky=False,toolbar_location="above") @@ -654,15 +691,13 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): avf = Span(location=averageforceav,dimension='width',line_color='blue', line_dash=[6,6],line_width=2) - plot.patch('x0199','y0199',source=sourcerange,color="red",alpha=0.05) + 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='cyan') + plot.circle('peakforceangle','peakforce',source=sourcepoints,color='black',alpha=0.1) 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) From d251b1ddd4bba011b914bbcd8c016a22a1f0eb6c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 2 Apr 2019 19:00:00 +0200 Subject: [PATCH 12/24] force curve with lines --- rowers/forms.py | 18 ++++- rowers/interactiveplots.py | 88 +++++++++++++++++++++++-- rowers/templates/forcecurve_single.html | 27 ++++---- rowers/views/statements.py | 1 + rowers/views/workoutviews.py | 27 +++++--- 5 files changed, 127 insertions(+), 34 deletions(-) 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 2c499cee..d601d558 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 @@ -374,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] @@ -642,12 +643,20 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): rowdata ) - sourcepoints = ColumnDataSource( - data = dict( - peakforceangle = rowdata['peakforceangle'], - peakforce = rowdata['peakforce'] + if plottype == 'scatter': + sourcepoints = ColumnDataSource( + data = dict( + peakforceangle = rowdata['peakforceangle'], + peakforce = rowdata['peakforce'] ) ) + else: + sourcepoints = ColumnDataSource( + data = dict( + peakforceangle = [], + peakforce = [] + )) + sourcerange = ColumnDataSource( data = dict( @@ -697,6 +706,50 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): 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.add_layout(avf) @@ -801,11 +854,20 @@ 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'] @@ -826,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 @@ -849,13 +914,21 @@ 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') { + multilines['xs'].push(multilinesx[i]) + multilines['ys'].push(multilinesy[i]) + } catchav += c[i] finishav += finish[i] slipav += slip[i] @@ -897,6 +970,7 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): 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/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), }) From ea649d2a499e24f5baf6ace9c9e05bed85338b8f Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 2 Apr 2019 19:05:39 +0200 Subject: [PATCH 13/24] line collection - averages do strange on sliders --- rowers/interactiveplots.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index d601d558..86217e08 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -926,8 +926,10 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): datapoints['peakforce'].push(peakforce[i]) } if (plottype=='line') { - multilines['xs'].push(multilinesx[i]) - multilines['ys'].push(multilinesy[i]) + 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] From 50b7af86e15183b2681ad07bfbb5d7a85c47eada Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 2 Apr 2019 21:31:58 +0200 Subject: [PATCH 14/24] working sliders on line collection (force curve) --- rowers/interactiveplots.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 86217e08..eb06995d 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -866,8 +866,8 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): var multilines2 = sourcemultiline2.data var plottype = plottype - var multilinesx = multilines['xs'] - var multilinesy = multilines['ys'] + var multilinesx = multilines2['x'] + var multilinesy = multilines2['y'] var x = data['x'] var y = data['y'] @@ -914,8 +914,8 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): datapoints['peakforceangle'] = [] datapoints['peakforce'] = [] - multilines['xs'] = [] - multilines['ys'] = [] + multilines['x'] = [] + multilines['y'] = [] for (i=0; i=minspm && spm1[i]<=maxspm) { @@ -926,10 +926,8 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): 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) + multilines['x'].push(multilinesx[i]) + multilines['y'].push(multilinesy[i]) } catchav += c[i] finishav += finish[i] @@ -967,6 +965,10 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): peakflabel.text = 'Fpeak: '+peakforceav.toFixed(2) peakforceanglelabel.text = 'Peak angle: '+peakforceangleav.toFixed(2) annolabel.text = annotation + + console.log(count); + console.log(multilines['x'].length); + console.log(multilines['y'].length); // source.trigger('change'); source.change.emit(); From c433d493c6a8b6104ccf753b811961e75c260869 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 2 Apr 2019 22:08:30 +0200 Subject: [PATCH 15/24] parsing error check --- rowers/tests/testdata/testdata.csv.gz | Bin 12557 -> 12523 bytes rowers/views/workoutviews.py | 2 ++ 2 files changed, 2 insertions(+) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index d29dbd37b0f2a98501d6e2578e4ac49c589b1007..f8fbb4692b63daad166a49e7b602b78186ab791e 100644 GIT binary patch literal 12523 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 Date: Tue, 2 Apr 2019 22:17:44 +0200 Subject: [PATCH 16/24] tested --- rowers/tests/testdata/testdata.csv.gz | Bin 12523 -> 12543 bytes rowers/views/workoutviews.py | 2 -- 2 files changed, 2 deletions(-) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index f8fbb4692b63daad166a49e7b602b78186ab791e..f98b3587c21b59648d1e7383a626ec0eba8e6155 100644 GIT binary patch literal 12543 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 zʩ#=1hb_}VRZ>xe7ZaMW6yPtn@>A+Q@%LOfFfy)Fm_xH(`J$-2~OP)gF7NL@BK zX0A0p)Ouqy?~`6Ze@x_iBV@Ythd_bDd?3o@(y^vncAri7*&srq8U};h9kQ*SHC+q? zSFPodmwHUJon=2mtfOLmJkM4%f8tX*#xWm9+V%sVDwi@y%~tj!F~@$kTHHHR61RDA zI%hJiJ$;c3sk3edR33runKoOS($lLE;&@q`gHXIzj>LDI)^$Dpk+eV7-Z zaHxEQYRVHT>>J&^a9pC@} literal 12523 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 Date: Tue, 2 Apr 2019 22:34:06 +0200 Subject: [PATCH 17/24] bug fix --- rowers/interactiveplots.py | 140 +++++++++++++++++++++++-------------- 1 file changed, 86 insertions(+), 54 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index eb06995d..e725c7c4 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -408,55 +408,85 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): # quick linear regression # peakforce = slope*peakforceangle + intercept - slope, intercept, r,p,stderr = linregress(rowdata['peakforceangle'],rowdata['peakforce']) + try: + slope, intercept, r,p,stderr = linregress(rowdata['peakforceangle'],rowdata['peakforce']) + except KeyError: + slope = 0 + intercept = 0 - covariancematrix = np.cov(rowdata['peakforceangle'],y=rowdata['peakforce']) - eig_vals, eig_vecs = np.linalg.eig(covariancematrix) + 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() + a = rowdata['peakforceangle']-rowdata['peakforceangle'].median() + F = rowdata['peakforce']-rowdata['peakforce'].median() - Rinv = eig_vecs - R = np.linalg.inv(Rinv) + 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 + 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) + 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() + 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() + 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 @@ -578,8 +608,6 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): ] - hull2575 = ConvexHull(points2575) - angles2575 = [] forces2575 = [] @@ -711,21 +739,25 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): 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] + 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] + y = [ + 0, + thresholdforce, + rowdata['peakforce'].values[i], + thresholdforce, + 0] + except KeyError: + x = [0,0] + y = [0,0] multilinedatax.append(x) multilinedatay.append(y) From 5181f6c6ab48e6d72f866feca67b596bb4187e25 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 3 Apr 2019 16:27:51 +0200 Subject: [PATCH 18/24] fix #454 --- rowers/forms.py | 2 +- rowers/interactiveplots.py | 82 +++++++++++++++++--------------------- rowers/mailprocessing.py | 3 +- 3 files changed, 39 insertions(+), 48 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index ae17b596..ff61a245 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -1193,7 +1193,7 @@ class ForceCurveOptionsForm(forms.Form): ('scatter','Peak Force Scatter Plot'), ('none','Only aggregrate data') ) - plottype = forms.ChoiceField(choices=plotchoices,initial='scatter', + plottype = forms.ChoiceField(choices=plotchoices,initial='line', label='Individual Stroke Chart Type') diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index e725c7c4..e1207fb6 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -337,41 +337,8 @@ 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 @@ -786,55 +753,69 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): 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', + 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', + 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, @@ -859,6 +840,8 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): 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)" @@ -886,6 +869,8 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): peakforceanglelabel=peakforceanglelabel, annolabel=annolabel, sliderlabel=sliderlabel, + lengthlabel=lengthlabel, + efflengthlabel=efflengthlabel, plottype=plottype, sourcemultiline=sourcemultiline, sourcemultiline2=sourcemultiline2 @@ -988,6 +973,9 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): 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) @@ -997,6 +985,8 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): 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); @@ -1033,12 +1023,12 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): 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 @@ -3323,12 +3313,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 @@ -3925,12 +3915,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 From c8f7d5541c33d0b06de14825a89fd0cd988832dd Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 3 Apr 2019 18:21:51 +0200 Subject: [PATCH 19/24] fix #455 --- rowers/templates/analysis.html | 70 ++++++++++++++-------------- rowers/templates/menu_analytics.html | 31 ++++++------ 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html index ee0d3664..d598c144 100644 --- a/rowers/templates/analysis.html +++ b/rowers/templates/analysis.html @@ -11,18 +11,6 @@
      -
    • -

      Ranking Pieces

      - -
      - Ranking Piece -
      -
      -

      - Analyze your Concept2 ranking pieces over a date range and predict your pace on other pieces. -

      -
    • Compare Workouts

      {% if team %} @@ -85,29 +73,6 @@ BETA: Box Chart Statistics of stroke metrics over a date range

    • -
    • -

      OTW Critical Power

      - -
      - OTW Critical Power -
      -
      -

      - Analyse power vs piece duration to make predictions. For On-The-Water rowing. -

      -
    • -
    • -

      OTE Critical Power

      - -
      - OTE Critical Power -
      -
      - -

      - Analyse power vs piece duration to make predictions, for erg pieces. -

      -
    • Trend Flex

      @@ -131,6 +96,41 @@ Monitoring power duration evidence from all your workouts. Feel free to explore.

    • +
    • +

      Ranking Pieces

      +
      +
      + Ranking Piece +
      +
      +

      + Analyze your Concept2 ranking pieces over a date range and predict your pace on other pieces. +

      +
    • +
    • +

      OTW Critical Power

      + +
      + OTW Critical Power +
      +
      +

      + Analyse power vs piece duration to make predictions. For On-The-Water rowing. +

      +
    • +
    • +

      OTE Critical Power

      + +
      + OTE Critical Power +
      +
      + +

      + Analyse power vs piece duration to make predictions, for erg pieces. +

      +
    diff --git a/rowers/templates/menu_analytics.html b/rowers/templates/menu_analytics.html index 86bbd079..24b99117 100644 --- a/rowers/templates/menu_analytics.html +++ b/rowers/templates/menu_analytics.html @@ -5,20 +5,30 @@
  • +  Fitness +
  • From 815ef34491c602b8c6d84fe4a966452ffcdbc119 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 3 Apr 2019 21:08:25 +0200 Subject: [PATCH 20/24] bug fix tombstones --- rowers/imports.py | 2 +- rowers/tests/testdata/testdata.csv.gz | Bin 12543 -> 12523 bytes 2 files changed, 1 insertion(+), 1 deletion(-) 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/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index f98b3587c21b59648d1e7383a626ec0eba8e6155..ce7f3029b3308346acd5c803619d3ba6b58ff270 100644 GIT binary patch literal 12523 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 zʩ#=1hb_}VRZ>xe7ZaMW6yPtn@>A+Q@%LOfFfy)Fm_xH(`J$-2~OP)gF7NL@BK zX0A0p)Ouqy?~`6Ze@x_iBV@Ythd_bDd?3o@(y^vncAri7*&srq8U};h9kQ*SHC+q? zSFPodmwHUJon=2mtfOLmJkM4%f8tX*#xWm9+V%sVDwi@y%~tj!F~@$kTHHHR61RDA zI%hJiJ$;c3sk3edR33runKoOS($lLE;&@q`gHXIzj>LDI)^$Dpk+eV7-Z zaHxEQYRVHT>>J&^a9pC@} From 9fb2523b9af4cb88d61e05ad6d074a09672821f1 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 3 Apr 2019 21:17:58 +0200 Subject: [PATCH 21/24] fix --- rowers/management/commands/processemail.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py index 6918fe70..25382b96 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: From 0ff24d8aeeff4e3954f5eb815df8050a00ba7dbd Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 3 Apr 2019 22:22:40 +0200 Subject: [PATCH 22/24] small improvements --- rowers/dataprepnodjango.py | 5 ++++- rowers/tasks.py | 28 +++++++++++++++----------- rowers/tests/testdata/testdata.csv.gz | Bin 12523 -> 12523 bytes 3 files changed, 20 insertions(+), 13 deletions(-) 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/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/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index ce7f3029b3308346acd5c803619d3ba6b58ff270..4caef7dc529d2810e8ee112fa054f8f3bb046f42 100644 GIT binary patch delta 15 WcmaEz_&SkIzMF%Ci+3a2Qv(1my#+h~ delta 15 WcmaEz_&SkIzMF$%KFdb7rv?Bs4h6^n From c3fda7e20651c145fd0e389b726a19d2f0e08d33 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 4 Apr 2019 22:05:10 +0200 Subject: [PATCH 23/24] fix #459 --- rowers/forms.py | 18 +++++++---- rowers/interactiveplots.py | 47 +++++++++++++++++++++------- rowers/templates/analysis.html | 4 +-- rowers/templates/histo.html | 7 +++++ rowers/templates/menu_analytics.html | 4 +-- rowers/views/analysisviews.py | 36 +++++++++++++++++++-- rowers/views/statements.py | 2 +- 7 files changed, 94 insertions(+), 24 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index ff61a245..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, @@ -797,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', diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index e1207fb6..141bc9f0 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1214,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) @@ -1227,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, @@ -1289,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)) @@ -1297,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}'), ]) @@ -1311,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): @@ -3889,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, diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html index d598c144..aca799ec 100644 --- a/rowers/templates/analysis.html +++ b/rowers/templates/analysis.html @@ -40,14 +40,14 @@

  • -

    Power Histogram

    +

    Histogram

    Power Histogram

    - Plot a power histogram of all your strokes over a date range. + Plot a histogram chart of one metric for all your strokes over a date range.

  • 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

    +
    • @@ -100,6 +102,11 @@ {{ form.as_table }}
    • +
    • + + {{ histoform.as_table }} +
      +
    • {% csrf_token %} diff --git a/rowers/templates/menu_analytics.html b/rowers/templates/menu_analytics.html index 24b99117..bb9078ba 100644 --- a/rowers/templates/menu_analytics.html +++ b/rowers/templates/menu_analytics.html @@ -62,9 +62,9 @@  Statistics
    • -
    • +
    • -  Power Histogram +  Histogram
    diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index a5f815e2..d367301e 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: diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 2179bebb..b9eb4985 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -48,7 +48,7 @@ from django.http import ( ) from django.contrib.auth import authenticate, login, logout from rowers.forms import ( - ForceCurveOptionsForm, + ForceCurveOptionsForm,HistoForm, LoginForm,DocumentsForm,UploadOptionsForm,ImageForm,CourseForm, TeamUploadOptionsForm,WorkFlowLeftPanelForm,WorkFlowMiddlePanelForm, WorkFlowLeftPanelElement,WorkFlowMiddlePanelElement, From 9488da2b3782660a9296b987489b76f639014433 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 5 Apr 2019 19:40:39 +0200 Subject: [PATCH 24/24] passing tests --- rowers/tests/testdata/testdata.csv.gz | Bin 12523 -> 12523 bytes rowers/views/workoutviews.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 4caef7dc529d2810e8ee112fa054f8f3bb046f42..810456e5b4736b4053d5a7991b0c48d9d9cc7cbe 100644 GIT binary patch delta 16 XcmaEz_&SkYzMF$XVDj>f>`x2;Im-rc delta 16 XcmaEz_&SkYzMF%Ci+AZp_9q4aHgg5y diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 2155f617..f3ea8c17 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -111,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]