Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9cd8874

Browse filesBrowse files
authored
Merge pull request #13705 from meeseeksmachine/auto-backport-of-pr-12419-on-v3.1.x
Backport PR #12419 on branch v3.1.x (Add DivergingNorm (again, again, again))
2 parents 7e7fe36 + 0a77469 commit 9cd8874
Copy full SHA for 9cd8874

File tree

Expand file treeCollapse file tree

9 files changed

+300
-54
lines changed
Filter options
Expand file treeCollapse file tree

9 files changed

+300
-54
lines changed

‎doc/api/colors_api.rst

Copy file name to clipboardExpand all lines: doc/api/colors_api.rst
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Classes
2525

2626
BoundaryNorm
2727
Colormap
28+
DivergingNorm
2829
LightSource
2930
LinearSegmentedColormap
3031
ListedColormap
+44Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
=====================================
3+
DivergingNorm colormap normalization
4+
=====================================
5+
6+
Sometimes we want to have a different colormap on either side of a
7+
conceptual center point, and we want those two colormaps to have
8+
different linear scales. An example is a topographic map where the land
9+
and ocean have a center at zero, but land typically has a greater
10+
elevation range than the water has depth range, and they are often
11+
represented by a different colormap.
12+
"""
13+
14+
import numpy as np
15+
import matplotlib.pyplot as plt
16+
import matplotlib.cbook as cbook
17+
import matplotlib.colors as colors
18+
19+
filename = cbook.get_sample_data('topobathy.npz', asfileobj=False)
20+
with np.load(filename) as dem:
21+
topo = dem['topo']
22+
longitude = dem['longitude']
23+
latitude = dem['latitude']
24+
25+
fig, ax = plt.subplots(constrained_layout=True)
26+
# make a colormap that has land and ocean clearly delineated and of the
27+
# same length (256 + 256)
28+
colors_undersea = plt.cm.terrain(np.linspace(0, 0.17, 256))
29+
colors_land = plt.cm.terrain(np.linspace(0.25, 1, 256))
30+
all_colors = np.vstack((colors_undersea, colors_land))
31+
terrain_map = colors.LinearSegmentedColormap.from_list('terrain_map',
32+
all_colors)
33+
34+
# make the norm: Note the center is offset so that the land has more
35+
# dynamic range:
36+
divnorm = colors.DivergingNorm(vmin=-500, vcenter=0, vmax=4000)
37+
38+
pcm = ax.pcolormesh(longitude, latitude, topo, rasterized=True, norm=divnorm,
39+
cmap=terrain_map,)
40+
ax.set_xlabel('Lon $[^o E]$')
41+
ax.set_ylabel('Lat $[^o N]$')
42+
ax.set_aspect(1 / np.cos(np.deg2rad(49)))
43+
fig.colorbar(pcm, shrink=0.6, extend='both', label='Elevation [m]')
44+
plt.show()

‎lib/matplotlib/colorbar.py

Copy file name to clipboardExpand all lines: lib/matplotlib/colorbar.py
+15-16Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -875,8 +875,8 @@ def _process_values(self, b=None):
875875
+ self._boundaries[1:])
876876
if isinstance(self.norm, colors.NoNorm):
877877
self._values = (self._values + 0.00001).astype(np.int16)
878-
return
879-
self._values = np.array(self.values)
878+
else:
879+
self._values = np.array(self.values)
880880
return
881881
if self.values is not None:
882882
self._values = np.array(self.values)
@@ -1113,20 +1113,19 @@ def _locate(self, x):
11131113
b = self.norm(self._boundaries, clip=False).filled()
11141114
xn = self.norm(x, clip=False).filled()
11151115

1116-
# The rest is linear interpolation with extrapolation at ends.
1117-
ii = np.searchsorted(b, xn)
1118-
i0 = ii - 1
1119-
itop = (ii == len(b))
1120-
ibot = (ii == 0)
1121-
i0[itop] -= 1
1122-
ii[itop] -= 1
1123-
i0[ibot] += 1
1124-
ii[ibot] += 1
1125-
1126-
y = self._y
1127-
db = b[ii] - b[i0]
1128-
dy = y[ii] - y[i0]
1129-
z = y[i0] + (xn - b[i0]) * dy / db
1116+
bunique = b
1117+
yunique = self._y
1118+
# trim extra b values at beginning and end if they are
1119+
# not unique. These are here for extended colorbars, and are not
1120+
# wanted for the interpolation.
1121+
if b[0] == b[1]:
1122+
bunique = bunique[1:]
1123+
yunique = yunique[1:]
1124+
if b[-1] == b[-2]:
1125+
bunique = bunique[:-1]
1126+
yunique = yunique[:-1]
1127+
1128+
z = np.interp(xn, bunique, yunique)
11301129
return z
11311130

11321131
def set_alpha(self, alpha):

‎lib/matplotlib/colors.py

Copy file name to clipboardExpand all lines: lib/matplotlib/colors.py
+70Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,76 @@ def scaled(self):
958958
return self.vmin is not None and self.vmax is not None
959959

960960

961+
class DivergingNorm(Normalize):
962+
def __init__(self, vcenter, vmin=None, vmax=None):
963+
"""
964+
Normalize data with a set center.
965+
966+
Useful when mapping data with an unequal rates of change around a
967+
conceptual center, e.g., data that range from -2 to 4, with 0 as
968+
the midpoint.
969+
970+
Parameters
971+
----------
972+
vcenter : float
973+
The data value that defines ``0.5`` in the normalization.
974+
vmin : float, optional
975+
The data value that defines ``0.0`` in the normalization.
976+
Defaults to the min value of the dataset.
977+
vmax : float, optional
978+
The data value that defines ``1.0`` in the normalization.
979+
Defaults to the the max value of the dataset.
980+
981+
Examples
982+
--------
983+
This maps data value -4000 to 0., 0 to 0.5, and +10000 to 1.0; data
984+
between is linearly interpolated::
985+
986+
>>> import matplotlib.colors as mcolors
987+
>>> offset = mcolors.DivergingNorm(vmin=-4000.,
988+
vcenter=0., vmax=10000)
989+
>>> data = [-4000., -2000., 0., 2500., 5000., 7500., 10000.]
990+
>>> offset(data)
991+
array([0., 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
992+
"""
993+
994+
self.vcenter = vcenter
995+
self.vmin = vmin
996+
self.vmax = vmax
997+
if vcenter is not None and vmax is not None and vcenter >= vmax:
998+
raise ValueError('vmin, vcenter, and vmax must be in '
999+
'ascending order')
1000+
if vcenter is not None and vmin is not None and vcenter <= vmin:
1001+
raise ValueError('vmin, vcenter, and vmax must be in '
1002+
'ascending order')
1003+
1004+
def autoscale_None(self, A):
1005+
"""
1006+
Get vmin and vmax, and then clip at vcenter
1007+
"""
1008+
super().autoscale_None(A)
1009+
if self.vmin > self.vcenter:
1010+
self.vmin = self.vcenter
1011+
if self.vmax < self.vcenter:
1012+
self.vmax = self.vcenter
1013+
1014+
def __call__(self, value, clip=None):
1015+
"""
1016+
Map value to the interval [0, 1]. The clip argument is unused.
1017+
"""
1018+
result, is_scalar = self.process_value(value)
1019+
self.autoscale_None(result) # sets self.vmin, self.vmax if None
1020+
1021+
if not self.vmin <= self.vcenter <= self.vmax:
1022+
raise ValueError("vmin, vcenter, vmax must increase monotonically")
1023+
result = np.ma.masked_array(
1024+
np.interp(result, [self.vmin, self.vcenter, self.vmax],
1025+
[0, 0.5, 1.]), mask=np.ma.getmask(result))
1026+
if is_scalar:
1027+
result = np.atleast_1d(result)[0]
1028+
return result
1029+
1030+
9611031
class LogNorm(Normalize):
9621032
"""Normalize a given value to the 0-1 range on a log scale."""
9631033

Binary file not shown.

‎lib/matplotlib/tests/test_colorbar.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_colorbar.py
+20-1Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from matplotlib import rc_context
55
from matplotlib.testing.decorators import image_comparison
66
import matplotlib.pyplot as plt
7-
from matplotlib.colors import BoundaryNorm, LogNorm, PowerNorm, Normalize
7+
from matplotlib.colors import (BoundaryNorm, LogNorm, PowerNorm, Normalize,
8+
DivergingNorm)
89
from matplotlib.cm import get_cmap
910
from matplotlib.colorbar import ColorbarBase, _ColorbarLogLocator
1011
from matplotlib.ticker import LogLocator, LogFormatter, FixedLocator
@@ -539,3 +540,21 @@ def test_colorbar_inverted_ticks():
539540
cbar.ax.invert_yaxis()
540541
np.testing.assert_allclose(ticks, cbar.get_ticks())
541542
np.testing.assert_allclose(minorticks, cbar.get_ticks(minor=True))
543+
544+
545+
def test_extend_colorbar_customnorm():
546+
# This was a funny error with DivergingNorm, maybe with other norms,
547+
# when extend='both'
548+
N = 100
549+
X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)]
550+
Z1 = np.exp(-X**2 - Y**2)
551+
Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
552+
Z = (Z1 - Z2) * 2
553+
554+
fig, ax = plt.subplots(2, 1)
555+
pcm = ax[0].pcolormesh(X, Y, Z,
556+
norm=DivergingNorm(vcenter=0., vmin=-2, vmax=1),
557+
cmap='RdBu_r')
558+
cb = fig.colorbar(pcm, ax=ax[0], extend='both')
559+
np.testing.assert_allclose(cb.ax.get_position().extents,
560+
[0.78375, 0.536364, 0.796147, 0.9], rtol=1e-3)

‎lib/matplotlib/tests/test_colors.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_colors.py
+90Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,96 @@ def test_Normalize():
221221
assert 0 < norm(1 + 50 * eps) < 1
222222

223223

224+
def test_DivergingNorm_autoscale():
225+
norm = mcolors.DivergingNorm(vcenter=20)
226+
norm.autoscale([10, 20, 30, 40])
227+
assert norm.vmin == 10.
228+
assert norm.vmax == 40.
229+
230+
231+
def test_DivergingNorm_autoscale_None_vmin():
232+
norm = mcolors.DivergingNorm(2, vmin=0, vmax=None)
233+
norm.autoscale_None([1, 2, 3, 4, 5])
234+
assert norm(5) == 1
235+
assert norm.vmax == 5
236+
237+
238+
def test_DivergingNorm_autoscale_None_vmax():
239+
norm = mcolors.DivergingNorm(2, vmin=None, vmax=10)
240+
norm.autoscale_None([1, 2, 3, 4, 5])
241+
assert norm(1) == 0
242+
assert norm.vmin == 1
243+
244+
245+
def test_DivergingNorm_scale():
246+
norm = mcolors.DivergingNorm(2)
247+
assert norm.scaled() is False
248+
norm([1, 2, 3, 4])
249+
assert norm.scaled() is True
250+
251+
252+
def test_DivergingNorm_scaleout_center():
253+
# test the vmin never goes above vcenter
254+
norm = mcolors.DivergingNorm(vcenter=0)
255+
x = norm([1, 2, 3, 5])
256+
assert norm.vmin == 0
257+
assert norm.vmax == 5
258+
259+
260+
def test_DivergingNorm_scaleout_center_max():
261+
# test the vmax never goes below vcenter
262+
norm = mcolors.DivergingNorm(vcenter=0)
263+
x = norm([-1, -2, -3, -5])
264+
assert norm.vmax == 0
265+
assert norm.vmin == -5
266+
267+
268+
def test_DivergingNorm_Even():
269+
norm = mcolors.DivergingNorm(vmin=-1, vcenter=0, vmax=4)
270+
vals = np.array([-1.0, -0.5, 0.0, 1.0, 2.0, 3.0, 4.0])
271+
expected = np.array([0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
272+
assert_array_equal(norm(vals), expected)
273+
274+
275+
def test_DivergingNorm_Odd():
276+
norm = mcolors.DivergingNorm(vmin=-2, vcenter=0, vmax=5)
277+
vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
278+
expected = np.array([0.0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
279+
assert_array_equal(norm(vals), expected)
280+
281+
282+
def test_DivergingNorm_VminEqualsVcenter():
283+
with pytest.raises(ValueError):
284+
norm = mcolors.DivergingNorm(vmin=-2, vcenter=-2, vmax=2)
285+
286+
287+
def test_DivergingNorm_VmaxEqualsVcenter():
288+
with pytest.raises(ValueError):
289+
norm = mcolors.DivergingNorm(vmin=-2, vcenter=2, vmax=2)
290+
291+
292+
def test_DivergingNorm_VminGTVcenter():
293+
with pytest.raises(ValueError):
294+
norm = mcolors.DivergingNorm(vmin=10, vcenter=0, vmax=20)
295+
296+
297+
def test_DivergingNorm_DivergingNorm_VminGTVmax():
298+
with pytest.raises(ValueError):
299+
norm = mcolors.DivergingNorm(vmin=10, vcenter=0, vmax=5)
300+
301+
302+
def test_DivergingNorm_VcenterGTVmax():
303+
vals = np.arange(50)
304+
with pytest.raises(ValueError):
305+
norm = mcolors.DivergingNorm(vmin=10, vcenter=25, vmax=20)
306+
307+
308+
def test_DivergingNorm_premature_scaling():
309+
norm = mcolors.DivergingNorm(vcenter=2)
310+
with pytest.raises(ValueError):
311+
norm.inverse(np.array([0.1, 0.5, 0.9]))
312+
313+
224314
def test_SymLogNorm():
225315
"""
226316
Test SymLogNorm behavior

‎lib/matplotlib/tests/test_contour.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_contour.py
+4-1Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,10 @@ def test_contourf_log_extension():
404404

405405

406406
@image_comparison(baseline_images=['contour_addlines'],
407-
extensions=['png'], remove_text=True, style='mpl20')
407+
extensions=['png'], remove_text=True, style='mpl20',
408+
tol=0.03)
409+
# tolerance is because image changed minutely when tick finding on
410+
# colorbars was cleaned up...
408411
def test_contour_addlines():
409412
fig, ax = plt.subplots()
410413
np.random.seed(19680812)

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.