diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 7ee36f4..f0edeea 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -84,11 +84,11 @@ cd pathways_app && timeout 60 python -m reflex run 2>&1 | head -30 - [x] KPIs should be reactive to filter state ### 2.4 Chart Container -- [ ] Create `chart_section()` component -- [ ] Full-width card with appropriate padding -- [ ] Placeholder for Plotly chart (integrate in Phase 3) -- [ ] Loading state with skeleton/spinner -- [ ] Error state with friendly message +- [x] Create `chart_section()` component +- [x] Full-width card with appropriate padding +- [x] Placeholder for Plotly chart (integrate in Phase 4) +- [x] Loading state with skeleton/spinner +- [x] Error state with friendly message ## Phase 3: State Management diff --git a/pathways_app/app_v2.py b/pathways_app/app_v2.py index 18c8da0..e70c494 100644 --- a/pathways_app/app_v2.py +++ b/pathways_app/app_v2.py @@ -893,41 +893,306 @@ def kpi_row() -> rx.Component: ) +def chart_loading_skeleton() -> rx.Component: + """ + Loading skeleton for the chart area. + + Displays animated pulsing bars to indicate loading state. + Uses design system colors and spacing. + """ + return rx.box( + rx.vstack( + # Simulated chart bars at different heights + rx.hstack( + rx.box( + background_color=Colors.SLATE_300, + width="12%", + height="60%", + border_radius=Radii.SM, + animation="pulse 1.5s ease-in-out infinite", + ), + rx.box( + background_color=Colors.SLATE_300, + width="12%", + height="80%", + border_radius=Radii.SM, + animation="pulse 1.5s ease-in-out infinite", + animation_delay="0.1s", + ), + rx.box( + background_color=Colors.SLATE_300, + width="12%", + height="45%", + border_radius=Radii.SM, + animation="pulse 1.5s ease-in-out infinite", + animation_delay="0.2s", + ), + rx.box( + background_color=Colors.SLATE_300, + width="12%", + height="70%", + border_radius=Radii.SM, + animation="pulse 1.5s ease-in-out infinite", + animation_delay="0.3s", + ), + rx.box( + background_color=Colors.SLATE_300, + width="12%", + height="55%", + border_radius=Radii.SM, + animation="pulse 1.5s ease-in-out infinite", + animation_delay="0.4s", + ), + rx.box( + background_color=Colors.SLATE_300, + width="12%", + height="90%", + border_radius=Radii.SM, + animation="pulse 1.5s ease-in-out infinite", + animation_delay="0.5s", + ), + spacing="3", + align="end", + justify="center", + height="100%", + width="100%", + ), + # Loading text + rx.hstack( + rx.spinner(size="2"), + rx.text( + "Generating chart...", + font_size=Typography.BODY_SIZE, + font_weight=Typography.BODY_WEIGHT, + color=Colors.SLATE_500, + font_family=Typography.FONT_FAMILY, + ), + spacing="2", + align="center", + ), + spacing="4", + align="center", + justify="center", + height="100%", + width="100%", + ), + background_color=Colors.SLATE_100, + border_radius=Radii.MD, + width="100%", + height="500px", + padding=Spacing.XL, + ) + + +def chart_error_state(error_message: rx.Var[str]) -> rx.Component: + """ + Error state for the chart area. + + Displays a friendly error message with an icon and the error details. + Provides guidance on how to resolve the issue. + """ + return rx.box( + rx.center( + rx.vstack( + rx.icon( + "triangle-alert", + size=48, + color=Colors.WARNING, + ), + rx.text( + "Unable to Generate Chart", + font_size=Typography.H2_SIZE, + font_weight=Typography.H2_WEIGHT, + color=Colors.SLATE_900, + font_family=Typography.FONT_FAMILY, + ), + rx.text( + error_message, + font_size=Typography.BODY_SIZE, + font_weight=Typography.BODY_WEIGHT, + color=Colors.SLATE_700, + font_family=Typography.FONT_FAMILY, + text_align="center", + max_width="400px", + ), + rx.text( + "Try adjusting the filters or check the data source.", + font_size=Typography.CAPTION_SIZE, + font_weight=Typography.CAPTION_WEIGHT, + color=Colors.SLATE_500, + font_family=Typography.FONT_FAMILY, + ), + spacing="3", + align="center", + ), + width="100%", + height="100%", + ), + background_color=Colors.SLATE_100, + border_radius=Radii.MD, + width="100%", + height="500px", + padding=Spacing.XL, + ) + + +def chart_empty_state() -> rx.Component: + """ + Empty state for when no data matches the filters. + + Displays a friendly message encouraging filter adjustment. + """ + return rx.box( + rx.center( + rx.vstack( + rx.icon( + "search-x", + size=48, + color=Colors.SLATE_500, + ), + rx.text( + "No Data to Display", + font_size=Typography.H2_SIZE, + font_weight=Typography.H2_WEIGHT, + color=Colors.SLATE_900, + font_family=Typography.FONT_FAMILY, + ), + rx.text( + "No patient records match your current filter criteria.", + font_size=Typography.BODY_SIZE, + font_weight=Typography.BODY_WEIGHT, + color=Colors.SLATE_700, + font_family=Typography.FONT_FAMILY, + text_align="center", + ), + rx.text( + "Try widening your date range or selecting more drugs/indications.", + font_size=Typography.CAPTION_SIZE, + font_weight=Typography.CAPTION_WEIGHT, + color=Colors.SLATE_500, + font_family=Typography.FONT_FAMILY, + ), + spacing="3", + align="center", + ), + width="100%", + height="100%", + ), + background_color=Colors.SLATE_100, + border_radius=Radii.MD, + width="100%", + height="500px", + padding=Spacing.XL, + ) + + +def chart_ready_placeholder() -> rx.Component: + """ + Ready state placeholder for the chart area. + + This will be replaced with actual rx.plotly() in Phase 4. + For now, shows a placeholder indicating the chart location. + """ + return rx.box( + rx.center( + rx.vstack( + rx.icon( + "chart-bar-stacked", + size=48, + color=Colors.PRIMARY, + ), + rx.text( + "Chart Ready", + font_size=Typography.H2_SIZE, + font_weight=Typography.H2_WEIGHT, + color=Colors.SLATE_900, + font_family=Typography.FONT_FAMILY, + ), + rx.text( + "Plotly icicle chart will render here in Phase 4.", + font_size=Typography.BODY_SIZE, + font_weight=Typography.BODY_WEIGHT, + color=Colors.SLATE_500, + font_family=Typography.FONT_FAMILY, + ), + spacing="3", + align="center", + ), + width="100%", + height="100%", + ), + background_color=Colors.PALE, + border=f"2px dashed {Colors.PRIMARY}", + border_radius=Radii.MD, + width="100%", + height="500px", + padding=Spacing.XL, + ) + + def chart_section() -> rx.Component: """ Main chart section component. - Contains: Plotly icicle chart with loading and error states. + Contains: Plotly icicle chart with loading, error, empty, and ready states. - Will be fully implemented in Task 2.4 and Phase 4. + State handling: + - Loading: Shows skeleton animation when chart_loading is True + - Error: Shows error message when error_message is not empty + - Empty: Shows empty state when data_loaded but unique_patients is 0 + - Ready: Shows chart placeholder (will be replaced with actual chart in Phase 4) + + Full implementation with Plotly integration in Phase 4. """ return rx.box( rx.vstack( - rx.text( - "Patient Pathway Chart", - **text_h1(), - ), - rx.text( - "Chart will be displayed here once data is loaded.", - font_size=Typography.BODY_SIZE, - font_weight=Typography.BODY_WEIGHT, - color=Colors.SLATE_500, - font_family=Typography.FONT_FAMILY, - ), - # Placeholder for chart area - rx.box( - rx.center( - rx.text( - "Chart Placeholder", - color=Colors.SLATE_500, - font_size=Typography.BODY_SIZE, - ), - width="100%", - height="400px", + # Header row with title and chart type info + rx.hstack( + rx.text( + "Patient Pathway Visualization", + **text_h1(), ), - background_color=Colors.SLATE_100, - border_radius=Radii.MD, + rx.hstack( + rx.icon( + "info", + size=14, + color=Colors.SLATE_500, + ), + rx.text( + "Hierarchical view: Trust → Directorate → Drug → Patient Pathway", + font_size=Typography.CAPTION_SIZE, + font_weight=Typography.CAPTION_WEIGHT, + color=Colors.SLATE_500, + font_family=Typography.FONT_FAMILY, + ), + spacing="1", + align="center", + ), + justify="between", + align="center", width="100%", + flex_wrap="wrap", + ), + # Chart container with state-based rendering + rx.cond( + # Priority 1: Loading state + AppState.chart_loading, + chart_loading_skeleton(), + # Not loading - check for error + rx.cond( + # Priority 2: Error state + AppState.error_message != "", + chart_error_state(AppState.error_message), + # No error - check if data loaded + rx.cond( + # Priority 3: Data loaded but empty + AppState.data_loaded & (AppState.unique_patients == 0), + chart_empty_state(), + # Priority 4: Ready state (data loaded and has records) + # or initial state (data not loaded yet) + chart_ready_placeholder(), + ), + ), ), spacing="4", width="100%", diff --git a/progress.txt b/progress.txt index 4b09191..fa104a8 100644 --- a/progress.txt +++ b/progress.txt @@ -323,3 +323,70 @@ Use `rx.cond(condition, true_value, false_value)` not Python `if`. ### Blocked items: - Debounced filter handlers (300ms) deferred to Phase 3.3 - Visual validation still blocked until rxconfig is updated + +## Iteration 6 - 2026-02-04 +### Task: 2.4 Chart Container +### Why this task: +- Previous iteration (5) explicitly recommended continuing with Task 2.4 +- Natural completion of Phase 2 Layout Components +- Chart container is foundational for Phase 4 Plotly integration +- No dependencies blocking this task +### Status: COMPLETE +### What was done: +- Created `chart_loading_skeleton()` component: + - Animated bar chart skeleton with 6 bars at different heights + - Uses CSS pulse animation (1.5s infinite) with staggered delays + - Spinner + "Generating chart..." text below + - Design tokens used (Colors, Typography, Spacing, Radii) +- Created `chart_error_state(error_message)` component: + - Triangle alert icon (48px, warning color) + - "Unable to Generate Chart" heading + - Dynamic error message from state + - Helpful guidance text for resolution +- Created `chart_empty_state()` component: + - Search-x icon (48px, slate color) + - "No Data to Display" heading + - Message explaining no records match filters + - Guidance to widen filters +- Created `chart_ready_placeholder()` component: + - Chart-bar-stacked icon (primary blue) + - "Chart Ready" heading + - Pale blue background with dashed primary border + - Clear indication that Plotly renders here in Phase 4 +- Rewrote `chart_section()` with state-based rendering: + - Header row: title + chart hierarchy info + - Uses nested rx.cond() for 4-state logic: + 1. Loading (chart_loading=True) → skeleton + 2. Error (error_message!="") → error state + 3. Empty (data_loaded & unique_patients==0) → empty state + 4. Ready → placeholder + - Full-width card styling +- Fixed icon names: alert-triangle → triangle-alert (Lucide convention) +- Fixed Colors.SLATE_400 → SLATE_500 (SLATE_400 doesn't exist in palette) +### Validation results: +- Tier 1 (Code): + - `python -m py_compile pathways_app/app_v2.py` PASSED + - `python -c "from pathways_app.app_v2 import app"` PASSED + - All 5 chart component functions return Box type correctly +- Tier 2 (Visual): Deferred - requires modifying rxconfig to test app_v2 +- Tier 3 (Functional): State-based rendering verified structurally via rx.cond nesting +### Files changed: +- Modified: pathways_app/app_v2.py (+180 lines - 4 new components, rewritten chart_section) +- Modified: IMPLEMENTATION_PLAN.md (marked 2.4 subtasks complete) +### Committed: [pending] +### Patterns discovered: +- Lucide icon names: triangle-alert not alert-triangle, search-x works +- CSS animations in Reflex: use animation prop with standard CSS syntax +- Staggered animations: animation_delay="0.1s" works for sequential delays +- Nested rx.cond() for multi-state logic: readable priority chain +- Colors.SLATE_400 doesn't exist in design system - use 300/500 instead +### Next iteration should: +- Phase 2 is now COMPLETE. Start Phase 3: State Management +- Task 3.1: Core State Variables - many already exist from Task 2.2, needs review +- Task 3.2: Data Loading - create load_data() to read from SQLite + - Reference data_processing/loader.py for SQLite patterns + - Populate available_drugs, available_indications, available_directorates from data + - Detect latest date for "to" date defaults +- Consider testing app_v2 visually by modifying pathways_app/__init__.py to import from app_v2 +### Blocked items: +- Visual validation still blocked until rxconfig is updated to point to app_v2