From 026e48a39f3a06f6bd568104ab09b55e67bb0e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Sun, 4 May 2025 13:33:47 +0200 Subject: [PATCH] Allow colorbar to accept a colorizer Updates colorbar.colorbar to accept a colorizer.Colorizer object, in addition to colorizer.ColorizingArtist. This commit also changes the docs from referencing cm.ScalarMappable --- lib/matplotlib/colorbar.py | 105 +++++++++++++++++--------- lib/matplotlib/colorbar.pyi | 15 +++- lib/matplotlib/figure.py | 18 ++--- lib/matplotlib/figure.pyi | 2 +- lib/matplotlib/tests/test_colorbar.py | 10 +++ 5 files changed, 101 insertions(+), 49 deletions(-) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index db33698c5514..dfaa99ab06fe 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -16,8 +16,9 @@ import numpy as np import matplotlib as mpl -from matplotlib import _api, cbook, collections, cm, colors, contour, ticker +from matplotlib import _api, cbook, collections, colors, contour, ticker import matplotlib.artist as martist +import matplotlib.colorizer as mcolorizer import matplotlib.patches as mpatches import matplotlib.path as mpath import matplotlib.spines as mspines @@ -199,12 +200,12 @@ class Colorbar: Draw a colorbar in an existing Axes. Typically, colorbars are created using `.Figure.colorbar` or - `.pyplot.colorbar` and associated with `.ScalarMappable`\s (such as an + `.pyplot.colorbar` and associated with `.ColorizingArtist`\s (such as an `.AxesImage` generated via `~.axes.Axes.imshow`). In order to draw a colorbar not associated with other elements in the figure, e.g. when showing a colormap by itself, one can create an empty - `.ScalarMappable`, or directly pass *cmap* and *norm* instead of *mappable* + `.Colorizer`, or directly pass *cmap* and *norm* instead of *mappable* to `Colorbar`. Useful public methods are :meth:`set_label` and :meth:`add_lines`. @@ -244,8 +245,8 @@ def __init__( ax : `~matplotlib.axes.Axes` The `~.axes.Axes` instance in which the colorbar is drawn. - mappable : `.ScalarMappable` - The mappable whose colormap and norm will be used. + mappable : `.ColorizingArtist` or `.Colorizer` + The mappable or colorizer whose colormap and norm will be used. To show the colors versus index instead of on a 0-1 scale, set the mappable's norm to ``colors.NoNorm()``. @@ -288,15 +289,21 @@ def __init__( colorbar and at the right for a vertical. """ if mappable is None: - mappable = cm.ScalarMappable(norm=norm, cmap=cmap) + mappable = mcolorizer.Colorizer(norm=norm, cmap=cmap) - self.mappable = mappable - cmap = mappable.cmap - norm = mappable.norm + if isinstance(mappable, mcolorizer.Colorizer): + self._mappable = None + self._colorizer = mappable + else: + self._mappable = mappable + self._colorizer = mappable._colorizer + self._historic_norm = self.colorizer.norm + + del mappable, cmap, norm filled = True - if isinstance(mappable, contour.ContourSet): - cs = mappable + if isinstance(self.mappable, contour.ContourSet): + cs = self.mappable alpha = cs.get_alpha() boundaries = cs._levels values = cs.cvalues @@ -304,11 +311,11 @@ def __init__( filled = cs.filled if ticks is None: ticks = ticker.FixedLocator(cs.levels, nbins=10) - elif isinstance(mappable, martist.Artist): - alpha = mappable.get_alpha() + elif isinstance(self.mappable, martist.Artist): + alpha = self.mappable.get_alpha() - mappable.colorbar = self - mappable.colorbar_cid = mappable.callbacks.connect( + self.colorizer.colorbar = self + self.colorizer.colorbar_cid = self.colorizer.callbacks.connect( 'changed', self.update_normal) location_orientation = _get_orientation_from_location(location) @@ -332,18 +339,16 @@ def __init__( self.ax._axes_locator = _ColorbarAxesLocator(self) if extend is None: - if (not isinstance(mappable, contour.ContourSet) - and getattr(cmap, 'colorbar_extend', False) is not False): - extend = cmap.colorbar_extend - elif hasattr(norm, 'extend'): - extend = norm.extend + if (not isinstance(self.mappable, contour.ContourSet) + and getattr(self.cmap, 'colorbar_extend', False) is not False): + extend = self.cmap.colorbar_extend + elif hasattr(self.norm, 'extend'): + extend = self.norm.extend else: extend = 'neither' self.alpha = None # Call set_alpha to handle array-like alphas properly self.set_alpha(alpha) - self.cmap = cmap - self.norm = norm self.values = values self.boundaries = boundaries self.extend = extend @@ -402,8 +407,8 @@ def __init__( self._formatter = format # Assume it is a Formatter or None self._draw_all() - if isinstance(mappable, contour.ContourSet) and not mappable.filled: - self.add_lines(mappable) + if isinstance(self.mappable, contour.ContourSet) and not self.mappable.filled: + self.add_lines(self.mappable) # Link the Axes and Colorbar for interactive use self.ax._colorbar = self @@ -425,6 +430,31 @@ def __init__( self._extend_cid2 = self.ax.callbacks.connect( "ylim_changed", self._do_extends) + @property + def mappable(self): + return self._mappable + + @property + def colorizer(self): + return self._colorizer + + @colorizer.setter + def colorizer(self, colorizer): + self._colorizer = colorizer + # need to know the norm as it is now + # so that we can monitor for changes in update_normal() + self._historic_norm = colorizer.norm + # assume the norm is changed + self.update_normal() + + @property + def cmap(self): + return self._colorizer.cmap + + @property + def norm(self): + return self._colorizer.norm + @property def long_axis(self): """Axis that has decorations (ticks, etc) on it.""" @@ -484,7 +514,7 @@ def update_normal(self, mappable=None): """ Update solid patches, lines, etc. - This is meant to be called when the norm of the image or contour plot + This is meant to be called when the norm of the colorizer (image etc.) to which this colorbar belongs changes. If the norm on the mappable is different than before, this resets the @@ -503,12 +533,16 @@ def update_normal(self, mappable=None): # Therefore, the mappable keyword can be deprecated if cm.ScalarMappable # is removed. self.mappable = mappable - _log.debug('colorbar update normal %r %r', self.mappable.norm, self.norm) - self.set_alpha(self.mappable.get_alpha()) - self.cmap = self.mappable.cmap - if self.mappable.norm != self.norm: - self.norm = self.mappable.norm + + _log.debug('colorbar update normal %r %r', + self.colorizer.norm, + self._historic_norm) + if self.mappable: + self.set_alpha(self.mappable.get_alpha()) + + if self.colorizer.norm != self._historic_norm: self._reset_locator_formatter_scale() + self._historic_norm = self.colorizer.norm self._draw_all() if isinstance(self.mappable, contour.ContourSet): @@ -1032,9 +1066,9 @@ def remove(self): self.ax.remove() - self.mappable.callbacks.disconnect(self.mappable.colorbar_cid) - self.mappable.colorbar = None - self.mappable.colorbar_cid = None + self.colorizer.callbacks.disconnect(self.colorizer.colorbar_cid) + self.colorizer.colorbar = None + self.colorizer.colorbar_cid = None # Remove the extension callbacks self.ax.callbacks.disconnect(self._extend_cid1) self.ax.callbacks.disconnect(self._extend_cid2) @@ -1090,8 +1124,9 @@ def _process_values(self): b = np.hstack((b, b[-1] + 1)) # transform from 0-1 to vmin-vmax: - if self.mappable.get_array() is not None: - self.mappable.autoscale_None() + if self.mappable: + if self.mappable.get_array() is not None: + self.mappable.autoscale_None() if not self.norm.scaled(): # If we still aren't scaled after autoscaling, use 0, 1 as default self.norm.vmin = 0 diff --git a/lib/matplotlib/colorbar.pyi b/lib/matplotlib/colorbar.pyi index 07467ca74f3d..355a3d11de4c 100644 --- a/lib/matplotlib/colorbar.pyi +++ b/lib/matplotlib/colorbar.pyi @@ -22,11 +22,8 @@ class _ColorbarSpine(mspines.Spines): class Colorbar: n_rasterize: int - mappable: cm.ScalarMappable | colorizer.ColorizingArtist ax: Axes alpha: float | None - cmap: colors.Colormap - norm: colors.Normalize values: Sequence[float] | None boundaries: Sequence[float] | None extend: Literal["neither", "both", "min", "max"] @@ -44,7 +41,7 @@ class Colorbar: def __init__( self, ax: Axes, - mappable: cm.ScalarMappable | colorizer.ColorizingArtist | None = ..., + mappable: cm.ScalarMappable | colorizer.ColorizingArtist | colorizer.Colorizer | None = ..., *, cmap: str | colors.Colormap | None = ..., norm: colors.Normalize | None = ..., @@ -64,6 +61,16 @@ class Colorbar: location: Literal["left", "right", "top", "bottom"] | None = ... ) -> None: ... @property + def mappable(self) -> cm.ScalarMappable | colorizer.ColorizingArtist : ... + @property + def colorizer(self) -> colorizer.Colorizer : ... + @colorizer.setter + def colorizer(self, colorizer) -> None : ... + @property + def cmap(self) -> colors.Colormap : ... + @property + def norm(self) -> colors.Normalize : ... + @property def long_axis(self) -> Axis: ... @property def locator(self) -> Locator: ... diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index bf4e2253324f..8fd8dd63a583 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1200,17 +1200,17 @@ def colorbar( Parameters ---------- mappable - The `matplotlib.cm.ScalarMappable` (i.e., `.AxesImage`, - `.ContourSet`, etc.) described by this colorbar. This argument is - mandatory for the `.Figure.colorbar` method but optional for the - `.pyplot.colorbar` function, which sets the default to the current - image. - - Note that one can create a `.ScalarMappable` "on-the-fly" to - generate colorbars not attached to a previously drawn artist, e.g. + The `matplotlib.colorizer.ColorizingArtist` (i.e., `.AxesImage`, + `.ContourSet`, etc.) or `matplotlib.colorizer.Colorizer` described + by this colorbar. This argument is mandatory for the + `.Figure.colorbar` method but optional for the `.pyplot.colorbar` + function, which sets the default to the current image. + + Note that one can create a `.Colorizer` "on-the-fly" to + generate colorbars not attached an artist, e.g. :: - fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax) + fig.colorbar(colorizer.Colorizer(norm=norm, cmap=cmap), ax=ax) cax : `~matplotlib.axes.Axes`, optional Axes into which the colorbar will be drawn. If `None`, then a new diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi index e7c5175d8af9..b2661ee602c4 100644 --- a/lib/matplotlib/figure.pyi +++ b/lib/matplotlib/figure.pyi @@ -169,7 +169,7 @@ class FigureBase(Artist): ) -> Text: ... def colorbar( self, - mappable: ScalarMappable | ColorizingArtist, + mappable: ScalarMappable | ColorizingArtist | Colorizer, cax: Axes | None = ..., ax: Axes | Iterable[Axes] | None = ..., use_gridspec: bool = ..., diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index f95f131e3bf6..8472d297af6c 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -1239,3 +1239,13 @@ def test_colorbar_format_string_and_old(): plt.imshow([[0, 1]]) cb = plt.colorbar(format="{x}%") assert isinstance(cb._formatter, StrMethodFormatter) + + +def test_colorizer(): + fig, ax = plt.subplots() + c = mpl.colorizer.Colorizer(norm=mcolors.Normalize(), cmap='viridis') + fig.colorbar(c, cax=ax) + c.vmin = -1 + c.vmax = 2 + np.testing.assert_almost_equal(ax.yaxis.get_ticklocs()[0], -1) + np.testing.assert_almost_equal(ax.yaxis.get_ticklocs()[-1], 2)