From 0830c7bc6ce8e89b12868b0bb9aada86a862992e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 12 May 2017 08:50:20 +0200 Subject: [PATCH 1/6] make PDF scrollable on ipad --- rowers/templates/brochure.html | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/rowers/templates/brochure.html b/rowers/templates/brochure.html index 00a9ef66..db236129 100644 --- a/rowers/templates/brochure.html +++ b/rowers/templates/brochure.html @@ -1,12 +1,25 @@ {% extends "base.html" %} - {% block title %}Brochure{% endblock title %} +{% block title %}Brochure{% endblock title %} +{% block meta %} + +{% endblock meta %} {% block content %}

Read our Brochure

- +
+ + object can't be rendered + +
+
{% endblock content %} From e1893ab4ffd1ed05f8a2fb698ada92eec37dec49 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 12 May 2017 11:03:38 +0200 Subject: [PATCH 2/6] different row summary in dataprep --- rowers/dataprep.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 49fde4be..71f154b3 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -452,9 +452,10 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower', duration = "%s:%s:%s.%s" % (hours,minutes,seconds,tenths) if dosummary: - summary = row.summary() - summary += '\n' - summary += row.intervalstats() + summary = row.allstats() + #summary = row.summary() + #summary += '\n' + #summary += row.intervalstats() workoutdate = row.rowdatetime.strftime('%Y-%m-%d') workoutstarttime = row.rowdatetime.strftime('%H:%M:%S') From 29f84d6e251e46c6e7c2b42387fc4368b7ef3f87 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 12 May 2017 14:19:50 +0200 Subject: [PATCH 3/6] added HR drift, normalized power, training score --- rowers/templates/workoutstats.html | 29 ++++++++++++++++ rowers/views.py | 54 ++++++++++++++++++++++++++++++ rowsandall_app/settings_dev.py | 3 +- 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/rowers/templates/workoutstats.html b/rowers/templates/workoutstats.html index 6d413dea..75dc0f8c 100644 --- a/rowers/templates/workoutstats.html +++ b/rowers/templates/workoutstats.html @@ -74,6 +74,34 @@ {% endif %}
+ {% if otherstats %} +
+   +
+
+

Other Stats

+ + + + + + + + + + {% for key, value in otherstats.items %} + + + + + + {% endfor %} + +
MetricValueUnit
{{ value.verbose_name }}{{ value.value }}{{ value.unit }}
+
+ {% endif %} + +
{% if cordict %}

Correlation matrix

This matrix indicates a positive (+) or negative (-) correlation between two parameters. The Spearman correlation coefficient has values between +1 and -1. Positive correlation between two metrics means that if one metric increases, the other value is also likely to increase. Negative is the opposite. The further from zero, the higher the likelyhood. @@ -112,6 +140,7 @@ {% endif %} +

