Skip to content

UC 4.13 Callbacks

uc_4_13_callbacks

UC-4.13 Callbacks - Interactive Genetic Profile by Compound Pathway (HADEG).

This module provides callback functions for UC-4.13, handling dropdown population and heatmap rendering based on compound pathway selection for genetic profile analysis.

Callbacks

register_uc_4_13_callbacks Register all UC-4.13 callbacks with Dash app.

toggle_uc_4_13_info_panel Toggle informative panel collapse.

initialize_compound_pathway_dropdown_uc_4_13 Populate dropdown with unique compound pathways from HADEG data.

render_uc_4_13 Generate heatmap showing gene-sample matrix for selected compound pathway.

Notes

Rendering Logic: - Dropdown population: On accordion open (active_item trigger) - Chart rendering: On dropdown selection (on-demand) - No auto-update (single trigger: dropdown value change)

Database Support: - HADEG: ✅ Contains 'sample', 'Gene', 'compound_pathway', 'Pathway', 'ko' - BioRemPP/KEGG/ToxCSM: ❌ Do NOT contain HADEG pathway data

Data Processing (inline): - Filter by selected compound pathway - Pass filtered data to PlotService - HeatmapStrategy handles: * Groupby (Gene, sample) → nunique(ko) * Pivot to matrix (Gene × sample) * Sort by totals * Create heatmap

Author: BioRemPP Development Team Date: 2025-11-25 Version: 1.0.0

Functions

register_uc_4_13_callbacks

register_uc_4_13_callbacks(app, plot_service) -> None

Register UC-4.13 callbacks with Dash app.

Parameters:

Name Type Description Default
app Dash

Dash application instance.

required
Notes

Registered callbacks: - toggle_uc_4_13_info_panel: Toggle informative panel collapse - initialize_compound_pathway_dropdown_uc_4_13: Populate dropdown options - render_uc_4_13: Render heatmap on dropdown selection

