cellestial 0.43.0__tar.gz → 0.45.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cellestial-0.45.0/PKG-INFO +264 -0
- cellestial-0.45.0/README.md +237 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/__init__.py +4 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/frames/__init__.py +2 -1
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/frames/operations.py +38 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/ondata_legend.py +6 -58
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/utilities.py +3 -3
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/__init__.py +4 -2
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/core/dimensional.py +0 -5
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/core/subdimensional.py +0 -20
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/core/subdimensionals.py +0 -25
- cellestial-0.45.0/cellestial/single/differential/__init__.py +4 -0
- cellestial-0.45.0/cellestial/single/differential/markers.py +446 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/heatmap/_key_groups.py +7 -4
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/heatmap/dotplot.py +2 -1
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/heatmap/heatmap.py +16 -10
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/heatmap/stacked_violin.py +0 -2
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/quick/__init__.py +2 -0
- cellestial-0.45.0/cellestial/single/quick/elbow.py +160 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/util/utilities.py +21 -9
- {cellestial-0.43.0 → cellestial-0.45.0}/pyproject.toml +2 -1
- cellestial-0.43.0/PKG-INFO +0 -123
- cellestial-0.43.0/README.md +0 -97
- cellestial-0.43.0/cellestial/single/differential/__init__.py +0 -3
- {cellestial-0.43.0 → cellestial-0.45.0}/LICENSE +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/_version.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/datasets/__init__.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/datasets/datasets.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/frames/build.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/__init__.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/_deferred.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/arrow.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/bracket.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/outline.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/stream.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/base/__init__.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/base/base.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/basic/__init__.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/basic/bar.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/basic/scatter.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/common/__init__.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/common/xyplot.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/common/xyplots.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/core/__init__.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/core/distribution.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/core/distributions.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/differential/volcano.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/heatmap/__init__.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/heatmap/_rank_genes_groups.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/quick/highest.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/quick/ridge.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/spatial/__init__.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/spatial/_components.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/spatial/spatial.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/spatial/spatials.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/themes/__init__.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/themes/_distributions.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/themes/_heatmaps.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/themes/_scatters.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/util/__init__.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/util/colors.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/util/dendrogram.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/util/errors.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/util/operations.py +0 -0
- {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/util/save.py +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cellestial
|
|
3
|
+
Version: 0.45.0
|
|
4
|
+
Summary:
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Zaf4
|
|
8
|
+
Author-email: zafermolbio@gmail.com
|
|
9
|
+
Requires-Python: >=3.11,<4.0
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Provides-Extra: extras
|
|
16
|
+
Requires-Dist: anndata (>=0.10.8,<1.0)
|
|
17
|
+
Requires-Dist: lets-plot (>=4.9.0,<5.0.0)
|
|
18
|
+
Requires-Dist: matplotlib (>=3.9,<4.0) ; extra == "extras"
|
|
19
|
+
Requires-Dist: polars[pyarrow] (>=1.30.0,<2.0.0)
|
|
20
|
+
Requires-Dist: scanpy[leiden] (>=1.10.4,<2.0.0) ; extra == "extras"
|
|
21
|
+
Requires-Dist: scikit-image (>=0.23.1,<1.0)
|
|
22
|
+
Requires-Dist: scvelo (>=0.3.3,<0.4.0) ; extra == "extras"
|
|
23
|
+
Requires-Dist: spatialdata (>=0.7,<1.0)
|
|
24
|
+
Requires-Dist: tqdm (>=4)
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Cellestial
|
|
28
|
+
|
|
29
|
+
[](https://github.com/DataVil/Cellestial)
|
|
30
|
+
[](https://pypi.org/project/cellestial/)
|
|
31
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
32
|
+
[](https://www.pola.rs/)
|
|
33
|
+
[](https://lets-plot.org/)
|
|
34
|
+
|
|
35
|
+
<img align="right" src="assets/cellestial.svg" alt="Cellestial Logo" width="300">
|
|
36
|
+
|
|
37
|
+
The __Grammar of Graphics__ for single-cell omics.
|
|
38
|
+
|
|
39
|
+
## Introduction
|
|
40
|
+
|
|
41
|
+
__Cellestial__ is an interactive and a highly customizable _Single-Cell_ & _Spatial_ omics data visualization library. Built on top [Lets-Plot](https://lets-plot.org/), it offers a ggplot-like layered and modular approach offering __high customizability__ and __publication-ready figures__.
|
|
42
|
+
|
|
43
|
+
Cellestial is highly integrated with scverse's [AnnData](https://github.com/scverse/anndata) with room for integration with any upcoming single-cell omics data type in the Python single-cell omics ecosystem.
|
|
44
|
+
|
|
45
|
+
Cellestial leverages the performance of [Polars](https://pola.rs/) ensuring speed and scalability.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install cellestial
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv add cellestial
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
poetry add cellestial
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quickstart
|
|
63
|
+
|
|
64
|
+
Cellestial accepts a standard `AnnData` directly. The same function plots a categorical column or a gene, and everything composes through `+` like ggplot.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from lets_plot import *
|
|
68
|
+
import cellestial as cl
|
|
69
|
+
|
|
70
|
+
data = cl.datasets.pbmc3k(cache_directory="data")
|
|
71
|
+
|
|
72
|
+
cl.umap(
|
|
73
|
+
data,
|
|
74
|
+
key="cell_type_lvl1",
|
|
75
|
+
axis_type="arrow",
|
|
76
|
+
legend_ondata=True,
|
|
77
|
+
) + scale_color_hue()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
<p align="center">
|
|
81
|
+
<img src="assets/first-umap.svg" alt="UMAP coloured by cell type" width="600">
|
|
82
|
+
</p>
|
|
83
|
+
|
|
84
|
+
## Examples
|
|
85
|
+
|
|
86
|
+
### Spatial omics
|
|
87
|
+
|
|
88
|
+
Overlay categorical labels or gene expression on tissue coordinates.
|
|
89
|
+
|
|
90
|
+
<details>
|
|
91
|
+
<summary><b>Show code</b></summary>
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import squidpy as sq
|
|
95
|
+
|
|
96
|
+
lymph_node = cl.datasets.human_lymph_node(cache_directory="data")
|
|
97
|
+
hne = sq.datasets.visium_hne_adata()
|
|
98
|
+
|
|
99
|
+
gggrid(
|
|
100
|
+
[
|
|
101
|
+
cl.spatial(lymph_node, key="clusters"),
|
|
102
|
+
cl.spatial(hne, key="leiden"),
|
|
103
|
+
],
|
|
104
|
+
ncol=2,
|
|
105
|
+
) + ggsize(1000, 400)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
</details>
|
|
109
|
+
|
|
110
|
+
<p align="center">
|
|
111
|
+
<img src="assets/spatial-categorical.svg" alt="Spatial categorical overlay" width="800">
|
|
112
|
+
</p>
|
|
113
|
+
|
|
114
|
+
### Dimensionality reduction with layers
|
|
115
|
+
|
|
116
|
+
Cellestial ships single-cell-specific layers (`cluster_outlines`, `stream`, `arrow_axis`, `ondata_legend`) that compose with `+` like any geom.
|
|
117
|
+
|
|
118
|
+
<details>
|
|
119
|
+
<summary><b>Show code</b></summary>
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
velocity_data = cl.datasets.pancreas(cache_directory="data")
|
|
123
|
+
|
|
124
|
+
outlined = cl.umap(
|
|
125
|
+
data,
|
|
126
|
+
key="cell_type_lvl1",
|
|
127
|
+
axis_type="arrow",
|
|
128
|
+
size=1.5,
|
|
129
|
+
legend_ondata=True,
|
|
130
|
+
) + scale_color_hue() + cl.cluster_outlines(groups=["Lymphocytes", "B Cells"])
|
|
131
|
+
|
|
132
|
+
streamed = cl.umap(
|
|
133
|
+
velocity_data,
|
|
134
|
+
key="clusters_coarse",
|
|
135
|
+
axis_type="arrow",
|
|
136
|
+
size=4,
|
|
137
|
+
alpha=0.4,
|
|
138
|
+
legend_ondata=True,
|
|
139
|
+
ondata_color="black",
|
|
140
|
+
) + cl.stream()
|
|
141
|
+
|
|
142
|
+
gggrid([outlined, streamed])
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
</details>
|
|
146
|
+
|
|
147
|
+
<p align="center">
|
|
148
|
+
<img src="assets/dimensionality-layers.svg" alt="UMAP with cluster outlines and velocity stream" width="800">
|
|
149
|
+
</p>
|
|
150
|
+
|
|
151
|
+
### Marker genes
|
|
152
|
+
|
|
153
|
+
Heatmaps, dotplots, matrixplots and stacked violins share the same call shape and ship with built-in dendrograms and group bars.
|
|
154
|
+
|
|
155
|
+
<details>
|
|
156
|
+
<summary><b>Show code</b></summary>
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
markers = [
|
|
160
|
+
"PSAP", "LYZ", "CST3", # Monocytes
|
|
161
|
+
"CD79A", "CD79B", # B cells
|
|
162
|
+
"IL7R", "CD3D", "CD3E", "CD4", # T cells (CD4+)
|
|
163
|
+
"CD8A", "CD8B", # T cells (CD8+)
|
|
164
|
+
"NKG7", "GNLY", "KLRD1", # NK cells
|
|
165
|
+
"HLA-DRA", "FCER1A", # Dendritic cells
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
cl.heatmap(
|
|
169
|
+
data,
|
|
170
|
+
group_by="cell_type_lvl1",
|
|
171
|
+
keys=markers,
|
|
172
|
+
geom="raster",
|
|
173
|
+
group_lines_size=0.5,
|
|
174
|
+
group_lines_color="white",
|
|
175
|
+
group_bars=True,
|
|
176
|
+
group_bars_labels=True,
|
|
177
|
+
dendrogram=True,
|
|
178
|
+
dendrogram_size=1,
|
|
179
|
+
) + scale_fill_viridis()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
</details>
|
|
183
|
+
|
|
184
|
+
<p align="center">
|
|
185
|
+
<img src="assets/heatmap.svg" alt="Marker gene heatmap" width="700">
|
|
186
|
+
</p>
|
|
187
|
+
|
|
188
|
+
### Statistical comparisons
|
|
189
|
+
|
|
190
|
+
`cl.bracket` runs pairwise tests on a `cl.boxplot` or `cl.violin` and draws annotated significance brackets.
|
|
191
|
+
|
|
192
|
+
<details>
|
|
193
|
+
<summary><b>Show code</b></summary>
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
cl.boxplot(
|
|
197
|
+
data,
|
|
198
|
+
key="CD3D",
|
|
199
|
+
fill="cell_type_lvl1",
|
|
200
|
+
threshold=0.1,
|
|
201
|
+
) + scale_fill_hue() + cl.bracket(
|
|
202
|
+
y_padding=0.2,
|
|
203
|
+
label="pvalue",
|
|
204
|
+
prefix="p",
|
|
205
|
+
prefix_style="<",
|
|
206
|
+
comparisons=[
|
|
207
|
+
("Lymphocytes", "Monocytes"),
|
|
208
|
+
("Monocytes", "Erythroid"),
|
|
209
|
+
("Monocytes", "B Cells"),
|
|
210
|
+
],
|
|
211
|
+
)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
</details>
|
|
215
|
+
|
|
216
|
+
<p align="center">
|
|
217
|
+
<img src="assets/boxplot-bracket.svg" alt="Boxplot with significance brackets" width="600">
|
|
218
|
+
</p>
|
|
219
|
+
|
|
220
|
+
### Ridge plots
|
|
221
|
+
|
|
222
|
+
Quick exploratory views of expression distributions across groups.
|
|
223
|
+
|
|
224
|
+
<details>
|
|
225
|
+
<summary><b>Show code</b></summary>
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
cl.ridge(
|
|
229
|
+
data,
|
|
230
|
+
key="B2M",
|
|
231
|
+
alpha=0.6,
|
|
232
|
+
group_by="cell_type_lvl1",
|
|
233
|
+
) + scale_fill_hue()
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
</details>
|
|
237
|
+
|
|
238
|
+
<p align="center">
|
|
239
|
+
<img src="assets/ridge.svg" alt="Ridge plot of B2M expression" width="600">
|
|
240
|
+
</p>
|
|
241
|
+
|
|
242
|
+
## Migrating from Scanpy
|
|
243
|
+
|
|
244
|
+
Cellestial mirrors most of `scanpy.pl` with a few naming shifts:
|
|
245
|
+
|
|
246
|
+
- `color=` becomes `key=`.
|
|
247
|
+
- Multi-panel calls use a plural function (`cl.umaps`, `cl.violins`, ...).
|
|
248
|
+
- `groupby` becomes `group_by`, `var_names` becomes `keys`.
|
|
249
|
+
- Saving is a separate `cl.save(plot, "umap.png")` call.
|
|
250
|
+
|
|
251
|
+
See the [migration guide](sphinx/migrating_from_scanpy.rst) for a side-by-side mapping.
|
|
252
|
+
|
|
253
|
+
## Documentation
|
|
254
|
+
|
|
255
|
+
- [Quickstart](sphinx/quickstart.rst) — guided tour of the API.
|
|
256
|
+
- [Features](sphinx/features.rst) — tooltips, layers, grids, colors, faceting.
|
|
257
|
+
- [API reference](sphinx/API.rst) — every function.
|
|
258
|
+
- [Migrating from Scanpy](sphinx/migrating_from_scanpy.rst).
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
Apache 2.0. See [LICENSE](LICENSE).
|
|
263
|
+
|
|
264
|
+
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Cellestial
|
|
2
|
+
|
|
3
|
+
[](https://github.com/DataVil/Cellestial)
|
|
4
|
+
[](https://pypi.org/project/cellestial/)
|
|
5
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
6
|
+
[](https://www.pola.rs/)
|
|
7
|
+
[](https://lets-plot.org/)
|
|
8
|
+
|
|
9
|
+
<img align="right" src="assets/cellestial.svg" alt="Cellestial Logo" width="300">
|
|
10
|
+
|
|
11
|
+
The __Grammar of Graphics__ for single-cell omics.
|
|
12
|
+
|
|
13
|
+
## Introduction
|
|
14
|
+
|
|
15
|
+
__Cellestial__ is an interactive and a highly customizable _Single-Cell_ & _Spatial_ omics data visualization library. Built on top [Lets-Plot](https://lets-plot.org/), it offers a ggplot-like layered and modular approach offering __high customizability__ and __publication-ready figures__.
|
|
16
|
+
|
|
17
|
+
Cellestial is highly integrated with scverse's [AnnData](https://github.com/scverse/anndata) with room for integration with any upcoming single-cell omics data type in the Python single-cell omics ecosystem.
|
|
18
|
+
|
|
19
|
+
Cellestial leverages the performance of [Polars](https://pola.rs/) ensuring speed and scalability.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install cellestial
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv add cellestial
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
poetry add cellestial
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quickstart
|
|
37
|
+
|
|
38
|
+
Cellestial accepts a standard `AnnData` directly. The same function plots a categorical column or a gene, and everything composes through `+` like ggplot.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from lets_plot import *
|
|
42
|
+
import cellestial as cl
|
|
43
|
+
|
|
44
|
+
data = cl.datasets.pbmc3k(cache_directory="data")
|
|
45
|
+
|
|
46
|
+
cl.umap(
|
|
47
|
+
data,
|
|
48
|
+
key="cell_type_lvl1",
|
|
49
|
+
axis_type="arrow",
|
|
50
|
+
legend_ondata=True,
|
|
51
|
+
) + scale_color_hue()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
<p align="center">
|
|
55
|
+
<img src="assets/first-umap.svg" alt="UMAP coloured by cell type" width="600">
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
## Examples
|
|
59
|
+
|
|
60
|
+
### Spatial omics
|
|
61
|
+
|
|
62
|
+
Overlay categorical labels or gene expression on tissue coordinates.
|
|
63
|
+
|
|
64
|
+
<details>
|
|
65
|
+
<summary><b>Show code</b></summary>
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import squidpy as sq
|
|
69
|
+
|
|
70
|
+
lymph_node = cl.datasets.human_lymph_node(cache_directory="data")
|
|
71
|
+
hne = sq.datasets.visium_hne_adata()
|
|
72
|
+
|
|
73
|
+
gggrid(
|
|
74
|
+
[
|
|
75
|
+
cl.spatial(lymph_node, key="clusters"),
|
|
76
|
+
cl.spatial(hne, key="leiden"),
|
|
77
|
+
],
|
|
78
|
+
ncol=2,
|
|
79
|
+
) + ggsize(1000, 400)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
</details>
|
|
83
|
+
|
|
84
|
+
<p align="center">
|
|
85
|
+
<img src="assets/spatial-categorical.svg" alt="Spatial categorical overlay" width="800">
|
|
86
|
+
</p>
|
|
87
|
+
|
|
88
|
+
### Dimensionality reduction with layers
|
|
89
|
+
|
|
90
|
+
Cellestial ships single-cell-specific layers (`cluster_outlines`, `stream`, `arrow_axis`, `ondata_legend`) that compose with `+` like any geom.
|
|
91
|
+
|
|
92
|
+
<details>
|
|
93
|
+
<summary><b>Show code</b></summary>
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
velocity_data = cl.datasets.pancreas(cache_directory="data")
|
|
97
|
+
|
|
98
|
+
outlined = cl.umap(
|
|
99
|
+
data,
|
|
100
|
+
key="cell_type_lvl1",
|
|
101
|
+
axis_type="arrow",
|
|
102
|
+
size=1.5,
|
|
103
|
+
legend_ondata=True,
|
|
104
|
+
) + scale_color_hue() + cl.cluster_outlines(groups=["Lymphocytes", "B Cells"])
|
|
105
|
+
|
|
106
|
+
streamed = cl.umap(
|
|
107
|
+
velocity_data,
|
|
108
|
+
key="clusters_coarse",
|
|
109
|
+
axis_type="arrow",
|
|
110
|
+
size=4,
|
|
111
|
+
alpha=0.4,
|
|
112
|
+
legend_ondata=True,
|
|
113
|
+
ondata_color="black",
|
|
114
|
+
) + cl.stream()
|
|
115
|
+
|
|
116
|
+
gggrid([outlined, streamed])
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
</details>
|
|
120
|
+
|
|
121
|
+
<p align="center">
|
|
122
|
+
<img src="assets/dimensionality-layers.svg" alt="UMAP with cluster outlines and velocity stream" width="800">
|
|
123
|
+
</p>
|
|
124
|
+
|
|
125
|
+
### Marker genes
|
|
126
|
+
|
|
127
|
+
Heatmaps, dotplots, matrixplots and stacked violins share the same call shape and ship with built-in dendrograms and group bars.
|
|
128
|
+
|
|
129
|
+
<details>
|
|
130
|
+
<summary><b>Show code</b></summary>
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
markers = [
|
|
134
|
+
"PSAP", "LYZ", "CST3", # Monocytes
|
|
135
|
+
"CD79A", "CD79B", # B cells
|
|
136
|
+
"IL7R", "CD3D", "CD3E", "CD4", # T cells (CD4+)
|
|
137
|
+
"CD8A", "CD8B", # T cells (CD8+)
|
|
138
|
+
"NKG7", "GNLY", "KLRD1", # NK cells
|
|
139
|
+
"HLA-DRA", "FCER1A", # Dendritic cells
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
cl.heatmap(
|
|
143
|
+
data,
|
|
144
|
+
group_by="cell_type_lvl1",
|
|
145
|
+
keys=markers,
|
|
146
|
+
geom="raster",
|
|
147
|
+
group_lines_size=0.5,
|
|
148
|
+
group_lines_color="white",
|
|
149
|
+
group_bars=True,
|
|
150
|
+
group_bars_labels=True,
|
|
151
|
+
dendrogram=True,
|
|
152
|
+
dendrogram_size=1,
|
|
153
|
+
) + scale_fill_viridis()
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
</details>
|
|
157
|
+
|
|
158
|
+
<p align="center">
|
|
159
|
+
<img src="assets/heatmap.svg" alt="Marker gene heatmap" width="700">
|
|
160
|
+
</p>
|
|
161
|
+
|
|
162
|
+
### Statistical comparisons
|
|
163
|
+
|
|
164
|
+
`cl.bracket` runs pairwise tests on a `cl.boxplot` or `cl.violin` and draws annotated significance brackets.
|
|
165
|
+
|
|
166
|
+
<details>
|
|
167
|
+
<summary><b>Show code</b></summary>
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
cl.boxplot(
|
|
171
|
+
data,
|
|
172
|
+
key="CD3D",
|
|
173
|
+
fill="cell_type_lvl1",
|
|
174
|
+
threshold=0.1,
|
|
175
|
+
) + scale_fill_hue() + cl.bracket(
|
|
176
|
+
y_padding=0.2,
|
|
177
|
+
label="pvalue",
|
|
178
|
+
prefix="p",
|
|
179
|
+
prefix_style="<",
|
|
180
|
+
comparisons=[
|
|
181
|
+
("Lymphocytes", "Monocytes"),
|
|
182
|
+
("Monocytes", "Erythroid"),
|
|
183
|
+
("Monocytes", "B Cells"),
|
|
184
|
+
],
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
</details>
|
|
189
|
+
|
|
190
|
+
<p align="center">
|
|
191
|
+
<img src="assets/boxplot-bracket.svg" alt="Boxplot with significance brackets" width="600">
|
|
192
|
+
</p>
|
|
193
|
+
|
|
194
|
+
### Ridge plots
|
|
195
|
+
|
|
196
|
+
Quick exploratory views of expression distributions across groups.
|
|
197
|
+
|
|
198
|
+
<details>
|
|
199
|
+
<summary><b>Show code</b></summary>
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
cl.ridge(
|
|
203
|
+
data,
|
|
204
|
+
key="B2M",
|
|
205
|
+
alpha=0.6,
|
|
206
|
+
group_by="cell_type_lvl1",
|
|
207
|
+
) + scale_fill_hue()
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
</details>
|
|
211
|
+
|
|
212
|
+
<p align="center">
|
|
213
|
+
<img src="assets/ridge.svg" alt="Ridge plot of B2M expression" width="600">
|
|
214
|
+
</p>
|
|
215
|
+
|
|
216
|
+
## Migrating from Scanpy
|
|
217
|
+
|
|
218
|
+
Cellestial mirrors most of `scanpy.pl` with a few naming shifts:
|
|
219
|
+
|
|
220
|
+
- `color=` becomes `key=`.
|
|
221
|
+
- Multi-panel calls use a plural function (`cl.umaps`, `cl.violins`, ...).
|
|
222
|
+
- `groupby` becomes `group_by`, `var_names` becomes `keys`.
|
|
223
|
+
- Saving is a separate `cl.save(plot, "umap.png")` call.
|
|
224
|
+
|
|
225
|
+
See the [migration guide](sphinx/migrating_from_scanpy.rst) for a side-by-side mapping.
|
|
226
|
+
|
|
227
|
+
## Documentation
|
|
228
|
+
|
|
229
|
+
- [Quickstart](sphinx/quickstart.rst) — guided tour of the API.
|
|
230
|
+
- [Features](sphinx/features.rst) — tooltips, layers, grids, colors, faceting.
|
|
231
|
+
- [API reference](sphinx/API.rst) — every function.
|
|
232
|
+
- [Migrating from Scanpy](sphinx/migrating_from_scanpy.rst).
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
Apache 2.0. See [LICENSE](LICENSE).
|
|
237
|
+
|
|
@@ -11,10 +11,12 @@ from cellestial.single import (
|
|
|
11
11
|
dimensional,
|
|
12
12
|
dimensionals,
|
|
13
13
|
dotplot,
|
|
14
|
+
elbow,
|
|
14
15
|
expression,
|
|
15
16
|
expressions,
|
|
16
17
|
heatmap,
|
|
17
18
|
highest_expressed_genes,
|
|
19
|
+
markers,
|
|
18
20
|
matrixplot,
|
|
19
21
|
pca,
|
|
20
22
|
pcas,
|
|
@@ -75,11 +77,13 @@ __all__ = [
|
|
|
75
77
|
"dimensional",
|
|
76
78
|
"dimensionals",
|
|
77
79
|
"dotplot",
|
|
80
|
+
"elbow",
|
|
78
81
|
"expression",
|
|
79
82
|
"expressions",
|
|
80
83
|
"get_mapping",
|
|
81
84
|
"heatmap",
|
|
82
85
|
"highest_expressed_genes",
|
|
86
|
+
"markers",
|
|
83
87
|
"matrixplot",
|
|
84
88
|
"ondata_legend",
|
|
85
89
|
"pca",
|
|
@@ -4,10 +4,11 @@ from cellestial.frames.build import (
|
|
|
4
4
|
anndata_variables_frame,
|
|
5
5
|
build_frame,
|
|
6
6
|
)
|
|
7
|
-
from cellestial.frames.operations import _highest_expressed_genes_frame
|
|
7
|
+
from cellestial.frames.operations import _highest_expressed_genes_frame, _pca_variance_frame
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"_highest_expressed_genes_frame",
|
|
11
|
+
"_pca_variance_frame",
|
|
11
12
|
"anndata_observations_frame",
|
|
12
13
|
"anndata_variable_columns",
|
|
13
14
|
"anndata_variables_frame",
|
|
@@ -43,3 +43,41 @@ def _highest_expressed_genes_frame(
|
|
|
43
43
|
raise UnsupportedDataTypeError(msg)
|
|
44
44
|
|
|
45
45
|
return frame
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _pca_variance_frame(
|
|
49
|
+
data: AnnData,
|
|
50
|
+
n_pcs: int | None = None,
|
|
51
|
+
component_column: str = "Principal Component",
|
|
52
|
+
variance_column: str = "Variance Ratio",
|
|
53
|
+
) -> DataFrame:
|
|
54
|
+
"""Get PCA variance ratio per principal component as a polars DataFrame."""
|
|
55
|
+
if isinstance(data, AnnData):
|
|
56
|
+
pca_uns = data.uns.get("pca")
|
|
57
|
+
if pca_uns is None or "variance_ratio" not in pca_uns:
|
|
58
|
+
msg = (
|
|
59
|
+
"PCA variance ratio is not available on this data. "
|
|
60
|
+
"Run PCA first (e.g., `sc.tl.pca(data)`)."
|
|
61
|
+
)
|
|
62
|
+
raise ValueError(msg)
|
|
63
|
+
variance_ratio = np.asarray(pca_uns["variance_ratio"]).ravel()
|
|
64
|
+
n_available = variance_ratio.size
|
|
65
|
+
if n_pcs is not None:
|
|
66
|
+
if n_pcs > n_available:
|
|
67
|
+
msg = (
|
|
68
|
+
f"Requested n_pcs={n_pcs} components, "
|
|
69
|
+
f"but only {n_available} components available."
|
|
70
|
+
)
|
|
71
|
+
raise ValueError(msg)
|
|
72
|
+
variance_ratio = variance_ratio[:n_pcs]
|
|
73
|
+
frame = pl.DataFrame(
|
|
74
|
+
{
|
|
75
|
+
component_column: np.arange(1, variance_ratio.size + 1, dtype=np.int64),
|
|
76
|
+
variance_column: variance_ratio,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
msg = f"Unsupported data type: `{type(data)}`"
|
|
81
|
+
raise UnsupportedDataTypeError(msg)
|
|
82
|
+
|
|
83
|
+
return frame
|
|
@@ -2,10 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
import numpy as np
|
|
6
5
|
import polars as pl
|
|
7
6
|
from lets_plot import aes, geom_label, geom_label_repel, geom_text, geom_text_repel, theme
|
|
8
|
-
from scipy.stats import gaussian_kde
|
|
9
7
|
|
|
10
8
|
from cellestial.layers._deferred import DeferredLayer
|
|
11
9
|
from cellestial.util import get_mapping, retrieve
|
|
@@ -16,63 +14,19 @@ if TYPE_CHECKING:
|
|
|
16
14
|
from polars import DataFrame
|
|
17
15
|
|
|
18
16
|
|
|
19
|
-
def _label_center(
|
|
20
|
-
points: np.ndarray,
|
|
21
|
-
*,
|
|
22
|
-
max_sample: int = 500,
|
|
23
|
-
top_fraction: float = 0.2,
|
|
24
|
-
density_factor: float = 2.0,
|
|
25
|
-
) -> np.ndarray:
|
|
26
|
-
"""
|
|
27
|
-
Return the cluster's dense-core centroid if it dominates the centroid, else the centroid.
|
|
28
|
-
|
|
29
|
-
For unimodal clusters the centroid is already in the dense region, so it wins. For
|
|
30
|
-
multimodal or strongly skewed clusters the dense core is much denser than the centroid
|
|
31
|
-
(which sits in a gap), and the label is placed on that core instead.
|
|
32
|
-
"""
|
|
33
|
-
centroid = points.mean(axis=0)
|
|
34
|
-
n_points = len(points)
|
|
35
|
-
if n_points < 3:
|
|
36
|
-
return centroid
|
|
37
|
-
sample = points
|
|
38
|
-
if n_points > max_sample:
|
|
39
|
-
rng = np.random.default_rng(0)
|
|
40
|
-
sample = points[rng.choice(n_points, max_sample, replace=False)]
|
|
41
|
-
try:
|
|
42
|
-
kde = gaussian_kde(sample.T)
|
|
43
|
-
density = kde(sample.T)
|
|
44
|
-
except (np.linalg.LinAlgError, ValueError):
|
|
45
|
-
return centroid
|
|
46
|
-
top_k = max(3, int(len(sample) * top_fraction))
|
|
47
|
-
top_indices = np.argpartition(density, -top_k)[-top_k:]
|
|
48
|
-
core = sample[top_indices].mean(axis=0)
|
|
49
|
-
if kde(core[:, None])[0] > density_factor * kde(centroid[:, None])[0]:
|
|
50
|
-
return core
|
|
51
|
-
return centroid
|
|
52
|
-
|
|
53
|
-
|
|
54
17
|
def _compute_label_positions(
|
|
55
18
|
frame: DataFrame,
|
|
56
19
|
*,
|
|
57
20
|
x: str,
|
|
58
21
|
y: str,
|
|
59
22
|
group_by: str,
|
|
60
|
-
dense: bool,
|
|
61
23
|
) -> DataFrame:
|
|
62
|
-
"""Aggregate per-group
|
|
24
|
+
"""Aggregate per-group median coordinates for `geom_text` placement."""
|
|
63
25
|
frame = frame.filter(pl.col(group_by).is_not_null())
|
|
64
|
-
if dense:
|
|
65
|
-
rows = []
|
|
66
|
-
for (group_value,), group_frame in frame.group_by(group_by):
|
|
67
|
-
points = group_frame.select(x, y).to_numpy()
|
|
68
|
-
center = _label_center(points)
|
|
69
|
-
rows.append({group_by: group_value, x: float(center[0]), y: float(center[1])})
|
|
70
|
-
return pl.DataFrame(
|
|
71
|
-
rows,
|
|
72
|
-
schema={group_by: frame.schema[group_by], x: pl.Float64, y: pl.Float64},
|
|
73
|
-
)
|
|
74
26
|
return frame.group_by(group_by).agg(
|
|
75
|
-
pl.col(x).
|
|
27
|
+
pl.col(x).median(),
|
|
28
|
+
pl.col(y).median(),
|
|
29
|
+
pl.selectors.categorical().mode().first(),
|
|
76
30
|
)
|
|
77
31
|
|
|
78
32
|
|
|
@@ -88,12 +42,11 @@ def ondata_legend(
|
|
|
88
42
|
family: str = "sans",
|
|
89
43
|
alpha: float = 1,
|
|
90
44
|
label: bool = False,
|
|
91
|
-
dense: bool = True,
|
|
92
45
|
repel: bool = False,
|
|
93
46
|
**geom_kwargs,
|
|
94
47
|
) -> DeferredLayer:
|
|
95
48
|
"""
|
|
96
|
-
Layer of `geom_text` that places per-group labels at
|
|
49
|
+
Layer of `geom_text` that places per-group labels at median coordinates.
|
|
97
50
|
|
|
98
51
|
Parameters
|
|
99
52
|
----------
|
|
@@ -124,9 +77,6 @@ def ondata_legend(
|
|
|
124
77
|
Alpha (transparency) of the legend text.
|
|
125
78
|
label : bool, default=False
|
|
126
79
|
If True, draw labels with a filled background using `geom_label`.
|
|
127
|
-
dense : bool, default=True
|
|
128
|
-
Whether to place the label at the cluster's density peak (KDE-based).
|
|
129
|
-
If False, the arithmetic mean of the group's coordinates is used.
|
|
130
80
|
repel : bool, default=False
|
|
131
81
|
If True, use `geom_text_repel` so labels are shifted to avoid overlapping
|
|
132
82
|
each other. Repel-specific options (e.g. `box_padding`, `point_padding`,
|
|
@@ -204,9 +154,7 @@ def ondata_legend(
|
|
|
204
154
|
raise MissingAestheticError(msg)
|
|
205
155
|
|
|
206
156
|
frame = retrieve(source)
|
|
207
|
-
grouped = _compute_label_positions(
|
|
208
|
-
frame, x=x, y=y, group_by=group_by, dense=dense
|
|
209
|
-
)
|
|
157
|
+
grouped = _compute_label_positions(frame, x=x, y=y, group_by=group_by)
|
|
210
158
|
layer_kwargs = dict(geom_kwargs)
|
|
211
159
|
if label:
|
|
212
160
|
geom = geom_label_repel if repel else geom_label
|