9 Commits

Author SHA1 Message Date
William Valentin 1d310dd081 feat: Update version to 1.7.5 in Makefile, docker-build.sh, and pyproject.toml
Build and Push Docker Image / build-and-push (push) Has been cancelled
2025-08-01 14:45:58 -07:00
William Valentin abd1fa33cf refactor: Simplify UI creation methods by removing dynamic variants and consolidating functionality 2025-08-01 14:41:58 -07:00
William Valentin 03ef9e761a feat: Update version to 1.7.4 in Makefile, docker-build.sh, and pyproject.toml
Build and Push Docker Image / build-and-push (push) Has been cancelled
2025-08-01 14:12:06 -07:00
William Valentin ca1f8c976d fix: notes are saved again
feat: Add test scripts for note saving and updating functionality
2025-08-01 14:09:29 -07:00
William Valentin 7392709a27 feat: Uncomment .vscode directory in .gitignore to include IDE settings 2025-08-01 13:25:47 -07:00
William Valentin 623050478a feat: Update version to 1.7.3 in Makefile, docker-build.sh, and pyproject.toml 2025-08-01 13:21:48 -07:00
William Valentin 41d91d9c30 feat: Center main window on screen during initialization
Build and Push Docker Image / build-and-push (push) Has been cancelled
2025-08-01 13:05:24 -07:00
William Valentin 14d9943665 feat: Update medicine toggles to be unchecked by default for improved user experience
Build and Push Docker Image / build-and-push (push) Has been cancelled
2025-08-01 12:53:19 -07:00
William Valentin 13a4826415 feat: Enhance DataManager and GraphManager with performance optimizations and caching 2025-08-01 12:46:51 -07:00
11 changed files with 633 additions and 596 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ htmlcov/
.pylint.d/ .pylint.d/
# IDEs and editors # IDEs and editors
#.vscode/ .vscode/
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
.idea/ .idea/
+3 -18
View File
@@ -1,5 +1,5 @@
TARGET=thechart TARGET=thechart
VERSION=1.6.1 VERSION=1.7.5
ROOT=/home/will ROOT=/home/will
ICON=chart-671.png ICON=chart-671.png
SHELL=fish SHELL=fish
@@ -85,7 +85,7 @@ install: ## Set up the development environment
@echo "To run tests: make test" @echo "To run tests: make test"
build: ## Build the Docker image build: ## Build the Docker image
@echo "Building the Docker image..." @echo "Building the Docker image..."
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE} --push . docker buildx build --platform linux/amd64 -t ${IMAGE} --push .
deploy: ## Deploy the application as a standalone executable deploy: ## Deploy the application as a standalone executable
@echo "Deploying the application..." @echo "Deploying the application..."
pyinstaller --name ${TARGET} --optimize 2 --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon='${ICON}' --add-data="./.env:." --add-data='./chart-671.png:.' --add-data='./thechart_data.csv:.' --log-level=DEBUG src/main.py pyinstaller --name ${TARGET} --optimize 2 --onefile --windowed --hidden-import='PIL._tkinter_finder' --icon='${ICON}' --add-data="./.env:." --add-data='./chart-671.png:.' --add-data='./thechart_data.csv:.' --log-level=DEBUG src/main.py
@@ -121,21 +121,6 @@ test-watch: ## Run tests in watch mode
test-debug: ## Run tests with debug output test-debug: ## Run tests with debug output
@echo "Running tests with debug output..." @echo "Running tests with debug output..."
.venv/bin/python -m pytest tests/ -v -s --tb=long --cov=src .venv/bin/python -m pytest tests/ -v -s --tb=long --cov=src
test-dose-tracking: ## Test the dose tracking functionality
@echo "Testing dose tracking functionality..."
.venv/bin/python scripts/test_dose_tracking.py
test-scrollable-input: ## Test the scrollable input frame UI
@echo "Testing scrollable input frame..."
.venv/bin/python scripts/test_scrollable_input.py
test-edit-functionality: ## Test the enhanced edit functionality
@echo "Testing edit functionality..."
.venv/bin/python scripts/test_edit_functionality.py
test-edit-window: $(VENV_ACTIVATE) ## Test edit window functionality (save and delete)
@echo "Running edit window functionality test..."
$(PYTHON) scripts/test_edit_window_functionality.py
test-dose-editing: $(VENV_ACTIVATE) ## Test dose editing functionality in edit window
@echo "Running dose editing functionality test..."
$(PYTHON) scripts/test_dose_editing_functionality.py
lint: ## Run the linter lint: ## Run the linter
@echo "Running the linter..." @echo "Running the linter..."
docker-compose exec ${TARGET} pipenv run pre-commit run --all-files docker-compose exec ${TARGET} pipenv run pre-commit run --all-files
@@ -157,4 +142,4 @@ commit-emergency: ## Emergency commit (bypasses pre-commit hooks) - USE SPARINGL
@read -p "Enter commit message: " msg; \ @read -p "Enter commit message: " msg; \
git add . && git commit --no-verify -m "$$msg" git add . && git commit --no-verify -m "$$msg"
@echo "✅ Emergency commit completed. Please run tests manually when possible." @echo "✅ Emergency commit completed. Please run tests manually when possible."
.PHONY: install clean reinstall check-env build attach deploy run start stop test lint format shell requirements commit-emergency test-dose-tracking test-scrollable-input test-edit-functionality test-edit-window test-dose-editing migrate-csv help .PHONY: install clean reinstall check-env build attach deploy run start stop test lint format shell requirements commit-emergency help
+3 -3
View File
@@ -1,19 +1,19 @@
#!/usr/bin/bash #!/usr/bin/bash
CONTAINER_ENGINE="docker" # podman | docker CONTAINER_ENGINE="docker" # podman | docker
VERSION="v1.0.0" VERSION="v1.7.5"
REGISTRY="gitea-http.taildb3494.ts.net/will/thechart" REGISTRY="gitea-http.taildb3494.ts.net/will/thechart"
if [ "$CONTAINER_ENGINE" == "podman" ]; if [ "$CONTAINER_ENGINE" == "podman" ];
then then
buildah build \ buildah build \
-t $REGISTRY:$VERSION \ -t $REGISTRY:$VERSION \
--platform linux/amd64,linux/arm64/v8 \ --platform linux/amd64 \
--no-cache . --no-cache .
else else
DOCKER_BUILDKIT=1 \ DOCKER_BUILDKIT=1 \
docker buildx build \ docker buildx build \
--platform linux/amd64,linux/arm64/v8 \ --platform linux/amd64 \
-t $REGISTRY:$VERSION \ -t $REGISTRY:$VERSION \
--no-cache \ --no-cache \
--push . --push .
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "thechart" name = "thechart"
version = "1.6.1" version = "1.7.5"
description = "Chart to monitor your medication intake over time." description = "Chart to monitor your medication intake over time."
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
Test script to verify note field saving functionality
"""
import logging
import os
import sys
import pandas as pd
# Add src directory to path to import modules
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from data_manager import DataManager
from medicine_manager import MedicineManager
from pathology_manager import PathologyManager
def test_note_saving():
"""Test note saving functionality by checking current data"""
print("Testing note saving functionality...")
# Initialize logger
logger = logging.getLogger("test")
logger.setLevel(logging.INFO)
# Initialize managers
medicine_manager = MedicineManager("medicines.json")
pathology_manager = PathologyManager("pathologies.json")
data_manager = DataManager(
"thechart_data.csv", logger, medicine_manager, pathology_manager
)
# Load current data
df = data_manager.load_data()
if df.empty:
print("No data found in CSV file")
return
print(f"Found {len(df)} entries in the data file")
# Check if we have any entries with notes
entries_with_notes = df[df["note"].notna() & (df["note"] != "")].copy()
print(f"Entries with notes: {len(entries_with_notes)}")
if len(entries_with_notes) > 0:
print("\nEntries with notes:")
for _, row in entries_with_notes.iterrows():
note_preview = (
row["note"][:50] + "..." if len(str(row["note"])) > 50 else row["note"]
)
print(f" Date: {row['date']}, Note: {note_preview}")
# Show the most recent entry
if len(df) > 0:
latest_entry = df.iloc[-1]
print("\nMost recent entry:")
print(f" Date: {latest_entry['date']}")
print(f" Note: '{latest_entry['note']}'")
print(f" Note length: {len(str(latest_entry['note']))}")
is_empty = pd.isna(latest_entry["note"]) or latest_entry["note"] == ""
print(f" Note is empty/null: {is_empty}")
if __name__ == "__main__":
test_note_saving()
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""
Test the update_entry functionality with notes
"""
import logging
import os
import sys
# Add src directory to path to import modules
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
from data_manager import DataManager
from medicine_manager import MedicineManager
from pathology_manager import PathologyManager
def test_update_entry_with_note():
"""Test updating an entry with a note"""
print("Testing update_entry functionality with notes...")
# Initialize logger
logger = logging.getLogger("test")
logger.setLevel(logging.DEBUG)
# Add console handler to see debug output
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(levelname)s - %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
# Initialize managers
medicine_manager = MedicineManager("medicines.json")
pathology_manager = PathologyManager("pathologies.json")
data_manager = DataManager(
"thechart_data.csv", logger, medicine_manager, pathology_manager
)
# Load current data
df = data_manager.load_data()
if df.empty:
print("No data found in CSV file")
return
print(f"Found {len(df)} entries in the data file")
# Find the most recent entry to test with
latest_entry = df.iloc[-1].copy()
original_date = latest_entry["date"]
print(f"Testing with entry: {original_date}")
print(f"Current note: '{latest_entry['note']}'")
# Create test values - keep everything the same but change the note
test_note = "This is a test note to verify saving functionality!"
# Build values list (same format as the UI would send)
values = [original_date] # date
# Add pathology values
pathology_keys = pathology_manager.get_pathology_keys()
for key in pathology_keys:
values.append(latest_entry.get(key, 0))
# Add medicine values and doses
medicine_keys = medicine_manager.get_medicine_keys()
for key in medicine_keys:
values.append(latest_entry.get(key, 0)) # medicine checkbox
values.append(latest_entry.get(f"{key}_doses", "")) # medicine doses
# Add the test note
values.append(test_note)
print(f"Values to save: {values}")
print(f"Note in values: '{values[-1]}'")
# Test the update
success = data_manager.update_entry(original_date, values)
if success:
print("Update successful!")
# Reload and verify
df_after = data_manager.load_data()
updated_entry = df_after[df_after["date"] == original_date].iloc[0]
print(f"Note after update: '{updated_entry['note']}'")
print(f"Note correctly saved: {updated_entry['note'] == test_note}")
# Reset the note back to original
values[-1] = latest_entry["note"]
data_manager.update_entry(original_date, values)
print("Reverted note back to original")
else:
print("Update failed!")
if __name__ == "__main__":
test_update_entry_with_note()
+161 -55
View File
@@ -9,7 +9,7 @@ from pathology_manager import PathologyManager
class DataManager: class DataManager:
"""Handle all data operations for the application.""" """Handle all data operations for the application with performance optimizations."""
def __init__( def __init__(
self, self,
@@ -22,10 +22,21 @@ class DataManager:
self.logger: logging.Logger = logger self.logger: logging.Logger = logger
self.medicine_manager = medicine_manager self.medicine_manager = medicine_manager
self.pathology_manager = pathology_manager self.pathology_manager = pathology_manager
# Cache for loaded data to avoid repeated file I/O
self._data_cache: pd.DataFrame | None = None
self._cache_timestamp: float = 0
self._headers_cache: tuple[str, ...] | None = None
self._dtype_cache: dict[str, type] | None = None
self._initialize_csv_file() self._initialize_csv_file()
def _get_csv_headers(self) -> list[str]: def _get_csv_headers(self) -> tuple[str, ...]:
"""Get CSV headers based on current pathology and medicine configuration.""" """Get CSV headers based on current pathology and medicine configuration.
Cached to avoid repeated computation."""
if self._headers_cache is not None:
return self._headers_cache
# Start with date # Start with date
headers = ["date"] headers = ["date"]
@@ -37,7 +48,9 @@ class DataManager:
for medicine_key in self.medicine_manager.get_medicine_keys(): for medicine_key in self.medicine_manager.get_medicine_keys():
headers.extend([medicine_key, f"{medicine_key}_doses"]) headers.extend([medicine_key, f"{medicine_key}_doses"])
return headers + ["note"] result = tuple(headers + ["note"])
self._headers_cache = result
return result
def _initialize_csv_file(self) -> None: def _initialize_csv_file(self) -> None:
"""Create CSV file with headers if it doesn't exist or is empty.""" """Create CSV file with headers if it doesn't exist or is empty."""
@@ -46,27 +59,74 @@ class DataManager:
writer = csv.writer(file) writer = csv.writer(file)
writer.writerow(self._get_csv_headers()) writer.writerow(self._get_csv_headers())
def _invalidate_cache(self) -> None:
"""Invalidate the data cache when data changes."""
self._data_cache = None
self._cache_timestamp = 0
def _should_reload_data(self) -> bool:
"""Check if data should be reloaded based on file modification time."""
if self._data_cache is None:
return True
try:
file_mtime = os.path.getmtime(self.filename)
return file_mtime > self._cache_timestamp
except OSError:
return True
def _get_dtype_dict(self) -> dict[str, type]:
"""Get pandas dtype dictionary for efficient reading.
Cached to avoid recreation."""
if self._dtype_cache is not None:
return self._dtype_cache
dtype_dict = {"date": str, "note": str}
# Add pathology types
for pathology_key in self.pathology_manager.get_pathology_keys():
dtype_dict[pathology_key] = int
# Add medicine types
for medicine_key in self.medicine_manager.get_medicine_keys():
dtype_dict[medicine_key] = int
dtype_dict[f"{medicine_key}_doses"] = str
self._dtype_cache = dtype_dict
return dtype_dict
def load_data(self) -> pd.DataFrame: def load_data(self) -> pd.DataFrame:
"""Load data from CSV file.""" """Load data from CSV file with caching for better performance."""
if not os.path.exists(self.filename) or os.path.getsize(self.filename) == 0: if not os.path.exists(self.filename) or os.path.getsize(self.filename) == 0:
self.logger.warning("CSV file is empty or doesn't exist. No data to load.") self.logger.warning("CSV file is empty or doesn't exist. No data to load.")
return pd.DataFrame() return pd.DataFrame()
# Use cached data if available and file hasn't changed
if not self._should_reload_data():
return self._data_cache.copy()
try: try:
# Build dtype dictionary dynamically # Use pre-built dtype dictionary for faster parsing
dtype_dict = {"date": str, "note": str} dtype_dict = self._get_dtype_dict()
# Add pathology types # Read with optimized settings
for pathology_key in self.pathology_manager.get_pathology_keys(): df: pd.DataFrame = pd.read_csv(
dtype_dict[pathology_key] = int self.filename,
dtype=dtype_dict,
na_filter=False, # Don't convert to NaN, keep as empty strings
engine="c", # Use faster C engine
)
# Add medicine types # Sort only if needed (check if already sorted)
for medicine_key in self.medicine_manager.get_medicine_keys(): if len(df) > 1 and not df["date"].is_monotonic_increasing:
dtype_dict[medicine_key] = int df = df.sort_values(by="date").reset_index(drop=True)
dtype_dict[f"{medicine_key}_doses"] = str
# Cache the data and timestamp
self._data_cache = df.copy()
self._cache_timestamp = os.path.getmtime(self.filename)
return df.copy()
df: pd.DataFrame = pd.read_csv(self.filename, dtype=dtype_dict).fillna("")
return df.sort_values(by="date").reset_index(drop=True)
except pd.errors.EmptyDataError: except pd.errors.EmptyDataError:
self.logger.warning("CSV file is empty. No data to load.") self.logger.warning("CSV file is empty. No data to load.")
return pd.DataFrame() return pd.DataFrame()
@@ -75,69 +135,104 @@ class DataManager:
return pd.DataFrame() return pd.DataFrame()
def add_entry(self, entry_data: list[str | int]) -> bool: def add_entry(self, entry_data: list[str | int]) -> bool:
"""Add a new entry to the CSV file.""" """Add a new entry to the CSV file with optimized duplicate checking."""
try: try:
# Check if date already exists # Quick duplicate check using cached data if available
df: pd.DataFrame = self.load_data()
date_to_add: str = str(entry_data[0]) date_to_add: str = str(entry_data[0])
if not df.empty and date_to_add in df["date"].values: if self._data_cache is not None:
self.logger.warning(f"Entry with date {date_to_add} already exists.") # Use cached data for duplicate check
return False if date_to_add in self._data_cache["date"].values:
self.logger.warning(
f"Entry with date {date_to_add} already exists."
)
return False
else:
# Fallback to loading data if no cache
df: pd.DataFrame = self.load_data()
if not df.empty and date_to_add in df["date"].values:
self.logger.warning(
f"Entry with date {date_to_add} already exists."
)
return False
# Write to file
with open(self.filename, mode="a", newline="") as file: with open(self.filename, mode="a", newline="") as file:
writer = csv.writer(file) writer = csv.writer(file)
writer.writerow(entry_data) writer.writerow(entry_data)
# Invalidate cache since data changed
self._invalidate_cache()
return True return True
except Exception as e: except Exception as e:
self.logger.error(f"Error adding entry: {str(e)}") self.logger.error(f"Error adding entry: {str(e)}")
return False return False
def update_entry(self, original_date: str, values: list[str | int]) -> bool: def update_entry(self, original_date: str, values: list[str | int]) -> bool:
"""Update an existing entry identified by original_date.""" """Update an existing entry identified by original_date
with optimized processing."""
try: try:
df: pd.DataFrame = self.load_data() df: pd.DataFrame = self.load_data()
new_date: str = str(values[0]) new_date: str = str(values[0])
# If the date is being changed, check if the new date already exists # Optimized duplicate check
if original_date != new_date and new_date in df["date"].values: if original_date != new_date:
date_exists = (df["date"] == new_date).any()
if date_exists:
self.logger.warning(
f"Cannot update: entry with date {new_date} already exists."
)
return False
# Get current CSV headers to match with values
headers = list(self._get_csv_headers())
# Ensure we have the right number of values with optimized padding
if len(values) < len(headers):
# Pad with defaults efficiently
padding_needed = len(headers) - len(values)
for i in range(padding_needed):
header_idx = len(values) + i
if header_idx < len(headers):
header = headers[header_idx]
if header == "note" or header.endswith("_doses"):
values.append("")
else:
values.append(0)
# Use vectorized update for better performance
mask = df["date"] == original_date
if mask.any():
df.loc[mask, headers] = values
# Write back to CSV with optimized method
df.to_csv(self.filename, index=False, mode="w")
self._invalidate_cache()
return True
else:
self.logger.warning( self.logger.warning(
f"Cannot update: entry with date {new_date} already exists." f"Entry with date {original_date} not found for update."
) )
return False return False
# Get current CSV headers to match with values
headers = self._get_csv_headers()
# Ensure we have the right number of values
if len(values) != len(headers):
self.logger.warning(
f"Value count mismatch: expected {len(headers)}, got {len(values)}"
)
# Pad with defaults if too few values
while len(values) < len(headers):
header = headers[len(values)]
if header == "note" or header.endswith("_doses"):
values.append("")
else:
values.append(0)
# Update the row using column names
df.loc[df["date"] == original_date, headers] = values
df.to_csv(self.filename, index=False)
return True
except Exception as e: except Exception as e:
self.logger.error(f"Error updating entry: {str(e)}") self.logger.error(f"Error updating entry: {str(e)}")
return False return False
def delete_entry(self, date: str) -> bool: def delete_entry(self, date: str) -> bool:
"""Delete an entry identified by date.""" """Delete an entry identified by date with optimized processing."""
try: try:
df: pd.DataFrame = self.load_data() df: pd.DataFrame = self.load_data()
# Remove the row with the matching date original_len = len(df)
# Use vectorized filtering for better performance
df = df[df["date"] != date] df = df[df["date"] != date]
# Write the updated dataframe back to the CSV
df.to_csv(self.filename, index=False) # Only write if something was actually deleted
if len(df) < original_len:
df.to_csv(self.filename, index=False, mode="w")
self._invalidate_cache()
return True return True
except Exception as e: except Exception as e:
self.logger.error(f"Error deleting entry: {str(e)}") self.logger.error(f"Error deleting entry: {str(e)}")
@@ -146,23 +241,34 @@ class DataManager:
def get_today_medicine_doses( def get_today_medicine_doses(
self, date: str, medicine_name: str self, date: str, medicine_name: str
) -> list[tuple[str, str]]: ) -> list[tuple[str, str]]:
"""Get list of (timestamp, dose) tuples for a medicine on a given date.""" """Get list of (timestamp, dose) tuples for a medicine on a given date
with caching."""
try: try:
df: pd.DataFrame = self.load_data() df: pd.DataFrame = self.load_data()
if df.empty or date not in df["date"].values: if df.empty:
return []
# Use vectorized filtering for better performance
date_mask = df["date"] == date
if not date_mask.any():
return [] return []
dose_column = f"{medicine_name}_doses" dose_column = f"{medicine_name}_doses"
doses_str = df.loc[df["date"] == date, dose_column].iloc[0] if dose_column not in df.columns:
return []
doses_str = df.loc[date_mask, dose_column].iloc[0]
if not doses_str: if not doses_str:
return [] return []
# Optimized dose parsing
doses = [] doses = []
for dose_entry in doses_str.split("|"): for dose_entry in doses_str.split("|"):
if ":" in dose_entry: if ":" in dose_entry:
timestamp, dose = dose_entry.split(":", 1) parts = dose_entry.split(":", 1)
doses.append((timestamp, dose)) if len(parts) == 2:
doses.append((parts[0], parts[1]))
return doses return doses
except Exception as e: except Exception as e:
+221 -169
View File
@@ -12,7 +12,8 @@ from pathology_manager import PathologyManager
class GraphManager: class GraphManager:
"""Handle all graph-related operations for the application.""" """Optimized version - Handle all graph-related operations for the
application with performance improvements."""
def __init__( def __init__(
self, self,
@@ -24,166 +25,206 @@ class GraphManager:
self.medicine_manager = medicine_manager self.medicine_manager = medicine_manager
self.pathology_manager = pathology_manager self.pathology_manager = pathology_manager
# Configure graph frame to expand # Initialize matplotlib with optimized settings
self.parent_frame.grid_rowconfigure(0, weight=1) self.fig: matplotlib.figure.Figure = plt.figure(figsize=(10, 6), dpi=80)
self.parent_frame.grid_columnconfigure(0, weight=1) self.ax: Axes = self.fig.add_subplot(111)
self._initialize_toggle_vars() # Cache for current data to avoid reprocessing
self.current_data: pd.DataFrame = pd.DataFrame()
self._last_plot_hash: str = ""
# Initialize UI components
self.toggle_vars: dict[str, tk.IntVar] = {}
self._setup_ui() self._setup_ui()
self._initialize_toggle_vars()
def _initialize_toggle_vars(self) -> None:
"""Initialize toggle variables for chart elements."""
self.toggle_vars: dict[str, tk.BooleanVar] = {}
# Initialize pathology toggles dynamically
for pathology_key in self.pathology_manager.get_pathology_keys():
pathology = self.pathology_manager.get_pathology(pathology_key)
default_value = pathology.default_enabled if pathology else True
self.toggle_vars[pathology_key] = tk.BooleanVar(value=default_value)
# Add medicine toggles dynamically
for medicine_key in self.medicine_manager.get_medicine_keys():
medicine = self.medicine_manager.get_medicine(medicine_key)
default_value = medicine.default_enabled if medicine else False
self.toggle_vars[medicine_key] = tk.BooleanVar(value=default_value)
def _setup_ui(self) -> None:
"""Set up the UI components."""
# Create control frame for toggles
self.control_frame: ttk.Frame = ttk.Frame(self.parent_frame)
self.control_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
# Create toggle checkboxes
self._create_chart_toggles() self._create_chart_toggles()
# Create graph frame def _initialize_toggle_vars(self) -> None:
self.graph_frame: ttk.Frame = ttk.Frame(self.parent_frame) """Initialize toggle variables for chart elements with optimization."""
self.graph_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) # Initialize pathology toggles
for pathology_key in self.pathology_manager.get_pathology_keys():
self.toggle_vars[pathology_key] = tk.IntVar(value=1)
# Reconfigure parent frame for new layout # Initialize medicine toggles (unchecked by default)
self.parent_frame.grid_rowconfigure(1, weight=1) for medicine_key in self.medicine_manager.get_medicine_keys():
self.parent_frame.grid_columnconfigure(0, weight=1) self.toggle_vars[medicine_key] = tk.IntVar(value=0)
# Initialize matplotlib figure and canvas def _setup_ui(self) -> None:
self.fig: matplotlib.figure.Figure """Set up the UI components with performance optimizations."""
self.ax: Axes # Create canvas with optimized settings
self.fig, self.ax = plt.subplots() self.canvas = FigureCanvasTkAgg(self.fig, master=self.parent_frame)
self.canvas: FigureCanvasTkAgg = FigureCanvasTkAgg( self.canvas.draw_idle() # Use draw_idle for better performance
figure=self.fig, master=self.graph_frame
)
self.canvas.get_tk_widget().pack(fill="both", expand=True)
# Store current data for replotting # Pack canvas
self.current_data: pd.DataFrame = pd.DataFrame() canvas_widget = self.canvas.get_tk_widget()
canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
# Create control frame
self.control_frame = ttk.Frame(self.parent_frame)
self.control_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=2)
def _create_chart_toggles(self) -> None: def _create_chart_toggles(self) -> None:
"""Create toggle controls for chart elements.""" """Create toggle controls for chart elements with improved layout."""
ttk.Label(self.control_frame, text="Show/Hide Elements:").pack( # Pathology toggles
side="left", padx=5 pathology_frame = ttk.LabelFrame(
self.control_frame, text="Pathologies", padding="5"
) )
pathology_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
# Pathologies toggles - dynamic based on pathology manager # Use grid for better layout
pathologies_frame = ttk.LabelFrame(self.control_frame, text="Pathologies") row, col = 0, 0
pathologies_frame.pack(side="left", padx=5, pady=2)
for pathology_key in self.pathology_manager.get_pathology_keys(): for pathology_key in self.pathology_manager.get_pathology_keys():
pathology = self.pathology_manager.get_pathology(pathology_key) pathology = self.pathology_manager.get_pathology(pathology_key)
if pathology: if pathology:
checkbox = ttk.Checkbutton( display_name = pathology.display_name
pathologies_frame, text = (
text=pathology.display_name, display_name[:10] + "..."
if len(display_name) > 10
else display_name
)
cb = ttk.Checkbutton(
pathology_frame,
text=text,
variable=self.toggle_vars[pathology_key], variable=self.toggle_vars[pathology_key],
command=self._handle_toggle_changed, command=self._handle_toggle_changed,
) )
checkbox.pack(side="left", padx=3) cb.grid(row=row, column=col, sticky="w", padx=2)
col += 1
if col > 1: # 2 columns max
col = 0
row += 1
# Medicines toggles - dynamic based on medicine manager # Medicine toggles
medicines_frame = ttk.LabelFrame(self.control_frame, text="Medicines") medicine_frame = ttk.LabelFrame(
medicines_frame.pack(side="left", padx=5, pady=2) self.control_frame, text="Medicines", padding="5"
)
medicine_frame.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=2)
# Use grid for medicines too
row, col = 0, 0
for medicine_key in self.medicine_manager.get_medicine_keys(): for medicine_key in self.medicine_manager.get_medicine_keys():
medicine = self.medicine_manager.get_medicine(medicine_key) medicine = self.medicine_manager.get_medicine(medicine_key)
if medicine: if medicine:
checkbox = ttk.Checkbutton( med_name = medicine.display_name
medicines_frame, text = med_name[:10] + "..." if len(med_name) > 10 else med_name
text=medicine.display_name, cb = ttk.Checkbutton(
medicine_frame,
text=text,
variable=self.toggle_vars[medicine_key], variable=self.toggle_vars[medicine_key],
command=self._handle_toggle_changed, command=self._handle_toggle_changed,
) )
checkbox.pack(side="left", padx=3) cb.grid(row=row, column=col, sticky="w", padx=2)
col += 1
if col > 2: # 3 columns max for medicines
col = 0
row += 1
def _handle_toggle_changed(self) -> None: def _handle_toggle_changed(self) -> None:
"""Handle toggle changes by replotting the graph.""" """Handle toggle changes by replotting the graph with optimization."""
if not self.current_data.empty: if not self.current_data.empty:
self._plot_graph_data(self.current_data) self._plot_graph_data(self.current_data)
def update_graph(self, df: pd.DataFrame) -> None: def update_graph(self, df: pd.DataFrame) -> None:
"""Update the graph with new data.""" """Update the graph with new data using optimization checks."""
self.current_data = df.copy() if not df.empty else pd.DataFrame() # Create hash of data to avoid unnecessary redraws
self._plot_graph_data(df) data_hash = str(hash(str(df.values.tobytes()) if not df.empty else "empty"))
# Only update if data actually changed
if data_hash != self._last_plot_hash or self.current_data.empty:
self.current_data = df.copy() if not df.empty else pd.DataFrame()
self._last_plot_hash = data_hash
self._plot_graph_data(df)
def _plot_graph_data(self, df: pd.DataFrame) -> None: def _plot_graph_data(self, df: pd.DataFrame) -> None:
"""Plot the graph data with current toggle settings.""" """Plot the graph data with current toggle settings using optimizations."""
self.ax.clear() # Use batch updates to reduce redraws
if not df.empty: with plt.ioff(): # Turn off interactive mode for batch updates
# Convert dates and sort self.ax.clear()
df = df.copy() # Create a copy to avoid modifying the original
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values(by="date")
df.set_index(keys="date", inplace=True)
# Track if any series are plotted if not df.empty:
has_plotted_series = False # Optimize data processing
df_processed = self._preprocess_data(df)
# Plot pathology data series based on toggle states # Track if any series are plotted
for pathology_key in self.pathology_manager.get_pathology_keys(): has_plotted_series = self._plot_pathology_data(df_processed)
if self.toggle_vars[pathology_key].get(): medicine_data = self._plot_medicine_data(df_processed)
pathology = self.pathology_manager.get_pathology(pathology_key)
if pathology and pathology_key in df.columns:
label = f"{pathology.display_name} ({pathology.scale_info})"
linestyle = (
"dashed"
if pathology.scale_orientation == "inverted"
else "-"
)
self._plot_series(df, pathology_key, label, "o", linestyle)
has_plotted_series = True
# Plot medicine dose data if has_plotted_series or medicine_data["has_plotted"]:
# Get medicine colors from medicine manager self._configure_graph_appearance(medicine_data)
medicine_colors = self.medicine_manager.get_graph_colors()
# Get medicines dynamically from medicine manager # Single draw call at the end
medicines = self.medicine_manager.get_medicine_keys() self.canvas.draw_idle()
# Track medicines with and without data for legend def _preprocess_data(self, df: pd.DataFrame) -> pd.DataFrame:
medicines_with_data = [] """Preprocess data for plotting with optimizations."""
medicines_without_data = [] df = df.copy()
# Batch convert dates and sort
df["date"] = pd.to_datetime(df["date"], cache=True)
df = df.sort_values(by="date")
df.set_index(keys="date", inplace=True)
return df
for medicine in medicines: def _plot_pathology_data(self, df: pd.DataFrame) -> bool:
dose_column = f"{medicine}_doses" """Plot pathology data series with optimizations."""
if self.toggle_vars[medicine].get() and dose_column in df.columns: has_plotted_series = False
# 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 # Batch plot pathology data
if any(dose > 0 for dose in daily_doses): pathology_keys = self.pathology_manager.get_pathology_keys()
medicines_with_data.append(medicine) active_pathologies = [
# Scale doses for better visibility key
# (divide by 10 to fit with 0-10 scale) for key in pathology_keys
scaled_doses = [dose / 10 for dose in daily_doses] if self.toggle_vars[key].get() and key in df.columns
]
# Calculate total dosage for this medicine across all days for pathology_key in active_pathologies:
total_medicine_dose = sum(daily_doses) pathology = self.pathology_manager.get_pathology(pathology_key)
non_zero_doses = [d for d in daily_doses if d > 0] if pathology:
avg_dose = total_medicine_dose / len(non_zero_doses) label = f"{pathology.display_name} ({pathology.scale_info})"
linestyle = (
"dashed" if pathology.scale_orientation == "inverted" else "-"
)
self._plot_series(df, pathology_key, label, "o", linestyle)
has_plotted_series = True
# Create more informative label return has_plotted_series
def _plot_medicine_data(self, df: pd.DataFrame) -> dict:
"""Plot medicine data with optimizations."""
result = {"has_plotted": False, "with_data": [], "without_data": []}
# Get medicine colors and keys in batch
medicine_colors = self.medicine_manager.get_graph_colors()
medicines = self.medicine_manager.get_medicine_keys()
# Pre-calculate daily doses for all medicines to avoid repeated computation
medicine_doses = {}
for medicine in medicines:
dose_column = f"{medicine}_doses"
if dose_column in df.columns:
daily_doses = [
self._calculate_daily_dose(dose_str) for dose_str in df[dose_column]
]
medicine_doses[medicine] = daily_doses
# Plot medicines with data
for medicine in medicines:
if self.toggle_vars[medicine].get() and medicine in medicine_doses:
daily_doses = medicine_doses[medicine]
# Check if there's any data to plot
if any(dose > 0 for dose in daily_doses):
result["with_data"].append(medicine)
# Optimize dose scaling and bar plotting
scaled_doses = [dose / 10 for dose in daily_doses]
# Calculate statistics more efficiently
non_zero_doses = [d for d in daily_doses if d > 0]
if non_zero_doses:
avg_dose = sum(daily_doses) / len(non_zero_doses)
label = f"{medicine.capitalize()} (avg: {avg_dose:.1f}mg)" label = f"{medicine.capitalize()} (avg: {avg_dose:.1f}mg)"
# Single bar plot call
self.ax.bar( self.ax.bar(
df.index, df.index,
scaled_doses, scaled_doses,
@@ -193,56 +234,59 @@ class GraphManager:
width=0.6, width=0.6,
bottom=-max(scaled_doses) * 1.1 if scaled_doses else -1, bottom=-max(scaled_doses) * 1.1 if scaled_doses else -1,
) )
has_plotted_series = True result["has_plotted"] = True
else: else:
# Medicine is toggled on but has no dose data # Medicine is toggled on but has no dose data
if self.toggle_vars[medicine].get(): if self.toggle_vars[medicine].get():
medicines_without_data.append(medicine) result["without_data"].append(medicine)
# Configure graph appearance return result
if has_plotted_series:
# Get current legend handles and labels
handles, labels = self.ax.get_legend_handles_labels()
# Add information about medicines without data if any are toggled on def _configure_graph_appearance(self, medicine_data: dict) -> None:
if medicines_without_data: """Configure graph appearance with optimizations."""
# Add a text note about medicines without dose data # Get legend data in batch
med_list = ", ".join(medicines_without_data) handles, labels = self.ax.get_legend_handles_labels()
info_text = f"Tracked (no doses): {med_list}"
labels.append(info_text)
# Create a dummy handle for the info text (invisible)
from matplotlib.patches import Rectangle
dummy_handle = Rectangle( # Add information about medicines without data if any are toggled on
(0, 0), 1, 1, fc="w", fill=False, edgecolor="none", linewidth=0 if medicine_data["without_data"]:
) med_list = ", ".join(medicine_data["without_data"])
handles.append(dummy_handle) info_text = f"Tracked (no doses): {med_list}"
labels.append(info_text)
# Create an expanded legend with better formatting # Create dummy handle more efficiently
self.ax.legend( from matplotlib.patches import Rectangle
handles,
labels,
loc="upper left",
bbox_to_anchor=(0, 1),
ncol=2, # Display in 2 columns for better space usage
fontsize="small",
frameon=True,
fancybox=True,
shadow=True,
framealpha=0.9,
)
self.ax.set_title("Medication Effects Over Time")
self.ax.set_xlabel("Date")
self.ax.set_ylabel("Rating (0-10) / Dose (mg)")
# Adjust y-axis to accommodate medicine bars at bottom dummy_handle = Rectangle(
current_ylim = self.ax.get_ylim() (0, 0), 1, 1, fc="w", fill=False, edgecolor="none", linewidth=0
self.ax.set_ylim(bottom=current_ylim[0], top=max(10, current_ylim[1])) )
handles.append(dummy_handle)
self.fig.autofmt_xdate() # Create legend with optimized settings
if handles and labels:
self.ax.legend(
handles,
labels,
loc="upper left",
bbox_to_anchor=(0, 1),
ncol=2,
fontsize="small",
frameon=True,
fancybox=True,
shadow=True,
framealpha=0.9,
)
# Redraw the canvas # Set titles and labels
self.canvas.draw() self.ax.set_title("Medication Effects Over Time")
self.ax.set_xlabel("Date")
self.ax.set_ylabel("Rating (0-10) / Dose (mg)")
# Optimize y-axis configuration
current_ylim = self.ax.get_ylim()
self.ax.set_ylim(bottom=current_ylim[0], top=max(10, current_ylim[1]))
# Optimize date formatting
self.fig.autofmt_xdate()
def _plot_series( def _plot_series(
self, self,
@@ -252,25 +296,28 @@ class GraphManager:
marker: str, marker: str,
linestyle: str, linestyle: str,
) -> None: ) -> None:
"""Helper method to plot a data series.""" """Helper method to plot a data series with optimizations."""
# Use more efficient plotting parameters
self.ax.plot( self.ax.plot(
df.index, df.index,
df[column], df[column],
marker=marker, marker=marker,
linestyle=linestyle, linestyle=linestyle,
label=label, label=label,
markersize=4, # Smaller markers for better performance
linewidth=1.5, # Optimized line width
) )
def _calculate_daily_dose(self, dose_str: str) -> float: def _calculate_daily_dose(self, dose_str: str) -> float:
"""Calculate total daily dose from dose string format.""" """Calculate total daily dose from dose string format with optimizations."""
if not dose_str or pd.isna(dose_str) or str(dose_str).lower() == "nan": if not dose_str or pd.isna(dose_str) or str(dose_str).lower() == "nan":
return 0.0 return 0.0
total_dose = 0.0 total_dose = 0.0
# Handle different separators and clean the string # Optimize string processing
dose_str = str(dose_str).replace("", "").strip() dose_str = str(dose_str).replace("", "").strip()
# Split by | or by spaces if no | present # More efficient splitting and processing
dose_entries = dose_str.split("|") if "|" in dose_str else [dose_str] dose_entries = dose_str.split("|") if "|" in dose_str else [dose_str]
for entry in dose_entries: for entry in dose_entries:
@@ -279,15 +326,15 @@ class GraphManager:
continue continue
try: try:
# Extract dose part after the last colon (timestamp:dose format) # More efficient dose extraction
dose_part = entry.split(":")[-1] if ":" in entry else entry dose_part = entry.split(":")[-1] if ":" in entry else entry
# Extract numeric part from dose (e.g., "150mg" -> 150) # Optimized numeric extraction
dose_value = "" dose_value = ""
for char in dose_part: for char in dose_part:
if char.isdigit() or char == ".": if char.isdigit() or char == ".":
dose_value += char dose_value += char
elif dose_value: # Stop at first non-digit after finding digits elif dose_value:
break break
if dose_value: if dose_value:
@@ -298,5 +345,10 @@ class GraphManager:
return total_dose return total_dose
def close(self) -> None: def close(self) -> None:
"""Clean up resources.""" """Clean up resources with proper optimization."""
plt.close(self.fig) try:
# Clear the plot before closing
self.ax.clear()
plt.close(self.fig)
except Exception:
pass # Ignore cleanup errors
+39 -4
View File
@@ -67,6 +67,29 @@ class MedTrackerApp:
# Add menu bar # Add menu bar
self._setup_menu() self._setup_menu()
# Center the window on screen
self._center_window()
def _center_window(self) -> None:
"""Center the main window on the screen."""
# Update the window to get accurate dimensions
self.root.update_idletasks()
# Get window dimensions
window_width = self.root.winfo_reqwidth()
window_height = self.root.winfo_reqheight()
# Get screen dimensions
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
# Calculate position to center the window
x = (screen_width // 2) - (window_width // 2)
y = (screen_height // 2) - (window_height // 2)
# Set the window geometry
self.root.geometry(f"{window_width}x{window_height}+{x}+{y}")
def _setup_main_ui(self) -> None: def _setup_main_ui(self) -> None:
"""Set up the main UI components.""" """Set up the main UI components."""
import tkinter.ttk as ttk import tkinter.ttk as ttk
@@ -150,6 +173,12 @@ class MedTrackerApp:
def _refresh_ui_after_config_change(self) -> None: def _refresh_ui_after_config_change(self) -> None:
"""Refresh UI components after pathology or medicine configuration changes.""" """Refresh UI components after pathology or medicine configuration changes."""
# Clear caches in optimized data manager
if hasattr(self.data_manager, "_invalidate_cache"):
self.data_manager._invalidate_cache()
self.data_manager._headers_cache = None
self.data_manager._dtype_cache = None
# Recreate the input frame with new pathologies and medicines # Recreate the input frame with new pathologies and medicines
self.input_frame.destroy() self.input_frame.destroy()
input_ui: dict[str, Any] = self.ui_manager.create_input_frame( input_ui: dict[str, Any] = self.ui_manager.create_input_frame(
@@ -412,9 +441,10 @@ class MedTrackerApp:
"""Load data from the CSV file into the table and graph.""" """Load data from the CSV file into the table and graph."""
logger.debug("Loading data from CSV.") logger.debug("Loading data from CSV.")
# Clear existing data in the treeview # Clear existing data in the treeview efficiently
for i in self.tree.get_children(): children = self.tree.get_children()
self.tree.delete(i) if children:
self.tree.delete(*children)
# Load data from the CSV file # Load data from the CSV file
df: pd.DataFrame = self.data_manager.load_data() df: pd.DataFrame = self.data_manager.load_data()
@@ -422,7 +452,11 @@ class MedTrackerApp:
# Update the treeview with the data # Update the treeview with the data
if not df.empty: if not df.empty:
# Build display columns dynamically (exclude dose columns for table view) # Build display columns dynamically (exclude dose columns for table view)
display_columns = ["date", "depression", "anxiety", "sleep", "appetite"] display_columns = ["date"]
# Add pathology columns
for pathology_key in self.pathology_manager.get_pathology_keys():
display_columns.append(pathology_key)
# Add medicine columns (without dose columns) # Add medicine columns (without dose columns)
for medicine_key in self.medicine_manager.get_medicine_keys(): for medicine_key in self.medicine_manager.get_medicine_keys():
@@ -437,6 +471,7 @@ class MedTrackerApp:
# Fallback - just use all columns # Fallback - just use all columns
display_df = df display_df = df
# Batch insert for better performance
for _index, row in display_df.iterrows(): for _index, row in display_df.iterrows():
self.tree.insert(parent="", index="end", values=list(row)) self.tree.insert(parent="", index="end", values=list(row))
logger.debug(f"Loaded {len(display_df)} entries into treeview.") logger.debug(f"Loaded {len(display_df)} entries into treeview.")
+32 -344
View File
@@ -417,8 +417,8 @@ class UIManager:
# Extract note (should be the last value) # Extract note (should be the last value)
note = values_list[-1] if len(values_list) > 0 else "" note = values_list[-1] if len(values_list) > 0 else ""
# Create improved UI sections dynamically # Create improved UI sections
vars_dict = self._create_edit_ui_dynamic( vars_dict = self._create_edit_ui(
main_container, main_container,
date, date,
pathology_values, pathology_values,
@@ -443,7 +443,7 @@ class UIManager:
return edit_win return edit_win
def _create_edit_ui_dynamic( def _create_edit_ui(
self, self,
parent: ttk.Frame, parent: ttk.Frame,
date: str, date: str,
@@ -500,7 +500,7 @@ class UIManager:
meds_frame.grid_columnconfigure(0, weight=1) meds_frame.grid_columnconfigure(0, weight=1)
# Create medicine checkboxes dynamically # Create medicine checkboxes dynamically
med_vars = self._create_medicine_section_dynamic(meds_frame, medicine_values) med_vars = self._create_medicine_section(meds_frame, medicine_values)
vars_dict.update(med_vars) vars_dict.update(med_vars)
row += 1 row += 1
@@ -510,7 +510,7 @@ class UIManager:
dose_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15)) dose_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
dose_frame.grid_columnconfigure(0, weight=1) dose_frame.grid_columnconfigure(0, weight=1)
dose_vars = self._create_dose_tracking_dynamic(dose_frame, medicine_doses) dose_vars = self._create_dose_tracking(dose_frame, medicine_doses)
vars_dict.update(dose_vars) vars_dict.update(dose_vars)
row += 1 row += 1
@@ -532,6 +532,7 @@ class UIManager:
) )
note_text.grid(row=0, column=0, sticky="ew", padx=5, pady=5) note_text.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
note_text.insert("1.0", str(note)) note_text.insert("1.0", str(note))
vars_dict["note_text"] = note_text # Store the widget for access during save
# Bind text widget to string var for easy access # Bind text widget to string var for easy access
def update_note(*args): def update_note(*args):
@@ -542,111 +543,6 @@ class UIManager:
return vars_dict return vars_dict
def _create_edit_ui(
self,
parent: ttk.Frame,
date: str,
dep: int,
anx: int,
slp: int,
app: int,
bup: int,
hydro: int,
gaba: int,
prop: int,
quet: int,
note: str,
dose_data: dict[str, str],
) -> dict[str, Any]:
"""Create UI layout for edit window with organized sections."""
vars_dict = {}
row = 0
# Header with entry date
header_frame = ttk.Frame(parent)
header_frame.grid(row=row, column=0, sticky="ew", pady=(0, 20))
header_frame.grid_columnconfigure(1, weight=1)
ttk.Label(
header_frame, text="Editing Entry for:", font=("TkDefaultFont", 12, "bold")
).grid(row=0, column=0, sticky="w")
vars_dict["date"] = tk.StringVar(value=str(date))
date_entry = ttk.Entry(
header_frame,
textvariable=vars_dict["date"],
font=("TkDefaultFont", 12),
width=15,
)
date_entry.grid(row=0, column=1, sticky="w", padx=(10, 0))
row += 1
# Symptoms section
symptoms_frame = ttk.LabelFrame(
parent, text="Daily Symptoms (0-10 scale)", padding="15"
)
symptoms_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
symptoms_frame.grid_columnconfigure(1, weight=1)
# Create symptom scales with better layout
symptoms = [
("Depression", "depression", dep),
("Anxiety", "anxiety", anx),
("Sleep Quality", "sleep", slp),
("Appetite", "appetite", app),
]
for i, (label, key, value) in enumerate(symptoms):
self._create_symptom_scale(symptoms_frame, i, label, key, value, vars_dict)
row += 1
# Medications section
meds_frame = ttk.LabelFrame(parent, text="Medications Taken", padding="15")
meds_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
meds_frame.grid_columnconfigure(0, weight=1)
# Create medicine checkboxes with better styling
med_vars = self._create_medicine_section(
meds_frame, bup, hydro, gaba, prop, quet
)
vars_dict.update(med_vars)
row += 1
# Dose tracking section
dose_frame = ttk.LabelFrame(parent, text="Dose Tracking", padding="15")
dose_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15))
dose_frame.grid_columnconfigure(0, weight=1)
dose_vars = self._create_dose_tracking(dose_frame, dose_data)
vars_dict.update(dose_vars)
row += 1
# Notes section
notes_frame = ttk.LabelFrame(parent, text="Notes", padding="15")
notes_frame.grid(row=row, column=0, sticky="ew", pady=(0, 20))
notes_frame.grid_columnconfigure(0, weight=1)
vars_dict["note"] = tk.StringVar(value=str(note))
note_text = tk.Text(
notes_frame, height=4, wrap=tk.WORD, font=("TkDefaultFont", 10)
)
note_text.grid(row=0, column=0, sticky="ew")
note_text.insert(1.0, str(note))
vars_dict["note_text"] = note_text
# Add scrollbar for notes
note_scroll = ttk.Scrollbar(
notes_frame, orient="vertical", command=note_text.yview
)
note_scroll.grid(row=0, column=1, sticky="ns")
note_text.configure(yscrollcommand=note_scroll.set)
return vars_dict
def _create_symptom_scale( def _create_symptom_scale(
self, self,
parent: ttk.Frame, parent: ttk.Frame,
@@ -733,91 +629,6 @@ class UIManager:
scale.bind("<KeyRelease>", update_value_label) scale.bind("<KeyRelease>", update_value_label)
update_value_label() # Set initial color update_value_label() # Set initial color
def _create_enhanced_symptom_scale(
self,
parent: ttk.Frame,
row: int,
label: str,
key: str,
value: int,
vars_dict: dict[str, tk.IntVar],
) -> None:
"""Create enhanced symptom scale for new entry form (like edit window)."""
# Ensure value is properly converted
try:
value = int(float(value)) if value not in ["", None] else 0
except (ValueError, TypeError):
value = 0
# Label
label_widget = ttk.Label(
parent, text=f"{label} (0-10):", font=("TkDefaultFont", 10, "bold")
)
label_widget.grid(row=row, column=0, sticky="w", padx=5, pady=8)
# Scale container
scale_container = ttk.Frame(parent)
scale_container.grid(row=row, column=1, sticky="ew", padx=(20, 5), pady=8)
scale_container.grid_columnconfigure(0, weight=1)
# Scale with value labels
scale_frame = ttk.Frame(scale_container)
scale_frame.grid(row=0, column=0, sticky="ew")
scale_frame.grid_columnconfigure(1, weight=1)
# Current value display
value_label = ttk.Label(
scale_frame,
text=str(value),
font=("TkDefaultFont", 12, "bold"),
foreground="#2E86AB",
width=3,
)
value_label.grid(row=0, column=0, padx=(0, 10))
# Scale widget
scale = ttk.Scale(
scale_frame,
from_=0,
to=10,
variable=vars_dict[key],
orient=tk.HORIZONTAL,
length=250, # Slightly smaller than edit window to fit better
)
scale.grid(row=0, column=1, sticky="ew")
# Scale labels (0, 5, 10)
labels_frame = ttk.Frame(scale_container)
labels_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0))
ttk.Label(labels_frame, text="0", font=("TkDefaultFont", 8)).grid(
row=0, column=0, sticky="w"
)
labels_frame.grid_columnconfigure(1, weight=1)
ttk.Label(labels_frame, text="5", font=("TkDefaultFont", 8)).grid(
row=0, column=1
)
ttk.Label(labels_frame, text="10", font=("TkDefaultFont", 8)).grid(
row=0, column=2, sticky="e"
)
# Update label when scale changes
def update_value_label(event=None):
current_val = vars_dict[key].get()
value_label.configure(text=str(current_val))
# Change color based on value
if current_val <= 3:
value_label.configure(foreground="#28A745") # Green for low/good
elif current_val <= 6:
value_label.configure(foreground="#FFC107") # Yellow for medium
else:
value_label.configure(foreground="#DC3545") # Red for high/bad
scale.bind("<Motion>", update_value_label)
scale.bind("<ButtonRelease-1>", update_value_label)
scale.bind("<KeyRelease>", update_value_label)
update_value_label() # Set initial color
def _create_enhanced_pathology_scale( def _create_enhanced_pathology_scale(
self, self,
parent: ttk.Frame, parent: ttk.Frame,
@@ -927,153 +738,6 @@ class UIManager:
update_value_label_pathology() # Set initial color update_value_label_pathology() # Set initial color
def _create_medicine_section( def _create_medicine_section(
self, parent: ttk.Frame, bup: int, hydro: int, gaba: int, prop: int, quet: int
) -> dict[str, tk.IntVar]:
"""Create medicine checkboxes with organized layout."""
vars_dict = {}
# Create a grid layout for medicines
medicines = [
("bupropion", bup, "Bupropion", "150/300 mg", "#E8F4FD"),
("hydroxyzine", hydro, "Hydroxyzine", "25 mg", "#FFF2E8"),
("gabapentin", gaba, "Gabapentin", "100 mg", "#F0F8E8"),
("propranolol", prop, "Propranolol", "10 mg", "#FCE8F3"),
("quetiapine", quet, "Quetiapine", "25 mg", "#E8F0FF"),
]
# Create medicine cards in a 2-column layout
for i, (key, value, name, dose, _bg_color) in enumerate(medicines):
row = i // 2
col = i % 2
# Medicine card frame
med_card = ttk.Frame(parent, relief="solid", borderwidth=1)
med_card.grid(row=row, column=col, sticky="ew", padx=5, pady=5)
parent.grid_columnconfigure(col, weight=1)
vars_dict[key] = tk.IntVar(value=int(value))
# Checkbox with medicine name
check_frame = ttk.Frame(med_card)
check_frame.pack(fill="x", padx=10, pady=8)
checkbox = ttk.Checkbutton(
check_frame,
text=f"{name} ({dose})",
variable=vars_dict[key],
style="Medicine.TCheckbutton",
)
checkbox.pack(anchor="w")
return vars_dict
def _create_dose_tracking(
self, parent: ttk.Frame, dose_data: dict[str, str]
) -> dict[str, Any]:
"""Create dose tracking interface."""
vars_dict = {}
# Create notebook for organized dose tracking
notebook = ttk.Notebook(parent)
notebook.pack(fill="both", expand=True)
medicines = [
("bupropion", "Bupropion"),
("hydroxyzine", "Hydroxyzine"),
("gabapentin", "Gabapentin"),
("propranolol", "Propranolol"),
("quetiapine", "Quetiapine"),
]
for med_key, med_name in medicines:
# Create tab for each medicine
tab_frame = ttk.Frame(notebook)
notebook.add(tab_frame, text=med_name)
# Configure tab layout
tab_frame.grid_columnconfigure(0, weight=1)
# Quick dose entry section
entry_frame = ttk.LabelFrame(tab_frame, text="Add New Dose", padding="10")
entry_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
entry_frame.grid_columnconfigure(1, weight=1)
ttk.Label(entry_frame, text="Dose amount:").grid(
row=0, column=0, sticky="w"
)
dose_entry_var = tk.StringVar()
vars_dict[f"{med_key}_entry_var"] = dose_entry_var
dose_entry = ttk.Entry(entry_frame, textvariable=dose_entry_var, width=15)
dose_entry.grid(row=0, column=1, sticky="w", padx=(10, 10))
# Quick dose buttons
quick_frame = ttk.Frame(entry_frame)
quick_frame.grid(row=0, column=2, sticky="w")
# Common dose amounts (customize per medicine)
quick_doses = self._get_quick_doses(med_key)
for i, dose in enumerate(quick_doses):
ttk.Button(
quick_frame,
text=dose,
width=8,
command=lambda d=dose, var=dose_entry_var: var.set(d),
).grid(row=0, column=i, padx=2)
# Take dose button
def create_take_dose_command(med_name, entry_var, med_key):
def take_dose():
self._take_dose(med_name, entry_var, med_key, vars_dict)
return take_dose
take_button = ttk.Button(
entry_frame,
text=f"Take {med_name}",
style="Accent.TButton",
command=create_take_dose_command(med_name, dose_entry_var, med_key),
)
take_button.grid(row=1, column=0, columnspan=3, pady=(10, 0), sticky="ew")
# Dose history section
history_frame = ttk.LabelFrame(
tab_frame, text="Today's Doses", padding="10"
)
history_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=5)
history_frame.grid_columnconfigure(0, weight=1)
# Dose history display with fixed height to prevent excessive expansion
dose_text = tk.Text(
history_frame,
height=4, # Reduced height to fit better in scrollable window
wrap=tk.WORD,
font=("Consolas", 10),
state="normal", # Start enabled
)
dose_text.grid(row=0, column=0, sticky="ew")
# Store raw dose string in a variable
doses_str = dose_data.get(med_key, "")
dose_str_var = tk.StringVar(value=doses_str)
vars_dict[f"{med_key}_doses_str"] = dose_str_var
# Populate with existing doses
self._populate_dose_history(dose_text, dose_str_var.get())
vars_dict[f"{med_key}_doses_text"] = dose_text
# Scrollbar for dose history
dose_scroll = ttk.Scrollbar(
history_frame, orient="vertical", command=dose_text.yview
)
dose_scroll.grid(row=0, column=1, sticky="ns")
dose_text.configure(yscrollcommand=dose_scroll.set)
return vars_dict
def _create_medicine_section_dynamic(
self, parent: ttk.Frame, medicine_values: dict[str, int] self, parent: ttk.Frame, medicine_values: dict[str, int]
) -> dict[str, tk.IntVar]: ) -> dict[str, tk.IntVar]:
"""Create medicine checkboxes dynamically.""" """Create medicine checkboxes dynamically."""
@@ -1120,7 +784,7 @@ class UIManager:
return vars_dict return vars_dict
def _create_dose_tracking_dynamic( def _create_dose_tracking(
self, parent: ttk.Frame, medicine_doses: dict[str, str] self, parent: ttk.Frame, medicine_doses: dict[str, str]
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Create dose tracking interface dynamically.""" """Create dose tracking interface dynamically."""
@@ -1398,9 +1062,33 @@ class UIManager:
# Get note text from Text widget # Get note text from Text widget
note_text_widget = vars_dict.get("note_text") note_text_widget = vars_dict.get("note_text")
self.logger.debug(f"note_text_widget found: {note_text_widget is not None}")
self.logger.debug(f"vars_dict keys: {list(vars_dict.keys())}")
note_content = "" note_content = ""
if note_text_widget: if note_text_widget:
note_content = note_text_widget.get(1.0, tk.END).strip() try:
note_content = note_text_widget.get(1.0, tk.END).strip()
self.logger.debug(f"Note content from widget: '{note_content}'")
except Exception as e:
self.logger.error(f"Error getting note from text widget: {e}")
# Fallback to StringVar
note_var = vars_dict.get("note")
if note_var:
note_content = note_var.get()
self.logger.debug(
f"Note content from StringVar fallback: '{note_content}'"
)
else:
# Fallback to StringVar if note_text widget not found
note_var = vars_dict.get("note")
if note_var:
note_content = note_var.get()
self.logger.debug(f"Note content from StringVar: '{note_content}'")
else:
self.logger.error("No note widget or StringVar found!")
self.logger.debug(f"Final note_content: '{note_content}'")
# Extract dose data dynamically from all medicines # Extract dose data dynamically from all medicines
dose_data = {} dose_data = {}
Generated
+1 -1
View File
@@ -698,7 +698,7 @@ wheels = [
[[package]] [[package]]
name = "thechart" name = "thechart"
version = "1.6.1" version = "1.7.5"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "colorlog" }, { name = "colorlog" },