Skip to content

Commit e418ac7

Browse files
committed
introduction of _ColorbarMappable
1 parent 83c3cbc commit e418ac7

File tree

7 files changed

+140
-90
lines changed

7 files changed

+140
-90
lines changed

doc/api/colorizer_api.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
:members:
77
:undoc-members:
88
:show-inheritance:
9-
:private-members: _ColorizerInterface, _ScalarMappable
9+
:private-members: _ColorbarMappable, _ScalarMappable

lib/matplotlib/colorbar.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,8 @@ def remove(self):
10431043

10441044
try:
10451045
ax = self.mappable.axes
1046+
if ax is None:
1047+
return
10461048
except AttributeError:
10471049
return
10481050
try:

lib/matplotlib/colorizer.py

Lines changed: 91 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def clip(self, clip):
313313
self.norm.clip = clip
314314

315315

316-
class _ColorizerInterface:
316+
class _ColorbarMappable:
317317
"""
318318
Base class that contains the interface to `Colorizer` objects from
319319
a `ColorizingArtist` or `.cm.ScalarMappable`.
@@ -322,9 +322,94 @@ class _ColorizerInterface:
322322
attribute. Other functions that as shared between `.ColorizingArtist`
323323
and `.cm.ScalarMappable` are not included.
324324
"""
325+
326+
def __init__(self, colorizer, **kwargs):
327+
"""
328+
Base class for objects that can connect to a colorbar.
329+
330+
All classes that can act as a mappable for `fig.colorbar(mappable)`
331+
will subclass this class.
332+
"""
333+
super().__init__(**kwargs)
334+
self._colorizer = colorizer
335+
self.colorbar = None
336+
self._id_colorizer = self._colorizer.callbacks.connect('changed', self.changed)
337+
self.callbacks = cbook.CallbackRegistry(signals=["changed"])
338+
self._axes = None
339+
self._A = None
340+
341+
@property
342+
def axes(self):
343+
return self._axes
344+
345+
@axes.setter
346+
def axes(self, axes):
347+
self._axes = axes
348+
349+
@property
350+
def colorizer(self):
351+
return self._colorizer
352+
353+
@colorizer.setter
354+
def colorizer(self, cl):
355+
_api.check_isinstance(Colorizer, colorizer=cl)
356+
self._colorizer.callbacks.disconnect(self._id_colorizer)
357+
self._colorizer = cl
358+
self._id_colorizer = cl.callbacks.connect('changed', self.changed)
359+
360+
def changed(self):
361+
"""
362+
Call this whenever the mappable is changed to notify all the
363+
callbackSM listeners to the 'changed' signal.
364+
"""
365+
self.callbacks.process('changed')
366+
self.stale = True
367+
325368
def _scale_norm(self, norm, vmin, vmax):
326369
self._colorizer._scale_norm(norm, vmin, vmax, self._A)
327370

371+
def set_array(self, A):
372+
"""
373+
Set the value array from array-like *A*.
374+
375+
Parameters
376+
----------
377+
A : array-like or None
378+
The values that are mapped to colors.
379+
380+
The base class `.ScalarMappable` does not make any assumptions on
381+
the dimensionality and shape of the value array *A*.
382+
"""
383+
if A is None:
384+
self._A = None
385+
return
386+
387+
A = _ensure_multivariate_data(A, self.norm.n_components)
388+
389+
A = cbook.safe_masked_invalid(A, copy=True)
390+
if not np.can_cast(A.dtype, float, "same_kind"):
391+
if A.dtype.fields is None:
392+
393+
raise TypeError(f"Image data of dtype {A.dtype} cannot be "
394+
f"converted to float")
395+
else:
396+
for key in A.dtype.fields:
397+
if not np.can_cast(A[key].dtype, float, "same_kind"):
398+
raise TypeError(f"Image data of dtype {A.dtype} cannot be "
399+
f"converted to a sequence of floats")
400+
self._A = A
401+
if not self.norm.scaled():
402+
self._colorizer.autoscale_None(A)
403+
404+
def get_array(self):
405+
"""
406+
Return the array of values, that are mapped to colors.
407+
408+
The base class `.ScalarMappable` does not make any assumptions on
409+
the dimensionality and shape of the array.
410+
"""
411+
return self._A
412+
328413
def to_rgba(self, x, alpha=None, bytes=False, norm=True):
329414
"""
330415
Return a normalized RGBA array corresponding to *x*.
@@ -514,7 +599,7 @@ def _sig_digits_from_norm(norm, data, n):
514599
return g_sig_digits
515600

516601

517-
class _ScalarMappable(_ColorizerInterface):
602+
class _ScalarMappable(_ColorbarMappable):
518603
"""
519604
A mixin class to map one or multiple sets of scalar data to RGBA.
520605
@@ -527,15 +612,8 @@ class _ScalarMappable(_ColorizerInterface):
527612
# and ColorizingArtist classes.
528613

