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

Skip to content

Commit d6f86c1

Browse files
committed
Set norms using scale names.
1 parent 04051bd commit d6f86c1

File tree

4 files changed

+110
-29
lines changed

4 files changed

+110
-29
lines changed

lib/matplotlib/cm.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
"""
1717

1818
from collections.abc import Mapping
19+
import functools
1920

2021
import numpy as np
2122
from numpy import ma
2223

2324
import matplotlib as mpl
24-
from matplotlib import _api, colors, cbook
25+
from matplotlib import _api, colors, cbook, scale
2526
from matplotlib._cm import datad
2627
from matplotlib._cm_listed import cmaps as cmaps_listed
2728

@@ -308,6 +309,34 @@ def unregister_cmap(name):
308309
return cmap
309310

310311

312+
def _auto_norm_from_scale(scale_cls):
313+
"""
314+
Automatically generate a norm class from *scale_cls*.
315+
316+
This differs from `.colors.make_norm_from_scale` in the following points:
317+
318+
- This function is not a class decorator, but directly returns a norm class
319+
(as if decorating `.Normalize`).
320+
- The scale is automatically constructed with ``nonpositive="mask"``, if it
321+
supports such a parameter, to work around the difference in defaults
322+
between standard scales (which use "clip") and norms (which use "mask").
323+
324+
Note that ``make_norm_from_scale`` caches the generated norm classes
325+
(not the instances) and reuses them for later calls. For example,
326+
``type(_auto_norm_from_scale("log")) == LogNorm``.
327+
"""
328+
# Actually try to construct an instance, to verify whether
329+
# ``nonpositive="mask"`` is supported.
330+
try:
331+
norm = colors.make_norm_from_scale(
332+
functools.partial(scale_cls, nonpositive="mask"))(
333+
colors.Normalize)()
334+
except TypeError:
335+
norm = colors.make_norm_from_scale(scale_cls)(
336+
colors.Normalize)()
337+
return type(norm)
338+
339+
311340
class ScalarMappable:
312341
"""
313342
A mixin class to map scalar data to RGBA.
@@ -318,12 +347,13 @@ class ScalarMappable:
318347

319348
def __init__(self, norm=None, cmap=None):
320349
"""
321-
322350
Parameters
323351
----------
324-
norm : `matplotlib.colors.Normalize` (or subclass thereof)
352+
norm : `.Normalize` (or subclass thereof) or str or None
325353
The normalizing object which scales data, typically into the
326354
interval ``[0, 1]``.
355+
If a `str`, a `.Normalize` subclass is dynamically generated based
356+
on the scale with the corresponding name.
327357
If *None*, *norm* defaults to a *colors.Normalize* object which
328358
initializes its scaling based on the first data processed.
329359
cmap : str or `~matplotlib.colors.Colormap`
@@ -353,11 +383,11 @@ def _scale_norm(self, norm, vmin, vmax):
353383
"""
354384
if vmin is not None or vmax is not None:
355385
self.set_clim(vmin, vmax)
356-
if norm is not None:
386+
if isinstance(norm, colors.Normalize):
357387
raise ValueError(
358-
"Passing parameters norm and vmin/vmax simultaneously is "
359-
"not supported. Please pass vmin/vmax directly to the "
360-
"norm when creating it.")
388+
"Passing a Normalize instance simultaneously with "
389+
"vmin/vmax is not supported. Please pass vmin/vmax "
390+
"directly to the norm when creating it.")
361391

362392
# always resolve the autoscaling so we have concrete limits
363393
# rather than deferring to draw time.
@@ -531,9 +561,18 @@ def norm(self):
531561

532562
@norm.setter
533563
def norm(self, norm):
534-
_api.check_isinstance((colors.Normalize, None), norm=norm)
564+
_api.check_isinstance((colors.Normalize, str, None), norm=norm)
535565
if norm is None:
536566
norm = colors.Normalize()
567+
elif isinstance(norm, str):
568+
try:
569+
scale_cls = scale._scale_mapping[norm]
570+
except KeyError:
571+
raise ValueError(
572+
"Invalid norm str name; the following values are "
573+
"supported: {}".format(", ".join(scale._scale_mapping))
574+
) from None
575+
norm = _auto_norm_from_scale(scale_cls)()
537576

