diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 421306bb..8301c231 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1527,6 +1527,139 @@ def interactive_forcecurve(theworkouts,workstrokesonly=True,plottype='scatter'): return [script,div,js_resources,css_resources] +def fitnessfit_chart(workouts,user,workoutmode='water',startdate=None, + enddate=None,nrdays=20): + + TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' + + dates = [] + fourminpower = [] + hourpower = [] + + workouts = workouts.order_by('date') + + for w in workouts: + ws = workouts.filter(date__gte=w.date-datetime.timedelta(days=nrdays), + date__lte=w.date) + wws = [ww for ww in ws] + delta,cpvalue,avgpower,workoutnames = dataprep.fetchcp_new(user.rower,wws) + powerdf = pd.DataFrame({ + 'Delta':delta, + 'CP':cpvalue, + 'workout':workoutnames, + }) + powerdf = powerdf[powerdf['CP']>0] + powerdf.dropna(axis=0,inplace=True) + powerdf.sort_values(['Delta','CP'],ascending=[1,0],inplace=True) + powerdf.drop_duplicates(subset='Delta',keep='first',inplace=True) + p1,fitt,fitpower,ratio = datautils.cpfit(powerdf) + fitfunc = lambda pars,x: abs(pars[0])/(1+(x/abs(pars[2]))) + abs(pars[1])/(1+(x/abs(pars[3]))) + + powerfourmin = fitfunc(p1,240.) + powerhour = fitfunc(p1,3600) + dates.append(datetime.datetime.combine(w.date,datetime.datetime.min.time())) + fourminpower.append(powerfourmin) + hourpower.append(powerhour) + + df = pd.DataFrame({ + 'date':dates, + 'fourminpower':fourminpower, + 'hourpower':hourpower, + }) + + df.sort_values(['date'],inplace=True) + + df['hourdup'] = df['hourpower'].shift(1) + df['fourmindup'] = df['fourminpower'].shift(1) + df['hourpower'] = df.apply(lambda x: np.nan if abs(x['hourpower'] - x['hourdup']) < 4 \ + else x['hourpower'],axis=1) + + df['fourminpower'] = df.apply(lambda x: np.nan if abs(x['fourminpower'] - x['fourmindup']) < 4 \ + else x['fourminpower'],axis=1) + + + + source = ColumnDataSource( + data = dict( + fourminpower = df['fourminpower'], + hourpower = df['hourpower'], + date = df['date'], + fdate = df['date'].map(lambda x: x.strftime('%d-%m-%Y')) + ) + ) + + + + plot = Figure(tools=TOOLS,x_axis_type='datetime', + plot_width=900, + toolbar_location="above", + toolbar_sticky=False) + + + # add watermark + watermarkurl = "/static/img/logo7.png" + watermarksource = ColumnDataSource(dict( + url = [watermarkurl],)) + + watermarkrange = Range1d(start=0,end=1) + watermarkalpha = 0.6 + watermarkx = 0.99 + watermarky = 0.01 + watermarkw = 184 + watermarkh = 35 + watermarkanchor = 'bottom_right' + 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", + ) + + plot.circle('date','fourminpower',source=source,fill_color='green',size=10,legend_label='4 min power') + plot.circle('date','hourpower',source=source,fill_color='blue',size=10,legend_label='60 min power') + + + + plot.xaxis.axis_label = 'Date' + plot.yaxis.axis_label = 'Power (W)' + + plot.xaxis.formatter = DatetimeTickFormatter( + days=["%d %B %Y"], + months=["%d %B %Y"], + years=["%d %B %Y"], + ) + + plot.xaxis.major_label_orientation = pi/4 + plot.sizing_mode = 'stretch_both' + + plot.y_range = Range1d(0,1.5*max(fourminpower)) + startdate = datetime.datetime.combine(startdate,datetime.datetime.min.time()) + enddate = datetime.datetime.combine(enddate,datetime.datetime.min.time()) + + plot.x_range = Range1d( + startdate,enddate+datetime.timedelta(days=5), + ) + plot.title.text = 'Power levels ('+workoutmode+') from workouts '+user.first_name + + hover = plot.select(dict(type=HoverTool)) + + hover.tooltips = OrderedDict([ + ('Power 4 minutes','@fourminpower'), + ('Power 1 hour','@hourpower'), + ('Date','@fdate') + ]) + + script,div = components(plot) + + return [script,div] + def fitnessmetric_chart(fitnessmetrics,user,workoutmode='rower',startdate=None, enddate=None): diff --git a/rowers/templates/fitnessfit.html b/rowers/templates/fitnessfit.html new file mode 100644 index 00000000..84c1cb5f --- /dev/null +++ b/rowers/templates/fitnessfit.html @@ -0,0 +1,103 @@ +{% extends "newbase.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Rowsandall Fitness Progress {% endblock %} + +{% block main %} + + + + + + +{{ chartscript |safe }} + + + +{% if rower.user %} +

