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