feat: add trust selection to drawer with filter wiring (Task 5.1)
This commit is contained in:
@@ -228,10 +228,10 @@ Drawer selection → update_drug_selection → app-state store → load_pathway_
|
|||||||
## Phase 5: Polish & Cleanup
|
## Phase 5: Polish & Cleanup
|
||||||
|
|
||||||
### 5.1 Trust selection
|
### 5.1 Trust selection
|
||||||
- [ ] Add trust selection either:
|
- [x] Add trust selection either:
|
||||||
- In the dmc.Drawer as a "Trusts" section (preferred — keeps all filters in one place), OR
|
- In the dmc.Drawer as a "Trusts" section (preferred — keeps all filters in one place), OR
|
||||||
- As sidebar checkboxes
|
- As sidebar checkboxes
|
||||||
- [ ] Wire trust selection to `selected_trusts` in `app-state` → pathway data reload
|
- [x] Wire trust selection to `selected_trusts` in `app-state` → pathway data reload
|
||||||
- **Checkpoint**: Selecting trusts filters the chart correctly
|
- **Checkpoint**: Selecting trusts filters the chart correctly
|
||||||
|
|
||||||
### 5.2 Loading/error/empty states + dynamic hierarchy label
|
### 5.2 Loading/error/empty states + dynamic hierarchy label
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ app.layout = dmc.MantineProvider(
|
|||||||
"date_filter_id": "all_6mo",
|
"date_filter_id": "all_6mo",
|
||||||
"selected_drugs": [],
|
"selected_drugs": [],
|
||||||
"selected_directorates": [],
|
"selected_directorates": [],
|
||||||
|
"selected_trusts": [],
|
||||||
}),
|
}),
|
||||||
dcc.Store(id="chart-data", storage_type="memory"),
|
dcc.Store(id="chart-data", storage_type="memory"),
|
||||||
dcc.Store(id="reference-data", storage_type="session"),
|
dcc.Store(id="reference-data", storage_type="session"),
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ def _generate_chart_title(app_state):
|
|||||||
else:
|
else:
|
||||||
parts.append(f"{len(selected_directorates)} directorates")
|
parts.append(f"{len(selected_directorates)} directorates")
|
||||||
|
|
||||||
|
selected_trusts = app_state.get("selected_trusts") or []
|
||||||
|
if selected_trusts:
|
||||||
|
if len(selected_trusts) <= 2:
|
||||||
|
parts.append(", ".join(selected_trusts))
|
||||||
|
else:
|
||||||
|
parts.append(f"{len(selected_trusts)} trusts")
|
||||||
|
|
||||||
return " | ".join(parts) if parts else "All Patients"
|
return " | ".join(parts) if parts else "All Patients"
|
||||||
|
|
||||||
|
|
||||||
@@ -53,12 +60,14 @@ def register_chart_callbacks(app):
|
|||||||
chart_type = app_state.get("chart_type", "directory")
|
chart_type = app_state.get("chart_type", "directory")
|
||||||
selected_drugs = app_state.get("selected_drugs") or None
|
selected_drugs = app_state.get("selected_drugs") or None
|
||||||
selected_directorates = app_state.get("selected_directorates") or None
|
selected_directorates = app_state.get("selected_directorates") or None
|
||||||
|
selected_trusts = app_state.get("selected_trusts") or None
|
||||||
|
|
||||||
return query_pathway_data(
|
return query_pathway_data(
|
||||||
filter_id=filter_id,
|
filter_id=filter_id,
|
||||||
chart_type=chart_type,
|
chart_type=chart_type,
|
||||||
selected_drugs=selected_drugs,
|
selected_drugs=selected_drugs,
|
||||||
selected_directorates=selected_directorates,
|
selected_directorates=selected_directorates,
|
||||||
|
selected_trusts=selected_trusts,
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.callback(
|
@app.callback(
|
||||||
|
|||||||
@@ -9,14 +9,16 @@ def register_drawer_callbacks(app):
|
|||||||
Output("drug-drawer", "opened"),
|
Output("drug-drawer", "opened"),
|
||||||
Input("sidebar-drug-selection", "n_clicks"),
|
Input("sidebar-drug-selection", "n_clicks"),
|
||||||
Input("sidebar-indications", "n_clicks"),
|
Input("sidebar-indications", "n_clicks"),
|
||||||
|
Input("sidebar-trust-selection", "n_clicks"),
|
||||||
prevent_initial_call=True,
|
prevent_initial_call=True,
|
||||||
)
|
)
|
||||||
def open_drawer(_drug_clicks, _indication_clicks):
|
def open_drawer(_drug_clicks, _indication_clicks, _trust_clicks):
|
||||||
"""Open the drawer when sidebar Drug Selection or Indications is clicked."""
|
"""Open the drawer when sidebar Drug Selection, Indications, or Trust Selection is clicked."""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@app.callback(
|
@app.callback(
|
||||||
Output("all-drugs-chips", "value"),
|
Output("all-drugs-chips", "value"),
|
||||||
|
Output("trust-chips", "value"),
|
||||||
Input({"type": "drug-fragment", "index": ALL}, "n_clicks"),
|
Input({"type": "drug-fragment", "index": ALL}, "n_clicks"),
|
||||||
Input("clear-drug-filters", "n_clicks"),
|
Input("clear-drug-filters", "n_clicks"),
|
||||||
State("all-drugs-chips", "value"),
|
State("all-drugs-chips", "value"),
|
||||||
@@ -27,20 +29,20 @@ def register_drawer_callbacks(app):
|
|||||||
"""Handle drug fragment badge click (substring match) or clear all filters.
|
"""Handle drug fragment badge click (substring match) or clear all filters.
|
||||||
|
|
||||||
Fragment click: find all full drug names containing the fragment substring,
|
Fragment click: find all full drug names containing the fragment substring,
|
||||||
toggle them in the chip selection.
|
toggle them in the chip selection. Trust chips unchanged.
|
||||||
Clear click: reset chip selection to empty.
|
Clear click: reset both drug and trust chip selections.
|
||||||
"""
|
"""
|
||||||
triggered = ctx.triggered_id
|
triggered = ctx.triggered_id
|
||||||
|
|
||||||
# Clear button
|
# Clear button — reset both drug and trust chips
|
||||||
if triggered == "clear-drug-filters":
|
if triggered == "clear-drug-filters":
|
||||||
return []
|
return [], []
|
||||||
|
|
||||||
# Fragment badge click — triggered_id is a dict like {"type": "drug-fragment", "index": "DIR|FRAG"}
|
# Fragment badge click — triggered_id is a dict like {"type": "drug-fragment", "index": "DIR|FRAG"}
|
||||||
if isinstance(triggered, dict) and triggered.get("type") == "drug-fragment":
|
if isinstance(triggered, dict) and triggered.get("type") == "drug-fragment":
|
||||||
# Check if any fragment was actually clicked (not just initial render)
|
# Check if any fragment was actually clicked (not just initial render)
|
||||||
if not any(n for n in (fragment_clicks or []) if n):
|
if not any(n for n in (fragment_clicks or []) if n):
|
||||||
return no_update
|
return no_update, no_update
|
||||||
|
|
||||||
fragment_key = triggered["index"] # e.g. "CARDIOLOGY|ABCIXIMAB"
|
fragment_key = triggered["index"] # e.g. "CARDIOLOGY|ABCIXIMAB"
|
||||||
fragment = fragment_key.split("|", 1)[-1] if "|" in fragment_key else fragment_key
|
fragment = fragment_key.split("|", 1)[-1] if "|" in fragment_key else fragment_key
|
||||||
@@ -55,7 +57,7 @@ def register_drawer_callbacks(app):
|
|||||||
]
|
]
|
||||||
|
|
||||||
if not matching_drugs:
|
if not matching_drugs:
|
||||||
return no_update
|
return no_update, no_update
|
||||||
|
|
||||||
# Toggle: if all matching drugs are already selected, deselect them;
|
# Toggle: if all matching drugs are already selected, deselect them;
|
||||||
# otherwise, add them to selection
|
# otherwise, add them to selection
|
||||||
@@ -67,6 +69,6 @@ def register_drawer_callbacks(app):
|
|||||||
else:
|
else:
|
||||||
updated = current | set(matching_drugs)
|
updated = current | set(matching_drugs)
|
||||||
|
|
||||||
return sorted(updated)
|
return sorted(updated), no_update
|
||||||
|
|
||||||
return no_update
|
return no_update, no_update
|
||||||
|
|||||||
@@ -32,12 +32,14 @@ def register_filter_callbacks(app):
|
|||||||
Input("filter-initiated", "value"),
|
Input("filter-initiated", "value"),
|
||||||
Input("filter-last-seen", "value"),
|
Input("filter-last-seen", "value"),
|
||||||
Input("all-drugs-chips", "value"),
|
Input("all-drugs-chips", "value"),
|
||||||
|
Input("trust-chips", "value"),
|
||||||
State("app-state", "data"),
|
State("app-state", "data"),
|
||||||
)
|
)
|
||||||
def update_app_state(
|
def update_app_state(
|
||||||
_dir_clicks, _ind_clicks, initiated, last_seen, selected_drugs, current_state
|
_dir_clicks, _ind_clicks, initiated, last_seen, selected_drugs,
|
||||||
|
selected_trusts, current_state
|
||||||
):
|
):
|
||||||
"""Update app-state when chart type toggle, date filters, or drug chips change."""
|
"""Update app-state when chart type toggle, date filters, drug chips, or trust chips change."""
|
||||||
if not current_state:
|
if not current_state:
|
||||||
current_state = {
|
current_state = {
|
||||||
"chart_type": "directory",
|
"chart_type": "directory",
|
||||||
@@ -46,6 +48,7 @@ def register_filter_callbacks(app):
|
|||||||
"date_filter_id": "all_6mo",
|
"date_filter_id": "all_6mo",
|
||||||
"selected_drugs": [],
|
"selected_drugs": [],
|
||||||
"selected_directorates": [],
|
"selected_directorates": [],
|
||||||
|
"selected_trusts": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
triggered_id = ctx.triggered_id
|
triggered_id = ctx.triggered_id
|
||||||
@@ -68,6 +71,7 @@ def register_filter_callbacks(app):
|
|||||||
"last_seen": last_seen,
|
"last_seen": last_seen,
|
||||||
"date_filter_id": date_filter_id,
|
"date_filter_id": date_filter_id,
|
||||||
"selected_drugs": selected_drugs or [],
|
"selected_drugs": selected_drugs or [],
|
||||||
|
"selected_trusts": selected_trusts or [],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Toggle pill CSS classes
|
# Toggle pill CSS classes
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
Drug browser drawer component using Dash Mantine Components.
|
Filter drawer component using Dash Mantine Components.
|
||||||
|
|
||||||
Provides a right-side drawer with:
|
Provides a right-side drawer with:
|
||||||
- "All Drugs" section: flat alphabetical list of all drugs from pathway_nodes
|
- "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,
|
- Directorate cards: grouped by PrimaryDirectorate from DimSearchTerm.csv,
|
||||||
with Accordion items per Search_Term containing drug fragment chips
|
with Accordion items per Search_Term containing drug fragment chips
|
||||||
- "Clear Filters" button at the bottom
|
- "Clear Filters" button at the bottom
|
||||||
@@ -11,7 +12,7 @@ Provides a right-side drawer with:
|
|||||||
from dash import html
|
from dash import html
|
||||||
import dash_mantine_components as dmc
|
import dash_mantine_components as dmc
|
||||||
|
|
||||||
from dash_app.data.card_browser import build_directorate_tree, get_all_drugs
|
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:
|
def _make_drug_chips(drugs: list[str]) -> dmc.ChipGroup:
|
||||||
@@ -27,6 +28,19 @@ def _make_drug_chips(drugs: list[str]) -> dmc.ChipGroup:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
def _make_directorate_card(directorate: str, indications: dict[str, list[str]]) -> dmc.AccordionItem:
|
||||||
"""
|
"""
|
||||||
Create an AccordionItem for a single directorate.
|
Create an AccordionItem for a single directorate.
|
||||||
@@ -97,6 +111,7 @@ def make_drawer():
|
|||||||
Returns a dmc.Drawer that will be opened/closed via callbacks in Phase 4.2.
|
Returns a dmc.Drawer that will be opened/closed via callbacks in Phase 4.2.
|
||||||
"""
|
"""
|
||||||
drugs = get_all_drugs()
|
drugs = get_all_drugs()
|
||||||
|
trusts = get_all_trusts()
|
||||||
directorate_tree = build_directorate_tree()
|
directorate_tree = build_directorate_tree()
|
||||||
|
|
||||||
# All Drugs section
|
# All Drugs section
|
||||||
@@ -116,6 +131,23 @@ def make_drawer():
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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 cards section
|
||||||
directorate_items = [
|
directorate_items = [
|
||||||
_make_directorate_card(directorate, indications)
|
_make_directorate_card(directorate, indications)
|
||||||
@@ -163,6 +195,8 @@ def make_drawer():
|
|||||||
children=[
|
children=[
|
||||||
all_drugs_section,
|
all_drugs_section,
|
||||||
dmc.Divider(),
|
dmc.Divider(),
|
||||||
|
trusts_section,
|
||||||
|
dmc.Divider(),
|
||||||
directorate_section,
|
directorate_section,
|
||||||
clear_button,
|
clear_button,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ def make_sidebar():
|
|||||||
_sidebar_item(
|
_sidebar_item(
|
||||||
"Drug Selection", "drug", item_id="sidebar-drug-selection"
|
"Drug Selection", "drug", item_id="sidebar-drug-selection"
|
||||||
),
|
),
|
||||||
_sidebar_item("Trust Selection", "trust"),
|
_sidebar_item(
|
||||||
|
"Trust Selection", "trust", item_id="sidebar-trust-selection"
|
||||||
|
),
|
||||||
_sidebar_item("Directory Selection", "directory"),
|
_sidebar_item("Directory Selection", "directory"),
|
||||||
_sidebar_item(
|
_sidebar_item(
|
||||||
"Indications", "indication", item_id="sidebar-indications"
|
"Indications", "indication", item_id="sidebar-indications"
|
||||||
|
|||||||
@@ -81,3 +81,15 @@ def get_all_drugs() -> list[str]:
|
|||||||
|
|
||||||
data = load_initial_data()
|
data = load_initial_data()
|
||||||
return data.get("available_drugs", [])
|
return data.get("available_drugs", [])
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_trusts() -> list[str]:
|
||||||
|
"""
|
||||||
|
Return a sorted flat list of all unique trust names from pathway_nodes level 1.
|
||||||
|
|
||||||
|
Delegates to load_initial_data() which already queries the database.
|
||||||
|
"""
|
||||||
|
from dash_app.data.queries import load_initial_data
|
||||||
|
|
||||||
|
data = load_initial_data()
|
||||||
|
return data.get("available_trusts", [])
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ def load_pathway_data(
|
|||||||
chart_type: str = "directory",
|
chart_type: str = "directory",
|
||||||
selected_drugs: Optional[list[str]] = None,
|
selected_drugs: Optional[list[str]] = None,
|
||||||
selected_directorates: Optional[list[str]] = None,
|
selected_directorates: Optional[list[str]] = None,
|
||||||
|
selected_trusts: Optional[list[str]] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Load pre-computed pathway nodes with optional filters."""
|
"""Load pre-computed pathway nodes with optional filters."""
|
||||||
return _load_pathway_nodes(
|
return _load_pathway_nodes(
|
||||||
@@ -34,4 +35,5 @@ def load_pathway_data(
|
|||||||
chart_type=chart_type,
|
chart_type=chart_type,
|
||||||
selected_drugs=selected_drugs,
|
selected_drugs=selected_drugs,
|
||||||
selected_directorates=selected_directorates,
|
selected_directorates=selected_directorates,
|
||||||
|
selected_trusts=selected_trusts,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ def load_initial_data(db_path: Path) -> dict:
|
|||||||
"available_drugs": [],
|
"available_drugs": [],
|
||||||
"available_directorates": [],
|
"available_directorates": [],
|
||||||
"available_indications": [],
|
"available_indications": [],
|
||||||
|
"available_trusts": [],
|
||||||
"total_records": 0,
|
"total_records": 0,
|
||||||
"last_updated": "",
|
"last_updated": "",
|
||||||
"error": "Database not found",
|
"error": "Database not found",
|
||||||
@@ -82,10 +83,20 @@ def load_initial_data(db_path: Path) -> dict:
|
|||||||
if not available_indications:
|
if not available_indications:
|
||||||
available_indications = ["(No indications available)"]
|
available_indications = ["(No indications available)"]
|
||||||
|
|
||||||
|
# Unique trusts from pathway_nodes level 1
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT DISTINCT trust_name
|
||||||
|
FROM pathway_nodes
|
||||||
|
WHERE level = 1 AND trust_name IS NOT NULL AND trust_name != ''
|
||||||
|
ORDER BY trust_name
|
||||||
|
""")
|
||||||
|
available_trusts = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"available_drugs": available_drugs,
|
"available_drugs": available_drugs,
|
||||||
"available_directorates": available_directorates,
|
"available_directorates": available_directorates,
|
||||||
"available_indications": available_indications,
|
"available_indications": available_indications,
|
||||||
|
"available_trusts": available_trusts,
|
||||||
"total_records": total_records,
|
"total_records": total_records,
|
||||||
"last_updated": last_updated,
|
"last_updated": last_updated,
|
||||||
}
|
}
|
||||||
@@ -94,6 +105,7 @@ def load_initial_data(db_path: Path) -> dict:
|
|||||||
"available_drugs": [],
|
"available_drugs": [],
|
||||||
"available_directorates": [],
|
"available_directorates": [],
|
||||||
"available_indications": [],
|
"available_indications": [],
|
||||||
|
"available_trusts": [],
|
||||||
"total_records": 0,
|
"total_records": 0,
|
||||||
"last_updated": "",
|
"last_updated": "",
|
||||||
"error": f"Database error: {e}",
|
"error": f"Database error: {e}",
|
||||||
@@ -108,6 +120,7 @@ def load_pathway_nodes(
|
|||||||
chart_type: str,
|
chart_type: str,
|
||||||
selected_drugs: Optional[list[str]] = None,
|
selected_drugs: Optional[list[str]] = None,
|
||||||
selected_directorates: Optional[list[str]] = None,
|
selected_directorates: Optional[list[str]] = None,
|
||||||
|
selected_trusts: Optional[list[str]] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Load pre-computed pathway nodes from SQLite.
|
Load pre-computed pathway nodes from SQLite.
|
||||||
@@ -120,6 +133,7 @@ def load_pathway_nodes(
|
|||||||
chart_type: "directory" or "indication"
|
chart_type: "directory" or "indication"
|
||||||
selected_drugs: optional list of drug names to filter by
|
selected_drugs: optional list of drug names to filter by
|
||||||
selected_directorates: optional list of directorate names to filter by
|
selected_directorates: optional list of directorate names to filter by
|
||||||
|
selected_trusts: optional list of trust names to filter by
|
||||||
|
|
||||||
Returns dict with keys:
|
Returns dict with keys:
|
||||||
nodes: list of dicts (JSON-serializable) with chart node data
|
nodes: list of dicts (JSON-serializable) with chart node data
|
||||||
@@ -157,6 +171,13 @@ def load_pathway_nodes(
|
|||||||
f"({' OR '.join(drug_conditions)} OR drug_sequence IS NULL)"
|
f"({' OR '.join(drug_conditions)} OR drug_sequence IS NULL)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if selected_trusts:
|
||||||
|
placeholders = ",".join("?" * len(selected_trusts))
|
||||||
|
where_clauses.append(
|
||||||
|
f"(trust_name IN ({placeholders}) OR trust_name IS NULL OR trust_name = '')"
|
||||||
|
)
|
||||||
|
params.extend(selected_trusts)
|
||||||
|
|
||||||
where_clause = " AND ".join(where_clauses)
|
where_clause = " AND ".join(where_clauses)
|
||||||
|
|
||||||
query = f"""
|
query = f"""
|
||||||
|
|||||||
Reference in New Issue
Block a user