From 4a70173a613b5f2fd9422f8accdc2c8a0d777eba Mon Sep 17 00:00:00 2001 From: Ilia Kats Date: Fri, 15 Aug 2025 13:33:05 +0200 Subject: [PATCH 1/5] .pl.embedding: use AnnData functionality to handle .raw subsetting That way we can rely on AnnData for validation and error handling. Closes #162. --- muon/_core/plot.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/muon/_core/plot.py b/muon/_core/plot.py index 52be000..b072bf1 100644 --- a/muon/_core/plot.py +++ b/muon/_core/plot.py @@ -134,6 +134,9 @@ def embedding( data, basis=basis, color=color, use_raw=use_raw, layer=layer, **kwargs ) + if use_raw and layer is not None: + raise ValueError("use_raw cannot be True when a layer is specified.") + # `data` is MuData if basis not in data.obsm and "X_" + basis in data.obsm: basis = "X_" + basis @@ -145,8 +148,8 @@ def embedding( # basis is not a joint embedding try: mod, basis_mod = basis.split(":") - except ValueError: - raise ValueError(f"Basis {basis} is not present in the MuData object (.obsm)") + except ValueError as e: + raise ValueError(f"Basis {basis} is not present in the MuData object (.obsm)") from e if mod not in data.mod: raise ValueError( @@ -216,10 +219,10 @@ def embedding( if use_raw is None or use_raw: if data.mod[m].raw is not None: - keysidx = data.mod[m].raw.var.index.get_indexer_for(mod_keys) + subset = data.mod[m].raw[:, mod_keys] fmod_adata = AnnData( - X=data.mod[m].raw.X[:, keysidx], - var=pd.DataFrame(index=mod_keys), + X=subset.X, + var=subset.var, obs=data.mod[m].obs, ) else: From d37beca9310bc2eb8bb7293fe2908dcbd72f6ca9 Mon Sep 17 00:00:00 2001 From: Ilia Kats Date: Fri, 15 Aug 2025 14:58:46 +0200 Subject: [PATCH 2/5] .pl.embedding: simplify raw handling and add gene_symbols argument use_raw is now a non-optional boolean. This is consistent with the scanpy API, simplifies our logic a lot and makes the output more predictable. Also implement a gene_symbols argument that specifies a column in .var to use as index (closes #124) --- muon/_core/plot.py | 93 +++++++++++++++------------------------------- 1 file changed, 30 insertions(+), 63 deletions(-) diff --git a/muon/_core/plot.py b/muon/_core/plot.py index b072bf1..cecdba5 100644 --- a/muon/_core/plot.py +++ b/muon/_core/plot.py @@ -99,8 +99,9 @@ def embedding( data: Union[AnnData, MuData], basis: str, color: Optional[Union[str, Sequence[str]]] = None, - use_raw: Optional[bool] = None, + use_raw: bool = False, layer: Optional[str] = None, + gene_symbols: Optional[str] = None, **kwargs, ): """ @@ -114,24 +115,30 @@ def embedding( Parameters ---------- - data : Union[AnnData, MuData] + data MuData or AnnData object - basis : str + basis Name of the `obsm` basis to use - color : Optional[Union[str, typing.Sequence[str]]], optional (default: None) + color Keys for variables or annotations of observations (.obs columns). Can be from any modality. - use_raw : Optional[bool], optional (default: None) + use_raw Use `.raw` attribute of the modality where a feature (from `color`) is derived from. - If `None`, defaults to `True` if `.raw` is present and a valid `layer` is not provided. - layer : Optional[str], optional (default: None) + layer Name of the layer in the modality where a feature (from `color`) is derived from. - No layer is used by default. If a valid `layer` is provided, this takes precedence - over `use_raw=True`. + No layer is used by default. + gene_symbols + Column of `.var` to search for `color` in. """ if isinstance(data, AnnData): return sc.pl.embedding( - data, basis=basis, color=color, use_raw=use_raw, layer=layer, **kwargs + data, + basis=basis, + color=color, + use_raw=use_raw, + layer=layer, + gene_symbols=gene_symbols, + **kwargs, ) if use_raw and layer is not None: @@ -183,77 +190,37 @@ def embedding( else: raise TypeError("Expected color to be a string or an iterable.") + varidx = {} + for modname, mod in data.mod.items(): + var = mod.var if not use_raw else mod.raw.var + varidx[modname] = var.index if gene_symbols is None else pd.Index(var[gene_symbols]) + # Fetch respective features if not all([key in obs for key in keys]): # {'rna': [True, False], 'prot': [False, True]} - keys_in_mod = {m: [key in data.mod[m].var_names for key in keys] for m in data.mod} - - # .raw slots might have exclusive var_names - if use_raw is None or use_raw: - for i, k in enumerate(keys): - for m in data.mod: - if keys_in_mod[m][i] == False and data.mod[m].raw is not None: - keys_in_mod[m][i] = k in data.mod[m].raw.var_names + keys_in_mod = {m: [key in varidx[m] for key in keys] for m in data.mod} # e.g. color="rna:CD8A" - especially relevant for mdata.axis == -1 mod_key_modifier: dict[str, str] = dict() for i, k in enumerate(keys): mod_key_modifier[k] = k - for m in data.mod: + for m, mod in data.mod.items(): if not keys_in_mod[m][i]: k_clean = k if k.startswith(f"{m}:"): k_clean = k.split(":", 1)[1] - keys_in_mod[m][i] = k_clean in data.mod[m].var_names + keys_in_mod[m][i] = k_clean in varidx[m] if keys_in_mod[m][i]: mod_key_modifier[k] = k_clean - if use_raw is None or use_raw: - if keys_in_mod[m][i] == False and data.mod[m].raw is not None: - keys_in_mod[m][i] = k_clean in data.mod[m].raw.var_names - for m in data.mod: + for m, mod in data.mod.items(): if np.sum(keys_in_mod[m]) > 0: - mod_keys = np.array(keys)[keys_in_mod[m]] - mod_keys = np.array([mod_key_modifier[k] for k in mod_keys]) - - if use_raw is None or use_raw: - if data.mod[m].raw is not None: - subset = data.mod[m].raw[:, mod_keys] - fmod_adata = AnnData( - X=subset.X, - var=subset.var, - obs=data.mod[m].obs, - ) - else: - if use_raw: - warnings.warn( - f"Attibute .raw is None for the modality {m}, using .X instead" - ) - fmod_adata = data.mod[m][:, mod_keys] - else: - fmod_adata = data.mod[m][:, mod_keys] - - if layer is not None: - if isinstance(layer, Dict): - m_layer = layer.get(m, None) - if m_layer is not None: - x = data.mod[m][:, mod_keys].layers[m_layer] - fmod_adata.X = x.todense() if issparse(x) else x - if use_raw: - warnings.warn(f"Layer='{layer}' superseded use_raw={use_raw}") - elif layer in data.mod[m].layers: - x = data.mod[m][:, mod_keys].layers[layer] - fmod_adata.X = x.todense() if issparse(x) else x - if use_raw: - warnings.warn(f"Layer='{layer}' superseded use_raw={use_raw}") - else: - warnings.warn( - f"Layer {layer} is not present for the modality {m}, using count matrix instead" - ) - x = fmod_adata.X.toarray() if issparse(fmod_adata.X) else fmod_adata.X + mod_keys = [mod_key_modifier[k] for i, k in enumerate(keys) if keys_in_mod[m][i]] obs = obs.join( - pd.DataFrame(x, columns=mod_keys, index=fmod_adata.obs_names), + sc.get.obs_df( + mod, keys=mod_keys, layer=layer, use_raw=use_raw, gene_symbols=gene_symbols + ), how="left", ) From c4163d179f83c1d4ddf0b88e8d562f37e7185107 Mon Sep 17 00:00:00 2001 From: Ilia Kats Date: Fri, 15 Aug 2025 15:29:56 +0200 Subject: [PATCH 3/5] .pl.embedding: allow setting use_raw, layer, gene_symbols per modality --- muon/_core/plot.py | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/muon/_core/plot.py b/muon/_core/plot.py index cecdba5..a8258fa 100644 --- a/muon/_core/plot.py +++ b/muon/_core/plot.py @@ -1,4 +1,5 @@ -from typing import Dict, Iterable, List, Optional, Sequence, Union +from collections import defaultdict +from typing import Dict, Iterable, List, Optional, Sequence, Union, Mapping import warnings from matplotlib.axes import Axes @@ -96,12 +97,12 @@ def scatter( def embedding( - data: Union[AnnData, MuData], + data: AnnData | MuData, basis: str, - color: Optional[Union[str, Sequence[str]]] = None, - use_raw: bool = False, - layer: Optional[str] = None, - gene_symbols: Optional[str] = None, + color: str | Sequence[str] | None = None, + use_raw: bool | Mapping[str, bool] = False, + layer: str | Mapping[str, str | None] | None = None, + gene_symbols: str | Mapping[str, str | None] | None = None, **kwargs, ): """ @@ -124,11 +125,13 @@ def embedding( Can be from any modality. use_raw Use `.raw` attribute of the modality where a feature (from `color`) is derived from. + If a dictionary is given, it must have one entry for each modality. layer Name of the layer in the modality where a feature (from `color`) is derived from. - No layer is used by default. + If a dictionary is given, it must have one entry for each modality. gene_symbols Column of `.var` to search for `color` in. + If a dictionary is given, it must have one entry for each modality. """ if isinstance(data, AnnData): return sc.pl.embedding( @@ -141,9 +144,6 @@ def embedding( **kwargs, ) - if use_raw and layer is not None: - raise ValueError("use_raw cannot be True when a layer is specified.") - # `data` is MuData if basis not in data.obsm and "X_" + basis in data.obsm: basis = "X_" + basis @@ -178,6 +178,16 @@ def embedding( obs = data.obs.loc[adata.obs.index.values] + if not isinstance(use_raw, Mapping): + use_rawd = use_raw + use_raw = defaultdict(lambda: use_rawd) + if not isinstance(layer, Mapping): + layerd = layer + layer = defaultdict(lambda: layerd) + if not isinstance(gene_symbols, Mapping): + gene_symbolsd = gene_symbols + gene_symbols = defaultdict(lambda: gene_symbolsd) + if color is None: ad = AnnData(obs=obs, obsm=adata.obsm, obsp=adata.obsp) return sc.pl.embedding(ad, basis=basis_mod, **kwargs) @@ -191,9 +201,12 @@ def embedding( raise TypeError("Expected color to be a string or an iterable.") varidx = {} - for modname, mod in data.mod.items(): - var = mod.var if not use_raw else mod.raw.var - varidx[modname] = var.index if gene_symbols is None else pd.Index(var[gene_symbols]) + for m, mod in data.mod.items(): + if layer[m] is not None and use_raw[m]: + raise ValueError("use_raw cannot be True when a layer is specified.") + + var = mod.var if not use_raw[m] else mod.raw.var + varidx[m] = var.index if gene_symbols[m] is None else pd.Index(var[gene_symbols[m]]) # Fetch respective features if not all([key in obs for key in keys]): @@ -219,7 +232,11 @@ def embedding( mod_keys = [mod_key_modifier[k] for i, k in enumerate(keys) if keys_in_mod[m][i]] obs = obs.join( sc.get.obs_df( - mod, keys=mod_keys, layer=layer, use_raw=use_raw, gene_symbols=gene_symbols + mod, + keys=mod_keys, + layer=layer[m], + use_raw=use_raw[m], + gene_symbols=gene_symbols[m], ), how="left", ) From b8e22016f1a10dd065e2d82b3a1431c6d4a37f60 Mon Sep 17 00:00:00 2001 From: Ilia Kats Date: Fri, 15 Aug 2025 16:28:05 +0200 Subject: [PATCH 4/5] .pl.scatter: make API consistent with .pl.embedding use_raw cannot be None anymore. This is consistent with the scanpy API and leads to more predictable results. Also add a gene_symbols argument. use_raw, layers, and gene_symbols now support being passed dictionaries specifiying different values for different modalities. --- muon/_core/plot.py | 63 ++++++++++++------ muon/_core/utils.py | 155 +++++++++++++++++++++++++------------------- 2 files changed, 131 insertions(+), 87 deletions(-) diff --git a/muon/_core/plot.py b/muon/_core/plot.py index a8258fa..f3738cd 100644 --- a/muon/_core/plot.py +++ b/muon/_core/plot.py @@ -20,12 +20,17 @@ def scatter( - data: Union[AnnData, MuData], - x: Optional[str] = None, - y: Optional[str] = None, - color: Optional[Union[str, Sequence[str]]] = None, - use_raw: Optional[bool] = None, - layers: Optional[Union[str, Sequence[str]]] = None, + data: AnnData | MuData, + x: str | None = None, + y: str | None = None, + color: str | Sequence[str] | None = None, + use_raw: bool | Mapping[str, bool] = False, + layers: str + | tuple[str, str, str] + | Mapping[str, str] + | tuple[Mapping[str, str], Mapping[str, str], Mapping[str, str]] + | None = None, + gene_symbols: str | Mapping[str, str | None] | None = None, **kwargs, ): """ @@ -37,42 +42,56 @@ def scatter( Parameters ---------- - data : Union[AnnData, MuData] + data MuData or AnnData object - x : Optional[str] + x x coordinate - y : Optional[str] + y y coordinate - color : Optional[Union[str, Sequence[str]]], optional (default: None) + color Keys or a single key for variables or annotations of observations (.obs columns), or a hex colour specification. - use_raw : Optional[bool], optional (default: None) + use_raw Use `.raw` attribute of the modality where a feature (from `color`) is derived from. - If `None`, defaults to `True` if `.raw` is present and a valid `layer` is not provided. - layers : Optional[Union[str, Sequence[str]]], optional (default: None) + If a dictionary is given, it must have one entry for each modality. + layers Names of the layers where x, y, and color come from. No layer is used by default. A single layer value will be expanded to [layer, layer, layer]. + If a dictionary is given, it must have one entry for each modality. + gene_symbols + Column of `.var` to search for `color` in. + If a dictionary is given, it must have one entry for each modality. """ if isinstance(data, AnnData): + localvars = locals() + for arg in ("use_raw", "layers", "gene_symbols"): + if isinstance(localvars[arg], Mapping): + raise ValueError( + f"`{arg}` can only be a dictionary if `data` is a `MuData` object." + ) return sc.pl.scatter(data, x=x, y=y, color=color, use_raw=use_raw, layers=layers, **kwargs) - if isinstance(layers, str) or layers is None: - layers = [layers, layers, layers] + if isinstance(layers, str) or isinstance(layers, Mapping) or layers is None: + layers = (layers, layers, layers) obs = pd.DataFrame( { - x: _get_values(data, x, use_raw=use_raw, layer=layers[0]), - y: _get_values(data, y, use_raw=use_raw, layer=layers[1]), + x: _get_values(data, x, use_raw=use_raw, layer=layers[0], gene_symbols=gene_symbols), + y: _get_values(data, y, use_raw=use_raw, layer=layers[1], gene_symbols=gene_symbols), } ) obs.index = data.obs_names if color is not None: # Workaround for scanpy#311, scanpy#1497 if isinstance(color, str): - color_obs = _get_values(data, color, use_raw=use_raw, layer=layers[2]) + color_obs = _get_values( + data, color, use_raw=use_raw, layer=layers[2], gene_symbols=gene_symbols + ) color_obs = pd.DataFrame({color: color_obs}) else: - color_obs = _get_values(data, color, use_raw=use_raw, layer=layers[2]) + color_obs = _get_values( + data, color, use_raw=use_raw, layer=layers[2], gene_symbols=gene_symbols + ) color_obs.index = data.obs_names obs = pd.concat([obs, color_obs], axis=1, ignore_index=False) @@ -134,6 +153,12 @@ def embedding( If a dictionary is given, it must have one entry for each modality. """ if isinstance(data, AnnData): + localvars = locals() + for arg in ("use_raw", "layer", "gene_symbols"): + if isinstance(localvars[arg], Mapping): + raise ValueError( + f"`{arg}` can only be a dictionary if `data` is a `MuData` object." + ) return sc.pl.embedding( data, basis=basis, diff --git a/muon/_core/utils.py b/muon/_core/utils.py index 94409a9..f7d889b 100644 --- a/muon/_core/utils.py +++ b/muon/_core/utils.py @@ -1,4 +1,5 @@ -from typing import Union, Optional, Iterable +from collections import defaultdict +from typing import Union, Optional, Iterable, Mapping import warnings import numpy as np @@ -13,12 +14,13 @@ def _get_values( - data: Union[AnnData, MuData], - key: Optional[str] = None, - use_raw: Optional[bool] = None, - layer: Optional[str] = None, - obsmap: Optional[np.ndarray] = None, -) -> Optional[Iterable]: + data: AnnData | MuData, + key: str | None = None, + use_raw: bool | Mapping[str, bool] = False, + layer: str | Mapping[str, str | None] | None = None, + gene_symbols: str | Mapping[str, str | None] | None = None, + obsmap: np.ndarray | None = None, +) -> Iterable | None: """ A helper function to get values for variables or annotations of observations (.obs columns). @@ -35,18 +37,20 @@ def _get_values( Parameters ---------- - data : Union[AnnData, MuData] + data MuData or AnnData object - key : Optional[str] + key String to search for - use_raw : Optional[bool], optional (default: None) + use_raw Use `.raw` attribute of the modality where a feature (from `color`) is derived from. - If `None`, defaults to `True` if `.raw` is present and a valid `layer` is not provided. - layer : Optional[str], optional (default: None) + If a dictionary is given, it must have one entry for each modality. + layer Name of the layer in the modality where a feature (from `color`) is derived from. - No layer is used by default. If a valid `layer` is provided, this takes precedence - over `use_raw=True`. - obsmap : Optional[np.ndarray], optional (default: None) + If a dictionary is given, it must have one entry for each modality. + gene_symbols + Column of `.var` to search for `color` in. + If a dictionary is given, it must have one entry for each modality. + obsmap Provide a vector of the desired size were 0 are missing values and non-zero values correspond to the 1-based index of the value. This is used internally for when AnnData as a modality has less observations @@ -65,7 +69,10 @@ def _maybe_apply_obsmap(vec, m): # Handle multiple keys if isinstance(key, Iterable) and not isinstance(key, str): all_values = [ - _get_values(data, k, use_raw=use_raw, layer=layer, obsmap=obsmap) for k in key + _get_values( + data, k, use_raw=use_raw, layer=layer, gene_symbols=gene_symbols, obsmap=obsmap + ) + for k in key ] df = pd.DataFrame(all_values).T df.columns = [k for k in key if k is not None] @@ -81,7 +88,16 @@ def _maybe_apply_obsmap(vec, m): # Handle composite keys, e.g. rna:n_counts key_mod, mod_key = None, None - if isinstance(data, MuData) and key not in data.var_names and key not in data.obsm: + if ( + isinstance(data, MuData) + and ( + gene_symbols is None + and key not in data.var_names + or gene_symbols is not None + and key not in data.var[gene_symbols] + ) + and key not in data.obsm + ): if ":" in key: maybe_mod, maybe_key = key.split(":", 1) if maybe_mod in data.mod: @@ -116,33 +132,23 @@ def _maybe_apply_obsmap(vec, m): if not data.obs_names.equals(data.mod[key_mod].obs_names) and obsmap is None: obsmap = data.obsmap[key_mod] return _get_values( - data.mod[key_mod], key=mod_key, use_raw=use_raw, layer=layer, obsmap=obsmap + data.mod[key_mod], + key=mod_key, + use_raw=use_raw, + layer=layer, + gene_symbols=gene_symbols, + obsmap=obsmap, ) # {'rna': True, 'prot': False} - key_in_mod = {m: key in data.mod[m].var_names for m in data.mod} - - # Check if the valid layer is requested - if layer is not None: - if sum(key_in_mod.values()) == 1: - use_mod = [m for m, v in key_in_mod.items() if v][0] - valid_layer = layer in data.mod[use_mod].layers - if not valid_layer: - warnings.warn( - f"Layer {layer} is not present when searching for the key {key}, using count matrix instead" - ) - layer = None - - # .raw slots might have exclusive var_names - if (use_raw is None or use_raw) and layer is None: - for m in data.mod: - if key_in_mod[m] == False and data.mod[m].raw is not None: - key_in_mod[m] = key in data.mod[m].raw.var_names - if key_in_mod[m] and data.mod[m].raw is None and layer is None: - warnings.warn( - f"Attibute .raw is None for the modality {m}, using .X instead" - ) - use_raw = False + key_in_mod = {} + for m, mod in data.mod.items(): + if layer is not None and use_raw: + raise ValueError("use_raw cannot be True when a layer is specified.") + + var = mod.var if not use_raw else mod.raw.var + varidx = var.index if gene_symbols is None else var[gene_symbols] + key_in_mod[m] = key in varidx if sum(key_in_mod.values()) == 0: pass # not in var names @@ -154,48 +160,61 @@ def _maybe_apply_obsmap(vec, m): use_mod = [m for m, v in key_in_mod.items() if v][0] if not data.obs_names.equals(data.mod[use_mod].obs_names) and obsmap is None: obsmap = data.obsmap[use_mod] + if isinstance(use_raw, Mapping): + use_raw = use_raw[use_mod] + if isinstance(layer, Mapping): + layer = layer[use_mod] + if isinstance(gene_symbols, Mapping): + gene_symbols = gene_symbols[use_mod] return _get_values( - data.mod[use_mod], key=key, use_raw=use_raw, layer=layer, obsmap=obsmap + data.mod[use_mod], + key=key, + use_raw=use_raw, + layer=layer, + gene_symbols=gene_symbols, + obsmap=obsmap, ) elif isinstance(data, AnnData): - if (use_raw is None or use_raw) and data.raw is not None and layer is None: - keysidx = data.raw.var.index.get_indexer_for([key]) - if keysidx == -1: + if use_raw and layer is not None: + raise ValueError("use_raw cannot be True when a layer is specified.") + + if use_raw: + keysidx = ( + data.raw.var.index.get_indexer_for([key]) + if gene_symbols is None + else np.nonzero(data.raw.var[gene_symbols] == key)[0] + ) + if len(keysidx) == 0 or keysidx == -1: raise ValueError(f"Key {key} could not be found.") values = data.raw.X[:, keysidx[0]] if len(keysidx) > 1: warnings.warn(f"Key {key} is not unique in the index, using the first value...") - - elif layer is not None and layer in data.layers: - if layer in data.layers: - keysidx = data.var.index.get_indexer_for([key]) - if keysidx == -1: - raise ValueError(f"Key {key} could not be found.") - values = data.layers[layer][:, keysidx[0]] - if use_raw: - warnings.warn(f"Layer='{layer}' superseded use_raw={use_raw}") - if len(keysidx) > 1: - warnings.warn(f"Key {key} is not unique in the index, using the first value...") - + elif layer is not None: + keysidx = ( + data.var.index.get_indexer_for([key]) + if gene_symbols is None + else np.nonzero(data.var[gene_symbols] == key)[0] + ) + if len(keysidx) == 0 or keysidx == -1: + raise ValueError(f"Key {key} could not be found.") + values = data.layers[layer][:, keysidx[0]] + if len(keysidx) > 1: + warnings.warn(f"Key {key} is not unique in the index, using the first value...") else: - if (use_raw is None or use_raw) and data.raw is None: - warnings.warn( - f"Attibute .raw is None when searching for the key {key}, using .X instead" - ) - if layer is not None and layer not in data.layers: - warnings.warn( - f"Layer {layer} is not present when searching for the key {key}, using count matrix instead" - ) - keysidx = data.var.index.get_indexer_for([key]) - if keysidx == -1: + keysidx = ( + data.var.index.get_indexer_for([key]) + if gene_symbols is None + else np.nonzero(data.var[gene_symbols] == key)[0] + ) + if len(keysidx) == 0 or keysidx == -1: raise ValueError(f"Key {key} could not be found.") values = data.X[:, keysidx[0]] if len(keysidx) > 1: warnings.warn(f"Key {key} is not unique in the index, using the first value...") if issparse(values): - values = np.array(values.todense()).squeeze() + values = np.asarray(values.todense()).squeeze() values = _maybe_apply_obsmap(values, obsmap) return values From 1fd43c1560335e419c17c20c635bdc29bb53b7ed Mon Sep 17 00:00:00 2001 From: Ilia Kats Date: Fri, 15 Aug 2025 16:38:14 +0200 Subject: [PATCH 5/5] pre-commit: update black version to the one used in CI --- .pre-commit-config.yaml | 2 +- muon/_core/plot.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cfbb58a..273cc5d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 23.3.0 # Replace by any tag/version: https://github.com/psf/black/tags + rev: 25.1.0 # Replace by any tag/version: https://github.com/psf/black/tags hooks: - id: black language_version: python3 diff --git a/muon/_core/plot.py b/muon/_core/plot.py index f3738cd..17a2067 100644 --- a/muon/_core/plot.py +++ b/muon/_core/plot.py @@ -25,11 +25,13 @@ def scatter( y: str | None = None, color: str | Sequence[str] | None = None, use_raw: bool | Mapping[str, bool] = False, - layers: str - | tuple[str, str, str] - | Mapping[str, str] - | tuple[Mapping[str, str], Mapping[str, str], Mapping[str, str]] - | None = None, + layers: ( + str + | tuple[str, str, str] + | Mapping[str, str] + | tuple[Mapping[str, str], Mapping[str, str], Mapping[str, str]] + | None + ) = None, gene_symbols: str | Mapping[str, str | None] | None = None, **kwargs, ):