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.
Files changed (63) hide show
  1. {cellestial-0.44.0 → cellestial-0.46.0}/PKG-INFO +1 -1
  2. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/__init__.py +2 -0
  3. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/utilities.py +3 -3
  4. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/__init__.py +2 -1
  5. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/core/dimensional.py +2 -2
  6. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/core/distribution.py +3 -4
  7. cellestial-0.46.0/cellestial/single/differential/__init__.py +4 -0
  8. cellestial-0.46.0/cellestial/single/differential/markers.py +446 -0
  9. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/heatmap/_rank_genes_groups.py +3 -4
  10. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/heatmap/dotplot.py +24 -16
  11. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/heatmap/heatmap.py +33 -32
  12. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/heatmap/stacked_violin.py +14 -16
  13. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/spatial/spatial.py +3 -3
  14. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/__init__.py +2 -0
  15. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/errors.py +4 -0
  16. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/utilities.py +62 -9
  17. {cellestial-0.44.0 → cellestial-0.46.0}/pyproject.toml +2 -1
  18. cellestial-0.44.0/cellestial/single/differential/__init__.py +0 -3
  19. {cellestial-0.44.0 → cellestial-0.46.0}/LICENSE +0 -0
  20. {cellestial-0.44.0 → cellestial-0.46.0}/README.md +0 -0
  21. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/_version.py +0 -0
  22. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/datasets/__init__.py +0 -0
  23. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/datasets/datasets.py +0 -0
  24. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/frames/__init__.py +0 -0
  25. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/frames/build.py +0 -0
  26. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/frames/operations.py +0 -0
  27. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/__init__.py +0 -0
  28. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/_deferred.py +0 -0
  29. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/arrow.py +0 -0
  30. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/bracket.py +0 -0
  31. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/ondata_legend.py +0 -0
  32. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/outline.py +0 -0
  33. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/layers/stream.py +0 -0
  34. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/base/__init__.py +0 -0
  35. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/base/base.py +0 -0
  36. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/basic/__init__.py +0 -0
  37. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/basic/bar.py +0 -0
  38. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/basic/scatter.py +0 -0
  39. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/common/__init__.py +0 -0
  40. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/common/xyplot.py +0 -0
  41. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/common/xyplots.py +0 -0
  42. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/core/__init__.py +0 -0
  43. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/core/distributions.py +0 -0
  44. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/core/subdimensional.py +0 -0
  45. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/core/subdimensionals.py +0 -0
  46. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/differential/volcano.py +0 -0
  47. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/heatmap/__init__.py +0 -0
  48. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/heatmap/_key_groups.py +0 -0
  49. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/quick/__init__.py +0 -0
  50. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/quick/elbow.py +0 -0
  51. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/quick/highest.py +0 -0
  52. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/single/quick/ridge.py +0 -0
  53. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/spatial/__init__.py +0 -0
  54. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/spatial/_components.py +0 -0
  55. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/spatial/spatials.py +0 -0
  56. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/themes/__init__.py +0 -0
  57. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/themes/_distributions.py +0 -0
  58. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/themes/_heatmaps.py +0 -0
  59. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/themes/_scatters.py +0 -0
  60. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/colors.py +0 -0
  61. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/dendrogram.py +0 -0
  62. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/operations.py +0 -0
  63. {cellestial-0.44.0 → cellestial-0.46.0}/cellestial/util/save.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cellestial
3
- Version: 0.44.0
3
+ Version: 0.46.0
4
4
  Summary:
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -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="green", size=panel_size),
22
- legend_background=element_rect(color="blue", size=legend_size),
23
- plot_background=element_rect(color="red", size=plot_size),
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
- warnings.warn(msg, stacklevel=1)
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
- warn(
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,4 @@
1
+ from cellestial.single.differential.markers import markers
2
+ from cellestial.single.differential.volcano import volcano, volcanos
3
+
4
+ __all__ = ["markers", "volcano", "volcanos"]
@@ -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
- warn(
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
- rank_genes_groups: bool | str = False,
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
- The variable keys to include in the dotplot. When a mapping is
94
- provided, each entry maps a group label to the keys belonging to that
95
- group; the keys are placed on the x-axis in mapping order. The same
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 ``rank_genes_groups`` is set.
101
- rank_genes_groups : bool | str, default=False
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 ``rank_genes_groups`` is set.
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 ``rank_genes_groups`` is set;
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 `rank_genes_groups` is enabled and the requested ranking result or
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 `rank_genes_groups` is
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, rank_genes_groups=True, n_genes=5)
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 rank_genes_groups:
263
+ if markers:
265
264
  keys, group_by = _resolve_rank_genes_groups_args(
266
265
  data,
267
- rank_genes_groups=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 `rank_genes_groups` to derive them)."
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]