Files
workout/utils.py
Peter Stockings f70438e4e4 Refactor dashboard
2025-01-27 14:46:20 +11:00

147 lines
5.6 KiB
Python

import colorsys
from datetime import datetime, date, timedelta
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.io as pio
def convert_str_to_date(date_str, format='%Y-%m-%d'):
try:
return datetime.strptime(date_str, format).date()
except ValueError:
return None
except TypeError:
return None
def get_exercise_graph_model(title, estimated_1rm, repetitions, weight, start_dates, messages, epoch, person_id, exercise_id, min_date=None, max_date=None):
# Precompute ranges
min_date, max_date = min(start_dates), max(start_dates)
total_span = (max_date - min_date).days or 1
min_e1rm, max_e1rm = min(estimated_1rm), max(estimated_1rm)
min_reps, max_reps = min(repetitions), max(repetitions)
min_weight, max_weight = min(weight), max(weight)
e1rm_range = max_e1rm - min_e1rm or 1
reps_range = max_reps - min_reps or 1
weight_range = max_weight - min_weight or 1
# Calculate viewBox dimensions
vb_width, vb_height = total_span, e1rm_range
vb_width *= 200 / vb_width # Scale to 200px width
vb_height *= 75 / vb_height # Scale to 75px height
# Use NumPy arrays for efficient scaling
relative_positions = np.array([(date - min_date).days / total_span for date in start_dates])
estimated_1rm_scaled = ((np.array(estimated_1rm) - min_e1rm) / e1rm_range) * vb_height
repetitions_scaled = ((np.array(repetitions) - min_reps) / reps_range) * vb_height
weight_scaled = ((np.array(weight) - min_weight) / weight_range) * vb_height
# Calculate slope and line of best fit
slope_kg_per_day = e1rm_range / total_span
best_fit_formula = {
'kg_per_week': round(slope_kg_per_day * 7, 1),
'kg_per_month': round(slope_kg_per_day * 30, 1)
}
best_fit_points = []
try:
if len(relative_positions) > 1: # Ensure there are enough points for polyfit
# Calculate line of best fit using NumPy
m, b = np.polyfit(relative_positions, estimated_1rm_scaled, 1)
y_best_fit = m * relative_positions + b
best_fit_points = list(zip(y_best_fit.tolist(), relative_positions.tolist()))
else:
raise ValueError("Not enough data points for polyfit")
except (np.linalg.LinAlgError, ValueError) as e:
# Handle cases where polyfit fails
best_fit_points = []
m, b = 0, 0
# Prepare data for plots
repetitions_data = {
'label': 'Reps',
'color': '#388fed',
'points': list(zip(repetitions_scaled.tolist(), relative_positions.tolist()))
}
weight_data = {
'label': 'Weight',
'color': '#bd3178',
'points': list(zip(weight_scaled.tolist(), relative_positions.tolist()))
}
estimated_1rm_data = {
'label': 'E1RM',
'color': '#2ca02c',
'points': list(zip(estimated_1rm_scaled.tolist(), relative_positions.tolist()))
}
# Prepare plot labels
plot_labels = list(zip(relative_positions.tolist(), messages))
# Return exercise data with SVG dimensions and data points
return {
'title': title,
'vb_width': vb_width,
'vb_height': vb_height,
'plots': [repetitions_data, weight_data, estimated_1rm_data],
'best_fit_points': best_fit_points,
'best_fit_formula': best_fit_formula,
'plot_labels': plot_labels,
'epochs': ['Custom', '1M', '3M', '6M', 'All'],
'selected_epoch': epoch,
'person_id': person_id,
'exercise_id': exercise_id,
'min_date': min_date,
'max_date': max_date
}
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
def generate_plot(df, title):
"""
Analyzes the DataFrame and generates an appropriate Plotly visualization.
Returns the Plotly figure as a div string.
"""
if df.empty:
return "<p>No data available to plot.</p>"
num_columns = len(df.columns)
# Simple logic to decide plot type based on DataFrame structure
if num_columns == 1:
# Single column: perhaps a histogram or bar chart
column = df.columns[0]
if pd.api.types.is_numeric_dtype(df[column]):
fig = px.histogram(df, x=column, title=title)
else:
fig = px.bar(df, x=column, title=title)
elif num_columns == 2:
# Two columns: scatter plot or line chart
col1, col2 = df.columns
if pd.api.types.is_numeric_dtype(df[col1]) and pd.api.types.is_numeric_dtype(df[col2]):
fig = px.scatter(df, x=col1, y=col2, title=title)
else:
fig = px.bar(df, x=col1, y=col2, title=title)
else:
# More than two columns: heatmap or other complex plots
fig = px.imshow(df.corr(), text_auto=True, title=title)
# Convert Plotly figure to HTML div
plot_div = pio.to_html(fig, full_html=False)
return plot_div
def calculate_estimated_1rm(weight, repetitions):
# Ensure the inputs are numeric
if repetitions == 0: # Avoid division by zero
return 0
estimated_1rm = round((100 * int(weight)) / (101.3 - 2.67123 * repetitions), 0)
return int(estimated_1rm)