diff --git a/rowers/dataprep.py b/rowers/dataprep.py
index 4cab2758..abfde140 100644
--- a/rowers/dataprep.py
+++ b/rowers/dataprep.py
@@ -38,7 +38,10 @@ import pandas as pd
import numpy as np
import itertools
import math
-from tasks import handle_sendemail_unrecognized,handle_sendemail_breakthrough
+from tasks import (
+ handle_sendemail_unrecognized,handle_sendemail_breakthrough,
+ handle_sendemail_hard
+ )
from django.conf import settings
from sqlalchemy import create_engine
@@ -654,6 +657,7 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
w.save()
isbreakthrough = False
+ ishard = False
if workouttype == 'water':
df = getsmallrowdata_db(['power','workoutid','time'],ids=[w.id])
# delta,cpvalues,avgpower = datautils.getsinglecp(row.df)
@@ -664,12 +668,15 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
dfgrouped = df.groupby(['workoutid'])
delta,cpvalues,avgpower = datautils.getcp(dfgrouped,logarr)
- res,btvalues = utils.isbreakthrough(delta,cpvalues,r.p0,r.p1,r.p2,r.p3,r.cpratio)
+ res,btvalues,res2 = utils.isbreakthrough(delta,cpvalues,r.p0,r.p1,r.p2,r.p3,r.cpratio)
else:
res = 0
+ res2 = 0
if res:
isbreakthrough = True
res = datautils.updatecp(delta,cpvalues,r)
+ if res2 and not isbreakthrough:
+ ishard = True
# submit email task to send email about breakthrough workout
if isbreakthrough:
@@ -691,6 +698,26 @@ def save_workout_database(f2,r,dosmooth=True,workouttype='rower',
pass
else:
pass
+ # submit email task to send email about breakthrough workout
+ if ishard:
+ a_messages.info(r.user,'That was a pretty hard workout')
+ if settings.DEBUG and r.getemailnotifications:
+ res = handle_sendemail_hard.delay(w.id,r.user.email,
+ r.user.first_name,
+ r.user.last_name,
+ btvalues=btvalues.to_json())
+ elif r.getemailnotifications:
+ try:
+ res = queuehigh.enqueue(
+ handle_sendemail_hard(w.id,
+ r.user.email,
+ r.user.first_name,
+ r.user.last_name,
+ btvalues=btvalues.to_json()))
+ except AttributeError:
+ pass
+ else:
+ pass
if privacy == 'visible':
ts = Team.objects.filter(rower=r)
diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py
index 7e828b40..2cc207c0 100644
--- a/rowers/interactiveplots.py
+++ b/rowers/interactiveplots.py
@@ -2482,13 +2482,13 @@ def interactive_flex_chart2(id=0,promember=0,
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,
slider_work_min,
slider_work_max,
- annotation,
],
),
plot])
@@ -2499,6 +2499,213 @@ def interactive_flex_chart2(id=0,promember=0,
return [script,div,js_resources,css_resources,workstrokesonly]
+def thumbnails_set(r,id,favorites):
+ charts = []
+
+ columns = [f.xparam for f in favorites]
+ columns += [f.yparam1 for f in favorites]
+ columns += [f.yparam2 for f in favorites]
+
+ columns += ['time']
+ rowdata = dataprep.getsmallrowdata_db(columns,ids=[id],doclean=True)
+ rowdata.dropna(axis=1,how='all',inplace=True)
+
+ if rowdata.empty:
+ return [
+ {'script':"",
+ 'div':"",
+ 'notes':""
+ }]
+ else:
+ try:
+ rowdata.sort_values(by='time',ascending=True,inplace=True)
+ except KeyError:
+ pass
+
+ l = len(rowdata)
+ maxlength = 50
+ if l > maxlength:
+ bins = np.linspace(rowdata['time'].min(),rowdata['time'].max(),maxlength)
+ groups = rowdata.groupby(np.digitize(rowdata['time'],bins))
+ rowdata = groups.mean()
+
+ for f in favorites:
+ workstrokesonly = not f.reststrokes
+ script,div = thumbnail_flex_chart(
+ rowdata,
+ id=id,
+ xparam=f.xparam,
+ yparam1=f.yparam1,
+ yparam2=f.yparam2,
+ plottype=f.plottype,
+ )
+
+
+ charts.append({
+ 'script':script,
+ 'div':div,
+ 'notes':f.notes})
+
+ return charts
+
+
+def thumbnail_flex_chart(rowdata,id=0,promember=0,
+ xparam='time',
+ yparam1='pace',
+ yparam2='hr',
+ plottype='line',
+ workstrokesonly=False):
+
+
+ try:
+ tests = rowdata[yparam2]
+ except KeyError:
+ yparam2 = 'None'
+
+ try:
+ tests = rowdata[yparam1]
+ except KeyError:
+ yparam1 = 'None'
+
+
+
+ try:
+ tseconds = rowdata.ix[:,'time']
+ except KeyError:
+ return '','No time data - cannot make flex plot','',''
+
+
+ try:
+ rowdata['x1'] = rowdata.ix[:,xparam]
+ except KeyError:
+ rowdata['x1'] = 0*rowdata.ix[:,'time']
+
+ try:
+ rowdata['y1'] = rowdata.ix[:,yparam1]
+ except KeyError:
+ rowdata['y1'] = 0*rowdata.ix[:,'time']
+
+ if yparam2 != 'None':
+ try:
+ rowdata['y2'] = rowdata.ix[:,yparam2]
+ except KeyError:
+ rowdata['y2'] = 0*rowdata.ix[:,'time']
+ else:
+ rowdata['y2'] = rowdata['y1']
+
+ if xparam=='time':
+ xaxmax = tseconds.max()
+ xaxmin = tseconds.min()
+ elif xparam=='distance' or xparam=='cumdist':
+ xaxmax = rowdata['x1'].max()
+ xaxmin = rowdata['x1'].min()
+ else:
+ xaxmax = yaxmaxima[xparam]
+ xaxmin = yaxminima[xparam]
+
+ x_axis_type = 'linear'
+ y_axis_type = 'linear'
+ if xparam == 'time':
+ x_axis_type = 'datetime'
+
+ if yparam1 == 'pace':
+ y_axis_type = 'datetime'
+ y1mean = rowdata.ix[:,'pace'].mean()
+
+
+ rowdata['xname'] = axlabels[xparam]
+ rowdata['yname1'] = axlabels[yparam1]
+ if yparam2 != 'None':
+ rowdata['yname2'] = axlabels[yparam2]
+ else:
+ rowdata['yname2'] = axlabels[yparam1]
+
+
+ # prepare data
+ source = ColumnDataSource(
+ rowdata
+ )
+
+
+ sizing_mode = 'fixed' # 'scale_width' also looks nice with this example
+ plot = Figure(x_axis_type=x_axis_type,y_axis_type=y_axis_type,
+ plot_width=200,plot_height=150,
+ )
+
+
+
+ plot.toolbar.logo = None
+ plot.toolbar_location = None
+ #plot.yaxis.visible = False
+ plot.xaxis.axis_label_text_font_size = "7pt"
+ plot.yaxis.axis_label_text_font_size = "7pt"
+ plot.xaxis.major_label_text_font_size = "7pt"
+ plot.yaxis.major_label_text_font_size = "7pt"
+
+ if plottype=='line':
+ plot.line('x1','y1',source=source)
+ elif plottype=='scatter':
+ plot.scatter('x1','y1',source=source,fill_alpha=0.4,
+ line_color=None)
+
+ plot.xaxis.axis_label = axlabels[xparam]
+ plot.yaxis.axis_label = axlabels[yparam1]
+
+
+
+ yrange1 = Range1d(start=yaxminima[yparam1],end=yaxmaxima[yparam1])
+ plot.y_range = yrange1
+
+ if (xparam != 'time') and (xparam != 'distance') and (xparam != 'cumdist'):
+ xrange1 = Range1d(start=yaxminima[xparam],end=yaxmaxima[xparam])
+ plot.x_range = xrange1
+
+ if xparam == 'time':
+ xrange1 = Range1d(start=xaxmin,end=xaxmax)
+ plot.x_range = xrange1
+ plot.xaxis[0].formatter = DatetimeTickFormatter(
+ hours = ["%H"],
+ minutes = ["%M"],
+ seconds = ["%S"],
+ days = ["0"],
+ months = [""],
+ years = [""]
+ )
+
+
+ if yparam1 == 'pace':
+ plot.yaxis[0].formatter = DatetimeTickFormatter(
+ seconds = ["%S"],
+ minutes = ["%M"]
+ )
+
+
+ if yparam2 != 'None':
+ yrange2 = Range1d(start=yaxminima[yparam2],end=yaxmaxima[yparam2])
+ plot.extra_y_ranges["yax2"] = yrange2
+ #= {"yax2": yrange2}
+
+ if plottype=='line':
+ plot.line('x1','y2',color="red",y_range_name="yax2",
+ source=source)
+
+ elif plottype=='scatter':
+ plot.scatter('x1','y2',source=source,
+ fill_alpha=0.4,
+ line_color=None,color="red",y_range_name="yax2")
+
+ plot.add_layout(LinearAxis(y_range_name="yax2",
+ axis_label=axlabels[yparam2],
+ major_label_text_font_size="7pt",
+ axis_label_text_font_size="7pt",
+ ),'right',
+ )
+
+
+ script, div = components(plot)
+
+ return [script,div]
+
def interactive_bar_chart(id=0,promember=0):
# check if valid ID exists (workout exists)
diff --git a/rowers/metrics.py b/rowers/metrics.py
index b4140f1c..b2986565 100644
--- a/rowers/metrics.py
+++ b/rowers/metrics.py
@@ -219,7 +219,7 @@ rowingmetrics = (
'ax_max': 15,
'default': 0,
'mode':'both',
- 'type': 'pro'}),
+ 'type': 'basic'}),
)
@@ -236,3 +236,68 @@ axlabels = {ax[0]:ax[1] for ax in axes}
yaxminima = {ax[0]:ax[2] for ax in axes}
yaxmaxima = {ax[0]:ax[3] for ax in axes}
+
+defaultfavoritecharts = (
+ {
+ 'yparam1':'pace',
+ 'yparam2':'spm',
+ 'xparam':'time',
+ 'plottype':'line',
+ 'workouttype':'both',
+ 'reststrokes':True,
+ 'notes':"""This chart shows your pace and stroke rate versus time. """,
+ },
+ {
+ 'yparam1':'pace',
+ 'yparam2':'hr',
+ 'xparam':'time',
+ 'plottype':'line',
+ 'workouttype':'both',
+ 'reststrokes':True,
+ 'notes':"""This chart shows your pace and heart rate versus time.
+Heart rate values will be shown only when it is in your data, i.e.
+in case you recorded your heart rate during your workout""",
+ },
+ {
+ 'yparam1':'distanceperstroke',
+ 'yparam2':'hr',
+ 'xparam':'time',
+ 'plottype':'line',
+ 'workouttype':'otw',
+ 'reststrokes':True,
+ 'notes':"""This chart shows the Distance covered per stroke, and your
+heart rate versus time. """,
+ },
+ {
+ 'yparam1':'strokeenergy',
+ 'yparam2':'hr',
+ 'xparam':'time',
+ 'plottype':'line',
+ 'workouttype':'ote',
+ 'reststrokes':True,
+ 'notes':"""This chart shows the Work per Stroke and your heart rate
+plotted versus time. """,
+ },
+ {
+ 'yparam1':'distanceperstroke',
+ 'yparam2':'None',
+ 'xparam':'spm',
+ 'plottype':'scatter',
+ 'workouttype':'otw',
+ 'reststrokes':True,
+ 'notes':"""This chart shows the Distance per Stroke versus stroke
+stroke rate. You should see a steady decline of the Distance per Stroke
+as you increase stroke rate. Typical values are > 10m for steady state
+dropping to 8m for race pace in the single.""",
+ },
+ {
+ 'yparam1':'strokeenergy',
+ 'yparam2':'None',
+ 'xparam':'spm',
+ 'plottype':'line',
+ 'workouttype':'ote',
+ 'reststrokes':True,
+ 'notes':"""This chart shows the Work per Stroke versus Stroke Rate.
+This value should be fairly constant across all stroke rates.""",
+ },
+ )
diff --git a/rowers/models.py b/rowers/models.py
index 6bb2c91e..83e699e3 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -298,6 +298,10 @@ class Rower(models.Model):
defaulttimezone = models.CharField(default='UTC',max_length=100,
choices=timezones,
verbose_name='Default Time Zone')
+
+ # Show flex chart notes
+ showfavoritechartnotes = models.BooleanField(default=True,
+ verbose_name='Show Notes for Favorite Charts')
def __str__(self):
return self.user.username
@@ -344,6 +348,8 @@ class FavoriteChart(models.Model):
default='both',
verbose_name='Workout Type')
reststrokes = models.BooleanField(default=True,verbose_name="Incl. Rest")
+ notes = models.CharField(max_length=300,verbose_name='Chart Notes',
+ default='Flex Chart Notes',blank=True)
user = models.ForeignKey(Rower)
@@ -351,7 +357,11 @@ class FavoriteForm(ModelForm):
class Meta:
model = FavoriteChart
fields = ['xparam','yparam1','yparam2',
- 'plottype','workouttype','reststrokes']
+ 'plottype','workouttype','reststrokes','notes']
+# widgets = {
+# 'notes': forms.Textarea,
+# }
+
# To generate favorite chart forms on the fly
class BaseFavoriteFormSet(BaseFormSet):
@@ -828,7 +838,8 @@ class RowerPowerZonesForm(ModelForm):
class AccountRowerForm(ModelForm):
class Meta:
model = Rower
- fields = ['weightcategory','getemailnotifications','defaulttimezone']
+ fields = ['weightcategory','getemailnotifications',
+ 'defaulttimezone','showfavoritechartnotes']
class UserForm(ModelForm):
class Meta:
diff --git a/rowers/tasks.py b/rowers/tasks.py
index 71399b50..ce7ff02c 100644
--- a/rowers/tasks.py
+++ b/rowers/tasks.py
@@ -139,6 +139,40 @@ def handle_sendemail_breakthrough(workoutid,useremail,
# remove tcx file
return 1
+# send email when a breakthrough workout is uploaded
+@app.task
+def handle_sendemail_hard(workoutid,useremail,
+ userfirstname,userlastname,
+ btvalues = pd.DataFrame().to_json()):
+
+ # send email with attachment
+ subject = "That was a pretty hard workout on rowsandall.com"
+ message = "Dear "+userfirstname+",\n"
+ message += "Congratulations! Your recent workout has been analyzed"
+ message += " by Rowsandall.com and it appears that it was pretty hard work."
+ message += " You were working pretty close to your Critical Power\n\n"
+ message += " Critical Power (CP) is the power that you can "
+ message += "sustain for a given duration. For more, see this "
+ message += " article in the analytics blog:\n\n"
+ message += " http://analytics.rowsandall.com/2017/06/17/how-do-we-calculate-critical-power/ \n\n"
+ message += "Link to the workout http://rowsandall.com/rowers/workout/"
+ message += str(workoutid)
+ message +="/edit\n\n"
+
+ message += "To opt out of these email notifications, deselect the checkbox on your Profile page under Account Information.\n\n"
+
+ message += "Best Regards, the Rowsandall Team"
+
+ email = EmailMessage(subject, message,
+ 'Rowsandall
+ Edit Workout
+
+ Advanced
+
+ Edit Intervals
+
+ Add Time Plot
+
+ Add Pie Chart
+
+ Workout Stats
+
+ Flex Charts
+{% for chart in charts %}
+ Navigation
+Static Charts
+{% for graph in statcharts %}
+
+
Create Charts
+Summary
+
+ {{ workout.summary }}
+
+
Click on the thumbnails to view the full chart
++ {% endblock %} +