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
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user