diff --git a/doc/api/next_api_changes/behaviour.rst b/doc/api/next_api_changes/behaviour.rst index ff7fdc3695e4..a49528d84fe3 100644 --- a/doc/api/next_api_changes/behaviour.rst +++ b/doc/api/next_api_changes/behaviour.rst @@ -98,9 +98,9 @@ deprecation warning. `~.Axes.errorbar` now color cycles when only errorbar color is set ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Previously setting the *ecolor* would turn off automatic color cycling for the plot, leading to the -the lines and markers defaulting to whatever the first color in the color cycle was in the case of -multiple plot calls. +Previously setting the *ecolor* would turn off automatic color cycling for the plot, leading to the +the lines and markers defaulting to whatever the first color in the color cycle was in the case of +multiple plot calls. `.rcsetup.validate_color_for_prop_cycle` now always raises TypeError for bytes input ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -147,3 +147,11 @@ The parameter ``s`` to `.Axes.annotate` and `.pyplot.annotate` is renamed to The old parameter name remains supported, but support for it will be dropped in a future Matplotlib release. +``get_cmap()`` now returns a copy of the colormap +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Previously, calling ``get_cmap()`` would return +the built-in Colormap. If you made modifications to that colormap, the +changes would be propagated in the global state. This function now +returns a copy of all registered colormaps to keep the built-in +colormaps untouched. + diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index fc5dacbf7cb8..d872254af3c9 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -15,6 +15,7 @@ normalization. """ +import copy import functools import numpy as np @@ -70,9 +71,9 @@ def _gen_cmap_d(): cmap_d = _gen_cmap_d() +# locals().update(copy.deepcopy(cmap_d)) locals().update(cmap_d) - # Continue with definitions ... @@ -95,6 +96,9 @@ def register_cmap(name=None, cmap=None, data=None, lut=None): and the resulting colormap is registered. Instead of this implicit colormap creation, create a `.LinearSegmentedColormap` and use the first case: ``register_cmap(cmap=LinearSegmentedColormap(name, data, lut))``. + + If *name* is the same as a built-in colormap this will replace the + built-in Colormap of the same name. """ cbook._check_isinstance((str, None), name=name) if name is None: @@ -105,6 +109,8 @@ def register_cmap(name=None, cmap=None, data=None, lut=None): "Colormap") from err if isinstance(cmap, colors.Colormap): cmap_d[name] = cmap + # We are overriding the global state, so reinitialize this. + cmap._global_changed = False return if lut is not None or data is not None: cbook.warn_deprecated( @@ -118,21 +124,24 @@ def register_cmap(name=None, cmap=None, data=None, lut=None): lut = mpl.rcParams['image.lut'] cmap = colors.LinearSegmentedColormap(name, data, lut) cmap_d[name] = cmap + cmap._global_changed = False def get_cmap(name=None, lut=None): """ Get a colormap instance, defaulting to rc values if *name* is None. - Colormaps added with :func:`register_cmap` take precedence over - built-in colormaps. + Colormaps added with :func:`register_cmap` with the same name as + built-in colormaps will replace them. Parameters ---------- name : `matplotlib.colors.Colormap` or str or None, default: None - If a `.Colormap` instance, it will be returned. Otherwise, the name of - a colormap known to Matplotlib, which will be resampled by *lut*. The - default, None, means :rc:`image.cmap`. + If a `.Colormap` instance, it will be returned. + Otherwise, the name of a colormap known to Matplotlib, which will + be resampled by *lut*. Currently, this returns the global colormap + instance which is deprecated. In Matplotlib 4, a copy of the requested + Colormap will be returned. The default, None, means :rc:`image.cmap`. lut : int or None, default: None If *name* is not already a Colormap instance and *lut* is not None, the colormap will be resampled to have *lut* entries in the lookup table. @@ -143,7 +152,17 @@ def get_cmap(name=None, lut=None): return name cbook._check_in_list(sorted(cmap_d), name=name) if lut is None: + if cmap_d[name]._global_changed: + cbook.warn_deprecated( + "3.3", + message="The colormap requested has had the global state " + "changed without being registered. Accessing a " + "colormap in this way has been deprecated. " + "Please register the colormap using " + "plt.register_cmap() before requesting " + "the modified colormap.") return cmap_d[name] + # return copy.copy(cmap_d[name]) else: return cmap_d[name]._resample(lut) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 86e369f9950a..f0acc55828a8 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -515,6 +515,8 @@ def __init__(self, name, N=256): self._i_over = self.N + 1 self._i_bad = self.N + 2 self._isinit = False + # This is to aid in deprecation transition for v3.3 + self._global_changed = False #: When this colormap exists on a scalar mappable and colorbar_extend #: is not False, colorbar creation will pick up ``colorbar_extend`` as @@ -599,13 +601,27 @@ def __copy__(self): cmapobject.__dict__.update(self.__dict__) if self._isinit: cmapobject._lut = np.copy(self._lut) + cmapobject._global_changed = False return cmapobject + def __eq__(self, other): + if isinstance(other, Colormap): + # To compare lookup tables the Colormaps have to be initialized + if not self._isinit: + self._init() + if not other._isinit: + other._init() + if self._lut.shape != other._lut.shape: + return False + return np.all(self._lut == other._lut) + return False + def set_bad(self, color='k', alpha=None): """Set the color for masked values.""" self._rgba_bad = to_rgba(color, alpha) if self._isinit: self._set_extremes() + self._global_changed = True def set_under(self, color='k', alpha=None): """ @@ -614,6 +630,7 @@ def set_under(self, color='k', alpha=None): self._rgba_under = to_rgba(color, alpha) if self._isinit: self._set_extremes() + self._global_changed = True def set_over(self, color='k', alpha=None): """ @@ -622,6 +639,7 @@ def set_over(self, color='k', alpha=None): self._rgba_over = to_rgba(color, alpha) if self._isinit: self._set_extremes() + self._global_changed = True def _set_extremes(self): if self._rgba_under: @@ -2098,6 +2116,7 @@ def from_levels_and_colors(levels, colors, extend='neither'): cmap.set_over('none') cmap.colorbar_extend = extend + cmap._global_changed = False norm = BoundaryNorm(levels, ncolors=n_data_colors) return cmap, norm diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 78f2256a3299..a38c0f17ae4a 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -61,7 +61,7 @@ def test_resample(): def test_register_cmap(): - new_cm = copy.copy(plt.cm.viridis) + new_cm = plt.get_cmap('viridis') cm.register_cmap('viridis2', new_cm) assert plt.get_cmap('viridis2') == new_cm @@ -70,6 +70,36 @@ def test_register_cmap(): cm.register_cmap() +@pytest.mark.skipif(matplotlib.__version__[0] < "4", + reason="This test modifies the global state of colormaps.") +def test_colormap_builtin_immutable(): + new_cm = plt.get_cmap('viridis') + new_cm.set_over('b') + # Make sure that this didn't mess with the original viridis cmap + assert new_cm != plt.get_cmap('viridis') + + # Also test that pyplot access doesn't mess the original up + new_cm = plt.cm.viridis + new_cm.set_over('b') + assert new_cm != plt.get_cmap('viridis') + + +def test_colormap_builtin_immutable_warn(): + new_cm = plt.get_cmap('viridis') + # Store the old value so we don't override the state later on. + orig_cmap = copy.copy(new_cm) + with pytest.warns(cbook.MatplotlibDeprecationWarning, + match="The colormap requested has had the global"): + new_cm.set_under('k') + # This should warn now because we've modified the global state + # without registering it + plt.get_cmap('viridis') + + # Test that re-registering the original cmap clears the warning + plt.register_cmap(cmap=orig_cmap) + plt.get_cmap('viridis') + + def test_colormap_copy(): cm = plt.cm.Reds cm_copy = copy.copy(cm)
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: