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 "
No data available to plot.
" 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='cdn', 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)