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:
bolade
2025-11-21 11:38:43 +01:00
parent 9d51b006c0
commit 47f0c6f3fb
8 changed files with 825 additions and 294 deletions
+7 -98
View File
@@ -127,104 +127,13 @@
Resting Heart Rate - {{ resting_heart_rate | default('53bpm') }}
</h3>
<table class="w-full border-collapse text-xs">
<thead>
<tr>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Age (F)
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Poor
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Below Average
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Average
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Above Average
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Good
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold"
>
Excellent
</th>
<th
class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold relative"
>
Athlete
<!-- Arrow indicator -->
<div
class="absolute -bottom-3 left-1/2 transform -translate-x-1/2"
>
<div
class="w-0 h-0 border-l-3 border-r-3 border-t-6 border-transparent border-t-black"
></div>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td
class="bg-cyan-200 border border-gray-400 p-2 text-black font-semibold text-center"
>
{{ hr_age_range | default('26-35') }}
</td>
<td
class="bg-gray-100 border border-gray-400 p-2 text-black text-center"
>
{{ hr_poor | default('82bpm +') }}
</td>
<td
class="bg-gray-100 border border-gray-400 p-2 text-black text-center"
>
{{ hr_below_avg | default('75-81bpm') }}
</td>
<td
class="bg-gray-100 border border-gray-400 p-2 text-black text-center"
>
{{ hr_average | default('71-74bpm') }}
</td>
<td
class="bg-gray-100 border border-gray-400 p-2 text-black text-center"
>
{{ hr_above_avg | default('66-70bpm') }}
</td>
<td
class="bg-gray-100 border border-gray-400 p-2 text-black text-center"
>
{{ hr_good | default('62-65bpm') }}
</td>
<td
class="bg-gray-100 border border-gray-400 p-2 text-black text-center"
>
{{ hr_excellent | default('55-61bpm') }}
</td>
<td
class="bg-green-200 border border-gray-400 p-2 text-black text-center font-bold"
>
{{ hr_athlete | default('44-54bpm') }}
</td>
</tr>
</tbody>
</table>
<div class="flex justify-center">
<img
src="data:image/png;base64, {{ rhr_table }}"
alt="Resting Heart Rate Table"
class="w-full max-w-4xl h-auto object-contain"
/>
</div>
</div>
</div>
+46 -193
View File
@@ -1,198 +1,51 @@
<div class="w-full page bg-white">
<!-- Main Content -->
<div class="px-8 py-6">
<!-- Page Title -->
<h1 class="text-3xl font-bold text-black mb-6">Cardio Metrics</h1>
<!-- Main Content -->
<div class="px-8 py-6">
<!-- Page Title -->
<h1 class="text-3xl font-bold text-black mb-6">Cardio Metrics</h1>
<!-- Active Metabolic Rate Assessment Section -->
<h2 class="text-xl font-bold text-black mb-4">Active Metabolic Rate Assessment</h2>
<p class="text-gray-700 text-sm mb-8">The active metabolic rate assessment is a key measure of aerobic fitness. It helps determine your specific heart rate zones and how well your body uses carbohydrates and fats as fuel while you exercise. It is also an indicator of overall health and wellbeing.</p>
<!-- Active Metabolic Rate Assessment Section -->
<h2 class="text-xl font-bold text-black mb-4">
Active Metabolic Rate Assessment
</h2>
<p class="text-gray-700 text-sm mb-8">
The active metabolic rate assessment is a key measure of aerobic
fitness. It helps determine your specific heart rate zones and how
well your body uses carbohydrates and fats as fuel while you
exercise. It is also an indicator of overall health and wellbeing.
</p>
<!-- VO2 Max Section -->
<div class="mb-8">
<h3 class="text-xl font-bold text-black mb-4 text-center">VO2 Max - {{ vo2_max_value | default('49.5') }} ({{ vo2_max_percentile | default('100th percentile') }})</h3>
<!-- VO2 Max Table -->
<div class="mb-8">
<table class="w-full border-collapse">
<thead>
<tr>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold">Age (F)</th>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold">Very Poor</th>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold">Poor</th>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold">Fair</th>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold">Good</th>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold">Excellent</th>
<th class="bg-cyan-300 border border-gray-400 p-3 text-black font-bold relative">
Superior
<!-- Arrow indicator -->
<div class="absolute -bottom-4 left-1/2 transform -translate-x-1/2">
<div class="w-0 h-0 border-l-4 border-r-4 border-t-8 border-transparent border-t-black"></div>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="bg-cyan-200 border border-gray-400 p-3 text-black font-semibold">{{ age_range | default('30-39') }}</td>
<td class="bg-gray-100 border border-gray-400 p-3 text-black text-center">{{ very_poor_range | default('19.0-24.1') }}</td>
<td class="bg-gray-100 border border-gray-400 p-3 text-black text-center">{{ poor_range | default('24.1-28.2') }}</td>
<td class="bg-gray-100 border border-gray-400 p-3 text-black text-center">{{ fair_range | default('28.2-32.2') }}</td>
<td class="bg-gray-100 border border-gray-400 p-3 text-black text-center">{{ good_range | default('32.2-35.7') }}</td>
<td class="bg-gray-100 border border-gray-400 p-3 text-black text-center">{{ excellent_range | default('35.7-45.8') }}</td>
<td class="bg-gray-100 border border-gray-400 p-3 text-black text-center font-bold">{{ superior_range | default('45.8+') }}</td>
</tr>
</tbody>
</table>
</div>
<!-- VO2 Max Section -->
<div class="mb-8">
<h3 class="text-xl font-bold text-black mb-4 text-center">
VO2 Max - {{ vo2_max_value | default('49.5') }} ({{
vo2_max_percentile | default('100th percentile') }})
</h3>
<!-- VO2 Max Table -->
<div class="mb-8 flex justify-center">
<img
src="data:image/png;base64, {{ vo2_max_table }}"
alt="VO2 Max Table"
class="w-full max-w-4xl h-auto object-contain"
/>
</div>
</div>
<!-- Personalized Heart Rate Zones Section -->
<div class="mb-8">
<h3 class="text-xl font-bold text-black mb-6 text-center">
Personalized Heart Rate Zones
</h3>
<!-- Heart Rate Zones Table -->
<div class="flex justify-center">
<img
src="data:image/png;base64, {{ hr_zones_table }}"
alt="Heart Rate Zones Table"
class="w-full max-w-4xl h-auto object-contain"
/>
</div>
</div>
</div>
<!-- Personalized Heart Rate Zones Section -->
<div class="mb-8">
<h3 class="text-xl font-bold text-black mb-6 text-center">Personalized Heart Rate Zones</h3>
<!-- Heart Rate Zones Table -->
<table class="w-full border-collapse text-sm">
<thead>
<tr>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Zone 1</th>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Zone 2</th>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Zone 3</th>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Zone 4</th>
<th class="bg-cyan-300 border border-gray-400 p-2 text-black font-bold">Zone 5</th>
</tr>
</thead>
<tbody>
<!-- Zone Descriptions -->
<tr>
<td class="border border-gray-400 p-3 text-center">
<div class="text-black font-semibold mb-1">Improves health and recovery capacity</div>
</td>
<td class="border border-gray-400 p-3 text-center">
<div class="text-black font-semibold mb-1">Improves endurance and fat burning</div>
</td>
<td class="border border-gray-400 p-3 text-center">
<div class="text-black font-semibold mb-1">Improves Aerobic fitness</div>
</td>
<td class="border border-gray-400 p-3 text-center">
<div class="text-black font-semibold mb-1">Improves maximum performance capacity</div>
</td>
<td class="border border-gray-400 p-3 text-center">
<div class="text-black font-semibold mb-1">Develops maximum performance and speed</div>
</td>
</tr>
<!-- Heart Rate Percentages -->
<tr>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">{{ zone1_percentage | default('55-65% of Max Heart Rate') }}</td>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">{{ zone2_percentage | default('65-75% of Max Heart Rate') }}</td>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">{{ zone3_percentage | default('80-85% of Max Heart Rate') }}</td>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">{{ zone4_percentage | default('85-88% of Max Heart Rate') }}</td>
<td class="border border-gray-400 p-2 text-center text-black font-semibold">{{ zone5_percentage | default('90% of Max Heart Rate') }}</td>
</tr>
<!-- Heart Rate BPM -->
<tr>
<td class="bg-red-200 border border-gray-400 p-2 text-center text-black font-bold">{{ zone1_bpm | default('81-96bpm') }}</td>
<td class="bg-red-200 border border-gray-400 p-2 text-center text-black font-bold">{{ zone2_bpm | default('96-100bpm') }}</td>
<td class="bg-yellow-200 border border-gray-400 p-2 text-center text-black font-bold">{{ zone3_bpm | default('100-178bpm') }}</td>
<td class="bg-green-200 border border-gray-400 p-2 text-center text-black font-bold">{{ zone4_bpm | default('178-188bpm') }}</td>
<td class="bg-green-200 border border-gray-400 p-2 text-center text-black font-bold">{{ zone5_bpm | default('188-198bpm') }}</td>
</tr>
<!-- Speed -->
<tr>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black font-semibold">{{ zone1_speed | default('3.5mph') }}</div>
<div class="text-black text-xs">{{ zone1_incline | default('2% Incline') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black font-semibold">{{ zone2_speed | default('3.5-4.0mph') }}</div>
<div class="text-black text-xs">{{ zone2_incline | default('2% Incline') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black font-semibold">{{ zone3_speed | default('4.0-6.5mph') }}</div>
<div class="text-black text-xs">{{ zone3_incline | default('2% Incline') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black font-semibold">{{ zone4_speed | default('6.5-7.0mph') }}</div>
<div class="text-black text-xs">{{ zone4_incline | default('2% Incline') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black font-semibold">{{ zone5_speed | default('7.0-8.0mph') }}</div>
<div class="text-black text-xs">{{ zone5_incline | default('2% Incline') }}</div>
</td>
</tr>
<!-- Pace -->
<tr>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone1_pace | default('10:39min/km Pace') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone2_pace | default('10:39-9:19min/km Pace') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone3_pace | default('9:19-5:44min/km Pace') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone4_pace | default('5:44-5:20min/km Pace') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone5_pace | default('5:20-4:40min/km Pace') }}</td>
</tr>
<!-- Average Calories -->
<tr>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black text-xs">Avg:</div>
<div class="text-black font-semibold">{{ zone1_calories | default('4.4kcals/minute') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black text-xs">Avg:</div>
<div class="text-black font-semibold">{{ zone2_calories | default('5.9kcals/minute') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black text-xs">Avg:</div>
<div class="text-black font-semibold">{{ zone3_calories | default('9.4kcals/minute') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black text-xs">Avg:</div>
<div class="text-black font-semibold">{{ zone4_calories | default('12.5kcals/minute') }}</div>
</td>
<td class="border border-gray-400 p-2 text-center">
<div class="text-black text-xs">Avg:</div>
<div class="text-black font-semibold">{{ zone5_calories | default('12.8kcals/minute') }}</div>
</td>
</tr>
<!-- Carb Utilization -->
<tr>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone1_carb | default('Avg: 0.4g/min Carb Utilization') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone2_carb | default('Avg: 0.6g/min Carb Utilization') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone3_carb | default('Avg: 1.9g/min Carb Utilization') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone4_carb | default('Avg: 2.9g/min Carb Utilization') }}</td>
<td class="border border-gray-400 p-2 text-center text-black">{{ zone5_carb | default('Avg: 3.1g/min Carb Utilization') }}</td>
</tr>
<!-- Breathing -->
<tr>
<td class="bg-red-200 border border-gray-400 p-2 text-center">
<div class="text-black font-bold">{{ zone1_breaths | default('Avg: 27 breaths') }}</div>
<div class="text-black text-xs italic">{{ zone1_breath_range | default('Ideal Range: 15-20 breaths') }}</div>
</td>
<td class="bg-red-200 border border-gray-400 p-2 text-center">
<div class="text-black font-bold">{{ zone2_breaths | default('Avg: 28 breaths') }}</div>
<div class="text-black text-xs italic">{{ zone2_breath_range | default('Ideal Range: 20-25 breaths') }}</div>
</td>
<td class="bg-yellow-200 border border-gray-400 p-2 text-center">
<div class="text-black font-bold">{{ zone3_breaths | default('Avg: 31 breaths') }}</div>
<div class="text-black text-xs italic">{{ zone3_breath_range | default('Ideal Range: 25-30 breaths') }}</div>
</td>
<td class="bg-green-200 border border-gray-400 p-2 text-center">
<div class="text-black font-bold">{{ zone4_breaths | default('Avg: 42 breaths') }}</div>
<div class="text-black text-xs italic">{{ zone4_breath_range | default('Ideal Range: 30-35 breaths') }}</div>
</td>
<td class="bg-green-200 border border-gray-400 p-2 text-center">
<div class="text-black font-bold">{{ zone5_breaths | default('Avg: 51 breaths') }}</div>
<div class="text-black text-xs italic">{{ zone5_breath_range | default('Ideal Range: 40+ breaths') }}</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
+157 -1
View File
@@ -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}"] = {
+83
View File
@@ -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)
+4 -1
View File
@@ -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