Another Solid Checkpoint
This commit is contained in:
@@ -76,7 +76,7 @@
|
||||
{% if i < 5 %}
|
||||
<div class="flex flex-col items-center py-1 px-2">
|
||||
<div class="font-bold text-sm text-black mb-1">
|
||||
{{ refeed_weekday_calories | default('1615KCals') }}
|
||||
{{ refeed_weekday_calories | default('1615KCals') }} KCals
|
||||
</div>
|
||||
<div class="text-xs text-black leading-tight text-left">
|
||||
<div>{{ refeed_weekday_protein | default('120g Protein') }}</div>
|
||||
@@ -88,7 +88,7 @@
|
||||
{% else %}
|
||||
<div class="flex flex-col items-center py-1 px-2">
|
||||
<div class="font-bold text-black mb-1">
|
||||
{{ refeed_weekend_calories | default('2000KCals') }}
|
||||
{{ refeed_weekend_calories | default('2000KCals') }} KCals
|
||||
</div>
|
||||
<div class="text-xs text-black leading-tight text-left">
|
||||
<div>{{ refeed_weekend_protein | default('120g Protein') }}</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<!-- Indications Box -->
|
||||
<div class="bg-gray-200 rounded-lg p-4 text-center mb-2">
|
||||
<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>
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+211
-78
@@ -1124,80 +1124,163 @@ class GraphGenerator:
|
||||
return self._image_to_base64(chart_path) if save_as_base64 else str(chart_path)
|
||||
|
||||
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:
|
||||
"""
|
||||
Generate metabolism chart (Slow vs Fast Metabolism).
|
||||
Matches the notebook implementation with ratio-based scale (0.3 to 1.9).
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
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
|
||||
# Use normalized positions (0-100 scale) for uniform bar length
|
||||
categories = ["Very Slow", "Slow", "Average", "Fast", "Very Fast"]
|
||||
positions = [10, 30, 50, 70, 90] # Normalized positions on 0-100 scale
|
||||
# Calculate ratio if we have all required parameters
|
||||
ratio = None
|
||||
if all([weight_kg, height_cm, age_years, sex]):
|
||||
# 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)
|
||||
max_kcal = 9000
|
||||
normalized_value = (rmr_kcal / max_kcal) * 100
|
||||
indicator_pos = normalized_value
|
||||
highlight_end = normalized_value
|
||||
if mifflin_rmr and mifflin_rmr > 0:
|
||||
ratio = rmr_kcal / mifflin_rmr
|
||||
|
||||
# Main Bar (Background) - using 0-100 scale
|
||||
main_bar = FancyBboxPatch(
|
||||
(0, 0.4),
|
||||
100,
|
||||
0.2,
|
||||
boxstyle="round,pad=0,rounding_size=0.1",
|
||||
# Bar setup - using ratio scale from 0.3 to 1.9 (as in notebook)
|
||||
scale_edges = [0.3, 0.7, 0.9, 1.1, 1.3, 1.5, 1.9]
|
||||
scale_labels = ["Very Slow", "Slow", "Average", "Fast", "Very Fast"]
|
||||
tick_edges = scale_edges[1:-1] # Remove first and last tick (omit 0.3 and 1.9)
|
||||
|
||||
x_start = scale_edges[0]
|
||||
x_end = scale_edges[-1]
|
||||
# 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="#E0E0E0",
|
||||
fc=color_after,
|
||||
lw=0,
|
||||
)
|
||||
)
|
||||
ax.add_patch(main_bar)
|
||||
|
||||
# Highlighted Bar
|
||||
highlight_bar = FancyBboxPatch(
|
||||
(0, 0.4),
|
||||
highlight_end,
|
||||
0.2,
|
||||
boxstyle="round,pad=0,rounding_size=0.1",
|
||||
# Highlighted rectangle
|
||||
if highlight_end > x_start:
|
||||
ax.add_patch(
|
||||
Rectangle(
|
||||
(x_start, y_bar),
|
||||
highlight_end - x_start,
|
||||
bar_height,
|
||||
ec="none",
|
||||
fc="#B2FFC8",
|
||||
fc=color_before,
|
||||
lw=0,
|
||||
)
|
||||
)
|
||||
ax.add_patch(highlight_bar)
|
||||
|
||||
# Text and Labels (show actual kcal value)
|
||||
# kCals label, left-aligned, bold inside green, TEXT COLOR gray
|
||||
ax.text(
|
||||
highlight_end / 2,
|
||||
0.5,
|
||||
f"{rmr_kcal:.0f}kCals",
|
||||
ha="center",
|
||||
x_start + 0.07,
|
||||
y_bar + bar_height / 2,
|
||||
f"{int(round(rmr_kcal))}kCals",
|
||||
ha="left",
|
||||
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,
|
||||
weight="bold",
|
||||
)
|
||||
|
||||
# Indicator Triangle
|
||||
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_xlim(x_start, x_end)
|
||||
ax.set_ylim(0, 1)
|
||||
ax.axis("off")
|
||||
|
||||
@@ -1214,6 +1297,7 @@ class GraphGenerator:
|
||||
) -> str:
|
||||
"""
|
||||
Generate fuel source chart (Fats vs Carbs).
|
||||
Matches the notebook implementation with proper tick styling.
|
||||
|
||||
Args:
|
||||
fat_percentage: Fat percentage at rest
|
||||
@@ -1224,67 +1308,112 @@ class GraphGenerator:
|
||||
"""
|
||||
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
|
||||
optimal_point = 75
|
||||
|
||||
# Main Bars (Fats and Carbs)
|
||||
# Fats bar (yellow)
|
||||
# Let the bars be a bit thicker as well: increase bar height and y
|
||||
fats_bar = FancyBboxPatch(
|
||||
(0, 0.4),
|
||||
(0, 0.36),
|
||||
fat_percentage,
|
||||
0.2,
|
||||
0.28,
|
||||
boxstyle="round,pad=0,rounding_size=0.1",
|
||||
ec="none",
|
||||
fc="#FEEAAB",
|
||||
)
|
||||
ax.add_patch(fats_bar)
|
||||
|
||||
# Carbs bar (blue) - starts where the fats bar ends
|
||||
carbs_bar = FancyBboxPatch(
|
||||
(fat_percentage, 0.4),
|
||||
(fat_percentage, 0.36),
|
||||
carb_percentage,
|
||||
0.2,
|
||||
0.28,
|
||||
boxstyle="round,pad=0,rounding_size=0.1",
|
||||
ec="none",
|
||||
fc="#A7F5FF",
|
||||
)
|
||||
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(
|
||||
fat_percentage / 2,
|
||||
0.5,
|
||||
f"Fats\n{fat_percentage:.1f}%",
|
||||
f"Fats\n{fat_percentage:.0f}%",
|
||||
ha="center",
|
||||
va="center",
|
||||
color="#333333",
|
||||
fontsize=12,
|
||||
weight="bold",
|
||||
**label_fontprops,
|
||||
)
|
||||
ax.text(
|
||||
fat_percentage + carb_percentage / 2,
|
||||
0.5,
|
||||
f"Carbs\n{carb_percentage:.1f}%",
|
||||
f"Carbs\n{100 - fat_percentage:.0f}%",
|
||||
ha="center",
|
||||
va="center",
|
||||
color="#333333",
|
||||
fontsize=12,
|
||||
weight="bold",
|
||||
**label_fontprops,
|
||||
)
|
||||
|
||||
# Add 'Optimal' label
|
||||
ax.text(optimal_point, 0.75, "Optimal", ha="center", va="center", fontsize=12)
|
||||
|
||||
# Indicator Triangle
|
||||
ax.plot(
|
||||
fat_percentage, 0.65, "v", markersize=15, color="#606060", clip_on=False
|
||||
ax.text(
|
||||
optimal_point,
|
||||
0.9,
|
||||
"Optimal",
|
||||
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]
|
||||
tick_color = "#606060"
|
||||
for pos in positions:
|
||||
# Smallest ticks (first and last) are thicker
|
||||
if pos == 0:
|
||||
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",
|
||||
)
|
||||
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,
|
||||
@@ -1293,15 +1422,19 @@ class GraphGenerator:
|
||||
va="center",
|
||||
fontsize=12,
|
||||
color="#333333",
|
||||
weight="bold",
|
||||
)
|
||||
ax.plot(
|
||||
[pos, pos],
|
||||
[0.25, 0.37],
|
||||
color=tick_color,
|
||||
lw=8,
|
||||
solid_capstyle="butt",
|
||||
)
|
||||
ax.plot([pos, pos], [0.35, 0.39], color="grey", lw=5)
|
||||
|
||||
# Add a special tick for the 'Optimal' point
|
||||
ax.plot([optimal_point, optimal_point], [0.6, 0.7], color="black", lw=2)
|
||||
|
||||
# Chart Styling
|
||||
ax.set_title("Fuel Source", fontsize=18, weight="bold", loc="left")
|
||||
ax.set_xlim(0, 100) # Normalized scale for uniformity
|
||||
# Chart Styling - uniform style for title
|
||||
ax.set_title("Fuel Source", fontsize=14, weight="bold", loc="left", pad=22)
|
||||
ax.set_xlim(0, 100)
|
||||
ax.set_ylim(0, 1)
|
||||
ax.axis("off")
|
||||
|
||||
|
||||
@@ -524,9 +524,36 @@ class ReportGeneratorService:
|
||||
}
|
||||
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(
|
||||
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
|
||||
|
||||
|
||||
+35
-26
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user