diff --git a/rowers/forms.py b/rowers/forms.py index d9c7da0c..dba009c4 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -1518,6 +1518,7 @@ class VirtualRaceSelectForm(forms.Form): choices = get_countries(),initial='All' ) + class FlexOptionsForm(forms.Form): includereststrokes = forms.BooleanField(initial=True, required = False, label='Include Rest Strokes') @@ -1540,24 +1541,80 @@ class ForceCurveOptionsForm(forms.Form): label='Individual Stroke Chart Type') +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])) + +class StravaChartForm(forms.Form): + xaxischoices = ( + ('cumdist','Distance'), + ('time','Time') + ) + + xaxis = forms.ChoiceField( + choices = xaxischoices,label='X-Axis',required=True) + + + yaxis1 = forms.ChoiceField( + choices=yaxchoices,label='First Chart',required=True) + yaxis2 = forms.ChoiceField( + choices=yaxchoices2,label='Second Chart',required=True) + + yaxis3 = forms.ChoiceField( + choices=yaxchoices,label='Third Chart',required=True) + yaxis4 = forms.ChoiceField( + choices=yaxchoices2,label='Fourth Chart',required=True) + + def __init__(self,request,*args,**kwargs): + extrametrics = kwargs.pop('extrametrics',[]) + super(StravaChartForm, self).__init__(*args, **kwargs) + + + rower = Rower.objects.get(user=request.user) + + axchoicespro = ( + ('',ax[1]) if ax[4] == 'pro' and ax[0] else (ax[0],ax[1]) for ax in axes + ) + + axchoicesbasicx = [] + axchoicesbasicy = [] + + for ax in axes: + if ax[4] != 'pro' and ax[0] != 'cumdist': + if ax[0] != 'None': + axchoicesbasicx.insert(0,(ax[0],ax[1])) + if ax[0] not in ['cumdist','distance','time']: + axchoicesbasicy.insert(0,(ax[0],ax[1])) + else: + if ax[0] != 'None': + axchoicesbasicx.insert(0,('None',ax[1]+' (PRO)')) + if ax[0] not in ['cumdist','distance','time']: + axchoicesbasicy.insert(0,('None',ax[1]+' (PRO)')) + + + if not user_is_not_basic(rower.user): + self.fields['xaxis'].choices = axchoicesbasicx + self.fields['yaxis1'].choices = axchoicesbasicy + self.fields['yaxis2'].choices = axchoicesbasicy + self.fields['yaxis3'].choices = axchoicesbasicy + self.fields['yaxis4'].choices = axchoicesbasicy + + class FlexAxesForm(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])) xaxis = forms.ChoiceField( choices=axchoices,label='X-Axis',required=True) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index c271506c..3297a83c 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -4412,6 +4412,213 @@ def interactive_cum_flex_chart2(theworkouts,promember=0, return [script,div,js_resources,css_resources] +def interactive_flexchart_stacked(id,r,xparam='time', + yparam1='pace', + yparam2='power', + yparam3='hr', + yparam4='spm', + mode='erg'): + + columns = [xparam,yparam1,yparam2, + 'ftime','distance','fpace', + 'power','hr','spm','driveenergy', + 'time','pace','workoutstate'] + + rowdata = dataprep.getsmallrowdata_db(columns,ids=[id],doclean=True, + workstrokesonly=False) + + if r.usersmooth > 1: + for column in columns: + try: + if metricsdicts[column]['maysmooth']: + nrsteps = int(log2(r.usersmooth)) + for i in range(nrsteps): + rowdata[column] = stravastuff.ewmovingaverage(rowdata[column],5) + except KeyError: + pass + + if len(rowdata)<2: + rowdata = dataprep.getsmallrowdata_db(columns,ids=[id], + doclean=False, + workstrokesonly=False) + + row = Workout.objects.get(id=id) + if rowdata.empty: + return "","No valid data",'','' + + try: + tseconds = rowdata.loc[:,'time'] + except KeyError: + return '','No time data - cannot make flex plot','','' + + try: + rowdata['x1'] = rowdata.loc[:,xparam] + rowmin = rowdata[xparam].min() + except KeyError: + rowdata['x1'] = 0*rowdata.loc[:,'time'] + + try: + rowdata['y1'] = rowdata.loc[:,yparam1] + rowmin = rowdata[yparam1].min() + except KeyError: + rowdata['y1'] = 0*rowdata.loc[:,'time'] + rowdata[yparam1] = rowdata['y1'] + + try: + rowdata['y2'] = rowdata.loc[:,yparam2] + rowmin = rowdata[yparam2].min() + except KeyError: + rowdata['y2'] = 0*rowdata.loc[:,'time'] + rowdata[yparam2] = rowdata['y2'] + + try: + rowdata['y3'] = rowdata.loc[:,yparam3] + rowmin = rowdata[yparam3].min() + except KeyError: + rowdata['y3'] = 0*rowdata.loc[:,'time'] + rowdata[yparam3] = rowdata['y3'] + + try: + rowdata['y4'] = rowdata.loc[:,yparam4] + rowmin = rowdata[yparam4].min() + except KeyError: + rowdata['y4'] = 0*rowdata.loc[:,'time'] + rowdata[yparam4] = rowdata['y4'] + + if xparam=='time': + xaxmax = tseconds.max() + xaxmin = tseconds.min() + elif xparam=='distance' or xparam=='cumdist': + xaxmax = rowdata['x1'].max() + xaxmin = rowdata['x1'].min() + else: + try: + xaxmax = get_yaxmaxima(r,xparam,mode) + xaxmin = get_yaxminima(r,xparam,mode) + except KeyError: + xaxmax = rowdata['x1'].max() + xaxmin = rowdata['x1'].min() + + + x_axis_type = 'linear' + y1_axis_type = 'linear' + y2_axis_type = 'linear' + y3_axis_type = 'linear' + y4_axis_type = 'linear' + if xparam == 'time': + x_axis_type = 'datetime' + + if yparam1 == 'pace': + y1_axis_type = 'datetime' + + if yparam2 == 'pace': + y2_axis_type = 'datetime' + + if yparam3 == 'pace': + y3_axis_type = 'datetime' + + if yparam4 == 'pace': + y4_axis_type = 'datetime' + + try: + rowdata['xname'] = axlabels[xparam] + except KeyError: + rowdata['xname'] = xparam + + try: + rowdata['yname1'] = axlabels[yparam1] + except KeyError: + rowdata['yname1'] = yparam1 + + try: + rowdata['yname2'] = axlabels[yparam2] + except KeyError: + rowdata['yname2'] = yparam2 + + try: + rowdata['yname3'] = axlabels[yparam3] + except KeyError: + rowdata['yname3'] = yparam3 + + try: + rowdata['yname4'] = axlabels[yparam4] + except KeyError: + rowdata['yname4'] = yparam4 + + # prepare data + source = ColumnDataSource( + rowdata + ) + + plot1 = Figure(x_axis_type=x_axis_type,y_axis_type=y1_axis_type,plot_width=920,plot_height=150) + plot2 = Figure(x_axis_type=x_axis_type,y_axis_type=y2_axis_type,plot_width=920,plot_height=150) + plot3 = Figure(x_axis_type=x_axis_type,y_axis_type=y3_axis_type,plot_width=920,plot_height=150) + plot4 = Figure(x_axis_type=x_axis_type,y_axis_type=y4_axis_type,plot_width=920,plot_height=150) + + y1min = get_yaxminima(r,yparam1,mode) + y2min = get_yaxminima(r,yparam2,mode) + y3min = get_yaxminima(r,yparam3,mode) + y4min = get_yaxminima(r,yparam4,mode) + + y1max = get_yaxmaxima(r,yparam1,mode) + y2max = get_yaxmaxima(r,yparam2,mode) + y3max = get_yaxmaxima(r,yparam3,mode) + y4max = get_yaxmaxima(r,yparam4,mode) + + plot1.y_range = Range1d(start=y1min,end=y1max) + plot2.y_range = Range1d(start=y2min,end=y2max) + plot3.y_range = Range1d(start=y3min,end=y3max) + plot4.y_range = Range1d(start=y4min,end=y4max) + + if yparam1 == 'pace': + plot1.yaxis[0].formatter = DatetimeTickFormatter( + seconds = ["%S"], + minutes = ["%M"] + ) + plot1.y_range = Range1d(y1min,y1max) + + if yparam2 == 'pace': + plot2.yaxis[0].formatter = DatetimeTickFormatter( + seconds = ["%S"], + minutes = ["%M"] + ) + plot2.y_range = Range1d(y2min,y2max) + + + if yparam3 == 'pace': + plot3.yaxis[0].formatter = DatetimeTickFormatter( + seconds = ["%S"], + minutes = ["%M"] + ) + plot3.y_range = Range1d(y3min,y3max) + + if yparam4 == 'pace': + plot4.yaxis[0].formatter = DatetimeTickFormatter( + seconds = ["%S"], + minutes = ["%M"] + ) + plot4.y_range = Range1d(y4min,y4max) + + plot1.line('x1','y1',source=source,color="cyan") + plot2.line('x1','y2',source=source,color="red") + plot3.line('x1','y3',source=source,color="green") + plot4.line('x1','y4',source=source,color="blue") + + layout = layoutcolumn([ + plot1, + plot2, + plot3, + plot4, + ]) + + layout.sizing_mode = 'scale_width' + + script, div = components(layout) + js_resources = INLINE.render_js() + css_resources = INLINE.render_css() + + return script,div,js_resources,css_resources + def interactive_flex_chart2(id,r,promember=0, diff --git a/rowers/templates/flexchartstacked.html b/rowers/templates/flexchartstacked.html new file mode 100644 index 00000000..d6468570 --- /dev/null +++ b/rowers/templates/flexchartstacked.html @@ -0,0 +1,66 @@ +{% extends "newbase.html" %} +{% load staticfiles %} +{% load rowerfilters %} +{% load tz %} + +{% block title %} Flexible Plot {% endblock %} + +{% localtime on %} +{% block main %} + +{{ js_res | safe }} +{{ css_res| safe }} + + + + + +{{ the_script |safe }} + +

+ {% if workout|previousworkout:rower.user %} + Previous  + {% endif %} + {% if workout|nextworkout:rower.user %} + Next + {% endif %} +

+ + +

Flexible Chart

+ +
    +
  • +
    + {{ the_div|safe }} +
    +
  • +
  • +
    + {% csrf_token %} + + {{ chartform.as_table }} +
    + + {{ optionsform.as_table }} +
    +

    + +

    +
    +
  • +
+ +{% endblock %} +{% endlocaltime %} + +{% block sidebar %} +{% include 'menu_workout.html' %} +{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index 236f8e4d..520aeffa 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -688,6 +688,7 @@ urlpatterns = [ re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/flexchart/(?P\w+.*)/(?P[\w\ ]+.*)/(?P[\w\ ]+.*)/(?P\w+.*)/$',views.workout_flexchart3_view,name='workout_flexchart3_view'), re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/flexchart/(?P\w+.*)/(?P[\w\ ]+.*)/(?P[\w\ ]+.*)/$',views.workout_flexchart3_view,name='workout_flexchart3_view'), re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/flexchart/$',views.workout_flexchart3_view,name='workout_flexchart3_view'), + re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/flexchartstacked/$',views.workout_flexchart_stacked_view,name='workout_flexchart_stacked_view'), # re_path(r'^workout/compare/(?P\d+)/(?P\d+)/(?P\w+.*)/(?P[\w\ ]+.*)/(?P[\w\ ]+.*)/$',views.workout_comparison_view2), # re_path(r'^workout/compare/(?P\d+)/(?P\d+)/(?P\w+.*)/(?P[\w\ ]+.*)/$',views.workout_comparison_view2), re_path(r'^test\_callback',views.rower_process_testcallback,name='rower_process_testcallback'), diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 665abf4e..fc2cdb13 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -76,6 +76,7 @@ from rowers.forms import ( disqualifiers,SearchForm,BillingForm,PlanSelectForm, VideoAnalysisCreateForm,WorkoutSingleSelectForm, VideoAnalysisMetricsForm,SurveyForm,HistorySelectForm, + StravaChartForm, ) from django.urls import reverse, reverse_lazy diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index fd91c6ef..d63c85c6 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -3938,7 +3938,90 @@ def workout_flexchart3_view(request,*args,**kwargs): 'maxfav':maxfav, }) +@login_required() +@permission_required('workout.view_workout',fn=get_workout_by_opaqueid,raise_exception=True) +def workout_flexchart_stacked_view(request,*args,**kwargs): + try: + id = kwargs['id'] + except KeyError: + raise Http404("Invalid workout number") + workout = get_workout(id) + r = getrequestrower(request) + + xparam = 'time' + yparam1 = 'pace' + yparam2 = 'power' + yparam3 = 'hr' + yparam4 = 'spm' + + if request.method == 'POST': + flexaxesform = StravaChartForm(request,request.POST) + if flexaxesform.is_valid(): + cd = flexaxesform.cleaned_data + xparam = cd['xaxis'] + yparam1 = cd['yaxis1'] + yparam2 = cd['yaxis2'] + yparam3 = cd['yaxis3'] + yparam4 = cd['yaxis4'] + + ( + script, div, js_resources, css_resources + ) = interactive_flexchart_stacked( + encoder.decode_hex(id),r,xparam=xparam, + yparam1=yparam1, + yparam2=yparam2, + yparam3=yparam3, + yparam4=yparam4, + mode=workout.workouttype, + ) + + initial = { + 'xaxis':xparam, + 'yaxis1':yparam1, + 'yaxis2':yparam2, + 'yaxis3':yparam3, + 'yaxis4':yparam4, + } + flexaxesform = StravaChartForm(request,initial=initial, + ) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': workout.name + }, + { + 'url':reverse('workout_flexchart_stacked_view',kwargs=kwargs), + 'name': 'Stacked Flex Chart' + } + + ] + + return render(request, + 'flexchartstacked.html', + { + 'the_script':script, + 'the_div':div, + 'breadcrumbs':breadcrumbs, + 'rower':r, + 'active':'nav-workouts', + 'workout':workout, + 'chartform':flexaxesform, + 'js_res':js_resources, + 'css_res':css_resources, + 'id':id, + 'xparam':xparam, + 'yparam1':yparam1, + 'yparam2':yparam2, + 'yparam3':yparam3, + 'yparam4':yparam4, + } + ) # The interactive plot with wind corrected pace for OTW outings def workout_otwpowerplot_view(request,id=0,message="",successmessage=""):