diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a54608c..b54c87f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning][]. ### Fixed -- Fixed `fill_alpha` ignoring `alpha` channel from custom cmap +- Fixed `fill_alpha` ignoring `alpha` channel from custom cmap ## [0.2.0] - 2024-03-24 diff --git a/src/spatialdata_plot/pl/basic.py b/src/spatialdata_plot/pl/basic.py index 5ff6e77e..dd016fd6 100644 --- a/src/spatialdata_plot/pl/basic.py +++ b/src/spatialdata_plot/pl/basic.py @@ -150,10 +150,10 @@ def _copy( def render_shapes( self, elements: list[str] | str | None = None, - color: str | None = None, + color: list[str | None] | str | None = None, fill_alpha: float | int = 1.0, - groups: list[list[str | None]] | list[str] | str | None = None, - palette: list[list[str | None]] | list[str] | str | None = None, + groups: list[list[str | None]] | list[str | None] | str | None = None, + palette: list[list[str | None]] | list[str | None] | str | None = None, na_color: ColorLike | None = "lightgrey", outline: bool = False, outline_width: float | int = 1.5, @@ -173,17 +173,29 @@ def render_shapes( elements : list[str] | str | None, optional The name(s) of the shapes element(s) to render. If `None`, all shapes elements in the `SpatialData` object will be used. - color : Colorlike | str | None, optional - Can either be a color-like or a key in :attr:`sdata.table.obs`. The latter - can be used to color by categorical or continuous variables. + color : list[str | None] | str | None + Can either be string(s) representing a color-like or key(s) in :attr:`sdata.table.obs`. The latter + can be used to color by categorical or continuous variables. If provided as a list, the length of the list + must match the number of elements that will be plotted. Otherwise, if possible the color will be broadcasted + to all elements. For this, the table in which the color key is found must + annotate the respective element (region must be set to the specific element). If the color column is found + in multiple locations, please provide the table_name to be used for the element. fill_alpha : float | int, default 1.0 - Alpha value for the fill of shapes. - groups : list[str] | str | None, optional + Alpha value for the fill of shapes. If the alpha channel is present in a cmap passed by the + user, this value will multiply the value present in the cmap. + groups : list[list[str | None]] | list[str | None] | str | None When using `color` and the key represents discrete labels, `groups` - can be used to show only a subset of them. Other values are set to NA. - palette : list[str] | str | None, optional + can be used to show only a subset of them. Other values are set to NA. In general the case of a list of + lists means that there is one list per element to be plotted in the list and this list can contain multiple + discrete labels to be visualized. If not provided as list of lists, broadcasting behaviour is attempted + (use the same values for all elements). + palette : list[list[str | None]] | list[str | None] | str | None Palette for discrete annotations. List of valid color names that should be - used for the categories. Must match the number of groups. + used for the categories. Must match the number of groups. Similarly to groups, in the case of a list of + lists means that there is one list per element to be plotted in the list and this list can contain multiple + palettes (one per group) to be visualized. If not provided as list of lists, broadcasting behaviour is + attempted (use the same values for all elements). If groups is provided but not palette, palette is set to + default "lightgray". na_color : str | list[float] | None, default "lightgrey" Color to be used for NAs values, if present. Can either be a named color ("red"), a hex representation ("#000000ff") or a list of floats that @@ -274,8 +286,8 @@ def render_points( elements: list[str] | str | None = None, color: list[str | None] | str | None = None, alpha: float | int = 1.0, - groups: list[list[str | None]] | list[str] | str | None = None, - palette: list[list[str | None]] | list[str] | str | None = None, + groups: list[list[str | None]] | list[str | None] | str | None = None, + palette: list[list[str | None]] | list[str | None] | str | None = None, na_color: ColorLike | None = "lightgrey", cmap: Colormap | str | None = None, norm: None | Normalize = None, @@ -291,17 +303,28 @@ def render_points( elements : list[str] | str | None, optional The name(s) of the points element(s) to render. If `None`, all points elements in the `SpatialData` object will be used. - color : Colorlike | str | None, optional - Can either be a color-like or a key in :attr:`sdata.table.obs`. The latter - can be used to color by categorical or continuous variables. + color : list[str | None] | str | None + Can either be string(s) representing a color-like or key(s) in :attr:`sdata.table.obs`. The latter + can be used to color by categorical or continuous variables. If provided as a list, the length of the list + must match the number of elements that will be plotted. Otherwise, if possible the color will be broadcasted + to all elements. For this, the table in which the color key is found must + annotate the respective element (region must be set to the specific element). If the color column is found + in multiple locations, please provide the table_name to be used for the element. alpha : float | int, default 1.0 Alpha value for the points. - groups : list[str] | str | None, optional + groups : list[list[str | None]] | list[str | None] | str | None When using `color` and the key represents discrete labels, `groups` - can be used to show only a subset of them. Other values are set to NA. - palette : list[str] | str | None, optional + can be used to show only a subset of them. Other values are set to NA. In general the case of a list of + lists means that there is one list per element to be plotted in the list and this list can contain multiple + discrete labels to be visualized. If not provided as list of lists, broadcasting behaviour is attempted + (use the same values for all elements). If groups is provided but not palette, palette is set to + default "lightgray". + palette : list[list[str | None]] | list[str | None] | str | None Palette for discrete annotations. List of valid color names that should be - used for the categories. Must match the number of groups. + used for the categories. Must match the number of groups. Similarly to groups, in the case of a list of + lists means that there is one list per element to be plotted in the list and this list can contain multiple + palettes (one per group) to be visualized. If not provided as list of lists, broadcasting behaviour is + attempted (use the same values for all elements). na_color : str | list[float] | None, default "lightgrey" Color to be used for NAs values, if present. Can either be a named color ("red"), a hex representation ("#000000ff") or a list of floats that @@ -374,7 +397,7 @@ def render_images( cmap: list[Colormap] | Colormap | str | None = None, norm: Normalize | None = None, na_color: ColorLike | None = (0.0, 0.0, 0.0, 0.0), - palette: list[str] | str | None = None, + palette: list[list[str | None]] | list[str | None] | str | None = None, alpha: float | int = 1.0, quantiles_for_norm: tuple[float | None, float | None] | None = None, scale: list[str] | str | None = None, @@ -400,6 +423,11 @@ def render_images( Applies to all channels if set. na_color : ColorLike | None, default (0.0, 0.0, 0.0, 0.0) Color to be used for NA values. Accepts color-like values (string, hex, RGB(A)). + palette : list[list[str | None]] | list[str | None] | str | None + Palette to color images. In the case of a list of + lists means that there is one list per element to be plotted in the list and this list contains the string + indicating the palette to be used. If not provided as list of lists, broadcasting behaviour is + attempted (use the same values for all elements). alpha : float | int, default 1.0 Alpha value for the images. Must be a numeric between 0 and 1. quantiles_for_norm : tuple[float | None, float | None] | None, optional @@ -476,10 +504,10 @@ def render_labels( self, elements: list[str] | str | None = None, color: list[str | None] | str | None = None, - groups: list[list[str | None]] | list[str] | str | None = None, + groups: list[list[str | None]] | list[str | None] | str | None = None, contour_px: int = 3, outline: bool = False, - palette: list[list[str | None]] | list[str] | str | None = None, + palette: list[list[str | None]] | list[str | None] | str | None = None, cmap: Colormap | str | None = None, norm: Normalize | None = None, na_color: ColorLike | None = (0.0, 0.0, 0.0, 0.0), @@ -497,19 +525,31 @@ def render_labels( elements : list[str] | str | None, optional The name(s) of the label element(s) to render. If `None`, all label elements in the `SpatialData` object will be used. - color : str | None, optional - Key for annotations in :attr:`anndata.AnnData.obs` or variables/genes. - groups : list[str] | str | None, optional + color : list[str | None] | str | None + Can either be string(s) representing a color-like or key(s) in :attr:`sdata.table.obs`. The latter + can be used to color by categorical or continuous variables. If provided as a list, the length of the list + must match the number of elements that will be plotted. Otherwise, if possible the color will be broadcasted + to all elements. For this, the table in which the color key is found must + annotate the respective element (region must be set to the specific element). If the color column is found + in multiple locations, please provide the table_name to be used for the element. + groups : list[list[str | None]] | list[str | None] | str | None When using `color` and the key represents discrete labels, `groups` - can be used to show only a subset of them. Other values are set to NA. + can be used to show only a subset of them. Other values are set to NA. In general the case of a list of + lists means that there is one list per element to be plotted in the list and this list can contain multiple + discrete labels to be visualized. If not provided as list of lists, broadcasting behaviour is attempted + (use the same values for all elements). + palette : list[list[str | None]] | list[str | None] | str | None + Palette for discrete annotations. List of valid color names that should be + used for the categories. Must match the number of groups. Similarly to groups, in the case of a list of + lists means that there is one list per element to be plotted in the list and this list can contain multiple + palettes (one per group) to be visualized. If not provided as list of lists, broadcasting behaviour is + attempted (use the same values for all elements). If groups is provided but not palette, palette is set to + default "lightgray". contour_px : int, default 3 Draw contour of specified width for each segment. If `None`, fills entire segment, see :func:`skimage.morphology.erosion`. outline : bool, default False Whether to plot boundaries around segmentation masks. - palette : list[str] | str | None, optional - Palette for discrete annotations. List of valid color names that should be - used for the categories. Must match the number of groups. cmap : Colormap | str | None, optional Colormap for continuous annotations, see :class:`matplotlib.colors.Colormap`. norm : Normalize | None, optional @@ -529,6 +569,10 @@ def render_labels( 3) "full": render the full image without rasterization. In the case of a multiscale image, the scale with the highest resolution is selected. This can lead to long computing times for large images! 4) List that is matched to the list of elements (can contain `None`, scale names or "full"). + table_name: + Name of the table(s) containing the color(s) columns. If one name is given than the table is used for each + spatial element to be plotted if the table annotates it. If multiple names are given in a list than the + length must be equal to the number of spatial elements being plotted. kwargs Additional arguments to be passed to cmap and norm. @@ -623,6 +667,12 @@ def show( Works only if there is one image in the SpatialData object. ncols : Number of columns in the figure. Default is 4. + return_ax : + Whether to return the axes object created. False by default. + colorbar : + Whether to plot the colorbar. True by default. + title : + The title of the plot. If not provided the plot will have the name of the coordinate system as title. Returns ------- diff --git a/src/spatialdata_plot/pl/render.py b/src/spatialdata_plot/pl/render.py index b82fe19c..df7c33d0 100644 --- a/src/spatialdata_plot/pl/render.py +++ b/src/spatialdata_plot/pl/render.py @@ -677,17 +677,6 @@ def _render_labels( na_color=render_params.cmap_params.na_color, ) - _cax = ax.imshow( - labels_infill, - rasterized=True, - cmap=None if categorical else render_params.cmap_params.cmap, - norm=None if categorical else render_params.cmap_params.norm, - alpha=render_params.fill_alpha, - origin="lower", - ) - _cax.set_transform(trans_data) - cax = ax.add_image(_cax) - # Then overlay the contour labels_contour = _map_color_seg( seg=label.values, @@ -708,6 +697,16 @@ def _render_labels( alpha=render_params.outline_alpha, origin="lower", ) + _cax = ax.imshow( + labels_infill, + rasterized=True, + cmap=None if categorical else render_params.cmap_params.cmap, + norm=None if categorical else render_params.cmap_params.norm, + alpha=render_params.fill_alpha, + origin="lower", + ) + _cax.set_transform(trans_data) + cax = ax.add_image(_cax) else: # Default: no alpha, contour = infill label = _map_color_seg( diff --git a/src/spatialdata_plot/pl/render_params.py b/src/spatialdata_plot/pl/render_params.py index bcfa92e7..2f52e39e 100644 --- a/src/spatialdata_plot/pl/render_params.py +++ b/src/spatialdata_plot/pl/render_params.py @@ -75,7 +75,7 @@ class ShapesRenderParams: col_for_color: str | None = None groups: str | list[list[str | None]] | list[str | None] | None = None contour_px: int | None = None - palette: ListedColormap | list[str | None] | None = None + palette: ListedColormap | list[list[str | None]] | list[str | None] | None = None outline_alpha: float = 1.0 fill_alpha: float = 0.3 scale: float = 1.0 @@ -106,7 +106,7 @@ class ImageRenderParams: cmap_params: list[CmapParams] | CmapParams elements: str | Sequence[str] | None = None channel: list[str] | list[int] | int | str | None = None - palette: ListedColormap | list[str | None] | None = None + palette: ListedColormap | list[list[str | None]] | list[str | None] | None = None alpha: float = 1.0 quantiles_for_norm: tuple[float | None, float | None] = (None, None) scale: str | list[str] | None = None diff --git a/src/spatialdata_plot/pl/utils.py b/src/spatialdata_plot/pl/utils.py index da58cb24..bd335c97 100644 --- a/src/spatialdata_plot/pl/utils.py +++ b/src/spatialdata_plot/pl/utils.py @@ -206,7 +206,20 @@ def _get_collection_shape( try: # fails when numeric - fill_c = ColorConverter().to_rgba_array(c) + if len(c.shape) == 1 and c.shape[0] in [3, 4] and c.shape[0] == len(shapes) and c.dtype == float: + if norm is None: + c = cmap(c) + else: + try: + norm = colors.Normalize(vmin=min(c), vmax=max(c)) + except ValueError as e: + raise ValueError( + "Could not convert values in the `color` column to float, if `color` column represents" + " categories, set the column to categorical dtype." + ) from e + c = cmap(norm(c)) + else: + fill_c = ColorConverter().to_rgba_array(c) except ValueError: if norm is None: c = cmap(c) @@ -695,7 +708,8 @@ def _set_color_source_vec( # do not rename categories, as colors need not be unique color_vector = color_source_vector.map(color_map) if color_vector.isna().any(): - color_vector = color_vector.add_categories([to_hex(na_color)]) + if na_cat_color := to_hex(na_color) not in color_vector.categories: + color_vector = color_vector.add_categories([na_cat_color]) color_vector = color_vector.fillna(to_hex(na_color)) return color_source_vector, color_vector, True @@ -826,7 +840,7 @@ def _decorate_axs( ax: Axes, cax: PatchCollection, fig_params: FigParams, - value_to_plot: str | None, # str | None, + value_to_plot: str | None, color_source_vector: pd.Series[CategoricalDtype], adata: AnnData | None = None, palette: ListedColormap | str | list[str] | None = None, @@ -1465,13 +1479,13 @@ def _validate_colors_element_table_mapping_points_shapes( params.col_for_color.append(col_color) element_table_mapping[element_name] = set() else: - if isinstance(mapping := element_table_mapping[element_name], set) and len(mapping.copy()) != 0: - for table_name in mapping.copy(): + if isinstance(table_set := element_table_mapping[element_name], set) and len(table_set) != 0: + for table_name in table_set.copy(): if ( col_color not in sdata[table_name].obs.columns and col_color not in sdata[table_name].var_names ): - mapping.remove(table_name) + table_set.remove(table_name) params.col_for_color.append(None) else: params.col_for_color.append(col_color) @@ -1635,14 +1649,14 @@ def _validate_render_params( contour_px: int | None = None, elements: list[str] | str | None = None, fill_alpha: float | int | None = None, - groups: list[list[str | None]] | list[str] | str | None = None, + groups: str | list[list[str | None]] | list[str | None] | None = None, na_color: ColorLike | None = None, norm: Normalize | bool | None = None, outline: bool | None = None, outline_alpha: float | int | None = None, outline_color: str | list[float] | None = None, outline_width: float | int | None = None, - palette: list[list[str | None]] | list[str] | str | None = None, + palette: list[list[str | None]] | list[str | None] | str | None = None, quantiles_for_norm: tuple[float | None, float | None] | None = None, scale: float | int | list[str] | str | None = None, size: float | int | None = None, @@ -1674,12 +1688,25 @@ def _validate_render_params( raise TypeError("All items in single 'groups' list must be strings.") else: - if not all(isinstance(g, (str, type(None))) for group in groups for g in group): + if not all( + ( + isinstance(group, list) and all(isinstance(g, (str, type(None))) for g in group) + if group is not None + else True + ) + for group in groups + ): raise TypeError("All items in lists within lists of 'groups' must be strings or None.") params_dict["groups"] = groups_overwrite palette_overwrite: list[list[str]] | None = None + if groups_overwrite is not None and palette is None: + warnings.warn( + "Groups is specified but palette is not. Setting palette to default 'lightgray'", UserWarning, stacklevel=2 + ) + palette_overwrite = [["lightgray" for _ in range(len(groups_sublist))] for groups_sublist in groups_overwrite] + if palette is not None: if not isinstance(palette, (list, str)): raise TypeError("Parameter 'palette' must be a string or a list of strings.") @@ -1690,7 +1717,14 @@ def _validate_render_params( raise TypeError("All items in single 'palette' list must be strings.") palette_overwrite = [[pal for pal in palette if isinstance(pal, str)]] else: - if not all(isinstance(p, str) or p is None for pal in palette for p in pal): + if not all( + ( + isinstance(pal, list) and all(isinstance(p, (str, type(None))) for p in pal) + if pal is not None + else True + ) + for pal in palette + ): raise TypeError("All items in lists within lists of 'groups' must be strings.") if element_type in ["shapes", "points", "labels"]: @@ -1957,7 +1991,7 @@ def _is_coercable_to_float(series: pd.Series) -> bool: def _return_list_str_none(parameter: list[str | None] | str | None) -> list[str | None]: """Force mypy to recognize list of string and None.""" if isinstance(parameter, list) and all(isinstance(item, (str, type(None))) for item in parameter): - checked_parameter = parameter if isinstance(parameter, list) else [None] + checked_parameter = parameter else: checked_parameter = [None] return checked_parameter diff --git a/tests/_images/Points_can_filter_with_groups_default_palette.png b/tests/_images/Points_can_filter_with_groups_default_palette.png new file mode 100644 index 00000000..6c298c64 Binary files /dev/null and b/tests/_images/Points_can_filter_with_groups_default_palette.png differ diff --git a/tests/pl/test_render_points.py b/tests/pl/test_render_points.py index cd31bc89..18f385d6 100644 --- a/tests/pl/test_render_points.py +++ b/tests/pl/test_render_points.py @@ -32,6 +32,11 @@ def test_plot_can_filter_with_groups(self, sdata_blobs: SpatialData): sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_points" sdata_blobs.pl.render_points(color="genes", groups="gene_b", palette="orange").pl.show() + def test_plot_can_filter_with_groups_default_palette(self, sdata_blobs: SpatialData): + sdata_blobs["table"].obs["region"] = ["blobs_points"] * sdata_blobs["table"].n_obs + sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_points" + sdata_blobs.pl.render_points(color="genes", groups="gene_b").pl.show() + def test_plot_coloring_with_palette(self, sdata_blobs: SpatialData): sdata_blobs["table"].obs["region"] = ["blobs_points"] * sdata_blobs["table"].n_obs sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_points"