diff --git a/rowers/dataprep.py b/rowers/dataprep.py
index 49fde4be..84a25803 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')
@@ -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')
@@ -967,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
@@ -979,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 f79b9ce2..72607cbf 100644
--- a/rowers/interactiveplots.py
+++ b/rowers/interactiveplots.py
@@ -4,14 +4,14 @@ 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
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,34 @@ def tailwind(bearing,vwind,winddir):
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,
+ 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)
+
+
+ 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/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 %}
{% endblock content %}
diff --git a/rowers/templates/cumstats.html b/rowers/templates/cumstats.html
index eb0b9654..7d814c62 100644
--- a/rowers/templates/cumstats.html
+++ b/rowers/templates/cumstats.html
@@ -5,6 +5,35 @@
{% block title %}Workout Statistics{% endblock %}
{% block content %}
+
+
+
+
+ {{ plotscript |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/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
+
+
+
+ | Metric |
+ Value |
+ Unit |
+
+
+
+ {% for key, value in otherstats.items %}
+
+ | {{ value.verbose_name }} |
+ {{ value.value }} |
+ {{ value.unit }} |
+
+ {% endfor %}
+
+
+
+ {% 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/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 a1685e48..f98d3a29 100644
--- a/rowers/views.py
+++ b/rowers/views.py
@@ -3834,11 +3834,12 @@ 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="",
enddatestring="",
+ plotfield='spm',
options={
'includereststrokes':False,
'workouttypes':['rower','dynamic','slides']
@@ -3957,6 +3958,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
@@ -3966,12 +3973,41 @@ 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
stats = {}
fielddict.pop('workoutstate')
+ fielddict.pop('workoutid')
for field,verbosename in fielddict.iteritems():
thedict = {
@@ -3998,8 +4034,14 @@ 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)
+ datadf = datadf.sort_values(['date'])
+ script,div = interactive_boxchart(datadf,plotfield)
+
# set options form correctly
initial = {}
initial['includereststrokes'] = includereststrokes
@@ -4027,6 +4069,9 @@ def cumstats(request,theuser=0,
'deltaform':deltaform,
'optionsform':optionsform,
'cordict':cordict,
+ 'plotscript':script,
+ 'plotdiv':div,
+ 'plotfield':plotfield,
})
request.session['options'] = options
@@ -4036,6 +4081,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 +4141,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 +4199,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']