feat: Implement dose calculation fix and enhance legend feature
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled

- Fixed dose calculation logic in `_calculate_daily_dose` to correctly parse timestamps with multiple colons.
- Added comprehensive test cases for various dose formats and edge cases in `test_dose_calculation.py`.
- Enhanced graph legend to display individual medicines with average dosages and track medicines without dose data.
- Updated legend styling and positioning for better readability and organization.
- Created new tests for enhanced legend functionality, including handling of medicines with and without data.
- Improved mocking for matplotlib components in tests to prevent TypeErrors.
This commit is contained in:
William Valentin
2025-07-30 14:22:07 -07:00
parent d14d19e7d9
commit b76191d66d
12 changed files with 1042 additions and 68 deletions

114
scripts/test_dose_calc.py Normal file
View File

@@ -0,0 +1,114 @@
"""
Direct test of dose calculation functionality.
"""
import pandas as pd
import pytest
def calculate_daily_dose(dose_str: str) -> float:
"""Calculate total daily dose from dose string format - copied from GraphManager."""
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:
# Extract dose part after the last colon (timestamp:dose format)
dose_part = entry.split(":")[-1] if ":" in entry else 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
class TestDoseCalculation:
"""Test dose calculation functionality."""
def test_standard_format(self):
"""Test dose calculation with standard timestamp:dose format."""
# Single dose
dose_str = "2025-07-28 18:59:45:150mg"
assert calculate_daily_dose(dose_str) == 150.0
# Multiple doses
dose_str = "2025-07-28 18:59:45:150mg|2025-07-28 19:34:19:75mg"
assert calculate_daily_dose(dose_str) == 225.0
def test_with_symbols(self):
"""Test dose calculation with bullet symbols."""
# With bullet symbols
dose_str = "• • • • 2025-07-30 07:50:00:300"
assert calculate_daily_dose(dose_str) == 300.0
def test_decimal_values(self):
"""Test dose calculation with decimal values."""
# Decimal dose
dose_str = "2025-07-28 18:59:45:12.5mg"
assert calculate_daily_dose(dose_str) == 12.5
# Multiple decimal doses
dose_str = "2025-07-28 18:59:45:12.5mg|2025-07-28 19:34:19:7.5mg"
assert calculate_daily_dose(dose_str) == 20.0
def test_no_timestamp_format(self):
"""Test dose calculation without timestamps."""
# Simple dose without timestamp
dose_str = "100mg|50mg"
assert calculate_daily_dose(dose_str) == 150.0
def test_mixed_format(self):
"""Test dose calculation with mixed formats."""
# Mixed format
dose_str = "• 2025-07-30 22:50:00:10|75mg"
assert calculate_daily_dose(dose_str) == 85.0
def test_edge_cases(self):
"""Test dose calculation with edge cases."""
# Empty string
assert calculate_daily_dose("") == 0.0
# NaN value
assert calculate_daily_dose("nan") == 0.0
# No units
dose_str = "2025-07-28 18:59:45:10|2025-07-28 19:34:19:5"
assert calculate_daily_dose(dose_str) == 15.0
def test_malformed_data(self):
"""Test dose calculation with malformed data."""
# Malformed data
assert calculate_daily_dose("malformed:data") == 0.0
assert calculate_daily_dose("::::") == 0.0
assert calculate_daily_dose("2025-07-28:") == 0.0
assert calculate_daily_dose("2025-07-28::mg") == 0.0
def test_partial_data(self):
"""Test dose calculation with partial data."""
# No units but valid dose
assert calculate_daily_dose("2025-07-28 18:59:45:150") == 150.0
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""
Simple test script to verify dose calculation functionality.
"""
import os
import sys
# Add the src directory to Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
import tkinter as tk
from graph_manager import GraphManager
def test_dose_calculation():
"""Test the dose calculation method directly."""
# Create a minimal tkinter setup for GraphManager
root = tk.Tk()
root.withdraw() # Hide the window
frame = tk.Frame(root)
try:
# Create GraphManager instance
gm = GraphManager(frame)
# Test cases
test_cases = [
# (input, expected_output, description)
("2025-07-28 18:59:45:150mg", 150.0, "Single dose with timestamp"),
(
"2025-07-28 18:59:45:150mg|2025-07-28 19:34:19:75mg",
225.0,
"Multiple doses",
),
("• • • • 2025-07-30 07:50:00:300", 300.0, "Dose with bullet symbols"),
(
"2025-07-28 18:59:45:12.5mg|2025-07-28 19:34:19:7.5mg",
20.0,
"Decimal doses",
),
("100mg|50mg", 150.0, "Doses without timestamps"),
("• 2025-07-30 22:50:00:10|75mg", 85.0, "Mixed format"),
("", 0.0, "Empty string"),
("nan", 0.0, "NaN value"),
("2025-07-28 18:59:45:10|2025-07-28 19:34:19:5", 15.0, "No units"),
]
print("Testing dose calculation...")
all_passed = True
for input_str, expected, description in test_cases:
result = gm._calculate_daily_dose(input_str)
passed = (
abs(result - expected) < 0.001
) # Allow for floating point precision
status = "PASS" if passed else "FAIL"
print(f"{status}: {description}")
print(f" Input: '{input_str}'")
print(f" Expected: {expected}, Got: {result}")
print()
if not passed:
all_passed = False
if all_passed:
print("All dose calculation tests PASSED!")
else:
print("Some dose calculation tests FAILED!")
return all_passed
finally:
root.destroy()
if __name__ == "__main__":
success = test_dose_calculation()
sys.exit(0 if success else 1)

View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
Simple test script to verify dose calculation functionality without GUI.
"""
import os
import sys
# Add the src directory to Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
def calculate_daily_dose(dose_str: str) -> float:
"""Calculate total daily dose from dose string format."""
import pandas as pd
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:
# Extract dose part after the last colon (timestamp:dose format)
dose_part = entry.split(":")[-1] if ":" in entry else 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 test_dose_calculation():
"""Test the dose calculation method directly."""
# Test cases
test_cases = [
# (input, expected_output, description)
("2025-07-28 18:59:45:150mg", 150.0, "Single dose with timestamp"),
("2025-07-28 18:59:45:150mg|2025-07-28 19:34:19:75mg", 225.0, "Multiple doses"),
("• • • • 2025-07-30 07:50:00:300", 300.0, "Dose with bullet symbols"),
("2025-07-28 18:59:45:12.5mg|2025-07-28 19:34:19:7.5mg", 20.0, "Decimal doses"),
("100mg|50mg", 150.0, "Doses without timestamps"),
("• 2025-07-30 22:50:00:10|75mg", 85.0, "Mixed format"),
("", 0.0, "Empty string"),
("nan", 0.0, "NaN value"),
("2025-07-28 18:59:45:10|2025-07-28 19:34:19:5", 15.0, "No units"),
]
print("Testing dose calculation...")
all_passed = True
for input_str, expected, description in test_cases:
result = calculate_daily_dose(input_str)
passed = abs(result - expected) < 0.001 # Allow for floating point precision
status = "PASS" if passed else "FAIL"
print(f"{status}: {description}")
print(f" Input: '{input_str}'")
print(f" Expected: {expected}, Got: {result}")
print()
if not passed:
all_passed = False
if all_passed:
print("All dose calculation tests PASSED!")
else:
print("Some dose calculation tests FAILED!")
return all_passed
if __name__ == "__main__":
success = test_dose_calculation()
sys.exit(0 if success else 1)

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Test script to demonstrate the improved edit window."""
import sys
import tkinter as tk
from pathlib import Path
# Add src directory to path
sys.path.insert(0, str(Path(__file__).parent / "src"))
from src.logger import logger
from src.ui_manager import UIManager
def test_edit_window():
"""Test the improved edit window."""
root = tk.Tk()
root.title("Edit Window Test")
root.geometry("400x300")
ui_manager = UIManager(root, logger)
# Sample data for testing (16 fields format)
test_values = (
"12/25/2024", # date
7, # depression
5, # anxiety
6, # sleep
4, # appetite
1, # bupropion
"09:00:00:150|18:00:00:150", # bupropion_doses
1, # hydroxyzine
"21:30:00:25", # hydroxyzine_doses
0, # gabapentin
"", # gabapentin_doses
1, # propranolol
"07:00:00:10|14:00:00:10", # propranolol_doses
0, # quetiapine
"", # quetiapine_doses
# Had a good day overall, feeling better with new medication routine
"Had a good day overall, feeling better with the new medication routine.",
)
# Mock callbacks
def save_callback(win, *args):
print("Save called with args:", args)
win.destroy()
def delete_callback(win):
print("Delete called")
win.destroy()
callbacks = {"save": save_callback, "delete": delete_callback}
# Create the improved edit window
edit_win = ui_manager.create_edit_window(test_values, callbacks)
# Center the edit window
edit_win.update_idletasks()
x = (edit_win.winfo_screenwidth() // 2) - (edit_win.winfo_width() // 2)
y = (edit_win.winfo_screenheight() // 2) - (edit_win.winfo_height() // 2)
edit_win.geometry(f"+{x}+{y}")
root.mainloop()
if __name__ == "__main__":
test_edit_window()

115
scripts/test_scrolling.py Normal file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
Test script to verify mouse wheel scrolling works in both the new entry window
and edit window of TheChart application.
"""
import logging
import tkinter as tk
from src.ui_manager import UIManager
def test_scrolling():
"""Test both new entry and edit window scrolling."""
print("Testing mouse wheel scrolling functionality...")
# Create test root window
root = tk.Tk()
root.title("Scrolling Test")
root.geometry("800x600")
# Create logger
logger = logging.getLogger("test")
logger.setLevel(logging.DEBUG)
# Create UI manager
ui_manager = UIManager(root, logger)
# Create main frame
main_frame = tk.Frame(root)
main_frame.pack(fill="both", expand=True)
main_frame.grid_rowconfigure(0, weight=1)
main_frame.grid_rowconfigure(1, weight=1)
main_frame.grid_columnconfigure(0, weight=1)
main_frame.grid_columnconfigure(1, weight=1)
# Test 1: Create input frame (new entry window)
print("✓ Creating new entry input frame with mouse wheel scrolling...")
ui_manager.create_input_frame(main_frame)
# Test 2: Create edit window
def test_edit_window():
print("✓ Creating edit window with mouse wheel scrolling...")
# Sample data for edit window
test_values = (
"01/15/2025", # date
"3", # depression
"5", # anxiety
"7", # sleep
"4", # appetite
"1", # bupropion
"09:00: 150", # bup_doses
"0", # hydroxyzine
"", # hydro_doses
"1", # gabapentin
"20:00: 100", # gaba_doses
"0", # propranolol
"", # prop_doses
"0", # quetiapine
"", # quet_doses
"Test note", # note
)
callbacks = {
"save": lambda *args: print("Save callback called"),
"delete": lambda *args: print("Delete callback called"),
}
edit_window = ui_manager.create_edit_window(test_values, callbacks)
return edit_window
# Add test button
test_button = tk.Button(
main_frame,
text="Test Edit Window Scrolling",
command=test_edit_window,
font=("TkDefaultFont", 12),
bg="#4CAF50",
fg="white",
padx=20,
pady=10,
)
test_button.grid(row=2, column=0, columnspan=2, pady=20)
# Add instructions
instructions = tk.Label(
main_frame,
text="Instructions:\n\n"
"1. Use mouse wheel anywhere in the 'New Entry' section to test scrolling\n"
"2. Click 'Test Edit Window Scrolling' button\n"
"3. Use mouse wheel anywhere in the edit window to test scrolling\n"
"4. Both windows should scroll smoothly with mouse wheel\n\n"
"✓ Mouse wheel scrolling has been enhanced for both windows!",
font=("TkDefaultFont", 10),
justify="left",
bg="#E8F5E8",
padx=20,
pady=15,
)
instructions.grid(row=3, column=0, columnspan=2, padx=20, pady=10, sticky="ew")
print("✓ Test setup complete!")
print("\nMouse wheel scrolling features implemented:")
print(" • Recursive binding to all child widgets")
print(" • Platform-specific event handling (Windows/Linux)")
print(" • Focus management for consistent scrolling")
print(" • Works anywhere within the scrollable areas")
print("\nTest the scrolling by moving your mouse wheel over any part of the")
print("'New Entry' section or the edit window when opened.")
root.mainloop()
if __name__ == "__main__":
test_scrolling()