Merge branch 'release/v2.74'
This commit is contained in:
+17
-4
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Brochure{% endblock title %}
|
||||
{% block title %}Brochure{% endblock title %}
|
||||
{% block meta %}
|
||||
<style>
|
||||
#container { overflow: auto; -webkit-overflow-scrolling: touch; height: 500px; }
|
||||
object { width: 900px; height: 5000px }
|
||||
</style>
|
||||
{% endblock meta %}
|
||||
{% block content %}
|
||||
|
||||
<div class="grid_12 alpha">
|
||||
<h2>Read our Brochure</h2>
|
||||
|
||||
<embed src="/static/brochure WEB.pdf" width="960" height="650">
|
||||
<div id="container">
|
||||
<object id="obj" data="/static/brochure WEB.pdf" >
|
||||
object can't be rendered
|
||||
</object>
|
||||
</div>
|
||||
<!--
|
||||
<embed src="/static/brochure WEB.pdf" width="960" height="650">
|
||||
-->
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
@@ -5,6 +5,35 @@
|
||||
{% block title %}Workout Statistics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script type="text/javascript" src="/static/js/bokeh-0.12.3.min.js"></script>
|
||||
<script async="true" type="text/javascript">
|
||||
Bokeh.set_log_level("info");
|
||||
</script>
|
||||
|
||||
{{ plotscript |safe }}
|
||||
|
||||
<script>
|
||||
// Set things up to resize the plot on a window resize. You can play with
|
||||
// the arguments of resize_width_height() to change the plot's behavior.
|
||||
var plot_resize_setup = function () {
|
||||
var plotid = Object.keys(Bokeh.index)[0]; // assume we have just one plot
|
||||
var plot = Bokeh.index[plotid];
|
||||
var plotresizer = function() {
|
||||
// arguments: use width, use height, maintain aspect ratio
|
||||
plot.resize_width_height(true, true, true);
|
||||
};
|
||||
window.addEventListener('resize', plotresizer);
|
||||
plotresizer();
|
||||
};
|
||||
window.addEventListener('load', plot_resize_setup);
|
||||
</script>
|
||||
<style>
|
||||
/* Need this to get the page in "desktop mode"; not having an infinite height.*/
|
||||
html, body {height: 100%; margin:5px;}
|
||||
</style>
|
||||
|
||||
|
||||
<div class="grid_12 alpha">
|
||||
<h1>Workout Statistics</h1>
|
||||
<p>
|
||||
@@ -18,7 +47,7 @@
|
||||
between {{ startdate|date }} and {{ enddate|date }}</p>
|
||||
|
||||
<p>Direct link for other Pro users:
|
||||
<a href="/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" }}</a>
|
||||
<a href="/rowers/{{ id }}/cumstats/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}/p/{{ plotfield }}">https://rowsandall.com/rowers/{{ id }}/cumstats/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}/p/{{ plotfield }}</a>
|
||||
</p>
|
||||
|
||||
<form enctype="multipart/form-data" action="{{ formloc }}" method="post">
|
||||
@@ -66,7 +95,13 @@
|
||||
<div class="grid_4 alpha">
|
||||
{% if stats %}
|
||||
{% for key, value in stats.items %}
|
||||
<h2>{{ value.verbosename }}</h2>
|
||||
<h2>{{ value.verbosename }}</h2>
|
||||
<div class="grid_1">
|
||||
<p>
|
||||
<a class="button blue small" href="/rowers/{{ id }}/cumstats/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}/p/{{ key }}">Plot</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<table width="100%" class="listtable">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -97,6 +132,7 @@
|
||||
</div>
|
||||
<div class="grid_8 omega">
|
||||
{% if cordict %}
|
||||
<div class="grid_8">
|
||||
<h2> Correlation Matrix</h2>
|
||||
<p>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.
|
||||
</p>
|
||||
@@ -132,8 +168,13 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="grid_8">
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="grid_8">
|
||||
{{ plotdiv|safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -51,4 +51,4 @@
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -74,6 +74,34 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid_8 omega">
|
||||
{% if otherstats %}
|
||||
<div class="grid_4 alpha">
|
||||
|
||||
</div>
|
||||
<div class="grid_4 omega">
|
||||
<h2>Other Stats</h2>
|
||||
<table width="100%" class="listtable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Metric</th>
|
||||
<th>Value</th>
|
||||
<th>Unit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key, value in otherstats.items %}
|
||||
<tr>
|
||||
<td>{{ value.verbose_name }}</td>
|
||||
<td>{{ value.value }}</td>
|
||||
<td>{{ value.unit }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid_8 alpha">
|
||||
{% if cordict %}
|
||||
<h2> Correlation matrix</h2>
|
||||
<p>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 @@
|
||||
</table>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -141,6 +141,8 @@ urlpatterns = [
|
||||
url(r'^histo/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.histo),
|
||||
url(r'^histo/(?P<deltadays>\d+)$',views.histo),
|
||||
url(r'^histo/$',views.histo),
|
||||
url(r'^cumstats/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)/p/(?P<plotfield>\w+.*)$',views.cumstats),
|
||||
url(r'^(?P<theuser>\d+)/cumstats/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)/p/(?P<plotfield>\w+.*)$',views.cumstats),
|
||||
url(r'^(?P<theuser>\d+)/cumstats/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.cumstats),
|
||||
url(r'^(?P<theuser>\d+)/cumstats/(?P<deltadays>\d+)$',views.cumstats),
|
||||
url(r'^cumstats/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.cumstats),
|
||||
|
||||
+102
-3
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
Reference in New Issue
Block a user