Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 03c293b

Browse files
authored
Merge pull request #17709 from jpmattern/symnorm
Enh: SymNorm for normalizing symmetrical data around a center
2 parents fda6398 + 101d08d commit 03c293b

5 files changed

Lines changed: 222 additions & 0 deletions

File tree

doc/api/colors_api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Classes
2020

2121
BoundaryNorm
2222
Colormap
23+
CenteredNorm
2324
LightSource
2425
LinearSegmentedColormap
2526
ListedColormap
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
New CenteredNorm for symmetrical data around a center
2+
-----------------------------------------------------
3+
In cases where data is symmetrical around a center, for example, positive and
4+
negative anomalies around a center zero, `~.matplotlib.colors.CenteredNorm`
5+
is a new norm that automatically creates a symmetrical mapping around the
6+
center. This norm is well suited to be combined with a divergent colormap which
7+
uses an unsaturated color in its center.
8+
9+
.. plot::
10+
11+
import matplotlib.pyplot as plt
12+
import numpy as np
13+
from matplotlib.colors import CenteredNorm
14+
15+
np.random.seed(20201004)
16+
data = np.random.normal(size=(3, 4), loc=1)
17+
18+
fig, ax = plt.subplots()
19+
pc = ax.pcolormesh(data, cmap=plt.get_cmap('RdGy'), norm=CenteredNorm())
20+
fig.colorbar(pc)
21+
ax.set_title('data centered around zero')
22+
23+
# add text annotation
24+
for irow, data_row in enumerate(data):
25+
for icol, val in enumerate(data_row):
26+
ax.text(icol + 0.5, irow + 0.5, f'{val:.2f}', color='C0',
27+
size=16, va='center', ha='center')
28+
plt.show()
29+
30+
If the center of symmetry is different from 0, it can be set with the *vcenter*
31+
argument. To manually set the range of `~.matplotlib.colors.CenteredNorm`, use
32+
the *halfrange* argument.
33+
34+
See :doc:`/tutorials/colors/colormapnorms` for an example and more details
35+
about data normalization.

lib/matplotlib/colors.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,101 @@ def __call__(self, value, clip=None):
12551255
return result
12561256

12571257

