Skip to content

Commit 422022e

Browse files
DivergingNorm Fair
1 parent 418984d commit 422022e

File tree

5 files changed

+192
-93
lines changed

5 files changed

+192
-93
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Fair DivergingNorm
2+
------------------
3+
`~.DivergingNorm` now has an argument ``fair``, which can be set to ``True``
4+
in order to create an off-centered normalization with equally spaced colors.
5+
6+
..plot::
7+
8+
import numpy as np
9+
import matplotlib.pyplot as plt
10+
from matplotlib.colors import DivergingNorm
11+
12+
np.random.seed(19680801)
13+
data = np.random.rand(4, 11)
14+
15+
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(7, 2))
16+
17+
norm1 = DivergingNorm(0.25, vmin=0, vmax=1, fair=False)
18+
im = ax1.imshow(data, cmap='RdBu', norm=norm1)
19+
cbar = fig.colorbar(im, ax=ax1, orientation="horizontal", aspect=15)
20+
21+
norm2 = DivergingNorm(0.25, vmin=0, vmax=1, fair=True)
22+
im = ax2.imshow(data, cmap='RdBu', norm=norm2)
23+
cbar = fig.colorbar(im, ax=ax2, orientation="horizontal", aspect=15)
24+
25+
ax1.set_title("DivergingNorm(.., fair=False)")
26+
ax2.set_title("DivergingNorm(.., fair=True)")
27+
plt.show()

examples/userdemo/colormap_normalizations_diverging.py

