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:
@@ -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
|
- [x] KPIs should be reactive to filter state
|
||||||
|
|
||||||
### 2.4 Chart Container
|
### 2.4 Chart Container
|
||||||
- [ ] Create `chart_section()` component
|
- [x] Create `chart_section()` component
|
||||||
- [ ] Full-width card with appropriate padding
|
- [x] Full-width card with appropriate padding
|
||||||
- [ ] Placeholder for Plotly chart (integrate in Phase 3)
|
- [x] Placeholder for Plotly chart (integrate in Phase 4)
|
||||||
- [ ] Loading state with skeleton/spinner
|
- [x] Loading state with skeleton/spinner
|
||||||
- [ ] Error state with friendly message
|
- [x] Error state with friendly message
|
||||||
|
|
||||||
## Phase 3: State Management
|
## Phase 3: State Management
|
||||||
|
|
||||||
|
|||||||
+282
-17
@@ -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.
|
Displays animated pulsing bars to indicate loading state.
|
||||||
|
Uses design system colors and spacing.
|
||||||
Will be fully implemented in Task 2.4 and Phase 4.
|
|
||||||
"""
|
"""
|
||||||
return rx.box(
|
return rx.box(
|
||||||
rx.vstack(
|
rx.vstack(
|
||||||
rx.text(
|
# Simulated chart bars at different heights
|
||||||
"Patient Pathway Chart",
|
rx.hstack(
|
||||||
**text_h1(),
|
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(
|
rx.text(
|
||||||
"Chart will be displayed here once data is loaded.",
|
"Generating chart...",
|
||||||
font_size=Typography.BODY_SIZE,
|
font_size=Typography.BODY_SIZE,
|
||||||
font_weight=Typography.BODY_WEIGHT,
|
font_weight=Typography.BODY_WEIGHT,
|
||||||
color=Colors.SLATE_500,
|
color=Colors.SLATE_500,
|
||||||
font_family=Typography.FONT_FAMILY,
|
font_family=Typography.FONT_FAMILY,
|
||||||
),
|
),
|
||||||
# Placeholder for chart area
|
spacing="2",
|
||||||
rx.box(
|
align="center",
|
||||||
rx.center(
|
|
||||||
rx.text(
|
|
||||||
"Chart Placeholder",
|
|
||||||
color=Colors.SLATE_500,
|
|
||||||
font_size=Typography.BODY_SIZE,
|
|
||||||
),
|
),
|
||||||
|
spacing="4",
|
||||||
|
align="center",
|
||||||
|
justify="center",
|
||||||
|
height="100%",
|
||||||
width="100%",
|
width="100%",
|
||||||
height="400px",
|
|
||||||
),
|
),
|
||||||
background_color=Colors.SLATE_100,
|
background_color=Colors.SLATE_100,
|
||||||
border_radius=Radii.MD,
|
border_radius=Radii.MD,
|
||||||
width="100%",
|
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",
|
spacing="4",
|
||||||
width="100%",
|
width="100%",
|
||||||
|
|||||||
@@ -323,3 +323,70 @@ Use `rx.cond(condition, true_value, false_value)` not Python `if`.
|
|||||||
### Blocked items:
|
### Blocked items:
|
||||||
- Debounced filter handlers (300ms) deferred to Phase 3.3
|
- Debounced filter handlers (300ms) deferred to Phase 3.3
|
||||||
- Visual validation still blocked until rxconfig is updated
|
- 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
|
||||||
|
|||||||
Reference in New Issue
Block a user