From 0959f017e66767c0fee7ea197a74ac3ea660c01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Wed, 24 Sep 2025 21:05:42 +0200 Subject: [PATCH 1/8] expose multivariate plotting functionality to top level functions imshow, pcolor, pcolormesh, and Collection --- doc/api/colors_api.rst | 1 + lib/matplotlib/axes/_axes.py | 53 ++-- lib/matplotlib/axes/_axes.pyi | 32 ++- lib/matplotlib/collections.py | 4 +- lib/matplotlib/collections.pyi | 11 +- lib/matplotlib/colorizer.py | 12 + lib/matplotlib/colorizer.pyi | 28 +-- lib/matplotlib/colors.py | 8 +- lib/matplotlib/image.py | 78 ++++-- lib/matplotlib/pyplot.py | 32 +-- .../test_axes/bivariate_cmap_shapes.png | Bin 0 -> 4116 bytes .../test_axes/bivariate_visualizations.png | Bin 0 -> 8311 bytes .../test_axes/multivariate_imshow_alpha.png | Bin 0 -> 5381 bytes .../test_axes/multivariate_imshow_norm.png | Bin 0 -> 6902 bytes .../multivariate_pcolormesh_alpha.png | Bin 0 -> 5239 bytes .../multivariate_pcolormesh_norm.png | Bin 0 -> 6576 bytes .../test_axes/multivariate_visualizations.png | Bin 0 -> 8222 bytes lib/matplotlib/tests/test_axes.py | 234 ++++++++++++++++++ lib/matplotlib/tests/test_image.py | 9 + 19 files changed, 417 insertions(+), 85 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_axes/bivariate_cmap_shapes.png create mode 100644 lib/matplotlib/tests/baseline_images/test_axes/bivariate_visualizations.png create mode 100644 lib/matplotlib/tests/baseline_images/test_axes/multivariate_imshow_alpha.png create mode 100644 lib/matplotlib/tests/baseline_images/test_axes/multivariate_imshow_norm.png create mode 100644 lib/matplotlib/tests/baseline_images/test_axes/multivariate_pcolormesh_alpha.png create mode 100644 lib/matplotlib/tests/baseline_images/test_axes/multivariate_pcolormesh_norm.png create mode 100644 lib/matplotlib/tests/baseline_images/test_axes/multivariate_visualizations.png diff --git a/doc/api/colors_api.rst b/doc/api/colors_api.rst index 18e7c43932a9..147762d0152b 100644 --- a/doc/api/colors_api.rst +++ b/doc/api/colors_api.rst @@ -55,6 +55,7 @@ Multivariate Colormaps BivarColormap SegmentedBivarColormap BivarColormapFromImage + MultivarColormap Other classes ------------- diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 66770e426386..8c4ad62d865f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6175,6 +6175,7 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, - (M, N): an image with scalar data. The values are mapped to colors using normalization and a colormap. See parameters *norm*, *cmap*, *vmin*, *vmax*. + - (v, M, N): if coupled with a cmap that supports v scalars - (M, N, 3): an image with RGB values (0-1 float or 0-255 int). - (M, N, 4): an image with RGBA values (0-1 float or 0-255 int), i.e. including transparency. @@ -6184,15 +6185,16 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, Out-of-range RGB(A) values are clipped. - %(cmap_doc)s + + %(multi_cmap_doc)s This parameter is ignored if *X* is RGB(A). - %(norm_doc)s + %(multi_norm_doc)s This parameter is ignored if *X* is RGB(A). - %(vmin_vmax_doc)s + %(multi_vmin_vmax_doc)s This parameter is ignored if *X* is RGB(A). @@ -6271,6 +6273,9 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, See :doc:`/gallery/images_contours_and_fields/image_antialiasing` for a discussion of image antialiasing. + Only 'data' is available when using `~matplotlib.colors.BivarColormap` + or `~matplotlib.colors.MultivarColormap` + alpha : float or array-like, optional The alpha blending value, between 0 (transparent) and 1 (opaque). If *alpha* is an array, the alpha blending values are applied pixel @@ -6376,6 +6381,7 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, if aspect is not None: self.set_aspect(aspect) + X = mcolorizer._ensure_multivariate_data(X, im.norm.n_components) im.set_data(X) im.set_alpha(alpha) if im.get_clip_path() is None: @@ -6531,9 +6537,10 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, Parameters ---------- - C : 2D array-like + C : 2D or 3D array-like The color-mapped values. Color-mapping is controlled by *cmap*, - *norm*, *vmin*, and *vmax*. + *norm*, *vmin*, and *vmax*. 3D arrays are supported only if the + cmap supports v channels, where v is the size along the first axis. X, Y : array-like, optional The coordinates of the corners of quadrilaterals of a pcolormesh:: @@ -6578,11 +6585,11 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, See :doc:`/gallery/images_contours_and_fields/pcolormesh_grids` for more description. - %(cmap_doc)s + %(multi_cmap_doc)s - %(norm_doc)s + %(multi_norm_doc)s - %(vmin_vmax_doc)s + %(multi_vmin_vmax_doc)s %(colorizer_doc)s @@ -6657,8 +6664,17 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, if shading is None: shading = mpl.rcParams['pcolor.shading'] shading = shading.lower() - X, Y, C, shading = self._pcolorargs('pcolor', *args, shading=shading, - kwargs=kwargs) + + if colorizer is None: + cmap = mcolorizer._ensure_cmap(cmap, accept_multivariate=True) + C = mcolorizer._ensure_multivariate_data(args[-1], cmap.n_variates) + else: + C = mcolorizer._ensure_multivariate_data(args[-1], + colorizer.cmap.n_variates) + + X, Y, C, shading = self._pcolorargs('pcolor', *args[:-1], C, + shading=shading, kwargs=kwargs) + linewidths = (0.25,) if 'linewidth' in kwargs: kwargs['linewidths'] = kwargs.pop('linewidth') @@ -6733,6 +6749,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, - (M, N) or M*N: a mesh with scalar data. The values are mapped to colors using normalization and a colormap. See parameters *norm*, *cmap*, *vmin*, *vmax*. + - (v, M, N): if coupled with a cmap that supports v scalars - (M, N, 3): an image with RGB values (0-1 float or 0-255 int). - (M, N, 4): an image with RGBA values (0-1 float or 0-255 int), i.e. including transparency. @@ -6767,11 +6784,11 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, expanded as needed into the appropriate 2D arrays, making a rectangular grid. - %(cmap_doc)s + %(multi_cmap_doc)s - %(norm_doc)s + %(multi_norm_doc)s - %(vmin_vmax_doc)s + %(multi_vmin_vmax_doc)s %(colorizer_doc)s @@ -6897,7 +6914,15 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, shading = mpl._val_or_rc(shading, 'pcolor.shading').lower() kwargs.setdefault('edgecolors', 'none') - X, Y, C, shading = self._pcolorargs('pcolormesh', *args, + if colorizer is None: + cmap = mcolorizer._ensure_cmap(cmap, accept_multivariate=True) + C = mcolorizer._ensure_multivariate_data(args[-1], cmap.n_variates) + else: + C = mcolorizer._ensure_multivariate_data(args[-1], + colorizer.cmap.n_variates) + + + X, Y, C, shading = self._pcolorargs('pcolormesh', *args[:-1], C, shading=shading, kwargs=kwargs) coords = np.stack([X, Y], axis=-1) diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 1c3e1e560d07..438a89e59242 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -12,7 +12,13 @@ from matplotlib.collections import ( QuadMesh, ) from matplotlib.colorizer import Colorizer -from matplotlib.colors import Colormap, Normalize +from matplotlib.colors import ( + Colormap, + BivarColormap, + MultivarColormap, + Norm, + Normalize, +) from matplotlib.container import ( BarContainer, PieContainer, ErrorbarContainer, StemContainer) from matplotlib.contour import ContourSet, QuadContourSet @@ -500,14 +506,14 @@ class Axes(_AxesBase): def imshow( self, X: ArrayLike | PIL.Image.Image, - cmap: str | Colormap | None = ..., - norm: str | Normalize | None = ..., + cmap: str | Colormap | BivarColormap | MultivarColormap | None = ..., + norm: str | Norm | None = ..., *, aspect: Literal["equal", "auto"] | float | None = ..., interpolation: str | None = ..., alpha: float | ArrayLike | None = ..., - vmin: float | None = ..., - vmax: float | None = ..., + vmin: float | tuple[float] | None = ..., + vmax: float | tuple[float] | None = ..., colorizer: Colorizer | None = ..., origin: Literal["upper", "lower"] | None = ..., extent: tuple[float, float, float, float] | None = ..., @@ -524,10 +530,10 @@ class Axes(_AxesBase): *args: ArrayLike, shading: Literal["flat", "nearest", "auto"] | None = ..., alpha: float | None = ..., - norm: str | Normalize | None = ..., - cmap: str | Colormap | None = ..., - vmin: float | None = ..., - vmax: float | None = ..., + norm: str | Norm | None = ..., + cmap: str | Colormap | BivarColormap | MultivarColormap | None = ..., + vmin: float | tuple[float] | None = ..., + vmax: float | tuple[float] | None = ..., colorizer: Colorizer | None = ..., data=..., **kwargs @@ -536,10 +542,10 @@ class Axes(_AxesBase): self, *args: ArrayLike, alpha: float | None = ..., - norm: str | Normalize | None = ..., - cmap: str | Colormap | None = ..., - vmin: float | None = ..., - vmax: float | None = ..., + norm: str | Norm | None = ..., + cmap: str | Colormap | BivarColormap | MultivarColormap | None = ..., + vmin: float | tuple[float] | None = ..., + vmax: float | tuple[float] | None = ..., colorizer: Colorizer | None = ..., shading: Literal["flat", "nearest", "gouraud", "auto"] | None = ..., antialiased: bool = ..., diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index c9e04a70b356..95150115fc56 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -2386,6 +2386,8 @@ def set_array(self, A): h, w = height, width ok_shapes = [(h, w, 3), (h, w, 4), (h, w), (h * w,)] if A is not None: + if hasattr(self, 'norm'): + A = mcolorizer._ensure_multivariate_data(A, self.norm.n_components) shape = np.shape(A) if shape not in ok_shapes: raise ValueError( @@ -2643,7 +2645,7 @@ def _get_unmasked_polys(self): mask = (mask[0:-1, 0:-1] | mask[1:, 1:] | mask[0:-1, 1:] | mask[1:, 0:-1]) arr = self.get_array() if arr is not None: - arr = np.ma.getmaskarray(arr) + arr = self._getmaskarray(arr) if arr.ndim == 3: # RGB(A) case mask |= np.any(arr, axis=-1) diff --git a/lib/matplotlib/collections.pyi b/lib/matplotlib/collections.pyi index ecd969cfacc6..e30ed77cafc9 100644 --- a/lib/matplotlib/collections.pyi +++ b/lib/matplotlib/collections.pyi @@ -7,7 +7,12 @@ from numpy.typing import ArrayLike, NDArray from . import colorizer, transforms from .backend_bases import MouseEvent from .artist import Artist -from .colors import Normalize, Colormap +from .colors import ( + Colormap, + BivarColormap, + MultivarColormap, + Norm, +) from .lines import Line2D from .path import Path from .patches import Patch @@ -29,8 +34,8 @@ class Collection(colorizer.ColorizingArtist): antialiaseds: bool | Sequence[bool] | None = ..., offsets: tuple[float, float] | Sequence[tuple[float, float]] | None = ..., offset_transform: transforms.Transform | None = ..., - norm: Normalize | None = ..., - cmap: Colormap | None = ..., + norm: Norm | None = ..., + cmap: Colormap | BivarColormap | MultivarColormap | None = ..., colorizer: colorizer.Colorizer | None = ..., pickradius: float = ..., hatch: str | None = ..., diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index 095b93ccfe85..0b449ecb17be 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -600,6 +600,18 @@ def get_array(self): """ return self._A + def _getmaskarray(self, A): + """ + Similar to np.ma.getmaskarray but also handles the case where + the data has multiple fields. + + The return array always has the same shape as the input, and dtype bool + """ + mask = np.ma.getmaskarray(A) + if isinstance(self.norm, colors.MultiNorm): + mask = np.any(mask.view('bool').reshape((*A.shape, -1)), axis=-1) + return mask + def changed(self): """ Call this whenever the mappable is changed to notify all the diff --git a/lib/matplotlib/colorizer.pyi b/lib/matplotlib/colorizer.pyi index 9a5a73415d83..81c351fa5354 100644 --- a/lib/matplotlib/colorizer.pyi +++ b/lib/matplotlib/colorizer.pyi @@ -9,13 +9,13 @@ class Colorizer: callbacks: cbook.CallbackRegistry def __init__( self, - cmap: str | colors.Colormap | None = ..., + cmap: str | colors.Colormap | colors.BivarColormap | colors.MultivarColormap | None = ..., norm: str | colors.Norm | None = ..., ) -> None: ... @property def norm(self) -> colors.Norm: ... @norm.setter - def norm(self, norm: colors.Norm | str | None) -> None: ... + def norm(self, norm: colors.Norm | str | tuple[str] | None) -> None: ... def to_rgba( self, x: np.ndarray, @@ -26,28 +26,28 @@ class Colorizer: def autoscale(self, A: ArrayLike) -> None: ... def autoscale_None(self, A: ArrayLike) -> None: ... @property - def cmap(self) -> colors.Colormap: ... + def cmap(self) -> colors.Colormap | colors.BivarColormap | colors.MultivarColormap: ... @cmap.setter - def cmap(self, cmap: colors.Colormap | str | None) -> None: ... + def cmap(self, cmap: colors.Colormap | colors.BivarColormap | colors.MultivarColormap | str | None) -> None: ... def get_clim(self) -> tuple[float, float]: ... def set_clim(self, vmin: float | tuple[float, float] | None = ..., vmax: float | None = ...) -> None: ... def changed(self) -> None: ... @property - def vmin(self) -> float | None: ... + def vmin(self) -> float | tuple[float] | None: ... @vmin.setter - def vmin(self, value: float | None) -> None: ... + def vmin(self, value: float | tuple[float] | None) -> None: ... @property - def vmax(self) -> float | None: ... + def vmax(self) -> float | tuple[float] | None: ... @vmax.setter - def vmax(self, value: float | None) -> None: ... + def vmax(self, value: float | tuple[float] | None) -> None: ... @property - def clip(self) -> bool: ... + def clip(self) -> bool | tuple[bool, ...]: ... @clip.setter - def clip(self, value: bool) -> None: ... + def clip(self, value: ArrayLike | bool) -> None: ... class _ColorizerInterface: - cmap: colors.Colormap + cmap: colors.Colormap | colors.BivarColormap | colors.MultivarColormap colorbar: colorbar.Colorbar | None callbacks: cbook.CallbackRegistry def to_rgba( @@ -60,8 +60,8 @@ class _ColorizerInterface: def get_clim(self) -> tuple[float, float]: ... def set_clim(self, vmin: float | tuple[float, float] | None = ..., vmax: float | None = ...) -> None: ... def get_alpha(self) -> float | None: ... - def get_cmap(self) -> colors.Colormap: ... - def set_cmap(self, cmap: str | colors.Colormap) -> None: ... + def get_cmap(self) -> colors.Colormap | colors.BivarColormap | colors.MultivarColormap: ... + def set_cmap(self, cmap: str | colors.Colormap | colors.BivarColormap | colors.MultivarColormap) -> None: ... @property def norm(self) -> colors.Norm: ... @norm.setter @@ -75,7 +75,7 @@ class _ScalarMappable(_ColorizerInterface): def __init__( self, norm: colors.Norm | None = ..., - cmap: str | colors.Colormap | None = ..., + cmap: str | colors.Colormap | colors.BivarColormap | colors.MultivarColormap | None = ..., *, colorizer: Colorizer | None = ..., **kwargs diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 02ea1d3723e9..eda7d5008fd8 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1432,9 +1432,9 @@ def __init__(self, colormaps, combination_mode, name='multivariate colormap'): Describe how colormaps are combined in sRGB space - If 'sRGB_add' -> Mixing produces brighter colors - `sRGB = sum(colors)` + ``sRGB = sum(colors)`` - If 'sRGB_sub' -> Mixing produces darker colors - `sRGB = 1 - sum(1 - colors)` + ``sRGB = 1 - sum(1 - colors)`` name : str, optional The name of the colormap family. """ @@ -1610,11 +1610,11 @@ def with_extremes(self, *, bad=None, under=None, over=None): If Matplotlib color, the bad value is set accordingly in the copy under tuple of :mpltype:`color`, default: None - If tuple, the `under` value of each component is set with the values + If tuple, the 'under' value of each component is set with the values from the tuple. over tuple of :mpltype:`color`, default: None - If tuple, the `over` value of each component is set with the values + If tuple, the 'over' value of each component is set with the values from the tuple. Returns diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 25e6a3bd5ee8..f8061db1772b 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -281,7 +281,14 @@ def __init__(self, ax, self.set_filternorm(filternorm) self.set_filterrad(filterrad) self.set_interpolation(interpolation) - self.set_interpolation_stage(interpolation_stage) + if isinstance(self.norm, mcolors.MultiNorm): + if interpolation_stage not in [None, 'data', 'auto']: + raise ValueError("'data' is the only valid interpolation_stage " + "when using multiple color channels, not " + f"{interpolation_stage}") + self.set_interpolation_stage('data') + else: + self.set_interpolation_stage(interpolation_stage) self.set_resample(resample) self.axes = ax @@ -471,30 +478,28 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, # input data is not going to match the size on the screen so we # have to resample to the correct number of pixels - if A.dtype.kind == 'f': # Float dtype: scale to same dtype. - scaled_dtype = np.dtype("f8" if A.dtype.itemsize > 4 else "f4") - if scaled_dtype.itemsize < A.dtype.itemsize: - _api.warn_external(f"Casting input data from {A.dtype}" - f" to {scaled_dtype} for imshow.") - else: # Int dtype, likely. - # TODO slice input array first - # Scale to appropriately sized float: use float32 if the - # dynamic range is small, to limit the memory footprint. - da = A.max().astype("f8") - A.min().astype("f8") - scaled_dtype = "f8" if da > 1e8 else "f4" - - # resample the input data to the correct resolution and shape - A_resampled = _resample(self, A.astype(scaled_dtype), out_shape, t) + if A.dtype.fields is None: # scalar data and colormap + arrs, norms, dtypes = [A], [self.norm], [A.dtype] + else: # using a multivariate colormap + arrs = [A[f] for f in A.dtype.fields] + norms = self.norm.norms + dtypes = [A.dtype.fields[f][0] for f in A.dtype.fields] + + A_resampled = [_resample(self, a.astype(_get_scaled_dtype(a)), + out_shape, t) + for a in arrs] # if using NoNorm, cast back to the original datatype - if isinstance(self.norm, mcolors.NoNorm): - A_resampled = A_resampled.astype(A.dtype) + for i, n in enumerate(norms): + if isinstance(n, mcolors.NoNorm): + A_resampled[i] = A_resampled[i].astype(dtypes[i]) # Compute out_mask (what screen pixels include "bad" data # pixels) and out_alpha (to what extent screen pixels are # covered by data pixels: 0 outside the data extent, 1 inside # (even for bad data), and intermediate values at the edges). - mask = (np.where(A.mask, np.float32(np.nan), np.float32(1)) + mask = (np.where(self._getmaskarray(A), np.float32(np.nan), + np.float32(1)) if A.mask.shape == A.shape # nontrivial mask else np.ones_like(A, np.float32)) # we always have to interpolate the mask to account for @@ -507,8 +512,13 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, alpha = self.get_alpha() if alpha is not None and np.ndim(alpha) > 0: out_alpha *= _resample(self, alpha, out_shape, t, resample=True) - # mask and run through the norm - resampled_masked = np.ma.masked_array(A_resampled, out_mask) + + # mask + resampled_masked = [np.ma.masked_array(r, out_mask) + for r in A_resampled] + + if A.dtype.fields is None: + resampled_masked = resampled_masked[0] res = self.norm(resampled_masked) else: if A.ndim == 2: # interpolation_stage = 'rgba' @@ -669,8 +679,15 @@ def _normalize_image_array(A): """ A = cbook.safe_masked_invalid(A, copy=True) if A.dtype != np.uint8 and not np.can_cast(A.dtype, float, "same_kind"): - raise TypeError(f"Image data of dtype {A.dtype} cannot be " - f"converted to float") + if A.dtype.fields is None: + raise TypeError(f"Image data of dtype {A.dtype} cannot be " + f"converted to float") + else: + for key in A.dtype.fields: + if not np.can_cast(A[key].dtype, float, "same_kind"): + raise TypeError(f"Image data of dtype {A.dtype} cannot be " + f"converted to a sequence of floats") + if A.ndim == 3 and A.shape[-1] == 1: A = A.squeeze(-1) # If just (M, N, 1), assume scalar and apply colormap. if not (A.ndim == 2 or A.ndim == 3 and A.shape[-1] in [3, 4]): @@ -1851,3 +1868,20 @@ def thumbnail(infile, thumbfile, scale=0.1, interpolation='bilinear', ax.imshow(im, aspect='auto', resample=True, interpolation=interpolation) fig.savefig(thumbfile, dpi=dpi) return fig + + +def _get_scaled_dtype(A): + + if A.dtype.kind == 'f': # Float dtype: scale to same dtype. + scaled_dtype = np.dtype('f8' if A.dtype.itemsize > 4 else 'f4') + if scaled_dtype.itemsize < A.dtype.itemsize: + _api.warn_external(f"Casting input data from {A.dtype}" + f" to {scaled_dtype} for imshow.") + else: # Int dtype, likely. + # TODO slice input array first + # Scale to appropriately sized float: use float32 if the + # dynamic range is small, to limit the memory footprint. + da = A.max().astype("f8") - A.min().astype("f8") + scaled_dtype = "f8" if da > 1e8 else "f4" + + return scaled_dtype diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index dd80da45e332..3db98478fa53 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -83,7 +83,11 @@ from matplotlib.scale import get_scale_names # noqa: F401 from matplotlib.cm import _colormaps -from matplotlib.colors import _color_sequences, Colormap +from matplotlib.colors import (_color_sequences, + Colormap, + BivarColormap, + MultivarColormap, + ) import numpy as np @@ -160,7 +164,7 @@ # We may not need the following imports here: -from matplotlib.colors import Normalize +from matplotlib.colors import Norm, Normalize from matplotlib.lines import Line2D, AxLine from matplotlib.text import Text, Annotation from matplotlib.patches import Arrow, Circle, Rectangle # noqa: F401 @@ -3759,14 +3763,14 @@ def hlines( @_copy_docstring_and_deprecators(Axes.imshow) def imshow( X: ArrayLike | PIL.Image.Image, - cmap: str | Colormap | None = None, - norm: str | Normalize | None = None, + cmap: str | Colormap | BivarColormap | MultivarColormap | None = None, + norm: str | Norm | None = None, *, aspect: Literal["equal", "auto"] | float | None = None, interpolation: str | None = None, alpha: float | ArrayLike | None = None, - vmin: float | None = None, - vmax: float | None = None, + vmin: float | tuple[float] | None = None, + vmax: float | tuple[float] | None = None, colorizer: Colorizer | None = None, origin: Literal["upper", "lower"] | None = None, extent: tuple[float, float, float, float] | None = None, @@ -3878,10 +3882,10 @@ def pcolor( *args: ArrayLike, shading: Literal["flat", "nearest", "auto"] | None = None, alpha: float | None = None, - norm: str | Normalize | None = None, - cmap: str | Colormap | None = None, - vmin: float | None = None, - vmax: float | None = None, + norm: str | Norm | None = None, + cmap: str | Colormap | BivarColormap | MultivarColormap | None = None, + vmin: float | tuple[float] | None = None, + vmax: float | tuple[float] | None = None, colorizer: Colorizer | None = None, data=None, **kwargs, @@ -3907,10 +3911,10 @@ def pcolor( def pcolormesh( *args: ArrayLike, alpha: float | None = None, - norm: str | Normalize | None = None, - cmap: str | Colormap | None = None, - vmin: float | None = None, - vmax: float | None = None, + norm: str | Norm | None = None, + cmap: str | Colormap | BivarColormap | MultivarColormap | None = None, + vmin: float | tuple[float] | None = None, + vmax: float | tuple[float] | None = None, colorizer: Colorizer | None = None, shading: Literal["flat", "nearest", "gouraud", "auto"] | None = None, antialiased: bool = False, diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bivariate_cmap_shapes.png b/lib/matplotlib/tests/baseline_images/test_axes/bivariate_cmap_shapes.png new file mode 100644 index 0000000000000000000000000000000000000000..a3339afa754541370107adf321f06915bafd18e5 GIT binary patch literal 4116 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bivariate_visualizations.png b/lib/matplotlib/tests/baseline_images/test_axes/bivariate_visualizations.png new file mode 100644 index 0000000000000000000000000000000000000000..536a440874d6d4d686d895f6c7edcc12f4cbfc91 GIT binary patch literal 8311 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 Zp*DV1TwPoh9~?RY-k!c5555RK_b&<~UM2tl literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_axes/multivariate_imshow_norm.png b/lib/matplotlib/tests/baseline_images/test_axes/multivariate_imshow_norm.png new file mode 100644 index 0000000000000000000000000000000000000000..134c73f1ca85fb9fe1d5813d8e27c91b55948384 GIT binary patch literal 6902 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_axes/multivariate_pcolormesh_norm.png b/lib/matplotlib/tests/baseline_images/test_axes/multivariate_pcolormesh_norm.png new file mode 100644 index 0000000000000000000000000000000000000000..e85e27fd183da992371f7cf511f6006eb8b5026e GIT binary patch literal 6576 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_axes/multivariate_visualizations.png b/lib/matplotlib/tests/baseline_images/test_axes/multivariate_visualizations.png new file mode 100644 index 0000000000000000000000000000000000000000..cf51df7650576ba2509dc2f2ea590a88425ececf GIT binary patch literal 8222 zcmd6MXIxWR*Kbrt)Dd|G#6oo}^j-|TSU`I3ZK$Dz4go?_1Ox;W1SA-$fV413F9B&% zLoW)Ugt1&&+~idz4ya?@2C3#a+2TPXP>>-TK}?6goc^|)iwHSAP|U3@x?PO z5a`kpaNU08GVmY1*rg79=zBgl@YHs(^@Lft+kjLpJYAh!Je}+FOcQ#|Qn-bY2&C2p{bq zIX%DX>r#iDY_+w^b>p@2bG)6uf{n8!MofOxeQaeZDF=&9tm!%6ko2RU zX?k@8jpMD`XeGR@m2W)Ke8;|}B77w-T6-f@?}JK)+`A=taPUTMeiv)a*6yX5_ird& zh(}>g;}yRFj|c+!(-9;toL|X?+#r3*-s?-hM7sDFNSJhLAP@zJO8ord!oP=r{(l_iO&n&#{JYHc`j+B@u^%fP@ufA_8?`^{|uA;wVXc<15{A~jV{6pJNp3_XLmma;PI z7U`9jm3@Bs^1@06MuS$gy#VoTt3UmcY*LrCAR;Y|C_z>AFa_gaYnzjI2bklQ#mY>Y z;3=16^Y^_KFTy3HXjRIB(B zcK5baM9RnGS8Q%)?t4#1trD-*9;OtZ-iAV<90^JybOpL1fBNd*9(r9Xvh~G;H@C4- zPJKl#la<-74bg5}s>B+PY7Hf-4C;s@z0s_${%=XoDRlj5n1v+4M_v6Hnm2HNeIu&& z)@r(SP%)bu?WL2{mS{Y|mqtI1D=DcWpY`F!$}5CVJ6<`4rDZ3R`Jn^urFUV3wV-0i zWKNaHpQ2U$=kW~&rOQ#0uI_>C`p%Q$3TXZOm%hJ4XcZAT%T|wNAsOUf_!BMtf&Js> zl5Cu@L+d>?k#MnusM@bKT#PZDONt#!svPgdGln)fo@d6Yf4eFfd6@~gvuyLi!h+|) z_WVg23-|D_Y9!pZh|gTp`J_f^#2aIkpAhH$yW<#7p}iwiP8oa}d#Ei$PcKn0)K1IO za|i$G*F8QZTCQClb}%e}N%yS`P<|~)cpx>3()%!Fq7&$$88P=EN9`olkM;qxO{Dj8 za$1M~;3?!dT9#=an{W`r9P9N73y_dYgwebCIADkXPxgWSbStX@H4zJ&}&X} z$Iq!Ye|`-<*2BXQK+tW)1)y5xk8lO_q%(5;yJ9zEeexKhpR%Js*Iiq4uqDBnM3jt( zB|IT7DdJS>z11kF&+CrSM(}R7R^Ca`c;es_Sf|UR76Ep~{?l8z*SMW$d88a> zGd`S{db?5Xd{qSx50B1y_ao!SDSHx+zG=5gX;(@IjF!TCsN<(hhTI9}{$k)`g^(NR z7Kt{?MU|6rHij*G_fVrH7p6t8H){wcA9r8&42^}9kGQa66<2U*%`}UMtnI>w7e`L>xWI;R$J3%=!5j)ouGO46; z!2+3Vsew<~()?84e{oyd8tKUaLOP`3vC;h^a_lOj0V1a3x1OEZfbe@ ziqp8BblT|dZdsEBvEEohNmbIos15(f`whJAhuJU%2sB{}f$OF?rj5w3-yDb-G#}{O z*H%sAbKYHz$errjn>sBjoZxAj1+sqKCMbJ1KIq4;SU1y z>*fwEZVpWa^!L}dENe|^K_JhRy;>ILn$KfHYg{PdMe1i(Xg}Pt%t({VS*4QRh7$TmZ>}Ip|;?#;Xj8A0u|7=#hTk_dMF(zD})j4SI++!RGnC zh3c1=Y)7yT@YMtc0vYR` zHWD(2hhG87=8l_G8^JI;r$bkxkv@~9I}B1BNYRr=Jb~N9m^M=Vl+dQ>i!yF;Q&U_5 z^IQa?x1>Z=$d&QNvc$A_;dYZG&jcmKJhh%$^(6HsMM(&PoqZ6OUcDp(g}xy8)@o0j zwDe9LPO7Ln!O5L4U(;1wG*vqgCppdcD;?}lCS90_chMpA$#p-znq-!w))137=90>d z4IB-nH!07nFc_s>R-wzkA->L%5{Q~7`cp@z=?hOTRt0(Nz002NBaji(6NxVxk2$(u z!kjIyUdf+w)HL%ZtgTmKy*Noo{8DKZfZV0r%854kGbcn&?9Y2wzFY-O+-pei!h$6O z)n@*dDH_<^_brIMZe*2UMHi_OhGcpJ(0m7&p@8BDq{NtMaU#>aj9YZP)KA#RnAdWg)ndpdB7r$z|QTv6Tli@z{^2OH( zAVVgcA@J;lS#*Tvb1O7YPfo?l#=B54?t+R^bkj38rpQ0YD3E2uQ2=CcHJSSmoANPUFsQkL`Z@m6FZN+pf8`Kh)jA|M09ea{IJ&15YLm*-=+UIhbA8$(*p_ zrxu%ayyZ=j>r)M7m9uy$&7s#Yk(uXGFD6F;&_cFXaEY+Qw)h!+>X>PI|E1~u$NYpg znWG=x!{KkIMb4j4^ed3Wd#)}0!m2E#7Z8iW)kf?$yQ89z+nI73DvgCtfG|)zVU5@1 zOcX5o$xKF5oS4;+RJFAkjaQUB+?Des;(9@t@rmmgA5AW57e5l z#`hl|*Kw6NPuAtd%A6XEqD53->%aHn!HCIo%h?m7G0$9K)|62@H|Ei7hA0=l}ojLghNdIJeU=*_0z{Z95q>0)ca>T2wac?Uik ze0O1;TsLnlypcgXB}d%?6KLK`@O)1kiX?9}R4jmtXZ=iEiz%DDsQ-_IKc|#?= z2VXS45FQplN7E^H;C75CA++hx2e*J!nO#(4jm(?Rbt&wH3%gzFXGg-r6M-3%l8J+^Mdg z*3L6@N$^*}Tk#kJqCA^RAH|*qbMf@Eu#j6_U2O{t#9B~Hp!9Tjna^na`9w{)$9VEdEWzAcQJAgb+ z`w1LK-QRU}YaM@mJKAU{cCYp?TpVEtm=s8gO^I7o(ra{!6P53ixJ@=P>LzHv;rOaV z8z%Tbwd=zb(MF>`r?3>B&YW`qzDE)J(qWwBLQFL5BfS8-6~ z`UqoE*j}BFIbYF~H@?FzFz~dbO?x$dC92I-qF1)Fminf7?coD;T#UI7Ccf%YZLUeI zJ-)i~wPu0ClWi35m#Z+x%v*f__7$Rb9v*{(`w2?VbL<^lT{qi%@rrNR86ygeIHW81 z8oZQS(RIZ{Ul(q3H*C@iwQ_#iGWsMEds!_@F+NiTxi*iC=>66E#|?#{Gz1DgSOJKD zt*v+mtq#t1tZTi05P;U9KhuYS`VQuW+zGqhyeUCNaYf|9H!3;jzDM?d@$-1h+F}A2 zxb#J&cV0Ekj8rN!aHX|Z6o`a}Eul>6!`C4aifM1;!IE`An{CsreBUiv+<< zw}1{>=#Q&vFzZ>m{x%(!*Yr%zyKc^*o8@d=8TB1L%{Oue#8|4s3(z=PmcomEDh1Q{ z0IPpM9p;M;=Syh??2`U{vFuldaP-!2HuVR3sVrAf_cag3;e}2%&f73X;*pduQ(fB? zk_!$-2@Cr6yOEfA58ePZO%ijVk?h64d9@j)VPie6N2h6j=?PdEua+=E@2uwZ z&bEwoE%)rDjY`@29Q!M8goX!^6-pJ#lz!KsPCai%+bhe8Fwdq@k+_70w(B;Li zd~XZNjnh42CJrsxx8iz;9a*L1E`jUBj~^d1H8=FUQa^sw5M5J~7#x+!$>@KY$X^Pb zs6O?87Meg0{hvD;gQWu%M%MKvW&z4!9pn=UkS?Pp*C8rOK1rm^FLgIU_SYSqjzAKWYVRj>~K z2D6`2c3G~(Xb`G4>a6f|=xyB7(iL^2#zP18n>PxyI6B9E##L9dd(7rU!lTb7u7)-t z#CB{#o68(0!eDjHfc3-l_8$FGHjKYDfcxG?L|n}tw6`fW)Ss0mVm(r^Q_R;wg*_I! z4tK#aI^|jpOJc)Hh6~?SlNrFKT~?ll9xJOXFdpVHN56KQufTr2^ z(JyUHt6b$|PPbwcdPmLG3N;};pZeR|-*AQIS8lKKqO$uAyej+((S2?jH+2IdEy`X&)x9{~>rSb(E&RP1_U{Yy7Y>d}LB zi4AvhQAzG$J$kDJ^TxaQ8VWl)Iu>Xh`T)VKiuC`vv$MNPPYB+MO?uHJCOylsC1rmb&}QT`3o^jQw`8Y+LSL$Z5#iNx4zWuqPI(pUsD8Z`bD-9PgW( zr2oo!yNJ9zj)0w%DIIn7*{s$F4<3-_(?q-uPi8`a1@Zx78%v+&o3Y$aS{ouw%q3ct zFEPW)C~fZn2LtL8WK&Nas%$ucqE86pMg|71lDs#sEr;GV>su{uK>0{cHeE-CHfxd= z@B5)`WL}?%J@iT_#D}FaTq8kpf8V}gc39e`sCF3_xrC_x<2x4KTYc8zFs<=ajEah? zai5Pwnbxun3ey2uBrcx6v)H?eYmK63VDJ#~8f}4J78W`B%Bo)f8HuM$UImv`mj3DJ zqCAg7IsLpAZYAg!C+MfG^jVEv#9!$>yYj6}>0ir{8)>jdBWr7gwh+kPcLtX#n&M%= zI@4UDRl2USkGv*M0ocXP=t=DkS0a9j8NKRIM|rw_Pz8#7E;Zn& zV&3_uNGL|Idy0Ws zkK`a3K-Hc*I^a7S@ZB{(q6Ph$MgW#1ewgJrLgG5D&h~%bl||w`#jOf$w2Qd_%C2PHQiY3C--XXX(Gn< zMC~dtGPtURX-S+r4bbTz3fc$%g9a_&JptAD-p24NwOP^}P8H zDtWjt&FUb)FU=*=&!4`{^QJuyq-f{A8=~p6x;6y{JdW8_MG3eHop-pIH$A&COC9wV zt<7c56T!t+z%xzTIXkOWn>Yt`*_Hy1&okPxJTvxpEO8LAkwy4pO zt}b*}ZyEl`U{wIn^2h_6XVE0`wkX2<_39wgUx5QVVC#=)=@Yg^Ou2$#?>hBpCy&cW3 zabmTcC&%BvR(MAR=DuPjwXY{hcIqEJ0Z?#{_K(CuE!~E?^8C?WD#K zDB)d}J;ydaObd~6EWPxD^1wRbiu$+ z(b_OkW$?H=Y+YUzBzyEXl%v7xtRc?2Yovj@_z*!G6RV8<2gKj_Pv|M$Xsi0;k!S0N z)n-a&@6SY-| zEc)fuEjA8D24Mf5)&IrnC8*_^o^9fX!<{3jC*w>`rtvzoK;q}=e13Ae)du}}I6WC= zCM7j-__^;UaGXo-Cb};}0M4?b9v90qDnf6BC2Qvi<{7-MMT}oPKiuH3lKU$B2DL;m zTdTwKQ9V`DPmE56YIe-#xj=G)?Cx+iRk^T_6v!|&q{}c$7e$@Wu28kDFuUky<1g3W zuCA$=S9@i5mmE7PtLJxIo;|UkYCH6l7RU*b@zd+wRp%@m^T@clt(EQX)16=`XB>)~ zp}fxzlqGO~+2)VAP4F2FgGHJw`e@?vZR5%^{eU0QNs$B286~0>PCQNvdzO|hSt>J+ z<{93 z{~e1GI(kma$$V<1J2vSiV>@%27839w=skn%KN@$SR}bj${ahjTCxZ)_-q$LWva_Uu z0$lVHB8y(Mq+~yx;icc@jqjZ@XWuG59oMDKG~c?H^YDZ^7FUt$FI}v|Iqx7Dfv|%( zlUf2-61RLB)Y=sGqK;SIr-GfRmz;c!;1Yj6x-6-50|i)q!NI}7PTNxYJ9jkLb5B$x5VW{s^PgMU!^eI8-1Az7~$xB=hB6YwzBQyF^ z-*5IWcYa`9lT#c}GZS8dzPgO=dfjY+>F#D+7VEJ73Pt?La;O|#Xl1v5=kTgZAr{jv z2J|cVD*wdKXNv#9+;8cVy)qgiq(M0B{*kc2ayN=``0pVvKTS;LAU354goMaC_by*(I zxIOpEaMx1TAB<#ick}E>Y4w^G2{V<`hIrin*`BD6LjIP6_mQ~EvP5D@d5#-$>;P7c07;p%pg4aWQ z1%hwY!nO1DCZap8_`(08ARmvVqv{ypul(@)RDQ<4a1_Lz^w}nM{k}zVIpG)AUHJPG zdg?QLX4+}<3M;v!HVqr=f_!WB?fS+qkQM?s=k;U{!L`YSyi$V??b6cODwli{g`tk)@)2YDO7xBmgV+m z3hWZ7X3W?HoqcfN+b|fceCyUN&%@p297_#aWNS3SX9z!DNdmH{@DYIX5Xs4IJj^2f zr=<6TK#~A}7m}52Y;24`B8Q8ND?cb@`*(K-Q8E^l6e@6 zD%Fh}kLjWiOV^!8TC(zo^Ai&jh57l@HP}6uwvfjVtaCA@0y@9rtE#Tf#pu1EHkvi^ zI&BOu<)K|Px~{7_y-xz7nscE(3-kx_blL)a6j|e$j>fS2!OvAaZ&k{8hBwbxMVEPP zr@2X>AM@2)OE|0U=OHJBDwzO`C$u`&&N|YNs^Gt!wSSn!9lsq6*wGA!UH5@DM&ZF` z#>BaqP_;lPGCyUi)Hx83f31i+pLzxKKMCCbMQs0HhU^E$Pe!|_E1Bt&o=Ne!+B4LX Hzh3<>(#I~T literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 6751666360b1..36d5b4093166 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -10366,3 +10366,237 @@ def test_errorbar_uses_rcparams(): assert_allclose([cap.get_markeredgewidth() for cap in caplines], 2.5) for barcol in barlinecols: assert_allclose(barcol.get_linewidths(), 1.75) + + +@image_comparison(["bivariate_visualizations.png"], style='mpl20') +def test_bivariate_visualizations(): + x_0 = np.arange(25, dtype='float32').reshape(5, 5) % 5 + x_1 = np.arange(25, dtype='float32').reshape(5, 5).T % 5 + + fig, axes = plt.subplots(1, 6, figsize=(10, 2)) + + axes[0].imshow((x_0, x_1), cmap='BiPeak', interpolation='nearest') + axes[1].matshow((x_0, x_1), cmap='BiPeak') + axes[2].pcolor((x_0, x_1), cmap='BiPeak') + axes[3].pcolormesh((x_0, x_1), cmap='BiPeak') + + x = np.arange(5) + y = np.arange(5) + X, Y = np.meshgrid(x, y) + axes[4].pcolormesh(X, Y, (x_0, x_1), cmap='BiPeak') + + patches = [ + mpl.patches.Wedge((.3, .7), .1, 0, 360), # Full circle + mpl.patches.Wedge((.7, .8), .2, 0, 360, width=0.05), # Full ring + mpl.patches.Wedge((.8, .3), .2, 0, 45), # Full sector + mpl.patches.Wedge((.8, .3), .2, 22.5, 90, width=0.10), # Ring sector + ] + colors_0 = np.arange(len(patches)) // 2 + colors_1 = np.arange(len(patches)) % 2 + p = mpl.collections.PatchCollection(patches, cmap='BiPeak', alpha=0.5) + p.set_array((colors_0, colors_1)) + axes[5].add_collection(p) + remove_ticks_and_titles(fig) + + +@image_comparison(["multivariate_visualizations.png"], style='mpl20') +def test_multivariate_visualizations(): + x_0 = np.arange(25, dtype='float32').reshape(5, 5) % 5 + x_1 = np.arange(25, dtype='float32').reshape(5, 5).T % 5 + x_2 = np.arange(25, dtype='float32').reshape(5, 5) % 6 + + fig, axes = plt.subplots(1, 6, figsize=(10, 2)) + + axes[0].imshow((x_0, x_1, x_2), cmap='3VarAddA', interpolation='nearest') + axes[1].matshow((x_0, x_1, x_2), cmap='3VarAddA') + axes[2].pcolor((x_0, x_1, x_2), cmap='3VarAddA') + axes[3].pcolormesh((x_0, x_1, x_2), cmap='3VarAddA') + + x = np.arange(5) + y = np.arange(5) + X, Y = np.meshgrid(x, y) + axes[4].pcolormesh(X, Y, (x_0, x_1, x_2), cmap='3VarAddA') + + patches = [ + mpl.patches.Wedge((.3, .7), .1, 0, 360), # Full circle + mpl.patches.Wedge((.7, .8), .2, 0, 360, width=0.05), # Full ring + mpl.patches.Wedge((.8, .3), .2, 0, 45), # Full sector + mpl.patches.Wedge((.8, .3), .2, 22.5, 90, width=0.10), # Ring sector + ] + colors_0 = np.arange(len(patches)) // 2 + colors_1 = np.arange(len(patches)) % 2 + colors_2 = np.arange(len(patches)) % 3 + p = mpl.collections.PatchCollection(patches, cmap='3VarAddA', alpha=0.5) + p.set_array((colors_0, colors_1, colors_2)) + axes[5].add_collection(p) + remove_ticks_and_titles(fig) + + +@image_comparison(["multivariate_pcolormesh_alpha.png"], style='mpl20') +def test_multivariate_pcolormesh_alpha(): + """ + Check that the the alpha keyword works for pcolormesh + This test covers all plotting modes that use the same pipeline + (inherit from Collection). + """ + x_0 = np.arange(25, dtype='float32').reshape(5, 5) % 5 + x_1 = np.arange(25, dtype='float32').reshape(5, 5).T % 5 + x_2 = np.arange(25, dtype='float32').reshape(5, 5) % 6 + + fig, axes = plt.subplots(2, 3) + + axes[0, 0].pcolormesh(x_1, alpha=0.5) + axes[0, 1].pcolormesh((x_0, x_1), cmap='BiPeak', alpha=0.5) + axes[0, 2].pcolormesh((x_0, x_1, x_2), cmap='3VarAddA', alpha=0.5) + + al = np.arange(25, dtype='float32').reshape(5, 5)[::-1].T % 6 / 5 + + axes[1, 0].pcolormesh(x_1, alpha=al) + axes[1, 1].pcolormesh((x_0, x_1), cmap='BiPeak', alpha=al) + axes[1, 2].pcolormesh((x_0, x_1, x_2), cmap='3VarAddA', alpha=al) + remove_ticks_and_titles(fig) + + +@image_comparison(["multivariate_imshow_alpha.png"], style='mpl20') +def test_multivariate_imshow_alpha(): + """ + Check that the the alpha keyword works for imshow. + """ + x_0 = np.arange(25, dtype='float32').reshape(5, 5) % 5 + x_1 = np.arange(25, dtype='float32').reshape(5, 5).T % 5 + x_2 = np.arange(25, dtype='float32').reshape(5, 5) % 6 + + fig, axes = plt.subplots(2, 3) + + # interpolation='nearest' to reduce size of baseline image + axes[0, 0].imshow(x_1, interpolation='nearest', alpha=0.5) + axes[0, 1].imshow((x_0, x_1), interpolation='nearest', cmap='BiPeak', alpha=0.5) + axes[0, 2].imshow((x_0, x_1, x_2), interpolation='nearest', + cmap='3VarAddA', alpha=0.5) + + al = np.arange(25, dtype='float32').reshape(5, 5)[::-1].T % 6 / 5 + + axes[1, 0].imshow(x_1, interpolation='nearest', alpha=al) + axes[1, 1].imshow((x_0, x_1), interpolation='nearest', cmap='BiPeak', alpha=al) + axes[1, 2].imshow((x_0, x_1, x_2), interpolation='nearest', + cmap='3VarAddA', alpha=al) + remove_ticks_and_titles(fig) + + +@image_comparison(["multivariate_pcolormesh_norm.png"], style='mpl20') +def test_multivariate_pcolormesh_norm(): + """ + Test vmin, vmax and norm + Norm is checked via a LogNorm, as this converts + A LogNorm converts the input to a masked array, masking for X <= 0 + By using a LogNorm, this functionality is also tested. + This test covers all plotting modes that use the same pipeline + (inherit from Collection). + """ + x_0 = np.arange(25, dtype='float32').reshape(5, 5) % 5 + x_1 = np.arange(25, dtype='float32').reshape(5, 5).T % 5 + x_2 = np.arange(25, dtype='float32').reshape(5, 5) % 6 + + fig, axes = plt.subplots(3, 5) + + axes[0, 0].pcolormesh(x_1) + axes[0, 1].pcolormesh((x_0, x_1), cmap='BiPeak') + axes[0, 2].pcolormesh((x_0, x_1, x_2), cmap='3VarAddA') + axes[0, 3].pcolormesh((x_0, x_1), cmap='BiPeak') + axes[0, 4].pcolormesh((x_0, x_1, x_2), cmap='3VarAddA') + + vmin = 1 + vmax = 3 + axes[1, 0].pcolormesh(x_1, vmin=vmin, vmax=vmax) + axes[1, 1].pcolormesh((x_0, x_1), cmap='BiPeak', vmin=[vmin]*2, vmax=[vmax]*2) + axes[1, 2].pcolormesh((x_0, x_1, x_2), cmap='3VarAddA', + vmin=[vmin]*3, vmax=[vmax]*3) + axes[1, 3].pcolormesh((x_0, x_1), cmap='BiPeak', + vmin=(None, vmin), vmax=(None, vmax)) + axes[1, 4].pcolormesh((x_0, x_1, x_2), cmap='3VarAddA', + vmin=(None, vmin, None), vmax=(None, vmax, None)) + + n = mcolors.LogNorm(vmin=1, vmax=5) + axes[2, 0].pcolormesh(x_1, norm=n) + axes[2, 1].pcolormesh((x_0, x_1), cmap='BiPeak', norm=(n, n)) + axes[2, 2].pcolormesh((x_0, x_1, x_2), cmap='3VarAddA', norm=(n, n, n)) + axes[2, 3].pcolormesh((x_0, x_1), cmap='BiPeak', norm=('linear', n)) + axes[2, 4].pcolormesh((x_0, x_1, x_2), cmap='3VarAddA', + norm=('linear', n, 'linear')) + + remove_ticks_and_titles(fig) + + +@image_comparison(["multivariate_imshow_norm.png"], style='mpl20') +def test_multivariate_imshow_norm(): + """ + Test vmin, vmax and norm + Norm is checked via a LogNorm. + A LogNorm converts the input to a masked array, masking for X <= 0 + By using a LogNorm, this functionality is also tested. + """ + x_0 = np.arange(25, dtype='float32').reshape(5, 5) % 5 + x_1 = np.arange(25, dtype='float32').reshape(5, 5).T % 5 + x_2 = np.arange(25, dtype='float32').reshape(5, 5) % 6 + + fig, axes = plt.subplots(3, 5) + + # interpolation='nearest' to reduce size of baseline image and + # removes ambiguity when using masked array (from LogNorm) + axes[0, 0].imshow(x_1, interpolation='nearest') + axes[0, 1].imshow((x_0, x_1), interpolation='nearest', cmap='BiPeak') + axes[0, 2].imshow((x_0, x_1, x_2), interpolation='nearest', cmap='3VarAddA') + axes[0, 3].imshow((x_0, x_1), interpolation='nearest', cmap='BiPeak') + axes[0, 4].imshow((x_0, x_1, x_2), interpolation='nearest', cmap='3VarAddA') + + vmin = 1 + vmax = 3 + axes[1, 0].imshow(x_1, interpolation='nearest', vmin=vmin, vmax=vmax) + axes[1, 1].imshow((x_0, x_1), interpolation='nearest', cmap='BiPeak', + vmin=[vmin]*2, vmax=[vmax]*2) + axes[1, 2].imshow((x_0, x_1, x_2), interpolation='nearest', cmap='3VarAddA', + vmin=[vmin]*3, vmax=[vmax]*3) + axes[1, 3].imshow((x_0, x_1), interpolation='nearest', cmap='BiPeak', + vmin=(None, vmin), vmax=(None, vmax)) + axes[1, 4].imshow((x_0, x_1, x_2), interpolation='nearest', cmap='3VarAddA', + vmin=(None, vmin, None), vmax=(None, vmax, None)) + + n = mcolors.LogNorm(vmin=1, vmax=5) + axes[2, 0].imshow(x_1, interpolation='nearest', norm=n) + axes[2, 1].imshow((x_0, x_1), interpolation='nearest', cmap='BiPeak', norm=(n, n)) + axes[2, 2].imshow((x_0, x_1, x_2), interpolation='nearest', cmap='3VarAddA', + norm=(n, n, n)) + axes[2, 3].imshow((x_0, x_1), interpolation='nearest', cmap='BiPeak', + norm=('linear', n)) + axes[2, 4].imshow((x_0, x_1, x_2), interpolation='nearest', cmap='3VarAddA', + norm=('linear', n, 'linear')) + + remove_ticks_and_titles(fig) + + +@image_comparison(["bivariate_cmap_shapes.png"], style='mpl20') +def test_bivariate_cmap_shapes(): + x_0 = np.arange(100, dtype='float32').reshape(10, 10) % 10 + x_1 = np.arange(100, dtype='float32').reshape(10, 10).T % 10 + + fig, axes = plt.subplots(1, 4, figsize=(10, 2)) + + # shape = square + axes[0].imshow((x_0, x_1), cmap='BiPeak', vmin=(1, 1), vmax=(8, 8), + interpolation='nearest') + # shape = cone + axes[1].imshow((x_0, x_1), cmap='BiCone', vmin=(0.5, 0.5), vmax=(8.5, 8.5), + interpolation='nearest') + + # shape = ignore + cmap = mpl.bivar_colormaps['BiCone'] + cmap = cmap.with_extremes(shape='ignore') + axes[2].imshow((x_0, x_1), cmap=cmap, vmin=(1, 1), vmax=(8, 8), + interpolation='nearest') + + # shape = circleignore + cmap = mpl.bivar_colormaps['BiCone'] + cmap = cmap.with_extremes(shape='circleignore') + axes[3].imshow((x_0, x_1), cmap=cmap, vmin=(0.5, 0.5), vmax=(8.5, 8.5), + interpolation='nearest') + remove_ticks_and_titles(fig) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index e193e0d9fd3e..20a7ca7b042c 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -2055,3 +2055,12 @@ def test_affine_fill_to_edges(): axs[i, j].vlines([-0.5, N - 0.5], N - 3, N, lw=0.5, color='red') axs[i, j].hlines([-0.5, N - 0.5], -1, 2, lw=0.5, color='red') axs[i, j].hlines([-0.5, N - 0.5], N - 3, N, lw=0.5, color='red') + + +def test_invalid_interpolation_stage_multinorm(): + fig, ax = plt.subplots() + data = np.arange(24).reshape((2, 3, 4)) + + with pytest.raises(ValueError, + match="'data' is the only valid interpolation_stage"): + ax.imshow(data, cmap='2VarAddA', interpolation_stage='rgba') From da497df0f6c50d21bba939f67e39ba1e9ad704cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Sun, 30 Nov 2025 22:23:08 +0100 Subject: [PATCH 2/8] updates from code review thank you @story645 @ksunden --- lib/matplotlib/axes/_axes.py | 39 ++++++++-------- lib/matplotlib/colorizer.py | 2 +- lib/matplotlib/image.py | 44 +++++++++--------- .../test_axes/multivariate_visualizations.png | Bin 8222 -> 8222 bytes lib/matplotlib/tests/test_axes.py | 25 +++++----- 5 files changed, 54 insertions(+), 56 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 8c4ad62d865f..d2d197434968 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6273,8 +6273,9 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, See :doc:`/gallery/images_contours_and_fields/image_antialiasing` for a discussion of image antialiasing. - Only 'data' is available when using `~matplotlib.colors.BivarColormap` - or `~matplotlib.colors.MultivarColormap` + When using a `~matplotlib.colors.BivarColormap` or + `~matplotlib.colors.MultivarColormap`, 'data' is the only valid + interpolation_stage. alpha : float or array-like, optional The alpha blending value, between 0 (transparent) and 1 (opaque). @@ -6537,10 +6538,10 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, Parameters ---------- - C : 2D or 3D array-like + C : 2D (I, J) or 3D (v, I, J) array-like The color-mapped values. Color-mapping is controlled by *cmap*, *norm*, *vmin*, and *vmax*. 3D arrays are supported only if the - cmap supports v channels, where v is the size along the first axis. + cmap supports v channels. X, Y : array-like, optional The coordinates of the corners of quadrilaterals of a pcolormesh:: @@ -6665,12 +6666,14 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, shading = mpl.rcParams['pcolor.shading'] shading = shading.lower() + mcolorizer.ColorizingArtist._check_exclusionary_keywords(colorizer, + vmin=vmin, vmax=vmax, + norm=norm, cmap=cmap) if colorizer is None: - cmap = mcolorizer._ensure_cmap(cmap, accept_multivariate=True) - C = mcolorizer._ensure_multivariate_data(args[-1], cmap.n_variates) - else: - C = mcolorizer._ensure_multivariate_data(args[-1], - colorizer.cmap.n_variates) + colorizer = mcolorizer.Colorizer(cmap=cmap, norm=norm) + + C = mcolorizer._ensure_multivariate_data(args[-1], + colorizer.cmap.n_variates) X, Y, C, shading = self._pcolorargs('pcolor', *args[:-1], C, shading=shading, kwargs=kwargs) @@ -6709,9 +6712,7 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, coords = stack([X, Y], axis=-1) collection = mcoll.PolyQuadMesh( - coords, array=C, cmap=cmap, norm=norm, colorizer=colorizer, - alpha=alpha, **kwargs) - collection._check_exclusionary_keywords(colorizer, vmin=vmin, vmax=vmax) + coords, array=C, colorizer=colorizer, alpha=alpha, **kwargs) collection._scale_norm(norm, vmin, vmax) coords = coords.reshape(-1, 2) # flatten the grid structure; keep x, y @@ -6914,13 +6915,14 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, shading = mpl._val_or_rc(shading, 'pcolor.shading').lower() kwargs.setdefault('edgecolors', 'none') + mcolorizer.ColorizingArtist._check_exclusionary_keywords(colorizer, + vmin=vmin, vmax=vmax, + norm=norm, cmap=cmap) if colorizer is None: - cmap = mcolorizer._ensure_cmap(cmap, accept_multivariate=True) - C = mcolorizer._ensure_multivariate_data(args[-1], cmap.n_variates) - else: - C = mcolorizer._ensure_multivariate_data(args[-1], - colorizer.cmap.n_variates) + colorizer = mcolorizer.Colorizer(cmap=cmap, norm=norm) + C = mcolorizer._ensure_multivariate_data(args[-1], + colorizer.cmap.n_variates) X, Y, C, shading = self._pcolorargs('pcolormesh', *args[:-1], C, shading=shading, kwargs=kwargs) @@ -6930,8 +6932,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, collection = mcoll.QuadMesh( coords, antialiased=antialiased, shading=shading, - array=C, cmap=cmap, norm=norm, colorizer=colorizer, alpha=alpha, **kwargs) - collection._check_exclusionary_keywords(colorizer, vmin=vmin, vmax=vmax) + array=C, colorizer=colorizer, alpha=alpha, **kwargs) collection._scale_norm(norm, vmin, vmax) coords = coords.reshape(-1, 2) # flatten the grid structure; keep x, y diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index 0b449ecb17be..f9274a7d1e61 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -74,7 +74,7 @@ def _scale_norm(self, norm, vmin, vmax, A): """ if vmin is not None or vmax is not None: self.set_clim(vmin, vmax) - if isinstance(norm, colors.Normalize): + if isinstance(norm, colors.Norm): raise ValueError( "Passing a Normalize instance simultaneously with " "vmin/vmax is not supported. Please pass vmin/vmax " diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index f8061db1772b..3556af934703 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -283,8 +283,8 @@ def __init__(self, ax, self.set_interpolation(interpolation) if isinstance(self.norm, mcolors.MultiNorm): if interpolation_stage not in [None, 'data', 'auto']: - raise ValueError("'data' is the only valid interpolation_stage " - "when using multiple color channels, not " + raise ValueError("when using multiple color channels 'data' " + "is the only valid interpolation_stage, not " f"{interpolation_stage}") self.set_interpolation_stage('data') else: @@ -485,8 +485,23 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, norms = self.norm.norms dtypes = [A.dtype.fields[f][0] for f in A.dtype.fields] - A_resampled = [_resample(self, a.astype(_get_scaled_dtype(a)), - out_shape, t) + def get_scaled_dt(A): + # gets the scaled dtype + if A.dtype.kind == 'f': # Float dtype: scale to same dtype. + scaled_dtype = np.dtype('f8' if A.dtype.itemsize > 4 else 'f4') + if scaled_dtype.itemsize < A.dtype.itemsize: + _api.warn_external(f"Casting input data from {A.dtype}" + f" to {scaled_dtype} for imshow.") + else: # Int dtype, likely. + # TODO slice input array first + # Scale to appropriately sized float: use float32 if the + # dynamic range is small, to limit the memory footprint. + da = A.max().astype("f8") - A.min().astype("f8") + scaled_dtype = "f8" if da > 1e8 else "f4" + + return scaled_dtype + + A_resampled = [_resample(self, a.astype(get_scaled_dt(a)), out_shape, t) for a in arrs] # if using NoNorm, cast back to the original datatype @@ -498,8 +513,8 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, # pixels) and out_alpha (to what extent screen pixels are # covered by data pixels: 0 outside the data extent, 1 inside # (even for bad data), and intermediate values at the edges). - mask = (np.where(self._getmaskarray(A), np.float32(np.nan), - np.float32(1)) + mask = (np.where(self._getmaskarray(A), + np.float32(np.nan), np.float32(1)) if A.mask.shape == A.shape # nontrivial mask else np.ones_like(A, np.float32)) # we always have to interpolate the mask to account for @@ -1868,20 +1883,3 @@ def thumbnail(infile, thumbfile, scale=0.1, interpolation='bilinear', ax.imshow(im, aspect='auto', resample=True, interpolation=interpolation) fig.savefig(thumbfile, dpi=dpi) return fig - - -def _get_scaled_dtype(A): - - if A.dtype.kind == 'f': # Float dtype: scale to same dtype. - scaled_dtype = np.dtype('f8' if A.dtype.itemsize > 4 else 'f4') - if scaled_dtype.itemsize < A.dtype.itemsize: - _api.warn_external(f"Casting input data from {A.dtype}" - f" to {scaled_dtype} for imshow.") - else: # Int dtype, likely. - # TODO slice input array first - # Scale to appropriately sized float: use float32 if the - # dynamic range is small, to limit the memory footprint. - da = A.max().astype("f8") - A.min().astype("f8") - scaled_dtype = "f8" if da > 1e8 else "f4" - - return scaled_dtype diff --git a/lib/matplotlib/tests/baseline_images/test_axes/multivariate_visualizations.png b/lib/matplotlib/tests/baseline_images/test_axes/multivariate_visualizations.png index cf51df7650576ba2509dc2f2ea590a88425ececf..d063425b27a6b132d59d46984dc27ea1aa16c7d8 100644 GIT binary patch delta 31 mcmbQ|FwbE^0FSAGcDjM3sb!jhq4~rZ6_)H;<=%}gv*ZDr{0a~N delta 31 ncmbQ|FwbE^0FQ~KcDk8?v4wGBvcbd{6&7K^W#=}w%#sHHoc; Date: Mon, 1 Dec 2025 12:50:09 +0100 Subject: [PATCH 3/8] Apply suggestions from code review Co-authored-by: Kyle Sunden --- lib/matplotlib/axes/_axes.pyi | 12 ++++++------ lib/matplotlib/pyplot.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 438a89e59242..880523112041 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -512,8 +512,8 @@ class Axes(_AxesBase): aspect: Literal["equal", "auto"] | float | None = ..., interpolation: str | None = ..., alpha: float | ArrayLike | None = ..., - vmin: float | tuple[float] | None = ..., - vmax: float | tuple[float] | None = ..., + vmin: float | tuple[float, ...] | None = ..., + vmax: float | tuple[float, ...] | None = ..., colorizer: Colorizer | None = ..., origin: Literal["upper", "lower"] | None = ..., extent: tuple[float, float, float, float] | None = ..., @@ -532,8 +532,8 @@ class Axes(_AxesBase): alpha: float | None = ..., norm: str | Norm | None = ..., cmap: str | Colormap | BivarColormap | MultivarColormap | None = ..., - vmin: float | tuple[float] | None = ..., - vmax: float | tuple[float] | None = ..., + vmin: float | tuple[float, ...] | None = ..., + vmax: float | tuple[float, ...] | None = ..., colorizer: Colorizer | None = ..., data=..., **kwargs @@ -544,8 +544,8 @@ class Axes(_AxesBase): alpha: float | None = ..., norm: str | Norm | None = ..., cmap: str | Colormap | BivarColormap | MultivarColormap | None = ..., - vmin: float | tuple[float] | None = ..., - vmax: float | tuple[float] | None = ..., + vmin: float | tuple[float, ...] | None = ..., + vmax: float | tuple[float, ...] | None = ..., colorizer: Colorizer | None = ..., shading: Literal["flat", "nearest", "gouraud", "auto"] | None = ..., antialiased: bool = ..., diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 3db98478fa53..9d336f81e309 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -3769,8 +3769,8 @@ def imshow( aspect: Literal["equal", "auto"] | float | None = None, interpolation: str | None = None, alpha: float | ArrayLike | None = None, - vmin: float | tuple[float] | None = None, - vmax: float | tuple[float] | None = None, + vmin: float | tuple[float, ...] | None = None, + vmax: float | tuple[float, ...] | None = None, colorizer: Colorizer | None = None, origin: Literal["upper", "lower"] | None = None, extent: tuple[float, float, float, float] | None = None, @@ -3884,8 +3884,8 @@ def pcolor( alpha: float | None = None, norm: str | Norm | None = None, cmap: str | Colormap | BivarColormap | MultivarColormap | None = None, - vmin: float | tuple[float] | None = None, - vmax: float | tuple[float] | None = None, + vmin: float | tuple[float, ...] | None = None, + vmax: float | tuple[float, ...] | None = None, colorizer: Colorizer | None = None, data=None, **kwargs, @@ -3913,8 +3913,8 @@ def pcolormesh( alpha: float | None = None, norm: str | Norm | None = None, cmap: str | Colormap | BivarColormap | MultivarColormap | None = None, - vmin: float | tuple[float] | None = None, - vmax: float | tuple[float] | None = None, + vmin: float | tuple[float, ...] | None = None, + vmax: float | tuple[float, ...] | None = None, colorizer: Colorizer | None = None, shading: Literal["flat", "nearest", "gouraud", "auto"] | None = None, antialiased: bool = False, From 14477115fe7cb1b3f4fb8df954c8f9d2073d16cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Mon, 1 Dec 2025 14:48:38 +0100 Subject: [PATCH 4/8] improved consistency of return types for better typing --- lib/matplotlib/colorizer.py | 24 +++++++++++++++++++++--- lib/matplotlib/colorizer.pyi | 22 +++++++++++----------- lib/matplotlib/colors.pyi | 6 +++--- lib/matplotlib/tests/test_colors.py | 24 +++++++++++++++++++----- 4 files changed, 54 insertions(+), 22 deletions(-) diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index f9274a7d1e61..33008eb453b4 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -278,7 +278,10 @@ def get_clim(self): """ Return the values (min, max) that are mapped to the colormap limits. """ - return self.norm.vmin, self.norm.vmax + if self.norm.n_components == 1: + return (self.norm.vmin, ), (self.norm.vmax, ) + else: + return self.norm.vmin, self.norm.vmax def changed(self): """ @@ -306,7 +309,10 @@ def vmax(self, vmax): @property def clip(self): - return self.norm.clip + if self.norm.n_components == 1: + return (self.norm.clip, ) + else: + return self.norm.clip @clip.setter def clip(self, clip): @@ -360,8 +366,14 @@ def to_rgba(self, x, alpha=None, bytes=False, norm=True): def get_clim(self): """ Return the values (min, max) that are mapped to the colormap limits. + + This function is not available for multivariate data. """ - return self._colorizer.get_clim() + if self._colorizer.norm.n_components > 1: + raise AttributeError("`.get_clim()` is unavailable when using a colormap " + "with multiple components. Use " + "`.colorizer.get_clim()` instead") + return self.colorizer.norm.vmin, self.colorizer.norm.vmax def set_clim(self, vmin=None, vmax=None): """ @@ -376,9 +388,15 @@ def set_clim(self, vmin=None, vmax=None): tuple (*vmin*, *vmax*) as a single positional argument. .. ACCEPTS: (vmin: float, vmax: float) + + This function is not available for multivariate data. """ # If the norm's limits are updated self.changed() will be called # through the callbacks attached to the norm + if self._colorizer.norm.n_components > 1: + raise AttributeError("`.set_clim(vmin, vmax)` is unavailable " + "when using a colormap with multiple components. Use " + "`.colorizer.set_clim(vmin, vmax)` instead") self._colorizer.set_clim(vmin, vmax) def get_alpha(self): diff --git a/lib/matplotlib/colorizer.pyi b/lib/matplotlib/colorizer.pyi index 81c351fa5354..3350709faf01 100644 --- a/lib/matplotlib/colorizer.pyi +++ b/lib/matplotlib/colorizer.pyi @@ -15,7 +15,7 @@ class Colorizer: @property def norm(self) -> colors.Norm: ... @norm.setter - def norm(self, norm: colors.Norm | str | tuple[str] | None) -> None: ... + def norm(self, norm: colors.Norm | str | tuple[str, ...] | None) -> None: ... def to_rgba( self, x: np.ndarray, @@ -29,21 +29,21 @@ class Colorizer: def cmap(self) -> colors.Colormap | colors.BivarColormap | colors.MultivarColormap: ... @cmap.setter def cmap(self, cmap: colors.Colormap | colors.BivarColormap | colors.MultivarColormap | str | None) -> None: ... - def get_clim(self) -> tuple[float, float]: ... - def set_clim(self, vmin: float | tuple[float, float] | None = ..., vmax: float | None = ...) -> None: ... + def get_clim(self) -> tuple[tuple[float | None, ...], tuple[float | None, ...]]: ... + def set_clim(self, vmin: float | tuple[float | None, ...] | None = ..., vmax: float | tuple[float | None, ...] | None = ...) -> None: ... def changed(self) -> None: ... @property - def vmin(self) -> float | tuple[float] | None: ... + def vmin(self) -> tuple[float | None, ...] | None: ... @vmin.setter - def vmin(self, value: float | tuple[float] | None) -> None: ... + def vmin(self, value: tuple[float | None, ...] | None) -> None: ... @property - def vmax(self) -> float | tuple[float] | None: ... + def vmax(self) -> tuple[float | None, ...] | None: ... @vmax.setter - def vmax(self, value: float | tuple[float] | None) -> None: ... + def vmax(self, value: tuple[float | None, ...] | None) -> None: ... @property - def clip(self) -> bool | tuple[bool, ...]: ... + def clip(self) -> tuple[bool, ...]: ... @clip.setter - def clip(self, value: ArrayLike | bool) -> None: ... + def clip(self, value: ArrayLike | bool | tuple[bool, ...]) -> None: ... class _ColorizerInterface: @@ -57,8 +57,8 @@ class _ColorizerInterface: bytes: bool = ..., norm: bool = ..., ) -> np.ndarray: ... - def get_clim(self) -> tuple[float, float]: ... - def set_clim(self, vmin: float | tuple[float, float] | None = ..., vmax: float | None = ...) -> None: ... + def get_clim(self) -> tuple[float, float] | tuple[tuple[float, ...], tuple[float, ...]]: ... + def set_clim(self, vmin: float | tuple[float, float] | tuple[float | None, ...] | None = ..., vmax: float | tuple[float | None, ...] | None = ...) -> None: ... def get_alpha(self) -> float | None: ... def get_cmap(self) -> colors.Colormap | colors.BivarColormap | colors.MultivarColormap: ... def set_cmap(self, cmap: str | colors.Colormap | colors.BivarColormap | colors.MultivarColormap) -> None: ... diff --git a/lib/matplotlib/colors.pyi b/lib/matplotlib/colors.pyi index d7fbbf181272..ddeb19975cf9 100644 --- a/lib/matplotlib/colors.pyi +++ b/lib/matplotlib/colors.pyi @@ -255,13 +255,13 @@ class Norm(ABC): def __init__(self) -> None: ... @property @abstractmethod - def vmin(self) -> float | tuple[float] | None: ... + def vmin(self) -> float | tuple[float | None, ...] | None: ... @property @abstractmethod - def vmax(self) -> float | tuple[float] | None: ... + def vmax(self) -> float | tuple[float | None, ...] | None: ... @property @abstractmethod - def clip(self) -> bool | tuple[bool]: ... + def clip(self) -> bool | tuple[bool, ...]: ... @abstractmethod def __call__(self, value: np.ndarray, clip: bool | None = ...) -> ArrayLike: ... @abstractmethod diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 5bc1f8aea973..a17b2bd26df5 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1785,16 +1785,30 @@ def test_is_color_like(input, expected): assert is_color_like(input) is expected -def test_colorizer_vmin_vmax(): +def test_colorizer_vmin_vmax_clip(): ca = mcolorizer.Colorizer() - assert ca.vmin is None - assert ca.vmax is None + assert len(ca.vmin) == 1 + assert len(ca.vmax) == 1 + assert ca.vmin[0] is None + assert ca.vmax[0] is None ca.vmin = 1 ca.vmax = 3 - assert ca.vmin == 1.0 - assert ca.vmax == 3.0 + assert ca.vmin == (1.0, ) + assert ca.vmax == (3.0, ) assert ca.norm.vmin == 1.0 assert ca.norm.vmax == 3.0 + assert ca.clip == (False, ) + + ca = mcolorizer.Colorizer('BiOrangeBlue') + assert len(ca.vmin) == 2 + assert len(ca.vmax) == 2 + ca.vmin = (1, 2) + ca.vmax = (3, 4) + assert ca.vmin == (1.0, 2.0) + assert ca.vmax == (3.0, 4.0) + assert ca.norm.vmin == (1.0, 2.0) + assert ca.norm.vmax == (3.0, 4.0) + assert ca.clip == (False, False) def test_LinearSegmentedColormap_from_list_color_alpha_tuple(): From 3eb9abda05f830249504324c918c8ce4b2990e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Wed, 7 Jan 2026 15:56:21 +0100 Subject: [PATCH 5/8] implementing feedback from story645 --- lib/matplotlib/axes/_axes.py | 2 +- lib/matplotlib/colorizer.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index d2d197434968..52c31e53e944 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6538,7 +6538,7 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, Parameters ---------- - C : 2D (I, J) or 3D (v, I, J) array-like + C : 2D (M, N) or 3D (v, M, N) array-like The color-mapped values. Color-mapping is controlled by *cmap*, *norm*, *vmin*, and *vmax*. 3D arrays are supported only if the cmap supports v channels. diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index 33008eb453b4..6b3e9e762285 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -368,6 +368,7 @@ def get_clim(self): Return the values (min, max) that are mapped to the colormap limits. This function is not available for multivariate data. + Use `self.colorizer.get_clim()` instead. """ if self._colorizer.norm.n_components > 1: raise AttributeError("`.get_clim()` is unavailable when using a colormap " From 16ed34e496caae5feac6bf4905d7c366dfc74202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Thu, 12 Feb 2026 22:23:34 +0100 Subject: [PATCH 6/8] doc fix for _ColorizerInterface.get_clim() --- lib/matplotlib/colorizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index 6b3e9e762285..d7b540d079d3 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -368,7 +368,7 @@ def get_clim(self): Return the values (min, max) that are mapped to the colormap limits. This function is not available for multivariate data. - Use `self.colorizer.get_clim()` instead. + Use `.Colorizer.get_clim()` via the .colorizer property instead. """ if self._colorizer.norm.n_components > 1: raise AttributeError("`.get_clim()` is unavailable when using a colormap " From b30a297096c274368ec26fd5d0b301550225cdbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Sun, 22 Feb 2026 14:35:11 +0100 Subject: [PATCH 7/8] update based on feedback from @timhoffm --- lib/matplotlib/axes/_axes.py | 11 ++++-- lib/matplotlib/colorizer.py | 5 +++ lib/matplotlib/colors.py | 6 +-- lib/matplotlib/image.py | 10 +++-- .../test_axes/bivariate_visualizations.png | Bin 8311 -> 7656 bytes .../test_axes/multivariate_visualizations.png | Bin 8222 -> 7556 bytes lib/matplotlib/tests/test_axes.py | 36 ++++++++++++------ 7 files changed, 45 insertions(+), 23 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 52c31e53e944..bedb88df21fa 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6175,7 +6175,7 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, - (M, N): an image with scalar data. The values are mapped to colors using normalization and a colormap. See parameters *norm*, *cmap*, *vmin*, *vmax*. - - (v, M, N): if coupled with a cmap that supports v scalars + - (K, M, N): if coupled with a cmap that supports K scalars - (M, N, 3): an image with RGB values (0-1 float or 0-255 int). - (M, N, 4): an image with RGBA values (0-1 float or 0-255 int), i.e. including transparency. @@ -6538,10 +6538,10 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, Parameters ---------- - C : 2D (M, N) or 3D (v, M, N) array-like + C : 2D (M, N) or 3D (K, M, N) array-like The color-mapped values. Color-mapping is controlled by *cmap*, *norm*, *vmin*, and *vmax*. 3D arrays are supported only if the - cmap supports v channels. + cmap supports K channels. X, Y : array-like, optional The coordinates of the corners of quadrilaterals of a pcolormesh:: @@ -6750,7 +6750,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, - (M, N) or M*N: a mesh with scalar data. The values are mapped to colors using normalization and a colormap. See parameters *norm*, *cmap*, *vmin*, *vmax*. - - (v, M, N): if coupled with a cmap that supports v scalars + - (K, M, N): if coupled with a cmap that supports K scalars - (M, N, 3): an image with RGB values (0-1 float or 0-255 int). - (M, N, 4): an image with RGBA values (0-1 float or 0-255 int), i.e. including transparency. @@ -8874,6 +8874,9 @@ def matshow(self, Z, **kwargs): """ Z = np.asanyarray(Z) + if Z.ndim != 2: + if Z.ndim != 3 or Z.shape[2] not in (1, 3, 4): + raise TypeError(f"Invalid shape {Z.shape} for image data") kw = {'origin': 'upper', 'interpolation': 'nearest', 'aspect': 'equal', # (already the imshow default) diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index d7b540d079d3..90517644ba8b 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -277,6 +277,11 @@ def set_clim(self, vmin=None, vmax=None): def get_clim(self): """ Return the values (min, max) that are mapped to the colormap limits. + + This function always returns min and max as tuples to ensure type consistency + when working with both scalar and multivariate color mapping. + See also `.ColorizingArtist.get_clim()` which returns scalars but is unavailable + for multivariate color mapping. """ if self.norm.n_components == 1: return (self.norm.vmin, ), (self.norm.vmax, ) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index eda7d5008fd8..6c0d3cb4c491 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1606,14 +1606,14 @@ def with_extremes(self, *, bad=None, under=None, over=None): Parameters ---------- - bad: :mpltype:`color`, default: None + bad : :mpltype:`color`, default: None If Matplotlib color, the bad value is set accordingly in the copy - under tuple of :mpltype:`color`, default: None + under : tuple of :mpltype:`color`, default: None If tuple, the 'under' value of each component is set with the values from the tuple. - over tuple of :mpltype:`color`, default: None + over : tuple of :mpltype:`color`, default: None If tuple, the 'over' value of each component is set with the values from the tuple. diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 3556af934703..100a0920e2e5 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -283,8 +283,8 @@ def __init__(self, ax, self.set_interpolation(interpolation) if isinstance(self.norm, mcolors.MultiNorm): if interpolation_stage not in [None, 'data', 'auto']: - raise ValueError("when using multiple color channels 'data' " - "is the only valid interpolation_stage, not " + raise ValueError("when using multivariate color mapping 'data' " + "is the only valid interpolation_stage, but got " f"{interpolation_stage}") self.set_interpolation_stage('data') else: @@ -485,7 +485,7 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, norms = self.norm.norms dtypes = [A.dtype.fields[f][0] for f in A.dtype.fields] - def get_scaled_dt(A): + def get_scaled_dtype(A): # gets the scaled dtype if A.dtype.kind == 'f': # Float dtype: scale to same dtype. scaled_dtype = np.dtype('f8' if A.dtype.itemsize > 4 else 'f4') @@ -501,7 +501,9 @@ def get_scaled_dt(A): return scaled_dtype - A_resampled = [_resample(self, a.astype(get_scaled_dt(a)), out_shape, t) + A_resampled = [_resample(self, + a.astype(get_scaled_dtype(a)), + out_shape, t) for a in arrs] # if using NoNorm, cast back to the original datatype diff --git a/lib/matplotlib/tests/baseline_images/test_axes/bivariate_visualizations.png b/lib/matplotlib/tests/baseline_images/test_axes/bivariate_visualizations.png index 536a440874d6d4d686d895f6c7edcc12f4cbfc91..501a87436e0d84c3b94c0dca4120e3a466d25051 100644 GIT binary patch literal 7656 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 B@vZ;> literal 8222 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 Hdi}ouedsQJ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 8e989b20a27b..ba91ec377884 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -10368,22 +10368,35 @@ def test_errorbar_uses_rcparams(): assert_allclose(barcol.get_linewidths(), 1.75) +def test_matshow_not_multivariate(): + """ + matshow() currently does not support multivariate/bivariate colormaps. + This test is to ensure coverage for the if-statement that checks for this. + + This test should be removed if matshow() is updated to support + multivariate/bivariate colormaps. + """ + fig, axes = plt.subplots() + arr = np.arange(24).reshape((-1, 4, 2)) + with pytest.raises(TypeError, match="Invalid shape"): + axes.matshow(arr) + + @image_comparison(["bivariate_visualizations.png"], style='mpl20') def test_bivariate_visualizations(): x_0 = np.arange(25, dtype='float32').reshape(5, 5) % 5 x_1 = np.arange(25, dtype='float32').reshape(5, 5).T % 5 - fig, axes = plt.subplots(1, 6, figsize=(10, 2)) + fig, axes = plt.subplots(1, 5, figsize=(8, 2)) axes[0].imshow((x_0, x_1), cmap='BiPeak', interpolation='nearest') - axes[1].matshow((x_0, x_1), cmap='BiPeak') - axes[2].pcolor((x_0, x_1), cmap='BiPeak') - axes[3].pcolormesh((x_0, x_1), cmap='BiPeak') + axes[1].pcolor((x_0, x_1), cmap='BiPeak') + axes[2].pcolormesh((x_0, x_1), cmap='BiPeak') x = np.arange(5) y = np.arange(5) X, Y = np.meshgrid(x, y) - axes[4].pcolormesh(X, Y, (x_0, x_1), cmap='BiPeak') + axes[3].pcolormesh(X, Y, (x_0, x_1), cmap='BiPeak') patches = [ mpl.patches.Wedge((.3, .7), .1, 0, 360), # Full circle @@ -10395,7 +10408,7 @@ def test_bivariate_visualizations(): colors_1 = np.arange(len(patches)) % 2 p = mpl.collections.PatchCollection(patches, cmap='BiPeak', alpha=0.5) p.set_array((colors_0, colors_1)) - axes[5].add_collection(p) + axes[4].add_collection(p) remove_ticks_and_titles(fig) @@ -10405,17 +10418,16 @@ def test_multivariate_visualizations(): x_1 = np.arange(25, dtype='float32').reshape(5, 5).T % 5 x_2 = np.arange(25, dtype='float32').reshape(5, 5) % 6 - fig, axes = plt.subplots(1, 6, figsize=(10, 2)) + fig, axes = plt.subplots(1, 5, figsize=(8, 2)) axes[0].imshow((x_0, x_1, x_2), cmap='3VarAddA') - axes[1].matshow((x_0, x_1, x_2), cmap='3VarAddA') - axes[2].pcolor((x_0, x_1, x_2), cmap='3VarAddA') - axes[3].pcolormesh((x_0, x_1, x_2), cmap='3VarAddA') + axes[1].pcolor((x_0, x_1, x_2), cmap='3VarAddA') + axes[2].pcolormesh((x_0, x_1, x_2), cmap='3VarAddA') x = np.arange(5) y = np.arange(5) X, Y = np.meshgrid(x, y) - axes[4].pcolormesh(X, Y, (x_0, x_1, x_2), cmap='3VarAddA') + axes[3].pcolormesh(X, Y, (x_0, x_1, x_2), cmap='3VarAddA') patches = [ mpl.patches.Wedge((.3, .7), .1, 0, 360), # Full circle @@ -10428,7 +10440,7 @@ def test_multivariate_visualizations(): colors_2 = np.arange(len(patches)) % 3 p = mpl.collections.PatchCollection(patches, cmap='3VarAddA', alpha=0.5) p.set_array((colors_0, colors_1, colors_2)) - axes[5].add_collection(p) + axes[4].add_collection(p) remove_ticks_and_titles(fig) From 6626f26e3885f82f26237b784fb10476475faf23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Fri, 27 Feb 2026 19:05:32 +0100 Subject: [PATCH 8/8] Apply suggestions from code review Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/colorizer.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index 90517644ba8b..70c669d02379 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -373,12 +373,11 @@ def get_clim(self): Return the values (min, max) that are mapped to the colormap limits. This function is not available for multivariate data. - Use `.Colorizer.get_clim()` via the .colorizer property instead. + Use `.Colorizer.get_clim` via the .colorizer property instead. """ if self._colorizer.norm.n_components > 1: - raise AttributeError("`.get_clim()` is unavailable when using a colormap " - "with multiple components. Use " - "`.colorizer.get_clim()` instead") + raise AttributeError("get_clim() cannot be used with a multi-component " + "colormap. Use .colorizer.get_clim() instead") return self.colorizer.norm.vmin, self.colorizer.norm.vmax def set_clim(self, vmin=None, vmax=None): @@ -400,9 +399,8 @@ def set_clim(self, vmin=None, vmax=None): # If the norm's limits are updated self.changed() will be called # through the callbacks attached to the norm if self._colorizer.norm.n_components > 1: - raise AttributeError("`.set_clim(vmin, vmax)` is unavailable " - "when using a colormap with multiple components. Use " - "`.colorizer.set_clim(vmin, vmax)` instead") + raise AttributeError("set_clim() cannot be used with a multi-component " + "colormap. Use .colorizer.set_clim() instead") self._colorizer.set_clim(vmin, vmax) def get_alpha(self):