def register_uc_8_3_callbacks(app, plot_service) -> None:
"""
Register all callbacks for UC-8.3.
Parameters
----------
app : Dash
Dash application instance.
plot_service : PlotService
Singleton PlotService instance (shared across all callbacks).
Notes
-----
- Registers information panel toggle and heatmap rendering callbacks
- Refer to official documentation for processing logic details
"""
logger.info("Registering UC-8.3 callbacks")
@app.callback(
Output("uc-8-3-collapse", "is_open"),
Input("uc-8-3-collapse-button", "n_clicks"),
State("uc-8-3-collapse", "is_open"),
prevent_initial_call=True,
)
def toggle_uc_8_3_info_panel(n_clicks: Optional[int], is_open: bool) -> bool:
"""
Toggle UC-8.3 information panel visibility.
Parameters
----------
n_clicks : int, optional
Number of times collapse button has been clicked.
is_open : bool
Current state of collapse component.
Returns
-------
bool
New state of collapse component (True = open, False = closed).
"""
if n_clicks:
logger.debug(f"UC-8.3 info panel toggled. New state: {not is_open}")
return not is_open
return is_open
@app.callback(
Output("uc-8-3-chart", "children"),
Input("uc-8-3-accordion-group", "active_item"),
State("merged-result-store", "data"),
prevent_initial_call=True,
)
def render_uc_8_3(
active_item: Optional[str], merged_data: Optional[Dict[str, Any]]
) -> html.Div:
"""
Render UC-8.3 heatmap scorecard when accordion is activated.
Parameters
----------
active_item : str, optional
ID of currently active accordion item.
merged_data : dict, optional
Dictionary containing 'biorempp_df' key.
Returns
-------
html.Div
Container with loading spinner and heatmap or error message.
Raises
------
PreventUpdate
If accordion not active or data not ready.
ValueError
If required columns are missing or data invalid.
Notes
-----
- Extracts BioRemPP DataFrame and maps column names flexibly
- Calculates completeness scores: (sample KOs / compound KOs) × 100%
- Generates heatmap with samples (rows) × compound names (columns)
- Uses PlotService with HeatmapScoredStrategy
"""
logger.debug(f"UC-8.3 render callback triggered. Active item: {active_item}")
# Check if UC-8.3 accordion is active
if not active_item or active_item != "uc-8-3-accordion":
logger.debug("UC-8.3 accordion not active. Preventing update.")
raise PreventUpdate
try:
# Validate merged_data structure
if not merged_data:
logger.warning("UC-8.3: merged_data is None or empty")
return _create_error_message(
"No data available. Please load or merge data first.",
"bi bi-exclamation-triangle",
)
if not isinstance(merged_data, dict) or "biorempp_df" not in merged_data:
logger.error("UC-8.3: merged_data does not contain 'biorempp_df' key")
return _create_error_message(
"Invalid data structure. Expected 'biorempp_df' in merged data.",
"bi bi-x-circle",
)
# Extract DataFrame
logger.debug("UC-8.3: Extracting DataFrame from merged_data")
df = pd.DataFrame(merged_data["biorempp_df"])
if df.empty:
logger.warning("UC-8.3: DataFrame is empty")
return _create_error_message(
"The dataset is empty. Please load data with KO, Sample, and Compound_Name information.",
"bi bi-inbox",
)
logger.info(
f"UC-8.3: Processing DataFrame with {len(df)} rows and {len(df.columns)} columns"
)
# Map column names flexibly to handle different naming conventions
col_map = {}
# Try to find sample column
sample_candidates = [
"Sample",
"sample",
"sample_id",
"Sample_ID",
"sampleID",
"genome",
"Genome",
"organism",
]
for col_name in sample_candidates:
if col_name in df.columns:
col_map["Sample"] = col_name
logger.debug(f"UC-8.3: Mapped Sample to column '{col_name}'")
break
# Try to find KO column
ko_candidates = ["KO", "ko", "ko_id", "KO_ID", "ko_number"]
for col_name in ko_candidates:
if col_name in df.columns:
col_map["KO"] = col_name
logger.debug(f"UC-8.3: Mapped KO to column '{col_name}'")
break
# Try to find Compound_Name column
compound_candidates = [
"Compound_Name",
"compound_name",
"compoundname",
"CompoundName",
"compound",
"Compound",
]
for col_name in compound_candidates:
if col_name in df.columns:
col_map["Compound_Name"] = col_name
logger.debug(f"UC-8.3: Mapped Compound_Name to column '{col_name}'")
break
# Validate required columns were found
required_fields = ["Sample", "KO", "Compound_Name"]
missing_fields = [
field for field in required_fields if field not in col_map
]
if missing_fields:
logger.error(f"UC-8.3: Missing required columns: {missing_fields}")
return _create_error_message(
f"Missing required columns: {', '.join(missing_fields)}. "
f"Available columns: {', '.join(df.columns.tolist())}",
"bi bi-x-circle",
)
# Rename columns to standard names for processing
df_processed = df.rename(columns={v: k for k, v in col_map.items()})
# Clean data: strip whitespace, handle nulls
logger.debug("UC-8.3: Cleaning data")
df_processed = df_processed.copy()
df_processed["Sample"] = df_processed["Sample"].astype(str).str.strip()
df_processed["KO"] = df_processed["KO"].astype(str).str.strip().str.upper()
df_processed["Compound_Name"] = (
df_processed["Compound_Name"].astype(str).str.strip()
)
# Remove null/empty entries
df_processed = df_processed[
(df_processed["Sample"] != "")
& (df_processed["Sample"] != "nan")
& (df_processed["KO"] != "")
& (df_processed["KO"] != "NAN")
& (df_processed["Compound_Name"] != "")
& (df_processed["Compound_Name"] != "nan")
]
if df_processed.empty:
logger.warning("UC-8.3: No valid data after cleaning")
return _create_error_message(
"No valid data available after cleaning. Check for null values.",
"bi bi-inbox",
)
logger.info(
f"UC-8.3: Cleaned data - {len(df_processed)} records, "
f"{df_processed['Sample'].nunique()} samples, "
f"{df_processed['Compound_Name'].nunique()} compounds"
)
# Generate heatmap using PlotService
logger.debug("UC-8.3: Generating heatmap via PlotService")
fig = plot_service.generate_plot(
data=df_processed,
use_case_id="UC-8.3",
filters={},
customizations={},
)
logger.info("UC-8.3: Heatmap generated successfully")
# Use canonical filename
try:
suggested = sanitize_filename(
"UC-8.3", "compound_specific_completeness", "png"
)
except Exception:
suggested = "compound_specific_completeness.png"
base_filename = os.path.splitext(suggested)[0]
return html.Div(
dcc.Graph(
figure=fig,
config={
"displayModeBar": True,
"displaylogo": False,
"toImageButtonOptions": {
"format": "svg",
"filename": base_filename,
"height": 800,
"width": 1000,
"scale": 2,
},
},
),
style={"minHeight": "650px"},
)
except ValueError as ve:
logger.error(f"UC-8.3: Validation error - {str(ve)}", exc_info=True)
return _create_error_message(
f"Data validation error: {str(ve)}",
"bi bi-exclamation-triangle",
)
except KeyError as ke:
logger.error(f"UC-8.3: Missing key error - {str(ke)}", exc_info=True)
return _create_error_message(
f"Configuration error: Missing key '{str(ke)}'. "
"Please ensure uc_8_3_config.yaml exists and is properly configured.",
"bi bi-gear",
)
except Exception as e:
logger.error(f"UC-8.3: Unexpected error - {str(e)}", exc_info=True)
return _create_error_message(
f"An unexpected error occurred while rendering the heatmap: {str(e)}",
"bi bi-bug",
)