From 1dce1cbfcb9b09bbc6faa132e7df8b313e2135df Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 8 Apr 2018 14:23:23 +0200 Subject: [PATCH 1/4] prototype fitness-progress chart --- rowers/interactiveplots.py | 90 +++++++++++++++++++++- rowers/templates/fitnessmetric.html | 114 ++++++++++++++++++++++++++++ rowers/urls.py | 3 + rowers/views.py | 24 ++++++ 4 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 rowers/templates/fitnessmetric.html diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index a1efd629..80e329df 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -628,7 +628,95 @@ def interactive_forcecurve(theworkouts,workstrokesonly=False): return [script,div,js_resources,css_resources] - +def fitnessmetric_chart(fitnessmetrics,user,workoutmode='rower'): + + power4min = [int(m.PowerFourMin) for m in fitnessmetrics] + power2k = [int(m.PowerTwoK) for m in fitnessmetrics] + power1hr = [int(m.PowerOneHour) for m in fitnessmetrics] + dates = [m.date for m in fitnessmetrics] + mode = [m.workoutmode for m in fitnessmetrics] + + df = pd.DataFrame( + {'power4min':power4min, + 'power2k':power2k, + 'power1hr':power1hr, + 'date':dates, + 'dates':dates, + 'mode':mode + }) + + + df = df[df['power2k']>0] + df = df[df['mode']==workoutmode] + + groups = df.groupby(by='date').max() + + power4min = groups['power4min'] + date = groups['dates'] + power2k = groups['power2k'] + power1hr = groups['power1hr'] + + source = ColumnDataSource( + data = dict( + power4min = power4min, + power2k = power2k, + date = date, + power1hr = power1hr + ) + ) + + TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,resize,crosshair' + + plot = Figure(tools=TOOLS,toolbar_location="above", + toolbar_sticky=False,width=900, + x_axis_type='datetime') + +# plot.extra_y_ranges = {"watermark": watermarkrange} + +# plot.image_url([watermarkurl],1.8*max(thesecs),watermarky, +# watermarkw,watermarkh, +# global_alpha=watermarkalpha, +# w_units='screen', +# h_units='screen', +# anchor=watermarkanchor, +# dilate=True, +# y_range_name = "watermark", +# ) + + plot.circle('date','power2k',source=source,fill_color='red',size=7, + legend='2k power') + + plot.circle('date','power1hr',source=source,fill_color='blue',size=7, + legend='1 hr power') + + plot.circle('date','power4min',source=source,fill_color='green',size=7, + legend='4 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.y_range = Range1d(0,1.5*max(power4min)) + plot.title.text = 'Fitness of '+user.first_name + + hover = plot.select(dict(type=HoverTool)) + + hover.tooltips = OrderedDict([ + ('Power 4 minutes','@power4min'), + ('Power 2000 m','@power2k'), + ('Power 1 hour','@power1hr'), + ]) + + script,div = components(plot) + + return [script,div] def interactive_histoall(theworkouts): TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,resize,crosshair' diff --git a/rowers/templates/fitnessmetric.html b/rowers/templates/fitnessmetric.html new file mode 100644 index 00000000..8bc5b23d --- /dev/null +++ b/rowers/templates/fitnessmetric.html @@ -0,0 +1,114 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Rowsandall Fitness Progress {% endblock %} + +{% block content %} + + + + + + +{{ chartscript |safe }} + + + + + +
+
+ {% if therower.user %} +

{{ therower.user.first_name }} Fitness Progress

+ {% else %} +

{{ user.first_name }} Fitness progress