Power Progress for {{ rower.user.first_name }}

+{% else %} +

Power Progress for {{ user.first_name }}

+{% endif %} + + +
    +
  • +
    + + {{ form.as_table }} +
    + {% csrf_token %} + +
    + +
  • +
  • + {{ the_div|safe }} +
  • +
+ + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_analytics.html' %} +{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index b2d003a4..b474f145 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -352,6 +352,9 @@ urlpatterns = [ re_path(r'^fitness-progress/$',views.fitnessmetric_view,name='fitnessmetric_view'), re_path(r'^fitness-progress/user/(?P\d+)/$',views.fitnessmetric_view,name='fitnessmetric_view'), re_path(r'^fitness-progress/user/(?P\d+)/(?P\w+.*)/$',views.fitnessmetric_view,name='fitnessmetric_view'), + re_path(r'^fitness-fit/$',views.fitness_from_cp_view,name='fitness_from_cp_view'), + re_path(r'^fitness-fit/user/(?P\d+)/$',views.fitness_from_cp_view,name='fitness_from_cp_view'), + re_path(r'^fitness-fit/user/(?P\d+)/(?P\w+.*)/$',views.fitness_from_cp_view,name='fitness_from_cp_view'), # re_path(r'^ote-bests/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.rankings_view,name='rankings_view'), re_path(r'^ote-bests/user/(?P\d+)/$',views.rankings_view,name='rankings_view'), # re_path(r'^ote-bests/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.rankings_view,name='rankings_view'), diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 1955a04e..c976449b 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -1539,6 +1539,68 @@ def fitnessmetric_view(request,userid=0,mode='rower', 'form':form, }) +@user_passes_test(isplanmember,login_url="/rowers/paidplans", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +@permission_required('rower.is_coach',fn=get_user_by_userid,raise_exception=True) +def fitness_from_cp_view(request,userid=0,mode='rower', + startdate=timezone.now()-timezone.timedelta(days=365), + enddate=timezone.now()): + + + + therower = getrequestrower(request,userid=userid) + theuser = therower.user + + + + if request.method == 'POST': + form = FitnessMetricForm(request.POST) + if form.is_valid(): + startdate = form.cleaned_data['startdate'] + enddate = form.cleaned_data['enddate'] + mode = form.cleaned_data['mode'] + else: + form = FitnessMetricForm() + + workouts = Workout.objects.filter(user=therower,date__gte=startdate, + date__lte=enddate, + workouttype__in=mytypes.otwtypes) + if mode == 'rower': + workouts = Workout.objects.filter(user=therower,date__gte=startdate, + date__lte=enddate,workouttype__in=mytypes.otetypes) + + + + script,thediv = fitnessfit_chart( + workouts,theuser, + workoutmode=mode,startdate=startdate, + enddate=enddate, + ) + + breadcrumbs = [ + { + 'url':'/rowers/analysis', + 'name':'Analysis' + }, + { + 'url':reverse('fitnessmetric_view'), + 'name': 'Power Progress' + } + ] + + + return render(request,'fitnessfit.html', + { + 'rower':therower, + 'active':'nav-analysis', + 'chartscript':script, + 'breadcrumbs':breadcrumbs, + 'the_div':thediv, + 'mode':mode, + 'form':form, + }) + # Show ranking distances including predicted paces @login_required()