This commit has no recorded session.
diff
commit 6471413681e09a42025bc9c0e6fad5e585eba3c5Author: Colin Powell <colin@unbl.ink>Date: Thu Jun 4 22:13:14 2026 -0400 [tasks] Add weigh in graph@@ -93,7 +93,7 @@ fetching and simple saving. :LOGBOOK: CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20 :END:-* Backlog [4/18] :vrobbler:project:personal:+* Backlog [5/18] :vrobbler:project:personal: ** TODO [#C] Add sentiment parsing for Scrobbles with notes :vrobbler:project:scrobbles:sentiment: :PROPERTIES: :ID: 37781d6a-f3b0-48b2-bf98-33c2c791cf85@@ -489,7 +489,7 @@ whatever time KoReader reports, we need to know, given the date and the user profile's historic timezone, how many hours to adjust the KoReader time to get to GMT to save it in the database. -** TODO [#B] Can we show a graph of all past Weigh-in tasks :scale:tasks:graphs:javascript:+** DONE [#B] Can we show a graph of all past Weigh-in tasks :scale:tasks:graphs:javascript: :PROPERTIES: :ID: ae499d87-03bf-4e48-9b2c-1a421a46af11 :END:@@ -503,6 +503,10 @@ The graph would contain all Weigh-in scrobbles for that user, no matter which date is being viewed, and the highlighted value on the graph would be the date being viewed. +Probably could use something like chart.js although maybe that's too heavy?++And can we have each metric overlayed on the same graph?+ ** DONE [#B] When viewing scrobbles by tag, sum the total time :scrobbles:tags: :PROPERTIES: :ID: d51f23df-c2c5-4e1a-b000-67c89032af02@@ -1,5 +1,6 @@ import logging +import pendulum from django.contrib import messages from django.http import HttpResponseRedirect from django.urls import reverse_lazy@@ -8,6 +9,7 @@ from rest_framework import status from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response+from scrobbles.models import Scrobble from scrobbles.views import ScrobbleableDetailView, ScrobbleableListView from tasks.models import Task @@ -23,6 +25,72 @@ class TaskListView(ScrobbleableListView): class TaskDetailView(ScrobbleableDetailView): model = Task + def get_context_data(self, **kwargs):+ ctx = super().get_context_data(**kwargs)+ if self.object.title != "Weigh-in":+ return ctx++ scrobbles = list(+ Scrobble.objects.filter(+ user=self.request.user,+ task=self.object,+ log__weight__isnull=False,+ ).order_by("timestamp")+ )+ if not scrobbles:+ return ctx++ labels = []+ weight_data = []+ body_fat_data = []+ bmi_data = []+ for s in scrobbles:+ ts = s.timestamp+ if isinstance(ts, str):+ ts = pendulum.parse(ts)+ labels.append(ts.strftime("%Y-%m-%d"))+ log = s.log if isinstance(s.log, dict) else {}+ raw_weight = log.get("weight")+ weight_data.append(+ float(raw_weight) if raw_weight is not None else None+ )+ raw_bf = log.get("body_fat")+ body_fat_data.append(+ float(raw_bf) if raw_bf is not None else None+ )+ raw_bmi = log.get("bmi")+ bmi_data.append(+ float(raw_bmi) if raw_bmi is not None else None+ )++ ctx["weighin_chart"] = {+ "labels": labels,+ "datasets": [+ {+ "label": "Weight",+ "data": weight_data,+ "borderColor": "#4bc0c0",+ "fill": False,+ "yAxisID": "y",+ },+ {+ "label": "Body Fat %",+ "data": body_fat_data,+ "borderColor": "#ff6384",+ "fill": False,+ "yAxisID": "y1",+ },+ {+ "label": "BMI",+ "data": bmi_data,+ "borderColor": "#36a2eb",+ "fill": False,+ "yAxisID": "y2",+ },+ ],+ }+ return ctx+ @api_view(["GET"]) @permission_classes([IsAuthenticated])@@ -2,6 +2,7 @@ {% load mathfilters %} {% load static %} {% load naturalduration %}+{% load l10n %} {% block title %}{{object.title}}{% endblock %} @@ -39,6 +40,15 @@ </p> </div> </div>++{% if weighin_chart %}+<div class="row mb-4">+ <div class="col-md-8">+ <canvas id="weighinChart" width="800" height="300"></canvas>+ </div>+</div>+{% endif %}+ <div class="row"> <p>{{scrobbles.count}} scrobbles</p> <p>@@ -94,3 +104,61 @@ {% endif %} </div> {% endblock %}++{% block extra_js %}+{% if weighin_chart %}+<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js"></script>+<script>+var ctx = document.getElementById('weighinChart').getContext('2d');+new Chart(ctx, {+ type: 'line',+ data: {+ labels: {{ weighin_chart.labels|safe }},+ datasets: [+ {+ label: 'Weight',+ data: {{ weighin_chart.datasets.0.data|safe }},+ borderColor: '{{ weighin_chart.datasets.0.borderColor }}',+ backgroundColor: 'transparent',+ pointRadius: 2,+ fill: false,+ yAxisID: 'y',+ },+ {+ label: 'Body Fat %',+ data: {{ weighin_chart.datasets.1.data|safe }},+ borderColor: '{{ weighin_chart.datasets.1.borderColor }}',+ backgroundColor: 'transparent',+ pointRadius: 2,+ fill: false,+ yAxisID: 'y1',+ },+ {+ label: 'BMI',+ data: {{ weighin_chart.datasets.2.data|safe }},+ borderColor: '{{ weighin_chart.datasets.2.borderColor }}',+ backgroundColor: 'transparent',+ pointRadius: 2,+ fill: false,+ yAxisID: 'y2',+ },+ ]+ },+ options: {+ responsive: true,+ scales: {+ xAxes: [{+ ticks: { maxTicksLimit: 25, maxRotation: 45 },+ }],+ yAxes: [+ { id: 'y', type: 'linear', position: 'left', scaleLabel: { display: true, labelString: 'Weight' } },+ { id: 'y1', type: 'linear', position: 'right', scaleLabel: { display: true, labelString: 'Body Fat %' }, gridLines: { display: false } },+ { id: 'y2', type: 'linear', position: 'right', scaleLabel: { display: true, labelString: 'BMI' }, gridLines: { display: false } },+ ]+ },+ legend: { position: 'bottom' },+ }+});+</script>+{% endif %}+{% endblock %}