538577
if norm is self.norm:
539578
# We aren't updating anything
@@ -555,7 +594,7 @@ def set_norm(self, norm):
555594
556595
Parameters
557596
----------
558-
norm : `.Normalize` or None
597+
norm : `.Normalize` or str or None
559598
560599
Notes
561600
-----

lib/matplotlib/colors.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1598,21 +1598,37 @@ class norm_cls(Normalize):
15981598
if base_norm_cls is None:
15991599
return functools.partial(make_norm_from_scale, scale_cls, init=init)
16001600

1601+
if isinstance(scale_cls, functools.partial):
1602+
scale_args = scale_cls.args
1603+
scale_kwargs_items = tuple(scale_cls.keywords.items())
1604+
scale_cls = scale_cls.func
1605+
else:
1606+
scale_args = scale_kwargs_items = ()
1607+
16011608
if init is None:
16021609
def init(vmin=None, vmax=None, clip=False): pass
16031610

16041611
return _make_norm_from_scale(
1605-
scale_cls, base_norm_cls, inspect.signature(init))
1612+
scale_cls, scale_args, scale_kwargs_items,
1613+
base_norm_cls, inspect.signature(init))
16061614

16071615

16081616
@functools.lru_cache(None)
1609-
def _make_norm_from_scale(scale_cls, base_norm_cls, bound_init_signature):
1617+
def _make_norm_from_scale(
1618+
scale_cls, scale_args, scale_kwargs_items,
1619+
base_norm_cls, bound_init_signature,
1620+
):
16101621
"""
16111622
Helper for `make_norm_from_scale`.
16121623
1613-
This function is split out so that it takes a signature object as third
1614-
argument (as signatures are picklable, contrary to arbitrary lambdas);
1615-
caching is also used so that different unpickles reuse the same class.
1624+
This function is split out to enable caching (in particular so that
1625+
different unpickles reuse the same class). In order to do so,
1626+
1627+
- ``functools.partial`` *scale_cls* is expanded into ``func, args, kwargs``
1628+
to allow memoizing returned norms (partial instances always compare
1629+
unequal, but we can check identity based on ``func, args, kwargs``;
1630+
- *init* is replaced by *init_signature*, as signatures are picklable,
1631+
unlike to arbitrary lambdas.
16161632
"""
16171633

16181634
class Norm(base_norm_cls):
@@ -1631,15 +1647,18 @@ def __reduce__(self):
16311647
except (ImportError, AttributeError):
16321648
pass
16331649
return (_picklable_norm_constructor,
1634-
(scale_cls, base_norm_cls, bound_init_signature),
1650+
(scale_cls, scale_args, scale_kwargs_items,
1651+
base_norm_cls, bound_init_signature),
16351652
vars(self))
16361653

16371654
def __init__(self, *args, **kwargs):
16381655
ba = bound_init_signature.bind(*args, **kwargs)
16391656
ba.apply_defaults()
16401657
super().__init__(
16411658
**{k: ba.arguments.pop(k) for k in ["vmin", "vmax", "clip"]})
1642-
self._scale = scale_cls(axis=None, **ba.arguments)
1659+
self._scale = functools.partial(
1660+
scale_cls, *scale_args, **dict(scale_kwargs_items))(
1661+
axis=None, **ba.arguments)
16431662
self._trf = self._scale.get_transform()
16441663

