feat: Enhance context generation with new table images for VO2 Max and Heart Rate Zones
- Added functionality to generate VO2 Max and Heart Rate Zones tables in the context_generator.py. - Integrated graph_generator to create table images with specified data and styles. - Updated report_generator.py to pass graph_generator to context generation. - Introduced a new method in graph_generator.py to generate table images with customizable options. - Created test scripts for Page 5 (RMR and NEAT calculations) and Page 6 (Meal Plan calculations) using actual patient data. - Updated Jupyter notebook metadata for better environment identification.
This commit is contained in:
@@ -6,7 +6,7 @@ of the medical report. It performs analysis on Pnoe, Spirometry, and SECA data.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional, Tuple
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
import pandas as pd
|
||||
|
||||
@@ -626,6 +626,7 @@ class ContextGenerator:
|
||||
patient_name: str,
|
||||
graphs: Dict[str, str],
|
||||
metric_overrides: Optional[Dict] = None,
|
||||
graph_generator: Optional[Any] = None,
|
||||
) -> Dict[str, Dict]:
|
||||
"""Main method to generate all page contexts
|
||||
|
||||
@@ -720,6 +721,127 @@ class ContextGenerator:
|
||||
"vo2_pulse_chart": graphs.get("vo2_pulse", ""),
|
||||
}
|
||||
|
||||
if graph_generator:
|
||||
# VO2 Max Table
|
||||
vo2_max_columns = [
|
||||
"Age (F)",
|
||||
"Very Poor",
|
||||
"Poor",
|
||||
"Fair",
|
||||
"Good",
|
||||
"Excellent",
|
||||
"Superior",
|
||||
]
|
||||
vo2_max_data = [
|
||||
[
|
||||
contexts["page_8"]["age_range"],
|
||||
"19.0-24.1",
|
||||
"24.1-28.2",
|
||||
"28.2-32.2",
|
||||
"32.2-35.7",
|
||||
"35.7-45.8",
|
||||
"45.8+",
|
||||
]
|
||||
]
|
||||
vo2_max_colors = [
|
||||
[
|
||||
"#b2ebf2",
|
||||
"#f5f5f5",
|
||||
"#f5f5f5",
|
||||
"#f5f5f5",
|
||||
"#f5f5f5",
|
||||
"#f5f5f5",
|
||||
"#f5f5f5",
|
||||
]
|
||||
]
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
# Heart Rate Zones Table
|
||||
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",
|
||||
],
|
||||
[
|
||||
"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",
|
||||
],
|
||||
[
|
||||
pnoe_metrics.get("zone1_bpm", "81-96bpm"),
|
||||
pnoe_metrics.get("zone2_bpm", "96-100bpm"),
|
||||
pnoe_metrics.get("zone3_bpm", "100-178bpm"),
|
||||
pnoe_metrics.get("zone4_bpm", "178-188bpm"),
|
||||
pnoe_metrics.get("zone5_bpm", "188-198bpm"),
|
||||
],
|
||||
[
|
||||
"3.5mph\n2% Incline",
|
||||
"3.5-4.0mph\n2% Incline",
|
||||
"4.0-6.5mph\n2% Incline",
|
||||
"6.5-7.0mph\n2% Incline",
|
||||
"7.0-8.0mph\n2% Incline",
|
||||
],
|
||||
[
|
||||
"10:39min/km Pace",
|
||||
"10:39-9:19min/km Pace",
|
||||
"9:19-5:44min/km Pace",
|
||||
"5:44-5:20min/km Pace",
|
||||
"5:20-4:40min/km Pace",
|
||||
],
|
||||
[
|
||||
"Avg:\n4.4kcals/minute",
|
||||
"Avg:\n5.9kcals/minute",
|
||||
"Avg:\n9.4kcals/minute",
|
||||
"Avg:\n12.5kcals/minute",
|
||||
"Avg:\n12.8kcals/minute",
|
||||
],
|
||||
[
|
||||
"Avg: 0.4g/min\nCarb Utilization",
|
||||
"Avg: 0.6g/min\nCarb Utilization",
|
||||
"Avg: 1.9g/min\nCarb Utilization",
|
||||
"Avg: 2.9g/min\nCarb Utilization",
|
||||
"Avg: 3.1g/min\nCarb Utilization",
|
||||
],
|
||||
[
|
||||
"Avg: 27 breaths\nIdeal: 15-20",
|
||||
"Avg: 28 breaths\nIdeal: 20-25",
|
||||
"Avg: 31 breaths\nIdeal: 25-30",
|
||||
"Avg: 42 breaths\nIdeal: 30-35",
|
||||
"Avg: 51 breaths\nIdeal: 40+",
|
||||
],
|
||||
]
|
||||
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"],
|
||||
]
|
||||
|
||||
contexts["page_8"]["hr_zones_table"] = graph_generator.generate_table_image(
|
||||
data=hr_zones_data,
|
||||
columns=hr_zones_columns,
|
||||
cell_colors=hr_zones_colors,
|
||||
header_color="#4dd0e1",
|
||||
save_as_base64=True,
|
||||
)
|
||||
|
||||
# Page 9
|
||||
contexts["page_9"] = {
|
||||
"fat_max_value": f"{pnoe_metrics['fat_max_value']:.2f}",
|
||||
@@ -752,6 +874,40 @@ class ContextGenerator:
|
||||
**resting_hr_metrics,
|
||||
}
|
||||
|
||||
if graph_generator:
|
||||
# Page 11 Resting Heart Rate Table
|
||||
rhr_columns = [
|
||||
"Age (F)",
|
||||
"Poor",
|
||||
"Below Average",
|
||||
"Average",
|
||||
"Above Average",
|
||||
"Good",
|
||||
"Excellent",
|
||||
"Athlete",
|
||||
]
|
||||
rhr_data = [
|
||||
[
|
||||
contexts["page_11"]["hr_age_range"],
|
||||
contexts["page_11"]["hr_poor"],
|
||||
contexts["page_11"]["hr_below_avg"],
|
||||
contexts["page_11"]["hr_average"],
|
||||
contexts["page_11"]["hr_above_avg"],
|
||||
contexts["page_11"]["hr_good"],
|
||||
contexts["page_11"]["hr_excellent"],
|
||||
contexts["page_11"]["hr_athlete"],
|
||||
]
|
||||
]
|
||||
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,
|
||||
)
|
||||
|
||||
# Pages 12-17
|
||||
for i in range(6):
|
||||
contexts[f"page_{i + 12}"] = {
|
||||
|
||||
@@ -1305,3 +1305,86 @@ class GraphGenerator:
|
||||
plt.close()
|
||||
|
||||
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
|
||||
|
||||
def generate_table_image(
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
fig, ax = plt.subplots(figsize=(width, height))
|
||||
ax.axis("off")
|
||||
|
||||
if title:
|
||||
plt.title(title, pad=20, fontsize=14, fontweight="bold")
|
||||
|
||||
# Create table
|
||||
table = ax.table(
|
||||
cellText=data,
|
||||
colLabels=columns,
|
||||
cellLoc="center",
|
||||
loc="center",
|
||||
colColours=[header_color] * len(columns),
|
||||
)
|
||||
|
||||
# Style the table
|
||||
table.auto_set_font_size(False)
|
||||
table.set_fontsize(10)
|
||||
table.scale(1, 1.5) # Increase row height
|
||||
|
||||
# Apply cell colors if provided
|
||||
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)
|
||||
cell = table[(i + 1, j)]
|
||||
cell.set_facecolor(color)
|
||||
|
||||
# Bold headers
|
||||
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()
|
||||
|
||||
if save_as_base64:
|
||||
import io
|
||||
|
||||
buf = io.BytesIO()
|
||||
plt.savefig(buf, format="png", bbox_inches="tight", dpi=300)
|
||||
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"
|
||||
)
|
||||
plt.savefig(output_path, bbox_inches="tight", dpi=300)
|
||||
plt.close(fig)
|
||||
return str(output_path)
|
||||
|
||||
@@ -507,7 +507,10 @@ class ReportGeneratorService:
|
||||
"gender": gender,
|
||||
}
|
||||
contexts = self.context_generator.generate_all_contexts(
|
||||
patient_name, graphs_dict, metric_overrides=metric_overrides
|
||||
patient_name,
|
||||
graphs_dict,
|
||||
metric_overrides=metric_overrides,
|
||||
graph_generator=self.graph_generator,
|
||||
)
|
||||
|
||||
# Step 5: Calculate analysis metrics
|
||||
|
||||
Reference in New Issue
Block a user