+ {% endif %} +
+
+ {% if user.is_authenticated and user|is_manager %} + + {% endif %} +
+ + +
+ +
+ + {{ the_div|safe }} + +
+ +{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index 2b513bb5..6d04aaa6 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -173,6 +173,9 @@ urlpatterns = [ url(r'^record-progress/(?P.*)$',views.post_progress), url(r'^record-progress$',views.post_progress), url(r'^list-graphs/$',views.graphs_view), + url(r'^fitness-progress/$',views.fitnessmetric_view), + url(r'^fitness-progress/rower/(?P\d+)$',views.fitnessmetric_view), + url(r'^fitness-progress/rower/(?P\d+)/(?P\w+.*)$',views.fitnessmetric_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), url(r'^ote-bests/(?P\w+.*)/(?P\w+.*)$',views.rankings_view), diff --git a/rowers/views.py b/rowers/views.py index 3143e41a..43ba4ab7 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -3062,6 +3062,30 @@ def cum_flex(request,theuser=0, }) +@user_passes_test(hasplannedsessions,login_url="/",redirect_field_name=None) +def fitnessmetric_view(request,id=0,mode='rower'): + if id==0: + id = request.user.id + + theuser = User.objects.get(id=id) + therower = Rower.objects.get(user=theuser) + + fitnessmetrics = PowerTimeFitnessMetric.objects.filter(user=theuser) + + script,thediv = fitnessmetric_chart( + fitnessmetrics,theuser, + workoutmode=mode + ) + + return render(request,'fitnessmetric.html', + { + 'therower':therower, + 'chartscript':script, + 'the_div':thediv, + 'mode':mode, + }) + + # Show the EMpower Oarlock generated Stroke Profile @user_passes_test(ispromember,login_url="/",redirect_field_name=None) def workout_forcecurve_view(request,id=0,workstrokesonly=False): From f8fcc83e6dc4e137fdb7f5cd58bef1bb9748f1ac Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 8 Apr 2018 17:17:25 +0200 Subject: [PATCH 2/4] changes --- rowers/templates/plannedsessions_print.html | 2 +- static/css/rowsandall.css | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/rowers/templates/plannedsessions_print.html b/rowers/templates/plannedsessions_print.html index f91ac599..34b065fc 100644 --- a/rowers/templates/plannedsessions_print.html +++ b/rowers/templates/plannedsessions_print.html @@ -63,7 +63,7 @@ Courses {% for ps in plannedsessions %} -
+

Session {{ ps.name }}

diff --git a/static/css/rowsandall.css b/static/css/rowsandall.css index 60918ee3..776bfd55 100644 --- a/static/css/rowsandall.css +++ b/static/css/rowsandall.css @@ -85,7 +85,21 @@ a:hover { text-decoration: underline; } +@media print { + h1 { + page-break-before: always; + page-break-after: avoid; + } + + h3, h4 { + page-break-after: avoid; + } + pre, blockquote, table { + page-break-inside: avoid; + } + +} h1 { From b3eed722d65ea40eb5cba4c58cb5debd0b97bb19 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 9 Apr 2018 13:19:19 +0200 Subject: [PATCH 3/4] linear interpolation in fitnessmetric chart --- rowers/interactiveplots.py | 73 ++++++++++++++++++++++++++++++-------- rowers/tasks.py | 45 ++++++++++++----------- 2 files changed, 83 insertions(+), 35 deletions(-) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 80e329df..3e0ff059 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -57,6 +57,7 @@ thetimezone = get_current_timezone() from scipy.stats import linregress,percentileofscore from scipy import optimize from scipy.signal import savgol_filter +from scipy.interpolate import griddata import stravastuff @@ -646,6 +647,14 @@ def fitnessmetric_chart(fitnessmetrics,user,workoutmode='rower'): }) + delta = df['power4min'].astype('int').diff() + + mask = delta == 0 + + df.loc[mask,'power4min'] = np.nan + df.dropna(inplace=True,axis=0,how='any') + + df = df[df['power2k']>0] df = df[df['mode']==workoutmode] @@ -661,37 +670,70 @@ def fitnessmetric_chart(fitnessmetrics,user,workoutmode='rower'): power4min = power4min, power2k = power2k, date = date, - power1hr = power1hr - ) + power1hr = power1hr, + fdate=groups['dates'].map(lambda x: x.strftime('%d-%m-%Y')) ) ) + + # fit + + resampled = groups.set_index('dates') + resampled.index = pd.to_datetime(resampled.index) + resampled = resampled.resample('D').interpolate( + method='linear',order=2) + + power4min = resampled['power4min'] + date = resampled.index.values + power2k = resampled['power2k'] + power1hr = resampled['power1hr'] + + source2 = ColumnDataSource( + data = dict( + power4min = power4min, + power2k = power2k, + date = date, + power1hr = power1hr, + fdate=resampled.index.map(lambda x: x.strftime('%d-%m-%Y')) ) + ) + + + + TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,resize,crosshair' plot = Figure(tools=TOOLS,toolbar_location="above", toolbar_sticky=False,width=900, x_axis_type='datetime') -# plot.extra_y_ranges = {"watermark": watermarkrange} + # add watermark + plot.extra_y_ranges = {"watermark": watermarkrange} + plot.extra_x_ranges = {"watermark": watermarkrange} -# plot.image_url([watermarkurl],1.8*max(thesecs),watermarky, -# watermarkw,watermarkh, -# global_alpha=watermarkalpha, -# w_units='screen', -# h_units='screen', -# anchor=watermarkanchor, -# dilate=True, -# y_range_name = "watermark", -# ) + 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','power2k',source=source,fill_color='red',size=7, + plot.circle('date','power2k',source=source,fill_color='red',size=10, legend='2k power') - plot.circle('date','power1hr',source=source,fill_color='blue',size=7, + + plot.circle('date','power1hr',source=source,fill_color='blue',size=10, legend='1 hr power') - plot.circle('date','power4min',source=source,fill_color='green',size=7, + plot.circle('date','power4min',source=source,fill_color='green',size=10, legend='4 min power') + plot.line('date','power4min',source=source2,color='green') + plot.line('date','power2k',source=source2,color='red') + plot.line('date','power1hr',source=source2,color='blue') + plot.xaxis.axis_label = 'Date' plot.yaxis.axis_label = 'Power (W)' @@ -712,6 +754,7 @@ def fitnessmetric_chart(fitnessmetrics,user,workoutmode='rower'): ('Power 4 minutes','@power4min'), ('Power 2000 m','@power2k'), ('Power 1 hour','@power1hr'), + ('Date','@fdate'), ]) script,div = components(plot) diff --git a/rowers/tasks.py b/rowers/tasks.py index c5625658..08f48c49 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -743,26 +743,7 @@ def handle_updateergcp(rower_id,workoutfilenames,debug=False,**kwargs): return 1 -@app.task -def handle_updatefitnessmetric(user_id,mode,workoutids,debug=False, - **kwargs): - - powerfourmin = -1 - power2k = -1 - powerhour = -1 - - mdict = { - 'user_id': user_id, - 'PowerFourMin': powerfourmin, - 'PowerTwoK': power2k, - 'PowerOneHour': powerhour, - 'workoutmode': mode, - 'last_workout': max(workoutids), - 'date': timezone.now().strftime('%Y-%m-%d'), - } - - result = fitnessmetric_to_sql(mdict,debug=debug,doclean=False) - +def cp_from_workoutids(workoutids,debug=False): columns = ['power','workoutid','time'] df = getsmallrowdata_db(columns,ids=workoutids,debug=debug) df.dropna(inplace=True,axis=0) @@ -821,6 +802,30 @@ def handle_updatefitnessmetric(user_id,mode,workoutids,debug=False, power2k = fitfunc(p1,t3) + return powerfourmin,power2k,powerhour + + +@app.task +def handle_updatefitnessmetric(user_id,mode,workoutids,debug=False, + **kwargs): + + powerfourmin = -1 + power2k = -1 + powerhour = -1 + + mdict = { + 'user_id': user_id, + 'PowerFourMin': powerfourmin, + 'PowerTwoK': power2k, + 'PowerOneHour': powerhour, + 'workoutmode': mode, + 'last_workout': max(workoutids), + 'date': timezone.now().strftime('%Y-%m-%d'), + } + + result = fitnessmetric_to_sql(mdict,debug=debug,doclean=False) + + powerfourmin,power2k,powerhour = cp_from_workoutids(workoutids,debug=debug) mdict = { 'user_id': user_id, From 4ad1787a1dfe98005e47eb1426883d88da649df4 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 9 Apr 2018 13:42:25 +0200 Subject: [PATCH 4/4] date form for fitness metric --- rowers/forms.py | 25 +++++++++++++++++++++++++ rowers/interactiveplots.py | 2 +- rowers/templates/fitnessmetric.html | 15 +++++++++++++-- rowers/views.py | 21 +++++++++++++++++++-- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index 9699aedc..df48585f 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -352,6 +352,31 @@ class DateRangeForm(forms.Form): class Meta: fields = ['startdate','enddate'] +class FitnessMetricForm(forms.Form): + startdate = forms.DateField( + initial=timezone.now()-datetime.timedelta(days=365), + # widget=SelectDateWidget(years=range(1990,2050)), + widget=AdminDateWidget(), + label='Start Date') + enddate = forms.DateField( + initial=timezone.now(), + widget=AdminDateWidget(), + label='End Date') + + modechoices = ( + ('rower','indoor rower'), + ('water','on the water') + ) + + mode = forms.ChoiceField(required=True, + choices=modechoices, + initial='rower', + label='Workout Mode' + ) + + class Meta: + fields = ['startdate','enddate','mode'] + class SessionDateShiftForm(forms.Form): shiftstartdate = forms.DateField( initial=timezone.now(), diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 3e0ff059..9e652900 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -746,7 +746,7 @@ def fitnessmetric_chart(fitnessmetrics,user,workoutmode='rower'): plot.xaxis.major_label_orientation = pi/4 plot.y_range = Range1d(0,1.5*max(power4min)) - plot.title.text = 'Fitness of '+user.first_name + plot.title.text = 'Power levels from workouts '+user.first_name hover = plot.select(dict(type=HoverTool)) diff --git a/rowers/templates/fitnessmetric.html b/rowers/templates/fitnessmetric.html index 8bc5b23d..cba71c9b 100644 --- a/rowers/templates/fitnessmetric.html +++ b/rowers/templates/fitnessmetric.html @@ -77,11 +77,22 @@
+
+
+
+ {{ form.as_table }} +
+ {% csrf_token %} +
+ +
+ +
{% if therower.user %} -

