Good good progress

This commit is contained in:
bolade
2025-11-21 14:15:29 +01:00
parent 4028b7c626
commit dbee12341a
7 changed files with 80 additions and 103 deletions
+1 -1
View File
@@ -26,7 +26,7 @@
<!-- Name and Date Section -->
<div class="text-right mt-16">
<h2 class="text-4xl font-bold tracking-wider mb-2">
{{ first_name|upper }}
{{ name|upper }}
</h2>
<h2 class="text-4xl font-bold tracking-wider mb-6">
{{ surname|upper }}
-4
View File
@@ -29,10 +29,6 @@
<!-- Personalized Heart Rate Zones Section -->
<div class="mb-8">
<h3 class="text-xl font-bold text-black mb-6 text-center">
Personalized Heart Rate Zones
</h3>
<!-- Heart Rate Zones Table -->
<div class="flex justify-center">
<img
+21 -31
View File
@@ -804,42 +804,42 @@ class ContextGenerator:
)
zone_df = self.pnoe_df[mask]
# HR BPM Range - match notebook exactly
hr_bpm_str = f"{int(start)}-{int(end)} bpm"
if not zone_df.empty:
# Speed calculation
# Speed calculation - match notebook exactly
speed_series = zone_df[zone_df["Speed"] > 0.1]["Speed"]
if not speed_series.empty:
min_speed = speed_series.min()
max_speed = speed_series.max()
if abs(min_speed - max_speed) < 0.1:
speed_str = f"{min_speed:.1f}mph\n2% Incline"
speed_str = f"{min_speed:.1f} mph\n2% Incline"
else:
speed_str = f"{min_speed:.1f}-{max_speed:.1f}mph\n2% Incline"
speed_str = f"{min_speed:.1f}-{max_speed:.1f} mph\n2% Incline"
# Pace calculation (max speed -> min pace, min speed -> max pace)
min_pace_m, min_pace_s = speed_to_pace(max_speed)
max_pace_m, max_pace_s = speed_to_pace(min_speed)
if min_pace_m == max_pace_m and min_pace_s == max_pace_s:
pace_str = f"{min_pace_m}:{min_pace_s:02d}min/km Pace"
pace_str = f"{min_pace_m}:{min_pace_s:02d} min/km Pace"
else:
pace_str = (
f"{max_pace_m}:{max_pace_s:02d}-{min_pace_m}:{min_pace_s:02d}\n"
f"min/km Pace"
)
pace_str = f"{max_pace_m}:{max_pace_s:02d}-{min_pace_m}:{min_pace_s:02d}\nmin/km Pace"
else:
speed_str = "-\n2% Incline"
pace_str = "-"
# Calories (using raw EE)
# Calories (using raw EE) - match notebook exactly
avg_cals = zone_df["EE(kcal/min)"].mean()
calories_str = f"Avg:\n{avg_cals:.1f}kcals/minute"
calories_str = f"Avg:\n{avg_cals:.1f} kcals/minute"
# Carb utilization (g/min)
# Carb utilization (g/min) - match notebook exactly
avg_carbs_g = zone_df["CHO"].mean() / 4
carb_str = f"Avg: {avg_carbs_g:.1f}g/min\nCarb Utilization"
# Breathing
# Breathing - match notebook exactly
avg_breaths = zone_df["BF(bpm)_smoothed"].mean()
breath_str = (
f"Avg: {int(avg_breaths)} breaths\n{ideal_breath_ranges[i]}"
@@ -854,7 +854,7 @@ class ContextGenerator:
zone_data.append(
{
"zone_name": name,
"hr_bpm": f"{int(start)}-{int(end)}bpm",
"hr_bpm": hr_bpm_str,
"speed": speed_str,
"pace": pace_str,
"calories": calories_str,
@@ -1246,18 +1246,18 @@ class ContextGenerator:
hr_zones_columns = ["Zone 1", "Zone 2", "Zone 3", "Zone 4", "Zone 5"]
hr_zones_data = [
[
"Improves health and recovery capacity",
"Improves endurance and fat burning",
"Improves Aerobic fitness",
"Improves maximum performance capacity",
"Develops maximum performance and speed",
"Improves health and\nrecovery capacity",
"Improves endurance\nand fat burning",
"Improves Aerobic\nfitness",
"Improves maximum\nperformance capacity",
"Develops maximum\nperformance and speed",
],
[
"55-65% of Max Heart Rate",
"65-75% of Max Heart Rate",
"80-85% of Max Heart Rate",
"85-88% of Max Heart Rate",
"90% of Max Heart Rate",
"90%+ of Max Heart Rate",
],
[zone_metrics["zones"][i]["hr_bpm"] for i in range(5)],
[zone_metrics["zones"][i]["speed"] for i in range(5)],
@@ -1266,22 +1266,12 @@ class ContextGenerator:
[zone_metrics["zones"][i]["carb"] for i in range(5)],
[zone_metrics["zones"][i]["breathing"] for i in range(5)],
]
hr_zones_colors = [
["#ffffff"] * 5,
["#ffffff"] * 5,
["#ffcdd2", "#ffcdd2", "#fff9c4", "#c8e6c9", "#c8e6c9"],
["#ffffff"] * 5,
["#ffffff"] * 5,
["#ffffff"] * 5,
["#ffffff"] * 5,
["#ffcdd2", "#ffcdd2", "#fff9c4", "#c8e6c9", "#c8e6c9"],
]
# Colors are now handled directly in the graph generator to match notebook
# No need to pass cell_colors
contexts["page_8"]["hr_zones_table"] = (
graph_generator.generate_heart_rate_zones_table(
data=hr_zones_data,
columns=hr_zones_columns,
cell_colors=hr_zones_colors,
save_as_base64=True,
)
)
+36 -45
View File
@@ -1465,67 +1465,60 @@ class GraphGenerator:
Args:
data: List of rows (each row is a list of values)
columns: List of column headers (Zone 1-5)
cell_colors: Optional matrix of cell colors
cell_colors: Optional matrix of cell colors (IGNORED - using notebook colors)
save_as_base64: If True, return base64 string
Returns:
Base64 string or file path
"""
import io
import textwrap
# Optimal sizing for HR Zones table (5 columns, 8 rows)
fig, ax = plt.subplots(figsize=(14, 8))
# Optimal sizing for HR Zones table (5 columns, 8 rows) - match notebook exactly
fig, ax = plt.subplots(figsize=(12, 8))
ax.axis("off")
# Column widths - slightly wider for first column which has longer text
# col_widths = [0.24, 0.19, 0.19, 0.19, 0.19]
# Data comes pre-formatted with newlines from context_generator - use as-is
# No text wrapping needed
# Wrap text in cells for better readability
wrapped_data = []
for row in data:
wrapped_row = []
for i, cell in enumerate(row):
cell_text = str(cell)
# First column needs more wrapping space
wrap_width = 35 if i == 0 else 25
wrapped_text = "\n".join(textwrap.wrap(cell_text, width=wrap_width))
wrapped_row.append(wrapped_text)
wrapped_data.append(wrapped_row)
# Create table
# Create table without rowLabels - match notebook exactly
table = ax.table(
cellText=wrapped_data,
cellText=data,
colLabels=columns,
cellLoc="center",
loc="center",
colColours=["#4dd0e1"] * len(columns),
# colWidths=col_widths,
cellLoc="center",
)
# Style the table
# Style the table - match notebook exactly
table.auto_set_font_size(False)
table.set_fontsize(11)
table.scale(1, 2.0)
table.set_fontsize(10)
table.scale(1, 3.5) # Increased vertical scale for multi-line text
# Apply cell colors
if cell_colors:
for i, row_colors in enumerate(cell_colors):
for j, color in enumerate(row_colors):
if color and j < len(columns):
cell = table[(i + 1, j)]
cell.set_facecolor(color)
# Header row styling
for j, label in enumerate(columns):
cell = table[(0, j)]
cell.set_facecolor("#7dd3fc") # cyan-300
cell.set_text_props(weight="bold")
# Style all cells
for (row, col), cell in table.get_celld().items():
if row == 0:
cell.set_text_props(weight="bold", fontsize=13)
cell.set_edgecolor("#333333")
cell.set_linewidth(1.5)
else:
cell.set_edgecolor("#666666")
cell.set_linewidth(0.8)
cell.set_text_props(fontsize=10)
# Row specific styling - match notebook colors exactly
colors = ["#fecaca", "#fecaca", "#fef08a", "#bbf7d0", "#bbf7d0"]
# HR BPM row is at index 2 (0-based in data) -> row 3 in table (0 is header)
for j in range(len(columns)):
cell = table[(3, j)]
cell.set_facecolor(colors[j])
cell.set_text_props(weight="bold")
# Breathing row is at index 7 -> row 8 in table
for j in range(len(columns)):
cell = table[(8, j)]
cell.set_facecolor(colors[j])
cell.set_text_props(weight="bold")
# Add title matching notebook
plt.title(
"Personalized Heart Rate Zones", fontsize=16, fontweight="bold", pad=5
)
plt.tight_layout()
if save_as_base64:
buf = io.BytesIO()
@@ -1535,7 +1528,6 @@ class GraphGenerator:
bbox_inches="tight",
dpi=300,
facecolor="white",
pad_inches=0.1,
)
plt.close(fig)
buf.seek(0)
@@ -1549,7 +1541,6 @@ class GraphGenerator:
bbox_inches="tight",
dpi=300,
facecolor="white",
pad_inches=0.1,
)
plt.close(fig)
return str(output_path)
+22 -22
View File
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 21,
"id": "63f43af5",
"metadata": {},
"outputs": [],
@@ -16,7 +16,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": 22,
"id": "97da3d1c",
"metadata": {},
"outputs": [],
@@ -26,7 +26,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": 23,
"id": "b0ee2af1",
"metadata": {},
"outputs": [
@@ -42,7 +42,7 @@
"name": "stderr",
"output_type": "stream",
"text": [
"/tmp/ipykernel_103354/3076306744.py:3: FutureWarning: errors='ignore' is deprecated and will raise in a future version. Use to_numeric without passing `errors` and catch exceptions explicitly instead\n",
"/tmp/ipykernel_150441/3076306744.py:3: FutureWarning: errors='ignore' is deprecated and will raise in a future version. Use to_numeric without passing `errors` and catch exceptions explicitly instead\n",
" df = df.apply(pd.to_numeric, errors='ignore')\n"
]
}
@@ -72,7 +72,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 24,
"id": "99116a35",
"metadata": {},
"outputs": [],
@@ -84,7 +84,7 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 25,
"id": "fbd292c3",
"metadata": {},
"outputs": [
@@ -102,7 +102,7 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 26,
"id": "4c439b2c",
"metadata": {},
"outputs": [
@@ -167,7 +167,7 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 27,
"id": "a565f1b3",
"metadata": {},
"outputs": [
@@ -237,7 +237,7 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 28,
"id": "470e871e",
"metadata": {},
"outputs": [
@@ -431,7 +431,7 @@
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": 29,
"id": "0ab6f0b0",
"metadata": {},
"outputs": [
@@ -577,7 +577,7 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": 30,
"id": "ef8bc7ac",
"metadata": {},
"outputs": [
@@ -638,7 +638,7 @@
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": 31,
"id": "06244aa2",
"metadata": {},
"outputs": [
@@ -753,7 +753,7 @@
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": 32,
"id": "8a1878a0",
"metadata": {},
"outputs": [
@@ -832,7 +832,7 @@
},
{
"cell_type": "code",
"execution_count": 13,
"execution_count": 33,
"id": "7361fb05",
"metadata": {},
"outputs": [
@@ -893,7 +893,7 @@
},
{
"cell_type": "code",
"execution_count": 14,
"execution_count": 34,
"id": "c89478ff",
"metadata": {},
"outputs": [
@@ -953,7 +953,7 @@
},
{
"cell_type": "code",
"execution_count": 15,
"execution_count": 35,
"id": "1db16040",
"metadata": {},
"outputs": [
@@ -1028,7 +1028,7 @@
},
{
"cell_type": "code",
"execution_count": 16,
"execution_count": 36,
"id": "c8ad6076",
"metadata": {},
"outputs": [
@@ -1109,7 +1109,7 @@
},
{
"cell_type": "code",
"execution_count": 17,
"execution_count": 37,
"id": "25327cc1",
"metadata": {},
"outputs": [
@@ -1415,7 +1415,7 @@
},
{
"cell_type": "code",
"execution_count": 24,
"execution_count": 38,
"id": "c46b53f0",
"metadata": {},
"outputs": [
@@ -1731,7 +1731,7 @@
},
{
"cell_type": "code",
"execution_count": 19,
"execution_count": 39,
"id": "84addc63",
"metadata": {},
"outputs": [
@@ -1869,7 +1869,7 @@
},
{
"cell_type": "code",
"execution_count": 20,
"execution_count": 40,
"id": "f324fe94",
"metadata": {},
"outputs": [
@@ -2066,7 +2066,7 @@
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"display_name": "report-generation",
"language": "python",
"name": "python3"
},