Skip to content

Commit 7edd74a

Browse files
authored
Abstract base class for Normalize (#30178)
* Abstract base class for Normalize * Include the Norm ABC in the docs * Changed name of temporary class Norm to ScaleNorm in _make_norm_from_scale() * removal of inverse() in Norm ABC
1 parent 75c9b0a commit 7edd74a

File tree

7 files changed

+177
-54
lines changed

7 files changed

+177
-54
lines changed

doc/api/colors_api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Color norms
2121
:toctree: _as_gen/
2222
:template: autosummary.rst
2323

24+
Norm
2425
Normalize
2526
NoNorm
2627
AsinhNorm

lib/matplotlib/colorizer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def norm(self):
9090

9191
@norm.setter
9292
def norm(self, norm):
93-
_api.check_isinstance((colors.Normalize, str, None), norm=norm)
93+
_api.check_isinstance((colors.Norm, str, None), norm=norm)
9494
if norm is None:
9595
norm = colors.Normalize()
9696
elif isinstance(norm, str):

lib/matplotlib/colorizer.pyi

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ class Colorizer:
1010
def __init__(
1111
self,
1212
cmap: str | colors.Colormap | None = ...,
13-
norm: str | colors.Normalize | None = ...,
13+
norm: str | colors.Norm | None = ...,
1414
) -> None: ...
1515
@property
16-
def norm(self) -> colors.Normalize: ...
16+
def norm(self) -> colors.Norm: ...
1717
@norm.setter
18-
def norm(self, norm: colors.Normalize | str | None) -> None: ...
18+
def norm(self, norm: colors.Norm | str | None) -> None: ...
1919
def to_rgba(
2020
self,
2121
x: np.ndarray,
@@ -63,18 +63,18 @@ class _ColorizerInterface:
6363
def get_cmap(self) -> colors.Colormap: ...
6464
def set_cmap(self, cmap: str | colors.Colormap) -> None: ...
6565
@property
66-
def norm(self) -> colors.Normalize: ...
66+
def norm(self) -> colors.Norm: ...
6767
@norm.setter
68-
def norm(self, norm: colors.Normalize | str | None) -> None: ...
69-
def set_norm(self, norm: colors.Normalize | str | None) -> None: ...
68+
def norm(self, norm: colors.Norm | str | None) -> None: ...
69+
def set_norm(self, norm: colors.Norm | str | None) -> None: ...
7070
def autoscale(self) -> None: ...
7171
def autoscale_None(self) -> None: ...
7272

7373

7474
class _ScalarMappable(_ColorizerInterface):
7575
def __init__(
7676
self,
77-
norm: colors.Normalize | None = ...,
77+
norm: colors.Norm | None = ...,
7878
cmap: str | colors.Colormap | None = ...,
7979
*,
8080
colorizer: Colorizer | None = ...,

lib/matplotlib/colors.py

Lines changed: 98 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
import base64
4343
from collections.abc import Sequence, Mapping
44+
from abc import ABC, abstractmethod
4445
import functools
4546
import importlib
4647
import inspect
@@ -2257,7 +2258,87 @@ def _init(self):
22572258
self._isinit = True
22582259

22592260

2260-
class Normalize:
2261+
class Norm(ABC):
2262+
"""
2263+
Abstract base class for normalizations.
2264+
2265+
Subclasses include `Normalize` which maps from a scalar to
2266+
a scalar. However, this class makes no such requirement, and subclasses may
2267+
support the normalization of multiple variates simultaneously, with
2268+
separate normalization for each variate.
2269+
"""
2270+
2271+
def __init__(self):
2272+
self.callbacks = cbook.CallbackRegistry(signals=["changed"])
2273+
2274+
@property
2275+
@abstractmethod
2276+
def vmin(self):
2277+
"""Lower limit of the input data interval; maps to 0."""
2278+
pass
2279+
2280+
@property
2281+
@abstractmethod
2282+
def vmax(self):
2283+
"""Upper limit of the input data interval; maps to 1."""
2284+
pass
2285+
2286+
@property
2287+
@abstractmethod
2288+
def clip(self):
2289+
"""
2290+
Determines the behavior for mapping values outside the range ``[vmin, vmax]``.
2291+
2292+
See the *clip* parameter in `.Normalize`.
2293+
"""
2294+
pass
2295+
2296+
@abstractmethod
2297+
def __call__(self, value, clip=None):
2298+
"""
2299+
Normalize the data and return the normalized data.
2300+
2301+
Parameters
2302+
----------
2303+
value
2304+
Data to normalize.
2305+
clip : bool, optional
2306+
See the description of the parameter *clip* in `.Normalize`.
2307+
2308+
If ``None``, defaults to ``self.clip`` (which defaults to
2309+
``False``).
2310+
2311+
Notes
2312+
-----
2313+
If not already initialized, ``self.vmin`` and ``self.vmax`` are
2314+
initialized using ``self.autoscale_None(value)``.
2315+
"""
2316+
pass
2317+
2318+
@abstractmethod
2319+
def autoscale(self, A):
2320+
"""Set *vmin*, *vmax* to min, max of *A*."""
2321+
pass
2322+
2323+
@abstractmethod
2324+
def autoscale_None(self, A):
2325+
"""If *vmin* or *vmax* are not set, use the min/max of *A* to set them."""
2326+
pass
2327+
2328+
@abstractmethod
2329+
def scaled(self):
2330+
"""Return whether *vmin* and *vmax* are both set."""
2331+
pass
2332+
2333+
def _changed(self):
2334+
"""
2335+
Call this whenever the norm is changed to notify all the
2336+
callback listeners to the 'changed' signal.
2337+
"""
2338+
self.callbacks.process('changed')
2339+
2340+
2341+
class Normalize(Norm):
22612342
"""
22622343
A class which, when called, maps values within the interval
22632344
``[vmin, vmax]`` linearly to the interval ``[0.0, 1.0]``. The mapping of
@@ -2307,15 +2388,15 @@ def __init__(self, vmin=None, vmax=None, clip=False):
23072388
-----
23082389
If ``vmin == vmax``, input data will be mapped to 0.
23092390
"""
2391+
super().__init__()
23102392
self._vmin = _sanitize_extrema(vmin)
23112393
self._vmax = _sanitize_extrema(vmax)
23122394
self._clip = clip
23132395
self._scale = None
2314-
self.callbacks = cbook.CallbackRegistry(signals=["changed"])
23152396

23162397
@property
23172398
def vmin(self):
2318-
"""Lower limit of the input data interval; maps to 0."""
2399+
# docstring inherited
23192400
return self._vmin
23202401

23212402
@vmin.setter
@@ -2327,7 +2408,7 @@ def vmin(self, value):
23272408

23282409
@property
23292410
def vmax(self):
2330-
"""Upper limit of the input data interval; maps to 1."""
2411+
# docstring inherited
23312412
return self._vmax
23322413

23332414
@vmax.setter
@@ -2339,11 +2420,7 @@ def vmax(self, value):
23392420

23402421
@property
23412422
def clip(self):
2342-
"""
2343-
Determines the behavior for mapping values outside the range ``[vmin, vmax]``.
2344-
2345-
See the *clip* parameter in `.Normalize`.
2346-
"""
2423+
# docstring inherited
23472424
return self._clip
23482425

23492426
@clip.setter
@@ -2352,13 +2429,6 @@ def clip(self, value):
23522429
self._clip = value
23532430
self._changed()
23542431

2355-
def _changed(self):
2356-
"""
2357-
Call this whenever the norm is changed to notify all the
2358-
callback listeners to the 'changed' signal.
2359-
"""
2360-
self.callbacks.process('changed')
2361-
23622432
@staticmethod
23632433
def process_value(value):
23642434
"""
@@ -2400,24 +2470,7 @@ def process_value(value):
24002470
return result, is_scalar
24012471

24022472
def __call__(self, value, clip=None):
2403-
"""
2404-
Normalize the data and return the normalized data.
2405-
2406-
Parameters
2407-
----------
2408-
value
2409-
Data to normalize.
2410-
clip : bool, optional
2411-
See the description of the parameter *clip* in `.Normalize`.
2412-
2413-
If ``None``, defaults to ``self.clip`` (which defaults to
2414-
``False``).
2415-
2416-
Notes
2417-
-----
2418-
If not already initialized, ``self.vmin`` and ``self.vmax`` are
2419-
initialized using ``self.autoscale_None(value)``.
2420-
"""
2473+
# docstring inherited
24212474
if clip is None:
24222475
clip = self.clip
24232476

@@ -2468,7 +2521,7 @@ def inverse(self, value):
24682521
return vmin + value * (vmax - vmin)
24692522

24702523
def autoscale(self, A):
2471-
"""Set *vmin*, *vmax* to min, max of *A*."""
2524+
# docstring inherited
24722525
with self.callbacks.blocked():
24732526
# Pause callbacks while we are updating so we only get
24742527
# a single update signal at the end
@@ -2477,7 +2530,7 @@ def autoscale(self, A):
24772530
self._changed()
24782531

24792532
def autoscale_None(self, A):
2480-
"""If *vmin* or *vmax* are not set, use the min/max of *A* to set them."""
2533+
# docstring inherited
24812534
A = np.asanyarray(A)
24822535

24832536
if isinstance(A, np.ma.MaskedArray):
@@ -2491,7 +2544,7 @@ def autoscale_None(self, A):
24912544
self.vmax = A.max()
24922545

24932546
def scaled(self):
2494-
"""Return whether *vmin* and *vmax* are both set."""
2547+
# docstring inherited
24952548
return self.vmin is not None and self.vmax is not None
24962549

24972550

@@ -2775,7 +2828,7 @@ def _make_norm_from_scale(
27752828
unlike to arbitrary lambdas.
27762829
"""
27772830

2778-
class Norm(base_norm_cls):
2831+
class ScaleNorm(base_norm_cls):
27792832
def __reduce__(self):
27802833
cls = type(self)
27812834
# If the class is toplevel-accessible, it is possible to directly
@@ -2855,15 +2908,15 @@ def autoscale_None(self, A):
28552908
return super().autoscale_None(in_trf_domain)
28562909

28572910
if base_norm_cls is Normalize:
2858-
Norm.__name__ = f"{scale_cls.__name__}Norm"
2859-
Norm.__qualname__ = f"{scale_cls.__qualname__}Norm"
2911+
ScaleNorm.__name__ = f"{scale_cls.__name__}Norm"
2912+
ScaleNorm.__qualname__ = f"{scale_cls.__qualname__}Norm"
28602913
else:
2861-
Norm.__name__ = base_norm_cls.__name__
2862-
Norm.__qualname__ = base_norm_cls.__qualname__
2863-
Norm.__module__ = base_norm_cls.__module__
2864-
Norm.__doc__ = base_norm_cls.__doc__
2914+
ScaleNorm.__name__ = base_norm_cls.__name__
2915+
ScaleNorm.__qualname__ = base_norm_cls.__qualname__
2916+
ScaleNorm.__module__ = base_norm_cls.__module__
2917+
ScaleNorm.__doc__ = base_norm_cls.__doc__
28652918

2866-
return Norm
2919+
return ScaleNorm
28672920

28682921

28692922
def _create_empty_object_of_class(cls):

lib/matplotlib/colors.pyi

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
2+
from abc import ABC, abstractmethod
23
from matplotlib import cbook, scale
34
import re
45

@@ -249,8 +250,29 @@ class BivarColormapFromImage(BivarColormap):
249250
origin: Sequence[float] = ..., name: str = ...
250251
) -> None: ...
251252

252-
class Normalize:
253+
class Norm(ABC):
253254
callbacks: cbook.CallbackRegistry
255+
def __init__(self) -> None: ...
256+
@property
257+
@abstractmethod
258+
def vmin(self) -> float | tuple[float] | None: ...
259+
@property
260+
@abstractmethod
261+
def vmax(self) -> float | tuple[float] | None: ...
262+
@property
263+
@abstractmethod
264+
def clip(self) -> bool | tuple[bool]: ...
265+
@abstractmethod
266+
def __call__(self, value: np.ndarray, clip: bool | None = ...) -> ArrayLike: ...
267+
@abstractmethod
268+
def autoscale(self, A: ArrayLike) -> None: ...
269+
@abstractmethod
270+
def autoscale_None(self, A: ArrayLike) -> None: ...
271+
@abstractmethod
272+
def scaled(self) -> bool: ...
273+
274+
275+
class Normalize(Norm):
254276
def __init__(
255277
self, vmin: float | None = ..., vmax: float | None = ..., clip: bool = ...
256278
) -> None: ...
Loading

lib/matplotlib/tests/test_colors.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from PIL import Image
88
import pytest
99
import base64
10+
import platform
1011

1112
from numpy.testing import assert_array_equal, assert_array_almost_equal
1213

@@ -1829,3 +1830,49 @@ def test_LinearSegmentedColormap_from_list_value_color_tuple():
18291830
cmap([value for value, _ in value_color_tuples]),
18301831
to_rgba_array([color for _, color in value_color_tuples]),
18311832
)
1833+
1834+
1835+
@image_comparison(['test_norm_abc.png'], remove_text=True,
1836+
tol=0 if platform.machine() == 'x86_64' else 0.05)
1837+
def test_norm_abc():
1838+
1839+
class CustomHalfNorm(mcolors.Norm):
1840+
def __init__(self):
1841+
super().__init__()
1842+
1843+
@property
1844+
def vmin(self):
1845+
return 0
1846+
1847+
@property
1848+
def vmax(self):
1849+
return 1
1850+
1851+
@property
1852+
def clip(self):
1853+
return False
1854+
1855+
def __call__(self, value, clip=None):
1856+
return value / 2
1857+
1858+
def inverse(self, value):
1859+
return 2 * value
1860+
1861+
def autoscale(self, A):
1862+
pass
1863+
1864+
def autoscale_None(self, A):
1865+
pass
1866+
1867+
def scaled(self):
1868+
return True
1869+
1870+
fig, axes = plt.subplots(2,2)
1871+
1872+
r = np.linspace(-1, 3, 16*16).reshape((16,16))
1873+
norm = CustomHalfNorm()
1874+
colorizer = mpl.colorizer.Colorizer(cmap='viridis', norm=norm)
1875+
c = axes[0,0].imshow(r, colorizer=colorizer)
1876+
axes[0,1].pcolor(r, colorizer=colorizer)
1877+
axes[1,0].contour(r, colorizer=colorizer)
1878+
axes[1,1].contourf(r, colorizer=colorizer)

0 commit comments

Comments
 (0)