From 0a77469f6271b2309dd3f2e2f47386f3852b17c7 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 18 Mar 2019 15:45:40 -0400 Subject: [PATCH] Backport PR #12419: Add DivergingNorm (again, again, again) --- doc/api/colors_api.rst | 1 + .../colormap_normalizations_diverging.py | 44 +++++++++ lib/matplotlib/colorbar.py | 31 +++--- lib/matplotlib/colors.py | 70 +++++++++++++ .../mpl-data/sample_data/topobathy.npz | Bin 0 -> 45224 bytes lib/matplotlib/tests/test_colorbar.py | 21 +++- lib/matplotlib/tests/test_colors.py | 90 +++++++++++++++++ lib/matplotlib/tests/test_contour.py | 5 +- tutorials/colors/colormapnorms.py | 92 +++++++++++------- 9 files changed, 300 insertions(+), 54 deletions(-) create mode 100644 examples/userdemo/colormap_normalizations_diverging.py create mode 100644 lib/matplotlib/mpl-data/sample_data/topobathy.npz diff --git a/doc/api/colors_api.rst b/doc/api/colors_api.rst index d691b9905c55..ccd1cf18aa07 100644 --- a/doc/api/colors_api.rst +++ b/doc/api/colors_api.rst @@ -25,6 +25,7 @@ Classes BoundaryNorm Colormap + DivergingNorm LightSource LinearSegmentedColormap ListedColormap diff --git a/examples/userdemo/colormap_normalizations_diverging.py b/examples/userdemo/colormap_normalizations_diverging.py new file mode 100644 index 000000000000..7a5a68c29b73 --- /dev/null +++ b/examples/userdemo/colormap_normalizations_diverging.py @@ -0,0 +1,44 @@ +""" +===================================== +DivergingNorm colormap normalization +===================================== + +Sometimes we want to have a different colormap on either side of a +conceptual center point, and we want those two colormaps to have +different linear scales. An example is a topographic map where the land +and ocean have a center at zero, but land typically has a greater +elevation range than the water has depth range, and they are often +represented by a different colormap. +""" + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.cbook as cbook +import matplotlib.colors as colors + +filename = cbook.get_sample_data('topobathy.npz', asfileobj=False) +with np.load(filename) as dem: + topo = dem['topo'] + longitude = dem['longitude'] + latitude = dem['latitude'] + +fig, ax = plt.subplots(constrained_layout=True) +# make a colormap that has land and ocean clearly delineated and of the +# same length (256 + 256) +colors_undersea = plt.cm.terrain(np.linspace(0, 0.17, 256)) +colors_land = plt.cm.terrain(np.linspace(0.25, 1, 256)) +all_colors = np.vstack((colors_undersea, colors_land)) +terrain_map = colors.LinearSegmentedColormap.from_list('terrain_map', + all_colors) + +# make the norm: Note the center is offset so that the land has more +# dynamic range: +divnorm = colors.DivergingNorm(vmin=-500, vcenter=0, vmax=4000) + +pcm = ax.pcolormesh(longitude, latitude, topo, rasterized=True, norm=divnorm, + cmap=terrain_map,) +ax.set_xlabel('Lon $[^o E]$') +ax.set_ylabel('Lat $[^o N]$') +ax.set_aspect(1 / np.cos(np.deg2rad(49))) +fig.colorbar(pcm, shrink=0.6, extend='both', label='Elevation [m]') +plt.show() diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 59f6c9e932c3..fba292890b1a 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -875,8 +875,8 @@ def _process_values(self, b=None): + self._boundaries[1:]) if isinstance(self.norm, colors.NoNorm): self._values = (self._values + 0.00001).astype(np.int16) - return - self._values = np.array(self.values) + else: + self._values = np.array(self.values) return if self.values is not None: self._values = np.array(self.values) @@ -1113,20 +1113,19 @@ def _locate(self, x): b = self.norm(self._boundaries, clip=False).filled() xn = self.norm(x, clip=False).filled() - # The rest is linear interpolation with extrapolation at ends. - ii = np.searchsorted(b, xn) - i0 = ii - 1 - itop = (ii == len(b)) - ibot = (ii == 0) - i0[itop] -= 1 - ii[itop] -= 1 - i0[ibot] += 1 - ii[ibot] += 1 - - y = self._y - db = b[ii] - b[i0] - dy = y[ii] - y[i0] - z = y[i0] + (xn - b[i0]) * dy / db + bunique = b + yunique = self._y + # trim extra b values at beginning and end if they are + # not unique. These are here for extended colorbars, and are not + # wanted for the interpolation. + if b[0] == b[1]: + bunique = bunique[1:] + yunique = yunique[1:] + if b[-1] == b[-2]: + bunique = bunique[:-1] + yunique = yunique[:-1] + + z = np.interp(xn, bunique, yunique) return z def set_alpha(self, alpha): diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 0ba21c33beba..bfc53ada8654 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -958,6 +958,76 @@ def scaled(self): return self.vmin is not None and self.vmax is not None +class DivergingNorm(Normalize): + def __init__(self, vcenter, vmin=None, vmax=None): + """ + Normalize data with a set center. + + Useful when mapping data with an unequal rates of change around a + conceptual center, e.g., data that range from -2 to 4, with 0 as + the midpoint. + + Parameters + ---------- + vcenter : float + The data value that defines ``0.5`` in the normalization. + vmin : float, optional + The data value that defines ``0.0`` in the normalization. + Defaults to the min value of the dataset. + vmax : float, optional + The data value that defines ``1.0`` in the normalization. + Defaults to the the max value of the dataset. + + Examples + -------- + This maps data value -4000 to 0., 0 to 0.5, and +10000 to 1.0; data + between is linearly interpolated:: + + >>> import matplotlib.colors as mcolors + >>> offset = mcolors.DivergingNorm(vmin=-4000., + vcenter=0., vmax=10000) + >>> data = [-4000., -2000., 0., 2500., 5000., 7500., 10000.] + >>> offset(data) + array([0., 0.25, 0.5, 0.625, 0.75, 0.875, 1.0]) + """ + + self.vcenter = vcenter + self.vmin = vmin + self.vmax = vmax + if vcenter is not None and vmax is not None and vcenter >= vmax: + raise ValueError('vmin, vcenter, and vmax must be in ' + 'ascending order') + if vcenter is not None and vmin is not None and vcenter <= vmin: + raise ValueError('vmin, vcenter, and vmax must be in ' + 'ascending order') + + def autoscale_None(self, A): + """ + Get vmin and vmax, and then clip at vcenter + """ + super().autoscale_None(A) + if self.vmin > self.vcenter: + self.vmin = self.vcenter + if self.vmax < self.vcenter: + self.vmax = self.vcenter + + def __call__(self, value, clip=None): + """ + Map value to the interval [0, 1]. The clip argument is unused. + """ + result, is_scalar = self.process_value(value) + self.autoscale_None(result) # sets self.vmin, self.vmax if None + + if not self.vmin <= self.vcenter <= self.vmax: + raise ValueError("vmin, vcenter, vmax must increase monotonically") + result = np.ma.masked_array( + np.interp(result, [self.vmin, self.vcenter, self.vmax], + [0, 0.5, 1.]), mask=np.ma.getmask(result)) + if is_scalar: + result = np.atleast_1d(result)[0] + return result + + class LogNorm(Normalize): """Normalize a given value to the 0-1 range on a log scale.""" diff --git a/lib/matplotlib/mpl-data/sample_data/topobathy.npz b/lib/matplotlib/mpl-data/sample_data/topobathy.npz new file mode 100644 index 0000000000000000000000000000000000000000..9f9b085fa29bf590f1a3083c7c80baf0295528f3 GIT binary patch literal 45224 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