cellestial 0.48.4__tar.gz → 0.49.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.48.4 → cellestial-0.49.0}/PKG-INFO +1 -1
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/datasets/datasets.py +1 -1
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/frames/build.py +23 -14
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/frames/operations.py +29 -14
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/layers/bracket.py +5 -6
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/layers/outline.py +1 -1
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/heatmap/stacked_violin.py +1 -2
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/util/dendrogram.py +1 -4
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/util/utilities.py +15 -29
- {cellestial-0.48.4 → cellestial-0.49.0}/pyproject.toml +1 -1
- {cellestial-0.48.4 → cellestial-0.49.0}/LICENSE +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/README.md +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/_version.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/datasets/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/frames/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/layers/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/layers/_deferred.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/layers/arrow.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/layers/ondata_legend.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/layers/stream.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/layers/utilities.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/base/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/base/base.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/basic/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/basic/bar.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/basic/scatter.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/common/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/common/xyplot.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/common/xyplots.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/core/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/core/dimensional.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/core/distribution.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/core/distributions.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/core/subdimensional.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/core/subdimensionals.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/differential/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/differential/markers.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/differential/volcano.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/heatmap/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/heatmap/_key_groups.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/heatmap/_rank_genes_groups.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/heatmap/dotplot.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/heatmap/heatmap.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/quick/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/quick/elbow.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/quick/highest.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/single/quick/ridge.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/spatial/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/spatial/_components.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/spatial/spatial.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/spatial/spatials.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/themes/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/themes/_distributions.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/themes/_heatmaps.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/themes/_scatters.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/util/__init__.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/util/colors.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/util/errors.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/util/operations.py +0 -0
- {cellestial-0.48.4 → cellestial-0.49.0}/cellestial/util/save.py +0 -0
|
@@ -487,7 +487,7 @@ def from_url(
|
|
|
487
487
|
print(f"No cache at {cache_file} !")
|
|
488
488
|
print(f"Downloading from {url} ...")
|
|
489
489
|
|
|
490
|
-
block_size = 1024
|
|
490
|
+
block_size = 1024 * 1024
|
|
491
491
|
# Stream to a .part file and atomically rename on success, so an interrupted
|
|
492
492
|
# download cannot poison the cache.
|
|
493
493
|
partial_file = cache_file.with_name(cache_file.name + ".part")
|
|
@@ -2,9 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING, Literal
|
|
4
4
|
|
|
5
|
+
import numpy as np
|
|
5
6
|
import pandas as pd
|
|
6
7
|
import polars as pl
|
|
7
8
|
from anndata import AnnData
|
|
9
|
+
from scipy.sparse import issparse
|
|
8
10
|
from spatialdata import SpatialData
|
|
9
11
|
|
|
10
12
|
from cellestial.util.errors import UnsupportedDataTypeError, VariableNotFoundError
|
|
@@ -29,17 +31,23 @@ def anndata_variable_columns(
|
|
|
29
31
|
columns = []
|
|
30
32
|
if isinstance(keys, str):
|
|
31
33
|
keys = [keys]
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
# remove keys that are already in column_names to avoid repeats
|
|
35
|
+
keys = [key for key in keys if key not in column_names]
|
|
36
|
+
# check that all keys are in data.var_names
|
|
37
|
+
available_names = set(data.var_names)
|
|
38
|
+
missing = [key for key in keys if key not in available_names]
|
|
39
|
+
if missing:
|
|
40
|
+
msg = f"Keys not found in data.var_names: {missing}"
|
|
41
|
+
raise VariableNotFoundError(msg)
|
|
42
|
+
|
|
43
|
+
# subset X
|
|
44
|
+
X = data[:, keys].X
|
|
45
|
+
|
|
46
|
+
if issparse(X):
|
|
47
|
+
X = X.toarray() # ty:ignore[unresolved-attribute]
|
|
48
|
+
else:
|
|
49
|
+
X = np.asarray(X)
|
|
50
|
+
columns = [pl.Series(key, X[:, i]) for i, key in enumerate(keys)]
|
|
43
51
|
|
|
44
52
|
return columns
|
|
45
53
|
|
|
@@ -210,7 +218,7 @@ def anndata_variables_frame(
|
|
|
210
218
|
|
|
211
219
|
|
|
212
220
|
def build_frame(
|
|
213
|
-
data: AnnData,
|
|
221
|
+
data: AnnData | SpatialData,
|
|
214
222
|
*,
|
|
215
223
|
variable_keys: str | Sequence[str] | None = None,
|
|
216
224
|
axis: Literal[0, 1] | None = None,
|
|
@@ -223,8 +231,8 @@ def build_frame(
|
|
|
223
231
|
|
|
224
232
|
Parameters
|
|
225
233
|
----------
|
|
226
|
-
data : AnnData
|
|
227
|
-
The AnnData object containing the variables.
|
|
234
|
+
data : AnnData | SpatialData
|
|
235
|
+
The AnnData or SpatialData object containing the variables.
|
|
228
236
|
variable_keys : str | Sequence[str] | None
|
|
229
237
|
Variable keys to add to the DataFrame. If None, no additional keys are added.
|
|
230
238
|
axis : {0,1} | None
|
|
@@ -282,6 +290,7 @@ def build_frame(
|
|
|
282
290
|
if isinstance(data, SpatialData):
|
|
283
291
|
tables = list(data.tables.keys())
|
|
284
292
|
if len(tables) == 1:
|
|
293
|
+
# retrieve an anndata
|
|
285
294
|
data = data.tables[tables[0]]
|
|
286
295
|
elif len(tables) == 0:
|
|
287
296
|
msg = "No annotation tables found in the SpatialData object."
|
|
@@ -6,7 +6,6 @@ from anndata import AnnData
|
|
|
6
6
|
from polars import DataFrame
|
|
7
7
|
from scipy.sparse import issparse
|
|
8
8
|
|
|
9
|
-
from cellestial.frames.build import anndata_variable_columns
|
|
10
9
|
from cellestial.util.errors import UnsupportedDataTypeError
|
|
11
10
|
|
|
12
11
|
|
|
@@ -16,28 +15,44 @@ def _highest_expressed_genes_frame(
|
|
|
16
15
|
) -> DataFrame:
|
|
17
16
|
"""Get the top n highest expressed genes by mean percentage across all cells."""
|
|
18
17
|
if isinstance(data, AnnData):
|
|
18
|
+
# VALIDATE: inputs
|
|
19
19
|
if n > data.n_vars:
|
|
20
20
|
msg = f"Requested n={n} genes, but only {data.n_vars} genes available in data."
|
|
21
21
|
raise ValueError(msg)
|
|
22
22
|
X = data.X
|
|
23
|
-
#
|
|
23
|
+
# matmul and fancy indexing below require an in-memory ndarray/sparse;
|
|
24
|
+
# fail fast on backed/dask/zappy rather than crashing deep in the math.
|
|
25
|
+
if not (isinstance(X, np.ndarray) or issparse(X)):
|
|
26
|
+
msg = (
|
|
27
|
+
f"Unsupported `X` type for this operation: {type(X).__name__}. "
|
|
28
|
+
"Load AnnData in non-backed mode or materialize `X` manually."
|
|
29
|
+
)
|
|
30
|
+
raise UnsupportedDataTypeError(msg)
|
|
31
|
+
|
|
32
|
+
# BUILD: per-cell normalizer so each cell's expression sums to 100
|
|
24
33
|
if issparse(X):
|
|
25
|
-
row_sums = np.
|
|
26
|
-
row_sums[row_sums == 0] = 1
|
|
27
|
-
X_normalized = X.multiply(100 / row_sums[:, np.newaxis])
|
|
28
|
-
mean_percent = np.array(X_normalized.mean(axis=0)).ravel()
|
|
34
|
+
row_sums = np.asarray(X.sum(axis=1)).ravel()
|
|
29
35
|
else:
|
|
30
|
-
row_sums = X.sum(axis=1
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
mean_percent = X_normalized.mean(axis=0)
|
|
36
|
+
row_sums = X.sum(axis=1)
|
|
37
|
+
row_sums[row_sums == 0] = 1 # guard cells with zero total to avoid div-by-zero
|
|
38
|
+
inv_rows = 100.0 / row_sums
|
|
34
39
|
|
|
40
|
+
# RANK: gene means via a single matvec instead of materializing the
|
|
41
|
+
# full normalized matrix. Identity: mean(X / row_sums * 100, axis=0)
|
|
42
|
+
# equals (inv_rows @ X) / n_cells, but uses O(n_vars) memory instead of O(n_obs * n_vars).
|
|
43
|
+
mean_percent = np.asarray(inv_rows @ X).ravel() / X.shape[0]
|
|
35
44
|
top_idx = np.argsort(mean_percent)[::-1][:n]
|
|
36
45
|
genes = data.var_names[top_idx].tolist()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
|
|
47
|
+
# EXTRACT: normalize only the top n columns (small n_obs * n slice),
|
|
48
|
+
# skipping the full n_obs * n_vars materialization the naive path would do.
|
|
49
|
+
X_top = X[:, top_idx]
|
|
50
|
+
if issparse(X_top):
|
|
51
|
+
X_top = X_top.toarray()
|
|
52
|
+
X_top_normalized = X_top * inv_rows[:, np.newaxis]
|
|
53
|
+
|
|
54
|
+
# BUILD: polars frame keyed by gene name
|
|
55
|
+
frame = pl.DataFrame({gene: X_top_normalized[:, i] for i, gene in enumerate(genes)})
|
|
41
56
|
else:
|
|
42
57
|
msg = f"Unsupported data type: `{type(data)}`"
|
|
43
58
|
raise UnsupportedDataTypeError(msg)
|
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Literal
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import polars as pl
|
|
8
8
|
from lets_plot import aes, geom_bracket
|
|
9
|
+
from scipy.stats import mannwhitneyu, ttest_ind
|
|
9
10
|
|
|
10
11
|
from cellestial.layers._deferred import DeferredLayer
|
|
11
12
|
from cellestial.util import get_mapping, retrieve
|
|
@@ -134,8 +135,6 @@ def _compute_bracket_frame(
|
|
|
134
135
|
y_padding: float,
|
|
135
136
|
) -> DataFrame:
|
|
136
137
|
"""Build a `geom_bracket` DataFrame from pairwise significance tests."""
|
|
137
|
-
from scipy.stats import mannwhitneyu, ttest_ind
|
|
138
|
-
|
|
139
138
|
# determine which pairs to compare
|
|
140
139
|
groups = frame[x].drop_nulls().unique(maintain_order=True).to_list()
|
|
141
140
|
if comparisons is None:
|
|
@@ -238,10 +237,10 @@ def _compute_bracket_frame(
|
|
|
238
237
|
brackets = brackets.with_columns(label=pl.Series(labels))
|
|
239
238
|
|
|
240
239
|
# compute y positions so brackets stack above the data without overlapping
|
|
241
|
-
|
|
242
|
-
data_min =
|
|
243
|
-
data_max =
|
|
244
|
-
data_range = data_max - data_min
|
|
240
|
+
|
|
241
|
+
data_min = frame[y].min()
|
|
242
|
+
data_max = frame[y].max()
|
|
243
|
+
data_range = data_max - data_min # ty:ignore[unsupported-operator]
|
|
245
244
|
_y_position = data_max + data_range * y_padding if y_position is None else y_position
|
|
246
245
|
_y_step = data_range * y_padding if y_step is None else y_step
|
|
247
246
|
positions = [_y_position + _y_step * index for index in range(len(brackets))]
|
|
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Literal
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
import polars as pl
|
|
7
7
|
from lets_plot import aes, geom_path
|
|
8
|
+
from scipy.stats import gaussian_kde
|
|
8
9
|
|
|
9
10
|
from cellestial.layers._deferred import DeferredLayer
|
|
10
11
|
from cellestial.util import get_mapping
|
|
@@ -28,7 +29,6 @@ def _get_density_boundaries(
|
|
|
28
29
|
grid_size: int = 200,
|
|
29
30
|
) -> DataFrame:
|
|
30
31
|
"""Creates and Returns a DataFrame to encircle the cluster via `geom_path`."""
|
|
31
|
-
from scipy.stats import gaussian_kde
|
|
32
32
|
from skimage import measure
|
|
33
33
|
|
|
34
34
|
# 1. Normalize input to a Sequence of Sequences: List[List[str]]
|
|
@@ -18,6 +18,7 @@ from lets_plot import (
|
|
|
18
18
|
theme,
|
|
19
19
|
)
|
|
20
20
|
from lets_plot.plot.core import FeatureSpec
|
|
21
|
+
from scipy.stats import gaussian_kde
|
|
21
22
|
|
|
22
23
|
from cellestial.frames import build_frame
|
|
23
24
|
from cellestial.single.heatmap._key_groups import (
|
|
@@ -65,8 +66,6 @@ def _compute_violin_polygons(
|
|
|
65
66
|
mapped to local y inside the row band and density mapped to local x around
|
|
66
67
|
the column position.
|
|
67
68
|
"""
|
|
68
|
-
from scipy.stats import gaussian_kde
|
|
69
|
-
|
|
70
69
|
var_stats = frame.group_by(variable_column).agg(
|
|
71
70
|
pl.col(value_column).min().alias("_vmin"),
|
|
72
71
|
pl.col(value_column).max().alias("_vmax"),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import numpy as np
|
|
3
4
|
import polars as pl
|
|
4
5
|
from anndata import AnnData
|
|
5
6
|
|
|
@@ -11,8 +12,6 @@ def _get_dendrogram(
|
|
|
11
12
|
use_key: str | None = None,
|
|
12
13
|
) -> tuple[list[str], pl.DataFrame]:
|
|
13
14
|
"""Get or compute the dendrogram for `group_by` and extract paths."""
|
|
14
|
-
import numpy as np
|
|
15
|
-
|
|
16
15
|
if isinstance(data, AnnData):
|
|
17
16
|
key = use_key if use_key is not None else f"dendrogram_{group_by}"
|
|
18
17
|
if key not in data.uns:
|
|
@@ -57,8 +56,6 @@ def _get_dendrogram_path_frame(
|
|
|
57
56
|
dendrogram_ratio: float = 0.15,
|
|
58
57
|
) -> pl.DataFrame:
|
|
59
58
|
"""Map normalized dendrogram paths into plot coordinates (right side along y-axis)."""
|
|
60
|
-
import numpy as np
|
|
61
|
-
|
|
62
59
|
y_dendrogram_size = n_x * dendrogram_ratio
|
|
63
60
|
leaf_xp = np.arange(n_groups, dtype=float)
|
|
64
61
|
plot_y = np.interp(paths["x"].to_numpy(), leaf_xp, group_centers)
|
|
@@ -4,6 +4,7 @@ import inspect
|
|
|
4
4
|
import re
|
|
5
5
|
import warnings
|
|
6
6
|
from collections.abc import Sequence
|
|
7
|
+
from functools import lru_cache
|
|
7
8
|
from math import ceil, log10
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Literal
|
|
@@ -286,19 +287,24 @@ def _fill_gradient(
|
|
|
286
287
|
midpoint=mid_value,
|
|
287
288
|
)
|
|
288
289
|
|
|
289
|
-
|
|
290
|
-
|
|
290
|
+
# cache to avoid recomputing for every plot in a grid...
|
|
291
|
+
@lru_cache(maxsize=32)
|
|
292
|
+
def _grid_places(total: int, ncol: int | None) -> tuple[frozenset[int], frozenset[int]]:
|
|
291
293
|
if ncol is None:
|
|
292
|
-
ncol =
|
|
293
|
-
total = len(keys)
|
|
294
|
+
ncol = total
|
|
294
295
|
nrow = ceil(total / ncol)
|
|
295
|
-
left_places =
|
|
296
|
-
bottom_places =
|
|
296
|
+
left_places = {idx for idx in range(total) if idx % ncol == 0}
|
|
297
|
+
bottom_places = {idx for idx in range(total) if idx >= ncol * (nrow - 1)}
|
|
297
298
|
# the last grid row may be incomplete; for the columns it does not cover,
|
|
298
299
|
# the bottom-most plot lives in the penultimate row.
|
|
299
300
|
last_row_count = total - ncol * (nrow - 1)
|
|
300
301
|
if nrow >= 2 and last_row_count < ncol:
|
|
301
|
-
bottom_places.
|
|
302
|
+
bottom_places.update(ncol * (nrow - 2) + col for col in range(last_row_count, ncol))
|
|
303
|
+
return frozenset(left_places), frozenset(bottom_places)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _share_labels(plot, i: int, keys: Sequence[str], ncol: int | None) -> SupPlotsSpec:
|
|
307
|
+
left_places, bottom_places = _grid_places(len(keys), ncol)
|
|
302
308
|
if i not in bottom_places: # remove x axis title except for bottom row
|
|
303
309
|
plot = plot + theme(axis_title_x=element_blank())
|
|
304
310
|
if i not in left_places: # remove y axis title except for left column
|
|
@@ -310,17 +316,7 @@ def _share_labels(plot, i: int, keys: Sequence[str], ncol: int | None) -> SupPlo
|
|
|
310
316
|
def _share_axis(
|
|
311
317
|
plot, i: int, keys: Sequence[str], ncol: int | None, axis_type: Literal["axis", "arrow"]
|
|
312
318
|
) -> SupPlotsSpec:
|
|
313
|
-
|
|
314
|
-
if ncol is None:
|
|
315
|
-
ncol = len(keys)
|
|
316
|
-
nrow = ceil(total / ncol)
|
|
317
|
-
left_places = [i for i in range(total) if i % ncol == 0]
|
|
318
|
-
bottom_places = [i for i in range(total) if i >= ncol * (nrow - 1)]
|
|
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(ncol * (nrow - 2) + col for col in range(last_row_count, ncol))
|
|
319
|
+
left_places, bottom_places = _grid_places(len(keys), ncol)
|
|
324
320
|
|
|
325
321
|
if axis_type == "axis":
|
|
326
322
|
if i not in bottom_places: # remove x axis title except for bottom row
|
|
@@ -347,17 +343,7 @@ def _share_axis(
|
|
|
347
343
|
|
|
348
344
|
|
|
349
345
|
def _share_ticks(plot, i: int, keys: Sequence[str], ncol: int | None) -> SupPlotsSpec:
|
|
350
|
-
|
|
351
|
-
ncol = len(keys)
|
|
352
|
-
total = len(keys)
|
|
353
|
-
nrow = ceil(total / ncol)
|
|
354
|
-
left_places = [i for i in range(total) if i % ncol == 0]
|
|
355
|
-
bottom_places = [i for i in range(total) if i >= ncol * (nrow - 1)]
|
|
356
|
-
# the last grid row may be incomplete; for the columns it does not cover,
|
|
357
|
-
# the bottom-most plot lives in the penultimate row.
|
|
358
|
-
last_row_count = total - ncol * (nrow - 1)
|
|
359
|
-
if nrow >= 2 and last_row_count < ncol:
|
|
360
|
-
bottom_places.extend(ncol * (nrow - 2) + col for col in range(last_row_count, ncol))
|
|
346
|
+
left_places, bottom_places = _grid_places(len(keys), ncol)
|
|
361
347
|
if i not in bottom_places: # remove x axis title except for bottom row
|
|
362
348
|
plot = plot + theme(axis_text_x=element_blank())
|
|
363
349
|
if i not in left_places: # remove y axis title except for left column
|
|
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
|
|
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
|