16451664
__init__.__signature__ = bound_init_signature.replace(parameters=[
@@ -1693,12 +1712,12 @@ def autoscale_None(self, A):
16931712
in_trf_domain = np.extract(np.isfinite(self._trf.transform(A)), A)
16941713
return super().autoscale_None(in_trf_domain)
16951714

1696-
Norm.__name__ = (
1697-
f"{scale_cls.__name__}Norm" if base_norm_cls is Normalize
1698-
else base_norm_cls.__name__)
1699-
Norm.__qualname__ = (
1700-
f"{scale_cls.__qualname__}Norm" if base_norm_cls is Normalize
1701-
else base_norm_cls.__qualname__)
1715+
if base_norm_cls is Normalize:
1716+
Norm.__name__ = f"{scale_cls.__name__}Norm"
1717+
Norm.__qualname__ = f"{scale_cls.__qualname__}Norm"
1718+
else:
1719+
Norm.__name__ = base_norm_cls.__name__
1720+
Norm.__qualname__ = base_norm_cls.__qualname__
17021721
Norm.__module__ = base_norm_cls.__module__
17031722
Norm.__doc__ = base_norm_cls.__doc__
17041723

@@ -1746,9 +1765,10 @@ def forward(values: array-like) -> array-like
17461765
"""
17471766

17481767

1749-
@make_norm_from_scale(functools.partial(scale.LogScale, nonpositive="mask"))
1750-
class LogNorm(Normalize):
1751-
"""Normalize a given value to the 0-1 range on a log scale."""
1768+
LogNorm = make_norm_from_scale(
1769+
functools.partial(scale.LogScale, nonpositive="mask"))(Normalize)
1770+
LogNorm.__name__ = LogNorm.__qualname__ = "LogNorm"
1771+
LogNorm.__doc__ = "Normalize a given value to the 0-1 range on a log scale."
17521772

17531773

17541774
@make_norm_from_scale(

lib/matplotlib/tests/test_axes.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -982,8 +982,8 @@ def test_imshow_norm_vminvmax():
982982
a = [[1, 2], [3, 4]]
983983
ax = plt.axes()
984984
with pytest.raises(ValueError,
985-
match="Passing parameters norm and vmin/vmax "
986-
"simultaneously is not supported."):
985+
match="Passing a Normalize instance simultaneously "
986+
"with vmin/vmax is not supported."):
987987
ax.imshow(a, norm=mcolors.Normalize(-10, 10), vmin=0, vmax=5)
988988

989989

@@ -2431,8 +2431,8 @@ def test_scatter_norm_vminvmax(self):
24312431
x = [1, 2, 3]
24322432
ax = plt.axes()
24332433
with pytest.raises(ValueError,
2434-
match="Passing parameters norm and vmin/vmax "
2435-
"simultaneously is not supported."):
2434+
match="Passing a Normalize instance simultaneously "
2435+
"with vmin/vmax is not supported."):
24362436
ax.scatter(x, x, c=x, norm=mcolors.Normalize(-10, 10),
24372437
vmin=0, vmax=5)
24382438

lib/matplotlib/tests/test_image.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,3 +1410,25 @@ def test_large_image(fig_test, fig_ref, dim, size, msg, origin):
14101410
extent=(0, 1, 0, 1),
14111411
interpolation='none',
14121412
origin=origin)
1413+
1414+
1415+
@check_figures_equal(extensions=["png"])
1416+
def test_str_norms(fig_test, fig_ref):
1417+
t = np.random.rand(10, 10) * .8 + .1 # between 0 and 1
1418+
axts = fig_test.subplots(1, 5)
1419+
axts[0].imshow(t, norm="log")
1420+
axts[1].imshow(t, norm="log", vmin=.2)
1421+
axts[2].imshow(t, norm="symlog")
1422+
axts[3].imshow(t, norm="symlog", vmin=.3, vmax=.7)
1423+
axts[4].imshow(t, norm="logit", vmin=.3, vmax=.7)
1424+
axrs = fig_ref.subplots(1, 5)
1425+
axrs[0].imshow(t, norm=colors.LogNorm())
1426+
axrs[1].imshow(t, norm=colors.LogNorm(vmin=.2))
1427+
# same linthresh as SymmetricalLogScale's default.
1428+
axrs[2].imshow(t, norm=colors.SymLogNorm(linthresh=2))
1429+
axrs[3].imshow(t, norm=colors.SymLogNorm(linthresh=2, vmin=.3, vmax=.7))
1430+
axrs[4].imshow(t, norm="logit", clim=(.3, .7))
1431+
1432+
assert type(axts[0].images[0].norm) == colors.LogNorm # Exactly that class
1433+
with pytest.raises(ValueError):
1434+
axts[0].imshow(t, norm="foobar")

0 commit comments

Comments
 (0)