From d1c5a6ab163f507e8b0f1b3871e0de8d72266098 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Fri, 28 May 2021 15:42:05 -0700 Subject: [PATCH] FIX: fix colorbars with no scales --- lib/matplotlib/colorbar.py | 36 +++++++++++------- lib/matplotlib/colors.py | 12 +++++- .../test_colorbar/colorbar_twoslope.png | Bin 0 -> 5886 bytes lib/matplotlib/tests/test_colorbar.py | 14 +++++++ lib/matplotlib/tests/test_colors.py | 3 +- 5 files changed, 49 insertions(+), 16 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_twoslope.png diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index b1cd08d955a7..16b0377a43bc 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -1006,24 +1006,34 @@ def _reset_locator_formatter_scale(self): self.locator = None self.minorlocator = None self.formatter = None - if ((self.spacing == 'uniform') and - ((self.boundaries is not None) or - isinstance(self.norm, colors.BoundaryNorm))): - funcs = (self._forward_boundaries, self._inverse_boundaries) - self.ax.set_xscale('function', functions=funcs) - self.ax.set_yscale('function', functions=funcs) - self.__scale = 'function' - elif hasattr(self.norm, '_scale') and (self.norm._scale is not None): + if (self.boundaries is not None or + isinstance(self.norm, colors.BoundaryNorm)): + if self.spacing == 'uniform': + funcs = (self._forward_boundaries, self._inverse_boundaries) + self.ax.set_xscale('function', functions=funcs) + self.ax.set_yscale('function', functions=funcs) + self.__scale = 'function' + elif self.spacing == 'proportional': + self.__scale = 'linear' + self.ax.set_xscale('linear') + self.ax.set_yscale('linear') + elif hasattr(self.norm, '_scale') and self.norm._scale is not None: + # use the norm's scale: self.ax.set_xscale(self.norm._scale) self.ax.set_yscale(self.norm._scale) self.__scale = self.norm._scale.name - else: + elif type(self.norm) is colors.Normalize: + # plain Normalize: self.ax.set_xscale('linear') self.ax.set_yscale('linear') - if type(self.norm) is colors.Normalize: - self.__scale = 'linear' - else: - self.__scale = 'manual' + self.__scale = 'linear' + else: + # norm._scale is None or not an attr: derive the scale from + # the Norm: + funcs = (self.norm, self.norm.inverse) + self.ax.set_xscale('function', functions=funcs) + self.ax.set_yscale('function', functions=funcs) + self.__scale = 'function' def _locate(self, x): """ diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index f678a4ffefd5..3b7e5988eab7 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1152,7 +1152,7 @@ def __init__(self, vmin=None, vmax=None, clip=False): self.vmin = _sanitize_extrema(vmin) self.vmax = _sanitize_extrema(vmax) self.clip = clip - self._scale = scale.LinearScale(axis=None) + self._scale = None # will default to LinearScale for colorbar @staticmethod def process_value(value): @@ -1334,6 +1334,16 @@ def __call__(self, value, clip=None): result = np.atleast_1d(result)[0] return result + def inverse(self, value): + if not self.scaled(): + raise ValueError("Not invertible until both vmin and vmax are set") + (vmin,), _ = self.process_value(self.vmin) + (vmax,), _ = self.process_value(self.vmax) + (vcenter,), _ = self.process_value(self.vcenter) + + result = np.interp(value, [0, 0.5, 1.], [vmin, vcenter, vmax]) + return result + class CenteredNorm(Normalize): def __init__(self, vcenter=0, halfrange=None, clip=False): diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_twoslope.png b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_twoslope.png new file mode 100644 index 0000000000000000000000000000000000000000..27d9227dcfe00c31b6ab1cb05484412a646662b1 GIT binary patch literal 5886 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 a*@fgacZ&*=ZD#@ex6{ePk@5F{fBrAgYcjV0 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index ac054755c4c1..6167618575dd 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -771,3 +771,17 @@ def test_inset_colorbar_layout(): np.testing.assert_allclose(cb.ax.get_position().bounds, [0.87, 0.342, 0.0237, 0.315], atol=0.01) assert cb.ax.outer_ax in ax.child_axes + + +@image_comparison(['colorbar_twoslope.png'], remove_text=True, + style='mpl20') +def test_twoslope_colorbar(): + # Note that the first tick = 20, and should be in the middle + # of the colorbar (white) + fig, ax = plt.subplots() + + norm = mcolors.TwoSlopeNorm(20, 0, 100) + pc = ax.pcolormesh(np.arange(1, 11), np.arange(1, 11), + np.arange(100).reshape(10, 10), + norm=norm, cmap='RdBu_r') + fig.colorbar(pc) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 81dd65bab713..ae004e957591 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1406,6 +1406,5 @@ def test_norm_deepcopy(): norm = mcolors.Normalize() norm.vmin = 0.0002 norm2 = copy.deepcopy(norm) - assert isinstance(norm2._scale, mscale.LinearScale) + assert norm2._scale is None assert norm2.vmin == norm.vmin - assert norm2._scale is not norm._scale