feat: add medicine dose graph plotting and toggle functionality with comprehensive tests
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled

This commit is contained in:
William Valentin
2025-07-30 13:18:25 -07:00
parent 0a8d27957f
commit d14d19e7d9
6 changed files with 591 additions and 15 deletions

View File

@@ -24,6 +24,11 @@ class GraphManager:
"anxiety": tk.BooleanVar(value=True),
"sleep": tk.BooleanVar(value=True),
"appetite": tk.BooleanVar(value=True),
"bupropion": tk.BooleanVar(value=True), # Show by default (most used)
"hydroxyzine": tk.BooleanVar(value=False),
"gabapentin": tk.BooleanVar(value=False),
"propranolol": tk.BooleanVar(value=True), # Show by default (commonly used)
"quetiapine": tk.BooleanVar(value=False),
}
# Create control frame for toggles
@@ -59,21 +64,46 @@ class GraphManager:
side="left", padx=5
)
toggle_configs = [
# Symptoms toggles
symptoms_frame = ttk.LabelFrame(self.control_frame, text="Symptoms")
symptoms_frame.pack(side="left", padx=5, pady=2)
symptom_configs = [
("depression", "Depression"),
("anxiety", "Anxiety"),
("sleep", "Sleep"),
("appetite", "Appetite"),
]
for key, label in toggle_configs:
for key, label in symptom_configs:
checkbox = ttk.Checkbutton(
self.control_frame,
symptoms_frame,
text=label,
variable=self.toggle_vars[key],
command=self._handle_toggle_changed,
)
checkbox.pack(side="left", padx=5)
checkbox.pack(side="left", padx=3)
# Medicines toggles
medicines_frame = ttk.LabelFrame(self.control_frame, text="Medicines")
medicines_frame.pack(side="left", padx=5, pady=2)
medicine_configs = [
("bupropion", "Bupropion"),
("hydroxyzine", "Hydroxyzine"),
("gabapentin", "Gabapentin"),
("propranolol", "Propranolol"),
("quetiapine", "Quetiapine"),
]
for key, label in medicine_configs:
checkbox = ttk.Checkbutton(
medicines_frame,
text=label,
variable=self.toggle_vars[key],
command=self._handle_toggle_changed,
)
checkbox.pack(side="left", padx=3)
def _handle_toggle_changed(self) -> None:
"""Handle toggle changes by replotting the graph."""
@@ -116,12 +146,59 @@ class GraphManager:
)
has_plotted_series = True
# Plot medicine dose data
medicine_colors = {
"bupropion": "#FF6B6B", # Red
"hydroxyzine": "#4ECDC4", # Teal
"gabapentin": "#45B7D1", # Blue
"propranolol": "#96CEB4", # Green
"quetiapine": "#FFEAA7", # Yellow
}
medicines = [
"bupropion",
"hydroxyzine",
"gabapentin",
"propranolol",
"quetiapine",
]
for medicine in medicines:
dose_column = f"{medicine}_doses"
if self.toggle_vars[medicine].get() and dose_column in df.columns:
# Calculate daily dose totals
daily_doses = []
for dose_str in df[dose_column]:
total_dose = self._calculate_daily_dose(dose_str)
daily_doses.append(total_dose)
# Only plot if there are non-zero doses
if any(dose > 0 for dose in daily_doses):
# Scale doses for better visibility
# (divide by 10 to fit with 0-10 scale)
scaled_doses = [dose / 10 for dose in daily_doses]
self.ax.bar(
df.index,
scaled_doses,
alpha=0.6,
color=medicine_colors.get(medicine, "#DDA0DD"),
label=f"{medicine.capitalize()} (mg/10)",
width=0.6,
bottom=-max(scaled_doses) * 1.1 if scaled_doses else -1,
)
has_plotted_series = True
# Configure graph appearance
if has_plotted_series:
self.ax.legend()
self.ax.set_title("Medication Effects Over Time")
self.ax.set_xlabel("Date")
self.ax.set_ylabel("Rating (0-10)")
self.ax.set_ylabel("Rating (0-10) / Dose (mg)")
# Adjust y-axis to accommodate medicine bars at bottom
current_ylim = self.ax.get_ylim()
self.ax.set_ylim(bottom=current_ylim[0], top=max(10, current_ylim[1]))
self.fig.autofmt_xdate()
# Redraw the canvas
@@ -144,6 +221,46 @@ class GraphManager:
label=label,
)
def _calculate_daily_dose(self, dose_str: str) -> float:
"""Calculate total daily dose from dose string format."""
if not dose_str or pd.isna(dose_str) or str(dose_str).lower() == "nan":
return 0.0
total_dose = 0.0
# Handle different separators and clean the string
dose_str = str(dose_str).replace("", "").strip()
# Split by | or by spaces if no | present
dose_entries = dose_str.split("|") if "|" in dose_str else [dose_str]
for entry in dose_entries:
entry = entry.strip()
if not entry:
continue
try:
if ":" in entry:
# Extract dose part after the timestamp
_, dose_part = entry.split(":", 1)
else:
# Handle cases where there's no timestamp
dose_part = entry
# Extract numeric part from dose (e.g., "150mg" -> 150)
dose_value = ""
for char in dose_part:
if char.isdigit() or char == ".":
dose_value += char
elif dose_value: # Stop at first non-digit after finding digits
break
if dose_value:
total_dose += float(dose_value)
except (ValueError, IndexError):
continue
return total_dose
def close(self) -> None:
"""Clean up resources."""
plt.close(self.fig)