M activities/activity.py => activities/activity.py +14 -3
@@ 125,8 125,18 @@ def opt_max(a, b):
return a
return max(a, b)
-def analyse_hr_zones(hr: np.array, athlete_max_hr: float) -> np.array:
- bins = (np.array([0.0, 0.5, 0.65, 0.8, 0.9, 10]) * athlete_max_hr).round()
+def opt_min(a, b):
+ if a is None:
+ return b
+ if b is None:
+ return a
+ return min(a, b)
+
+def analyse_hr_zones(hr: np.array, athlete_max_hr: float, athlete_resting_hr:
+ float = 0) -> np.array:
+ hr_range = athlete_max_hr - athlete_resting_hr
+ bins = (np.array([0.0, 0.3, 0.4, 0.6, 0.85, 10]) * hr_range + athlete_resting_hr).round()
+ bins[0] = 0.0
hist = np.histogram(np.convolve(hr[:, 1], np.array([0.5, 0.5]), 'valid'), weights=np.diff(hr[:, 0]), bins=bins)
hr_zones = {
'seconds': list(hist[0].round()),
@@ 202,7 212,8 @@ def analyse(track: Track, athlete_data: Mapping[str, float], raw: bool = False):
min_hr = np.min(hr[:, 1])
potential_max_hr = opt_max(max_hr, athlete_data.get('max_hr'))
- hr_zones = analyse_hr_zones(hr, potential_max_hr)
+ resting_hr = opt_min(min_hr, athlete_data.get('resting_hr'))
+ hr_zones = analyse_hr_zones(hr, potential_max_hr, resting_hr)
elif hr.shape[0] == 1:
avg_hr = hr[0][1]
max_hr = hr[0][1]
A activities/graph.py => activities/graph.py +31 -0
@@ 0,0 1,31 @@
+import io
+
+import matplotlib
+matplotlib.use("Agg")
+from matplotlib import pyplot as plt
+
+from .models import Activity
+
+def hr_zones(act: Activity):
+ p = plt.figure()
+ bins = act.analysis['hr_zones']['bins']
+ seconds = act.analysis['hr_zones']['seconds']
+
+ bin_names = [
+ 'resting',
+ 'light',
+ 'moderate',
+ 'vigorous',
+ 'max effort',
+ ]
+
+ labels = [f'{name} (>{x:.0f} bpm)' for name, x in zip(bin_names, bins)]
+ labels[0] = 'resting'
+
+ pie = plt.pie(seconds, wedgeprops={'width': 0.57}, autopct='%1.f%%', labels=labels, colors=['#eee', '#fcc', '#f99', '#f00', '#600'])
+
+ bio = io.BytesIO()
+ p.savefig(bio, format='svg')
+ bio.seek(0)
+ #len(bio.read())
+ return bio
M activities/jinja2/details.html => activities/jinja2/details.html +5 -0
@@ 23,5 23,10 @@
{{ stat('Max. Heartrate', act.analysis.get('max_hr') | optional_int) }}
{{ stat('Energy', act.analysis.get('energy') | optional_int, 'kcal') }}
</div>
+
+ {% if 'hr_zones' in act.analysis %}
+ <h2>Heart Rate Zone Analysis</h2>
+ <img src="/graphs/hrzones/{{ act.id }}">
+ {% endif %}
</div>
{% endblock %}
M activities/services.py => activities/services.py +2 -0
@@ 94,12 94,14 @@ def process_activity(act: Activity):
logger.error('unknown filetype %s', header[:32])
logger.debug('raw track: %s', track)
+ resting_hr = get_user_healthdata(act.user.id, 'RESTING_HR', track.start_date())
max_hr = get_user_healthdata(act.user.id, 'MAX_HR', track.start_date())
weight = get_user_healthdata(act.user.id, 'WEIGHT', track.start_date())
logger.debug('max HR for analysis: %f', max_hr)
logger.debug('athlete weight for analysis: %f', weight)
athlete_data = {
+ "resting_hr": resting_hr or 60,
"max_hr": max_hr,
"weight": weight,
}
M activities/urls.py => activities/urls.py +1 -0
@@ 12,6 12,7 @@ urlpatterns = [
path('upload', views.upload, name='upload'),
path('activities', views.overview, name='overview'),
path('activities/<int:activity_id>', views.summary, name='summary'),
+ path('graphs/hrzones/<int:activity_id>', views.graph_hr_zones, name='graph-hrzones'),
path('api/v1/activities', rest.activity_upload, name='rest_upload'),
path('api/v1/import', rest.transfer_import, name='rest_import'),
]
M activities/views.py => activities/views.py +6 -0
@@ 10,6 10,7 @@ from django import forms
from .models import Activity
from .services import UPLOAD_MAX_SIZE, create_activity, queue_analysis
+from . import graph
import datetime
import hashlib
@@ 53,6 54,11 @@ def summary(request, activity_id):
return render(request, 'details.html', context)
@login_required
+def graph_hr_zones(request, activity_id):
+ act = request.user.activity_set.get(id=activity_id)
+ return HttpResponse(graph.hr_zones(act), content_type='image/svg+xml')
+
+@login_required
def upload(request):
if request.method == 'POST':
form = UploadFileForm(request.POST, request.FILES)