WIP: When selecting an exercise on new workout view, render a graph of exercise progress for the active user

This commit is contained in:
Peter Stockings
2023-12-07 20:34:26 +11:00
parent 5bf31d0cb9
commit 469054048e
5 changed files with 120 additions and 9 deletions

47
app.py
View File

@@ -424,14 +424,45 @@ def get_most_recent_topset_for_exercise(person_id, workout_id):
return render_template('partials/new_set_form.html', person_id=person_id, workout_id=workout_id, exercises=exercises, has_value=True, exercise_id=exercise_id, repetitions=repetitions, weight=weight) return render_template('partials/new_set_form.html', person_id=person_id, workout_id=workout_id, exercises=exercises, has_value=True, exercise_id=exercise_id, repetitions=repetitions, weight=weight)
# # TODO: Remove me, just for testing def calculate_relative_positions(start_dates):
# @ app.route("/sparkline", methods=['GET']) min_date = min(start_dates)
# def get_sparkline(): max_date = max(start_dates)
# width = request.args.get('width', 400, type=int) total_span = (max_date - min_date).days if max_date != min_date else 1
# height = request.args.get('height', 200, type=int) return [(date - min_date).days / total_span for date in start_dates]
# number_of_points = request.args.get('number_of_points', 50, type=int)
# points = [random.randint(1, 100) for _ in range(number_of_points)] @ app.route("/person/<int:person_id>/exercise/<int:exercise_id>/sparkline", methods=['GET'])
# return render_template('partials/sparkline.html', width=width, height=height, points=points) def get_exercise_progress_for_user(person_id, exercise_id):
width = request.args.get('width', 300, type=int)
height = request.args.get('height', 100, type=int)
(estimated_1rm, start_dates) = db.get_exercise_progress_for_user(person_id, exercise_id)
# Calculate vb_width
min_date = min(start_dates)
max_date = max(start_dates)
date_range = (max_date - min_date).days # e.g., 30 days
vb_width = date_range # This can be scaled if needed
# Calculate vb_height
min_value = min(estimated_1rm)
max_value = max(estimated_1rm)
value_range = max_value - min_value # e.g., 100
vb_height = value_range # This can be scaled if needed
# Scaling factors (optional, for design)
width_scaling_factor = 200 / vb_width # e.g., if you want 200px width
height_scaling_factor = 75 / vb_height # e.g., if you want 100px height
# Apply scaling
vb_width *= width_scaling_factor
vb_height *= height_scaling_factor
# Scale estimated_1rm between 0 and vb_height
estimated_1rm = [((value - min_value) / value_range) * vb_height for value in estimated_1rm]
relative_positions = calculate_relative_positions(start_dates)
data_points = list(zip(estimated_1rm, relative_positions))
return render_template('partials/sparkline.html', title="GHR", vb_width=vb_width, vb_height=vb_height, data_points=data_points)
@app.teardown_appcontext @app.teardown_appcontext

28
db.py
View File

@@ -462,3 +462,31 @@ class DataBase():
exercises = self.execute( exercises = self.execute(
'SELECT exercise_id, name FROM exercise') 'SELECT exercise_id, name FROM exercise')
return exercises return exercises
def get_exercise_progress_for_user(self, person_id, exercise_id):
topsets = self.execute("""
SELECT
T.topset_id,
E.name AS exercise_name,
W.person_id,
T.workout_id,
T.repetitions,
T.weight,
ROUND((100 * T.weight::NUMERIC::INTEGER) / (101.3 - 2.67123 * T.repetitions), 0)::NUMERIC::INTEGER AS estimated_1rm,
W.start_date
FROM
topset T
JOIN
exercise E ON T.exercise_id = E.exercise_id
JOIN
workout W ON T.workout_id = W.workout_id
WHERE
W.person_id = %s AND
E.exercise_id = %s
ORDER BY
W.start_date;""", [person_id, exercise_id])
# Get a list of all estimated_1rm values
estimated_1rm = [t['estimated_1rm'] for t in topsets]
# Get a list of all start_dates
start_dates = [t['start_date'] for t in topsets]
return (estimated_1rm, start_dates)

View File

@@ -67,4 +67,11 @@
class="py-2 px-3 mb-3 text-sm font-medium text-center text-gray-900 bg-white rounded-lg border border-gray-300 hover:bg-gray-100 hover:scale-[1.02] transition-transform">Delete class="py-2 px-3 mb-3 text-sm font-medium text-center text-gray-900 bg-white rounded-lg border border-gray-300 hover:bg-gray-100 hover:scale-[1.02] transition-transform">Delete
workout</button> workout</button>
</div> </div>
</form> </form>
{% if has_value==True %}
<div class="hidden"
hx-get="{{ url_for('get_exercise_progress_for_user', person_id=person_id, exercise_id=exercise_id) }}"
hx-trigger="load" hx-target="#exercise-progress-{{ person_id }}" hx-swap="innerHTML">
</div>
{% endif %}

View File

@@ -0,0 +1,42 @@
{% set fill = "#dcfce7" %}
{% set stroke = "#bbf7d0" %}
{% set stroke_width = 4 %}
{% set margin = 0 %} {# space allocated for axis labels and ticks #}
{% macro path(data_points, vb_height) %}
{% for value, position in data_points %}
{% set x = position * vb_width %}
{% set y = vb_height - value %}
{% if loop.first %}M{{ x }} {{ y }}{% else %} L{{ x }} {{ y }}{% endif %}
{% endfor %}
{% endmacro %}
{% macro circles(data_points, vb_height) %}
{% for value, position in data_points %}
{% set x = position * vb_width %}
{% set y = vb_height - value %}
<circle cx="{{ x }}" cy="{{ y }}" r="5" class="cursor-pointer" data-message="{{ position }} - {{ value }}" fill-opacity="0%"
_="on mouseover
put my @data-message into #popover
then remove .hidden from #popover
on mouseout
add .hidden to #popover">
</circle>
{% endfor %}
{% endmacro %}
{% macro closed_path(points, vb_width, vb_height) %}
{{ path(points, vb_width, vb_height) }} L {{ vb_width + 2*margin }} {{ vb_height + 2*margin }} L {{ 2*margin }} {{ vb_height + 2*margin }} Z
{% endmacro %}
<svg viewBox="0 0 {{ vb_width }} {{ vb_height }}" preserveAspectRatio="none">
<path d="{{ path(data_points, vb_height) }}" stroke="blue" fill="none" />
{{ circles(data_points, vb_height) }}
</svg>
<div id="popover" class="hidden bg-white border border-gray-300 p-2 z-10">
<!-- Popover content will be dynamically inserted here -->
</div>

View File

@@ -100,6 +100,9 @@
exercises=exercises, exercises=exercises,
has_value=False) }} has_value=False) }}
</div> </div>
<div id="exercise-progress-{{ workout['PersonId'] }}" class="mx-5">
</div>
</div> </div>
</div> </div>
</div> </div>