diff --git a/src/spac/templates/hierarchical_heatmap_template.py b/src/spac/templates/hierarchical_heatmap_template.py index 92bec6cc..f77e265a 100644 --- a/src/spac/templates/hierarchical_heatmap_template.py +++ b/src/spac/templates/hierarchical_heatmap_template.py @@ -42,8 +42,9 @@ 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 @@ -51,9 +52,10 @@ def run_from_json( 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) @@ -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 diff --git a/tests/templates/test_hierarchical_heatmap_template.py b/tests/templates/test_hierarchical_heatmap_template.py index 1964b9a0..ce2f2968 100644 --- a/tests/templates/test_hierarchical_heatmap_template.py +++ b/tests/templates/test_hierarchical_heatmap_template.py @@ -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. """ @@ -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 @@ -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"}, @@ -82,8 +96,10 @@ 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, @@ -91,15 +107,69 @@ def test_hierarchical_heatmap_produces_expected_outputs(self) -> None: 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__":