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 03c293b

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

File tree

Expand file treeCollapse file tree

5 files changed

+222
-0
lines changed
Filter options
Expand file treeCollapse file tree

5 files changed

+222
-0
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
@@ -20,6 +20,7 @@ Classes
2020

2121
BoundaryNorm
2222
Colormap
23+
CenteredNorm
2324
LightSource
2425
LinearSegmentedColormap
2526
ListedColormap
+35Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/colors.py
+95Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_colors.py
+50Lines 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

Copy file name to clipboardExpand all lines: tutorials/colors/colormapnorms.py
+41Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.