diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 50b96f6..9a3b1d6 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -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 ### 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 - - Drug selection: clicking a drug chip → adds drug to `selected_drugs` in `app-state` → triggers chart reload - - Indication selection: clicking an indication accordion item → filters to drugs under that indication - - Visual highlights: selected drugs get active styling (e.g., blue background on chips) - - Clear filters: resets `selected_drugs` and `selected_directorates` in `app-state` - - Use pattern-matching callbacks for dynamic drug chips: `@app.callback(..., Input({"type": "drug-chip", "index": ALL}, "n_clicks"))` + - Drug selection: ChipGroup value change → app-state.selected_drugs via update_app_state + - Drug fragment click: pattern-matching badge clicks → substring match → update chip selection + - Clear filters: resets chip selection → app-state.selected_drugs empties + - Fragment matching uses `drug.upper() in fragment.upper()` for substring match + - Toggle behavior: clicking already-selected fragment deselects matching drugs - **Checkpoint**: Select drug from drawer → chart filters to show that drug → clear resets --- diff --git a/dash_app/callbacks/__init__.py b/dash_app/callbacks/__init__.py index 35265e8..ac156eb 100644 --- a/dash_app/callbacks/__init__.py +++ b/dash_app/callbacks/__init__.py @@ -6,7 +6,9 @@ def register_callbacks(app): from dash_app.callbacks.filters import register_filter_callbacks from dash_app.callbacks.chart import register_chart_callbacks from dash_app.callbacks.kpi import register_kpi_callbacks + from dash_app.callbacks.drawer import register_drawer_callbacks register_filter_callbacks(app) register_chart_callbacks(app) register_kpi_callbacks(app) + register_drawer_callbacks(app) diff --git a/dash_app/callbacks/drawer.py b/dash_app/callbacks/drawer.py new file mode 100644 index 0000000..39eb959 --- /dev/null +++ b/dash_app/callbacks/drawer.py @@ -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 diff --git a/dash_app/callbacks/filters.py b/dash_app/callbacks/filters.py index 0f89199..ff523dc 100644 --- a/dash_app/callbacks/filters.py +++ b/dash_app/callbacks/filters.py @@ -31,12 +31,13 @@ def register_filter_callbacks(app): Input("chart-type-indication", "n_clicks"), Input("filter-initiated", "value"), Input("filter-last-seen", "value"), + Input("all-drugs-chips", "value"), State("app-state", "data"), ) 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: current_state = { "chart_type": "directory", @@ -66,6 +67,7 @@ def register_filter_callbacks(app): "initiated": initiated, "last_seen": last_seen, "date_filter_id": date_filter_id, + "selected_drugs": selected_drugs or [], } # Toggle pill CSS classes