44import os
55import warnings
66from collections import Counter , OrderedDict
7- from collections .abc import Callable , Iterable , Mapping , Sequence
7+ from collections .abc import Callable , Mapping , Sequence
88from copy import copy
99from functools import partial
1010from pathlib import Path
7878 CmapParams ,
7979 Color ,
8080 ColorbarSpec ,
81+ ColorLike ,
8182 FigParams ,
8283 GraphRenderParams ,
8384 ImageRenderParams ,
9394
9495to_hex = partial (colors .to_hex , keep_alpha = True )
9596
96- # replace with
97- # from spatialdata._types import ColorLike
98- # once https://github.com/scverse/spatialdata/pull/689/ is in a release
99- ColorLike = tuple [float , ...] | list [float ] | str
100-
10197_GROUPS_IGNORED_WARNING = "Parameter 'groups' is ignored when 'color' is a literal color, not a column name."
10298
10399_RENDER_CMD_TO_CS_FLAG : dict [str , str ] = {
@@ -996,44 +992,6 @@ def _set_outline(
996992 )
997993
998994
999- def _get_subplots (num_images : int , ncols : int = 4 , width : int = 4 , height : int = 3 ) -> plt .Figure | plt .Axes :
1000- """Set up the axs objects.
1001-
1002- Parameters
1003- ----------
1004- num_images
1005- Number of images to plot. Must be greater than 1.
1006- ncols
1007- Number of columns in the subplot grid, by default 4
1008- width
1009- Width of each subplot, by default 4
1010-
1011- Returns
1012- -------
1013- Union[plt.Figure, plt.Axes]
1014- Matplotlib figure and axes object.
1015- """
1016- if num_images < ncols :
1017- nrows = 1
1018- ncols = num_images
1019- else :
1020- nrows , reminder = divmod (num_images , ncols )
1021-
1022- if nrows == 0 :
1023- nrows = 1
1024- if reminder > 0 :
1025- nrows += 1
1026-
1027- fig , axes = plt .subplots (nrows , ncols , figsize = (width * ncols , height * nrows ))
1028-
1029- if not isinstance (axes , Iterable ):
1030- axes = np .array ([axes ])
1031-
1032- # get rid of the empty axes
1033- _ = [ax .axis ("off" ) for ax in axes .flatten ()[num_images :]]
1034- return fig , axes
1035-
1036-
1037995def _get_colors_for_categorical_obs (
1038996 categories : Sequence [str | int ],
1039997 palette : ListedColormap | str | list [str ] | None = None ,
@@ -1503,7 +1461,7 @@ def _map_color_seg(
15031461
15041462 if isinstance (color_vector .dtype , pd .CategoricalDtype ):
15051463 # Case A: users wants to plot a categorical column
1506- val_im : ArrayLike = map_array (seg . copy () , cell_id , color_vector .codes + 1 )
1464+ val_im : ArrayLike = map_array (seg , cell_id , color_vector .codes + 1 )
15071465 cols = colors .to_rgba_array (color_vector .categories )
15081466 elif pd .api .types .is_numeric_dtype (color_vector .dtype ):
15091467 # Case B: user wants to plot a continous column
@@ -1515,7 +1473,7 @@ def _map_color_seg(
15151473 normed_color_vector [~ np .isnan (normed_color_vector )]
15161474 )
15171475 cols = cmap_params .cmap (normed_color_vector )
1518- val_im = map_array (seg . copy () , cell_id , cell_id )
1476+ val_im = map_array (seg , cell_id , cell_id )
15191477 else :
15201478 # Case C: User didn't specify any colors
15211479 if color_source_vector is not None and (
@@ -1524,12 +1482,12 @@ def _map_color_seg(
15241482 and set (color_vector ) == {na_color .get_hex_with_alpha ()}
15251483 and not na_color .color_modified_by_user ()
15261484 ):
1527- val_im = map_array (seg . copy () , cell_id , cell_id )
1485+ val_im = map_array (seg , cell_id , cell_id )
15281486 RNG = default_rng (42 )
15291487 cols = RNG .random ((len (color_vector ), 3 ))
15301488 else :
15311489 # Case D: User didn't specify a column to color by, but modified the na_color
1532- val_im = map_array (seg . copy () , cell_id , cell_id )
1490+ val_im = map_array (seg , cell_id , cell_id )
15331491 first_value = color_vector .iloc [0 ] if isinstance (color_vector , pd .Series ) else color_vector [0 ]
15341492 if _is_color_like (first_value ):
15351493 # we have color-like values (e.g., hex or named colors)
@@ -1550,7 +1508,7 @@ def _map_color_seg(
15501508 if outline_color_source_vector is not None :
15511509 cat = pd .Categorical (outline_color_source_vector )
15521510 cat_codes = cat .codes
1553- outline_val_im : ArrayLike = map_array (seg . copy () , cell_id , cat_codes + 1 )
1511+ outline_val_im : ArrayLike = map_array (seg , cell_id , cat_codes + 1 )
15541512 color_arr = np .asarray (outline_color_vector , dtype = object )
15551513 # Pick the first per-cell hex for each category in one vectorized pass
15561514 # (avoids `K × O(N)` Python loops on large label sets).
@@ -1572,7 +1530,7 @@ def _map_color_seg(
15721530 if finite .any ():
15731531 normed [finite ] = cmap_params .norm (normed [finite ])
15741532 outline_cols = cmap_params .cmap (normed )
1575- outline_val_im = map_array (seg . copy () , cell_id , cell_id )
1533+ outline_val_im = map_array (seg , cell_id , cell_id )
15761534 if seg_erosionpx is not None :
15771535 outline_val_im [
15781536 outline_val_im == erosion (outline_val_im , footprint_rectangle ((seg_erosionpx , seg_erosionpx )))
@@ -1814,8 +1772,12 @@ def _to_hex_no_alpha(color_value: Any) -> str | None:
18141772 if col_to_colorby in adata .obs and hasattr (adata .obs [col_to_colorby ], "cat" )
18151773 else categories
18161774 )
1775+ # Map category -> index once (O(K)) instead of a per-category list scan
1776+ # (was O(K^2) via list.index). all_cats comes from pandas .categories,
1777+ # which is unique, so a plain dict comprehension is sufficient.
1778+ cat_to_idx : dict [Any , int ] = {c : i for i , c in enumerate (all_cats )}
18171779 for category in categories :
1818- idx = all_cats . index (category ) if category in all_cats else None
1780+ idx = cat_to_idx . get (category )
18191781 if idx is not None and idx < len (hex_colors ) and hex_colors [idx ] is not None :
18201782 hex_color = hex_colors [idx ]
18211783 assert hex_color is not None # type narrowing for mypy
0 commit comments