Create endpoint that returns graphs of an overview of users workouts (Needs to be refactored)

This commit is contained in:
Peter Stockings
2023-10-21 18:35:03 +11:00
parent 536e0b28bc
commit 3e110a7d4f
2 changed files with 152 additions and 0 deletions

144
app.py
View File

@@ -1,3 +1,4 @@
from collections import defaultdict
from flask_basicauth import BasicAuth
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
@@ -213,6 +214,149 @@ def create_workout(user_id):
return f'Added {humanize.naturaldelta(workout.duration)} session.', 201
@app.route('/user/<int:user_id>/workouts/graph', methods=['GET'])
def graph_user_workouts(user_id):
user = User.query.get(user_id)
workouts = user.workouts
earliest_workout_date = min([workout.created_at.date()
for workout in workouts])
most_recent_workout_date = max(
[workout.created_at.date() for workout in workouts])
start_date = request.args.get(
'start_date', default=earliest_workout_date, type=toDate)
end_date = request.args.get(
'end_date', default=most_recent_workout_date, type=toDate)
attributes = request.args.getlist(
'attributes', type=str)
period = request.args.get('period', default='day', type=str)
return plot_averaged_attributes(workouts, start_date, end_date, period, attributes)
def daterange(start_date, end_date, delta=timedelta(days=1)):
"""Helper generator to iterate over date ranges."""
curr_date = start_date
while curr_date < end_date:
yield curr_date
curr_date += delta
def average_workout_attributes_per_period(workouts, start_date, end_date, period, attributes):
"""
Returns a dictionary of averaged attributes for workouts within each given period
between start_date and end_date.
Parameters:
- workouts (list): List of Workout objects.
- start_date, end_date: Date range to consider.
- period (str): 'day', 'week', or 'month'.
- attributes (list): List of attribute names to average.
Returns:
- Dictionary: A nested dictionary where keys are dates representing the start of each period,
and the values are dictionaries with averaged attributes for that period.
"""
if period == "day":
delta = timedelta(days=1)
elif period == "week":
delta = timedelta(weeks=1)
elif period == "month":
# approximating month as 4 weeks for simplicity
delta = timedelta(weeks=4)
else:
raise ValueError(f"Invalid period: {period}")
results = defaultdict(lambda: defaultdict(float))
for start_period in daterange(start_date, end_date, delta):
end_period = start_period + delta
filtered_workouts = [
w for w in workouts if start_period <= w.created_at.date() < end_period]
for attribute in attributes:
if hasattr(Workout, attribute):
valid_values = [getattr(w, attribute) for w in filtered_workouts if getattr(
w, attribute) is not None]
if valid_values:
average = sum(valid_values) / len(valid_values)
results[start_period][attribute] = average
else:
results[start_period][attribute] = 0 # None
results[start_period]['workout_count'] = len(
filtered_workouts)
return dict(results)
def create_user_graph(x_values, y_data, filename, x_label='Time'):
"""Create a graph for given x-values and y-values.
Parameters:
- x_values: A list of x-values (common for all graphs).
- y_data: A dictionary where key is y_label and value is list of y-values.
- filename: Name for the generated file.
- x_label: Label for x-axis.
Returns:
- Flask Response object containing the image of the graph.
"""
fig, ax = plt.subplots()
# Plotting multiple lines
for y_label, y_values in y_data.items():
ax.plot(x_values, y_values, label=y_label)
ax.set_xlabel(x_label)
ax.legend() # Show legend to differentiate between multiple attributes
ax.xaxis.set_major_formatter(mdates.DateFormatter("%d/%m"))
ax.set_ylim(bottom=0)
# Save the graph to a bytes buffer
buffer = io.BytesIO()
plt.savefig(buffer, format='png', transparent=True, bbox_inches='tight')
buffer.seek(0)
# Create a response object with the graph image
response = make_response(buffer.getvalue())
response.headers['Content-Type'] = 'image/png'
response.headers['Content-Disposition'] = f'attachment; filename={filename}.png'
return response
def plot_averaged_attributes(workouts_list, start_date, end_date, period, attributes):
"""Creates a graph for averaged attributes over a period.
Parameters:
- start_date, end_date: Date range to consider.
- period (str): 'day', 'week', or 'month'.
- attributes (list): A list of attribute names to plot.
Returns:
- Flask Response object containing the image of the graph.
"""
# Fetching the data
averaged_attributes = average_workout_attributes_per_period(
workouts_list, start_date, end_date, period, attributes)
# Extracting x_values and y_values
x_values = list(averaged_attributes.keys())
y_data = {}
for attribute in attributes:
y_data[attribute] = [averaged_attributes[date][attribute]
for date in x_values]
# Creating the graph
return create_user_graph(x_values, y_data, filename=f"average_attributes_over_{period}")
@app.route('/user/<int:user_id>/workout/<int:workout_id>/<string:graph_type>', methods=['GET'])
def workout(user_id, workout_id, graph_type):
workout = Workout.query.filter_by(user_id=user_id, id=workout_id) \

View File

@@ -71,9 +71,17 @@
</span>
</button>
</h2>
<div class="!visible collapse p-4 hidden" id="workouts-list-accordion-{{ user.id }}">
{{ render_partial('partials/calendar.html', calendar_month=user.calendar_month, user_id = user.id) }}
<img src="{{ url_for('graph_user_workouts', user_id=user.id, period='week', attributes=['workout_count']) }}"
loading="lazy" alt="No image" class="mx-auto">
<img src="{{ url_for('graph_user_workouts', user_id=user.id, period='month', attributes=['duration']) }}"
loading="lazy" alt="No image" class="mx-auto">
<div id="workouts-list-wrapper-for-user-{{ user.id }}" class="mt-5 pl-2">{{
render_partial('partials/workouts_list_fragment.html',
workouts=workouts[:7], user_id = user.id) }}</div>