![]()
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,
)
)
diff --git a/app/services/graph_generator.py b/app/services/graph_generator.py
index 4c9f31a..441bc78 100644
--- a/app/services/graph_generator.py
+++ b/app/services/graph_generator.py
@@ -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)
diff --git a/notebooks/graphs.ipynb b/notebooks/graphs.ipynb
index 7d12178..c418f07 100644
--- a/notebooks/graphs.ipynb
+++ b/notebooks/graphs.ipynb
@@ -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"
},