feat: implement KPI row with reactive metrics (Task 2.3)

- Create kpi_card() component with icon, value, label, and highlight option
- Create kpi_row() with 4 KPI cards: Unique Patients, Drug Types, Total Cost, Indication Match
- Add computed vars for formatted KPI display values
- Add placeholder KPI state variables (unique_patients, total_drugs, total_cost, indication_match_rate)
- Use design system tokens for styling with hover effects
- Responsive flex-wrap layout for smaller screens
This commit is contained in:
Andrew Charlwood
2026-02-04 13:52:57 +00:00
parent 23335387b8
commit 2df3a0976b
2 changed files with 159 additions and 30 deletions
+5 -5
View File
@@ -74,14 +74,14 @@ cd pathways_app && timeout 60 python -m reflex run 2>&1 | head -30
- [x] Style according to design system - [x] Style according to design system
### 2.3 KPI Row ### 2.3 KPI Row
- [ ] Create `kpi_card()` component: - [x] Create `kpi_card()` component:
- Large mono number (32-48px) - Large mono number (32-48px)
- Label below (caption style) - Label below (caption style)
- Subtle background tint - Subtle background tint
- [ ] Create `kpi_row()` component with responsive grid - [x] Create `kpi_row()` component with responsive grid
- [ ] Initially show: Unique Patients count - [x] Initially show: Unique Patients count
- [ ] Leave space for future metrics (Drugs count, Total cost, Match rate) - [x] Leave space for future metrics (Drugs count, Total cost, Match rate)
- [ ] 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 - [ ] Create `chart_section()` component
+147 -18
View File
@@ -23,6 +23,9 @@ from pathways_app.styles import (
text_h3, text_h3,
text_caption, text_caption,
button_ghost_style, button_ghost_style,
kpi_card_style,
kpi_value_style,
kpi_label_style,
) )
@@ -244,6 +247,51 @@ class AppState(rx.State):
return f"All {total} directorates" return f"All {total} directorates"
return f"{count} of {total} selected" return f"{count} of {total} selected"
# =========================================================================
# KPI State Variables
# =========================================================================
# Placeholder KPI values (will be computed from filtered data in Phase 3)
# For now, these are static placeholders that demonstrate reactivity
unique_patients: int = 0
total_drugs: int = 0
total_cost: float = 0.0
indication_match_rate: float = 0.0
# Computed KPI display values
@rx.var
def unique_patients_display(self) -> str:
"""Format unique patients count for display."""
if self.unique_patients == 0:
return ""
return f"{self.unique_patients:,}"
@rx.var
def total_drugs_display(self) -> str:
"""Format total drugs count for display."""
if self.total_drugs == 0:
return ""
return f"{self.total_drugs:,}"
@rx.var
def total_cost_display(self) -> str:
"""Format total cost for display."""
if self.total_cost == 0.0:
return ""
# Format as £X.XM or £X.XK depending on magnitude
if self.total_cost >= 1_000_000:
return f"£{self.total_cost / 1_000_000:.1f}M"
if self.total_cost >= 1_000:
return f"£{self.total_cost / 1_000:.1f}K"
return f"£{self.total_cost:,.0f}"
@rx.var
def match_rate_display(self) -> str:
"""Format indication match rate for display."""
if self.indication_match_rate == 0.0:
return ""
return f"{self.indication_match_rate:.0f}%"
# ============================================================================= # =============================================================================
# Layout Components # Layout Components
@@ -727,40 +775,121 @@ def filter_section() -> rx.Component:
) )
def kpi_row() -> rx.Component: def kpi_card(
value: rx.Var[str],
label: str,
icon_name: str = None,
highlight: bool = False,
) -> rx.Component:
""" """
KPI metrics row component. KPI card component displaying a metric value with label.
Contains: Unique patients count, and space for additional metrics. Args:
value: The display value (should be a formatted string from computed var)
label: Label describing the metric
icon_name: Optional Lucide icon name to display
highlight: If True, uses Pale Blue background tint
Will be fully implemented in Task 2.3. Design specs from DESIGN_SYSTEM.md:
- Large mono number: 32-48px, Slate 900
- Label: Caption size, Slate 500
- Background: White or Pale Blue tint
""" """
return rx.hstack( # Build content - icon only if provided
rx.box( content_items = []
if icon_name:
content_items.append(
rx.icon(
icon_name,
size=20,
color=Colors.PRIMARY,
)
)
return rx.box(
rx.vstack( rx.vstack(
# Optional icon
rx.icon(
icon_name if icon_name else "activity",
size=20,
color=Colors.PRIMARY,
) if icon_name else rx.fragment(),
# Value
rx.text( rx.text(
"", value,
font_family=Typography.FONT_MONO, **kpi_value_style(),
font_size="32px",
font_weight="600",
color=Colors.SLATE_900,
), ),
# Label
rx.text( rx.text(
"Unique Patients", label,
font_size=Typography.CAPTION_SIZE, **kpi_label_style(),
font_weight=Typography.CAPTION_WEIGHT,
color=Colors.SLATE_500,
), ),
spacing="1", spacing="1",
align="center", align="center",
), ),
**card_style(), # Apply card styling manually to allow background_color override
min_width="200px", background_color=Colors.PALE if highlight else Colors.WHITE,
border=f"1px solid {Colors.SLATE_300}",
border_radius=Radii.LG,
padding=Spacing.XL,
box_shadow=Shadows.SM,
text_align="center", text_align="center",
min_width="180px",
flex="1",
transition=f"box-shadow {Transitions.SHADOW}, transform {Transitions.TRANSFORM}",
_hover={
"box_shadow": Shadows.MD,
"transform": "translateY(-2px)",
},
)
def kpi_row() -> rx.Component:
"""
KPI metrics row component with responsive grid layout.
Contains:
- Unique Patients: COUNT(DISTINCT patient_id)
- Total Drugs: Count of selected/filtered drugs
- Total Cost: Sum of costs in filtered data
- Match Rate: Indication match percentage
Layout: Responsive flex row that wraps on smaller screens.
KPIs update reactively when filters change (Phase 3).
"""
return rx.hstack(
# Unique Patients KPI - highlighted as primary metric
kpi_card(
value=AppState.unique_patients_display,
label="Unique Patients",
icon_name="users",
highlight=True,
),
# Total Drugs KPI
kpi_card(
value=AppState.total_drugs_display,
label="Drug Types",
icon_name="pill",
highlight=False,
),
# Total Cost KPI
kpi_card(
value=AppState.total_cost_display,
label="Total Cost",
icon_name="pound-sterling",
highlight=False,
),
# Indication Match Rate KPI
kpi_card(
value=AppState.match_rate_display,
label="Indication Match",
icon_name="circle-check",
highlight=False,
), ),
# Space for additional KPI cards
spacing="4", spacing="4",
width="100%", width="100%",
flex_wrap="wrap",
align="stretch",
) )