|
9 | 9 | from typing import Any, Dict, List, Optional, Tuple, Union |
10 | 10 |
|
11 | 11 | import anndata as ad |
| 12 | + |
12 | 13 | # Set non-interactive backend for matplotlib to prevent GUI popups on macOS |
13 | 14 | import matplotlib |
14 | 15 |
|
|
34 | 35 | INFERCNVPY_AVAILABLE = False |
35 | 36 |
|
36 | 37 | from ..models.data import VisualizationParameters # noqa: E402 |
| 38 | + |
37 | 39 | # Import spatial coordinates helper from data adapter |
38 | 40 | from ..utils.data_adapter import get_spatial_coordinates # noqa: E402 |
| 41 | + |
39 | 42 | # Import error handling utilities |
40 | 43 | from ..utils.error_handling import DataCompatibilityError # noqa: E402 |
41 | 44 | from ..utils.error_handling import DataNotFoundError # noqa: E402 |
42 | 45 | from ..utils.error_handling import InvalidParameterError, ProcessingError # noqa: E402 |
| 46 | + |
43 | 47 | # Import standardized image utilities |
44 | 48 | from ..utils.image_utils import optimize_fig_to_image_with_cache # noqa: E402 |
| 49 | + |
45 | 50 | # Import path utilities for safe file operations |
46 | 51 | from ..utils.path_utils import get_output_dir_from_config # noqa: E402 |
47 | 52 | from ..utils.path_utils import get_safe_output_path # noqa: E402 |
| 53 | + |
48 | 54 | # Import color utilities for categorical data |
49 | 55 | from ._color_utils import _ensure_categorical_colors # noqa: E402 |
50 | 56 |
|
@@ -2846,42 +2852,51 @@ async def create_spatial_domains_visualization( |
2846 | 2852 | adata: ad.AnnData, params: VisualizationParameters, context=None |
2847 | 2853 | ) -> plt.Figure: |
2848 | 2854 | """Create spatial domains visualization""" |
2849 | | - # Look for spatial domain results in adata.obs |
2850 | | - domain_keys = [ |
2851 | | - col |
2852 | | - for col in adata.obs.columns |
2853 | | - if "spatial_domains" in col.lower() or "domain" in col.lower() |
2854 | | - ] |
| 2855 | + # Check if user explicitly specified cluster_key parameter |
| 2856 | + if params.cluster_key: |
| 2857 | + if params.cluster_key not in adata.obs.columns: |
| 2858 | + raise ValueError( |
| 2859 | + f"Specified cluster_key '{params.cluster_key}' not found in data.\n\n" |
| 2860 | + f"Available columns: {', '.join(list(adata.obs.columns)[:20])}" |
| 2861 | + ) |
| 2862 | + domain_keys = [params.cluster_key] |
| 2863 | + else: |
| 2864 | + # Look for spatial domain results in adata.obs |
| 2865 | + domain_keys = [ |
| 2866 | + col |
| 2867 | + for col in adata.obs.columns |
| 2868 | + if "spatial_domains" in col.lower() or "domain" in col.lower() |
| 2869 | + ] |
2855 | 2870 |
|
2856 | | - # Also check for any categorical clustering results that might represent domains |
| 2871 | + # FAIL HONESTLY: Don't guess which column contains spatial domains |
2857 | 2872 | if not domain_keys: |
2858 | | - # Find categorical columns (potential clustering/domain results) |
| 2873 | + # Show available categorical columns for user reference |
2859 | 2874 | categorical_cols = [ |
2860 | 2875 | col |
2861 | 2876 | for col in adata.obs.columns |
2862 | 2877 | if adata.obs[col].dtype.name in ["object", "category"] |
2863 | 2878 | ] |
2864 | | - domain_keys = categorical_cols[ |
2865 | | - :5 |
2866 | | - ] # Take first 5 categorical columns as potential domains |
2867 | 2879 |
|
2868 | | - if not domain_keys: |
2869 | | - # No spatial domains found, suggest running domain identification first |
2870 | | - fig, ax = plt.subplots(figsize=params.figure_size or (10, 8)) |
2871 | | - ax.text( |
2872 | | - 0.5, |
2873 | | - 0.5, |
| 2880 | + error_msg = ( |
2874 | 2881 | "No spatial domains found in dataset.\n\n" |
2875 | | - "Please run spatial domain identification first:\n" |
2876 | | - 'identify_spatial_domains(data_id="data_1", params={"method": "spagcn"})', |
2877 | | - ha="center", |
2878 | | - va="center", |
2879 | | - transform=ax.transAxes, |
2880 | | - fontsize=12, |
| 2882 | + f"Available categorical columns ({len(categorical_cols)} total):\n" |
| 2883 | + f" {', '.join(categorical_cols[:15])}" |
2881 | 2884 | ) |
2882 | | - ax.set_title("Spatial Domains - Not Available") |
2883 | | - ax.axis("off") |
2884 | | - return fig |
| 2885 | + |
| 2886 | + if len(categorical_cols) > 15: |
| 2887 | + error_msg += f"\n ... and {len(categorical_cols) - 15} more" |
| 2888 | + |
| 2889 | + error_msg += ( |
| 2890 | + "\n\nSOLUTIONS:\n" |
| 2891 | + "1. Run spatial domain identification first:\n" |
| 2892 | + ' identify_spatial_domains(data_id="your_data_id", params={"method": "spagcn"})\n\n' |
| 2893 | + "2. Or specify an existing clustering column explicitly:\n" |
| 2894 | + ' visualize_data(data_id, params={"plot_type": "spatial_domains", "cluster_key": "leiden"})\n\n' |
| 2895 | + "NOTE: ChatSpatial uses 'cluster_key' parameter.\n" |
| 2896 | + " This maintains consistency with other visualization functions." |
| 2897 | + ) |
| 2898 | + |
| 2899 | + raise ValueError(error_msg) |
2885 | 2900 |
|
2886 | 2901 | # Use the first available domain key |
2887 | 2902 | domain_key = domain_keys[0] |
@@ -3058,23 +3073,18 @@ async def create_cell_communication_visualization( |
3058 | 3073 | elif lr_columns: |
3059 | 3074 | return _create_lr_expression_plot(adata, lr_columns, params, context) |
3060 | 3075 | else: |
3061 | | - # No communication data found, create instructional plot |
3062 | | - fig, ax = plt.subplots(figsize=params.figure_size or (10, 8)) |
3063 | | - ax.text( |
3064 | | - 0.5, |
3065 | | - 0.5, |
| 3076 | + # No communication data found - fail honestly |
| 3077 | + raise DataNotFoundError( |
3066 | 3078 | "No cell communication results found in dataset.\n\n" |
3067 | | - "To analyze cell communication, first run:\n" |
3068 | | - 'analyze_cell_communication(data_id="data_1", params={"method": "liana"})\n\n' |
3069 | | - "This will generate LIANA+ results for visualization.", |
3070 | | - ha="center", |
3071 | | - va="center", |
3072 | | - transform=ax.transAxes, |
3073 | | - fontsize=12, |
| 3079 | + "SOLUTIONS:\n" |
| 3080 | + "1. Run cell communication analysis first:\n" |
| 3081 | + ' analyze_cell_communication(data_id="your_data_id", ' |
| 3082 | + 'params={"species": "human", "cell_type_key": "your_cell_type_column"})\n\n' |
| 3083 | + "2. Ensure analysis completed successfully and generated results in:\n" |
| 3084 | + " - adata.uns (for cluster-based analysis)\n" |
| 3085 | + " - adata.obsm (for spatial analysis)\n\n" |
| 3086 | + "Available analysis methods: liana, cellphonedb, cellchat_liana" |
3074 | 3087 | ) |
3075 | | - ax.set_title("Cell Communication - Not Available") |
3076 | | - ax.axis("off") |
3077 | | - return fig |
3078 | 3088 |
|
3079 | 3089 |
|
3080 | 3090 | def _create_spatial_communication_plot( |
@@ -3135,18 +3145,17 @@ def _create_spatial_communication_plot( |
3135 | 3145 | pair_indices = [] |
3136 | 3146 |
|
3137 | 3147 | if not top_pairs: |
3138 | | - fig, ax = plt.subplots(figsize=(8, 6)) |
3139 | | - ax.text( |
3140 | | - 0.5, |
3141 | | - 0.5, |
3142 | | - "No communication pairs found in spatial scores", |
3143 | | - ha="center", |
3144 | | - va="center", |
3145 | | - transform=ax.transAxes, |
| 3148 | + raise DataNotFoundError( |
| 3149 | + "No communication pairs found in spatial scores.\n\n" |
| 3150 | + "POSSIBLE CAUSES:\n" |
| 3151 | + "1. Spatial communication analysis generated empty results\n" |
| 3152 | + "2. No significant L-R pairs detected in your dataset\n" |
| 3153 | + "3. Analysis parameters too stringent\n\n" |
| 3154 | + "SOLUTIONS:\n" |
| 3155 | + "1. Check if cell communication analysis completed successfully\n" |
| 3156 | + "2. Try adjusting analysis parameters (e.g., lower significance threshold)\n" |
| 3157 | + "3. Verify spatial coordinates and cell type annotations are correct" |
3146 | 3158 | ) |
3147 | | - ax.set_title("Cell Communication - Spatial") |
3148 | | - ax.axis("off") |
3149 | | - return fig |
3150 | 3159 |
|
3151 | 3160 | # Create subplot layout |
3152 | 3161 | n_pairs = min(len(top_pairs), 6) # Limit to 6 for display |
@@ -3234,18 +3243,17 @@ def _create_spatial_communication_plot( |
3234 | 3243 | return fig |
3235 | 3244 |
|
3236 | 3245 | except Exception as e: |
3237 | | - fig, ax = plt.subplots(figsize=(8, 6)) |
3238 | | - ax.text( |
3239 | | - 0.5, |
3240 | | - 0.5, |
3241 | | - f"Error creating spatial communication plot:\n{str(e)}", |
3242 | | - ha="center", |
3243 | | - va="center", |
3244 | | - transform=ax.transAxes, |
3245 | | - ) |
3246 | | - ax.set_title("Cell Communication - Error") |
3247 | | - ax.axis("off") |
3248 | | - return fig |
| 3246 | + raise ProcessingError( |
| 3247 | + f"Failed to create spatial communication plot: {str(e)}\n\n" |
| 3248 | + "POSSIBLE CAUSES:\n" |
| 3249 | + "1. Invalid spatial coordinates\n" |
| 3250 | + "2. Incompatible data format in adata.obsm['liana_spatial_scores']\n" |
| 3251 | + "3. Missing or corrupted communication results\n\n" |
| 3252 | + "SOLUTIONS:\n" |
| 3253 | + "1. Verify spatial coordinates exist in adata.obsm['spatial']\n" |
| 3254 | + "2. Re-run cell communication analysis\n" |
| 3255 | + "3. Check data integrity" |
| 3256 | + ) from e |
3249 | 3257 |
|
3250 | 3258 |
|
3251 | 3259 | def _create_cluster_communication_plot(adata, communication_results, params, context): |
@@ -3317,18 +3325,17 @@ def _create_cluster_communication_plot(adata, communication_results, params, con |
3317 | 3325 | return fig |
3318 | 3326 |
|
3319 | 3327 | except Exception as e: |
3320 | | - fig, ax = plt.subplots(figsize=(8, 6)) |
3321 | | - ax.text( |
3322 | | - 0.5, |
3323 | | - 0.5, |
3324 | | - f"Error creating cluster communication plot:\n{str(e)}", |
3325 | | - ha="center", |
3326 | | - va="center", |
3327 | | - transform=ax.transAxes, |
3328 | | - ) |
3329 | | - ax.set_title("Cell Communication - Error") |
3330 | | - ax.axis("off") |
3331 | | - return fig |
| 3328 | + raise ProcessingError( |
| 3329 | + f"Failed to create cluster communication plot: {str(e)}\n\n" |
| 3330 | + "POSSIBLE CAUSES:\n" |
| 3331 | + "1. Invalid communication results format\n" |
| 3332 | + "2. Missing required columns in results DataFrame\n" |
| 3333 | + "3. Data corruption in adata.uns\n\n" |
| 3334 | + "SOLUTIONS:\n" |
| 3335 | + "1. Verify communication analysis completed successfully\n" |
| 3336 | + "2. Check adata.uns contains valid LIANA+ results\n" |
| 3337 | + "3. Re-run cell communication analysis" |
| 3338 | + ) from e |
3332 | 3339 |
|
3333 | 3340 |
|
3334 | 3341 | def _create_lr_expression_plot(adata, lr_columns, params, context): |
@@ -3416,18 +3423,17 @@ def _create_lr_expression_plot(adata, lr_columns, params, context): |
3416 | 3423 | return fig |
3417 | 3424 |
|
3418 | 3425 | except Exception as e: |
3419 | | - fig, ax = plt.subplots(figsize=(8, 6)) |
3420 | | - ax.text( |
3421 | | - 0.5, |
3422 | | - 0.5, |
3423 | | - f"Error creating LR expression plot:\n{str(e)}", |
3424 | | - ha="center", |
3425 | | - va="center", |
3426 | | - transform=ax.transAxes, |
3427 | | - ) |
3428 | | - ax.set_title("Cell Communication - Error") |
3429 | | - ax.axis("off") |
3430 | | - return fig |
| 3426 | + raise ProcessingError( |
| 3427 | + f"Failed to create ligand-receptor expression plot: {str(e)}\n\n" |
| 3428 | + "POSSIBLE CAUSES:\n" |
| 3429 | + "1. Missing spatial coordinates\n" |
| 3430 | + "2. Invalid L-R columns in adata.obs\n" |
| 3431 | + "3. Data format incompatibility\n\n" |
| 3432 | + "SOLUTIONS:\n" |
| 3433 | + "1. Verify spatial coordinates exist in adata.obsm['spatial']\n" |
| 3434 | + "2. Check L-R expression columns in adata.obs\n" |
| 3435 | + "3. Re-run cell communication analysis" |
| 3436 | + ) from e |
3431 | 3437 |
|
3432 | 3438 |
|
3433 | 3439 | async def create_multi_gene_umap_visualization( |
|
0 commit comments