def register_uc_1_2_callbacks(app, plot_service) -> None:
"""
Register all UC-1.2 callbacks with Dash app.
Parameters
----------
app : Dash
Dash application instance.
plot_service : PlotService
Singleton PlotService instance (shared across all callbacks).
Notes
-----
- Registers panel toggle and plot rendering callbacks
- Refer to official documentation for processing logic details
"""
logger.info("[UC-1.2] Registering callbacks")
# ========================================
# Callback 1: Toggle Informative Panel
# ========================================
@app.callback(
Output("uc-1-2-collapse", "is_open"),
Input("uc-1-2-collapse-button", "n_clicks"),
State("uc-1-2-collapse", "is_open"),
prevent_initial_call=True,
)
def toggle_uc_1_2_info_panel(n_clicks: Optional[int], is_open: bool) -> bool:
"""
Toggle UC-1.2 informative panel collapse state.
Parameters
----------
n_clicks : int
Number of times button was clicked.
is_open : bool
Current collapse state.
Returns
-------
bool
New collapse state (toggled).
Notes
-----
Simple toggle: open → close, close → open
"""
if n_clicks:
logger.debug(f"[UC-1.2] Toggling info panel: {is_open} → {not is_open}")
return not is_open
return is_open
# ========================================
# Callback 2: Render UpSet Plot
# ========================================
@app.callback(
Output("uc-1-2-chart", "children"),
Input("uc-1-2-accordion-group", "active_item"),
State("merged-result-store", "data"),
prevent_initial_call=True,
)
def render_uc_1_2(active_item: Optional[str], merged_data: Optional[dict]) -> Any:
"""
Render UC-1.2 UpSet plot showing compound overlap across agencies.
Parameters
----------
active_item : str or None
Active accordion item ID (triggers rendering when opened).
merged_data : dict or None
Data from merged-result-store.
Returns
-------
dcc.Graph or html.Div
UpSet plot figure or error message.
Notes
-----
- Extracts and normalizes compound sets grouped by regulatory agency
- Generates visualization using UpSetStrategy via PlotService
"""
logger.info(f"[UC-1.2] ========== RENDER CALLBACK TRIGGERED ==========")
logger.info(f"[UC-1.2] active_item received: '{active_item}'")
logger.info(
f"[UC-1.2] merged_data present: "
f"{merged_data is not None and len(merged_data) > 0}"
)
# Validate accordion is open
if not active_item:
logger.debug("[UC-1.2] active_item is None, accordion not opened yet")
raise PreventUpdate
if active_item != "uc-1-2-accordion":
logger.warning(
f"[UC-1.2] Unexpected active_item: '{active_item}' "
f"(expected 'uc-1-2-accordion')"
)
raise PreventUpdate
logger.info("[UC-1.2] Accordion opened, starting plot generation")
# Validate merged_data
if not merged_data:
logger.error("[UC-1.2] No data available in merged-result-store")
return html.Div(
"No data loaded. Please upload data first.",
className="alert alert-warning",
)
# Extract BioRemPP DataFrame
logger.info("[UC-1.2] Step 1: Extracting BioRemPP data...")
biorempp_df = _extract_biorempp_data(merged_data)
if biorempp_df is None or biorempp_df.empty:
logger.error(
"[UC-1.2] FAILED: Could not extract BioRemPP data "
f"(None: {biorempp_df is None}, "
f"Empty: {biorempp_df.empty if biorempp_df is not None else 'N/A'})"
)
return html.Div(
"Error: BioRemPP data not available.", className="alert alert-danger"
)
logger.info(
f"[UC-1.2] SUCCESS: Extracted BioRemPP DataFrame - "
f"{len(biorempp_df)} rows × {len(biorempp_df.columns)} columns"
)
# Build compound sets
logger.info("[UC-1.2] Step 2: Building compound sets by agency...")
compound_sets = _build_compound_sets(biorempp_df)
if compound_sets is None:
logger.error(
"[UC-1.2] FAILED: Could not build compound sets "
"(check compound/agency columns)"
)
return html.Div(
"Error: Could not build compound sets. " "Please check data structure.",
className="alert alert-danger",
)
logger.info(
f"[UC-1.2] SUCCESS: Built {len(compound_sets)} compound sets - "
f"Agencies: {list(compound_sets.keys())}"
)
for agency, compounds in compound_sets.items():
logger.debug(f"[UC-1.2] - {agency}: {len(compounds)} compounds")
# Convert to DataFrame format expected by UpSetStrategy
logger.info("[UC-1.2] Step 3: Converting to UpSet DataFrame format...")
upset_data = []
for agency, compounds in compound_sets.items():
for compound in compounds:
upset_data.append({"category": agency, "identifier": compound})
upset_df = pd.DataFrame(upset_data)
logger.info(
f"[UC-1.2] SUCCESS: Created UpSet DataFrame - "
f"{len(upset_df)} rows × {len(upset_df.columns)} columns"
)
logger.debug(f"[UC-1.2] DataFrame columns: {list(upset_df.columns)}")
logger.debug(f"[UC-1.2] DataFrame head (5 rows):\n{upset_df.head()}")
# Generate plot using PlotService
logger.info("[UC-1.2] Step 4: Generating UpSet plot...")
try:
logger.debug("[UC-1.2] PlotService instantiated, calling generate_plot()")
fig = plot_service.generate_plot(data=upset_df, use_case_id="UC-1.2")
logger.info("[UC-1.2] SUCCESS: UpSet plot generated successfully!")
logger.debug(f"[UC-1.2] Figure type: {type(fig)}")
# UC-1.2 visual tuning:
# reduce blank area between the UpSet image and "Set Sizes" annotation.
try:
current_margin = {}
if getattr(fig.layout, "margin", None):
current_margin = fig.layout.margin.to_plotly_json()
fig.update_layout(
height=520,
margin={
"l": current_margin.get("l", 20),
"r": current_margin.get("r", 20),
"t": current_margin.get("t", 60),
"b": 40,
},
)
annotations = []
for ann in getattr(fig.layout, "annotations", []) or []:
ann_dict = ann.to_plotly_json()
if "Set Sizes" in str(ann_dict.get("text", "")):
ann_dict["y"] = -0.04
ann_dict["yanchor"] = "top"
annotations.append(ann_dict)
if annotations:
fig.update_layout(annotations=annotations)
except Exception as visual_tuning_error:
logger.debug(
f"[UC-1.2] Visual tuning skipped due to: {visual_tuning_error}"
)
# Return as Dash Graph component
logger.info("[UC-1.2] Step 5: Returning dcc.Graph component")
try:
suggested = sanitize_filename("UC-1.2", "compound_overlap", "png")
base_filename = os.path.splitext(suggested)[0]
except Exception:
base_filename = "uc_1_2_compound_overlap"
graph_component = dcc.Graph(
figure=fig,
config={
"displayModeBar": True,
"displaylogo": False,
"modeBarButtonsToRemove": ["pan2d", "lasso2d", "select2d"],
"toImageButtonOptions": {
"format": "svg",
"filename": base_filename,
"height": 900,
"width": 1400,
"scale": 2,
},
},
)
logger.info("[UC-1.2] ========== RENDER COMPLETE ==========")
return graph_component
except Exception as e:
logger.error(f"[UC-1.2] ========== RENDER FAILED ==========")
logger.error(
f"[UC-1.2] Error type: {type(e).__name__}",
)
logger.error(f"[UC-1.2] Error message: {str(e)}", exc_info=True)
return html.Div(
f"Error generating plot: {str(e)}", className="alert alert-danger"
)
logger.info("[UC-1.2] All callbacks registered successfully")