{{ therower.user.first_name }} Fitness Progress

+

{{ therower.user.first_name }} Power Estimates

{% else %} -

{{ user.first_name }} Fitness progress

+

{{ user.first_name }} Power Estimates

{% endif %}
diff --git a/rowers/views.py b/rowers/views.py index 43ba4ab7..9dce32c6 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -46,6 +46,7 @@ from django.core.mail import send_mail, BadHeaderError from rowers.forms import ( SummaryStringForm,IntervalUpdateForm,StrokeDataForm, StatsOptionsForm,PredictedPieceForm,DateRangeForm,DeltaDaysForm, + FitnessMetricForm, EmailForm, RegistrationForm, RegistrationFormTermsOfService, RegistrationFormUniqueEmail,RegistrationFormSex, CNsummaryForm,UpdateWindForm, @@ -3063,14 +3064,29 @@ def cum_flex(request,theuser=0, @user_passes_test(hasplannedsessions,login_url="/",redirect_field_name=None) -def fitnessmetric_view(request,id=0,mode='rower'): +def fitnessmetric_view(request,id=0,mode='rower', + startdate=timezone.now()-timezone.timedelta(days=365), + enddate=timezone.now()): if id==0: id = request.user.id theuser = User.objects.get(id=id) therower = Rower.objects.get(user=theuser) - fitnessmetrics = PowerTimeFitnessMetric.objects.filter(user=theuser) + + 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() + + fitnessmetrics = PowerTimeFitnessMetric.objects.filter( + user=theuser, + date__gte=startdate, + date__lte=enddate) script,thediv = fitnessmetric_chart( fitnessmetrics,theuser, @@ -3083,6 +3099,7 @@ def fitnessmetric_view(request,id=0,mode='rower'): 'chartscript':script, 'the_div':thediv, 'mode':mode, + 'form':form, })