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 @@ Resting Heart Rate Table 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 @@ VO2 Max Table @@ -43,7 +43,7 @@ Heart Rate Zones Table 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; + }}