Skip to content

Commit affeff3

Browse files
committed
added small plotting func
1 parent 9ef97fd commit affeff3

3 files changed

Lines changed: 172 additions & 1 deletion

File tree

src/squidpy/exp/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@
99
from . import im
1010
from .im._qc import qc_sharpness
1111

12-
__all__ = ["qc_sharpness"]
12+
from . import pl
13+
from .pl._qc import qc_sharpness_metrics
14+
15+
__all__ = ["qc_sharpness", "qc_sharpness_metrics"]

src/squidpy/exp/pl/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ._qc import qc_sharpness_metrics
2+
3+
__all__ = ["qc_sharpness_metrics"]

src/squidpy/exp/pl/_qc.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import matplotlib.pyplot as plt
2+
import numpy as np
3+
from typing import Optional, Tuple
4+
from spatialdata._logging import logger as logg
5+
from squidpy.exp.im._qc import SHARPNESS_METRICS
6+
7+
8+
9+
def qc_sharpness_metrics(
10+
sdata,
11+
image_key: str,
12+
metrics: Optional[SHARPNESS_METRICS | list[SHARPNESS_METRICS]] = None,
13+
figsize: Optional[Tuple[int, int]] = None,
14+
return_fig: bool = False,
15+
**kwargs
16+
) -> Optional[plt.Figure]:
17+
"""
18+
Plot a summary view of raw sharpness metrics from qc_sharpness results.
19+
20+
Automatically scans adata.uns for calculated metrics and plots the raw sharpness values.
21+
Creates a multi-panel plot: one panel per calculated sharpness metric.
22+
Each panel shows: spatial view, histogram, and statistics.
23+
24+
Parameters
25+
----------
26+
sdata : SpatialData
27+
SpatialData object containing QC results.
28+
image_key : str
29+
Image key used in qc_sharpness function.
30+
metrics : SHARPNESS_METRICS or list of SHARPNESS_METRICS, optional
31+
Specific metrics to plot. If None, plots all calculated sharpness metrics.
32+
Use SHARPNESS_METRICS enum values.
33+
figsize : tuple, optional
34+
Figure size (width, height). Auto-calculated if None.
35+
return_fig : bool
36+
Whether to return the figure object. Default is False.
37+
**kwargs
38+
Additional arguments passed to render_shapes().
39+
40+
Returns
41+
-------
42+
fig : matplotlib.Figure or None
43+
The matplotlib figure object if return_fig=True, otherwise None.
44+
"""
45+
import matplotlib.pyplot as plt
46+
47+
# Expected keys
48+
table_key = f"qc_img_{image_key}_sharpness"
49+
shapes_key = f"qc_img_{image_key}_sharpness_grid"
50+
51+
if table_key not in sdata.tables:
52+
raise ValueError(f"No QC data found for image '{image_key}'. Run sq.exp.im.qc_sharpness() first.")
53+
54+
adata = sdata.tables[table_key]
55+
56+
# Check if qc_sharpness metadata exists
57+
if "qc_sharpness" not in adata.uns:
58+
raise ValueError(f"No qc_sharpness metadata found. Run sq.exp.im.qc_sharpness() first.")
59+
60+
# Get calculated metrics from metadata
61+
calculated_metrics = adata.uns["qc_sharpness"]["metrics"]
62+
63+
if not calculated_metrics:
64+
raise ValueError(f"No sharpness metrics found in metadata.")
65+
66+
# Filter for specific metrics if requested
67+
if metrics is not None:
68+
# Convert metrics to list if single metric provided
69+
metrics_list = metrics if isinstance(metrics, list) else [metrics]
70+
# Convert enum to string names using the same logic as main function
71+
metrics_to_plot = []
72+
for metric in metrics_list:
73+
metric_name = metric.name.lower() if isinstance(metric, SHARPNESS_METRICS) else metric
74+
if metric_name not in calculated_metrics:
75+
raise ValueError(f"Metric '{metric_name}' not found. Available: {calculated_metrics}")
76+
metrics_to_plot.append(metric_name)
77+
else:
78+
metrics_to_plot = calculated_metrics
79+
80+
logg.info(f"Plotting {len(metrics_to_plot)} sharpness metrics: {metrics_to_plot}")
81+
82+
# Create subplots: 3 columns, one row per metric
83+
n_metrics = len(metrics_to_plot)
84+
ncols = 3 # spatial, histogram, stats
85+
nrows = n_metrics
86+
87+
if figsize is None:
88+
figsize = (12, 4 * nrows) # 12 width for 3 columns, 4 height per row
89+
90+
fig, axes = plt.subplots(nrows, ncols, figsize=figsize)
91+
92+
# Ensure axes is always 2D array for consistent indexing
93+
if nrows == 1:
94+
axes = axes.reshape(1, -1)
95+
if ncols == 1:
96+
axes = axes.reshape(-1, 1)
97+
98+
# Plot each metric
99+
for i, metric_name in enumerate(metrics_to_plot):
100+
# Find the metric in adata.var_names and get raw values
101+
var_name = f"sharpness_{metric_name}"
102+
if var_name not in adata.var_names:
103+
logg.warning(f"Variable '{var_name}' not found in adata.var_names. Skipping.")
104+
continue
105+
106+
# Get metric index and raw values
107+
metric_idx = list(adata.var_names).index(var_name)
108+
raw_values = adata.X[:, metric_idx]
109+
110+
# Get axes for this metric (row i, columns 0, 1, 2)
111+
ax_spatial = axes[i, 0]
112+
ax_hist = axes[i, 1]
113+
ax_stats = axes[i, 2]
114+
115+
# Panel 1: Spatial plot
116+
try:
117+
(
118+
sdata
119+
.pl.render_shapes(shapes_key, color=var_name, **kwargs)
120+
.pl.show(ax=ax_spatial, title=f"{metric_name.replace('_', ' ').title()}")
121+
)
122+
except Exception as e:
123+
logg.warning(f"Error plotting spatial view for {metric_name}: {e}")
124+
ax_spatial.text(0.5, 0.5, f"Error plotting\n{metric_name}",
125+
ha='center', va='center', transform=ax_spatial.transAxes)
126+
ax_spatial.set_title(f"{metric_name.replace('_', ' ').title()}")
127+
128+
# Panel 2: Histogram
129+
ax_hist.hist(raw_values, bins=50, alpha=0.7, edgecolor='black')
130+
ax_hist.set_xlabel(f"{metric_name.replace('_', ' ').title()}")
131+
ax_hist.set_ylabel('Count')
132+
ax_hist.set_title('Distribution')
133+
ax_hist.grid(True, alpha=0.3)
134+
135+
# Panel 3: Statistics
136+
ax_stats.axis('off')
137+
stats_text = f"""
138+
Raw {metric_name.replace('_', ' ').title()} Statistics:
139+
140+
Count: {len(raw_values):,}
141+
Mean: {np.mean(raw_values):.4f}
142+
Std: {np.std(raw_values):.4f}
143+
Min: {np.min(raw_values):.4f}
144+
Max: {np.max(raw_values):.4f}
145+
146+
Percentiles:
147+
5%: {np.percentile(raw_values, 5):.4f}
148+
25%: {np.percentile(raw_values, 25):.4f}
149+
50%: {np.percentile(raw_values, 50):.4f}
150+
75%: {np.percentile(raw_values, 75):.4f}
151+
95%: {np.percentile(raw_values, 95):.4f}
152+
153+
Non-zero: {np.count_nonzero(raw_values):,}
154+
Zero: {np.sum(raw_values == 0):,}
155+
"""
156+
157+
ax_stats.text(
158+
0.05, 0.95, stats_text.strip(),
159+
transform=ax_stats.transAxes, fontsize=9,
160+
verticalalignment='top', fontfamily='monospace'
161+
)
162+
163+
plt.tight_layout()
164+
165+
return fig if return_fig else None

0 commit comments

Comments
 (0)