529614
# _ScalarMappable can be depreciated so that ColorizingArtist
530-
# inherits directly from _ColorizerInterface.
531-
# in this case, the following changes should occur:
532-
# __init__() has its functionality moved to ColorizingArtist.
533-
# set_array(), get_array(), _get_colorizer() and
534-
# _check_exclusionary_keywords() are moved to ColorizingArtist.
535-
# changed() can be removed so long as colorbar.Colorbar
536-
# is changed to connect to the colorizer instead of the
537-
# ScalarMappable/ColorizingArtist,
538-
# otherwise changed() can be moved to ColorizingArtist.
615+
# inherits directly from _ColorbarMappable.
616+
# in this case, all functionality should be moved to ColorizingArtist.
539617
def __init__(self, norm=None, cmap=None, *, colorizer=None, **kwargs):
540618
"""
541619
Parameters
@@ -550,63 +628,8 @@ def __init__(self, norm=None, cmap=None, *, colorizer=None, **kwargs):
550628
cmap : str or `~matplotlib.colors.Colormap`
551629
The colormap used to map normalized data values to RGBA colors.
552630
"""
553-
super().__init__(**kwargs)
554-
self._A = None
555-
self._colorizer = self._get_colorizer(colorizer=colorizer, norm=norm, cmap=cmap)
556-
557-
self.colorbar = None
558-
self._id_colorizer = self._colorizer.callbacks.connect('changed', self.changed)
559-
self.callbacks = cbook.CallbackRegistry(signals=["changed"])
560-
561-
def set_array(self, A):
562-
"""
563-
Set the value array from array-like *A*.
564-
565-
Parameters
566-
----------
567-
A : array-like or None
568-
The values that are mapped to colors.
569-
570-
The base class `.ScalarMappable` does not make any assumptions on
571-
the dimensionality and shape of the value array *A*.
572-
"""
573-
if A is None:
574-
self._A = None
575-
return
576-
577-
A = _ensure_multivariate_data(A, self.norm.n_components)
578-
579-
A = cbook.safe_masked_invalid(A, copy=True)
580-
if not np.can_cast(A.dtype, float, "same_kind"):
581-
if A.dtype.fields is None:
582-
583-
raise TypeError(f"Image data of dtype {A.dtype} cannot be "
584-
f"converted to float")
585-
else:
586-
for key in A.dtype.fields:
587-
if not np.can_cast(A[key].dtype, float, "same_kind"):
588-
raise TypeError(f"Image data of dtype {A.dtype} cannot be "
589-
f"converted to a sequence of floats")
590-
self._A = A
591-
if not self.norm.scaled():
592-
self._colorizer.autoscale_None(A)
593-
594-
def get_array(self):
595-
"""
596-
Return the array of values, that are mapped to colors.
597-
598-
The base class `.ScalarMappable` does not make any assumptions on
599-
the dimensionality and shape of the array.
600-
"""
601-
return self._A
602-
603-
def changed(self):
604-
"""
605-
Call this whenever the mappable is changed to notify all the
606-
callbackSM listeners to the 'changed' signal.
607-
"""
608-
self.callbacks.process('changed', self)
609-
self.stale = True
631+
colorizer = self._get_colorizer(colorizer=colorizer, norm=norm, cmap=cmap)
632+
super().__init__(colorizer, **kwargs)
610633

611634
@staticmethod
612635
def _check_exclusionary_keywords(colorizer, **kwargs):
@@ -710,17 +733,6 @@ def __init__(self, colorizer, **kwargs):
710733
_api.check_isinstance(Colorizer, colorizer=colorizer)
711734
super().__init__(colorizer=colorizer, **kwargs)
712735

