cellestial 0.58.0__tar.gz → 0.58.2__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.58.0 → cellestial-0.58.2}/PKG-INFO +18 -1
- {cellestial-0.58.0 → cellestial-0.58.2}/README.md +17 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/datasets/datasets.py +2 -1
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/layers/arrow.py +14 -4
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/layers/ondata_legend.py +2 -2
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/layers/outline.py +3 -2
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/layers/stream.py +3 -1
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/common/xyplot.py +2 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/core/dimensional.py +2 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/core/utilities.py +2 -1
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/differential/utilities.py +8 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/differential/volcano.py +0 -4
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/heatmap/dotplot.py +1 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/heatmap/heatmap.py +19 -13
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/heatmap/stacked_violin.py +2 -1
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/heatmap/utilities.py +9 -3
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/quick/highest.py +3 -2
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/quick/ridge.py +3 -2
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/spatial/spatial.py +12 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/util/__init__.py +2 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/util/operations.py +19 -12
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/util/utilities.py +16 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/pyproject.toml +7 -2
- {cellestial-0.58.0 → cellestial-0.58.2}/LICENSE +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/_version.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/complex/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/complex/annotated_heatmap.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/datasets/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/frames/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/frames/build.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/frames/operations.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/layers/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/layers/_deferred.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/layers/bracket.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/layers/utilities.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/base/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/base/base.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/basic/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/basic/bar.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/basic/scatter.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/common/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/common/xyplots.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/core/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/core/distribution.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/core/distributions.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/core/subdimensional.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/core/subdimensionals.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/differential/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/differential/markers.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/heatmap/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/quick/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/single/quick/elbow.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/spatial/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/spatial/spatials.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/spatial/utilities.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/themes/__init__.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/themes/_distributions.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/themes/_heatmaps.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/themes/_scatters.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/util/colors.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/util/dendrogram.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/util/errors.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/util/markers.py +0 -0
- {cellestial-0.58.0 → cellestial-0.58.2}/cellestial/util/save.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cellestial
|
|
3
|
-
Version: 0.58.
|
|
3
|
+
Version: 0.58.2
|
|
4
4
|
Summary: Interactive single-cell and spatial omics visualization library
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -46,6 +46,7 @@ Description-Content-Type: text/markdown
|
|
|
46
46
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
47
47
|
[](https://www.pola.rs/)
|
|
48
48
|
[](https://lets-plot.org/)
|
|
49
|
+
[](https://doi.org/10.5281/zenodo.21710312)
|
|
49
50
|
|
|
50
51
|
<img align="left" src="https://raw.githubusercontent.com/datavil/cellestial/master/assets/cellestial.anim.svg" alt="Cellestial Logo" width="280">
|
|
51
52
|
|
|
@@ -270,6 +271,22 @@ See the [migration guide](https://cellestial.datavil.org/migrating_from_scanpy.h
|
|
|
270
271
|
- [API reference](https://cellestial.datavil.org/API.html) — every function.
|
|
271
272
|
- [Migrating from Scanpy](https://cellestial.datavil.org/migrating_from_scanpy.html).
|
|
272
273
|
|
|
274
|
+
## Citation
|
|
275
|
+
|
|
276
|
+
If you use Cellestial, please cite:
|
|
277
|
+
|
|
278
|
+
```bibtex
|
|
279
|
+
@misc{https://doi.org/10.5281/zenodo.21710312,
|
|
280
|
+
doi = {10.5281/ZENODO.21710312},
|
|
281
|
+
url = {https://zenodo.org/doi/10.5281/zenodo.21710312},
|
|
282
|
+
author = {Kosar, Zafer},
|
|
283
|
+
title = {Cellestial: A grammar of graphics for single-cell omics},
|
|
284
|
+
publisher = {Zenodo},
|
|
285
|
+
year = {2026},
|
|
286
|
+
copyright = {Apache License 2.0}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
273
290
|
## License
|
|
274
291
|
|
|
275
292
|
Apache 2.0. See [LICENSE](LICENSE).
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
7
|
[](https://www.pola.rs/)
|
|
8
8
|
[](https://lets-plot.org/)
|
|
9
|
+
[](https://doi.org/10.5281/zenodo.21710312)
|
|
9
10
|
|
|
10
11
|
<img align="left" src="https://raw.githubusercontent.com/datavil/cellestial/master/assets/cellestial.anim.svg" alt="Cellestial Logo" width="280">
|
|
11
12
|
|
|
@@ -230,6 +231,22 @@ See the [migration guide](https://cellestial.datavil.org/migrating_from_scanpy.h
|
|
|
230
231
|
- [API reference](https://cellestial.datavil.org/API.html) — every function.
|
|
231
232
|
- [Migrating from Scanpy](https://cellestial.datavil.org/migrating_from_scanpy.html).
|
|
232
233
|
|
|
234
|
+
## Citation
|
|
235
|
+
|
|
236
|
+
If you use Cellestial, please cite:
|
|
237
|
+
|
|
238
|
+
```bibtex
|
|
239
|
+
@misc{https://doi.org/10.5281/zenodo.21710312,
|
|
240
|
+
doi = {10.5281/ZENODO.21710312},
|
|
241
|
+
url = {https://zenodo.org/doi/10.5281/zenodo.21710312},
|
|
242
|
+
author = {Kosar, Zafer},
|
|
243
|
+
title = {Cellestial: A grammar of graphics for single-cell omics},
|
|
244
|
+
publisher = {Zenodo},
|
|
245
|
+
year = {2026},
|
|
246
|
+
copyright = {Apache License 2.0}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
233
250
|
## License
|
|
234
251
|
|
|
235
252
|
Apache 2.0. See [LICENSE](LICENSE).
|
|
@@ -13,6 +13,7 @@ _UUID_PATTERN = re.compile(
|
|
|
13
13
|
)
|
|
14
14
|
_DOWNLOAD_TIMEOUT_SECONDS = 300
|
|
15
15
|
|
|
16
|
+
|
|
16
17
|
def _resolve_cache_file(
|
|
17
18
|
cache_directory: str | Path | PathLike,
|
|
18
19
|
filename: str,
|
|
@@ -198,7 +199,7 @@ def from_url(
|
|
|
198
199
|
# download cannot poison the cache.
|
|
199
200
|
partial_file = cache_file.with_name(cache_file.name + ".part")
|
|
200
201
|
|
|
201
|
-
with urlopen(url, timeout
|
|
202
|
+
with urlopen(url, timeout=_DOWNLOAD_TIMEOUT_SECONDS) as response:
|
|
202
203
|
total_size = int(response.headers.get("Content-Length", 0))
|
|
203
204
|
progress_bar = tqdm(
|
|
204
205
|
total=total_size, unit="iB", unit_scale=True, desc=f"Downloading {filename}"
|
|
@@ -72,10 +72,20 @@ def _axis_arrow_layers(
|
|
|
72
72
|
"""
|
|
73
73
|
arrow_kwargs = arrow_kwargs or {}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
bounds = frame.select(
|
|
76
|
+
pl.col(x).min().alias("x_min"),
|
|
77
|
+
pl.col(x).max().alias("x_max"),
|
|
78
|
+
pl.col(y).min().alias("y_min"),
|
|
79
|
+
pl.col(y).max().alias("y_max"),
|
|
80
|
+
).row(0, named=True)
|
|
81
|
+
if any(value is None for value in bounds.values()):
|
|
82
|
+
msg = "Arrow axes require at least one valid coordinate pair."
|
|
83
|
+
raise ValueError(msg)
|
|
84
|
+
|
|
85
|
+
x_min = bounds["x_min"]
|
|
86
|
+
x_max = bounds["x_max"]
|
|
87
|
+
y_min = bounds["y_min"]
|
|
88
|
+
y_max = bounds["y_max"]
|
|
79
89
|
|
|
80
90
|
# find total difference between the max and min for both axis
|
|
81
91
|
x_diff = x_max - x_min
|
|
@@ -6,7 +6,7 @@ import polars as pl
|
|
|
6
6
|
from lets_plot import aes, geom_label, geom_label_repel, geom_text, geom_text_repel, theme
|
|
7
7
|
|
|
8
8
|
from cellestial.layers._deferred import DeferredLayer
|
|
9
|
-
from cellestial.util import get_mapping, retrieve
|
|
9
|
+
from cellestial.util import _drop_nonfinite_rows, get_mapping, retrieve
|
|
10
10
|
from cellestial.util.errors import MissingAestheticError
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
@@ -22,7 +22,7 @@ def _compute_label_positions(
|
|
|
22
22
|
group_by: str,
|
|
23
23
|
) -> DataFrame:
|
|
24
24
|
"""Aggregate per-group median coordinates for `geom_text` placement."""
|
|
25
|
-
frame = frame.filter(pl.col(group_by).is_not_null())
|
|
25
|
+
frame = _drop_nonfinite_rows(frame, [x, y]).filter(pl.col(group_by).is_not_null())
|
|
26
26
|
return frame.group_by(group_by).agg(
|
|
27
27
|
pl.col(x).median(),
|
|
28
28
|
pl.col(y).median(),
|
|
@@ -8,7 +8,7 @@ from lets_plot import aes, geom_path
|
|
|
8
8
|
from scipy.stats import gaussian_kde
|
|
9
9
|
|
|
10
10
|
from cellestial.layers._deferred import DeferredLayer
|
|
11
|
-
from cellestial.util import get_mapping
|
|
11
|
+
from cellestial.util import _drop_nonfinite_rows, get_mapping
|
|
12
12
|
from cellestial.util.errors import MissingAestheticError
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
@@ -50,7 +50,8 @@ def _get_density_boundaries(
|
|
|
50
50
|
boundaries = []
|
|
51
51
|
for group in groups:
|
|
52
52
|
group_label = "+".join(group) if len(group) > 1 else group[0]
|
|
53
|
-
|
|
53
|
+
group_frame = frame.filter(pl.col(group_by).is_in(group))
|
|
54
|
+
points = _drop_nonfinite_rows(group_frame, [x, y]).select([x, y]).to_numpy()
|
|
54
55
|
if len(points) < 5:
|
|
55
56
|
continue
|
|
56
57
|
|
|
@@ -12,7 +12,7 @@ from lets_plot import (
|
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
from cellestial.layers._deferred import DeferredLayer
|
|
15
|
-
from cellestial.util import get_mapping, retrieve
|
|
15
|
+
from cellestial.util import _drop_nonfinite_rows, get_mapping, retrieve
|
|
16
16
|
from cellestial.util.errors import MissingAestheticError
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
@@ -227,6 +227,8 @@ def stream(
|
|
|
227
227
|
msg = "`scvelo` must be installed to use the stream layer."
|
|
228
228
|
raise ImportError(msg) from err
|
|
229
229
|
|
|
230
|
+
frame = _drop_nonfinite_rows(frame, [x, y, x_velocity, y_velocity])
|
|
231
|
+
|
|
230
232
|
# extract coordinates and velocities as numpy arrays
|
|
231
233
|
dimensions = frame.select(pl.col(x), pl.col(y)).to_numpy()
|
|
232
234
|
velocities = frame.select(pl.col(x_velocity), pl.col(y_velocity)).to_numpy()
|
|
@@ -17,6 +17,7 @@ from cellestial.themes import _THEME_SCATTER
|
|
|
17
17
|
from cellestial.util import (
|
|
18
18
|
_collect_aes_columns,
|
|
19
19
|
_determine_axis,
|
|
20
|
+
_drop_nonfinite_rows,
|
|
20
21
|
_resolve_tooltips,
|
|
21
22
|
_select_variable_keys,
|
|
22
23
|
_validate_tooltips,
|
|
@@ -221,6 +222,7 @@ def xyplot(
|
|
|
221
222
|
include_dimensions=include_dimensions,
|
|
222
223
|
metadata_columns=metadata_columns,
|
|
223
224
|
)
|
|
225
|
+
frame = _drop_nonfinite_rows(frame, [x, y])
|
|
224
226
|
_validate_tooltips(tooltips, frame)
|
|
225
227
|
|
|
226
228
|
# BUILD: the scatterplot
|
|
@@ -24,6 +24,7 @@ from cellestial.themes import _THEME_DIMENSION
|
|
|
24
24
|
from cellestial.util import (
|
|
25
25
|
_collect_aes_columns,
|
|
26
26
|
_color_gradient,
|
|
27
|
+
_drop_nonfinite_rows,
|
|
27
28
|
_reject_sequence_key,
|
|
28
29
|
_require_feature_key,
|
|
29
30
|
_resolve_embedding_key,
|
|
@@ -282,6 +283,7 @@ def dimensional(
|
|
|
282
283
|
include_dimensions=max(xy),
|
|
283
284
|
metadata_columns=metadata_columns,
|
|
284
285
|
)
|
|
286
|
+
frame = _drop_nonfinite_rows(frame, [x, y])
|
|
285
287
|
_validate_tooltips(tooltips, frame)
|
|
286
288
|
|
|
287
289
|
# HANDLE: groups filter (categorical-only)
|
|
@@ -25,6 +25,7 @@ from cellestial.themes import _THEME_DIST
|
|
|
25
25
|
from cellestial.util import (
|
|
26
26
|
_collect_aes_columns,
|
|
27
27
|
_determine_axis,
|
|
28
|
+
_drop_nonfinite_rows,
|
|
28
29
|
_resolve_tooltips,
|
|
29
30
|
_validate_tooltips,
|
|
30
31
|
_warn,
|
|
@@ -178,7 +179,7 @@ def _distribution(
|
|
|
178
179
|
frame = frame.unpivot(
|
|
179
180
|
on=keys, index=index, value_name=value_column, variable_name=variable_column
|
|
180
181
|
)
|
|
181
|
-
frame = frame
|
|
182
|
+
frame = _drop_nonfinite_rows(frame, [value_column])
|
|
182
183
|
if threshold is not None:
|
|
183
184
|
frame = frame.filter(pl.col(value_column) >= threshold)
|
|
184
185
|
if group_by is None:
|
|
@@ -134,6 +134,13 @@ def _build_volcano_frame(
|
|
|
134
134
|
else:
|
|
135
135
|
raise _unsupported_data_type(data, AnnData)
|
|
136
136
|
|
|
137
|
+
frame = frame.filter(
|
|
138
|
+
pl.col(logfoldchange_column).is_not_null()
|
|
139
|
+
& pl.col(logfoldchange_column).is_finite()
|
|
140
|
+
& pl.col(pvalue_column).is_not_null()
|
|
141
|
+
& pl.col(pvalue_column).is_finite()
|
|
142
|
+
)
|
|
143
|
+
|
|
137
144
|
# CRITICAL PARTS: Dataframe Operations
|
|
138
145
|
# 1. -log10(pvalue) transform
|
|
139
146
|
frame = frame.with_columns(
|
|
@@ -299,6 +306,7 @@ def _build_markers_frame(
|
|
|
299
306
|
# Round scores to keep the embedded JSON compact when the frame is
|
|
300
307
|
# serialized into the plot output.
|
|
301
308
|
frame = frame.with_columns(pl.col(score_column).round(4))
|
|
309
|
+
frame = frame.filter(pl.col(score_column).is_not_null() & pl.col(score_column).is_finite())
|
|
302
310
|
|
|
303
311
|
return frame, list(selected), str(stored_group_by)
|
|
304
312
|
|
|
@@ -248,10 +248,6 @@ def volcano(
|
|
|
248
248
|
.otherwise(cap)
|
|
249
249
|
.alias(neg_log_pvalue_column)
|
|
250
250
|
)
|
|
251
|
-
# logFC infinities mean the gene is expressed in only one group; fold change
|
|
252
|
-
# is undefined, drop those rows.
|
|
253
|
-
frame = frame.filter(pl.col(logfoldchange_column).is_finite())
|
|
254
|
-
|
|
255
251
|
# SUBSAMPLE: cap non-significant points to keep the embedded data small.
|
|
256
252
|
# The non-significant cloud overplots itself heavily, so dropping most of
|
|
257
253
|
# it has no visible effect but shrinks the rendered plot's on-disk size.
|
|
@@ -315,6 +315,7 @@ def dotplot(
|
|
|
315
315
|
variable_name=variable_column,
|
|
316
316
|
value_name=value_name,
|
|
317
317
|
)
|
|
318
|
+
frame = frame.filter(pl.col(value_name).is_not_null() & pl.col(value_name).is_finite())
|
|
318
319
|
# 2. Aggregate and compute stats
|
|
319
320
|
frame = frame.group_by([group_by, variable_column]).agg(
|
|
320
321
|
[
|
|
@@ -27,6 +27,7 @@ from cellestial.single.heatmap.utilities import (
|
|
|
27
27
|
_get_group_lines_frame,
|
|
28
28
|
_key_groups_bar_y,
|
|
29
29
|
_key_groups_layers,
|
|
30
|
+
_nonaggregate_y_step,
|
|
30
31
|
_resolve_key_groups,
|
|
31
32
|
_resolve_padding,
|
|
32
33
|
_resolve_rank_genes_groups_args,
|
|
@@ -286,7 +287,7 @@ def heatmap(
|
|
|
286
287
|
group_lines_size=0.5,
|
|
287
288
|
group_lines_color="white",
|
|
288
289
|
dendrogram=True,
|
|
289
|
-
dendrogram_size=
|
|
290
|
+
dendrogram_size=0.7,
|
|
290
291
|
group_bars_labels=True,
|
|
291
292
|
group_bars=True,
|
|
292
293
|
) + scale_fill_viridis()
|
|
@@ -362,7 +363,8 @@ def heatmap(
|
|
|
362
363
|
include_dimensions=include_dimensions,
|
|
363
364
|
metadata_columns=metadata_columns,
|
|
364
365
|
)
|
|
365
|
-
|
|
366
|
+
row_identifier = variables_name if axis == 1 else observations_name
|
|
367
|
+
index_columns = [group_by] if aggregate else [row_identifier, group_by]
|
|
366
368
|
frame = frame.unpivot(
|
|
367
369
|
on=keys,
|
|
368
370
|
index=index_columns,
|
|
@@ -370,13 +372,15 @@ def heatmap(
|
|
|
370
372
|
value_name=value_column,
|
|
371
373
|
)
|
|
372
374
|
if aggregate:
|
|
373
|
-
frame = frame.group_by(group_by, variable_column).agg(
|
|
375
|
+
frame = frame.group_by(group_by, variable_column).agg(
|
|
376
|
+
pl.col(value_column).filter(pl.col(value_column).is_finite()).mean()
|
|
377
|
+
)
|
|
374
378
|
frame = frame.drop_nulls()
|
|
375
379
|
|
|
376
380
|
# HANDLE: standard scaling
|
|
377
381
|
if scale_axis is not None:
|
|
378
382
|
partition_key = (
|
|
379
|
-
variable_column if scale_axis == 0 else (group_by if aggregate else
|
|
383
|
+
variable_column if scale_axis == 0 else (group_by if aggregate else row_identifier)
|
|
380
384
|
)
|
|
381
385
|
frame = _scale_values(frame, value_column=value_column, partition_key=partition_key)
|
|
382
386
|
|
|
@@ -393,7 +397,7 @@ def heatmap(
|
|
|
393
397
|
if not aggregate and max_rows is not None:
|
|
394
398
|
frame = _bin_within_groups(
|
|
395
399
|
frame,
|
|
396
|
-
observations_name=
|
|
400
|
+
observations_name=row_identifier,
|
|
397
401
|
group_by=group_by,
|
|
398
402
|
variable_column=variable_column,
|
|
399
403
|
value_column=value_column,
|
|
@@ -407,7 +411,7 @@ def heatmap(
|
|
|
407
411
|
frame,
|
|
408
412
|
aggregate=aggregate,
|
|
409
413
|
group_by=group_by,
|
|
410
|
-
observations_name=
|
|
414
|
+
observations_name=row_identifier,
|
|
411
415
|
variable_column=variable_column,
|
|
412
416
|
x_keys=x_keys,
|
|
413
417
|
y_order_groups=y_order_groups,
|
|
@@ -428,15 +432,17 @@ def heatmap(
|
|
|
428
432
|
# Compute top edge of data and the y limit, extending the latter when
|
|
429
433
|
# key-group brackets are drawn so rotated labels are not clipped.
|
|
430
434
|
if aggregate:
|
|
435
|
+
data_bottom = -0.5
|
|
431
436
|
data_top = n_y - 0.5
|
|
432
437
|
else:
|
|
433
|
-
|
|
434
|
-
half_step =
|
|
435
|
-
|
|
438
|
+
y_step = _nonaggregate_y_step(n_x, n_y)
|
|
439
|
+
half_step = y_step / 2
|
|
440
|
+
data_bottom = -half_step
|
|
441
|
+
data_top = y_step * max(n_y - 1, 0) + half_step
|
|
436
442
|
y_max_limit = data_top
|
|
437
443
|
key_groups_total_span: float | None = None
|
|
438
444
|
if key_labels and key_groups is not None:
|
|
439
|
-
data_range = data_top
|
|
445
|
+
data_range = data_top - data_bottom
|
|
440
446
|
key_groups_padding = _resolve_padding(
|
|
441
447
|
key_groups,
|
|
442
448
|
padding=None,
|
|
@@ -452,19 +458,19 @@ def heatmap(
|
|
|
452
458
|
htmp += scale_y_continuous(
|
|
453
459
|
breaks=list(range(n_y)),
|
|
454
460
|
labels=y_order_groups,
|
|
455
|
-
limits=[
|
|
461
|
+
limits=[data_bottom, y_max_limit],
|
|
456
462
|
expand=[0, 0],
|
|
457
463
|
)
|
|
458
464
|
elif group_bars and group_bars_labels:
|
|
459
465
|
htmp += scale_y_continuous(
|
|
460
466
|
breaks=group_centers,
|
|
461
467
|
labels=y_order_groups,
|
|
462
|
-
limits=[
|
|
468
|
+
limits=[data_bottom, y_max_limit],
|
|
463
469
|
expand=[0, 0],
|
|
464
470
|
)
|
|
465
471
|
htmp += guides(color="none")
|
|
466
472
|
else:
|
|
467
|
-
htmp += scale_y_continuous(limits=[
|
|
473
|
+
htmp += scale_y_continuous(limits=[data_bottom, y_max_limit], expand=[0, 0])
|
|
468
474
|
htmp += theme(axis_text_y=element_blank(), axis_ticks_y=element_blank())
|
|
469
475
|
|
|
470
476
|
# GROUP color bar on left for non-aggregate
|
|
@@ -29,6 +29,7 @@ from cellestial.single.heatmap.utilities import (
|
|
|
29
29
|
)
|
|
30
30
|
from cellestial.themes import _THEME_DOTPLOT
|
|
31
31
|
from cellestial.util import (
|
|
32
|
+
_drop_nonfinite_rows,
|
|
32
33
|
_fill_gradient,
|
|
33
34
|
_get_dendrogram,
|
|
34
35
|
_get_dendrogram_path_frame,
|
|
@@ -332,7 +333,7 @@ def stacked_violin(
|
|
|
332
333
|
variable_name=variable_column,
|
|
333
334
|
value_name=value_column,
|
|
334
335
|
)
|
|
335
|
-
frame = frame
|
|
336
|
+
frame = _drop_nonfinite_rows(frame, [value_column])
|
|
336
337
|
# 2. Apply threshold filter
|
|
337
338
|
if threshold is not None:
|
|
338
339
|
frame = frame.filter(pl.col(value_column) >= threshold)
|
|
@@ -123,6 +123,12 @@ def _group_bar_gap(n_x: int) -> float:
|
|
|
123
123
|
return max(_GROUP_BAR_GAP_MIN, n_x * _GROUP_BAR_GAP_RATIO)
|
|
124
124
|
|
|
125
125
|
|
|
126
|
+
def _nonaggregate_y_step(n_x: int, n_y: int) -> float:
|
|
127
|
+
"""Return the distance between adjacent nonaggregated row centers."""
|
|
128
|
+
y_span = max(n_x - 1, 1)
|
|
129
|
+
return y_span / max(n_y - 1, 1)
|
|
130
|
+
|
|
131
|
+
|
|
126
132
|
def _assign_positions(
|
|
127
133
|
frame: pl.DataFrame,
|
|
128
134
|
*,
|
|
@@ -164,7 +170,7 @@ def _assign_positions(
|
|
|
164
170
|
)
|
|
165
171
|
return frame, None, n_x, n_y, [float(i) for i in range(n_y)]
|
|
166
172
|
|
|
167
|
-
#
|
|
173
|
+
# Nonaggregate: use a width-comparable y span for a square-ish aspect.
|
|
168
174
|
group_index = {g: i for i, g in enumerate(y_order_groups)}
|
|
169
175
|
cell_frame = (
|
|
170
176
|
frame.select(observations_name, group_by)
|
|
@@ -178,7 +184,7 @@ def _assign_positions(
|
|
|
178
184
|
.sort(["_group_index", observations_name])
|
|
179
185
|
)
|
|
180
186
|
n_y = cell_frame.height
|
|
181
|
-
y_step = (n_x
|
|
187
|
+
y_step = _nonaggregate_y_step(n_x, n_y)
|
|
182
188
|
cell_frame = cell_frame.with_columns(
|
|
183
189
|
(pl.int_range(pl.len()).cast(pl.Float64) * y_step).alias("position_y")
|
|
184
190
|
)
|
|
@@ -231,7 +237,7 @@ def _get_group_lines_frame(
|
|
|
231
237
|
.head(n_groups - 1)["y_max"]
|
|
232
238
|
.to_list()
|
|
233
239
|
)
|
|
234
|
-
half_step = (n_x
|
|
240
|
+
half_step = _nonaggregate_y_step(n_x, n_y) / 2
|
|
235
241
|
line_ys = [y + half_step for y in boundaries]
|
|
236
242
|
|
|
237
243
|
x_start = -0.5
|
|
@@ -12,6 +12,7 @@ from lets_plot import (
|
|
|
12
12
|
|
|
13
13
|
from cellestial.frames import _highest_expressed_genes_frame
|
|
14
14
|
from cellestial.themes import _THEME_HIGHEST
|
|
15
|
+
from cellestial.util import _drop_nonfinite_rows
|
|
15
16
|
from cellestial.util.errors import _unsupported_data_type
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
@@ -134,8 +135,8 @@ def highest_expressed_genes(
|
|
|
134
135
|
descending=True,
|
|
135
136
|
)
|
|
136
137
|
|
|
137
|
-
# FILTER:
|
|
138
|
-
frame = frame
|
|
138
|
+
# FILTER: keep finite values and apply threshold if provided
|
|
139
|
+
frame = _drop_nonfinite_rows(frame, [value_column])
|
|
139
140
|
frame = frame.filter(
|
|
140
141
|
pl.col(value_column) >= threshold if threshold is not None else True,
|
|
141
142
|
)
|
|
@@ -18,6 +18,7 @@ from cellestial.frames import build_frame
|
|
|
18
18
|
from cellestial.util import (
|
|
19
19
|
_collect_aes_columns,
|
|
20
20
|
_determine_axis,
|
|
21
|
+
_drop_nonfinite_rows,
|
|
21
22
|
_reject_sequence_key,
|
|
22
23
|
_resolve_tooltips,
|
|
23
24
|
_validate_tooltips,
|
|
@@ -201,8 +202,8 @@ def ridge(
|
|
|
201
202
|
metadata_columns=metadata_columns,
|
|
202
203
|
)
|
|
203
204
|
|
|
204
|
-
# FILTER:
|
|
205
|
-
frame = frame
|
|
205
|
+
# FILTER: keep finite values and apply threshold
|
|
206
|
+
frame = _drop_nonfinite_rows(frame, [key]).filter(pl.col(group_by).is_not_null())
|
|
206
207
|
frame = frame.filter(
|
|
207
208
|
pl.col(key) >= threshold if threshold is not None else True,
|
|
208
209
|
)
|
|
@@ -25,6 +25,7 @@ from cellestial.themes import _THEME_SPATIAL
|
|
|
25
25
|
from cellestial.util import (
|
|
26
26
|
_collect_aes_columns,
|
|
27
27
|
_color_gradient,
|
|
28
|
+
_drop_nonfinite_rows,
|
|
28
29
|
_fill_gradient,
|
|
29
30
|
_reject_sequence_key,
|
|
30
31
|
_resolve_tooltips,
|
|
@@ -358,6 +359,17 @@ def spatial(
|
|
|
358
359
|
pl.Series("spatial_y", spot_coordinates[:, 1]),
|
|
359
360
|
)
|
|
360
361
|
|
|
362
|
+
if is_polygon:
|
|
363
|
+
finite_vertex = pl.all_horizontal(
|
|
364
|
+
pl.col("polygon_x").is_not_null(),
|
|
365
|
+
pl.col("polygon_x").is_finite(),
|
|
366
|
+
pl.col("polygon_y").is_not_null(),
|
|
367
|
+
pl.col("polygon_y").is_finite(),
|
|
368
|
+
)
|
|
369
|
+
frame = frame.filter(finite_vertex.all().over("instance_id"))
|
|
370
|
+
else:
|
|
371
|
+
frame = _drop_nonfinite_rows(frame, ["spatial_x", "spatial_y"])
|
|
372
|
+
|
|
361
373
|
_validate_tooltips(tooltips, frame)
|
|
362
374
|
|
|
363
375
|
# HANDLE: groups filter (categorical-only)
|
|
@@ -9,6 +9,7 @@ from cellestial.util.utilities import (
|
|
|
9
9
|
_collect_aes_columns,
|
|
10
10
|
_color_gradient,
|
|
11
11
|
_determine_axis,
|
|
12
|
+
_drop_nonfinite_rows,
|
|
12
13
|
_fill_gradient,
|
|
13
14
|
_is_observation_key,
|
|
14
15
|
_is_variable_key,
|
|
@@ -30,6 +31,7 @@ __all__ = [
|
|
|
30
31
|
"_collect_aes_columns",
|
|
31
32
|
"_color_gradient",
|
|
32
33
|
"_determine_axis",
|
|
34
|
+
"_drop_nonfinite_rows",
|
|
33
35
|
"_fill_gradient",
|
|
34
36
|
"_get_dendrogram",
|
|
35
37
|
"_get_dendrogram_path_frame",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Sequence
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import TYPE_CHECKING, cast
|
|
5
5
|
|
|
6
6
|
from lets_plot import gggrid
|
|
7
7
|
from lets_plot.plot.core import PlotSpec
|
|
@@ -30,32 +30,39 @@ def _normalize_widths(
|
|
|
30
30
|
msg = f"`widths` must be a flat or nested sequence, but received {type(widths)}"
|
|
31
31
|
raise TypeError(msg)
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
nested_flags = [
|
|
34
|
+
isinstance(row_widths, Sequence) and not isinstance(row_widths, (str, bytes))
|
|
35
|
+
for row_widths in widths
|
|
36
|
+
]
|
|
37
|
+
if any(nested_flags):
|
|
38
|
+
if not all(nested_flags):
|
|
39
|
+
msg = "`widths` mixes numbers and sequences; use one sequence per row."
|
|
37
40
|
raise ValueError(msg)
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
nested_widths = cast("Sequence[Sequence[float]]", widths)
|
|
42
|
+
if len(nested_widths) != len(plots):
|
|
43
|
+
msg = (
|
|
44
|
+
f"nested `widths` has {len(nested_widths)} rows but `plots` has {len(plots)} rows."
|
|
45
|
+
)
|
|
40
46
|
raise ValueError(msg)
|
|
41
|
-
for index, (row, row_widths) in enumerate(zip(plots,
|
|
47
|
+
for index, (row, row_widths) in enumerate(zip(plots, nested_widths, strict=True)):
|
|
42
48
|
if len(row_widths) != len(row):
|
|
43
49
|
msg = (
|
|
44
50
|
f"`widths[{index}]` has {len(row_widths)} values "
|
|
45
51
|
f"but row {index} has {len(row)} plots."
|
|
46
52
|
)
|
|
47
53
|
raise ValueError(msg)
|
|
48
|
-
return [list(row_widths) for row_widths in
|
|
54
|
+
return [list(row_widths) for row_widths in nested_widths]
|
|
49
55
|
|
|
50
56
|
# flat widths: broadcast to every row, valid only when all rows share that length.
|
|
51
|
-
|
|
57
|
+
flat_widths = cast("Sequence[float]", widths)
|
|
58
|
+
uneven = [index for index, row in enumerate(plots) if len(row) != len(flat_widths)]
|
|
52
59
|
if uneven:
|
|
53
60
|
msg = (
|
|
54
|
-
f"flat `widths` of length {len(
|
|
61
|
+
f"flat `widths` of length {len(flat_widths)} cannot broadcast to rows {uneven} "
|
|
55
62
|
"with differing plot counts; pass a nested `widths` instead."
|
|
56
63
|
)
|
|
57
64
|
raise ValueError(msg)
|
|
58
|
-
return [list(
|
|
65
|
+
return [list(flat_widths) for _ in plots]
|
|
59
66
|
|
|
60
67
|
|
|
61
68
|
def layout(
|
|
@@ -65,6 +65,22 @@ def _warn(message: str) -> None:
|
|
|
65
65
|
warnings.formatwarning = original_format
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
def _drop_nonfinite_rows(frame: pl.DataFrame, columns: Sequence[str]) -> pl.DataFrame:
|
|
69
|
+
"""Keep rows whose selected numeric columns are present and finite."""
|
|
70
|
+
numeric_columns = [
|
|
71
|
+
column
|
|
72
|
+
for column in dict.fromkeys(columns)
|
|
73
|
+
if column in frame.columns and frame.schema[column].is_numeric()
|
|
74
|
+
]
|
|
75
|
+
if not numeric_columns:
|
|
76
|
+
return frame
|
|
77
|
+
return frame.filter(
|
|
78
|
+
pl.all_horizontal(
|
|
79
|
+
pl.col(column).is_not_null() & pl.col(column).is_finite() for column in numeric_columns
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
68
84
|
def _build_tooltips(
|
|
69
85
|
*,
|
|
70
86
|
tooltips: list[str] | str,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cellestial"
|
|
3
|
-
version = "0.58.
|
|
3
|
+
version = "0.58.2"
|
|
4
4
|
description = "Interactive single-cell and spatial omics visualization library"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Zaf4", email = "zafermolbio@gmail.com" },
|
|
@@ -71,6 +71,8 @@ tqdm = "^4.67.3"
|
|
|
71
71
|
pytest = "^9.0.3"
|
|
72
72
|
pytest-cov = "^7.1.0"
|
|
73
73
|
nbstripout = "^0.9.1"
|
|
74
|
+
pytest-xdist = "^3.8.0"
|
|
75
|
+
pytest-playwright = "^0.8.0"
|
|
74
76
|
|
|
75
77
|
[tool.poetry.group.docs.dependencies]
|
|
76
78
|
sphinx = ">=8.0.0"
|
|
@@ -84,7 +86,10 @@ squidpy = "^1.8.1"
|
|
|
84
86
|
|
|
85
87
|
[tool.pytest.ini_options]
|
|
86
88
|
testpaths = ["tests"]
|
|
87
|
-
addopts = "--cov=cellestial --cov-report=xml:tests/coverage.xml --cov-report=term-missing"
|
|
89
|
+
addopts = "-n 4 --dist=loadscope -m 'not browser' --cov=cellestial --cov-report=xml:tests/coverage.xml --cov-report=term-missing"
|
|
90
|
+
markers = [
|
|
91
|
+
"browser: exercises Lets-Plot in a real browser",
|
|
92
|
+
]
|
|
88
93
|
|
|
89
94
|
[tool.coverage.run]
|
|
90
95
|
data_file = "tests/.coverage"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|