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:
@@ -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
@@ -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",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user