Another Solid Checkpoint
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+223
-90
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user