Skip to content

UC 1.2 Callbacks

uc_1_2_callbacks

UC-1.2 Callbacks - Regulatory Agency Compound Overlap Analysis.

This module implements callback functions for compound overlap analysis across environmental regulatory agencies using UpSet plots.

Functions:

Name Description
register_uc_1_2_callbacks

Register all UC-1.2 callbacks with Dash app.

Notes
  • Refer to official documentation for use case details
  • Uses UpSetStrategy for set intersection visualization
  • Implements on-demand rendering for performance optimization

Version: 1.0.0

Functions

register_uc_1_2_callbacks

register_uc_1_2_callbacks(app, plot_service) -> None

Register all UC-1.2 callbacks with Dash app.

Parameters:

Name Type Description Default
app Dash

Dash application instance.

required
plot_service PlotService

Singleton PlotService instance (shared across all callbacks).

required
Notes
  • Registers panel toggle and plot rendering callbacks
  • Refer to official documentation for processing logic details
Source code in src/presentation/callbacks/module1/uc_1_2_callbacks.py
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")