cellestial 0.44.0__tar.gz → 0.45.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {cellestial-0.44.0 → cellestial-0.45.0}/PKG-INFO +1 -1
  2. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/__init__.py +2 -0
  3. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/layers/utilities.py +3 -3
  4. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/__init__.py +2 -1
  5. cellestial-0.45.0/cellestial/single/differential/__init__.py +4 -0
  6. cellestial-0.45.0/cellestial/single/differential/markers.py +446 -0
  7. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/util/utilities.py +21 -9
  8. {cellestial-0.44.0 → cellestial-0.45.0}/pyproject.toml +2 -1
  9. cellestial-0.44.0/cellestial/single/differential/__init__.py +0 -3
  10. {cellestial-0.44.0 → cellestial-0.45.0}/LICENSE +0 -0
  11. {cellestial-0.44.0 → cellestial-0.45.0}/README.md +0 -0
  12. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/_version.py +0 -0
  13. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/datasets/__init__.py +0 -0
  14. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/datasets/datasets.py +0 -0
  15. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/frames/__init__.py +0 -0
  16. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/frames/build.py +0 -0
  17. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/frames/operations.py +0 -0
  18. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/layers/__init__.py +0 -0
  19. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/layers/_deferred.py +0 -0
  20. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/layers/arrow.py +0 -0
  21. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/layers/bracket.py +0 -0
  22. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/layers/ondata_legend.py +0 -0
  23. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/layers/outline.py +0 -0
  24. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/layers/stream.py +0 -0
  25. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/base/__init__.py +0 -0
  26. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/base/base.py +0 -0
  27. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/basic/__init__.py +0 -0
  28. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/basic/bar.py +0 -0
  29. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/basic/scatter.py +0 -0
  30. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/common/__init__.py +0 -0
  31. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/common/xyplot.py +0 -0
  32. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/common/xyplots.py +0 -0
  33. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/core/__init__.py +0 -0
  34. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/core/dimensional.py +0 -0
  35. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/core/distribution.py +0 -0
  36. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/core/distributions.py +0 -0
  37. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/core/subdimensional.py +0 -0
  38. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/core/subdimensionals.py +0 -0
  39. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/differential/volcano.py +0 -0
  40. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/heatmap/__init__.py +0 -0
  41. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/heatmap/_key_groups.py +0 -0
  42. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/heatmap/_rank_genes_groups.py +0 -0
  43. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/heatmap/dotplot.py +0 -0
  44. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/heatmap/heatmap.py +0 -0
  45. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/heatmap/stacked_violin.py +0 -0
  46. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/quick/__init__.py +0 -0
  47. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/quick/elbow.py +0 -0
  48. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/quick/highest.py +0 -0
  49. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/single/quick/ridge.py +0 -0
  50. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/spatial/__init__.py +0 -0
  51. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/spatial/_components.py +0 -0
  52. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/spatial/spatial.py +0 -0
  53. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/spatial/spatials.py +0 -0
  54. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/themes/__init__.py +0 -0
  55. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/themes/_distributions.py +0 -0
  56. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/themes/_heatmaps.py +0 -0
  57. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/themes/_scatters.py +0 -0
  58. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/util/__init__.py +0 -0
  59. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/util/colors.py +0 -0
  60. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/util/dendrogram.py +0 -0
  61. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/util/errors.py +0 -0
  62. {cellestial-0.44.0 → cellestial-0.45.0}/cellestial/util/operations.py +0 -0
  63. {cellestial-0.44.0 → cellestial-0.45.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.45.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",
@@ -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
@@ -253,9 +253,13 @@ def _share_labels(plot, i: int, keys: Sequence[str], ncol: int | None) -> SupPlo
253
253
  nrow = ceil(total / ncol)
254
254
  left_places = [i for i in range(total) if i % ncol == 0]
255
255
  bottom_places = [i for i in range(total) if i >= ncol * (nrow - 1)]
256
- if len(bottom_places) < ncol:
257
- penultimate_row = list(range((nrow - 2) * ncol, (nrow - 1) * ncol))
258
- bottom_places.extend(penultimate_row)
256
+ # the last grid row may be incomplete; for the columns it does not cover,
257
+ # the bottom-most plot lives in the penultimate row.
258
+ last_row_count = total - ncol * (nrow - 1)
259
+ if nrow >= 2 and last_row_count < ncol:
260
+ bottom_places.extend(
261
+ ncol * (nrow - 2) + col for col in range(last_row_count, ncol)
262
+ )
259
263
  if i not in bottom_places: # remove x axis title except for bottom row
260
264
  plot = plot + theme(axis_title_x=element_blank())
261
265
  if i not in left_places: # remove y axis title except for left column
@@ -273,9 +277,13 @@ def _share_axis(
273
277
  nrow = ceil(total / ncol)
274
278
  left_places = [i for i in range(total) if i % ncol == 0]
275
279
  bottom_places = [i for i in range(total) if i >= ncol * (nrow - 1)]
276
- if len(bottom_places) < ncol:
277
- penultimate_row = list(range((nrow - 2) * ncol, (nrow - 1) * ncol))
278
- bottom_places.extend(penultimate_row)
280
+ # the last grid row may be incomplete; for the columns it does not cover,
281
+ # the bottom-most plot lives in the penultimate row.
282
+ last_row_count = total - ncol * (nrow - 1)
283
+ if nrow >= 2 and last_row_count < ncol:
284
+ bottom_places.extend(
285
+ ncol * (nrow - 2) + col for col in range(last_row_count, ncol)
286
+ )
279
287
 
280
288
  if axis_type == "axis":
281
289
  if i not in bottom_places: # remove x axis title except for bottom row
@@ -308,9 +316,13 @@ def _share_ticks(plot, i: int, keys: Sequence[str], ncol: int | None) -> SupPlot
308
316
  nrow = ceil(total / ncol)
309
317
  left_places = [i for i in range(total) if i % ncol == 0]
310
318
  bottom_places = [i for i in range(total) if i >= ncol * (nrow - 1)]
311
- if len(bottom_places) < ncol:
312
- penultimate_row = list(range((nrow - 2) * ncol, (nrow - 1) * ncol))
313
- bottom_places.extend(penultimate_row)
319
+ # the last grid row may be incomplete; for the columns it does not cover,
320
+ # the bottom-most plot lives in the penultimate row.
321
+ last_row_count = total - ncol * (nrow - 1)
322
+ if nrow >= 2 and last_row_count < ncol:
323
+ bottom_places.extend(
324
+ ncol * (nrow - 2) + col for col in range(last_row_count, ncol)
325
+ )
314
326
  if i not in bottom_places: # remove x axis title except for bottom row
315
327
  plot = plot + theme(axis_text_x=element_blank())
316
328
  if i not in left_places: # remove y axis title except for left column
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cellestial"
3
- version = "0.44.0"
3
+ version = "0.45.0"
4
4
  description = ""
5
5
  authors = [
6
6
  { name = "Zaf4", email = "zafermolbio@gmail.com" },
@@ -40,6 +40,7 @@ pandas = "2.2.2" # pinned to avoid 3.0
40
40
  tqdm = "^4.67.3"
41
41
  pytest = "^9.0.3"
42
42
  pytest-cov = "^7.1.0"
43
+ nbstripout = "^0.9.1"
43
44
 
44
45
  [tool.poetry.group.docs.dependencies]
45
46
  sphinx = ">=8.0.0"
@@ -1,3 +0,0 @@
1
- from cellestial.single.differential.volcano import volcano, volcanos
2
-
3
- __all__ = ["volcano", "volcanos"]
File without changes
File without changes