Another Solid Checkpoint

This commit is contained in:
bolade
2025-11-28 12:11:00 +01:00
parent e66b9e6c29
commit fc62b64624
7 changed files with 290 additions and 121 deletions
+2 -2
View File
@@ -76,7 +76,7 @@
{% if i < 5 %} {% if i < 5 %}
<div class="flex flex-col items-center py-1 px-2"> <div class="flex flex-col items-center py-1 px-2">
<div class="font-bold text-sm text-black mb-1"> <div class="font-bold text-sm text-black mb-1">
{{ refeed_weekday_calories | default('1615KCals') }} {{ refeed_weekday_calories | default('1615KCals') }} KCals
</div> </div>
<div class="text-xs text-black leading-tight text-left"> <div class="text-xs text-black leading-tight text-left">
<div>{{ refeed_weekday_protein | default('120g Protein') }}</div> <div>{{ refeed_weekday_protein | default('120g Protein') }}</div>
@@ -88,7 +88,7 @@
{% else %} {% else %}
<div class="flex flex-col items-center py-1 px-2"> <div class="flex flex-col items-center py-1 px-2">
<div class="font-bold text-black mb-1"> <div class="font-bold text-black mb-1">
{{ refeed_weekend_calories | default('2000KCals') }} {{ refeed_weekend_calories | default('2000KCals') }} KCals
</div> </div>
<div class="text-xs text-black leading-tight text-left"> <div class="text-xs text-black leading-tight text-left">
<div>{{ refeed_weekend_protein | default('120g Protein') }}</div> <div>{{ refeed_weekend_protein | default('120g Protein') }}</div>
+1 -1
View File
@@ -26,7 +26,7 @@
<!-- Indications Box --> <!-- Indications Box -->
<div class="bg-gray-200 rounded-lg p-4 text-center mb-2"> <div class="bg-gray-200 rounded-lg p-4 text-center mb-2">
<h3 class="font-semibold text-lg mb-2">Indications</h3> <h3 class="font-semibold text-lg mb-2">Indications</h3>
<p class="text-gray-700">{{ indication }}</p> <p >{{ indication | default('No Respiratory Capacity Limitations')}}</p>
</div> </div>
</div> </div>
+223 -90
View File
@@ -1124,80 +1124,163 @@ class GraphGenerator:
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path) return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
def generate_metabolism_chart( def generate_metabolism_chart(
self, rmr_kcal: float, save_as_base64: bool = True self,
rmr_kcal: float,
weight_kg: float = None,
height_cm: float = None,
age_years: int = None,
sex: str = None,
save_as_base64: bool = True,
) -> str: ) -> str:
""" """
Generate metabolism chart (Slow vs Fast Metabolism). Generate metabolism chart (Slow vs Fast Metabolism).
Matches the notebook implementation with ratio-based scale (0.3 to 1.9).
Args: Args:
rmr_kcal: Resting metabolic rate in kcal/day rmr_kcal: Resting metabolic rate in kcal/day (measured RMR)
weight_kg: Weight in kg (optional, for calculating ratio)
height_cm: Height in cm (optional, for calculating ratio)
age_years: Age in years (optional, for calculating ratio)
sex: Sex ("male" or "female", optional, for calculating ratio)
save_as_base64: If True, return base64 string, else return file path save_as_base64: If True, return base64 string, else return file path
Returns: Returns:
Base64 string or file path Base64 string or file path
""" """
from matplotlib.patches import FancyBboxPatch from matplotlib.patches import Rectangle
fig, ax = plt.subplots(figsize=(10, 2.5)) fig, ax = plt.subplots(figsize=(11.5, 2.5))
# Chart data and positions # Calculate ratio if we have all required parameters
# Use normalized positions (0-100 scale) for uniform bar length ratio = None
categories = ["Very Slow", "Slow", "Average", "Fast", "Very Fast"] if all([weight_kg, height_cm, age_years, sex]):
positions = [10, 30, 50, 70, 90] # Normalized positions on 0-100 scale # Mifflin-St Jeor equation
if sex.lower() == "male":
mifflin_rmr = 10 * weight_kg + 6.25 * height_cm - 5 * age_years + 5
elif sex.lower() == "female":
mifflin_rmr = 10 * weight_kg + 6.25 * height_cm - 5 * age_years - 161
else:
mifflin_rmr = None
# Normalize the kcal value to 0-100 scale (assuming range 0-9000 kcal) if mifflin_rmr and mifflin_rmr > 0:
max_kcal = 9000 ratio = rmr_kcal / mifflin_rmr
normalized_value = (rmr_kcal / max_kcal) * 100
indicator_pos = normalized_value
highlight_end = normalized_value
# Main Bar (Background) - using 0-100 scale # Bar setup - using ratio scale from 0.3 to 1.9 (as in notebook)
main_bar = FancyBboxPatch( scale_edges = [0.3, 0.7, 0.9, 1.1, 1.3, 1.5, 1.9]
(0, 0.4), scale_labels = ["Very Slow", "Slow", "Average", "Fast", "Very Fast"]
100, tick_edges = scale_edges[1:-1] # Remove first and last tick (omit 0.3 and 1.9)
0.2,
boxstyle="round,pad=0,rounding_size=0.1", x_start = scale_edges[0]
ec="none", x_end = scale_edges[-1]
fc="#E0E0E0", # Make the bar THICKER by increasing bar_height and adjusting y_bar
bar_height = 0.36
y_bar = 0.48
color_before = "#B2FFC8"
color_after = "#ECEDF2"
gray_color = "#606060"
# If we have a ratio, use it; otherwise map rmr_kcal to the scale
if ratio is not None:
highlight_end = min(max(ratio, x_start), x_end)
else:
# Fallback: map rmr_kcal to scale (assuming typical range 1000-3000 kcal/day)
# Map to 0.3-1.9 scale
min_rmr = 1000
max_rmr = 3000
normalized = (rmr_kcal - min_rmr) / (max_rmr - min_rmr)
highlight_end = x_start + normalized * (x_end - x_start)
highlight_end = min(max(highlight_end, x_start), x_end)
# Draw plain rectangle bar (no rounding)
ax.add_patch(
Rectangle(
(x_start, y_bar),
x_end - x_start,
bar_height,
ec="none",
fc=color_after,
lw=0,
)
) )
ax.add_patch(main_bar)
# Highlighted Bar # Highlighted rectangle
highlight_bar = FancyBboxPatch( if highlight_end > x_start:
(0, 0.4), ax.add_patch(
highlight_end, Rectangle(
0.2, (x_start, y_bar),
boxstyle="round,pad=0,rounding_size=0.1", highlight_end - x_start,
ec="none", bar_height,
fc="#B2FFC8", ec="none",
) fc=color_before,
ax.add_patch(highlight_bar) lw=0,
)
)
# Text and Labels (show actual kcal value) # kCals label, left-aligned, bold inside green, TEXT COLOR gray
ax.text( ax.text(
highlight_end / 2, x_start + 0.07,
0.5, y_bar + bar_height / 2,
f"{rmr_kcal:.0f}kCals", f"{int(round(rmr_kcal))}kCals",
ha="center", ha="left",
va="center", va="center",
color="#006400", color=gray_color,
fontsize=12,
weight="bold",
bbox=dict(boxstyle="round,pad=0.14", ec="none", fc="#B2FFC8", alpha=1.0),
)
# Triangle marker above highlight end, gray
ax.plot(
[highlight_end],
[y_bar + bar_height + 0.08],
marker="v",
markersize=14,
color=gray_color,
clip_on=False,
)
# Draw ticks omit leftmost/rightmost (thicker and below bar), color gray
tick_width = 4.1
tick_bottom = y_bar - 0.07 # further below bar
tick_top = y_bar # at the base of bar
for edge in tick_edges:
ax.plot(
[edge, edge],
[tick_bottom, tick_top],
color=gray_color,
lw=tick_width,
solid_capstyle="butt",
clip_on=False,
zorder=2,
)
# Label locations (place directly under each tick), text color gray
label_y = tick_bottom - 0.08
for label, tick in zip(scale_labels, tick_edges):
ax.text(
tick,
label_y,
label,
ha="center",
va="top",
fontsize=11,
weight="bold",
color=gray_color,
)
# Axis title: bold, with extra gap above the graph
ax.text(
x_start,
y_bar + bar_height + 0.5,
"Slow vs Fast Metabolism",
ha="left",
va="bottom",
fontsize=14, fontsize=14,
weight="bold", weight="bold",
) )
# Indicator Triangle ax.set_xlim(x_start, x_end)
ax.plot(indicator_pos, 0.65, "v", markersize=15, color="#606060", clip_on=False)
# Ticks and Labels
for pos, label in zip(positions, categories):
ax.text(
pos, 0.15, label, ha="center", va="center", fontsize=12, color="#333333"
)
ax.plot([pos, pos], [0.35, 0.39], color="grey", lw=5)
# Chart Styling
ax.set_title("Slow vs Fast Metabolism", fontsize=18, weight="bold", loc="left")
ax.set_xlim(0, 100) # Normalized scale for uniformity
ax.set_ylim(0, 1) ax.set_ylim(0, 1)
ax.axis("off") ax.axis("off")
@@ -1214,6 +1297,7 @@ class GraphGenerator:
) -> str: ) -> str:
""" """
Generate fuel source chart (Fats vs Carbs). Generate fuel source chart (Fats vs Carbs).
Matches the notebook implementation with proper tick styling.
Args: Args:
fat_percentage: Fat percentage at rest fat_percentage: Fat percentage at rest
@@ -1224,84 +1308,133 @@ class GraphGenerator:
""" """
from matplotlib.patches import FancyBboxPatch from matplotlib.patches import FancyBboxPatch
fig, ax = plt.subplots(figsize=(10, 2.5)) fig, ax = plt.subplots(figsize=(11.5, 2.5))
carb_percentage = 100 - fat_percentage carb_percentage = 100 - fat_percentage
optimal_point = 75 optimal_point = 75
# Main Bars (Fats and Carbs) # Let the bars be a bit thicker as well: increase bar height and y
# Fats bar (yellow)
fats_bar = FancyBboxPatch( fats_bar = FancyBboxPatch(
(0, 0.4), (0, 0.36),
fat_percentage, fat_percentage,
0.2, 0.28,
boxstyle="round,pad=0,rounding_size=0.1", boxstyle="round,pad=0,rounding_size=0.1",
ec="none", ec="none",
fc="#FEEAAB", fc="#FEEAAB",
) )
ax.add_patch(fats_bar) ax.add_patch(fats_bar)
# Carbs bar (blue) - starts where the fats bar ends
carbs_bar = FancyBboxPatch( carbs_bar = FancyBboxPatch(
(fat_percentage, 0.4), (fat_percentage, 0.36),
carb_percentage, carb_percentage,
0.2, 0.28,
boxstyle="round,pad=0,rounding_size=0.1", boxstyle="round,pad=0,rounding_size=0.1",
ec="none", ec="none",
fc="#A7F5FF", fc="#A7F5FF",
) )
ax.add_patch(carbs_bar) ax.add_patch(carbs_bar)
# Text and Labels # Style: match font weight/color/size with other chart
label_fontprops = dict(fontsize=12, weight="bold", color="#333333")
ax.text( ax.text(
fat_percentage / 2, fat_percentage / 2,
0.5, 0.5,
f"Fats\n{fat_percentage:.1f}%", f"Fats\n{fat_percentage:.0f}%",
ha="center", ha="center",
va="center", va="center",
color="#333333", **label_fontprops,
fontsize=12,
weight="bold",
) )
ax.text( ax.text(
fat_percentage + carb_percentage / 2, fat_percentage + carb_percentage / 2,
0.5, 0.5,
f"Carbs\n{carb_percentage:.1f}%", f"Carbs\n{100 - fat_percentage:.0f}%",
ha="center", ha="center",
va="center", va="center",
color="#333333", **label_fontprops,
fontsize=12,
weight="bold",
) )
# Add 'Optimal' label # Add 'Optimal' label
ax.text(optimal_point, 0.75, "Optimal", ha="center", va="center", fontsize=12) ax.text(
optimal_point,
# Indicator Triangle 0.9,
ax.plot( "Optimal",
fat_percentage, 0.65, "v", markersize=15, color="#606060", clip_on=False ha="center",
va="center",
fontsize=12,
weight="bold",
color="#606060",
) )
# Ticks and Labels # Optimal point line
ax.plot([optimal_point, optimal_point], [0.65, 0.8], color="#606060", lw=3)
# Indicator Triangle
ax.plot(fat_percentage, 0.7, "v", markersize=15, color="#606060", clip_on=False)
# Ticks and Labels - matching notebook implementation
positions = [0, 25, 50, 75, 100] positions = [0, 25, 50, 75, 100]
tick_color = "#606060"
for pos in positions: for pos in positions:
ax.text( # Smallest ticks (first and last) are thicker
pos, if pos == 0:
0.15, ax.text(
str(pos), pos + 0.5,
ha="center", 0.15,
va="center", str(pos),
fontsize=12, ha="center",
color="#333333", va="center",
) fontsize=12,
ax.plot([pos, pos], [0.35, 0.39], color="grey", lw=5) color="#333333",
weight="bold",
)
ax.plot(
[pos, pos],
[0.25, 0.37],
color=tick_color,
lw=14,
solid_capstyle="butt",
)
elif pos == 100:
ax.text(
pos - 0.5,
0.15,
str(pos),
ha="center",
va="center",
fontsize=12,
color="#333333",
weight="bold",
)
ax.plot(
[pos, pos],
[0.25, 0.37],
color=tick_color,
lw=14,
solid_capstyle="butt",
)
else:
ax.text(
pos,
0.15,
str(pos),
ha="center",
va="center",
fontsize=12,
color="#333333",
weight="bold",
)
ax.plot(
[pos, pos],
[0.25, 0.37],
color=tick_color,
lw=8,
solid_capstyle="butt",
)
# Add a special tick for the 'Optimal' point # Chart Styling - uniform style for title
ax.plot([optimal_point, optimal_point], [0.6, 0.7], color="black", lw=2) ax.set_title("Fuel Source", fontsize=14, weight="bold", loc="left", pad=22)
ax.set_xlim(0, 100)
# Chart Styling
ax.set_title("Fuel Source", fontsize=18, weight="bold", loc="left")
ax.set_xlim(0, 100) # Normalized scale for uniformity
ax.set_ylim(0, 1) ax.set_ylim(0, 1)
ax.axis("off") ax.axis("off")
+29 -2
View File
@@ -524,9 +524,36 @@ class ReportGeneratorService:
} }
rmr_metrics = temp_context_gen.calculate_rmr_and_fuel_source() rmr_metrics = temp_context_gen.calculate_rmr_and_fuel_source()
# Generate metabolism chart # Convert height to cm if available
height_cm = None
height_str = patient_info.get("height", "")
if height_str:
try:
# Try to parse height string (e.g., "5'4"", "165cm", "165")
import re
# Check if it's in feet'inches" format
feet_inches_match = re.match(r"(\d+)'(\d+)\"", height_str)
if feet_inches_match:
feet = int(feet_inches_match.group(1))
inches = int(feet_inches_match.group(2))
height_cm = (feet * 12 + inches) * 2.54
# Check if it ends with cm
elif "cm" in height_str.lower():
height_cm = float(re.sub(r"[^\d.]", "", height_str))
# Otherwise try to parse as number (assume cm)
else:
height_cm = float(re.sub(r"[^\d.]", "", height_str))
except (ValueError, AttributeError):
pass
# Generate metabolism chart with ratio calculation if we have all parameters
metabolism_chart_b64 = self.graph_generator.generate_metabolism_chart( metabolism_chart_b64 = self.graph_generator.generate_metabolism_chart(
rmr_metrics["rmr_kcal"], save_as_base64=True rmr_metrics["rmr_kcal"],
weight_kg=weight_kg,
height_cm=height_cm,
age_years=patient_info.get("age", None),
sex=gender,
save_as_base64=True,
) )
graphs_dict["metabolism_chart"] = metabolism_chart_b64 graphs_dict["metabolism_chart"] = metabolism_chart_b64
+35 -26
View File
File diff suppressed because one or more lines are too long