From 5dc552f8c5c7c6335fde89a4bb688d7b1a9ce94d Mon Sep 17 00:00:00 2001 From: Andrew Charlwood Date: Fri, 6 Feb 2026 13:51:24 +0000 Subject: [PATCH] feat: add dmc.Drawer drug browser with directorate cards and drug chips (Task 4.1) --- IMPLEMENTATION_PLAN.md | 2 +- dash_app/app.py | 2 + dash_app/assets/nhs.css | 27 ++++++ dash_app/components/drawer.py | 172 ++++++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 dash_app/components/drawer.py diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 6375db4..50b96f6 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -202,7 +202,7 @@ Drawer selection → update_drug_selection → app-state store → load_pathway_ ## Phase 4: Directorate Card Browser ### 4.1 dmc.Drawer layout -- [ ] Create `dash_app/components/drawer.py` — `make_drawer()` function: +- [x] Create `dash_app/components/drawer.py` — `make_drawer()` function: - `dmc.Drawer(id="drug-drawer", position="right", size="480px")` - **Top section**: "All Drugs" card — flat alphabetical list of all drug names from pathway_nodes level 3 - Each drug as a `dmc.Chip` or clickable badge, ID pattern: `{"type": "drug-chip", "index": drug_name}` diff --git a/dash_app/app.py b/dash_app/app.py index 9f32a3a..cb35ab6 100644 --- a/dash_app/app.py +++ b/dash_app/app.py @@ -8,6 +8,7 @@ from dash_app.components.kpi_row import make_kpi_row from dash_app.components.filter_bar import make_filter_bar from dash_app.components.chart_card import make_chart_card from dash_app.components.footer import make_footer +from dash_app.components.drawer import make_drawer app = Dash( __name__, @@ -32,6 +33,7 @@ app.layout = dmc.MantineProvider( # Page structure make_header(), make_sidebar(), + make_drawer(), html.Main( className="main", children=[ diff --git a/dash_app/assets/nhs.css b/dash_app/assets/nhs.css index d3b6872..050c50b 100644 --- a/dash_app/assets/nhs.css +++ b/dash_app/assets/nhs.css @@ -254,6 +254,33 @@ body { .chart-tab:hover:not(.chart-tab--active) { color: var(--nhs-dark-grey); } .chart-tab:focus-visible { box-shadow: inset 0 0 0 3px var(--nhs-yellow); } +/* ── Drug Browser Drawer ── */ +.drawer-section { + padding: 4px 0; +} +.drawer-section-title { + margin-bottom: 4px; +} +.drawer-chips-wrap { + margin-top: 8px; +} +.drawer-chips-wrap .mantine-Chip-label { + font-family: 'Source Sans 3', Arial, sans-serif; +} +.drawer-drug-badge { + cursor: pointer; + transition: background 0.15s; +} +.drawer-drug-badge:hover { + filter: brightness(0.9); +} +.drawer-directorate-accordion { + margin-top: 8px; +} +.drawer-clear-btn { + margin-top: 8px; +} + /* ── Footer ── */ .page-footer { background: var(--nhs-pale-grey); diff --git a/dash_app/components/drawer.py b/dash_app/components/drawer.py new file mode 100644 index 0000000..8d9dffc --- /dev/null +++ b/dash_app/components/drawer.py @@ -0,0 +1,172 @@ +""" +Drug browser drawer component using Dash Mantine Components. + +Provides a right-side drawer with: +- "All Drugs" section: flat alphabetical list of all drugs from pathway_nodes +- Directorate cards: grouped by PrimaryDirectorate from DimSearchTerm.csv, + with Accordion items per Search_Term containing drug fragment chips +- "Clear Filters" button at the bottom +""" + +from dash import html +import dash_mantine_components as dmc + +from dash_app.data.card_browser import build_directorate_tree, get_all_drugs + + +def _make_drug_chips(drugs: list[str]) -> dmc.ChipGroup: + """Create a ChipGroup with multiple selection for the 'All Drugs' section.""" + return dmc.ChipGroup( + id="all-drugs-chips", + multiple=True, + value=[], + children=[ + dmc.Chip(drug, value=drug, size="xs") + for drug in drugs + ], + ) + + +def _make_directorate_card(directorate: str, indications: dict[str, list[str]]) -> dmc.AccordionItem: + """ + Create an AccordionItem for a single directorate. + + Each indication becomes a panel with drug fragment badges inside. + """ + panels = [] + for search_term, fragments in indications.items(): + panels.append( + dmc.AccordionItem( + value=f"{directorate}|{search_term}", + children=[ + dmc.AccordionControl( + search_term.title(), + className="drawer-indication", + ), + dmc.AccordionPanel( + dmc.Group( + gap="xs", + children=[ + dmc.Badge( + frag, + id={"type": "drug-fragment", "index": f"{directorate}|{frag}"}, + variant="light", + size="sm", + className="drawer-drug-badge", + style={"cursor": "pointer"}, + ) + for frag in fragments + ], + ), + ), + ], + ) + ) + + return dmc.AccordionItem( + value=directorate, + children=[ + dmc.AccordionControl( + dmc.Group( + gap="xs", + children=[ + dmc.Text(directorate.title(), fw=600, size="sm"), + dmc.Badge( + str(len(indications)), + size="xs", + variant="light", + color="gray", + ), + ], + ), + ), + dmc.AccordionPanel( + dmc.Accordion( + variant="separated", + children=panels, + ), + ), + ], + ) + + +def make_drawer(): + """ + Build the drug browser drawer component. + + Returns a dmc.Drawer that will be opened/closed via callbacks in Phase 4.2. + """ + drugs = get_all_drugs() + directorate_tree = build_directorate_tree() + + # All Drugs section + all_drugs_section = html.Div( + className="drawer-section", + children=[ + dmc.Text("All Drugs", fw=700, size="sm", className="drawer-section-title"), + dmc.Text( + f"{len(drugs)} drugs from pathway data", + size="xs", + c="dimmed", + ), + html.Div( + className="drawer-chips-wrap", + children=_make_drug_chips(drugs), + ), + ], + ) + + # Directorate cards section + directorate_items = [ + _make_directorate_card(directorate, indications) + for directorate, indications in directorate_tree.items() + ] + + directorate_section = html.Div( + className="drawer-section", + children=[ + dmc.Text("By Directorate", fw=700, size="sm", className="drawer-section-title"), + dmc.Text( + f"{len(directorate_tree)} directorates \u00b7 {sum(len(v) for v in directorate_tree.values())} indications", + size="xs", + c="dimmed", + ), + dmc.Accordion( + variant="separated", + children=directorate_items, + className="drawer-directorate-accordion", + ), + ], + ) + + # Clear filters button + clear_button = dmc.Button( + "Clear All Filters", + id="clear-drug-filters", + variant="outline", + color="red", + fullWidth=True, + className="drawer-clear-btn", + ) + + return dmc.Drawer( + id="drug-drawer", + opened=False, + position="right", + size="480px", + title=dmc.Text("Drug & Indication Browser", fw=700, size="lg"), + children=[ + dmc.ScrollArea( + h="calc(100vh - 140px)", + children=dmc.Stack( + gap="md", + children=[ + all_drugs_section, + dmc.Divider(), + directorate_section, + clear_button, + ], + ), + ), + ], + )