feat: implement chart container with state-based rendering (Task 2.4)

- Add chart_loading_skeleton() with animated bar chart and spinner
- Add chart_error_state() for displaying errors with guidance
- Add chart_empty_state() for when filters yield no results
- Add chart_ready_placeholder() for Phase 4 Plotly integration
- Rewrite chart_section() with 4-state rx.cond() logic
- Fix icon names (triangle-alert) and color references (SLATE_500)

This completes Phase 2 Layout Components.
This commit is contained in:
Andrew Charlwood
2026-02-04 13:59:01 +00:00
parent 984374a3a8
commit 17478c96ae
3 changed files with 362 additions and 30 deletions
+5 -5
View File
@@ -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
+282 -17
View File
@@ -893,41 +893,306 @@ def kpi_row() -> rx.Component:
)
def chart_section() -> rx.Component:
def chart_loading_skeleton() -> rx.Component:
"""
Main chart section component.
Loading skeleton for the chart area.
Contains: Plotly icicle chart with loading and error states.
Will be fully implemented in Task 2.4 and Phase 4.
Displays animated pulsing bars to indicate loading state.
Uses design system colors and spacing.
"""
return rx.box(
rx.vstack(
rx.text(
"Patient Pathway Chart",
**text_h1(),
# 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(
"Chart will be displayed here once data is loaded.",
"Generating chart...",
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,
spacing="2",
align="center",
),
spacing="4",
align="center",
justify="center",
height="100%",
width="100%",
height="400px",
),
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, error, empty, and ready states.
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(
# Header row with title and chart type info
rx.hstack(
rx.text(
"Patient Pathway Visualization",
**text_h1(),
),
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%",
+67
View File
@@ -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