From f1888ab9fafef4c193cd1cef1f0d5fb04982fc00 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 28 Apr 2019 15:46:34 +0200 Subject: [PATCH 01/10] slightly working prototype only boxplot implemented, doesn't change y param --- rowers/forms.py | 34 +++ rowers/templates/user_analysis_select.html | 158 ++++++++++ rowers/urls.py | 5 + rowers/views/analysisviews.py | 337 ++++++++++++++++++++- rowers/views/statements.py | 1 + 5 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 rowers/templates/user_analysis_select.html diff --git a/rowers/forms.py b/rowers/forms.py index 30c6836b..760b536c 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): @@ -809,7 +820,30 @@ class PlannedSessionMultipleCloneForm(forms.Form): label='Planned Sessions' ) +analysischoices = ( + ('boxplot','Box Chart'), + ('trendflex','Trend Flex'), + ) + +class AnalysisChoiceForm(forms.Form): + function = forms.ChoiceField(choices=analysischoices,initial='boxplot', + label='Analysis') + 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 BoxPlotChoiceForm(forms.Form): yparam = forms.ChoiceField(choices=parchoices,initial='spm', label='Metric') diff --git a/rowers/templates/user_analysis_select.html b/rowers/templates/user_analysis_select.html new file mode 100644 index 00000000..8c35838d --- /dev/null +++ b/rowers/templates/user_analysis_select.html @@ -0,0 +1,158 @@ +{% 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/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..b1f4272e 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -5,6 +5,341 @@ from __future__ import unicode_literals from __future__ import unicode_literals, absolute_import from rowers.views.statements import * +# 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 + + 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': + 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: + 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, + 'form':form, + 'active':'nav-analysis', + 'chartform':chartform, + 'searchform':searchform, + 'optionsform':optionsform, + 'teams':get_my_teams(request.user), + }) + + +def boxplotdata(workouts,options): + try: + 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'] + except KeyError: + includereststrokes = False + spmmin = 15 + spmmax = 55 + workmin = 0 + workmax = 55 + ids = [] + userid = 0 + plotfield = 'spm' + function = 'boxplot' + + + 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) + 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", @@ -2769,7 +3104,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 ( From 5d18644f9ee72ab4edb560a171cb4c5bd6c687a4 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 28 Apr 2019 18:02:44 +0200 Subject: [PATCH 02/10] dynamic form for trendflex/boxplot combo --- rowers/dataprep.py | 6 +- rowers/forms.py | 95 +++++++++++++--------- rowers/templates/user_analysis_select.html | 72 ++++++++++++++++ rowers/views/analysisviews.py | 33 +++----- 4 files changed, 145 insertions(+), 61 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 897805b3..69bfe263 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -2174,13 +2174,17 @@ def dataprep(rowdatadf, id=0, bands=True, barchart=True, otwpower=True, drivespeed = drivespeed.fillna(value=0) try: - driveenergy = rowdatadf['driveenergy'] + print('driveenergy',rowdatadf['driveenergy'].mean()) except KeyError: + print('KeyError') if forceunit == 'lbs': driveenergy = drivelength * averageforce * lbstoN 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 760b536c..4e1d910c 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -820,45 +820,6 @@ class PlannedSessionMultipleCloneForm(forms.Form): label='Planned Sessions' ) -analysischoices = ( - ('boxplot','Box Chart'), - ('trendflex','Trend Flex'), - ) - -class AnalysisChoiceForm(forms.Form): - function = forms.ChoiceField(choices=analysischoices,initial='boxplot', - label='Analysis') - 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 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' @@ -876,6 +837,62 @@ from rowers.utils import palettes palettechoices = tuple((p,p) for p in palettes.keys()) +analysischoices = ( + ('boxplot','Box Chart'), + ('trendflex','Trend Flex'), + ) + +class AnalysisChoiceForm(forms.Form): + function = forms.ChoiceField(choices=analysischoices,initial='boxplot', + label='Analysis') + 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/templates/user_analysis_select.html b/rowers/templates/user_analysis_select.html index 8c35838d..c5ee4387 100644 --- a/rowers/templates/user_analysis_select.html +++ b/rowers/templates/user_analysis_select.html @@ -63,6 +63,78 @@ }); + + - +
+ + +
+
+ + +
+ diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index dcb5a610..f5cd3a77 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -24,11 +24,13 @@ def analysis_new(request,userid=0,function='boxplot'): 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: @@ -409,7 +411,23 @@ def trendflexdata(workouts, options,userid=0): script = ''.join(scripta) return(script,div) + +def histodata(workouts, options): + includereststrokes = options['includereststrokes'] + plotfield = options['plotfield'] + function = options['function'] + + workstrokesonly = not includereststrokes + + script, div = interactive_histoall(workouts,plotfield,includereststrokes) + + + scripta = script.split('\n')[2:-1] + script = ''.join(scripta) + + return(script,div) + def boxplotdata(workouts,options): @@ -516,6 +534,8 @@ def analysis_view_data(request,userid=0): script, div = boxplotdata(workouts,options) elif function == 'trendflex': script, div = trendflexdata(workouts, options,userid=userid) + elif function == 'histo': + script, div = histodata(workouts, options) else: script = '' div = 'Unknown analysis functions' From a670ee0884960a07be5eb6bdc64701c1747d0fca Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 29 Apr 2019 19:54:08 +0200 Subject: [PATCH 07/10] added spmmin and work filtering to histo --- rowers/dataprep.py | 2 +- rowers/interactiveplots.py | 10 +++++++- rowers/tests/testdata/testdata.csv.gz | Bin 12534 -> 12523 bytes rowers/views/analysisviews.py | 33 +++++++++++++++++--------- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 3e86d293..4cd30c5e 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -2175,7 +2175,7 @@ def dataprep(rowdatadf, id=0, bands=True, barchart=True, otwpower=True, drivespeed = drivespeed.fillna(value=0) try: - print('driveenergy',rowdatadf['driveenergy'].mean()) + driveenergy = rowdatadf['driveenergy'] except KeyError: if forceunit == 'lbs': driveenergy = drivelength * averageforce * lbstoN diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 64a18653..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,6 +1226,12 @@ 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" diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 49e8b984154d1ce30ccdbced1e06e0645411f776..93a865ec40b792573b032b75dee9993606effb20 100644 GIT binary patch literal 12523 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 Date: Mon, 29 Apr 2019 20:34:59 +0200 Subject: [PATCH 08/10] added flexall --- rowers/forms.py | 31 ++++++++++- rowers/templates/user_analysis_select.html | 63 ++++++++++++++++++++-- rowers/views/analysisviews.py | 25 +++++++++ 3 files changed, 114 insertions(+), 5 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index 173bf4b7..8746e331 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -841,11 +841,39 @@ analysischoices = ( ('boxplot','Box Chart'), ('trendflex','Trend Flex'), ('histo','Histogram'), + ('flexall','Cumulative Flex Chart') ) - + + + 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, @@ -854,6 +882,7 @@ class AnalysisChoiceForm(forms.Form): 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') diff --git a/rowers/templates/user_analysis_select.html b/rowers/templates/user_analysis_select.html index 6b8a8e95..266648c1 100644 --- a/rowers/templates/user_analysis_select.html +++ b/rowers/templates/user_analysis_select.html @@ -72,12 +72,20 @@ var plotfield = $("#id_plotfield").parent().parent(); var x_param = $("#id_xparam").parent().parent(); var y_param = $("#id_yparam").parent().parent(); + var groupby = $("#id_groupby").parent().parent(); var binsize = $("#id_binsize").parent().parent(); var errorbars = $("#id_ploterrorbars").parent().parent(); var palette = $("#id_palette").parent().parent(); + var spmmin = $("#id_spmmin").parent().parent(); + var spmmax = $("#id_spmmax").parent().parent(); + var workmin = $("#id_workmin").parent().parent(); + var workmax = $("#id_workmax").parent().parent(); - + var xaxis = $("#id_xaxis").parent().parent(); + var yaxis1 = $("#id_yaxis1").parent().parent(); + var yaxis2 = $("#id_yaxis2").parent().parent(); + // Hide the fields. // Use JS to do this in case the user doesn't have JS // enabled. @@ -88,6 +96,9 @@ errorbars.hide(); palette.hide(); binsize.hide(); + xaxis.hide(); + yaxis1.hide(); + yaxis2.hide(); if (functionfield.val() == 'boxplot') { plotfield.show(); @@ -95,7 +106,7 @@ if (functionfield.val() == 'histo') { plotfield.show() - } + }; if (functionfield.val() == 'trendflex') { x_param.show(); @@ -106,6 +117,12 @@ errorbars.show(); }; + if (functionfield.val() == 'flexall') { + xaxis.show(); + yaxis1.show(); + yaxis2.show(); + } + // Setup an event listener for when the state of the // checkbox changes. @@ -117,21 +134,36 @@ if (Value=='boxplot') { // Show the hidden fields. plotfield.show(); + spmmin.show(); + spmmax.show(); + workmin.show(); + workmax.show(); x_param.hide(); y_param.hide(); groupby.hide(); palette.hide(); binsize.hide(); errorbars.hide(); + xaxis.hide(); + yaxis1.hide(); + yaxis2.hide(); } else if (Value=='histo') { plotfield.show(); + spmmin.show(); + spmmax.show(); + workmin.show(); + workmax.show(); x_param.hide(); y_param.hide(); groupby.hide(); palette.hide(); binsize.hide(); errorbars.hide(); + xaxis.hide(); + yaxis1.hide(); + yaxis2.hide(); + } else if (Value=='trendflex') { x_param.show(); @@ -140,12 +172,35 @@ palette.show(); binsize.show(); errorbars.show(); + spmmin.show(); + spmmax.show(); + workmin.show(); + workmax.show(); plotfield.hide(); + xaxis.hide(); + yaxis1.hide(); + yaxis2.hide(); + + } + else if (Value=='flexall') { + xaxis.show(); + yaxis1.show(); + yaxis2.show(); + x_param.hide(); + y_param.hide(); + groupby.hide(); + spmmin.hide(); + spmmax.hide(); + workmin.hide(); + workmax.hide(); + plotfield.hide(); + palette.hide(); + binsize.hide(); + errorbars.hide(); } }); + }); - - });
diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 6a753d5d..00279f6a 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -415,6 +415,29 @@ def trendflexdata(workouts, options,userid=0): 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'] @@ -545,6 +568,8 @@ def analysis_view_data(request,userid=0): script, div = trendflexdata(workouts, options,userid=userid) elif function == 'histo': script, div = histodata(workouts, options) + elif function == 'flexall': + script,div = flexalldata(workouts,options) else: script = '' div = 'Unknown analysis functions' From 6b830873a5536cbf352ce7670b9897636fbf84dd Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 29 Apr 2019 21:48:50 +0200 Subject: [PATCH 09/10] added stats --- rowers/forms.py | 3 +- rowers/templates/statsdiv.html | 76 ++++++++++++++++++++ rowers/templates/user_analysis_select.html | 18 ++++- rowers/views/analysisviews.py | 83 +++++++++++++++++++++- 4 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 rowers/templates/statsdiv.html diff --git a/rowers/forms.py b/rowers/forms.py index 8746e331..e03a4cbe 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -841,7 +841,8 @@ analysischoices = ( ('boxplot','Box Chart'), ('trendflex','Trend Flex'), ('histo','Histogram'), - ('flexall','Cumulative Flex Chart') + ('flexall','Cumulative Flex Chart'), + ('stats','Statistics'), ) 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 index 266648c1..58a80876 100644 --- a/rowers/templates/user_analysis_select.html +++ b/rowers/templates/user_analysis_select.html @@ -121,7 +121,11 @@ xaxis.show(); yaxis1.show(); yaxis2.show(); - } + } + + if (functionfield.val() == 'stats') { + plotfield.hide(); + } // Setup an event listener for when the state of the @@ -198,6 +202,18 @@ binsize.hide(); errorbars.hide(); } + else if (Value=='stats') { + xaxis.hide(); + yaxis1.hide(); + yaxis2.hide(); + x_param.hide(); + y_param.hide(); + groupby.hide(); + plotfield.hide(); + palette.hide(); + binsize.hide(); + errorbars.hide(); + } }); }); diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 00279f6a..ff1fe142 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -5,6 +5,19 @@ 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 = { @@ -458,11 +471,77 @@ def histodata(workouts, options): 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'] @@ -570,6 +649,8 @@ def analysis_view_data(request,userid=0): 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' From 0d537c5e982ce3e41c8b9670fc4a640dae7ba275 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 29 Apr 2019 22:19:08 +0200 Subject: [PATCH 10/10] first test version of analysis under lab --- rowers/templates/laboratory.html | 1 + 1 file changed, 1 insertion(+) 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 %}