diff --git a/rowers/forms.py b/rowers/forms.py
index 0566c38c..3763b132 100644
--- a/rowers/forms.py
+++ b/rowers/forms.py
@@ -298,6 +298,7 @@ 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',
label='Metric')
@@ -313,6 +314,36 @@ class BoxPlotChoiceForm(forms.Form):
includereststrokes = forms.BooleanField(initial=False,
required=False,
label='Include Rest Strokes')
+
+grouplabels = axlabels.copy()
+grouplabels['date'] = 'Date'
+grouplabels['workoutid'] = 'Workout'
+grouplabels.pop('None')
+grouplabels.pop('time')
+groupchoices = list(sorted(grouplabels.items(), key = lambda x:x[1]))
+
+class MultiFlexChoiceForm(forms.Form):
+ xparam = forms.ChoiceField(choices=parchoices,initial='spm',
+ label='X axis')
+ yparam = forms.ChoiceField(choices=parchoices,initial='power',
+ label='Y axis')
+ groupby = forms.ChoiceField(choices=groupchoices,initial='date',
+ label='Group By')
+ binsize = forms.FloatField(initial=1,required=False,label = 'Bin Size')
+ spmmin = forms.FloatField(initial=15,
+ required=False,label = 'Min SPM')
+ spmmax = forms.FloatField(initial=55,
+ required=False,label = 'Max SPM')
+ workmin = forms.FloatField(initial=0,
+ required=False,label = 'Min Work per Stroke')
+ workmax = forms.FloatField(initial=1500,
+ required=False,label = 'Max Work per Stroke')
+ ploterrorbars = forms.BooleanField(initial=False,
+ required=False,
+ label='Plot Error Bars')
+ includereststrokes = forms.BooleanField(initial=False,
+ required=False,
+ label='Include Rest Strokes')
class ChartParamChoiceForm(forms.Form):
plotchoices = (
diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py
index e8b5ec77..eb524261 100644
--- a/rowers/interactiveplots.py
+++ b/rowers/interactiveplots.py
@@ -69,7 +69,46 @@ watermarkw = 184
watermarkh = 35
watermarkanchor = 'bottom_right'
+def errorbar(fig, x, y, source=ColumnDataSource(),
+ xerr=False, yerr=False, color='red',
+ point_kwargs={}, error_kwargs={}):
+ fig.circle(x, y, source=source, name='data',color=color, **point_kwargs)
+
+ xvalues = source.data[x]
+ yvalues = source.data[y]
+
+ xerrvalues = source.data['xerror']
+ yerrvalues = source.data['yerror']
+
+
+ try:
+ a = xvalues[0]+1
+ if xerr:
+ x_err_x = []
+ x_err_y = []
+ for px, py, err in zip(xvalues, yvalues, xerrvalues):
+ x_err_x.append((px - err, px + err))
+ x_err_y.append((py, py))
+ fig.multi_line(x_err_x, x_err_y, color=color,
+ name='xerr',
+ **error_kwargs)
+ except TypeError:
+ pass
+
+ try:
+ a = yvalues[0]+1
+ if yerr:
+ y_err_x = []
+ y_err_y = []
+ for px, py, err in zip(xvalues, yvalues, yerrvalues):
+ y_err_x.append((px, px))
+ y_err_y.append((py - err, py + err))
+ fig.multi_line(y_err_x, y_err_y, color=color,
+ name='yerr',**error_kwargs)
+ except TypeError:
+ pass
+
def tailwind(bearing,vwind,winddir):
""" Calculates head-on head/tailwind in direction of rowing
@@ -1124,6 +1163,98 @@ def interactive_chart(id=0,promember=0):
return [script,div]
+def interactive_multiflex(datadf,xparam,yparam,groupby,extratitle='',
+ ploterrorbars=False):
+ if datadf.empty:
+ return ['','
No non-zero data in selection
']
+
+
+ xparamname = axlabels[xparam]
+ yparamname = axlabels[yparam]
+
+ if xparam=='distance':
+ xaxmax = datadf['x1'].max()
+ xaxmin = datadf['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 yparam == 'pace':
+ y_axis_type = 'datetime'
+
+ source = ColumnDataSource(
+ datadf,
+ )
+
+
+ TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,resize,hover'
+
+ plot = Figure(x_axis_type=x_axis_type,y_axis_type=y_axis_type,
+ tools=TOOLS,
+ toolbar_location="above",
+ toolbar_sticky=False)
+ # add watermark
+ plot.extra_y_ranges = {"watermark": watermarkrange}
+ plot.extra_x_ranges = {"watermark": watermarkrange}
+
+ plot.image_url([watermarkurl],watermarkx,watermarky,
+ watermarkw,watermarkh,
+ global_alpha=watermarkalpha,
+ w_units='screen',
+ h_units='screen',
+ anchor=watermarkanchor,
+ dilate=True,
+ x_range_name = "watermark",
+ y_range_name = "watermark",
+ )
+
+ errorbar(plot,xparam,yparam,source=source,
+ xerr=ploterrorbars,
+ yerr=ploterrorbars,
+ point_kwargs={
+ 'line_color':None,
+ 'legend':yparamname,
+ 'size':10,
+ })
+
+ plot.xaxis.axis_label = axlabels[xparam]
+ plot.yaxis.axis_label = axlabels[yparam]
+
+
+ yrange1 = Range1d(start=yaxminima[yparam],end=yaxmaxima[yparam])
+ plot.y_range = yrange1
+
+ xrange1 = Range1d(start=yaxminima[xparam],end=yaxmaxima[xparam])
+ plot.x_range = xrange1
+
+ if yparam == 'pace':
+ plot.yaxis[0].formatter = DatetimeTickFormatter(
+ seconds = ["%S"],
+ minutes = ["%M"]
+ )
+
+ hover = plot.select(dict(type=HoverTool))
+
+ if groupby != 'date':
+ hover.tooltips = OrderedDict([
+ (groupby,'@groupval{1.1}'),
+ ])
+ else:
+ hover.tooltips = OrderedDict([
+ (groupby,'@groupval'),
+ ])
+
+ hover.mode = 'mouse'
+
+ script,div = components(plot)
+
+
+ return [script,div]
+
def interactive_cum_flex_chart2(theworkouts,promember=0,
xparam='spm',
yparam1='power',
diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html
index 4eae9987..2aeca53a 100644
--- a/rowers/templates/analysis.html
+++ b/rowers/templates/analysis.html
@@ -95,7 +95,7 @@
Analyse power vs piece duration to make predictions.
-
+
{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
Multi Compare
@@ -107,6 +107,18 @@
Compare many workouts
+
+
+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %}
+ Multi Flex
+ {% else %}
+ Multi Flex
+ {% endif %}
+
+
+ Select workouts and make X-Y charts of averages over various metrics
+
+
diff --git a/rowers/templates/multiflex.html b/rowers/templates/multiflex.html
new file mode 100644
index 00000000..62666379
--- /dev/null
+++ b/rowers/templates/multiflex.html
@@ -0,0 +1,70 @@
+{% extends "base.html" %}
+{% load staticfiles %}
+{% load rowerfilters %}
+
+{% block title %}View Comparison {% endblock %}
+
+{% block content %}
+
+
+
+
+{{ interactiveplot |safe }}
+
+
+
+
+
+
+
Multi Flex Chart
+
+
+ {{ the_div|safe }}
+
+
+
+
+
+
+ You can use the form above to change the metric or filter the data.
+ Set Min SPM and Max SPM to select only strokes in a certain range of
+ stroke rates.
+ Set Work per Stroke to a minimum value to remove "paddle" strokes or turns.
+
+
+
+
+
+
+{% endblock %}
diff --git a/rowers/templates/user_multiflex_select.html b/rowers/templates/user_multiflex_select.html
new file mode 100644
index 00000000..0b0b8642
--- /dev/null
+++ b/rowers/templates/user_multiflex_select.html
@@ -0,0 +1,118 @@
+{% extends "base.html" %}
+{% load staticfiles %}
+{% load rowerfilters %}
+
+{% block title %}Workouts{% endblock %}
+
+{% block content %}
+
+
+
+ {% if team %}
+
+ {% include "teambuttons.html" with teamid=team.id team=team %}
+
+{% endif %}
+
+
+ {% if theuser %}
+
{{ theuser.first_name }}'s Workouts
+ {% else %}
+ {{ user.first_name }}'s Workouts
+ {% endif %}
+
+
+ {% if user.is_authenticated and user|is_manager %}
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/rowers/urls.py b/rowers/urls.py
index edf1f23d..b94aafbb 100644
--- a/rowers/urls.py
+++ b/rowers/urls.py
@@ -129,6 +129,10 @@ urlpatterns = [
url(r'^user-boxplot-select/user/(?P
\d+)/$',views.user_boxplot_select),
url(r'^user-boxplot-select/(?P\w+.*)/(?P\w+.*)$',views.user_boxplot_select),
url(r'^user-boxplot-select/$',views.user_boxplot_select),
+ url(r'^user-multiflex-select/user/(?P\d+)/(?P\w+.*)/(?P\w+.*)$',views.user_multiflex_select),
+ url(r'^user-multiflex-select/user/(?P\d+)/$',views.user_multiflex_select),
+ url(r'^user-multiflex-select/(?P\w+.*)/(?P\w+.*)$',views.user_multiflex_select),
+ url(r'^user-multiflex-select/$',views.user_multiflex_select),
url(r'^list-graphs/$',views.graphs_view),
url(r'^(?P\d+)/ote-bests/(?P\w+.*)/(?P\w+.*)$',views.rankings_view),
url(r'^(?P\d+)/ote-bests/(?P\d+)$',views.rankings_view),
@@ -243,6 +247,9 @@ urlpatterns = [
url(r'^user-boxplot/(?P\d+)$',views.boxplot_view),
url(r'^user-boxplot/$',views.boxplot_view),
url(r'^user-boxplot$',views.boxplot_view),
+ url(r'^user-multiflex/(?P\d+)$',views.multiflex_view),
+ url(r'^user-multiflex/$',views.multiflex_view),
+ url(r'^user-multiflex$',views.multiflex_view),
url(r'^me/teams/$',views.rower_teams_view),
url(r'^team/(?P\d+)/$',views.team_view),
url(r'^team/(?P\d+)/memberstats$',views.team_members_stats_view),
diff --git a/rowers/views.py b/rowers/views.py
index 4a580263..48e174d4 100644
--- a/rowers/views.py
+++ b/rowers/views.py
@@ -34,7 +34,7 @@ from rowers.forms import (
EmailForm, RegistrationForm, RegistrationFormTermsOfService,
RegistrationFormUniqueEmail,CNsummaryForm,UpdateWindForm,
UpdateStreamForm,WorkoutMultipleCompareForm,ChartParamChoiceForm,
- FusionMetricChoiceForm,BoxPlotChoiceForm,
+ FusionMetricChoiceForm,BoxPlotChoiceForm,MultiFlexChoiceForm,
)
from rowers.models import Workout, User, Rower, WorkoutForm,FavoriteChart
from rowers.models import (
@@ -3324,6 +3324,358 @@ def multi_compare_view(request):
url = reverse(workouts_view)
return HttpResponseRedirect(url)
+# Multi Flex Chart with Grouping
+@user_passes_test(ispromember,login_url="/",redirect_field_name=None)
+def user_multiflex_select(request,
+ startdatestring="",
+ enddatestring="",
+ message='',
+ successmessage='',
+ startdate=timezone.now()-datetime.timedelta(days=30),
+ enddate=timezone.now()+datetime.timedelta(days=1),
+ userid=0):
+
+ if userid == 0:
+ user = request.user
+ else:
+ user = User.objects.get(id=userid)
+
+
+ r = getrower(user)
+
+ if 'startdate' in request.session:
+ startdate = iso8601.parse_date(request.session['startdate'])
+
+
+ if 'enddate' in request.session:
+ enddate = iso8601.parse_date(request.session['enddate'])
+
+
+ if request.method == 'POST':
+ dateform = DateRangeForm(request.POST)
+ if dateform.is_valid():
+ startdate = dateform.cleaned_data['startdate']
+ enddate = dateform.cleaned_data['enddate']
+ else:
+ dateform = DateRangeForm(initial={
+ 'startdate':startdate,
+ 'enddate':enddate,
+ })
+
+ startdate = datetime.datetime.combine(startdate,datetime.time())
+ enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59))
+ enddate = enddate+datetime.timedelta(days=1)
+
+ if startdatestring:
+ startdate = iso8601.parse_date(startdatestring)
+ if enddatestring:
+ enddate = iso8601.parse_date(enddatestring)
+
+ if enddate < startdate:
+ s = enddate
+ enddate = startdate
+ startdate = s
+
+
+ workouts = Workout.objects.filter(user=r,
+ startdatetime__gte=startdate,
+ startdatetime__lte=enddate).order_by("-date", "-starttime")
+
+ query = request.GET.get('q')
+ if query:
+ query_list = query.split()
+ workouts = workouts.filter(
+ reduce(operator.and_,
+ (Q(name__icontains=q) for q in query_list)) |
+ reduce(operator.and_,
+ (Q(notes__icontains=q) for q in query_list))
+ )
+
+ form = WorkoutMultipleCompareForm()
+ form.fields["workouts"].queryset = workouts
+
+ chartform = MultiFlexChoiceForm()
+
+ messages.info(request,successmessage)
+ messages.error(request,message)
+
+ startdatestring = startdate.strftime('%Y-%m-%d')
+ enddatestring = enddate.strftime('%Y-%m-%d')
+ request.session['startdate'] = startdatestring
+ request.session['enddate'] = enddatestring
+
+ return render(request, 'user_multiflex_select.html',
+ {'workouts': workouts,
+ 'dateform':dateform,
+ 'startdate':startdate,
+ 'enddate':enddate,
+ 'theuser':user,
+ 'form':form,
+ 'chartform':chartform,
+ 'teams':get_my_teams(request.user),
+ })
+
+@user_passes_test(ispromember,login_url="/",redirect_field_name=None)
+def multiflex_view(request,userid=0,
+ options={
+ 'includereststrokes':False,
+ 'ploterrorbars':False,
+ }):
+
+ if 'options' in request.session:
+ options = request.session['options']
+
+ includereststrokes = options['includereststrokes']
+ ploterrorbars = options['ploterrorbars']
+ workstrokesonly = not includereststrokes
+
+ if userid==0:
+ userid = request.user.id
+
+ if request.method == 'POST' and 'workouts' in request.POST:
+ form = WorkoutMultipleCompareForm(request.POST)
+ chartform = MultiFlexChoiceForm(request.POST)
+ if form.is_valid() and chartform.is_valid():
+ cd = form.cleaned_data
+ workouts = cd['workouts']
+ xparam = chartform.cleaned_data['xparam']
+ yparam = chartform.cleaned_data['yparam']
+ includereststrokes = chartform.cleaned_data['includereststrokes']
+ ploterrorbars = chartform.cleaned_data['ploterrorbars']
+ request.session['ploterrorbars'] = ploterrorbars
+ request.session['includereststrokes'] = includereststrokes
+ workstrokesonly = not includereststrokes
+
+ groupby = chartform.cleaned_data['groupby']
+ binsize = chartform.cleaned_data['binsize']
+
+ spmmin = chartform.cleaned_data['spmmin']
+ spmmax = chartform.cleaned_data['spmmax']
+ workmin = chartform.cleaned_data['workmin']
+ workmax = chartform.cleaned_data['workmax']
+
+ ids = [int(w.id) for w in workouts]
+ request.session['ids'] = ids
+
+ fieldlist,fielddict = dataprep.getstatsfields()
+ fieldlist = [xparam,yparam,groupby,
+ 'workoutid','spm','driveenergy',
+ 'workoutstate']
+
+ # prepare data frame
+ datadf = dataprep.read_cols_df_sql(ids,fieldlist)
+
+ datadf = dataprep.clean_df_stats(datadf,workstrokesonly=workstrokesonly)
+
+ datadf = dataprep.filter_df(datadf,'spm',spmmin,
+ largerthan=True)
+ datadf = dataprep.filter_df(datadf,'spm',spmmax,
+ largerthan=False)
+ datadf = dataprep.filter_df(datadf,'driveenergy',workmin,
+ largerthan=True)
+ datadf = dataprep.filter_df(datadf,'driveneergy',workmax,
+ largerthan=False)
+
+ datadf.dropna(axis=0,how='any',inplace=True)
+ datemapping = {
+ w.id:w.date for w in workouts
+ }
+
+ datadf['date'] = datadf['workoutid']
+ datadf['date'].replace(datemapping,inplace=True)
+
+ today = datetime.date.today()
+ datadf['days ago'] = map(lambda x : x.days, datadf.date - today)
+
+ if groupby != 'date':
+ bins = np.arange(datadf[groupby].min()-binsize,
+ datadf[groupby].max()+binsize,
+ binsize)
+ groups = datadf.groupby(pd.cut(datadf[groupby],bins))
+ else:
+ bins = np.arange(datadf['days ago'].min()-binsize,
+ datadf['days ago'].max()+binsize,
+ binsize,
+ )
+ groups = datadf.groupby(pd.cut(datadf['days ago'], bins))
+
+
+ xvalues = groups.mean()[xparam]
+ yvalues = groups.mean()[yparam]
+ xerror = groups.std()[xparam]
+ yerror = groups.std()[yparam]
+
+ df = pd.DataFrame({
+ xparam:xvalues,
+ yparam:yvalues,
+ 'xerror':xerror,
+ 'yerror':yerror,
+ })
+
+ if groupby != 'date':
+ try:
+ df['groupval'] = groups.mean()[groupby],
+ except ValueError:
+ df['groupval'] = groups.mean()[groupby].fillna(value=0)
+ else:
+ df['groupval'] = [x.strftime("%Y-%m-%d") for x in groups.min()[groupby]]
+
+ if userid == 0:
+ extratitle = ''
+ else:
+ u = User.objects.get(id=userid)
+ extratitle = ' '+u.first_name+' '+u.last_name
+
+
+
+ script,div = interactive_multiflex(df,xparam,yparam,
+ groupby,
+ extratitle=extratitle,
+ ploterrorbars=ploterrorbars)
+
+
+
+ return render(request,'multiflex.html',
+ {'interactiveplot':script,
+ 'the_div':div,
+ 'chartform':chartform,
+ 'userid':userid,
+ 'teams':get_my_teams(request.user),
+ })
+ else:
+ return HttpResponse("Form is not valid")
+ elif request.method == 'POST' and 'ids' in request.session:
+ chartform = MultiFlexChoiceForm(request.POST)
+ if chartform.is_valid():
+ xparam = chartform.cleaned_data['xparam']
+ yparam = chartform.cleaned_data['yparam']
+ includereststrokes = chartform.cleaned_data['includereststrokes']
+ ploterrorbars = chartform.cleaned_data['ploterrorbars']
+ request.session['ploterrorbars'] = ploterrorbars
+ request.session['includereststrokes'] = includereststrokes
+ workstrokesonly = not includereststrokes
+
+ groupby = chartform.cleaned_data['groupby']
+ binsize = chartform.cleaned_data['binsize']
+
+ spmmin = chartform.cleaned_data['spmmin']
+ spmmax = chartform.cleaned_data['spmmax']
+ workmin = chartform.cleaned_data['workmin']
+ workmax = chartform.cleaned_data['workmax']
+
+ ids = request.session['ids']
+ request.session['ids'] = ids
+ workouts = dataprep.get_workouts(ids,userid)
+ if not workouts:
+ message = 'Error: Workouts in session storage do not belong to this user.'
+ messages.error(request,message)
+ url = reverse(user_multiflex_select,
+ kwargs={
+ 'userid':userid,
+ }
+ )
+ return HttpResponseRedirect(url)
+
+ # workouts = [Workout.objects.get(id=id) for id in ids]
+
+ labeldict = {
+ int(w.id): w.__unicode__() for w in workouts
+ }
+
+ fieldlist,fielddict = dataprep.getstatsfields()
+ fieldlist = [xparam,yparam,groupby,
+ 'workoutid','spm','driveenergy',
+ 'workoutstate']
+
+ # prepare data frame
+ datadf = dataprep.read_cols_df_sql(ids,fieldlist)
+
+
+ datadf = dataprep.clean_df_stats(datadf,workstrokesonly=workstrokesonly)
+
+ datadf = dataprep.filter_df(datadf,'spm',spmmin,
+ largerthan=True)
+ datadf = dataprep.filter_df(datadf,'spm',spmmax,
+ largerthan=False)
+ datadf = dataprep.filter_df(datadf,'driveenergy',workmin,
+ largerthan=True)
+ datadf = dataprep.filter_df(datadf,'driveneergy',workmax,
+ largerthan=False)
+
+ datadf.dropna(axis=0,how='any',inplace=True)
+
+
+ datemapping = {
+ w.id:w.date for w in workouts
+ }
+ datadf['date'] = datadf['workoutid']
+ datadf['date'].replace(datemapping,inplace=True)
+ today = datetime.date.today()
+ datadf['days ago'] = map(lambda x : x.days, datadf.date - today)
+
+ if groupby != 'date':
+ bins = np.arange(datadf[groupby].min()-binsize,
+ datadf[groupby].max()+binsize,
+ binsize)
+ groups = datadf.groupby(pd.cut(datadf[groupby],bins))
+ else:
+ bins = np.arange(datadf['days ago'].min()-binsize,
+ datadf['days ago'].max()+binsize,
+ binsize,
+ )
+ groups = datadf.groupby(pd.cut(datadf['days ago'], bins))
+
+
+
+
+ xvalues = groups.mean()[xparam]
+ yvalues = groups.mean()[yparam]
+ xerror = groups.std()[xparam]
+ yerror = groups.std()[yparam]
+
+ df = pd.DataFrame({
+ xparam:xvalues,
+ yparam:yvalues,
+ 'xerror':xerror,
+ 'yerror':yerror,
+ })
+
+ if groupby != 'date':
+ try:
+ df['groupval'] = groups.mean()[groupby],
+ except ValueError:
+ df['groupval'] = groups.mean()[groupby].fillna(value=0)
+ else:
+ df['groupval'] = [x.strftime("%Y-%m-%d") for x in groups.min()[groupby]]
+
+ if userid == 0:
+ extratitle = ''
+ else:
+ u = User.objects.get(id=userid)
+ extratitle = ' '+u.first_name+' '+u.last_name
+
+
+
+ script,div = interactive_multiflex(df,xparam,yparam,
+ groupby,
+ extratitle=extratitle,
+ ploterrorbars=ploterrorbars)
+
+
+
+ return render(request,'multiflex.html',
+ {'interactiveplot':script,
+ 'the_div':div,
+ 'chartform':chartform,
+ 'userid':userid,
+ 'teams':get_my_teams(request.user),
+ })
+ else:
+ return HttpResponse("invalid form")
+ else:
+ url = reverse(user_multiflex_select)
+ return HttpResponseRedirect(url)
+
# Box plots
@user_passes_test(ispromember,login_url="/",redirect_field_name=None)
def user_boxplot_select(request,
@@ -3564,7 +3916,7 @@ def boxplot_view(request,userid=0,
largerthan=False)
datadf.dropna(axis=0,how='any',inplace=True)
-
+
datadf['workoutid'].replace(datemapping,inplace=True)
datadf.rename(columns={"workoutid":"date"},inplace=True)
datadf = datadf.sort_values(['date'])