diff --git a/requirements.txt b/requirements.txt index c8108737..d7f1e8b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -157,7 +157,7 @@ ratelim==0.1.6 redis==3.2.1 requests==2.21.0 requests-oauthlib==1.2.0 -rowingdata==2.2.7 +rowingdata==2.3.1 rowingphysics==0.5.0 rq==1.0 rq-dashboard==0.4.0 diff --git a/rowers/dataprep.py b/rowers/dataprep.py index d711c7f7..4cd30c5e 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -2182,6 +2182,9 @@ def dataprep(rowdatadf, id=0, bands=True, barchart=True, otwpower=True, else: drivenergy = drivelength * averageforce + if driveenergy.mean() == 0 and driveenergy.std() == 0: + driveenergy = 0*driveenergy+100 + distance = rowdatadf.loc[:, 'cum_dist'] velo = 500. / p diff --git a/rowers/forms.py b/rowers/forms.py index 30c6836b..e03a4cbe 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -721,6 +721,17 @@ class HistoForm(forms.Form): histoparam = forms.ChoiceField(choices=parchoices,initial='power', label='Metric') +class AnalysisOptionsForm(forms.Form): + modality = forms.ChoiceField(choices=workouttypes, + label='Workout Type', + initial='all') + waterboattype = forms.MultipleChoiceField(choices=boattypes, + label='Water Boat Type', + initial = mytypes.waterboattype) + rankingonly = forms.BooleanField(initial=False, + label='Only Ranking Pieces', + required=False) + # form to select modality and boat type for trend flex class TrendFlexModalForm(forms.Form): @@ -810,22 +821,6 @@ class PlannedSessionMultipleCloneForm(forms.Form): ) -class BoxPlotChoiceForm(forms.Form): - yparam = forms.ChoiceField(choices=parchoices,initial='spm', - label='Metric') - 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') - - includereststrokes = forms.BooleanField(initial=False, - required=False, - label='Include Rest Strokes') - grouplabels = axlabels.copy() grouplabels['date'] = 'Date' grouplabels['workoutid'] = 'Workout' @@ -842,6 +837,93 @@ from rowers.utils import palettes palettechoices = tuple((p,p) for p in palettes.keys()) +analysischoices = ( + ('boxplot','Box Chart'), + ('trendflex','Trend Flex'), + ('histo','Histogram'), + ('flexall','Cumulative Flex Chart'), + ('stats','Statistics'), + ) + + + +class AnalysisChoiceForm(forms.Form): + axchoices = list( + (ax[0],ax[1]) for ax in axes if ax[0] not in ['cumdist','None'] + ) + axchoices = dict((x,y) for x,y in axchoices) + axchoices = list(sorted(axchoices.items(), key = lambda x:x[1])) + + + yaxchoices = list((ax[0],ax[1]) for ax in axes if ax[0] not in ['cumdist','distance','time']) + yaxchoices = dict((x,y) for x,y in yaxchoices) + yaxchoices = list(sorted(yaxchoices.items(), key = lambda x:x[1])) + + + yaxchoices2 = list( + (ax[0],ax[1]) for ax in axes if ax[0] not in ['cumdist','distance','time'] + ) + yaxchoices2 = dict((x,y) for x,y in yaxchoices2) + yaxchoices2 = list(sorted(yaxchoices2.items(), key = lambda x:x[1])) + + function = forms.ChoiceField(choices=analysischoices,initial='boxplot', + label='Analysis') + xaxis = forms.ChoiceField( + choices=axchoices,label='X-Axis',required=True,initial='spm') + yaxis1 = forms.ChoiceField( + choices=yaxchoices,label='Left Axis',required=True,initial='power') + yaxis2 = forms.ChoiceField( + choices=yaxchoices2,label='Right Axis',required=True,initial='None') + + plotfield = forms.ChoiceField(choices=parchoices,initial='spm', + label='Metric') + xparam = forms.ChoiceField(choices=parchoicesmultiflex, + initial='hr', + label='X axis') + yparam = forms.ChoiceField(choices=parchoicesmultiflex, + initial='pace', + label='Y axis') + + groupby = forms.ChoiceField(choices=groupchoices,initial='spm', + label='Group By') + binsize = forms.FloatField(initial=1,required=False,label = 'Bin Size') + + ploterrorbars = forms.BooleanField(initial=False, + required=False, + label='Plot Error Bars') + + palette = forms.ChoiceField(choices=palettechoices, + label = 'Color Scheme', + initial='monochrome_blue') + + 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') + + includereststrokes = forms.BooleanField(initial=False, + required=False, + label='Include Rest Strokes') + +class BoxPlotChoiceForm(forms.Form): + yparam = forms.ChoiceField(choices=parchoices,initial='spm', + label='Metric') + 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') + + includereststrokes = forms.BooleanField(initial=False, + required=False, + label='Include Rest Strokes') class MultiFlexChoiceForm(forms.Form): xparam = forms.ChoiceField(choices=parchoicesmultiflex, diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 01146381..a75bee66 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1214,7 +1214,9 @@ def fitnessmetric_chart(fitnessmetrics,user,workoutmode='rower',startdate=None, return [script,div] -def interactive_histoall(theworkouts,histoparam,includereststrokes): +def interactive_histoall(theworkouts,histoparam,includereststrokes, + spmmin=0,spmmax=55, + workmin=0,workmax=1500): TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' ids = [int(w.id) for w in theworkouts] @@ -1224,15 +1226,21 @@ def interactive_histoall(theworkouts,histoparam,includereststrokes): rowdata.dropna(axis=0,how='any',inplace=True) + rowdata = dataprep.filter_df(rowdata,'spm',spmmin,largerthan=True) + rowdata = dataprep.filter_df(rowdata,'spm',spmmax,largerthan=False) + + rowdata = dataprep.filter_df(rowdata,'driveenergy',workmin,largerthan=True) + rowdata = dataprep.filter_df(rowdata,'driveenergy',workmax,largerthan=False) + if rowdata.empty: - return "","No Valid Data Available","","" + return "","No Valid Data Available" try: histopwr = rowdata[histoparam].values except KeyError: - return "","No data","","" + return "","No data" if len(histopwr) == 0: - return "","No valid data available","","" + return "","No valid data available" # throw out nans histopwr = histopwr[~np.isinf(histopwr)] @@ -1341,7 +1349,6 @@ def interactive_histoall(theworkouts,histoparam,includereststrokes): script = '' div = '' - return [script,div] def course_map(course): diff --git a/rowers/templates/laboratory.html b/rowers/templates/laboratory.html index c8706a51..bc9021f7 100644 --- a/rowers/templates/laboratory.html +++ b/rowers/templates/laboratory.html @@ -11,6 +11,7 @@

Rower: {{ rower.user.first_name }}

+Be adventurous and try our new Analysis page {% endblock %} diff --git a/rowers/templates/statsdiv.html b/rowers/templates/statsdiv.html new file mode 100644 index 00000000..a4072f22 --- /dev/null +++ b/rowers/templates/statsdiv.html @@ -0,0 +1,76 @@ + +{% if stats %} +

Statistics

+ + + + + + + + + + + + + + + {% for key, value in stats.items() %} + + + + + + + + + + + {% endfor %} + +
MetricMeanMinimum25%Median75%MaximumStandard Deviation
{{ value.verbosename }}{{ value.mean|floatformat }}{{ value.min|floatformat }}{{ value.firstq|floatformat }}{{ value.median|floatformat }}{{ value.thirdq|floatformat }}{{ value.max|floatformat }}{{ value.std|floatformat }}
+ +{% 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. +

+ + + + + {% for key,value in cordict.items() %} + + {% endfor %} + + + + {% for key, thedict in cordict.items() %} + + + {% for key2,value in thedict.items() %} + + {% endfor %} + + {% endfor %} + +
 
{{ key }}
{{ key }} + {% if value > 0.5 %} +
{{ value|floatformat }}
+ {% elif value > 0.1 %} +
{{ value|floatformat }}
+ {% elif value < -0.5 %} +
{{ value|floatformat }}
+ {% elif value < -0.1 %} +
{{ value|floatformat }}
+ {% else %} +   + {% endif %} +
+ +{% endif %} + + + + diff --git a/rowers/templates/user_analysis_select.html b/rowers/templates/user_analysis_select.html new file mode 100644 index 00000000..58a80876 --- /dev/null +++ b/rowers/templates/user_analysis_select.html @@ -0,0 +1,322 @@ +{% extends "newbase.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block main %} + + + + + + +
+ + +
+
+ + +
+ + + +
+
+ +
    +
  • +
    + {{ the_div|safe }} +
    +
  • +
  • +

    You can use the date and search forms 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.

    +
  • +
  • +
    + {{ searchform }} + +
    +
    + + {% if workouts %} + + Toggle All
    + + {{ form.as_table }} +
    + {% else %} +

    No workouts found

    + {% endif %} +
  • +
  • +

    Select two or more workouts, set your plot settings below, + and press submit +

    + {% csrf_token %} + + {{ chartform.as_table }} +
    +
  • +
  • + + + {{ dateform.as_table }} +
    + + {{ optionsform.as_table }} +
    + {% csrf_token %} + + +
  • +
+ + +{% endblock %} + +{% block scripts %} +{% if request.method == 'POST' %} + + + + +{% endif %} + +{% endblock %} + +{% block sidebar %} +{% include 'menu_analytics.html' %} +{% endblock %} diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 49e8b984..93a865ec 100644 Binary files a/rowers/tests/testdata/testdata.csv.gz and b/rowers/tests/testdata/testdata.csv.gz differ diff --git a/rowers/urls.py b/rowers/urls.py index f4d1ace5..22847ffa 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -223,6 +223,10 @@ urlpatterns = [ re_path(r'^workouts-join-select/user/(?P\d+)/$',views.workouts_join_select,name='workouts_join_select'), re_path(r'^user-boxplot-select/user/(?P\d+)/$',views.user_boxplot_select,name='user_boxplot_select'), re_path(r'^user-boxplot-select/$',views.user_boxplot_select,name='user_boxplot_select'), + re_path(r'^user-analysis-select/(?P\w.*)/user/(?P\d+)/$',views.analysis_new,name='analysis_new'), + re_path(r'^user-analysis-select/(?P\w.*)/$',views.analysis_new,name='analysis_new'), + re_path(r'^user-analysis-select/user/(?P\d+)/$',views.analysis_new,name='analysis_new'), + re_path(r'^user-analysis-select/$',views.analysis_new,name='analysis_new'), # re_path(r'^user-multiflex-select/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.user_multiflex_select,name='user_multiflex_select'), re_path(r'^user-multiflex-select/user/(?P\d+)/$',views.user_multiflex_select,name='user_multiflex_select'), # re_path(r'^user-multiflex-select/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.user_multiflex_select,name='user_multiflex_select'), @@ -259,6 +263,7 @@ urlpatterns = [ # re_path(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.cum_flex,name='cum_flex'), # re_path(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.cum_flex,name='cum_flex'), re_path(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/$',views.cum_flex,name='cum_flex'), + re_path(r'^analysisdata/$',views.analysis_view_data,name='analysis_view_data'), re_path(r'^flexall/user/(?P\d+)/$',views.cum_flex,name='cum_flex'), re_path(r'^flexall/$',views.cum_flex,name='cum_flex'), re_path(r'^flexalldata/$',views.cum_flex_data,name='cum_flex_data'), diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 7a44188d..ff1fe142 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -5,6 +5,663 @@ from __future__ import unicode_literals from __future__ import unicode_literals, absolute_import from rowers.views.statements import * +from jinja2 import Template,Environment,FileSystemLoader + +def floatformat(x,prec=2): + return '{x}'.format(x=round(x,prec)) + + +env = Environment(loader = FileSystemLoader(["rowers/templates"])) +env.filters['floatformat'] = floatformat + + +from django.contrib.staticfiles import finders + + +# generic Analysis view - + +defaultoptions = { + 'includereststrokes': False, + 'workouttypes':['rower','dynamic','slides'], + 'waterboattype': mytypes.waterboattype, + 'rankingonly': False, + 'function':'boxplot' +} + + +@user_passes_test(ispromember, login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def analysis_new(request,userid=0,function='boxplot'): + r = getrequestrower(request, userid=userid) + user = r.user + userid = user.id + + + if 'options' in request.session: + options = request.session['options'] + else: + options=defaultoptions + + options['userid'] = userid + try: + workouttypes = options['workouttypes'] + except KeyError: + workouttypes = ['rower','dynamic','slides'] + + try: + rankingonly = options['rankingonly'] + except KeyError: + rankingonly = False + + try: + includereststrokes = options['includereststrokes'] + except KeyError: + includereststrokes = False + + if 'startdate' in request.session: + startdate = iso8601.parse_date(request.session['startdate']) + + + if 'enddate' in request.session: + enddate = iso8601.parse_date(request.session['enddate']) + + workstrokesonly = not includereststrokes + + waterboattype = mytypes.waterboattype + + if request.method == 'POST': + thediv = get_call() + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + optionsform = AnalysisOptionsForm(request.POST) + if optionsform.is_valid(): + for key, value in optionsform.cleaned_data.items(): + options[key] = value + + modality = optionsform.cleaned_data['modality'] + waterboattype = optionsform.cleaned_data['waterboattype'] + if modality == 'all': + modalities = [m[0] for m in mytypes.workouttypes] + else: + modalities = [modality] + if modality != 'water': + waterboattype = [b[0] for b in mytypes.boattypes] + + + if 'rankingonly' in optionsform.cleaned_data: + rankingonly = optionsform.cleaned_data['rankingonly'] + else: + rankingonly = False + + options['modalities'] = modalities + options['waterboattype'] = waterboattype + + chartform = AnalysisChoiceForm(request.POST) + if chartform.is_valid(): + for key, value in chartform.cleaned_data.items(): + options[key] = value + + + form = WorkoutMultipleCompareForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + selectedworkouts = cd['workouts'] + ids = [int(w.id) for w in selectedworkouts] + options['ids'] = ids + else: + ids = [] + options['ids'] = ids + else: + thediv = '' + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + if 'modalities' in request.session: + modalities = request.session['modalities'] + if len(modalities) > 1: + modality = 'all' + else: + modality = modalities[0] + else: + modalities = [m[0] for m in mytypes.workouttypes] + modality = 'all' + + + + + negtypes = [] + for b in mytypes.boattypes: + if b[0] not in waterboattype: + negtypes.append(b[0]) + + + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + negtypes = [] + for b in mytypes.boattypes: + if b[0] not in waterboattype: + negtypes.append(b[0]) + + + workouts = Workout.objects.filter(user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate, + workouttype__in=modalities, + ).order_by( + "-date", "-starttime" + ).exclude(boattype__in=negtypes) + if rankingonly: + workouts = workouts.exclude(rankingpiece=False) + + 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)) + ) + searchform = SearchForm(initial={'q':query}) + else: + searchform = SearchForm() + + if request.method != 'POST': + form = WorkoutMultipleCompareForm() + chartform = AnalysisChoiceForm() + selectedworkouts = Workout.objects.none() + else: + selectedworkouts = Workout.objects.filter(id__in=ids) + + form.fields["workouts"].queryset = workouts | selectedworkouts + + + optionsform = AnalysisOptionsForm(initial={ + 'modality':modality, + 'waterboattype':waterboattype, + 'rankingonly':rankingonly, + }) + + + + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + request.session['options'] = options + + + breadcrumbs = [ + { + 'url':'/rowers/analysis', + 'name':'Analysis' + }, + { + 'url':reverse('analysis_new',kwargs={'userid':userid}), + 'name': 'Analysis Select' + }, + ] + return render(request, 'user_analysis_select.html', + {'workouts': workouts, + 'dateform':dateform, + 'startdate':startdate, + 'enddate':enddate, + 'rower':r, + 'breadcrumbs':breadcrumbs, + 'theuser':user, + 'the_div':thediv, + 'form':form, + 'active':'nav-analysis', + 'chartform':chartform, + 'searchform':searchform, + 'optionsform':optionsform, + 'teams':get_my_teams(request.user), + }) + +def trendflexdata(workouts, options,userid=0): + + includereststrokes = options['includereststrokes'] + palette = options['palette'] + groupby = options['groupby'] + binsize = options['binsize'] + xparam = options['xparam'] + yparam = options['yparam'] + spmmin = options['spmmin'] + spmmax = options['spmmax'] + workmin = options['workmin'] + workmax = options['workmax'] + ploterrorbars = options['ploterrorbars'] + ids = options['ids'] + workstrokesonly = not includereststrokes + + labeldict = { + int(w.id): w.__str__() for w in workouts + } + + fieldlist,fielddict = dataprep.getstatsfields() + fieldlist = [xparam,yparam,groupby, + 'workoutid','spm','driveenergy', + 'workoutstate'] + + # prepare data frame + datadf,extracols = dataprep.read_cols_df_sql(ids,fieldlist) + + if xparam == groupby: + datadf['groupby'] = datadf[xparam] + groupy = 'groupby' + + 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': + try: + bins = np.arange(datadf[groupby].min()-binsize, + datadf[groupby].max()+binsize, + binsize) + groups = datadf.groupby(pd.cut(datadf[groupby],bins,labels=False)) + except ValueError: + messages.error( + request, + "Unable to compete. Probably not enough data selected" + ) + url = reverse(user_multiflex_select) + return HttpResponseRedirect(url) + else: + bins = np.arange(datadf['days ago'].min()-binsize, + datadf['days ago'].max()+binsize, + binsize, + ) + groups = datadf.groupby(pd.cut(datadf['days ago'], bins, + labels=False)) + + + xvalues = groups.mean()[xparam] + yvalues = groups.mean()[yparam] + xerror = groups.std()[xparam] + yerror = groups.std()[yparam] + groupsize = groups.count()[xparam] + + mask = groupsize <= min([0.01*groupsize.sum(),0.2*groupsize.mean()]) + xvalues.loc[mask] = np.nan + + yvalues.loc[mask] = np.nan + xerror.loc[mask] = np.nan + yerror.loc[mask] = np.nan + groupsize.loc[mask] = np.nan + + xvalues.dropna(inplace=True) + yvalues.dropna(inplace=True) + xerror.dropna(inplace=True) + yerror.dropna(inplace=True) + groupsize.dropna(inplace=True) + + if len(groupsize) == 0: + messages.error(request,'No data in selection') + url = reverse(user_multiflex_select) + return HttpResponseRedirect(url) + else: + groupsize = 30.*np.sqrt(groupsize/float(groupsize.max())) + + df = pd.DataFrame({ + xparam:xvalues, + yparam:yvalues, + 'x':xvalues, + 'y':yvalues, + 'xerror':xerror, + 'yerror':yerror, + 'groupsize':groupsize, + }) + + + if yparam == 'pace': + df['y'] = dataprep.paceformatsecs(df['y']/1.0e3) + + aantal = len(df) + + if groupby != 'date': + try: + df['groupval'] = groups.mean()[groupby] + df['groupval'].loc[mask] = np.nan + + groupcols = df['groupval'] + except ValueError: + df['groupval'] = groups.mean()[groupby].fillna(value=0) + df['groupval'].loc[mask] = np.nan + groupcols = df['groupval'] + except KeyError: + messages.error(request,'Data selection error') + url = reverse(user_multiflex_select) + return HttpResponseRedirect(url) + else: + try: + dates = groups.min()[groupby] + dates.loc[mask] = np.nan + dates.dropna(inplace=True) + df['groupval'] = [x.strftime("%Y-%m-%d") for x in dates] + df['groupval'].loc[mask] = np.nan + groupcols = 100.*np.arange(aantal)/float(aantal) + except AttributeError: + df['groupval'] = groups.mean()['days ago'].fillna(value=0) + groupcols = 100.*np.arange(aantal)/float(aantal) + + + groupcols = (groupcols-groupcols.min())/(groupcols.max()-groupcols.min()) + + if aantal == 1: + groupcols = np.array([1.]) + + + colors = range_to_color_hex(groupcols,palette=palette) + + df['color'] = colors + + clegendx = np.arange(0,1.2,.2) + legcolors = range_to_color_hex(clegendx,palette=palette) + if groupby != 'date': + clegendy = df['groupval'].min()+clegendx*(df['groupval'].max()-df['groupval'].min()) + else: + clegendy = df.index.min()+clegendx*(df.index.max()-df.index.min()) + + + + colorlegend = zip(range(6),clegendy,legcolors) + + + 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, + binsize=binsize, + colorlegend=colorlegend, + spmmin=spmmin,spmmax=spmmax, + workmin=workmin,workmax=workmax) + + scripta= script.split('\n')[2:-1] + script = ''.join(scripta) + + return(script,div) + +def flexalldata(workouts, options): + includereststrokes = options['includereststrokes'] + xparam = options['xaxis'] + yparam1 = options['yaxis1'] + yparam2 = options['yaxis2'] + promember=True + + workstrokesonly = not includereststrokes + + res = interactive_cum_flex_chart2(workouts, xparam=xparam, + yparam1=yparam1, + yparam2=yparam2, + promember=promember, + workstrokesonly=workstrokesonly, + ) + script = res[0] + div = res[1] + + scripta = script.split('\n')[2:-1] + script = ''.join(scripta) + + return(script,div) + +def histodata(workouts, options): + includereststrokes = options['includereststrokes'] + plotfield = options['plotfield'] + function = options['function'] + spmmin = options['spmmin'] + spmmax = options['spmmax'] + workmin = options['workmin'] + workmax = options['workmax'] + + + workstrokesonly = not includereststrokes + + script, div = interactive_histoall(workouts,plotfield,includereststrokes, + spmmin=spmmin,spmmax=spmmax,workmin=workmin,workmax=workmax) + + + scripta = script.split('\n')[2:-1] + script = ''.join(scripta) + + return(script,div) + +def statsdata(workouts, options): + includereststrokes = options['includereststrokes'] + spmmin = options['spmmin'] + spmmax = options['spmmax'] + workmin = options['workmin'] + workmax = options['workmax'] + ids = options['ids'] + userid = options['userid'] + plotfield = options['plotfield'] + function = options['function'] + + workstrokesonly = not includereststrokes + + ids = [w.id for w in workouts] + + datamapping = { + w.id:w.date for w in workouts + } + + fieldlist,fielddict = dataprep.getstatsfields() + + # prepare data frame + datadf,extracols = dataprep.read_cols_df_sql(ids,fieldlist) + + datadf = dataprep.clean_df_stats(datadf,workstrokesonly=workstrokesonly) + + # Create stats + stats = {} + fielddict.pop('workoutstate') + fielddict.pop('workoutid') + + for field,verbosename in fielddict.items(): + thedict = { + 'mean':datadf[field].mean(), + 'min': datadf[field].min(), + 'std': datadf[field].std(), + 'max': datadf[field].max(), + 'median': datadf[field].median(), + 'firstq':datadf[field].quantile(q=0.25), + 'thirdq':datadf[field].quantile(q=0.75), + 'verbosename':verbosename, + } + stats[field] = thedict + + # Create a dict with correlation values + cor = datadf.corr(method='spearman') + cor.fillna(value=0,inplace=True) + cordict = {} + for field1,verbosename in fielddict.items(): + thedict = {} + for field2,verbosename in fielddict.items(): + try: + thedict[field2] = cor.loc[field1,field2] + except KeyError: + thedict[field2] = 0 + + cordict[field1] = thedict + + context = { + 'stats':stats, + 'cordict':cordict, + } + + htmly = env.get_template('statsdiv.html') + html_content = htmly.render(context) + + return('',html_content) + +def boxplotdata(workouts,options): + + includereststrokes = options['includereststrokes'] + spmmin = options['spmmin'] + spmmax = options['spmmax'] + workmin = options['workmin'] + workmax = options['workmax'] + ids = options['ids'] + userid = options['userid'] + plotfield = options['plotfield'] + function = options['function'] + + workstrokesonly = not includereststrokes + labeldict = { + int(w.id): w.__str__() for w in workouts + } + + + datemapping = { + w.id:w.date for w in workouts + } + + + + fieldlist,fielddict = dataprep.getstatsfields() + fieldlist = [plotfield,'workoutid','spm','driveenergy', + 'workoutstate'] + + ids = [w.id for w in workouts] + + # prepare data frame + datadf,extracols = 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) + + + datadf['workoutid'].replace(datemapping,inplace=True) + datadf.rename(columns={"workoutid":"date"},inplace=True) + datadf = datadf.sort_values(['date']) + + if userid == 0: + extratitle = '' + else: + u = User.objects.get(id=userid) + extratitle = ' '+u.first_name+' '+u.last_name + + + + script,div = interactive_boxchart(datadf,plotfield, + extratitle=extratitle, + spmmin=spmmin,spmmax=spmmax,workmin=workmin,workmax=workmax) + + scripta = script.split('\n')[2:-1] + script = ''.join(scripta) + + return(script,div) + +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def analysis_view_data(request,userid=0): + + if 'options' in request.session: + options = request.session['options'] + else: + options = defaultoptions + + + if userid==0: + userid = request.user.id + + workouts = [] + + ids = options['ids'] + function = options['function'] + + if not ids: + return JSONResponse({ + "script":'', + "div":'No data found' + }) + + for id in ids: + try: + workouts.append(Workout.objects.get(id=id)) + except Workout.DoesNotExist: + pass + + if function == 'boxplot': + script, div = boxplotdata(workouts,options) + elif function == 'trendflex': + script, div = trendflexdata(workouts, options,userid=userid) + elif function == 'histo': + script, div = histodata(workouts, options) + elif function == 'flexall': + script,div = flexalldata(workouts,options) + elif function == 'stats': + script,div = statsdata(workouts,options) + else: + script = '' + div = 'Unknown analysis functions' + + + return JSONResponse({ + "script":script, + "div":div, + }) + + # Histogram for a date/time range @user_passes_test(ispromember,login_url="/rowers/paidplans", message="This functionality requires a Pro plan or higher", @@ -2412,6 +3069,8 @@ def multiflex_data(request,userid=0, 'ploterrorbars':False, }): + def_options = options + if 'options' in request.session: options = request.session['options'] @@ -2436,16 +3095,16 @@ def multiflex_data(request,userid=0, userid = request.user.id - palette = options['palette'] - groupby = options['groupby'] - binsize = options['binsize'] - xparam = options['xparam'] - yparam = options['yparam'] - spmmin = options['spmmin'] - spmmax = options['spmmax'] - workmin = options['workmin'] - workmax = options['workmax'] - ids = options['ids'] + palette = keyvalue_get_default('palette',options, def_options) + groupby = keyvalue_get_default('groupby',options, def_options) + binsize = keyvalue_get_default('binsize',options, def_options) + xparam = keyvalue_get_default('xparam',options, def_options) + yparam = keyvalue_get_default('yparam',options, def_options) + spmmin = keyvalue_get_default('spmmin',options, def_options) + spmmax = keyvalue_get_default('spmmax',options, def_options) + workmin = keyvalue_get_default('workmin',options, def_options) + workmax = keyvalue_get_default('workmax',options, def_options) + ids = keyvalue_get_default('ids',options, def_options) workouts = [] @@ -2769,7 +3428,7 @@ def multiflex_view(request,userid=0, options['spmmax'] = spmmax options['workmin'] = workmin options['workmax'] = workmax - options['ids'] = ids + options['idso'] = ids request.session['options'] = options diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 40087500..420b560c 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -80,6 +80,7 @@ from rowers.forms import ( UpdateStreamForm,WorkoutMultipleCompareForm,ChartParamChoiceForm, FusionMetricChoiceForm,BoxPlotChoiceForm,MultiFlexChoiceForm, TrendFlexModalForm,WorkoutSplitForm,WorkoutJoinParamForm, + AnalysisOptionsForm, AnalysisChoiceForm, PlannedSessionMultipleCloneForm,SessionDateShiftForm, ) from rowers.models import ( diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index d5cdc6f3..6a74633e 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -1798,11 +1798,12 @@ def workout_downloadwind_view(request,id=0, windspeed = winddata[0] windbearing = winddata[1] message = winddata[2] - try: - row.notes += "\n"+message - except TypeError: - if message and row.notes: - row.notes += message + if message is not None: + try: + row.notes += "\n"+message + except TypeError: + if message is not None and row.notes is not None: + row.notes += message row.save() rowdata.add_wind(windspeed,windbearing)