+46-9
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@
22
=====================================
33
DivergingNorm colormap normalization
44
=====================================
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.
125
"""
136

7+
##############################################################################
8+
# .. _divergingnorm-diffmap:
9+
#
10+
# Different mapping on either side of a center
11+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
12+
#
13+
# Sometimes we want to have a different colormap on either side of a
14+
# conceptual center point, and we want those two colormaps to have
15+
# different linear scales. An example is a topographic map where the land
16+
# and ocean have a center at zero, but land typically has a greater
17+
# elevation range than the water has depth range, and they are often
18+
# represented by a different colormap.
19+
# This achieved with a `~.DivergingNorm` and by setting its ``vcenter``
20+
# argument to zero.
21+
1422
import numpy as np
1523
import matplotlib.pyplot as plt
1624
import matplotlib.cbook as cbook
@@ -29,16 +37,45 @@
2937
colors_land = plt.cm.terrain(np.linspace(0.25, 1, 256))
3038
all_colors = np.vstack((colors_undersea, colors_land))
3139
terrain_map = colors.LinearSegmentedColormap.from_list('terrain_map',
32-
all_colors)
40+
all_colors)
3341

3442
# make the norm: Note the center is offset so that the land has more
3543
# dynamic range:
3644
divnorm = colors.DivergingNorm(vmin=-500, vcenter=0, vmax=4000)
3745

3846
pcm = ax.pcolormesh(longitude, latitude, topo, rasterized=True, norm=divnorm,
39-
cmap=terrain_map,)
47+
cmap=terrain_map)
4048
ax.set_xlabel('Lon $[^o E]$')
4149
ax.set_ylabel('Lat $[^o N]$')
4250
ax.set_aspect(1 / np.cos(np.deg2rad(49)))
4351
fig.colorbar(pcm, shrink=0.6, extend='both', label='Elevation [m]')
4452
plt.show()
53+
54+
55+
##############################################################################
56+
# .. _divergingnorm-fairmap:
57+
#
58+
# Fair mapping on either side of a center
59+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
60+
#
61+
# On other occasions it may be useful to preserve the linear mapping to colors,
62+
# but still define a center point, such that the colormap extends to both sides
63+
# of the center equally. This can be achieved by using the ``fair=True``
64+
# argument of the `~.DivergingNorm`.
65+
66+
np.random.seed(19680801)
67+
data = np.random.rand(11, 11)
68+
69+
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(8, 3.5))
70+
71+
norm1 = colors.DivergingNorm(0.25, vmin=0, vmax=1, fair=False)
72+
im = ax1.imshow(data, cmap='RdBu', norm=norm1)
73+
cbar = fig.colorbar(im, ax=ax1, ticks=[0, 0.25, 0.5, 0.75, 1])
74+
75+
norm2 = colors.DivergingNorm(0.25, vmin=0, vmax=1, fair=True)
76+
im = ax2.imshow(data, cmap='RdBu', norm=norm2)
77+
cbar = fig.colorbar(im, ax=ax2, ticks=[0, 0.25, 0.5, 0.75, 1])
78+
79+
ax1.set_title("DivergingNorm(.., fair=False)")
80+
ax2.set_title("DivergingNorm(.., fair=True)")
81+
plt.show()

lib/matplotlib/colors.py

+37-26
Original file line numberDiff line numberDiff line change
@@ -1061,11 +1061,11 @@ def scaled(self):
10611061

10621062

10631063
class DivergingNorm(Normalize):
1064-
def __init__(self, vcenter, vmin=None, vmax=None):
1064+
def __init__(self, vcenter, vmin=None, vmax=None, fair=False):
10651065
"""
10661066
Normalize data with a set center.
10671067
1068-
Useful when mapping data with an unequal rates of change around a
1068+
Useful when mapping data around a
10691069
conceptual center, e.g., data that range from -2 to 4, with 0 as
10701070
the midpoint.
10711071
@@ -1079,6 +1079,12 @@ def __init__(self, vcenter, vmin=None, vmax=None):
10791079
vmax : float, optional
10801080
The data value that defines ``1.0`` in the normalization.
10811081
Defaults to the the max value of the dataset.
1082+
fair : bool, optional
1083+
If *False* (default), the range between vmin and vmax will be
1084+
mapped to the normalized range ``[0,1]``. If *True*, the range
1085+
``[vcenter-d, vcenter+d]`` with
1086+
``d=max(abs(vcenter-vmin), abs(vmax-vcenter))`` is mapped to
1087+
``[0,1]``. This is useful to ensure colors are equally distributed.
10821088
10831089
Examples
10841090
--------
@@ -1091,40 +1097,45 @@ def __init__(self, vcenter, vmin=None, vmax=None):
10911097
>>> data = [-4000., -2000., 0., 2500., 5000., 7500., 10000.]
10921098
>>> offset(data)
10931099
array([0., 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
1100+
1101+
A more detailed example is found in
1102+
:doc:`/gallery/userdemo/colormap_normalizations_diverging`
10941103
"""
10951104

10961105
self.vcenter = vcenter
1097-
self.vmin = vmin
1098-
self.vmax = vmax
1099-
if vcenter is not None and vmax is not None and vcenter >= vmax:
1100-
raise ValueError('vmin, vcenter, and vmax must be in '
1101-
'ascending order')
1102-
if vcenter is not None and vmin is not None and vcenter <= vmin:
1103-
raise ValueError('vmin, vcenter, and vmax must be in '
1104-
'ascending order')
1105-
1106-
def autoscale_None(self, A):
1107-
"""
1108-
Get vmin and vmax, and then clip at vcenter
1109-
"""
1110-
super().autoscale_None(A)
1111-
if self.vmin > self.vcenter:
1112-
self.vmin = self.vcenter
1113-
if self.vmax < self.vcenter:
1114-
self.vmax = self.vcenter
1106+
self.fair = fair
1107+
super().__init__(vmin=vmin, vmax=vmax)
11151108

11161109
def __call__(self, value, clip=None):
11171110
"""
1118-
Map value to the interval [0, 1]. The clip argument is unused.
1111+
Map value to (a subset of) the interval [0, 1].
1112+
The clip argument is unused.
11191113
"""
11201114
result, is_scalar = self.process_value(value)
11211115
self.autoscale_None(result) # sets self.vmin, self.vmax if None
11221116

1123-
if not self.vmin <= self.vcenter <= self.vmax:
1124-
raise ValueError("vmin, vcenter, vmax must increase monotonically")
1125-
result = np.ma.masked_array(
1126-
np.interp(result, [self.vmin, self.vcenter, self.vmax],
1127-
[0, 0.5, 1.]), mask=np.ma.getmask(result))
1117+
if self.vmin > self.vmax:
1118+
raise ValueError("vmin must be less or equal vmax")
1119+
elif self.vmin == self.vmax:
1120+
interp_x = [self.vmin, self.vmax]
1121+
interp_y = [0.0, 0.0]
1122+
elif self.vcenter >= self.vmax:
1123+
interp_x = [self.vmin, self.vcenter]
1124+
interp_y = [0.0, 0.5]
1125+
elif self.vcenter <= self.vmin:
1126+
interp_x = [self.vcenter, self.vmax]
1127+
interp_y = [0.5, 1.0]
1128+
elif self.fair:
1129+
maxrange = max(np.abs(self.vcenter - self.vmin),
1130+
np.abs(self.vmax - self.vcenter))
1131+
interp_x = [self.vcenter - maxrange, self.vcenter + maxrange]
1132+
interp_y = [0, 1.]
1133+
else:
1134+
interp_x = [self.vmin, self.vcenter, self.vmax]
1135+
interp_y = [0, 0.5, 1.]
1136+
1137+
result = np.ma.masked_array(np.interp(result, interp_x, interp_y),
1138+
mask=np.ma.getmask(result))
11281139
if is_scalar:
11291140
result = np.atleast_1d(result)[0]
11301141
return result

lib/matplotlib/tests/test_colors.py

+34-48
Original file line numberDiff line numberDiff line change
@@ -324,58 +324,44 @@ def test_DivergingNorm_scale():
324324

325325

326326
def test_DivergingNorm_scaleout_center():
327-
# test the vmin never goes above vcenter
327+
# test vcenter outside [vmin, vmax]
328328
norm = mcolors.DivergingNorm(vcenter=0)
329-
norm([1, 2, 3, 5])
330-
assert norm.vmin == 0
331-
assert norm.vmax == 5
332-
329+
a = norm([1., 2., 3., 5.])
330+
assert norm.vmin == 1.
331+
assert norm.vmax == 5.
332+
assert_array_almost_equal(a, np.array([0.6, 0.7, 0.8, 1.0]))
333333

334-
def test_DivergingNorm_scaleout_center_max():
335-
# test the vmax never goes below vcenter
336334
norm = mcolors.DivergingNorm(vcenter=0)
337-
norm([-1, -2, -3, -5])
338-
assert norm.vmax == 0
339-
assert norm.vmin == -5
340-
341-
342-
def test_DivergingNorm_Even():
343-
norm = mcolors.DivergingNorm(vmin=-1, vcenter=0, vmax=4)
344-
vals = np.array([-1.0, -0.5, 0.0, 1.0, 2.0, 3.0, 4.0])
345-
expected = np.array([0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
346-
assert_array_equal(norm(vals), expected)
347-
348-
349-
def test_DivergingNorm_Odd():
350-
norm = mcolors.DivergingNorm(vmin=-2, vcenter=0, vmax=5)
351-
vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
352-
expected = np.array([0.0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
353-
assert_array_equal(norm(vals), expected)
354-
355-
356-
def test_DivergingNorm_VminEqualsVcenter():
357-
with pytest.raises(ValueError):
358-
mcolors.DivergingNorm(vmin=-2, vcenter=-2, vmax=2)
359-
360-
361-
def test_DivergingNorm_VmaxEqualsVcenter():
362-
with pytest.raises(ValueError):
363-
mcolors.DivergingNorm(vmin=-2, vcenter=2, vmax=2)
364-
365-
366-
def test_DivergingNorm_VminGTVcenter():
367-
with pytest.raises(ValueError):
368-
mcolors.DivergingNorm(vmin=10, vcenter=0, vmax=20)
369-
370-
371-
def test_DivergingNorm_DivergingNorm_VminGTVmax():
372-
with pytest.raises(ValueError):
373-
mcolors.DivergingNorm(vmin=10, vcenter=0, vmax=5)
374-
375-
376-
def test_DivergingNorm_VcenterGTVmax():
335+
a = norm([-1., -2., -3., -5.])
336+
assert norm.vmax == -1.
337+
assert norm.vmin == -5.
338+
assert_array_almost_equal(a, np.array([0.4, 0.3, 0.2, 0.0]))
339+
340+
341+
@pytest.mark.parametrize("vmin,vc,vmax,fair,vals,expect",
342+
[[-1, 0, 4, False, [-1.0, -0.5, 0.0, 1.0, 2.0, 3.0, 4.0],
343+
[0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0]],
344+
[-2, 0, 5, False, [-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
345+
[0.0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]],
346+
[-2, -2, 2, False, [-3, -2, -1, 0, 1, 2, 3],
347+
[0.5, 0.5, 0.625, 0.75, 0.875, 1., 1.]],
348+
[-2, 2, 2, False, [-3, -2, -1, 0, 1, 2, 3],
349+
[0., 0., 0.125, 0.25, 0.375, 0.5, 0.5]],
350+
[10, 0, 20, False, [0, 5, 10, 15, 20], [0.5, 0.625, 0.75, 0.875, 1.]],
351+
[10, 30, 20, False, [10, 15, 20, 25, 30], [0., 0.125, 0.25, 0.375, 0.5]],
352+
[-4, -4, -4, False, [-8, -6, -4, -2, 0], [0., 0., 0., 0., 0.]],
353+
[-6, 0, 12, True, [-12, -6, -3, 0, 6, 12],
354+
[0., 0.25, 0.375, 0.5, 0.75, 1.]]])
355+
def test_DivergingNorm_Misc(vmin, vc, vmax, fair, vals, expect):
356+
norm = mcolors.DivergingNorm(vmin=vmin, vcenter=vc, vmax=vmax, fair=fair)
357+
assert_array_equal(norm(vals), np.array(expect))
358+
359+
360+
def test_DivergingNorm_rasing():
361+
# test for vmin > vmax -> not allowed
377362
with pytest.raises(ValueError):
378-
mcolors.DivergingNorm(vmin=10, vcenter=25, vmax=20)
363+
norm = mcolors.DivergingNorm(vmin=10, vcenter=0, vmax=5)
364+
norm(np.array([-3, -2, -1, 0, 1, 2, 3]))
379365

380366

381367
def test_DivergingNorm_premature_scaling():

0 commit comments

Comments
 (0)