WIP: When selecting an exercise on new workout view, render a graph of exercise progress for the active user
This commit is contained in:
47
app.py
47
app.py
@@ -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
28
db.py
@@ -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)
|
||||||
@@ -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 %}
|
||||||
42
templates/partials/sparkline.html
Normal file
42
templates/partials/sparkline.html
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user