Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions src/spac/templates/hierarchical_heatmap_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,20 @@ def run_from_json(
json_path : str, Path, or dict
Path to JSON file, JSON string, or parameter dictionary
save_results_flag : bool, optional
Whether to save results to file. If False, returns the figure and
dataframe directly for in-memory workflows. Default is True.
Whether to save results to file. If False, returns a tuple of
(clustergrid_figure, mean_intensity_dataframe) for in-memory
workflows (e.g., Shiny server). Default is True.
show_plot : bool, optional
Whether to display the plot. Default is True.
output_dir : str or Path, optional
Directory for outputs. If None, uses params['Output_Directory'] or '.'

Returns
-------
dict or DataFrame
dict or tuple
If save_results_flag=True: Dictionary of saved file paths
If save_results_flag=False: The mean intensity dataframe
If save_results_flag=False: Tuple of (figure, mean_intensity_df)
where figure is the matplotlib Figure from the ClusterGrid
"""
# Parse parameters from JSON
params = parse_params(json_path)
Expand Down Expand Up @@ -185,9 +187,11 @@ def run_from_json(
print("Hierarchical Heatmap completed successfully.")
return saved_files
else:
# Return the dataframe directly for in-memory workflows
print("Returning mean intensity dataframe (not saving to file)")
return mean_intensity
# Return the ClusterGrid and dataframe for in-memory workflows
# Returns ClusterGrid (not .fig) so consumers can access
# .ax_heatmap for post-processing (e.g. label rotation).
print("Returning (clustergrid, mean_intensity_df) for in-memory use")
return clustergrid, mean_intensity


# CLI interface
Expand Down
94 changes: 82 additions & 12 deletions tests/templates/test_hierarchical_heatmap_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
"""
Real (non-mocked) unit test for the Hierarchical Heatmap template.

Validates template I/O behaviour only.
Snowball seed test — validates template I/O behaviour only:
• Expected output files are produced on disk
• Filenames follow the convention
• Output artifacts are non-empty

No mocking. Uses real data, real filesystem, and tempfile.
"""

Expand All @@ -15,7 +19,7 @@
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
matplotlib.use("Agg") # Headless backend for CI

import anndata as ad
import numpy as np
Expand Down Expand Up @@ -53,14 +57,24 @@ def setUp(self) -> None:
"Upstream_Analysis": self.in_file,
"Annotation": "cell_type",
"Table_to_Visualize": "Original",
"Features_to_Visualize": ["All"],
"Standard_Scale": "None",
"Method": "average",
"Metric": "euclidean",
"Feature_s_": ["All"],
"Standard_Scale_": "None",
"Z_Score": "None",
"Feature_Dendrogram": True,
"Annotation_Dendrogram": True,
"Figure_Title": "Test Hierarchical Heatmap",
"Figure_Width": 6,
"Figure_Height": 4,
"Figure_DPI": 72,
"Font_Size": 8,
"Matrix_Plot_Ratio": 0.8,
"Swap_Axes": False,
"Rotate_Label_": False,
"Horizontal_Dendrogram_Display_Ratio": 0.2,
"Vertical_Dendrogram_Display_Ratio": 0.2,
"Value_Min": "None",
"Value_Max": "None",
"Color_Map": "seismic",
"Output_Directory": self.tmp_dir.name,
"outputs": {
"figures": {"type": "directory", "name": "figures_dir"},
Expand All @@ -82,24 +96,80 @@ def test_hierarchical_heatmap_produces_expected_outputs(self) -> None:
Validates:
1. saved_files dict has 'figures' and 'dataframe' keys
2. Figures directory contains non-empty PNG(s)
3. Summary CSV exists
3. Summary CSV exists and is non-empty
4. Figure title matches the parameter
"""
# -- Act (save_results_flag=True): write outputs to disk -------
saved_files = run_from_json(
self.json_file,
save_results_flag=True,
show_plot=False,
output_dir=self.tmp_dir.name,
)

self.assertIsInstance(saved_files, dict)
self.assertIn("figures", saved_files)
# -- Act (save_results_flag=False): get objects in memory ------
clustergrid, mean_intensity_df = run_from_json(
self.json_file,
save_results_flag=False,
show_plot=False,
)

# -- Assert: return type ---------------------------------------
self.assertIsInstance(
saved_files, dict,
f"Expected dict from run_from_json, got {type(saved_files)}"
)

# -- Assert: figures directory contains at least one PNG -------
self.assertIn("figures", saved_files,
"Missing 'figures' key in saved_files")
figure_paths = saved_files["figures"]
self.assertGreaterEqual(len(figure_paths), 1)
self.assertGreaterEqual(
len(figure_paths), 1, "No figure files were saved"
)

for fig_path in figure_paths:
fig_file = Path(fig_path)
self.assertTrue(fig_file.exists())
self.assertGreater(fig_file.stat().st_size, 0)
self.assertTrue(
fig_file.exists(), f"Figure not found: {fig_path}"
)
self.assertGreater(
fig_file.stat().st_size, 0,
f"Figure file is empty: {fig_path}"
)
self.assertEqual(
fig_file.suffix, ".png",
f"Expected .png extension, got {fig_file.suffix}"
)

# -- Assert: figure has the correct title ----------------------
axes_title = clustergrid.ax_heatmap.get_title()
self.assertEqual(
axes_title, "Test Hierarchical Heatmap",
f"Expected 'Test Hierarchical Heatmap', got '{axes_title}'"
)

# -- Assert: in-memory mean_intensity_df is a DataFrame --------
self.assertIsInstance(
mean_intensity_df, pd.DataFrame,
f"Expected DataFrame, got {type(mean_intensity_df)}"
)
self.assertIn(
"cell_type", mean_intensity_df.columns,
"Annotation column 'cell_type' missing from mean_intensity_df"
)

# -- Assert: summary CSV exists and is non-empty ---------------
self.assertIn("dataframe", saved_files,
"Missing 'dataframe' key in saved_files")
csv_path = Path(saved_files["dataframe"])
self.assertTrue(
csv_path.exists(), f"Summary CSV not found: {csv_path}"
)
self.assertGreater(
csv_path.stat().st_size, 0,
f"Summary CSV is empty: {csv_path}"
)


if __name__ == "__main__":
Expand Down
Loading