Fix forecast chart continuity and hover

This commit is contained in:
Peter Stockings
2026-09-06 13:59:50 +10:00
parent e2c4196792
commit cada28b54b
2 changed files with 41 additions and 14 deletions
+14 -3
View File
@@ -84,10 +84,14 @@ def build_forecast(values, dates, horizon_weeks=8, metric='E1RM'):
horizon_days = horizon_weeks * 7
future_x = np.arange(0, horizon_days + 1, 7, dtype=float) + x[-1]
future_dates = [last_date + timedelta(days=int(day - x[-1])) for day in future_x]
future_values = intercept + slope * future_x
# Anchor the projection to the most recent observed value. A pure
# regression line can sit noticeably above or below the latest workout,
# which makes the forecast appear to jump when it starts.
future_values = observed_values[-1] + slope * (future_x - x[-1])
# A 95% prediction band, widened for points further from the observations.
prediction_error = 1.96 * residual_std * np.sqrt(
# A restrained 90% prediction band, widened for points further from the
# observations so the uncertainty remains readable on the small chart.
prediction_error = 1.645 * residual_std * np.sqrt(
1 + (1 / len(observed_values)) + ((future_x - mean_x) ** 2 / sum_squared_x)
)
lower_values = np.maximum(future_values - prediction_error, 0)
@@ -212,6 +216,7 @@ def get_exercise_graph_model(title, estimated_1rm, repetitions, weight, start_da
forecast_points = []
forecast_band_lower = []
forecast_band_upper = []
forecast_labels = []
forecast_start_position = None
if forecast.get('available'):
forecast_start_position = round((observed_max_date - observed_min_date).days / total_span, 3)
@@ -220,6 +225,11 @@ def get_exercise_graph_model(title, estimated_1rm, repetitions, weight, start_da
forecast_points.append((round(((point['value'] - min_e1rm) / e1rm_range) * vb_height, 1), position))
forecast_band_lower.append((round(((point['lower'] - min_e1rm) / e1rm_range) * vb_height, 1), position))
forecast_band_upper.append((round(((point['upper'] - min_e1rm) / e1rm_range) * vb_height, 1), position))
forecast_labels.append((
position,
f"{point['date'].strftime('%d %b %y')} · Projected E1RM: {point['value']}kg "
f"(range {point['lower']}–{point['upper']}kg)"
))
# Return exercise data with SVG dimensions and data points
return {
@@ -243,6 +253,7 @@ def get_exercise_graph_model(title, estimated_1rm, repetitions, weight, start_da
'forecast_points': forecast_points,
'forecast_band_lower': forecast_band_lower,
'forecast_band_upper': forecast_band_upper,
'forecast_labels': forecast_labels,
'forecast_start_position': forecast_start_position
}