Skip to content

Update colorbar with new colorizer API #30008

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 70 additions & 35 deletions lib/matplotlib/colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -199,12 +200,12 @@
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`.
Expand Down Expand Up @@ -244,8 +245,8 @@
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()``.
Expand Down Expand Up @@ -288,27 +289,33 @@
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
extend = cs.extend
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)
Expand All @@ -332,18 +339,16 @@
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
Expand Down Expand Up @@ -402,8 +407,8 @@
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
Expand All @@ -425,6 +430,31 @@
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

Check warning on line 443 in lib/matplotlib/colorbar.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/colorbar.py#L443

Added line #L443 was not covered by tests
# need to know the norm as it is now
# so that we can monitor for changes in update_normal()
self._historic_norm = colorizer.norm

Check warning on line 446 in lib/matplotlib/colorbar.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/colorbar.py#L446

Added line #L446 was not covered by tests
# assume the norm is changed
self.update_normal()

Check warning on line 448 in lib/matplotlib/colorbar.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/colorbar.py#L448

Added line #L448 was not covered by tests

@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."""
Expand Down Expand Up @@ -484,7 +514,7 @@
"""
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
Expand All @@ -503,12 +533,16 @@
# 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):
Expand Down Expand Up @@ -1032,9 +1066,9 @@

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)
Expand Down Expand Up @@ -1090,8 +1124,9 @@
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
Expand Down
15 changes: 11 additions & 4 deletions lib/matplotlib/colorbar.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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 = ...,
Expand All @@ -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: ...
Expand Down
18 changes: 9 additions & 9 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/figure.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ...,
Expand Down
10 changes: 10 additions & 0 deletions lib/matplotlib/tests/test_colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading