diff --git a/app.py b/app.py index a2ab33f..15803b1 100644 --- a/app.py +++ b/app.py @@ -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//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//workout//', methods=['GET']) def workout(user_id, workout_id, graph_type): workout = Workout.query.filter_by(user_id=user_id, id=workout_id) \ diff --git a/templates/workouts_list.html b/templates/workouts_list.html index 415aae8..a18b12a 100644 --- a/templates/workouts_list.html +++ b/templates/workouts_list.html @@ -71,9 +71,17 @@ +