diff --git a/app/report_gen/page_11.html b/app/report_gen/page_11.html index 0ef0c45..4cde673 100644 --- a/app/report_gen/page_11.html +++ b/app/report_gen/page_11.html @@ -123,10 +123,6 @@
-

- Resting Heart Rate - {{ resting_heart_rate | default('53bpm') }} -

-
str: + """Determine resting heart rate category based on value, age, and gender (matching notebook logic)""" + rhr_table_info = self._calculate_rhr_table_data(age, gender) + ranges = rhr_table_info["raw_ranges"] + + # Check Poor category first (open-ended at top) + min_val, max_val = ranges["Poor"] + if max_val is None and rhr >= min_val: + return "Poor" + + # Check other categories from Below Average down to Athlete + # For RHR, lower is better, so we check from highest to lowest + for category in [ + "Below Average", + "Average", + "Above Average", + "Good", + "Excellent", + "Athlete", + ]: + min_val, max_val = ranges[category] + # Check if value falls in this range (inclusive of min, exclusive of max) + if min_val <= rhr < max_val: + return category + + # If value is below all ranges (below Athlete minimum), return Athlete + # This handles the case where rhr < min of Athlete + return "Athlete" + def _calculate_zone_metrics(self, pnoe_metrics: Dict) -> Dict: """Calculate detailed metrics for each heart rate zone based on actual data""" import math @@ -1294,14 +1324,24 @@ class ContextGenerator: self.patient_info["age"], self.patient_info["gender"] ) - gender_label = ( - "Age (F)" - if self.patient_info["gender"].lower().startswith("f") - else "Age (M)" + # Get resting heart rate value and determine category + # Extract numeric value from "53bpm" format (resting_hr_metrics already calculated above) + rhr_value_str = resting_hr_metrics.get("resting_heart_rate", "0bpm") + rhr_value = float(rhr_value_str.replace("bpm", "").strip()) + + category = self._determine_rhr_category( + rhr_value, + self.patient_info["age"], + self.patient_info["gender"], ) + gender_label = ( + "F" if self.patient_info["gender"].lower().startswith("f") else "M" + ) + age_range_label = f"{rhr_table_info['age_range']} ({gender_label})" + rhr_columns = [ - gender_label, + "Age", "Poor", "Below Average", "Average", @@ -1312,7 +1352,7 @@ class ContextGenerator: ] rhr_data = [ [ - rhr_table_info["age_range"], + age_range_label, rhr_table_info["ranges"]["Poor"], rhr_table_info["ranges"]["Below Average"], rhr_table_info["ranges"]["Average"], @@ -1322,13 +1362,13 @@ class ContextGenerator: rhr_table_info["ranges"]["Athlete"], ] ] - rhr_colors = [["#b2ebf2"] + ["#f5f5f5"] * 7] contexts["page_11"]["rhr_table"] = ( graph_generator.generate_resting_heart_rate_table( data=rhr_data, columns=rhr_columns, - cell_colors=rhr_colors, + rhr_value=rhr_value, + category=category, save_as_base64=True, ) ) diff --git a/app/services/graph_generator.py b/app/services/graph_generator.py index 340d210..4c9f31a 100644 --- a/app/services/graph_generator.py +++ b/app/services/graph_generator.py @@ -1334,7 +1334,7 @@ class GraphGenerator: from matplotlib.patches import FancyArrowPatch, RegularPolygon # Fixed optimal sizing for VO2 Max table (7 columns, 1 data row) - fig, ax = plt.subplots(figsize=(14, 3)) + fig, ax = plt.subplots(figsize=(14, 2.2)) ax.axis("off") # Create table @@ -1349,7 +1349,7 @@ class GraphGenerator: # Style the table table.auto_set_font_size(False) table.set_fontsize(11) - table.scale(1, 2.5) + table.scale(1, 1.8) # Header row styling (cyan background) for i in range(len(columns)): @@ -1423,7 +1423,7 @@ class GraphGenerator: percentile = percentile_map.get(category, "N/A") title = f"VO2 Max - {vo2_max_value:.1f} ({percentile})" - ax.set_title(title, fontsize=14, fontweight="bold", pad=20) + ax.set_title(title, fontsize=14, fontweight="bold", pad=10) if save_as_base64: buf = io.BytesIO() @@ -1433,7 +1433,7 @@ class GraphGenerator: bbox_inches="tight", dpi=300, facecolor="white", - pad_inches=0.1, + pad_inches=0.05, ) plt.close(fig) buf.seek(0) @@ -1447,7 +1447,7 @@ class GraphGenerator: bbox_inches="tight", dpi=300, facecolor="white", - pad_inches=0.1, + pad_inches=0.05, ) plt.close(fig) return str(output_path) @@ -1506,7 +1506,7 @@ class GraphGenerator: # Style the table table.auto_set_font_size(False) table.set_fontsize(11) - table.scale(1, 2.8) + table.scale(1, 2.0) # Apply cell colors if cell_colors: @@ -1558,15 +1558,19 @@ class GraphGenerator: self, data: list[list], columns: list[str], + rhr_value: float = None, + category: str = None, cell_colors: list[list[str]] = None, save_as_base64: bool = True, ) -> str: """ - Generate Resting Heart Rate table as an image with optimized sizing. + Generate Resting Heart Rate table as an image with optimized sizing, highlighting the patient's category. Args: data: List of rows (each row is a list of values) columns: List of column headers + rhr_value: Patient's resting heart rate value in bpm (for title and arrow) + category: Category that the patient falls into (e.g., 'Good', 'Excellent') cell_colors: Optional matrix of cell colors save_as_base64: If True, return base64 string @@ -1575,12 +1579,11 @@ class GraphGenerator: """ import io - # Optimal sizing for RHR table (8 columns, 1 data row) - fig, ax = plt.subplots(figsize=(18, 2.5)) - ax.axis("off") + from matplotlib.patches import FancyArrowPatch, RegularPolygon - # Even column widths - col_widths = [1.0 / len(columns)] * len(columns) + # Optimal sizing for RHR table (8 columns, 1 data row) + fig, ax = plt.subplots(figsize=(16, 2.2)) + ax.axis("off") # Create table table = ax.table( @@ -1588,33 +1591,77 @@ class GraphGenerator: colLabels=columns, cellLoc="center", loc="center", - colColours=["#4dd0e1"] * len(columns), - colWidths=col_widths, + bbox=[0, 0, 1, 1], ) # Style the table table.auto_set_font_size(False) table.set_fontsize(11) - table.scale(1, 3.0) + table.scale(1, 1.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) + # Header row styling (cyan background) + for i in range(len(columns)): + cell = table[(0, i)] + cell.set_facecolor("#7dd3fc") # cyan-300 equivalent + cell.set_text_props(weight="bold", color="black", fontsize=12) + cell.set_edgecolor("#9ca3af") # gray-400 + cell.set_linewidth(1) - # 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) + # Find the column index for the category (if provided) + category_index = None + if category and category in columns: + category_index = columns.index(category) + + # Data row styling + for i in range(len(data[0])): + cell = table[(1, i)] + if i == 0: # Age column + cell.set_facecolor("#a5f3fc") # cyan-200 + cell.set_text_props(weight="semibold", color="black", fontsize=11) else: - cell.set_edgecolor("#666666") - cell.set_linewidth(1.0) - cell.set_text_props(fontsize=10) + # Highlight the category cell with light green background + if category_index is not None and i == category_index: + cell.set_facecolor("#d1fae5") # green-200 equivalent + cell.set_text_props(weight="bold", color="black", fontsize=11) + else: + cell.set_facecolor("#f3f4f6") # gray-100 + cell.set_text_props(color="black", fontsize=10) + cell.set_edgecolor("#9ca3af") # gray-400 + cell.set_linewidth(1) + + # Add arrow indicator below the category column + if category_index is not None: + # Calculate position + cell_width = 1.0 / len(columns) + arrow_x = (category_index + 0.5) * cell_width + + # Draw arrow pointing up + arrow = FancyArrowPatch( + (arrow_x, -0.15), + (arrow_x, -0.05), + arrowstyle="->", + mutation_scale=20, + linewidth=2, + color="black", + transform=ax.transAxes, + ) + ax.add_patch(arrow) + + # Add triangle at the top + triangle = RegularPolygon( + (arrow_x, -0.05), + 3, + radius=0.02, + orientation=np.pi / 2, + color="black", + transform=ax.transAxes, + ) + ax.add_patch(triangle) + + # Set title + if rhr_value is not None: + title = f"Resting Heart Rate - {rhr_value:.0f}bpm" + ax.set_title(title, fontsize=14, fontweight="bold", pad=10) if save_as_base64: buf = io.BytesIO() @@ -1624,7 +1671,7 @@ class GraphGenerator: bbox_inches="tight", dpi=300, facecolor="white", - pad_inches=0.1, + pad_inches=0.05, ) plt.close(fig) buf.seek(0) @@ -1638,7 +1685,7 @@ class GraphGenerator: bbox_inches="tight", dpi=300, facecolor="white", - pad_inches=0.1, + pad_inches=0.05, ) plt.close(fig) return str(output_path)