713-
@property
714-
def colorizer(self):
715-
return self._colorizer
716-
717-
@colorizer.setter
718-
def colorizer(self, cl):
719-
_api.check_isinstance(Colorizer, colorizer=cl)
720-
self._colorizer.callbacks.disconnect(self._id_colorizer)
721-
self._colorizer = cl
722-
self._id_colorizer = cl.callbacks.connect('changed', self.changed)
723-
724736
def _set_colorizer_check_keywords(self, colorizer, **kwargs):
725737
"""
726738
Raises a ValueError if any kwarg is not None while colorizer is not None.

lib/matplotlib/colorizer.pyi

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from matplotlib import cbook, colorbar, colors, artist
1+
from matplotlib import cbook, colorbar, colors, artist, axes as maxes
22

33
import numpy as np
44
from numpy.typing import ArrayLike
@@ -46,10 +46,27 @@ class Colorizer:
4646
def clip(self, value: bool) -> None: ...
4747

4848

49-
class _ColorizerInterface:
50-
cmap: colors.Colormap
49+
50+
class _ColorbarMappable:
5151
colorbar: colorbar.Colorbar | None
5252
callbacks: cbook.CallbackRegistry
53+
cmap: colors.Colormap
54+
def __init__(
55+
self,
56+
colorizer: Colorizer | None,
57+
**kwargs
58+
) -> None: ...
59+
@property
60+
def colorizer(self) -> Colorizer: ...
61+
@colorizer.setter
62+
def colorizer(self, cl: Colorizer) -> None: ...
63+
def changed(self) -> None: ...
64+
def set_array(self, A: ArrayLike | None) -> None: ...
65+
def get_array(self) -> np.ndarray | None: ...
66+
@property
67+
def axes(self) -> maxes._base._AxesBase | None: ...
68+
@axes.setter
69+
def axes(self, new_axes: maxes._base._AxesBase | None) -> None: ...
5370
def to_rgba(
5471
self,
5572
x: np.ndarray,
@@ -71,7 +88,7 @@ class _ColorizerInterface:
7188
def autoscale_None(self) -> None: ...
7289

7390

74-
class _ScalarMappable(_ColorizerInterface):
91+
class _ScalarMappable(_ColorbarMappable):
7592
def __init__(
7693
self,
7794
norm: colors.Norm | None = ...,
@@ -80,9 +97,6 @@ class _ScalarMappable(_ColorizerInterface):
8097
colorizer: Colorizer | None = ...,
8198
**kwargs
8299
) -> None: ...
83-
def set_array(self, A: ArrayLike | None) -> None: ...
84-
def get_array(self) -> np.ndarray | None: ...
85-
def changed(self) -> None: ...
86100

87101

88102
class ColorizingArtist(_ScalarMappable, artist.Artist):

lib/matplotlib/pyplot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
from matplotlib.axes import Subplot # noqa: F401
7979
from matplotlib.backends import BackendFilter, backend_registry
8080
from matplotlib.projections import PolarAxes
81-
from matplotlib.colorizer import _ColorizerInterface, ColorizingArtist, Colorizer
81+
from matplotlib.colorizer import _ColorbarMappable, ColorizingArtist, Colorizer
8282
from matplotlib import mlab # for detrend_none, window_hanning
8383
from matplotlib.scale import get_scale_names # noqa: F401
8484

@@ -4201,7 +4201,7 @@ def spy(
42014201
origin=origin,
42024202
**kwargs,
42034203
)
4204-
if isinstance(__ret, _ColorizerInterface):
4204+
if isinstance(__ret, _ColorbarMappable):
42054205
sci(__ret)
42064206
return __ret
42074207

lib/matplotlib/tests/test_colorbar.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
BoundaryNorm, LogNorm, PowerNorm, Normalize, NoNorm
1616
)
1717
from matplotlib.colorbar import Colorbar
18+
from matplotlib.colorizer import Colorizer, _ColorbarMappable
1819
from matplotlib.ticker import FixedLocator, LogFormatter, StrMethodFormatter
1920
from matplotlib.testing.decorators import check_figures_equal
2021

@@ -1242,3 +1243,24 @@ def test_colorbar_format_string_and_old():
12421243
plt.imshow([[0, 1]])
12431244
cb = plt.colorbar(format="{x}%")
12441245
assert isinstance(cb._formatter, StrMethodFormatter)
1246+
1247+
1248+
def test_ColorbarMappable_as_input():
1249+
# check that _ColorbarMappable can function as a
1250+
# valid input for colorbar
1251+
fig, ax = plt.subplots()
1252+
norm = Normalize(vmin=-5, vmax=10)
1253+
cl = Colorizer(norm=norm)
1254+
cbm = _ColorbarMappable(cl)
1255+
# test without an axes on the mappable, no kwarg
1256+
with pytest.raises(ValueError, match='Unable to determine Axes to steal'):
1257+
cb = fig.colorbar(cbm)
1258+
# test without an axes on the mappable, with kwarg
1259+
cb = fig.colorbar(cbm, ax=ax)
1260+
cb = fig.colorbar(cbm, cax=ax)
1261+
# test without an axes on the mappable
1262+
cbm.axes = ax
1263+
cb = fig.colorbar(cbm)
1264+
assert cb.mappable is cbm
1265+
assert cb.vmin == -5
1266+
assert cb.vmax == 10

tools/boilerplate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def boilerplate_gen():
313313
'hist2d': 'sci(__ret[-1])',
314314
'imshow': 'sci(__ret)',
315315
'spy': (
316-
'if isinstance(__ret, _ColorizerInterface):\n'
316+
'if isinstance(__ret, _ColorbarMappable):\n'
317317
' sci(__ret)'
318318
),
319319
'quiver': 'sci(__ret)',

0 commit comments

Comments
 (0)