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.
Files changed (65) hide show
  1. cellestial-0.45.0/PKG-INFO +264 -0
  2. cellestial-0.45.0/README.md +237 -0
  3. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/__init__.py +4 -0
  4. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/frames/__init__.py +2 -1
  5. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/frames/operations.py +38 -0
  6. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/ondata_legend.py +6 -58
  7. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/utilities.py +3 -3
  8. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/__init__.py +4 -2
  9. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/core/dimensional.py +0 -5
  10. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/core/subdimensional.py +0 -20
  11. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/core/subdimensionals.py +0 -25
  12. cellestial-0.45.0/cellestial/single/differential/__init__.py +4 -0
  13. cellestial-0.45.0/cellestial/single/differential/markers.py +446 -0
  14. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/heatmap/_key_groups.py +7 -4
  15. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/heatmap/dotplot.py +2 -1
  16. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/heatmap/heatmap.py +16 -10
  17. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/heatmap/stacked_violin.py +0 -2
  18. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/quick/__init__.py +2 -0
  19. cellestial-0.45.0/cellestial/single/quick/elbow.py +160 -0
  20. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/util/utilities.py +21 -9
  21. {cellestial-0.43.0 → cellestial-0.45.0}/pyproject.toml +2 -1
  22. cellestial-0.43.0/PKG-INFO +0 -123
  23. cellestial-0.43.0/README.md +0 -97
  24. cellestial-0.43.0/cellestial/single/differential/__init__.py +0 -3
  25. {cellestial-0.43.0 → cellestial-0.45.0}/LICENSE +0 -0
  26. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/_version.py +0 -0
  27. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/datasets/__init__.py +0 -0
  28. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/datasets/datasets.py +0 -0
  29. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/frames/build.py +0 -0
  30. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/__init__.py +0 -0
  31. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/_deferred.py +0 -0
  32. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/arrow.py +0 -0
  33. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/bracket.py +0 -0
  34. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/outline.py +0 -0
  35. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/layers/stream.py +0 -0
  36. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/base/__init__.py +0 -0
  37. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/base/base.py +0 -0
  38. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/basic/__init__.py +0 -0
  39. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/basic/bar.py +0 -0
  40. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/basic/scatter.py +0 -0
  41. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/common/__init__.py +0 -0
  42. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/common/xyplot.py +0 -0
  43. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/common/xyplots.py +0 -0
  44. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/core/__init__.py +0 -0
  45. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/core/distribution.py +0 -0
  46. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/core/distributions.py +0 -0
  47. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/differential/volcano.py +0 -0
  48. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/heatmap/__init__.py +0 -0
  49. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/heatmap/_rank_genes_groups.py +0 -0
  50. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/quick/highest.py +0 -0
  51. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/single/quick/ridge.py +0 -0
  52. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/spatial/__init__.py +0 -0
  53. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/spatial/_components.py +0 -0
  54. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/spatial/spatial.py +0 -0
  55. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/spatial/spatials.py +0 -0
  56. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/themes/__init__.py +0 -0
  57. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/themes/_distributions.py +0 -0
  58. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/themes/_heatmaps.py +0 -0
  59. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/themes/_scatters.py +0 -0
  60. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/util/__init__.py +0 -0
  61. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/util/colors.py +0 -0
  62. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/util/dendrogram.py +0 -0
  63. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/util/errors.py +0 -0
  64. {cellestial-0.43.0 → cellestial-0.45.0}/cellestial/util/operations.py +0 -0
  65. {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
+ [![GitHub](https://img.shields.io/badge/GitHub-100000?style=flat&logo=github&logoColor=white)](https://github.com/DataVil/Cellestial)
30
+ [![PyPI](https://img.shields.io/pypi/v/cellestial?color=377eb8)](https://pypi.org/project/cellestial/)
31
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-ff0000)](https://opensource.org/licenses/Apache-2.0)
32
+ [![Powered by Polars](https://img.shields.io/badge/Powered%20by-Polars-377eb8?logo=polars&logoColor=white)](https://www.pola.rs/)
33
+ [![Graphics: Lets-Plot](https://img.shields.io/badge/Graphics-Lets--Plot-FF00CC?logo=jetbrains&logoColor=white)](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
+ [![GitHub](https://img.shields.io/badge/GitHub-100000?style=flat&logo=github&logoColor=white)](https://github.com/DataVil/Cellestial)
4
+ [![PyPI](https://img.shields.io/pypi/v/cellestial?color=377eb8)](https://pypi.org/project/cellestial/)
5
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-ff0000)](https://opensource.org/licenses/Apache-2.0)
6
+ [![Powered by Polars](https://img.shields.io/badge/Powered%20by-Polars-377eb8?logo=polars&logoColor=white)](https://www.pola.rs/)
7
+ [![Graphics: Lets-Plot](https://img.shields.io/badge/Graphics-Lets--Plot-FF00CC?logo=jetbrains&logoColor=white)](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 label coordinates for `geom_text` placement."""
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).mean(), pl.col(y).mean(), pl.selectors.categorical().mode().first()
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 the center of each cluster.
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