Skip to content

Commit 989c966

Browse files
committed
ENH: Add func norm
1 parent 1b9de36 commit 989c966

File tree

5 files changed

+85
-2
lines changed

5 files changed

+85
-2
lines changed

doc/api/colors_api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Classes
3030
PowerNorm
3131
SymLogNorm
3232
TwoSlopeNorm
33+
FuncNorm
3334

3435
Functions
3536
---------

examples/userdemo/colormap_normalizations.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@
7777
shading='nearest')
7878
fig.colorbar(pcm, ax=ax[1], extend='both')
7979

80-
8180
###############################################################################
8281
# Custom Norm: An example with a customized normalization. This one
8382
# uses the example above, and normalizes the negative data differently

lib/matplotlib/colors.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,6 +1464,38 @@ def inverse(self, value):
14641464
*bound_init_signature.parameters.values()])
14651465
return Norm
14661466

1467+
@_make_norm_from_scale(
1468+
scale.FuncScale,
1469+
init=lambda functions, vmin=None, vmax=None, clip=False: None)
1470+
class FuncNorm(Normalize):
1471+
"""
1472+
Arbitrary normalization using functions for the forward and inverse.
1473+
1474+
Parameters
1475+
----------
1476+
functions : (callable, callable)
1477+
two-tuple of the forward and inverse functions for the normalization.
1478+
The forward function must be monotonic.
1479+
1480+
Both functions must have the signature ::
1481+
1482+
def forward(values: array-like) -> array-like
1483+
1484+
vmin, vmax : float or None
1485+
If *vmin* and/or *vmax* is not given, they are initialized from the
1486+
minimum and maximum value, respectively, of the first input
1487+
processed; i.e., ``__call__(A)`` calls ``autoscale_None(A)``.
1488+
1489+
clip : bool, default: False
1490+
If ``True`` values falling outside the range ``[vmin, vmax]``,
1491+
are mapped to 0 or 1, whichever is closer, and masked values are
1492+
set to 1. If ``False`` masked values remain masked.
1493+
1494+
Clipping silently defeats the purpose of setting the over, under,
1495+
and masked colors in a colormap, so it is likely to lead to
1496+
surprises; therefore the default is ``clip=False``.
1497+
"""
1498+
14671499

14681500
@_make_norm_from_scale(functools.partial(scale.LogScale, nonpositive="mask"))
14691501
class LogNorm(Normalize):

lib/matplotlib/tests/test_colors.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,25 @@ def test_Normalize():
526526
assert 0 < norm(1 + 50 * eps) < 1
527527

528528

529+
def test_FuncNorm():
530+
def _forward(x):
531+
return (x**2)
532+
def _inverse(x):
533+
return np.sqrt(x)
534+
535+
norm = mcolors.FuncNorm((_forward, _inverse), vmin=0, vmax=10)
536+
expected = np.array([0, 0.25, 1])
537+
assert_array_almost_equal(norm([0, 5, 10]), expected)
538+
539+
def _forward(x):
540+
return np.log10(x)
541+
def _inverse(x):
542+
return 10**x
543+
norm = mcolors.FuncNorm((_forward, _inverse), vmin=0.1, vmax=10)
544+
lognorm = mcolors.LogNorm(vmin=0.1, vmax=10)
545+
assert_array_almost_equal(norm([0.2, 5, 10]), lognorm([0.2, 5, 10]))
546+
547+
529548
def test_TwoSlopeNorm_autoscale():
530549
norm = mcolors.TwoSlopeNorm(vcenter=20)
531550
norm.autoscale([10, 20, 30, 40])

tutorials/colors/colormapnorms.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,16 @@
169169
X, Y = np.mgrid[0:3:complex(0, N), 0:2:complex(0, N)]
170170
Z1 = (1 + np.sin(Y * 10.)) * X**2
171171

172-
fig, ax = plt.subplots(2, 1)
172+
fig, ax = plt.subplots(2, 1, constrained_layout=True)
173173

174174
pcm = ax[0].pcolormesh(X, Y, Z1, norm=colors.PowerNorm(gamma=0.5),
175175
cmap='PuBu_r', shading='auto')
176176
fig.colorbar(pcm, ax=ax[0], extend='max')
177+
ax[0].set_title('PowerNorm()')
177178

178179
pcm = ax[1].pcolormesh(X, Y, Z1, cmap='PuBu_r', shading='auto')
179180
fig.colorbar(pcm, ax=ax[1], extend='max')
181+
ax[1].set_title('Normalize()')
180182
plt.show()
181183

182184
###############################################################################
@@ -274,17 +276,46 @@
274276
# Simple geographic plot, set aspect ratio beecause distance between lines of
275277
# longitude depends on latitude.
276278
ax.set_aspect(1 / np.cos(np.deg2rad(49)))
279+
ax.set_title('TwoSlopeNorm(x)')
277280
fig.colorbar(pcm, shrink=0.6)
278281
plt.show()
279282

280283

284+
###############################################################################
285+
# FuncNorm: Arbitrary function normalization
286+
# ------------------------------------------
287+
#
288+
# If the above norms do not provide the normalization you want, and you
289+
# can express your normalization in terms of function, you can use
290+
# `~.colors.FuncNorm`. Note that this example is the same as
291+
# `PowerNorm` with a power of 0.5:
292+
293+
def _forward(x):
294+
return np.sqrt(x)
295+
296+
297+
def _inverse(x):
298+
return x**2
299+
300+
N = 100
301+
X, Y = np.mgrid[0:3:complex(0, N), 0:2:complex(0, N)]
302+
Z1 = (1 + np.sin(Y * 10.)) * X**2
303+
fig, ax = plt.subplots()
304+
305+
norm = colors.FuncNorm((_forward, _inverse), vmin=0, vmax=20)
306+
pcm = ax.pcolormesh(X, Y, Z1, norm=norm, cmap='PuBu_r', shading='auto')
307+
ax.set_title('FuncNorm(x)')
308+
fig.colorbar(pcm, shrink=0.6)
309+
plt.show()
310+
281311
###############################################################################
282312
# Custom normalization: Manually implement two linear ranges
283313
# ----------------------------------------------------------
284314
#
285315
# The `.TwoSlopeNorm` described above makes a useful example for
286316
# defining your own norm.
287317

318+
288319
class MidpointNormalize(colors.Normalize):
289320
def __init__(self, vmin=None, vmax=None, vcenter=None, clip=False):
290321
self.vcenter = vcenter
@@ -303,5 +334,6 @@ def __call__(self, value, clip=None):
303334
pcm = ax.pcolormesh(longitude, latitude, topo, rasterized=True, norm=midnorm,
304335
cmap=terrain_map, shading='auto')
305336
ax.set_aspect(1 / np.cos(np.deg2rad(49)))
337+
ax.set_title('Custom norm')
306338
fig.colorbar(pcm, shrink=0.6, extend='both')
307339
plt.show()

0 commit comments

Comments
 (0)