diff --git a/app/report_gen/page_11.html b/app/report_gen/page_11.html
index 7c79a95..0ef0c45 100644
--- a/app/report_gen/page_11.html
+++ b/app/report_gen/page_11.html
@@ -131,7 +131,7 @@
diff --git a/app/report_gen/page_8.html b/app/report_gen/page_8.html
index 52cb11f..63d7391 100644
--- a/app/report_gen/page_8.html
+++ b/app/report_gen/page_8.html
@@ -27,7 +27,7 @@
@@ -43,7 +43,7 @@
diff --git a/app/services/__pycache__/context_generator.cpython-312.pyc b/app/services/__pycache__/context_generator.cpython-312.pyc
index b291d4c..fb6bb68 100644
Binary files a/app/services/__pycache__/context_generator.cpython-312.pyc and b/app/services/__pycache__/context_generator.cpython-312.pyc differ
diff --git a/app/services/__pycache__/graph_generator.cpython-312.pyc b/app/services/__pycache__/graph_generator.cpython-312.pyc
index 6caa01b..f04f751 100644
Binary files a/app/services/__pycache__/graph_generator.cpython-312.pyc and b/app/services/__pycache__/graph_generator.cpython-312.pyc differ
diff --git a/app/services/__pycache__/report_generator.cpython-312.pyc b/app/services/__pycache__/report_generator.cpython-312.pyc
index 602f562..de346c8 100644
Binary files a/app/services/__pycache__/report_generator.cpython-312.pyc and b/app/services/__pycache__/report_generator.cpython-312.pyc differ
diff --git a/app/services/context_generator.py b/app/services/context_generator.py
index fd9a1f7..06195ab 100644
--- a/app/services/context_generator.py
+++ b/app/services/context_generator.py
@@ -1163,12 +1163,13 @@ class ContextGenerator:
]
]
- contexts["page_8"]["vo2_max_table"] = graph_generator.generate_table_image(
- data=vo2_max_data,
- columns=vo2_max_columns,
- cell_colors=vo2_max_colors,
- header_color="#4dd0e1",
- save_as_base64=True,
+ contexts["page_8"]["vo2_max_table"] = (
+ graph_generator.generate_vo2_max_table(
+ data=vo2_max_data,
+ columns=vo2_max_columns,
+ cell_colors=vo2_max_colors,
+ save_as_base64=True,
+ )
)
# Calculate zone metrics for the table
@@ -1211,11 +1212,10 @@ class ContextGenerator:
]
contexts["page_8"]["hr_zones_table"] = (
- graph_generator.generate_table_image(
+ graph_generator.generate_heart_rate_zones_table(
data=hr_zones_data,
columns=hr_zones_columns,
cell_colors=hr_zones_colors,
- header_color="#4dd0e1",
save_as_base64=True,
)
)
@@ -1288,12 +1288,13 @@ class ContextGenerator:
]
rhr_colors = [["#b2ebf2"] + ["#f5f5f5"] * 7]
- contexts["page_11"]["rhr_table"] = graph_generator.generate_table_image(
- data=rhr_data,
- columns=rhr_columns,
- cell_colors=rhr_colors,
- header_color="#4dd0e1",
- save_as_base64=True,
+ contexts["page_11"]["rhr_table"] = (
+ graph_generator.generate_resting_heart_rate_table(
+ data=rhr_data,
+ columns=rhr_columns,
+ cell_colors=rhr_colors,
+ save_as_base64=True,
+ )
)
# Pages 12-17
diff --git a/app/services/graph_generator.py b/app/services/graph_generator.py
index f8a0632..616b337 100644
--- a/app/services/graph_generator.py
+++ b/app/services/graph_generator.py
@@ -1306,41 +1306,33 @@ class GraphGenerator:
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
- def generate_table_image(
+ def generate_vo2_max_table(
self,
data: list[list],
columns: list[str],
- title: str = None,
- col_widths: list[float] = None,
cell_colors: list[list[str]] = None,
- header_color: str = "#4dd0e1",
save_as_base64: bool = True,
) -> str:
"""
- Generate a table as an image.
+ Generate VO2 Max table as an image with optimized sizing.
Args:
data: List of rows (each row is a list of values)
columns: List of column headers
- title: Optional title for the table
- col_widths: Optional list of column widths
- cell_colors: Optional matrix of cell colors (same shape as data)
- header_color: Color for the header row
+ cell_colors: Optional matrix of cell colors
save_as_base64: If True, return base64 string
Returns:
Base64 string or file path
"""
- # Calculate figure size based on rows and columns
- # Approximate height: header + rows
- height = (len(data) + 1) * 0.5 + (0.5 if title else 0)
- width = len(columns) * 2.5 if not col_widths else sum(col_widths) * 10
+ import io
- fig, ax = plt.subplots(figsize=(width, height))
+ # Fixed optimal sizing for VO2 Max table (8 columns, 1 data row)
+ fig, ax = plt.subplots(figsize=(16, 2.5))
ax.axis("off")
- if title:
- plt.title(title, pad=20, fontsize=14, fontweight="bold")
+ # Even column widths for VO2 Max table
+ col_widths = [1.0 / len(columns)] * len(columns)
# Create table
table = ax.table(
@@ -1348,43 +1340,248 @@ class GraphGenerator:
colLabels=columns,
cellLoc="center",
loc="center",
- colColours=[header_color] * len(columns),
+ colColours=["#4dd0e1"] * len(columns),
+ colWidths=col_widths,
)
# Style the table
table.auto_set_font_size(False)
- table.set_fontsize(10)
- table.scale(1, 1.5) # Increase row height
+ table.set_fontsize(11)
+ table.scale(1, 3.0)
- # Apply cell colors if provided
+ # Apply cell colors
if cell_colors:
for i, row_colors in enumerate(cell_colors):
for j, color in enumerate(row_colors):
- if color:
- # (row_idx, col_idx) - row_idx starts at 1 for data (0 is header)
+ if color and j < len(columns):
cell = table[(i + 1, j)]
cell.set_facecolor(color)
- # Bold headers
+ # Style all cells
for (row, col), cell in table.get_celld().items():
if row == 0:
- cell.set_text_props(weight="bold")
- cell.set_height(0.1)
-
- plt.tight_layout()
+ cell.set_text_props(weight="bold", fontsize=12)
+ cell.set_edgecolor("#333333")
+ cell.set_linewidth(1.5)
+ else:
+ cell.set_edgecolor("#666666")
+ cell.set_linewidth(1.0)
+ cell.set_text_props(fontsize=10)
if save_as_base64:
- import io
-
buf = io.BytesIO()
- plt.savefig(buf, format="png", bbox_inches="tight", dpi=300)
+ plt.savefig(
+ buf,
+ format="png",
+ bbox_inches="tight",
+ dpi=300,
+ facecolor="white",
+ pad_inches=0.1,
+ )
plt.close(fig)
buf.seek(0)
return base64.b64encode(buf.read()).decode("utf-8")
else:
output_path = (
- self.charts_dir / f"table_{pd.Timestamp.now().timestamp()}.png"
+ self.charts_dir / f"vo2_max_table_{pd.Timestamp.now().timestamp()}.png"
+ )
+ plt.savefig(
+ output_path,
+ bbox_inches="tight",
+ dpi=300,
+ facecolor="white",
+ pad_inches=0.1,
+ )
+ plt.close(fig)
+ return str(output_path)
+
+ def generate_heart_rate_zones_table(
+ self,
+ data: list[list],
+ columns: list[str],
+ cell_colors: list[list[str]] = None,
+ save_as_base64: bool = True,
+ ) -> str:
+ """
+ Generate Heart Rate Zones table as an image with optimized sizing.
+
+ 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
+ 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=(18, 12))
+ 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]
+
+ # 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
+ table = ax.table(
+ cellText=wrapped_data,
+ colLabels=columns,
+ cellLoc="center",
+ loc="center",
+ colColours=["#4dd0e1"] * len(columns),
+ colWidths=col_widths,
+ )
+
+ # Style the table
+ table.auto_set_font_size(False)
+ table.set_fontsize(11)
+ table.scale(1, 2.8)
+
+ # 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)
+
+ # 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)
+
+ if save_as_base64:
+ buf = io.BytesIO()
+ plt.savefig(
+ buf,
+ format="png",
+ bbox_inches="tight",
+ dpi=300,
+ facecolor="white",
+ pad_inches=0.1,
+ )
+ plt.close(fig)
+ buf.seek(0)
+ return base64.b64encode(buf.read()).decode("utf-8")
+ else:
+ output_path = (
+ self.charts_dir / f"hr_zones_table_{pd.Timestamp.now().timestamp()}.png"
+ )
+ plt.savefig(
+ output_path,
+ bbox_inches="tight",
+ dpi=300,
+ facecolor="white",
+ pad_inches=0.1,
+ )
+ plt.close(fig)
+ return str(output_path)
+
+ def generate_resting_heart_rate_table(
+ self,
+ data: list[list],
+ columns: list[str],
+ cell_colors: list[list[str]] = None,
+ save_as_base64: bool = True,
+ ) -> str:
+ """
+ Generate Resting Heart Rate table as an image with optimized sizing.
+
+ Args:
+ data: List of rows (each row is a list of values)
+ columns: List of column headers
+ cell_colors: Optional matrix of cell colors
+ save_as_base64: If True, return base64 string
+
+ Returns:
+ Base64 string or file path
+ """
+ import io
+
+ # Optimal sizing for RHR table (8 columns, 1 data row)
+ fig, ax = plt.subplots(figsize=(18, 2.5))
+ ax.axis("off")
+
+ # Even column widths
+ col_widths = [1.0 / len(columns)] * len(columns)
+
+ # Create table
+ table = ax.table(
+ cellText=data,
+ colLabels=columns,
+ cellLoc="center",
+ loc="center",
+ colColours=["#4dd0e1"] * len(columns),
+ colWidths=col_widths,
+ )
+
+ # Style the table
+ table.auto_set_font_size(False)
+ table.set_fontsize(11)
+ table.scale(1, 3.0)
+
+ # 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)
+
+ # Style all cells
+ for (row, col), cell in table.get_celld().items():
+ if row == 0:
+ cell.set_text_props(weight="bold", fontsize=12)
+ cell.set_edgecolor("#333333")
+ cell.set_linewidth(1.5)
+ else:
+ cell.set_edgecolor("#666666")
+ cell.set_linewidth(1.0)
+ cell.set_text_props(fontsize=10)
+
+ if save_as_base64:
+ buf = io.BytesIO()
+ plt.savefig(
+ buf,
+ format="png",
+ bbox_inches="tight",
+ dpi=300,
+ facecolor="white",
+ pad_inches=0.1,
+ )
+ plt.close(fig)
+ buf.seek(0)
+ return base64.b64encode(buf.read()).decode("utf-8")
+ else:
+ output_path = (
+ self.charts_dir / f"rhr_table_{pd.Timestamp.now().timestamp()}.png"
+ )
+ plt.savefig(
+ output_path,
+ bbox_inches="tight",
+ dpi=300,
+ facecolor="white",
+ pad_inches=0.1,
)
- plt.savefig(output_path, bbox_inches="tight", dpi=300)
plt.close(fig)
return str(output_path)
diff --git a/app/services/report_generator.py b/app/services/report_generator.py
index 2588901..55b5498 100644
--- a/app/services/report_generator.py
+++ b/app/services/report_generator.py
@@ -260,6 +260,13 @@ class ReportGeneratorService:
.chart-large {{
max-height: 500px !important;
}}
+ .table-image {{
+ max-height: none !important;
+ width: auto !important;
+ max-width: 100% !important;
+ height: auto !important;
+ object-fit: contain;
+ }}