feat: add drawer callbacks for drug selection, fragment matching, and clear (Task 4.2)
This commit is contained in:
@@ -214,13 +214,13 @@ Drawer selection → update_drug_selection → app-state store → load_pathway_
|
|||||||
- **Checkpoint**: Drawer opens with correct layout, all directorates and drugs visible
|
- **Checkpoint**: Drawer opens with correct layout, all directorates and drugs visible
|
||||||
|
|
||||||
### 4.2 Drawer callbacks
|
### 4.2 Drawer callbacks
|
||||||
- [ ] Create `dash_app/callbacks/drawer.py`:
|
- [x] Create `dash_app/callbacks/drawer.py`:
|
||||||
- Open/close drawer: sidebar "Drug Selection" or "Indications" click → open drawer
|
- Open/close drawer: sidebar "Drug Selection" or "Indications" click → open drawer
|
||||||
- Drug selection: clicking a drug chip → adds drug to `selected_drugs` in `app-state` → triggers chart reload
|
- Drug selection: ChipGroup value change → app-state.selected_drugs via update_app_state
|
||||||
- Indication selection: clicking an indication accordion item → filters to drugs under that indication
|
- Drug fragment click: pattern-matching badge clicks → substring match → update chip selection
|
||||||
- Visual highlights: selected drugs get active styling (e.g., blue background on chips)
|
- Clear filters: resets chip selection → app-state.selected_drugs empties
|
||||||
- Clear filters: resets `selected_drugs` and `selected_directorates` in `app-state`
|
- Fragment matching uses `drug.upper() in fragment.upper()` for substring match
|
||||||
- Use pattern-matching callbacks for dynamic drug chips: `@app.callback(..., Input({"type": "drug-chip", "index": ALL}, "n_clicks"))`
|
- Toggle behavior: clicking already-selected fragment deselects matching drugs
|
||||||
- **Checkpoint**: Select drug from drawer → chart filters to show that drug → clear resets
|
- **Checkpoint**: Select drug from drawer → chart filters to show that drug → clear resets
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ def register_callbacks(app):
|
|||||||
from dash_app.callbacks.filters import register_filter_callbacks
|
from dash_app.callbacks.filters import register_filter_callbacks
|
||||||
from dash_app.callbacks.chart import register_chart_callbacks
|
from dash_app.callbacks.chart import register_chart_callbacks
|
||||||
from dash_app.callbacks.kpi import register_kpi_callbacks
|
from dash_app.callbacks.kpi import register_kpi_callbacks
|
||||||
|
from dash_app.callbacks.drawer import register_drawer_callbacks
|
||||||
|
|
||||||
register_filter_callbacks(app)
|
register_filter_callbacks(app)
|
||||||
register_chart_callbacks(app)
|
register_chart_callbacks(app)
|
||||||
register_kpi_callbacks(app)
|
register_kpi_callbacks(app)
|
||||||
|
register_drawer_callbacks(app)
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""Callbacks for the drug browser drawer: open/close, drug selection, fragment matching, clear."""
|
||||||
|
from dash import Input, Output, State, ctx, no_update, ALL
|
||||||
|
|
||||||
|
|
||||||
|
def register_drawer_callbacks(app):
|
||||||
|
"""Register drawer-related callbacks."""
|
||||||
|
|
||||||
|
@app.callback(
|
||||||
|
Output("drug-drawer", "opened"),
|
||||||
|
Input("sidebar-drug-selection", "n_clicks"),
|
||||||
|
Input("sidebar-indications", "n_clicks"),
|
||||||
|
prevent_initial_call=True,
|
||||||
|
)
|
||||||
|
def open_drawer(_drug_clicks, _indication_clicks):
|
||||||
|
"""Open the drawer when sidebar Drug Selection or Indications is clicked."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@app.callback(
|
||||||
|
Output("all-drugs-chips", "value"),
|
||||||
|
Input({"type": "drug-fragment", "index": ALL}, "n_clicks"),
|
||||||
|
Input("clear-drug-filters", "n_clicks"),
|
||||||
|
State("all-drugs-chips", "value"),
|
||||||
|
State("reference-data", "data"),
|
||||||
|
prevent_initial_call=True,
|
||||||
|
)
|
||||||
|
def handle_fragment_or_clear(fragment_clicks, _clear_clicks, current_chips, ref_data):
|
||||||
|
"""Handle drug fragment badge click (substring match) or clear all filters.
|
||||||
|
|
||||||
|
Fragment click: find all full drug names containing the fragment substring,
|
||||||
|
toggle them in the chip selection.
|
||||||
|
Clear click: reset chip selection to empty.
|
||||||
|
"""
|
||||||
|
triggered = ctx.triggered_id
|
||||||
|
|
||||||
|
# Clear button
|
||||||
|
if triggered == "clear-drug-filters":
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 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":
|
||||||
|
# Check if any fragment was actually clicked (not just initial render)
|
||||||
|
if not any(n for n in (fragment_clicks or []) if n):
|
||||||
|
return no_update
|
||||||
|
|
||||||
|
fragment_key = triggered["index"] # e.g. "CARDIOLOGY|ABCIXIMAB"
|
||||||
|
fragment = fragment_key.split("|", 1)[-1] if "|" in fragment_key else fragment_key
|
||||||
|
|
||||||
|
# Get all available drugs from reference data
|
||||||
|
all_drugs = (ref_data or {}).get("available_drugs", [])
|
||||||
|
|
||||||
|
# Find drugs whose names contain this fragment (case-insensitive substring)
|
||||||
|
matching_drugs = [
|
||||||
|
drug for drug in all_drugs
|
||||||
|
if fragment.upper() in drug.upper()
|
||||||
|
]
|
||||||
|
|
||||||
|
if not matching_drugs:
|
||||||
|
return no_update
|
||||||
|
|
||||||
|
# Toggle: if all matching drugs are already selected, deselect them;
|
||||||
|
# otherwise, add them to selection
|
||||||
|
current = set(current_chips or [])
|
||||||
|
all_selected = all(d in current for d in matching_drugs)
|
||||||
|
|
||||||
|
if all_selected:
|
||||||
|
updated = current - set(matching_drugs)
|
||||||
|
else:
|
||||||
|
updated = current | set(matching_drugs)
|
||||||
|
|
||||||
|
return sorted(updated)
|
||||||
|
|
||||||
|
return no_update
|
||||||
@@ -31,12 +31,13 @@ def register_filter_callbacks(app):
|
|||||||
Input("chart-type-indication", "n_clicks"),
|
Input("chart-type-indication", "n_clicks"),
|
||||||
Input("filter-initiated", "value"),
|
Input("filter-initiated", "value"),
|
||||||
Input("filter-last-seen", "value"),
|
Input("filter-last-seen", "value"),
|
||||||
|
Input("all-drugs-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, current_state
|
_dir_clicks, _ind_clicks, initiated, last_seen, selected_drugs, current_state
|
||||||
):
|
):
|
||||||
"""Update app-state when chart type toggle or date filters change."""
|
"""Update app-state when chart type toggle, date filters, or drug chips change."""
|
||||||
if not current_state:
|
if not current_state:
|
||||||
current_state = {
|
current_state = {
|
||||||
"chart_type": "directory",
|
"chart_type": "directory",
|
||||||
@@ -66,6 +67,7 @@ def register_filter_callbacks(app):
|
|||||||
"initiated": initiated,
|
"initiated": initiated,
|
||||||
"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 [],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Toggle pill CSS classes
|
# Toggle pill CSS classes
|
||||||
|
|||||||
Reference in New Issue
Block a user