Source code in src/presentation/callbacks/module4/uc_4_13_callbacks.py
def register_uc_4_13_callbacks(app, plot_service) -> None:
    """
    Register UC-4.13 callbacks with Dash app.

    Parameters
    ----------
    app : Dash
        Dash application instance.

    Notes
    -----
    Registered callbacks:
    - toggle_uc_4_13_info_panel: Toggle informative panel collapse
    - initialize_compound_pathway_dropdown_uc_4_13: Populate dropdown options
    - render_uc_4_13: Render heatmap on dropdown selection
    """

    @app.callback(
        Output("uc-4-13-collapse", "is_open"),
        Input("uc-4-13-collapse-button", "n_clicks"),
        State("uc-4-13-collapse", "is_open"),
        prevent_initial_call=True,
    )
    def toggle_uc_4_13_info_panel(n_clicks, is_open):
        """
        Toggle UC-4.13 informative panel collapse.

        Parameters
        ----------
        n_clicks : int
            Number of clicks on collapse button.
        is_open : bool
            Current collapse state.

        Returns
        -------
        bool
            New collapse state (toggled).
        """
        logger.info(
            f"[UC-4.13] 🔘 Toggle clicked! n_clicks={n_clicks}, " f"is_open={is_open}"
        )
        if n_clicks:
            new_state = not is_open
            logger.info(f"[UC-4.13] [OK] Panel toggled to: {new_state}")
            return new_state
        logger.info(f"[UC-4.13] ⊘ No clicks, keeping is_open={is_open}")
        return is_open

    @app.callback(
        [
            Output("uc-4-13-compound-pathway-dropdown", "options"),
            Output("uc-4-13-compound-pathway-dropdown", "value"),
        ],
        [
            Input("merged-result-store", "data"),
            Input("uc-4-13-accordion-group", "active_item"),
        ],
        prevent_initial_call=True,
    )
    def initialize_compound_pathway_dropdown_uc_4_13(
        merged_data: Optional[dict], active_item: Optional[str]
    ) -> Tuple[list, None]:
        """
        Initialize compound pathway dropdown with HADEG data.

        This callback populates the dropdown menu with available compound
        pathways extracted from processed HADEG data, enabling users to select
        specific pathways for genetic profile analysis.

        Data Processing (inline):
        1. Extract HADEG data from store
        2. Validate 'compound_pathway' column exists
        3. Extract unique compound pathways
        4. Sort alphabetically
        5. Create dropdown options

        Parameters
        ----------
        merged_data : Optional[dict]
            Pre-processed merged data stored in merged-result-store.
            Expected structure: dict with 'hadeg_df' key.
        active_item : Optional[str]
            Currently active accordion item (triggers re-initialization).

        Returns
        -------
        Tuple[list, None]
            - First element: List of dropdown option dictionaries with
              label/value pairs for compound pathway selection. Empty list
              if no data available.
            - Second element: Default selection value (None for no
              initial selection).

        Raises
        ------
        PreventUpdate
            If no data available or required column not found.
        """
        logger.info(
            f"[UC-4.13] 🔄 Dropdown init triggered, data type: {type(merged_data)}"
        )

        if not merged_data:
            logger.debug("[UC-4.13] No data in store, preventing dropdown init")
            return [], None

        # Check if this is initial call with empty/invalid data
        if isinstance(merged_data, dict) and not merged_data:
            logger.debug("[UC-4.13] Empty dict in store, preventing dropdown init")
            return [], None

        try:
            # Extract HADEG DataFrame from store
            if not isinstance(merged_data, dict) or "hadeg_df" not in merged_data:
                logger.error(
                    f"[UC-4.13] Invalid data format: expected dict with 'hadeg_df', "
                    f"got {type(merged_data)}"
                )
                raise PreventUpdate

            df = pd.DataFrame(merged_data["hadeg_df"])

            # Validate 'Compound' column exists (compound pathway in HADEG)
            compound_pathway_col_variants = [
                "Compound",
                "compound",
                "compound_pathway",
                "Compound_Pathway",
                "CompoundPathway",
                "compound_path",
                "Compound_Path",
            ]

            compound_pathway_col = None
            for variant in compound_pathway_col_variants:
                if variant in df.columns:
                    compound_pathway_col = variant
                    logger.debug(
                        f"[UC-4.13] Found compound pathway column: '{variant}'"
                    )
                    break

            if not compound_pathway_col:
                logger.error(
                    f"[UC-4.13] Required column 'Compound' not found. "
                    f"Available columns: {df.columns.tolist()}"
                )
                raise PreventUpdate

            # Extract unique compound pathways
            compound_pathways = sorted(df[compound_pathway_col].dropna().unique())

            logger.debug(
                f"[UC-4.13] Extracted {len(compound_pathways)} unique compounds: "
                f"{compound_pathways[:5]}..."  # Show first 5
            )

            # Create dropdown options
            options = [
                {"label": pathway, "value": pathway} for pathway in compound_pathways
            ]

            logger.info(
                f"[UC-4.13] Dropdown initialized with {len(options)} "
                f"compound pathways"
            )

            return options, None

        except Exception as e:
            logger.error(f"[UC-4.13] Dropdown initialization error: {e}")
            raise PreventUpdate

    @app.callback(
        Output("uc-4-13-chart-container", "children"),
        Input("uc-4-13-compound-pathway-dropdown", "value"),
        State("merged-result-store", "data"),
        prevent_initial_call=True,
    )
    def render_uc_4_13(
        selected_compound_pathway: Optional[str], merged_data: Optional[dict]
    ) -> Any:
        """
        Render UC-4.13 heatmap for selected compound pathway.

        This callback generates a heatmap visualization showing the genetic
        profile (Gene × Sample matrix) with unique KO counts for the selected
        compound pathway.

        Data Processing (inline):
        1. Extract HADEG data from store
        2. Validate required columns
        3. Filter by selected compound pathway
        4. Pass filtered data to PlotService
        5. HeatmapStrategy processes:
           - Groups by (Gene, sample)
           - Counts unique KOs
           - Pivots to matrix
           - Sorts by totals
           - Creates heatmap

        Parameters
        ----------
        selected_compound_pathway : Optional[str]
            Selected compound pathway from dropdown.
        merged_data : Optional[dict]
            Merged data from store with 'hadeg_df' key.

        Returns
        -------
        dcc.Graph or html.Div
            Heatmap chart component or informative/error message.

        Raises
        ------
        PreventUpdate
            If no compound pathway selected or no data available.
        """
        if not selected_compound_pathway:
            logger.debug("[UC-4.13] No compound pathway selected, preventing update")
            raise PreventUpdate

        if not merged_data:
            logger.warning("[UC-4.13] No data available")
            return _create_error_message("No data available for visualization")

        try:
            # Extract HADEG DataFrame from store
            logger.debug(f"[UC-4.13] Received data type: {type(merged_data)}")

            if not isinstance(merged_data, dict) or "hadeg_df" not in merged_data:
                logger.error(
                    f"[UC-4.13] Invalid data format: expected dict with 'hadeg_df'"
                )
                return _create_error_message(
                    "HADEG database data not found. "
                    "Please ensure HADEG data is loaded."
                )

            df = pd.DataFrame(merged_data["hadeg_df"])

            # Validate required columns with variants
            required_cols_variants = {
                "sample": ["Sample", "sample", "sample_id"],
                "Gene": ["Gene", "gene", "GeneSymbol", "gene_symbol"],
                "compound_pathway": [
                    "Compound",
                    "compound",
                    "compound_pathway",
                    "Compound_Pathway",
                    "CompoundPathway",
                ],
                "Pathway": ["Pathway", "pathway", "Path"],
                "ko": ["KO", "ko", "ko_id"],
            }

            col_mapping = {}
            for required, variants in required_cols_variants.items():
                found = False
                for variant in variants:
                    if variant in df.columns:
                        col_mapping[required] = variant
                        found = True
                        logger.debug(f"[UC-4.13] Mapped '{required}' → '{variant}'")
                        break
                if not found:
                    logger.error(
                        f"[UC-4.13] Required column '{required}' not found. "
                        f"Available: {df.columns.tolist()}"
                    )
                    return _create_error_message(f"Missing required column: {required}")

            # Normalize column names
            rename_mapping = {}
            if col_mapping["sample"] != "sample":
                rename_mapping[col_mapping["sample"]] = "sample"
            if col_mapping["Gene"] != "Gene":
                rename_mapping[col_mapping["Gene"]] = "Gene"
            if col_mapping["compound_pathway"] != "compound_pathway":
                rename_mapping[col_mapping["compound_pathway"]] = "compound_pathway"
            if col_mapping["Pathway"] != "Pathway":
                rename_mapping[col_mapping["Pathway"]] = "Pathway"
            if col_mapping["ko"] != "ko":
                rename_mapping[col_mapping["ko"]] = "ko"

            if rename_mapping:
                df = df.rename(columns=rename_mapping)
                logger.debug(f"[UC-4.13] Renamed columns: {rename_mapping}")

            # Filter by selected compound pathway
            filtered_df = df[df["compound_pathway"] == selected_compound_pathway].copy()

            if filtered_df.empty:
                logger.warning(
                    f"[UC-4.13] No data found for compound pathway: "
                    f"'{selected_compound_pathway}'"
                )
                return _create_error_message(
                    f"No data found for compound pathway '{selected_compound_pathway}'. "
                    f"Try selecting a different pathway."
                )

            logger.info(
                f"[UC-4.13] Filtered data: {len(filtered_df)} rows for "
                f"compound pathway '{selected_compound_pathway}'"
            )

            # Remove NaNs from required columns
            filtered_df = filtered_df.dropna(subset=["sample", "Gene", "ko"])

            if filtered_df.empty:
                logger.warning(f"[UC-4.13] No valid data after removing NaNs")
                return _create_error_message("No valid data found after data cleaning.")

            # Generate plot using PlotService
            # HeatmapStrategy handles aggregation and matrix creation
            use_case_id = "UC-4.13"

            logger.info(
                f"[UC-4.13] Calling PlotService for {use_case_id} "
                f"with {len(filtered_df)} rows"
            )

            fig = plot_service.generate_plot(use_case_id=use_case_id, data=filtered_df)

            # Update title dynamically
            fig.update_layout(
                title=f"Genetic Profile for {selected_compound_pathway}", title_x=0.5
            )

            logger.info(f"[UC-4.13] [OK] Heatmap generated successfully")

            try:
                suggested = sanitize_filename("UC-4.13", "pathway_summary", "png")
            except Exception:
                suggested = "pathway_summary.png"

            base_filename = os.path.splitext(suggested)[0]

            return dcc.Graph(
                figure=fig,
                config={
                    "displayModeBar": True,
                    "toImageButtonOptions": {
                        "format": "svg",
                        "filename": base_filename,
                    },
                },
                style={"height": "700px"},
            )

        except ValueError as ve:
            logger.error(f"[UC-4.13] Value error: {ve}")
            return _create_error_message(str(ve))
        except Exception as e:
            logger.error(f"[UC-4.13] Rendering error: {e}", exc_info=True)
            return _create_error_message(f"Error generating heatmap: {str(e)}")