{% endblock %} diff --git a/rowers/views.py b/rowers/views.py index a1685e48..1e6b24c8 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -4036,6 +4036,9 @@ def cumstats(request,theuser=0, # Stats page @login_required() def workout_stats_view(request,id=0,message="",successmessage=""): + + r = Rower.objects.get(user=request.user) + workstrokesonly = True if request.method == 'POST' and 'workstrokesonly' in request.POST: workstrokesonly = request.POST['workstrokesonly'] @@ -4093,6 +4096,56 @@ def workout_stats_view(request,id=0,message="",successmessage=""): cordict[field1] = thedict + # additional non-automated stats + otherstats = {} + + # Normalized power & TSS + duration = datadf['time'].max()-datadf['time'].min() + duration /= 1.0e3 + pwr4 = datadf['power']**(4) + normp = (pwr4.mean())**(0.25) + if not np.isnan(normp): + intensityfactor = datadf['power'].mean()/float(r.ftp) + intensityfactor = normp/float(r.ftp) + tss = 100.*((duration*normp*intensityfactor)/(3600.*r.ftp)) + + otherstats['np'] = { + 'verbose_name':'rPower', + 'value':int(10*normp)/10., + 'unit':'Watt' + } + + otherstats['tss'] = { + 'verbose_name':'rScore', + 'value':int(tss), + 'unit':'' + } + + # HR Drift + tmax = datadf['time'].max() + tmin = datadf['time'].min() + thalf = tmin+0.5*(tmax-tmin) + mask1 = datadf['time'] < thalf + mask2 = datadf['time'] > thalf + + hr1 = datadf.loc[mask1,'hr'].mean() + hr2 = datadf.loc[mask2,'hr'].mean() + + pwr1 = datadf.loc[mask1,'power'].mean() + pwr2 = datadf.loc[mask2,'power'].mean() + + try: + hrdrift = ((pwr1/hr1)-(pwr2/hr2))/(pwr1/hr1) + hrdrift *= 100. + otherstats['hrdrift'] = { + 'verbose_name': 'Heart Rate Drift', + 'value': int(100*hrdrift)/100., + 'unit': '%', + } + except ZeroDivisionError: + pass + + return render(request, 'workoutstats.html', { @@ -4101,6 +4154,7 @@ def workout_stats_view(request,id=0,message="",successmessage=""): 'workout':row, 'workstrokesonly':workstrokesonly, 'cordict':cordict, + 'otherstats':otherstats, }) # The Advanced edit page diff --git a/rowsandall_app/settings_dev.py b/rowsandall_app/settings_dev.py index 8720c8b4..82d0dffe 100644 --- a/rowsandall_app/settings_dev.py +++ b/rowsandall_app/settings_dev.py @@ -45,7 +45,8 @@ CELERY_SEND_TASK_SENT_EVENT = True # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -TEMPLATE_DEBUG = DEBUG + +TEMPLATES[0]['OPTIONS']['debug'] = DEBUG ALLOWED_HOSTS = ['localhost'] From 50faf1ff1fd0cc455e841f73c2da5fe5cb581b18 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 12 May 2017 15:48:47 +0200 Subject: [PATCH 4/6] first version of box chart (wash only) --- rowers/dataprep.py | 2 +- rowers/interactiveplots.py | 18 ++++++++++++++++- rowers/templates/cumstats.html | 37 +++++++++++++++++++++++++++++++++- rowers/templates/histoall.html | 2 +- rowers/views.py | 18 +++++++++++++++-- 5 files changed, 71 insertions(+), 6 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 71f154b3..0cb791c5 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -272,7 +272,7 @@ def getstatsfields(): fielddict = {field.name:field.verbose_name for field in fields} - fielddict.pop('workoutid') + #fielddict.pop('workoutid') fielddict.pop('ergpace') fielddict.pop('hr_an') fielddict.pop('hr_tr') diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index f79b9ce2..14d42578 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -11,7 +11,7 @@ from bokeh.palettes import Dark2_8 as palette import itertools from bokeh.plotting import figure, ColumnDataSource, Figure,curdoc from bokeh.models import CustomJS,Slider, TextInput -from bokeh.charts import Histogram,HeatMap +from bokeh.charts import Histogram,HeatMap,Area,BoxPlot from bokeh.resources import CDN,INLINE from bokeh.embed import components from bokeh.layouts import layout,widgetbox @@ -87,6 +87,22 @@ def tailwind(bearing,vwind,winddir): from rowers.dataprep import nicepaceformat,niceformat from rowers.dataprep import timedeltaconv +def interactive_boxchart(datadf,fieldname): + + plot = BoxPlot(datadf, values=fieldname, label='date', + legend=False, + title=axlabels[fieldname], + outliers=False) + + yrange1 = Range1d(start=yaxminima[fieldname],end=yaxmaxima[fieldname]) + plot.y_range = yrange1 + + + script, div = components(plot) + + + return script,div + def interactive_forcecurve(theworkouts,workstrokesonly=False): TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,resize,crosshair' diff --git a/rowers/templates/cumstats.html b/rowers/templates/cumstats.html index eb0b9654..484e89ff 100644 --- a/rowers/templates/cumstats.html +++ b/rowers/templates/cumstats.html @@ -5,6 +5,35 @@ {% block title %}Workout Statistics{% endblock %} {% block content %} + + + + + {{ plotscript |safe }} + + + + +

Workout Statistics

@@ -97,6 +126,7 @@

{% if cordict %} +

Correlation Matrix

This matrix indicates a positive (+) or negative (-) correlation between two parameters. The Spearman correlation coefficient has values between +1 and -1. Positive correlation between two metrics means that if one metric increases, the other value is also likely to increase. Negative is the opposite. The further from zero, the higher the likelyhood.

@@ -132,8 +162,13 @@ {% endfor %} +
{% endif %} -
+ +
+ {{ plotdiv|safe }} +
+
{% endblock %} diff --git a/rowers/templates/histoall.html b/rowers/templates/histoall.html index 11f7b63e..6b1ee9c6 100644 --- a/rowers/templates/histoall.html +++ b/rowers/templates/histoall.html @@ -51,4 +51,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/rowers/views.py b/rowers/views.py index 1e6b24c8..b0cb9fcd 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -3834,7 +3834,7 @@ def workout_geeky_view(request,id=0,message="",successmessage=""): # Cumulative stats page @login_required() def cumstats(request,theuser=0, - startdate=timezone.now()-datetime.timedelta(days=365), + startdate=timezone.now()-datetime.timedelta(days=30), enddate=timezone.now(), deltadays=-1, startdatestring="", @@ -3957,6 +3957,12 @@ def cumstats(request,theuser=0, ids = [int(workout.id) for workout in allergworkouts] + datemapping = { + w.id:w.date for w in allergworkouts + } + + + fieldlist,fielddict = dataprep.getstatsfields() # prepare data frame @@ -3972,6 +3978,7 @@ def cumstats(request,theuser=0, # Create stats stats = {} fielddict.pop('workoutstate') + fielddict.pop('workoutid') for field,verbosename in fielddict.iteritems(): thedict = { @@ -3998,8 +4005,13 @@ def cumstats(request,theuser=0, except KeyError: thedict[field2] = 0 - cordict[field1] = thedict + cordict[field1] = thedict + # interactive box plot + datadf['workoutid'].replace(datemapping,inplace=True) + datadf.rename(columns={"workoutid":"date"},inplace=True) + script,div = interactive_boxchart(datadf,'wash') + # set options form correctly initial = {} initial['includereststrokes'] = includereststrokes @@ -4027,6 +4039,8 @@ def cumstats(request,theuser=0, 'deltaform':deltaform, 'optionsform':optionsform, 'cordict':cordict, + 'plotscript':script, + 'plotdiv':div, }) request.session['options'] = options From 3c0157475f9b8abc1f5a0464002ae4b19ca9fc4e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 12 May 2017 19:10:13 +0200 Subject: [PATCH 5/6] minimum viable version of improved stats --- rowers/dataprep.py | 12 ++++++++++++ rowers/interactiveplots.py | 18 +++++++++++++++--- rowers/templates/cumstats.html | 10 ++++++++-- rowers/urls.py | 2 ++ rowers/views.py | 5 ++++- 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 0cb791c5..84a25803 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -968,6 +968,12 @@ def read_cols_df_sql(ids,columns): connection = engine.raw_connection() df = pd.read_sql_query(query,engine) df = df.fillna(value=0) + try: + df['peakforce'] = df['peakforce']*lbstoN + df['averageforce'] = df['averageforce']*lbstoN + except KeyError: + pass + engine.dispose() return df @@ -980,6 +986,12 @@ def read_df_sql(id): engine.dispose() df = df.fillna(value=0) + try: + df['peakforce'] = df['peakforce']*lbstoN + df['averageforce'] = df['averageforce']*lbstoN + except KeyError: + pass + return df # Get the necessary data from the strokedata table in the DB. diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 14d42578..72607cbf 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -4,7 +4,7 @@ from rowingdata import main as rmain from rowingdata import cumcpdata,histodata from rowingdata import rowingdata as rrdata - +from math import pi from django.utils import timezone from bokeh.palettes import Dark2_8 as palette @@ -88,15 +88,27 @@ from rowers.dataprep import nicepaceformat,niceformat from rowers.dataprep import timedeltaconv def interactive_boxchart(datadf,fieldname): - + TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,resize' plot = BoxPlot(datadf, values=fieldname, label='date', legend=False, title=axlabels[fieldname], - outliers=False) + outliers=False, + tools=TOOLS, + x_mapper_type='datetime') yrange1 = Range1d(start=yaxminima[fieldname],end=yaxmaxima[fieldname]) plot.y_range = yrange1 + + plot.xaxis.axis_label = 'Date' + plot.yaxis.axis_label = axlabels[fieldname] + + plot.xaxis.formatter = DatetimeTickFormatter( + days=["%d %B %Y"], + months=["%d %B %Y"], + years=["%d %B %Y"], + ) + plot.xaxis.major_label_orientation = pi/4 script, div = components(plot) diff --git a/rowers/templates/cumstats.html b/rowers/templates/cumstats.html index 484e89ff..7d814c62 100644 --- a/rowers/templates/cumstats.html +++ b/rowers/templates/cumstats.html @@ -47,7 +47,7 @@ between {{ startdate|date }} and {{ enddate|date }}

Direct link for other Pro users: - https://rowsandall.com/rowers/{{ id }}/cumstats/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }} + https://rowsandall.com/rowers/{{ id }}/cumstats/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}/p/{{ plotfield }}

@@ -95,7 +95,13 @@
{% if stats %} {% for key, value in stats.items %} -

{{ value.verbosename }}

+

{{ value.verbosename }}

+
+

+ Plot +

+
+ diff --git a/rowers/urls.py b/rowers/urls.py index a7593b1a..952b6175 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -141,6 +141,8 @@ urlpatterns = [ url(r'^histo/(?P\w+.*)/(?P\w+.*)$',views.histo), url(r'^histo/(?P\d+)$',views.histo), url(r'^histo/$',views.histo), + url(r'^cumstats/(?P\w+.*)/(?P\w+.*)/p/(?P\w+.*)$',views.cumstats), + url(r'^(?P\d+)/cumstats/(?P\w+.*)/(?P\w+.*)/p/(?P\w+.*)$',views.cumstats), url(r'^(?P\d+)/cumstats/(?P\w+.*)/(?P\w+.*)$',views.cumstats), url(r'^(?P\d+)/cumstats/(?P\d+)$',views.cumstats), url(r'^cumstats/(?P\w+.*)/(?P\w+.*)$',views.cumstats), diff --git a/rowers/views.py b/rowers/views.py index b0cb9fcd..6746e7fe 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -3839,6 +3839,7 @@ def cumstats(request,theuser=0, deltadays=-1, startdatestring="", enddatestring="", + plotfield='spm', options={ 'includereststrokes':False, 'workouttypes':['rower','dynamic','slides'] @@ -4010,7 +4011,8 @@ def cumstats(request,theuser=0, # interactive box plot datadf['workoutid'].replace(datemapping,inplace=True) datadf.rename(columns={"workoutid":"date"},inplace=True) - script,div = interactive_boxchart(datadf,'wash') + datadf = datadf.sort_values(['date']) + script,div = interactive_boxchart(datadf,plotfield) # set options form correctly initial = {} @@ -4041,6 +4043,7 @@ def cumstats(request,theuser=0, 'cordict':cordict, 'plotscript':script, 'plotdiv':div, + 'plotfield':plotfield, }) request.session['options'] = options From 79149ca7a315b9286190cb92f5d41fc72d6476c4 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 12 May 2017 19:22:46 +0200 Subject: [PATCH 6/6] stats DF empty behavior --- rowers/views.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/rowers/views.py b/rowers/views.py index 6746e7fe..f98d3a29 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -3973,7 +3973,35 @@ def cumstats(request,theuser=0, if datadf.empty: - return HttpResponse("No data found") + stats = {} + plotfield = 'spm' + cordict = {} + div = "No Data found" + script = '' + + response = render(request, + 'cumstats.html', + { + 'stats':stats, + 'teams':get_my_teams(request.user), + 'options':options, + 'id':theuser, + 'theuser':u, + 'startdate':startdate, + 'enddate':enddate, + 'form':form, + 'deltaform':deltaform, + 'optionsform':optionsform, + 'cordict':cordict, + 'plotscript':script, + 'plotdiv':div, + 'plotfield':plotfield, + }) + + request.session['options'] = options + + return response + # Create stats