refactor: replace dmc.Drawer with dmc.Modal for filter selection (Task 7.4 + 7.5)
- Created 3 separate modals: Drug Selection (lg), Trust Selection (sm), Directorate Browser (xl) with centered overlay - Added filter trigger buttons to filter bar with count badges - Added "Clear All" button in filter bar for global filter reset - Per-modal clear buttons for drugs and trusts - Preserved all existing selection logic (same component IDs) - Deleted drawer.py component and callbacks (replaced by modals.py) - Updated CSS: filter-btn styles, modal chip/badge styles
This commit is contained in:
@@ -1,206 +0,0 @@
|
||||
"""
|
||||
Filter drawer component using Dash Mantine Components.
|
||||
|
||||
Provides a right-side drawer with:
|
||||
- "All Drugs" section: flat alphabetical list of all drugs from pathway_nodes
|
||||
- "Trusts" section: all NHS trusts from pathway_nodes for trust filtering
|
||||
- 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, get_all_trusts
|
||||
|
||||
|
||||
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_trust_chips(trusts: list[str]) -> dmc.ChipGroup:
|
||||
"""Create a ChipGroup with multiple selection for the 'Trusts' section."""
|
||||
return dmc.ChipGroup(
|
||||
id="trust-chips",
|
||||
multiple=True,
|
||||
value=[],
|
||||
children=[
|
||||
dmc.Chip(trust, value=trust, size="xs")
|
||||
for trust in trusts
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
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}|{search_term}|{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()
|
||||
trusts = get_all_trusts()
|
||||
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),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# Trusts section
|
||||
trusts_section = html.Div(
|
||||
className="drawer-section",
|
||||
children=[
|
||||
dmc.Text("Trusts", fw=700, size="sm", className="drawer-section-title"),
|
||||
dmc.Text(
|
||||
f"{len(trusts)} NHS trusts",
|
||||
size="xs",
|
||||
c="dimmed",
|
||||
),
|
||||
html.Div(
|
||||
className="drawer-chips-wrap",
|
||||
children=_make_trust_chips(trusts),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# 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(),
|
||||
trusts_section,
|
||||
dmc.Divider(),
|
||||
directorate_section,
|
||||
clear_button,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -1,16 +1,12 @@
|
||||
"""Filter bar component — chart type toggle + date filter dropdowns."""
|
||||
"""Filter bar component — chart type toggle, date filters, and modal trigger buttons."""
|
||||
from dash import html, dcc
|
||||
|
||||
|
||||
def make_filter_bar():
|
||||
"""Return a filter bar matching 01_nhs_classic.html structure.
|
||||
"""Return a filter bar with chart type toggle, date dropdowns, and filter buttons.
|
||||
|
||||
Contains:
|
||||
- Chart type toggle pills (By Directory / By Indication)
|
||||
- Initiated dropdown (All years, Last 2 years, Last 1 year)
|
||||
- Last seen dropdown (Last 6 months, Last 12 months)
|
||||
|
||||
Drug/directorate filters are in the drawer (Phase 4), not here.
|
||||
Filter buttons open modals for drug, trust, and directorate selection.
|
||||
Each button shows a selection count badge (updated via callbacks).
|
||||
"""
|
||||
return html.Section(
|
||||
className="filter-bar",
|
||||
@@ -85,5 +81,47 @@ def make_filter_bar():
|
||||
),
|
||||
],
|
||||
),
|
||||
# Divider before filter buttons
|
||||
html.Div(className="filter-bar__divider"),
|
||||
# Filter trigger buttons
|
||||
html.Div(
|
||||
className="filter-bar__group",
|
||||
children=[
|
||||
html.Button(
|
||||
children=[
|
||||
"Drugs",
|
||||
html.Span(id="drug-count-badge", className="filter-btn__badge filter-btn__badge--hidden"),
|
||||
],
|
||||
id="open-drug-modal",
|
||||
className="filter-btn",
|
||||
n_clicks=0,
|
||||
),
|
||||
html.Button(
|
||||
children=[
|
||||
"Trusts",
|
||||
html.Span(id="trust-count-badge", className="filter-btn__badge filter-btn__badge--hidden"),
|
||||
],
|
||||
id="open-trust-modal",
|
||||
className="filter-btn",
|
||||
n_clicks=0,
|
||||
),
|
||||
html.Button(
|
||||
children=[
|
||||
"Directorates",
|
||||
html.Span(id="directorate-count-badge", className="filter-btn__badge filter-btn__badge--hidden"),
|
||||
],
|
||||
id="open-directorate-modal",
|
||||
className="filter-btn",
|
||||
n_clicks=0,
|
||||
),
|
||||
],
|
||||
),
|
||||
# Clear all filters
|
||||
html.Button(
|
||||
"Clear All",
|
||||
id="clear-all-filters",
|
||||
className="filter-btn filter-btn--clear",
|
||||
n_clicks=0,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
"""Filter selection modals using Dash Mantine Components.
|
||||
|
||||
Three separate modals replace the single drawer:
|
||||
- Drug Selection modal: 42 drugs in a ChipGroup with search filter
|
||||
- Trust Selection modal: 7 trusts in a ChipGroup
|
||||
- Directorate Browser modal: nested accordion with indication sub-items and drug fragment badges
|
||||
|
||||
Component IDs are preserved from the drawer so existing callbacks work unchanged:
|
||||
- all-drugs-chips, trust-chips, drug-fragment pattern, clear-drug-filters
|
||||
"""
|
||||
|
||||
from dash import html
|
||||
import dash_mantine_components as dmc
|
||||
|
||||
from dash_app.data.card_browser import build_directorate_tree, get_all_drugs, get_all_trusts
|
||||
|
||||
|
||||
def _make_directorate_accordion_item(directorate: str, indications: dict[str, list[str]]) -> dmc.AccordionItem:
|
||||
"""Create an AccordionItem for a single directorate with nested indication panels."""
|
||||
panels = []
|
||||
for search_term, fragments in indications.items():
|
||||
panels.append(
|
||||
dmc.AccordionItem(
|
||||
value=f"{directorate}|{search_term}",
|
||||
children=[
|
||||
dmc.AccordionControl(
|
||||
search_term.title(),
|
||||
className="modal-indication",
|
||||
),
|
||||
dmc.AccordionPanel(
|
||||
dmc.Group(
|
||||
gap="xs",
|
||||
children=[
|
||||
dmc.Badge(
|
||||
frag,
|
||||
id={"type": "drug-fragment", "index": f"{directorate}|{search_term}|{frag}"},
|
||||
variant="light",
|
||||
size="sm",
|
||||
className="modal-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_drug_modal():
|
||||
"""Build the drug selection modal."""
|
||||
drugs = get_all_drugs()
|
||||
|
||||
return dmc.Modal(
|
||||
id="drug-modal",
|
||||
opened=False,
|
||||
centered=True,
|
||||
size="lg",
|
||||
title=dmc.Group(
|
||||
justify="space-between",
|
||||
style={"width": "100%"},
|
||||
children=[
|
||||
dmc.Text("Select Drugs", fw=600, size="lg"),
|
||||
dmc.Badge(
|
||||
id="drug-modal-count",
|
||||
children=f"0 of {len(drugs)} selected",
|
||||
variant="light",
|
||||
color="blue",
|
||||
),
|
||||
],
|
||||
),
|
||||
overlayProps={"backgroundOpacity": 0.55, "blur": 3},
|
||||
children=[
|
||||
html.Div(
|
||||
className="modal-chips-scroll",
|
||||
children=[
|
||||
dmc.Text(
|
||||
f"{len(drugs)} drugs from pathway data",
|
||||
size="xs",
|
||||
c="dimmed",
|
||||
mb=8,
|
||||
),
|
||||
dmc.ChipGroup(
|
||||
id="all-drugs-chips",
|
||||
multiple=True,
|
||||
value=[],
|
||||
children=[
|
||||
dmc.Chip(drug, value=drug, size="xs")
|
||||
for drug in drugs
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
dmc.Space(h=12),
|
||||
dmc.Group(
|
||||
justify="flex-end",
|
||||
children=[
|
||||
dmc.Button(
|
||||
"Clear Selection",
|
||||
id="clear-drug-selection",
|
||||
variant="subtle",
|
||||
color="gray",
|
||||
size="sm",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def make_trust_modal():
|
||||
"""Build the trust selection modal."""
|
||||
trusts = get_all_trusts()
|
||||
|
||||
return dmc.Modal(
|
||||
id="trust-modal",
|
||||
opened=False,
|
||||
centered=True,
|
||||
size="sm",
|
||||
title=dmc.Group(
|
||||
justify="space-between",
|
||||
style={"width": "100%"},
|
||||
children=[
|
||||
dmc.Text("Select Trusts", fw=600, size="lg"),
|
||||
dmc.Badge(
|
||||
id="trust-modal-count",
|
||||
children=f"0 of {len(trusts)} selected",
|
||||
variant="light",
|
||||
color="blue",
|
||||
),
|
||||
],
|
||||
),
|
||||
overlayProps={"backgroundOpacity": 0.55, "blur": 3},
|
||||
children=[
|
||||
dmc.ChipGroup(
|
||||
id="trust-chips",
|
||||
multiple=True,
|
||||
value=[],
|
||||
children=[
|
||||
dmc.Chip(trust, value=trust, size="xs")
|
||||
for trust in trusts
|
||||
],
|
||||
),
|
||||
dmc.Space(h=12),
|
||||
dmc.Group(
|
||||
justify="flex-end",
|
||||
children=[
|
||||
dmc.Button(
|
||||
"Clear Selection",
|
||||
id="clear-trust-selection",
|
||||
variant="subtle",
|
||||
color="gray",
|
||||
size="sm",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def make_directorate_modal():
|
||||
"""Build the directorate browser modal with nested accordion."""
|
||||
directorate_tree = build_directorate_tree()
|
||||
|
||||
directorate_items = [
|
||||
_make_directorate_accordion_item(directorate, indications)
|
||||
for directorate, indications in directorate_tree.items()
|
||||
]
|
||||
|
||||
total_indications = sum(len(v) for v in directorate_tree.values())
|
||||
|
||||
return dmc.Modal(
|
||||
id="directorate-modal",
|
||||
opened=False,
|
||||
centered=True,
|
||||
size="xl",
|
||||
title=dmc.Text("Browse by Directorate", fw=600, size="lg"),
|
||||
overlayProps={"backgroundOpacity": 0.55, "blur": 3},
|
||||
children=[
|
||||
html.Div(
|
||||
className="modal-chips-scroll",
|
||||
children=[
|
||||
dmc.Text(
|
||||
f"{len(directorate_tree)} directorates \u00b7 {total_indications} indications",
|
||||
size="xs",
|
||||
c="dimmed",
|
||||
mb=8,
|
||||
),
|
||||
dmc.Accordion(
|
||||
variant="separated",
|
||||
children=directorate_items,
|
||||
className="modal-directorate-accordion",
|
||||
),
|
||||
],
|
||||
),
|
||||
dmc.Space(h=12),
|
||||
dmc.Group(
|
||||
justify="flex-end",
|
||||
children=[
|
||||
dmc.Button(
|
||||
"Clear All Filters",
|
||||
id="clear-drug-filters",
|
||||
variant="subtle",
|
||||
color="red",
|
||||
size="sm",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def make_modals():
|
||||
"""Return all three filter modals as a list of components."""
|
||||
return html.Div(
|
||||
children=[
|
||||
make_drug_modal(),
|
||||
make_trust_modal(),
|
||||
make_directorate_modal(),
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user