From fc94ae174d2547eb736a77cd3e9bab065a0d5574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Sun, 15 Jun 2025 19:04:58 +0200 Subject: [PATCH] Abstract base class for Normalize --- lib/matplotlib/colorizer.py | 2 +- lib/matplotlib/colorizer.pyi | 14 +-- lib/matplotlib/colors.py | 106 ++++++++++++++++-- lib/matplotlib/colors.pyi | 26 ++++- .../test_colors/test_norm_abc.png | Bin 0 -> 16129 bytes lib/matplotlib/tests/test_colors.py | 47 ++++++++ 6 files changed, 177 insertions(+), 18 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_colors/test_norm_abc.png diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index b4223f389804..92a6e4ea4c4f 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -90,7 +90,7 @@ def norm(self): @norm.setter def norm(self, norm): - _api.check_isinstance((colors.Normalize, str, None), norm=norm) + _api.check_isinstance((colors.Norm, str, None), norm=norm) if norm is None: norm = colors.Normalize() elif isinstance(norm, str): diff --git a/lib/matplotlib/colorizer.pyi b/lib/matplotlib/colorizer.pyi index f35ebe5295e4..9a5a73415d83 100644 --- a/lib/matplotlib/colorizer.pyi +++ b/lib/matplotlib/colorizer.pyi @@ -10,12 +10,12 @@ class Colorizer: def __init__( self, cmap: str | colors.Colormap | None = ..., - norm: str | colors.Normalize | None = ..., + norm: str | colors.Norm | None = ..., ) -> None: ... @property - def norm(self) -> colors.Normalize: ... + def norm(self) -> colors.Norm: ... @norm.setter - def norm(self, norm: colors.Normalize | str | None) -> None: ... + def norm(self, norm: colors.Norm | str | None) -> None: ... def to_rgba( self, x: np.ndarray, @@ -63,10 +63,10 @@ class _ColorizerInterface: def get_cmap(self) -> colors.Colormap: ... def set_cmap(self, cmap: str | colors.Colormap) -> None: ... @property - def norm(self) -> colors.Normalize: ... + def norm(self) -> colors.Norm: ... @norm.setter - def norm(self, norm: colors.Normalize | str | None) -> None: ... - def set_norm(self, norm: colors.Normalize | str | None) -> None: ... + def norm(self, norm: colors.Norm | str | None) -> None: ... + def set_norm(self, norm: colors.Norm | str | None) -> None: ... def autoscale(self) -> None: ... def autoscale_None(self) -> None: ... @@ -74,7 +74,7 @@ class _ColorizerInterface: class _ScalarMappable(_ColorizerInterface): def __init__( self, - norm: colors.Normalize | None = ..., + norm: colors.Norm | None = ..., cmap: str | colors.Colormap | None = ..., *, colorizer: Colorizer | None = ..., diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 254e2c1a203b..b82e6bbc425c 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -41,6 +41,7 @@ import base64 from collections.abc import Sequence, Mapping +from abc import ABC, abstractmethod import functools import importlib import inspect @@ -2257,7 +2258,101 @@ def _init(self): self._isinit = True -class Normalize: +class Norm(ABC): + + def __init__(self): + """ + Abstract base class for normalizations. + + Subclasses include `colors.Normalize` which maps from a scalar to + a scalar. However, this class makes no such requirement, and subclasses may + support the normalization of multiple variates simultaneously, with + separate normalization for each variate. + """ + self.callbacks = cbook.CallbackRegistry(signals=["changed"]) + + @property + @abstractmethod + def vmin(self): + """Lower limit of the input data interval; maps to 0.""" + pass + + @property + @abstractmethod + def vmax(self): + """Upper limit of the input data interval; maps to 1.""" + pass + + @property + @abstractmethod + def clip(self): + """ + Determines the behavior for mapping values outside the range ``[vmin, vmax]``. + + See the *clip* parameter in `.Normalize`. + """ + pass + + @abstractmethod + def __call__(self, value, clip=None): + """ + Normalize the data and return the normalized data. + + Parameters + ---------- + value + Data to normalize. + clip : bool, optional + See the description of the parameter *clip* in `.Normalize`. + + If ``None``, defaults to ``self.clip`` (which defaults to + ``False``). + + Notes + ----- + If not already initialized, ``self.vmin`` and ``self.vmax`` are + + initialized using ``self.autoscale_None(value)``. + """ + pass + + @abstractmethod + def inverse(self, value): + """ + Maps the normalized value (i.e., index in the colormap) back to image + data value. + + Parameters + ---------- + value + Normalized value. + """ + pass + + @abstractmethod + def autoscale(self, A): + """Set *vmin*, *vmax* to min, max of *A*.""" + pass + + @abstractmethod + def autoscale_None(self, A): + """If *vmin* or *vmax* are not set, use the min/max of *A* to set them.""" + pass + + @abstractmethod + def scaled(self): + """Return whether *vmin* and *vmax* are both set.""" + pass + + def _changed(self): + """ + Call this whenever the norm is changed to notify all the + callback listeners to the 'changed' signal. + """ + self.callbacks.process('changed') + + +class Normalize(Norm): """ A class which, when called, maps values within the interval ``[vmin, vmax]`` linearly to the interval ``[0.0, 1.0]``. The mapping of @@ -2307,11 +2402,11 @@ def __init__(self, vmin=None, vmax=None, clip=False): ----- If ``vmin == vmax``, input data will be mapped to 0. """ + super().__init__() self._vmin = _sanitize_extrema(vmin) self._vmax = _sanitize_extrema(vmax) self._clip = clip self._scale = None - self.callbacks = cbook.CallbackRegistry(signals=["changed"]) @property def vmin(self): @@ -2352,13 +2447,6 @@ def clip(self, value): self._clip = value self._changed() - def _changed(self): - """ - Call this whenever the norm is changed to notify all the - callback listeners to the 'changed' signal. - """ - self.callbacks.process('changed') - @staticmethod def process_value(value): """ diff --git a/lib/matplotlib/colors.pyi b/lib/matplotlib/colors.pyi index eadd759bcaa3..02864408d4d0 100644 --- a/lib/matplotlib/colors.pyi +++ b/lib/matplotlib/colors.pyi @@ -1,4 +1,5 @@ from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence +from abc import ABC, abstractmethod from matplotlib import cbook, scale import re @@ -249,8 +250,31 @@ class BivarColormapFromImage(BivarColormap): origin: Sequence[float] = ..., name: str = ... ) -> None: ... -class Normalize: +class Norm(ABC): callbacks: cbook.CallbackRegistry + def __init__(self) -> None: ... + @property + @abstractmethod + def vmin(self) -> float | tuple[float] | None: ... + @property + @abstractmethod + def vmax(self) -> float | tuple[float] | None: ... + @property + @abstractmethod + def clip(self) -> bool | tuple[bool]: ... + @abstractmethod + def __call__(self, value: np.ndarray, clip: bool | None = ...) -> ArrayLike: ... + @abstractmethod + def inverse(self, value: ArrayLike) -> ArrayLike: ... + @abstractmethod + def autoscale(self, A: ArrayLike) -> None: ... + @abstractmethod + def autoscale_None(self, A: ArrayLike) -> None: ... + @abstractmethod + def scaled(self) -> bool: ... + + +class Normalize(Norm): def __init__( self, vmin: float | None = ..., vmax: float | None = ..., clip: bool = ... ) -> None: ... diff --git a/lib/matplotlib/tests/baseline_images/test_colors/test_norm_abc.png b/lib/matplotlib/tests/baseline_images/test_colors/test_norm_abc.png new file mode 100644 index 0000000000000000000000000000000000000000..077365674ac27803460ddb533d5efa09ea7ef9c7 GIT binary patch literal 16129 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 m@y{*)?+fJr!spj}