diff --git a/app/report_gen/page_1.html b/app/report_gen/page_1.html index d7f2297..ffd43fb 100644 --- a/app/report_gen/page_1.html +++ b/app/report_gen/page_1.html @@ -26,7 +26,7 @@

- {{ first_name|upper }} + {{ name|upper }}

{{ surname|upper }} diff --git a/app/report_gen/page_8.html b/app/report_gen/page_8.html index 7dfd679..7e0e15d 100644 --- a/app/report_gen/page_8.html +++ b/app/report_gen/page_8.html @@ -29,10 +29,6 @@
-

- Personalized Heart Rate Zones -

-
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" },