Files
workout/utils.py
2025-02-01 21:26:52 +11:00

142 lines
5.5 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: pd.DataFrame, title: str) -> str:
"""
Analyzes the DataFrame and generates an appropriate Plotly visualization.
Returns the Plotly figure as a div string.
Optimized for speed.
"""
if df.empty:
return "<p>No data available to plot.</p>"
num_columns = len(df.columns)
# Dictionary-based lookup for faster decision-making
plot_funcs = {
1: lambda: px.histogram(df, x=df.columns[0], title=title)
if pd.api.types.is_numeric_dtype(df.iloc[:, 0]) else px.bar(df, x=df.columns[0], title=title),
2: lambda: px.scatter(df, x=df.columns[0], y=df.columns[1], title=title)
if pd.api.types.is_numeric_dtype(df.iloc[:, 0]) and pd.api.types.is_numeric_dtype(df.iloc[:, 1])
else px.bar(df, x=df.columns[0], y=df.columns[1], title=title)
}
# Select plot function based on column count
fig = plot_funcs.get(num_columns, lambda: px.imshow(df.corr(numeric_only=True), text_auto=True, title=title))()
# Use static rendering for speed
return pio.to_html(fig, full_html=False, include_plotlyjs=False, config={'staticPlot': True})
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)