Good good progress
This commit is contained in:
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user