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 }} +
+
+
+
+
+ {% csrf_token %} + + {{ chartform.as_table }} +
+
+

+ +

+
+
+
+
+

+ 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 %} + +
+
+
+ + {% if theuser %} +
+ {% else %} + + {% endif %} + + + {{ dateform.as_table }} +
+ {% csrf_token %} +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ +
+ + +
+
+ + + {% if workouts %} + + Toggle All
+ + {{ form.as_table }} +
+{% else %} +

No workouts found

+{% endif %} +
+
+

Warning: You are on an experimental part of the site. Use at your own risk.

+

Select one or more workouts on the left, set your plot settings below, + and press submit"

+ {% csrf_token %} + + {{ chartform.as_table }} +
+
+

+ +

+
+
+

You can use the date and search forms above to search through all + workouts from this team.

+

TIP: Agree with your team members to put tags (e.g. '8x500m') in the notes section of + your workouts. That makes it easy to search.

+
+
+
+ + +{% 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'])