1258+
class CenteredNorm(Normalize):
1259+
def __init__(self, vcenter=0, halfrange=None, clip=False):
1260+
"""
1261+
Normalize symmetrical data around a center (0 by default).
1262+
1263+
Unlike `TwoSlopeNorm`, `CenteredNorm` applies an equal rate of change
1264+
around the center.
1265+
1266+
Useful when mapping symmetrical data around a conceptual center
1267+
e.g., data that range from -2 to 4, with 0 as the midpoint, and
1268+
with equal rates of change around that midpoint.
1269+
1270+
Parameters
1271+
----------
1272+
vcenter : float, default: 0
1273+
The data value that defines ``0.5`` in the normalization.
1274+
halfrange : float, optional
1275+
The range of data values that defines a range of ``0.5`` in the
1276+
normalization, so that *vcenter* - *halfrange* is ``0.0`` and
1277+
*vcenter* + *halfrange* is ``1.0`` in the normalization.
1278+
Defaults to the largest absolute difference to *vcenter* for
1279+
the values in the dataset.
1280+
1281+
Examples
1282+
--------
1283+
This maps data values -2 to 0.25, 0 to 0.5, and 4 to 1.0
1284+
(assuming equal rates of change above and below 0.0):
1285+
1286+
>>> import matplotlib.colors as mcolors
1287+
>>> norm = mcolors.CenteredNorm(halfrange=4.0)
1288+
>>> data = [-2., 0., 4.]
1289+
>>> norm(data)
1290+
array([0.25, 0.5 , 1. ])
1291+
"""
1292+
self._vcenter = vcenter
1293+
# calling the halfrange setter to set vmin and vmax
1294+
self.halfrange = halfrange
1295+
self.clip = clip
1296+
1297+
def _set_vmin_vmax(self):
1298+
"""
1299+
Set *vmin* and *vmax* based on *vcenter* and *halfrange*.
1300+
"""
1301+
self.vmax = self._vcenter + self._halfrange
1302+
self.vmin = self._vcenter - self._halfrange
1303+
1304+
def autoscale(self, A):
1305+
"""
1306+
Set *halfrange* to ``max(abs(A-vcenter))``, then set *vmin* and *vmax*.
1307+
"""
1308+
A = np.asanyarray(A)
1309+
self._halfrange = max(self._vcenter-A.min(),
1310+
A.max()-self._vcenter)
1311+
self._set_vmin_vmax()
1312+
1313+
def autoscale_None(self, A):
1314+
"""Set *vmin* and *vmax*."""
1315+
A = np.asanyarray(A)
1316+
if self.vmax is None and A.size:
1317+
self.autoscale(A)
1318+
1319+
@property
1320+
def vcenter(self):
1321+
return self._vcenter
1322+
1323+
@vcenter.setter
1324+
def vcenter(self, vcenter):
1325+
self._vcenter = vcenter
1326+
if self.vmax is not None:
1327+
# recompute halfrange assuming vmin and vmax represent
1328+
# min and max of data
1329+
self._halfrange = max(self._vcenter-self.vmin,
1330+
self.vmax-self._vcenter)
1331+
self._set_vmin_vmax()
1332+
1333+
@property
1334+
def halfrange(self):
1335+
return self._halfrange
1336+
1337+
@halfrange.setter
1338+
def halfrange(self, halfrange):
1339+
if halfrange is None:
1340+
self._halfrange = None
1341+
self.vmin = None
1342+
self.vmax = None
1343+
else:
1344+
self._halfrange = abs(halfrange)
1345+
1346+
def __call__(self, value, clip=None):
1347+
if self._halfrange is not None:
1348+
# enforce symmetry, reset vmin and vmax
1349+
self._set_vmin_vmax()
1350+
return super().__call__(value, clip=clip)
1351+
1352+
12581353
def _make_norm_from_scale(scale_cls, base_norm_cls=None, *, init=None):
12591354
"""
12601355
Decorator for building a `.Normalize` subclass from a `.Scale` subclass.

lib/matplotlib/tests/test_colors.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,56 @@ def test_BoundaryNorm():
390390
assert_array_equal(cmshould(mynorm(x)), cmref(refnorm(x)))
391391

392392

393+
def test_CenteredNorm():
394+
np.random.seed(0)
395+
396+
# Assert equivalence to symmetrical Normalize.
397+
x = np.random.normal(size=100)
398+
x_maxabs = np.max(np.abs(x))
399+
norm_ref = mcolors.Normalize(vmin=-x_maxabs, vmax=x_maxabs)
400+
norm = mcolors.CenteredNorm()
401+
assert_array_almost_equal(norm_ref(x), norm(x))
402+
403+
# Check that vcenter is in the center of vmin and vmax
404+
# when vcenter is set.
405+
vcenter = int(np.random.normal(scale=50))
406+
norm = mcolors.CenteredNorm(vcenter=vcenter)
407+
norm.autoscale_None([1, 2])
408+
assert norm.vmax + norm.vmin == 2 * vcenter
409+
410+
# Check that halfrange input works correctly.
411+
x = np.random.normal(size=10)
412+
norm = mcolors.CenteredNorm(vcenter=0.5, halfrange=0.5)
413+
assert_array_almost_equal(x, norm(x))
414+
norm = mcolors.CenteredNorm(vcenter=1, halfrange=1)
415+
assert_array_almost_equal(x, 2 * norm(x))
416+
417+
# Check that halfrange input works correctly and use setters.
418+
norm = mcolors.CenteredNorm()
419+
norm.vcenter = 2
420+
norm.halfrange = 2
421+
assert_array_almost_equal(x, 4 * norm(x))
422+
423+
# Check that prior to adding data, setting halfrange first has same effect.
424+
norm = mcolors.CenteredNorm()
425+
norm.halfrange = 2
426+
norm.vcenter = 2
427+
assert_array_almost_equal(x, 4 * norm(x))
428+
429+
# Check that manual change of vcenter adjusts halfrange accordingly.
430+
norm = mcolors.CenteredNorm()
431+
assert norm.vcenter == 0
432+
# add data
433+
norm(np.linspace(-1.0, 0.0, 10))
434+
assert norm.vmax == 1.0
435+
assert norm.halfrange == 1.0
436+
# set vcenter to 1, which should double halfrange
437+
norm.vcenter = 1
438+
assert norm.vmin == -1.0
439+
assert norm.vmax == 3.0
440+
assert norm.halfrange == 2.0
441+
442+
393443
@pytest.mark.parametrize("vmin,vmax", [[-1, 2], [3, 1]])
394444
def test_lognorm_invalid(vmin, vmax):
395445
# Check that invalid limits in LogNorm error

tutorials/colors/colormapnorms.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import matplotlib.pyplot as plt
4848
import matplotlib.colors as colors
4949
import matplotlib.cbook as cbook
50+
from matplotlib import cm
5051

5152
N = 100
5253
X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)]
@@ -69,6 +70,46 @@
6970
fig.colorbar(pcm, ax=ax[1], extend='max')
7071
plt.show()
7172

73+
###############################################################################
74+
# Centered
75+
# --------
76+
#
77+
# In many cases, data is symmetrical around a center, for example, positive and
78+
# negative anomalies around a center 0. In this case, we would like the center
79+
# to be mapped to 0.5 and the datapoint with the largest deviation from the
80+
# center to be mapped to 1.0, if its value is greater than the center, or 0.0
81+
# otherwise. The norm `.colors.CenteredNorm` creates such a mapping
82+
# automatically. It is well suited to be combined with a divergent colormap
83+
# which uses different colors edges that meet in the center at an unsaturated
84+
# color.
85+
#
86+
# If the center of symmetry is different from 0, it can be set with the
87+
# *vcenter* argument. For logarithmic scaling on both sides of the center, see
88+
# `.colors.SymLogNorm` below; to apply a different mapping above and below the
89+
# center, use `.colors.TwoSlopeNorm` below.
90+
91+
delta = 0.1
92+
x = np.arange(-3.0, 4.001, delta)
93+
y = np.arange(-4.0, 3.001, delta)
94+
X, Y = np.meshgrid(x, y)
95+
Z1 = np.exp(-X**2 - Y**2)
96+
Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
97+
Z = (0.9*Z1 - 0.5*Z2) * 2
98+
99+
# select a divergent colormap
100+
cmap = cm.coolwarm
101+
102+
fig, (ax1, ax2) = plt.subplots(ncols=2)
103+
pc = ax1.pcolormesh(Z, cmap=cmap)
104+
fig.colorbar(pc, ax=ax1)
105+
ax1.set_title('Normalize()')
106+
107+
pc = ax2.pcolormesh(Z, norm=colors.CenteredNorm(), cmap=cmap)
108+
fig.colorbar(pc, ax=ax2)
109+
ax2.set_title('CenteredNorm()')
110+
111+
plt.show()
112+
72113
###############################################################################
73114
# Symmetric logarithmic
74115
# ---------------------

0 commit comments

Comments
 (0)