diff --git a/templates/partials/svg_line_graph.html b/templates/partials/svg_line_graph.html
new file mode 100644
index 0000000..23d5a0b
--- /dev/null
+++ b/templates/partials/svg_line_graph.html
@@ -0,0 +1,88 @@
+{% set stroke_width = 4 %}
+{% set margin = 2 %}
+
+{% macro path(data_points, vb_height) %}
+{% for value, position in data_points %}
+{% set x = (position * vb_width)+margin %}
+{% set y = (vb_height - value)+margin %}
+{% if loop.first %}M{{ x | int }} {{ y | int }}{% else %} L{{ x | int }} {{ y | int }}{% endif %}
+{% endfor %}
+{% endmacro %}
+
+{% macro path_best_fit(best_fit_points, vb_height) %}
+{% for value, position in best_fit_points %}
+{% set x = (position * vb_width)+margin %}
+{% set y = (vb_height - value)+margin %}
+{% if loop.first %}M{{ x | int }} {{ y | int }}{% else %} L{{ x | int }} {{ y | int }}{% endif %}
+{% endfor %}
+{% endmacro %}
+
+{% macro circles(data_points, color) %}
+ {% for i in range(data_points|length) %}
+ {% set current_value, current_position = data_points[i] %}
+ {% set prev_value = data_points[i - 1][0] if i > 0 else None %}
+ {% set next_value = data_points[i + 1][0] if i < data_points|length - 1 else None %}
+ {# Plot the circle only if the current value is different from both previous and next values #}
+ {% if next_value != prev_value or (next_value == prev_value and next_value != current_value) %}
+ {% set x=(current_position * vb_width) + margin %}
+ {% set y=(vb_height - current_value) + margin %}
+
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
+
+ {% macro plot_line(points, color) %}
+
+ {{ circles(points, color) }}
+ {% endmacro %}
+
+
+ {% macro random_int() %}{% for n in [0,1,2,3,4,5] %}{{ [0,1,2,3,4,5,6,7,8,9]|random }}{% endfor %}{% endmacro %}
+
+
+ {% set parts = [random_int()] %}
+ {% set unique_id = parts|join('-') %}
+
+
+
+
+
+
{{ title }}
+
+
+ {% for plot in plots %}
+
+
+
{{ plot.label }}
+
+ {% endfor %}
+
+
+
\ No newline at end of file
diff --git a/utils.py b/utils.py
index 7f59d3f..88b43b0 100644
--- a/utils.py
+++ b/utils.py
@@ -1,7 +1,8 @@
+import colorsys
from datetime import datetime, date, timedelta
+import random
import numpy as np
-import json
-
+import pandas as pd
def get_workouts(topsets):
# Get all unique workout_ids (No duplicates)
@@ -305,4 +306,136 @@ def get_exercise_graph_model(title, estimated_1rm, repetitions, weight, start_da
'plots': [repetitions, weight, estimated_1rm],
'best_fit_points': best_fit_points,
'plot_labels': plot_labels
- }
\ No newline at end of file
+ }
+
+def get_workout_counts(workouts, period='week'):
+ # Convert to DataFrame
+ df = pd.DataFrame(workouts)
+
+ # Convert 'StartDate' to datetime
+ df['StartDate'] = pd.to_datetime(df['StartDate'])
+
+ # Determine the range of periods to cover
+ min_date = df['StartDate'].min()
+ max_date = pd.Timestamp(datetime.now())
+
+ # Generate a complete range of periods
+ freq = 'W-MON' if period == 'week' else 'MS'
+ period_range = pd.date_range(start=min_date, end=max_date, freq=freq)
+
+ # Initialize a dictionary to store workout counts and person names
+ workout_counts = {
+ person_id: {
+ "PersonName": person_name,
+ "PRCounts": {p: 0 for p in period_range}
+ } for person_id, person_name in df[['PersonId', 'PersonName']].drop_duplicates().values
+ }
+
+ # Process the workouts
+ for person_id, person_data in workout_counts.items():
+ person_df = df[df['PersonId'] == person_id]
+
+ for period_start in person_data["PRCounts"]:
+ period_end = period_start + pd.DateOffset(weeks=1) if period == 'week' else period_start + pd.DateOffset(months=1)
+ period_workouts = person_df[(person_df['StartDate'] >= period_start) & (person_df['StartDate'] < period_end)]
+ person_data["PRCounts"][period_start] = len(period_workouts)
+
+ return workout_counts
+
+def count_prs_over_time(workouts, period='week'):
+ # Convert to DataFrame
+ df = pd.DataFrame(workouts)
+
+ # Convert 'StartDate' to datetime
+ df['StartDate'] = pd.to_datetime(df['StartDate'])
+
+ # Determine the range of periods to cover
+ min_date = df['StartDate'].min()
+ max_date = pd.Timestamp(datetime.now())
+
+ # Generate a complete range of periods
+ period_range = pd.date_range(start=min_date, end=max_date, freq='W-MON' if period == 'week' else 'MS')
+
+ # Initialize a dictionary to store PR counts and names
+ pr_counts = {
+ person_id: {
+ "PersonName": person_name,
+ "PRCounts": {p: 0 for p in period_range}
+ } for person_id, person_name in df[['PersonId', 'PersonName']].drop_duplicates().values
+ }
+
+ # Process the workouts
+ for person_id, person_data in pr_counts.items():
+ person_df = df[df['PersonId'] == person_id]
+
+ for period_start in person_data["PRCounts"]:
+ period_end = period_start + pd.DateOffset(weeks=1) if period == 'week' else period_start + pd.DateOffset(months=1)
+ period_workouts = person_df[(person_df['StartDate'] >= period_start) & (person_df['StartDate'] < period_end)]
+
+ for exercise_id in period_workouts['ExerciseId'].unique():
+ exercise_max = period_workouts[period_workouts['ExerciseId'] == exercise_id]['Estimated1RM'].max()
+
+ # Check if this is a PR
+ previous_max = person_df[(person_df['StartDate'] < period_start) &
+ (person_df['ExerciseId'] == exercise_id)]['Estimated1RM'].max()
+
+ if pd.isna(previous_max) or exercise_max > previous_max:
+ person_data["PRCounts"][period_start] += 1
+
+ return pr_counts
+
+
+def get_weekly_pr_graph_model(title, weekly_pr_data):
+ # Assuming weekly_pr_data is in the format {1: {"PersonName": "Alice", "PRCounts": {Timestamp('2022-01-01', freq='W-MON'): 0, ...}}, 2: {...}, ...}
+
+ # Find the overall date range for all users
+ all_dates = [date for user_data in weekly_pr_data.values() for date in user_data["PRCounts"].keys()]
+ min_date, max_date = min(all_dates), max(all_dates)
+ total_span = (max_date - min_date).days or 1
+ relative_positions = [(date - min_date).days / total_span for date in all_dates]
+
+ # Calculate viewBox dimensions
+ max_pr_count = max(max(user_data["PRCounts"].values()) for user_data in weekly_pr_data.values()) or 1
+ vb_width, vb_height = total_span, max_pr_count
+ vb_width *= 200 / vb_width # Scale to 200px width
+ vb_height *= 75 / vb_height # Scale to 75px height
+
+ plots = []
+ colors = get_distinct_colors(len(weekly_pr_data.items()))
+ for count, (user_id, user_data) in enumerate(weekly_pr_data.items()):
+ pr_counts = user_data["PRCounts"]
+ person_name = user_data["PersonName"]
+
+ values = pr_counts.values()
+ min_value, max_value = min(values), max(values)
+ value_range = (max_value - min_value) or 1
+
+ values_scaled = [((value - min_value) / value_range) * vb_height for value in values]
+ plot_points = list(zip(values_scaled, relative_positions))
+
+ # Create a plot for each user
+ plot = {
+ 'label': person_name, # Use PersonName instead of User ID
+ 'color': colors[count],
+ 'points': plot_points
+ }
+ plots.append(plot)
+
+ # Return workout data with SVG dimensions and data points
+ return {
+ 'title': title,
+ 'vb_width': vb_width,
+ 'vb_height': vb_height,
+ 'plots': plots
+ }
+
+def get_distinct_colors(n):
+ colors = []
+ for i in range(n):
+ # Divide the color wheel into n parts
+ hue = i / n
+ # Convert HSL (Hue, Saturation, Lightness) to RGB and then to a Hex string
+ rgb = colorsys.hls_to_rgb(hue, 0.6, 0.4) # Fixed lightness and saturation
+ hex_color = '#{:02x}{:02x}{:02x}'.format(int(rgb[0]*255), int(rgb[1]*255), int(rgb[2]*255))
+ colors.append(hex_color)
+ return colors
\ No newline at end of file