cellestial 0.44.0__tar.gz → 0.46.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.44.0 → cellestial-0.46.0}/PKG-INFO +1 -1
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/__init__.py +2 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/utilities.py +3 -3
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/__init__.py +2 -1
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/core/dimensional.py +2 -2
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/core/distribution.py +3 -4
- cellestial-0.46.0/cellestial/single/differential/__init__.py +4 -0
- cellestial-0.46.0/cellestial/single/differential/markers.py +446 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/heatmap/_rank_genes_groups.py +3 -4
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/heatmap/dotplot.py +24 -16
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/heatmap/heatmap.py +33 -32
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/heatmap/stacked_violin.py +14 -16
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/spatial/spatial.py +3 -3
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/__init__.py +2 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/errors.py +4 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/utilities.py +62 -9
- {cellestial-0.44.0 → cellestial-0.46.0}/pyproject.toml +2 -1
- cellestial-0.44.0/cellestial/single/differential/__init__.py +0 -3
- {cellestial-0.44.0 → cellestial-0.46.0}/LICENSE +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/README.md +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/_version.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/datasets/__init__.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/datasets/datasets.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/frames/__init__.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/frames/build.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/frames/operations.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/__init__.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/_deferred.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/arrow.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/bracket.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/ondata_legend.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/outline.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/stream.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/base/__init__.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/base/base.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/basic/__init__.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/basic/bar.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/basic/scatter.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/common/__init__.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/common/xyplot.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/common/xyplots.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/core/__init__.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/core/distributions.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/core/subdimensional.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/core/subdimensionals.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/differential/volcano.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/heatmap/__init__.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/heatmap/_key_groups.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/quick/__init__.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/quick/elbow.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/quick/highest.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/quick/ridge.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/spatial/__init__.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/spatial/_components.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/spatial/spatials.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/themes/__init__.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/themes/_distributions.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/themes/_heatmaps.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/themes/_scatters.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/colors.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/dendrogram.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/operations.py +0 -0
- {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/save.py +0 -0
|
@@ -16,6 +16,7 @@ from cellestial.single import (
|
|
|
16
16
|
expressions,
|
|
17
17
|
heatmap,
|
|
18
18
|
highest_expressed_genes,
|
|
19
|
+
markers,
|
|
19
20
|
matrixplot,
|
|
20
21
|
pca,
|
|
21
22
|
pcas,
|
|
@@ -82,6 +83,7 @@ __all__ = [
|
|
|
82
83
|
"get_mapping",
|
|
83
84
|
"heatmap",
|
|
84
85
|
"highest_expressed_genes",
|
|
86
|
+
"markers",
|
|
85
87
|
"matrixplot",
|
|
86
88
|
"ondata_legend",
|
|
87
89
|
"pca",
|
|
@@ -18,7 +18,7 @@ def borders(
|
|
|
18
18
|
)-> FeatureSpec:
|
|
19
19
|
"""Add colored rectangels around panel, legend, and plot areas."""
|
|
20
20
|
return theme(
|
|
21
|
-
panel_background=element_rect(color=
|
|
22
|
-
legend_background=element_rect(color=
|
|
23
|
-
plot_background=element_rect(color=
|
|
21
|
+
panel_background=element_rect(color=panel_color, size=panel_size),
|
|
22
|
+
legend_background=element_rect(color=legend_color, size=legend_size),
|
|
23
|
+
plot_background=element_rect(color=plot_color, size=plot_size),
|
|
24
24
|
)
|
|
@@ -17,7 +17,7 @@ from cellestial.single.core import (
|
|
|
17
17
|
violin,
|
|
18
18
|
violins,
|
|
19
19
|
)
|
|
20
|
-
from cellestial.single.differential import volcano, volcanos
|
|
20
|
+
from cellestial.single.differential import markers, volcano, volcanos
|
|
21
21
|
from cellestial.single.heatmap import dotplot, heatmap, matrixplot, stacked_violin
|
|
22
22
|
from cellestial.single.quick import elbow, highest_expressed_genes, ridge, ridges
|
|
23
23
|
|
|
@@ -33,6 +33,7 @@ __all__ = [
|
|
|
33
33
|
"expressions",
|
|
34
34
|
"heatmap",
|
|
35
35
|
"highest_expressed_genes",
|
|
36
|
+
"markers",
|
|
36
37
|
"matrixplot",
|
|
37
38
|
"pca",
|
|
38
39
|
"pcas",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import warnings
|
|
4
3
|
from collections.abc import Sequence
|
|
5
4
|
from typing import TYPE_CHECKING, Literal
|
|
6
5
|
|
|
@@ -27,6 +26,7 @@ from cellestial.util import (
|
|
|
27
26
|
_is_variable_key,
|
|
28
27
|
_resolve_tooltips,
|
|
29
28
|
_validate_tooltips,
|
|
29
|
+
_warn,
|
|
30
30
|
)
|
|
31
31
|
from cellestial.util.errors import UnsupportedDataTypeError
|
|
32
32
|
|
|
@@ -335,6 +335,6 @@ def dimensional(
|
|
|
335
335
|
)
|
|
336
336
|
elif frame[key].dtype != pl.Categorical:
|
|
337
337
|
msg = f"key `{key}` is not categorical, legend on data will not be added"
|
|
338
|
-
|
|
338
|
+
_warn(msg)
|
|
339
339
|
|
|
340
340
|
return scttr
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import Sequence
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Literal
|
|
5
|
-
from warnings import warn
|
|
6
5
|
|
|
7
6
|
import polars as pl
|
|
8
7
|
from anndata import AnnData
|
|
@@ -27,6 +26,7 @@ from cellestial.util import (
|
|
|
27
26
|
_resolve_tooltips,
|
|
28
27
|
_select_variable_keys,
|
|
29
28
|
_validate_tooltips,
|
|
29
|
+
_warn,
|
|
30
30
|
)
|
|
31
31
|
from cellestial.util.errors import UnsupportedDataTypeError
|
|
32
32
|
|
|
@@ -131,11 +131,10 @@ def _distribution(
|
|
|
131
131
|
if group_by is None:
|
|
132
132
|
group_by = variable_column
|
|
133
133
|
elif len(keys) > 1 and group_by != variable_column:
|
|
134
|
-
|
|
134
|
+
_warn(
|
|
135
135
|
f"Multiple keys provided; `group_by={group_by!r}` cannot share the "
|
|
136
136
|
f"x-axis with the variable column. Falling back to "
|
|
137
|
-
f"`group_by={variable_column!r}`. For a per-key panel, use the plural variant."
|
|
138
|
-
stacklevel=3,
|
|
137
|
+
f"`group_by={variable_column!r}`. For a per-key panel, use the plural variant."
|
|
139
138
|
)
|
|
140
139
|
group_by = variable_column
|
|
141
140
|
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import polars as pl
|
|
8
|
+
from anndata import AnnData
|
|
9
|
+
from lets_plot import (
|
|
10
|
+
aes,
|
|
11
|
+
element_text,
|
|
12
|
+
geom_path,
|
|
13
|
+
geom_text,
|
|
14
|
+
gggrid,
|
|
15
|
+
ggplot,
|
|
16
|
+
ggtb,
|
|
17
|
+
ggtitle,
|
|
18
|
+
labs,
|
|
19
|
+
scale_color_gradient,
|
|
20
|
+
scale_y_continuous,
|
|
21
|
+
theme,
|
|
22
|
+
theme_bw,
|
|
23
|
+
)
|
|
24
|
+
from lets_plot.plot.core import FeatureSpec, LayerSpec
|
|
25
|
+
|
|
26
|
+
from cellestial.single.heatmap._rank_genes_groups import _resolve_rank_genes_groups_key
|
|
27
|
+
from cellestial.util import _share_axis, _share_labels
|
|
28
|
+
from cellestial.util.errors import KeyNotFoundError, UnsupportedDataTypeError
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from lets_plot.plot.subplots import SupPlotsSpec
|
|
32
|
+
from polars import DataFrame
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _build_markers_frame(
|
|
36
|
+
data: AnnData,
|
|
37
|
+
*,
|
|
38
|
+
key: bool | str,
|
|
39
|
+
n_genes: int,
|
|
40
|
+
groups: Sequence[str] | None,
|
|
41
|
+
variable_column: str,
|
|
42
|
+
score_column: str,
|
|
43
|
+
rank_column: str,
|
|
44
|
+
group_column: str,
|
|
45
|
+
) -> tuple[DataFrame, list[str], str]:
|
|
46
|
+
"""
|
|
47
|
+
Build a long-form frame of the top-N ranked genes and scores per group.
|
|
48
|
+
|
|
49
|
+
Reads ``data.uns[key]`` (scanpy's ``rank_genes_groups`` convention) and
|
|
50
|
+
pulls the top ``n_genes`` gene names and their scores for each selected
|
|
51
|
+
group. Unlike the heatmap extractor, genes are kept per group with no
|
|
52
|
+
cross-group de-duplication, since each group is drawn in its own panel.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
data : AnnData
|
|
57
|
+
Source object that already holds a ``rank_genes_groups`` result.
|
|
58
|
+
key : bool | str
|
|
59
|
+
``True`` reads the default ``rank_genes_groups`` key; a string reads
|
|
60
|
+
the matching custom key (e.g. ``"rank_genes_groups_wilcoxon"``).
|
|
61
|
+
n_genes : int
|
|
62
|
+
Number of top genes to pull per group.
|
|
63
|
+
groups : Sequence[str] | None
|
|
64
|
+
Subset of groups to include, in order. ``None`` keeps all groups in
|
|
65
|
+
their stored order.
|
|
66
|
+
variable_column : str
|
|
67
|
+
Output column name for the gene/feature names.
|
|
68
|
+
score_column : str
|
|
69
|
+
Output column name for the ranking scores.
|
|
70
|
+
rank_column : str
|
|
71
|
+
Output column name for the per-group rank index (0-based).
|
|
72
|
+
group_column : str
|
|
73
|
+
Output column name for the group label.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
frame : DataFrame
|
|
78
|
+
Long-form polars DataFrame with one row per (group, gene).
|
|
79
|
+
selected : list[str]
|
|
80
|
+
The selected group labels, in plotting order.
|
|
81
|
+
group_by : str
|
|
82
|
+
The categorical key used when ``rank_genes_groups`` was computed.
|
|
83
|
+
|
|
84
|
+
Raises
|
|
85
|
+
------
|
|
86
|
+
UnsupportedDataTypeError
|
|
87
|
+
If `data` is not a supported single-cell data object.
|
|
88
|
+
KeyNotFoundError
|
|
89
|
+
If the ranking result, a requested group, or the stored ``groupby``
|
|
90
|
+
is missing.
|
|
91
|
+
ValueError
|
|
92
|
+
If `n_genes` is out of range.
|
|
93
|
+
"""
|
|
94
|
+
if n_genes < 1:
|
|
95
|
+
msg = f"`n_genes` must be >= 1, got {n_genes}."
|
|
96
|
+
raise ValueError(msg)
|
|
97
|
+
|
|
98
|
+
uns_key = _resolve_rank_genes_groups_key(key)
|
|
99
|
+
|
|
100
|
+
if isinstance(data, AnnData):
|
|
101
|
+
if uns_key not in data.uns:
|
|
102
|
+
msg = (
|
|
103
|
+
f"`adata.uns[{uns_key!r}]` not found. "
|
|
104
|
+
"Run `scanpy.tl.rank_genes_groups` first "
|
|
105
|
+
"(or pass the correct `key_added` value as `key`)."
|
|
106
|
+
)
|
|
107
|
+
raise KeyNotFoundError(msg)
|
|
108
|
+
|
|
109
|
+
record = data.uns[uns_key]
|
|
110
|
+
if "names" not in record or "scores" not in record or "params" not in record:
|
|
111
|
+
msg = (
|
|
112
|
+
f"`adata.uns[{uns_key!r}]` does not look like a "
|
|
113
|
+
"`rank_genes_groups` result (missing 'names', 'scores', or 'params')."
|
|
114
|
+
)
|
|
115
|
+
raise KeyNotFoundError(msg)
|
|
116
|
+
|
|
117
|
+
names = record["names"]
|
|
118
|
+
scores = record["scores"]
|
|
119
|
+
available_groups = list(names.dtype.names)
|
|
120
|
+
|
|
121
|
+
if groups is None:
|
|
122
|
+
selected = available_groups
|
|
123
|
+
else:
|
|
124
|
+
selected = list(groups)
|
|
125
|
+
unknown = [group for group in selected if group not in available_groups]
|
|
126
|
+
if unknown:
|
|
127
|
+
msg = (
|
|
128
|
+
f"Groups {unknown!r} not found in "
|
|
129
|
+
f"`adata.uns[{uns_key!r}]`. Available: {available_groups!r}."
|
|
130
|
+
)
|
|
131
|
+
raise KeyNotFoundError(msg)
|
|
132
|
+
|
|
133
|
+
n_available = len(names)
|
|
134
|
+
if n_genes > n_available:
|
|
135
|
+
msg = (
|
|
136
|
+
f"`n_genes={n_genes}` exceeds the {n_available} ranked genes "
|
|
137
|
+
f"stored in `adata.uns[{uns_key!r}]`."
|
|
138
|
+
)
|
|
139
|
+
raise ValueError(msg)
|
|
140
|
+
|
|
141
|
+
ranks = np.arange(n_genes)
|
|
142
|
+
group_frames = [
|
|
143
|
+
pl.DataFrame(
|
|
144
|
+
{
|
|
145
|
+
group_column: group,
|
|
146
|
+
rank_column: ranks,
|
|
147
|
+
variable_column: [str(name) for name in names[group][:n_genes]],
|
|
148
|
+
score_column: np.asarray(scores[group][:n_genes], dtype=np.float64),
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
for group in selected
|
|
152
|
+
]
|
|
153
|
+
frame = pl.concat(group_frames)
|
|
154
|
+
|
|
155
|
+
params = record["params"]
|
|
156
|
+
stored_group_by = params["groupby"] if "groupby" in params else None
|
|
157
|
+
if stored_group_by is None:
|
|
158
|
+
msg = (
|
|
159
|
+
f"`adata.uns[{uns_key!r}]['params']` is missing 'groupby'; "
|
|
160
|
+
"cannot infer `group_by`."
|
|
161
|
+
)
|
|
162
|
+
raise KeyNotFoundError(msg)
|
|
163
|
+
|
|
164
|
+
# Round scores to keep the embedded JSON compact when the frame is
|
|
165
|
+
# serialized into the plot output.
|
|
166
|
+
frame = frame.with_columns(pl.col(score_column).round(4))
|
|
167
|
+
|
|
168
|
+
return frame, list(selected), str(stored_group_by)
|
|
169
|
+
|
|
170
|
+
msg = f"Unsupported data type: `{type(data)}`"
|
|
171
|
+
raise UnsupportedDataTypeError(msg)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def markers(
|
|
175
|
+
data: AnnData,
|
|
176
|
+
groups: Sequence[str] | None = None,
|
|
177
|
+
*,
|
|
178
|
+
key: str = "rank_genes_groups",
|
|
179
|
+
n_genes: int = 20,
|
|
180
|
+
mapping: FeatureSpec | None = None,
|
|
181
|
+
text_color: str = "#1f1f1f",
|
|
182
|
+
text_size: float = 4.0,
|
|
183
|
+
fontface: str = "bold",
|
|
184
|
+
angle: float = 90.0,
|
|
185
|
+
rank_color: bool = False,
|
|
186
|
+
line: bool = False,
|
|
187
|
+
line_color: str = "#3f3f3f",
|
|
188
|
+
line_size: float = 0.4,
|
|
189
|
+
line_alpha: float = 0.6,
|
|
190
|
+
line_kwargs: dict | None = None,
|
|
191
|
+
variable_column: str = "variable",
|
|
192
|
+
score_column: str = "score",
|
|
193
|
+
rank_column: str = "rank",
|
|
194
|
+
group_column: str = "group",
|
|
195
|
+
interactive: bool = False,
|
|
196
|
+
# multi plot args
|
|
197
|
+
share_labels: bool = True,
|
|
198
|
+
share_axis: bool = False,
|
|
199
|
+
layers: Sequence[FeatureSpec | LayerSpec] | FeatureSpec | LayerSpec | None = None,
|
|
200
|
+
# grid args
|
|
201
|
+
ncol: int | None = 3,
|
|
202
|
+
sharex: str | None = None,
|
|
203
|
+
sharey: str | None = None,
|
|
204
|
+
widths: list[float] | None = None,
|
|
205
|
+
heights: list[float] | None = None,
|
|
206
|
+
hspace: float | None = None,
|
|
207
|
+
vspace: float | None = None,
|
|
208
|
+
fit: bool | None = None,
|
|
209
|
+
align: bool | None = None,
|
|
210
|
+
guides: str = "auto",
|
|
211
|
+
**text_kwargs,
|
|
212
|
+
) -> SupPlotsSpec:
|
|
213
|
+
"""
|
|
214
|
+
Grid of ranked genes per group.
|
|
215
|
+
|
|
216
|
+
Builds one panel per group from a precomputed ranking, placing the top
|
|
217
|
+
``n_genes`` gene names as text at their ``(rank, score)`` position. Each
|
|
218
|
+
panel is titled ``"{group} vs. rest"``.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
data : AnnData
|
|
223
|
+
The AnnData object holding the differential expression results
|
|
224
|
+
in ``data.uns[key]`` (scanpy's ``rank_genes_groups`` convention).
|
|
225
|
+
groups : Sequence[str] | None, default=None
|
|
226
|
+
Subset of groups to plot, one panel per group. ``None`` keeps all
|
|
227
|
+
groups in their stored order.
|
|
228
|
+
key : str, default='rank_genes_groups'
|
|
229
|
+
The key under ``data.uns`` storing the differential expression results.
|
|
230
|
+
n_genes : int, default=20
|
|
231
|
+
Number of top genes to show per panel.
|
|
232
|
+
mapping : FeatureSpec | None, default=None
|
|
233
|
+
Additional aesthetic mappings, the result of `aes()`. Merged on top of
|
|
234
|
+
the default ``aes(x=rank, y=score, label=variable)``.
|
|
235
|
+
text_color : str, default='#1f1f1f'
|
|
236
|
+
Color of the gene-name text.
|
|
237
|
+
text_size : float, default=4.0
|
|
238
|
+
Size of the gene-name text.
|
|
239
|
+
fontface : str, default='bold'
|
|
240
|
+
Font face of the gene-name text (e.g. 'plain', 'bold', 'italic').
|
|
241
|
+
angle : float, default=90.0
|
|
242
|
+
Rotation angle of the gene-name text, in degrees.
|
|
243
|
+
rank_color : bool, default=False
|
|
244
|
+
Whether to color the gene-name text by rank. When True, the best-ranked
|
|
245
|
+
gene is pure red, fading toward light gray for the lowest-ranked.
|
|
246
|
+
line : bool, default=False
|
|
247
|
+
Whether to draw a dashed path connecting the genes' ``(rank, score)``
|
|
248
|
+
points, showing the score decay across the ranking.
|
|
249
|
+
line_color : str, default='#3f3f3f'
|
|
250
|
+
Color of the dashed score-curve line.
|
|
251
|
+
line_size : float, default=0.4
|
|
252
|
+
Size (thickness) of the dashed score-curve line.
|
|
253
|
+
line_alpha : float, default=0.6
|
|
254
|
+
Alpha (opacity) of the dashed score-curve line.
|
|
255
|
+
line_kwargs : dict | None, default=None
|
|
256
|
+
Additional parameters passed to the score-curve ``geom_path`` layer.
|
|
257
|
+
variable_column : str, default='variable'
|
|
258
|
+
Output column name for the gene/feature names.
|
|
259
|
+
score_column : str, default='score'
|
|
260
|
+
Output column name for the ranking scores.
|
|
261
|
+
rank_column : str, default='rank'
|
|
262
|
+
Output column name for the per-group rank index (0-based).
|
|
263
|
+
group_column : str, default='group'
|
|
264
|
+
Output column name for the group label.
|
|
265
|
+
interactive : bool, default=False
|
|
266
|
+
Whether to make the plot interactive.
|
|
267
|
+
share_labels : bool, default=True
|
|
268
|
+
Whether to share axis labels across the grid.
|
|
269
|
+
If True, only X labels on bottom row and Y labels on left column are shown.
|
|
270
|
+
share_axis : bool, default=False
|
|
271
|
+
Whether to share axes across the grid.
|
|
272
|
+
If True, only X axis on bottom row and Y axis on left column is shown.
|
|
273
|
+
layers : Sequence[FeatureSpec|LayerSpec] | FeatureSpec | LayerSpec | None, default=None
|
|
274
|
+
Layers to add to all the plots in the grid.
|
|
275
|
+
ncol : int, default=3
|
|
276
|
+
Number of columns in grid. If None, shows plots horizontally, in one row.
|
|
277
|
+
sharex, sharey : str | None, default=None
|
|
278
|
+
Controls sharing of axis limits between subplots in the grid.
|
|
279
|
+
See :func:`lets_plot.gggrid`.
|
|
280
|
+
widths : list[float], default=None
|
|
281
|
+
Relative width of each column of the grid.
|
|
282
|
+
heights : list[float], default=None
|
|
283
|
+
Relative height of each row of the grid.
|
|
284
|
+
hspace, vspace : float | None, default=None
|
|
285
|
+
Cell horizontal/vertical spacing in px.
|
|
286
|
+
fit : bool, default=None
|
|
287
|
+
Whether to stretch each plot to match the aspect ratio of its cell.
|
|
288
|
+
align : bool, default=None
|
|
289
|
+
Whether to align inner areas of plots.
|
|
290
|
+
guides : str, default='auto'
|
|
291
|
+
How guides (legends/colorbars) should be treated in the layout.
|
|
292
|
+
See :func:`lets_plot.gggrid`.
|
|
293
|
+
**text_kwargs
|
|
294
|
+
Additional parameters for the ``geom_text`` layer of each panel.
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
SupPlotsSpec
|
|
299
|
+
Grid of ranked-genes panels, one per group.
|
|
300
|
+
|
|
301
|
+
Raises
|
|
302
|
+
------
|
|
303
|
+
UnsupportedDataTypeError
|
|
304
|
+
If `data` is not a supported single-cell data object.
|
|
305
|
+
KeyNotFoundError
|
|
306
|
+
If the ranking result or a requested group is missing.
|
|
307
|
+
ValueError
|
|
308
|
+
If `n_genes` is out of range.
|
|
309
|
+
TypeError
|
|
310
|
+
If `groups` is neither a Sequence of strings nor None.
|
|
311
|
+
|
|
312
|
+
Examples
|
|
313
|
+
--------
|
|
314
|
+
A simple grid of ranked genes per group, with optional score-curve lines.
|
|
315
|
+
|
|
316
|
+
.. jupyter-execute::
|
|
317
|
+
|
|
318
|
+
import scanpy as sc
|
|
319
|
+
|
|
320
|
+
import cellestial as cl
|
|
321
|
+
|
|
322
|
+
data = sc.datasets.pbmc68k_reduced()
|
|
323
|
+
|
|
324
|
+
cl.markers(data, line=True)
|
|
325
|
+
|
|
326
|
+
Color the gene names by rank.
|
|
327
|
+
|
|
328
|
+
.. jupyter-execute::
|
|
329
|
+
|
|
330
|
+
cl.markers(data, rank_color=True)
|
|
331
|
+
|
|
332
|
+
Select a subset of groups.
|
|
333
|
+
|
|
334
|
+
.. jupyter-execute::
|
|
335
|
+
|
|
336
|
+
cl.markers(
|
|
337
|
+
data,
|
|
338
|
+
groups=["CD14+ Monocyte", "CD4+/CD25 T Reg", "CD4+/CD45RO+ Memory", "CD34+"],
|
|
339
|
+
ncol=2,
|
|
340
|
+
rank_color=True,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
"""
|
|
344
|
+
# HANDLE: Data types
|
|
345
|
+
if not isinstance(data, AnnData):
|
|
346
|
+
msg = f"Unsupported data type: `{type(data)}`"
|
|
347
|
+
raise UnsupportedDataTypeError(msg)
|
|
348
|
+
|
|
349
|
+
if groups is not None and (not isinstance(groups, Sequence) or isinstance(groups, str)):
|
|
350
|
+
msg = "`groups` must be a Sequence of strings or None"
|
|
351
|
+
raise TypeError(msg)
|
|
352
|
+
|
|
353
|
+
# BUILD: long-form frame via the helper
|
|
354
|
+
frame, selected_groups, _ = _build_markers_frame(
|
|
355
|
+
data=data,
|
|
356
|
+
key=key,
|
|
357
|
+
n_genes=n_genes,
|
|
358
|
+
groups=groups,
|
|
359
|
+
variable_column=variable_column,
|
|
360
|
+
score_column=score_column,
|
|
361
|
+
rank_column=rank_column,
|
|
362
|
+
group_column=group_column,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# DEFINE: mapping with defaults
|
|
366
|
+
_mapping = {
|
|
367
|
+
"x": rank_column,
|
|
368
|
+
"y": score_column,
|
|
369
|
+
"label": variable_column,
|
|
370
|
+
}
|
|
371
|
+
if rank_color:
|
|
372
|
+
_mapping["color"] = rank_column
|
|
373
|
+
if mapping is not None:
|
|
374
|
+
_mapping.update(mapping.as_dict())
|
|
375
|
+
|
|
376
|
+
# BUILD: one panel per group
|
|
377
|
+
plots = []
|
|
378
|
+
for i, group in enumerate(selected_groups):
|
|
379
|
+
group_frame = frame.filter(pl.col(group_column) == group)
|
|
380
|
+
plot = ggplot(group_frame)
|
|
381
|
+
|
|
382
|
+
# ADD: optional dashed score-curve path (drawn under the text)
|
|
383
|
+
if line:
|
|
384
|
+
_line_kwargs = {
|
|
385
|
+
"color": line_color,
|
|
386
|
+
"size": line_size,
|
|
387
|
+
"alpha": line_alpha,
|
|
388
|
+
"linetype": "dashed",
|
|
389
|
+
} | (line_kwargs or {})
|
|
390
|
+
plot += geom_path(aes(x=rank_column, y=score_column), **_line_kwargs)
|
|
391
|
+
|
|
392
|
+
# A constant `color=` would override the rank color aesthetic, so only
|
|
393
|
+
# set it when not coloring by rank.
|
|
394
|
+
_text_color = {} if rank_color else {"color": text_color}
|
|
395
|
+
plot += geom_text(
|
|
396
|
+
aes(**_mapping),
|
|
397
|
+
size=text_size,
|
|
398
|
+
fontface=fontface,
|
|
399
|
+
angle=angle,
|
|
400
|
+
**_text_color,
|
|
401
|
+
**text_kwargs,
|
|
402
|
+
)
|
|
403
|
+
if rank_color:
|
|
404
|
+
# rank 0 (best) is pure red, fading toward light gray for the rest.
|
|
405
|
+
plot += scale_color_gradient(low="#ff0000", high="#e6e6e6")
|
|
406
|
+
# Gene names run vertically (8-10 chars), so expand the y range to keep
|
|
407
|
+
# the top- and bottom-most labels from being clipped.
|
|
408
|
+
plot += scale_y_continuous(expand=[0.25, 0.0])
|
|
409
|
+
plot += labs(x="ranking", y="score")
|
|
410
|
+
plot += ggtitle(f"{group} vs. rest")
|
|
411
|
+
plot += theme(
|
|
412
|
+
plot_title=element_text(size=11, hjust=0.5, family="Georgia"),
|
|
413
|
+
legend_position="none",
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# handle the layers
|
|
417
|
+
if layers is not None:
|
|
418
|
+
if isinstance(layers, (FeatureSpec, LayerSpec)):
|
|
419
|
+
layers = [layers]
|
|
420
|
+
for layer in layers:
|
|
421
|
+
plot += layer
|
|
422
|
+
if share_labels:
|
|
423
|
+
plot = _share_labels(plot, i, selected_groups, ncol)
|
|
424
|
+
if share_axis:
|
|
425
|
+
plot = _share_axis(plot, i, selected_groups, ncol, "axis")
|
|
426
|
+
|
|
427
|
+
plots.append(plot)
|
|
428
|
+
|
|
429
|
+
grid = gggrid(
|
|
430
|
+
plots,
|
|
431
|
+
ncol=ncol, # ty:ignore[invalid-argument-type]
|
|
432
|
+
sharex=sharex, # ty:ignore[invalid-argument-type]
|
|
433
|
+
sharey=sharey, # ty:ignore[invalid-argument-type]
|
|
434
|
+
widths=widths, # ty:ignore[invalid-argument-type]
|
|
435
|
+
heights=heights, # ty:ignore[invalid-argument-type]
|
|
436
|
+
hspace=hspace, # ty:ignore[invalid-argument-type]
|
|
437
|
+
vspace=vspace, # ty:ignore[invalid-argument-type]
|
|
438
|
+
fit=fit, # ty:ignore[invalid-argument-type]
|
|
439
|
+
align=align, # ty:ignore[invalid-argument-type]
|
|
440
|
+
guides=guides,
|
|
441
|
+
) + theme_bw()
|
|
442
|
+
|
|
443
|
+
if interactive:
|
|
444
|
+
grid += ggtb(size_zoomin=-1)
|
|
445
|
+
|
|
446
|
+
return grid
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
|
-
from warnings import warn
|
|
5
4
|
|
|
6
5
|
from anndata import AnnData
|
|
7
6
|
|
|
7
|
+
from cellestial.util import _warn
|
|
8
8
|
from cellestial.util.errors import KeyNotFoundError, UnsupportedDataTypeError
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
@@ -124,13 +124,12 @@ def _extract_rank_genes_groups(
|
|
|
124
124
|
for gene, first, later in dropped[:3]
|
|
125
125
|
)
|
|
126
126
|
suffix = f" and {len(dropped) - 3} more" if len(dropped) > 3 else ""
|
|
127
|
-
|
|
127
|
+
_warn(
|
|
128
128
|
"Some genes ranked highly in multiple groups; "
|
|
129
129
|
"keeping the first occurrence and dropping the rest "
|
|
130
130
|
f"({examples}{suffix}). "
|
|
131
131
|
"Increase `n_genes`, restrict `groups`, "
|
|
132
|
-
"or pass an explicit `keys` mapping to control this."
|
|
133
|
-
stacklevel=2,
|
|
132
|
+
"or pass an explicit `keys` mapping to control this."
|
|
134
133
|
)
|
|
135
134
|
|
|
136
135
|
params = record["params"]
|
|
@@ -35,6 +35,7 @@ from cellestial.util import (
|
|
|
35
35
|
_get_dendrogram_path_frame,
|
|
36
36
|
_resolve_tooltips,
|
|
37
37
|
_validate_tooltips,
|
|
38
|
+
_warn,
|
|
38
39
|
)
|
|
39
40
|
from cellestial.util.errors import UnsupportedDataTypeError
|
|
40
41
|
|
|
@@ -49,7 +50,7 @@ def dotplot(
|
|
|
49
50
|
keys: Sequence[str] | Mapping[str, Sequence[str]] | None = None,
|
|
50
51
|
group_by: str | None = None,
|
|
51
52
|
*,
|
|
52
|
-
|
|
53
|
+
markers: bool | str = False,
|
|
53
54
|
n_genes: int = 5,
|
|
54
55
|
groups: Sequence[str] | None = None,
|
|
55
56
|
mapping: FeatureSpec | None = None,
|
|
@@ -90,22 +91,20 @@ def dotplot(
|
|
|
90
91
|
data : AnnData
|
|
91
92
|
The AnnData object of the single cell data.
|
|
92
93
|
keys : Sequence[str] | Mapping[str, Sequence[str]] | None, default=None
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
key cannot appear in more than one group. Must be ``None`` when
|
|
97
|
-
``rank_genes_groups`` is set.
|
|
94
|
+
Variable keys to include, placed on the x-axis. A mapping assigns
|
|
95
|
+
keys to group labels (no key in more than one group). Must be
|
|
96
|
+
``None`` when ``markers`` is set.
|
|
98
97
|
group_by : str | None, default=None
|
|
99
98
|
The key to group the data by. Inferred from a precomputed ranking
|
|
100
|
-
when ``
|
|
101
|
-
|
|
99
|
+
when ``markers`` is set.
|
|
100
|
+
markers : bool | str, default=False
|
|
102
101
|
Derive ``keys`` from a precomputed ranking. Pass ``True`` to use the
|
|
103
102
|
default ranking key, or a string to read a custom key (e.g.
|
|
104
103
|
``"rank_genes_groups_wilcoxon"``).
|
|
105
104
|
n_genes : int, default=5
|
|
106
|
-
Number of top genes to take per group when ``
|
|
105
|
+
Number of top genes to take per group when ``markers`` is set.
|
|
107
106
|
groups : Sequence[str] | None, default=None
|
|
108
|
-
Subset of groups to include when ``
|
|
107
|
+
Subset of groups to include when ``markers`` is set;
|
|
109
108
|
``None`` keeps all groups in their stored order.
|
|
110
109
|
mapping : FeatureSpec | None, default=None
|
|
111
110
|
Aesthetic mappings for the plot, the result of `aes()`.
|
|
@@ -187,12 +186,12 @@ def dotplot(
|
|
|
187
186
|
UnsupportedDataTypeError
|
|
188
187
|
If `data` is not a supported single-cell data object.
|
|
189
188
|
KeyNotFoundError
|
|
190
|
-
If `
|
|
189
|
+
If `markers` is enabled and the requested ranking result or
|
|
191
190
|
group is missing.
|
|
192
191
|
DuplicateKeysError
|
|
193
192
|
If a mapping passed to `keys` assigns the same key to multiple groups.
|
|
194
193
|
ValueError
|
|
195
|
-
If `keys` and `group_by` are missing while `
|
|
194
|
+
If `keys` and `group_by` are missing while `markers` is
|
|
196
195
|
disabled.
|
|
197
196
|
|
|
198
197
|
Examples
|
|
@@ -253,7 +252,7 @@ def dotplot(
|
|
|
253
252
|
.. jupyter-execute::
|
|
254
253
|
|
|
255
254
|
sc.tl.rank_genes_groups(data, groupby="cell_type_lvl1")
|
|
256
|
-
cl.dotplot(data,
|
|
255
|
+
cl.dotplot(data, markers=True, n_genes=5)
|
|
257
256
|
|
|
258
257
|
"""
|
|
259
258
|
# HANDLE: Data types
|
|
@@ -261,10 +260,10 @@ def dotplot(
|
|
|
261
260
|
msg = f"Unsupported data type: `{type(data)}`"
|
|
262
261
|
raise UnsupportedDataTypeError(msg)
|
|
263
262
|
|
|
264
|
-
if
|
|
263
|
+
if markers:
|
|
265
264
|
keys, group_by = _resolve_rank_genes_groups_args(
|
|
266
265
|
data,
|
|
267
|
-
rank_genes_groups=
|
|
266
|
+
rank_genes_groups=markers,
|
|
268
267
|
n_genes=n_genes,
|
|
269
268
|
groups=groups,
|
|
270
269
|
keys=keys,
|
|
@@ -273,7 +272,7 @@ def dotplot(
|
|
|
273
272
|
elif keys is None or group_by is None:
|
|
274
273
|
msg = (
|
|
275
274
|
"`keys` and `group_by` are required "
|
|
276
|
-
"(or enable `
|
|
275
|
+
"(or enable `markers` to derive them)."
|
|
277
276
|
)
|
|
278
277
|
raise ValueError(msg)
|
|
279
278
|
|
|
@@ -288,6 +287,15 @@ def dotplot(
|
|
|
288
287
|
axis=0,
|
|
289
288
|
variable_keys=keys,
|
|
290
289
|
)
|
|
290
|
+
# WARN: negative expression makes the percent-expressed (dot size) misleading
|
|
291
|
+
overall_min = frame.select(pl.min_horizontal(pl.col(keys).min())).item()
|
|
292
|
+
if overall_min is not None and overall_min < 0:
|
|
293
|
+
_warn(
|
|
294
|
+
"Expression matrix contains negative values, which suggests scaled data. "
|
|
295
|
+
"Dot size (percentage of cells above `threshold`, default 0) assumes "
|
|
296
|
+
"non-negative expression and may be misleading here; pass raw or "
|
|
297
|
+
"log-normalized expression, or set `threshold` explicitly."
|
|
298
|
+
)
|
|
291
299
|
# DROP: rows with null group_by to avoid null labels downstream
|
|
292
300
|
frame = frame.filter(pl.col(group_by).is_not_null())
|
|
293
301
|
index_columns = [x for x in frame.columns if x not in keys]
|