feat: add parsing utilities and 8-tab chart infrastructure (Task 9.1)

- Create src/data_processing/parsing.py with parse_average_spacing(),
  parse_pathway_drugs(), and calculate_retention_rate()
- Add 8-tab bar to chart_card.py (Icicle, Market Share, Cost Effectiveness,
  Cost Waterfall, Sankey, Dosing, Heatmap, Duration)
- Add active-tab dcc.Store and tab switching callback in chart.py
- Remove Chart Views section from sidebar (now in tab bar)
- Lazy rendering: only active tab's chart is computed
This commit is contained in:
Andrew Charlwood
2026-02-06 19:13:19 +00:00
parent 8a45ff1ca7
commit fe2d048a21
6 changed files with 351 additions and 28 deletions
+35 -4
View File
@@ -1,19 +1,50 @@
"""Chart card component — header and dcc.Graph for icicle chart."""
"""Chart card component — tab bar, header, and dcc.Graph for charts."""
from dash import html, dcc
TAB_DEFINITIONS = [
("icicle", "Icicle"),
("market-share", "Market Share"),
("cost-effectiveness", "Cost Effectiveness"),
("cost-waterfall", "Cost Waterfall"),
("sankey", "Sankey"),
("dosing", "Dosing"),
("heatmap", "Heatmap"),
("duration", "Duration"),
]
def make_chart_card():
"""Return a chart card matching 01_nhs_classic.html structure.
"""Return a chart card with tab bar and dcc.Graph.
Contains:
- Header with title and dynamic subtitle (hierarchy label)
- Tab bar with 8 chart tabs (Icicle active by default)
- Header with title and dynamic subtitle
- dcc.Loading wrapper around dcc.Graph for loading spinner
Chart view selection (icicle/sankey/timeline) is in the sidebar.
"""
tab_buttons = []
for tab_id, label in TAB_DEFINITIONS:
is_active = tab_id == "icicle"
class_name = "chart-tab chart-tab--active" if is_active else "chart-tab"
tab_buttons.append(
html.Button(
label,
id=f"tab-{tab_id}",
className=class_name,
n_clicks=0,
)
)
return html.Section(
className="chart-card",
**{"aria-label": "Patient pathway chart"},
children=[
# Tab bar
html.Div(
className="chart-card__tabs",
role="tablist",
children=tab_buttons,
),
# Card header
html.Div(
className="chart-card__header",
+1 -16
View File
@@ -19,9 +19,6 @@ def _svg_icon(svg_body):
# SVG icon bodies (Feather-style)
_ICONS = {
"pathway": '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>',
"icicle": '<rect x="3" y="3" width="18" height="4" rx="1"/><rect x="3" y="10" width="10" height="4" rx="1"/><rect x="3" y="17" width="6" height="4" rx="1"/>',
"sankey": '<path d="M6 3v18"/><path d="M18 3v18"/><path d="M6 8c6 0 6 5 12 5"/><path d="M6 16c4 0 4-3 12-3"/>',
"timeline": '<line x1="3" y1="12" x2="21" y2="12"/><circle cx="6" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="18" cy="12" r="2"/>',
}
@@ -39,16 +36,6 @@ def make_sidebar():
_sidebar_item("Pathway Overview", "pathway", active=True),
],
),
# Chart views section
html.Div(
className="sidebar__section",
children=[
html.Div("Chart Views", className="sidebar__label"),
_sidebar_item("Icicle Chart", "icicle", active=True),
_sidebar_item("Sankey Diagram", "sankey", disabled=True),
_sidebar_item("Timeline", "timeline", disabled=True),
],
),
# Footer
html.Div(
className="sidebar__footer",
@@ -62,13 +49,11 @@ def make_sidebar():
)
def _sidebar_item(label, icon_key, active=False, disabled=False, item_id=None):
def _sidebar_item(label, icon_key, active=False, item_id=None):
"""Create a single sidebar navigation item."""
class_name = "sidebar__item"
if active:
class_name += " sidebar__item--active"
if disabled:
class_name += " sidebar__item--disabled"
props = {"className": class_name}
if item_id: