Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Abstract base class for Normalize
  • Loading branch information
trygvrad committed Jun 15, 2025
commit fc94ae174d2547eb736a77cd3e9bab065a0d5574
2 changes: 1 addition & 1 deletion lib/matplotlib/colorizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
14 changes: 7 additions & 7 deletions lib/matplotlib/colorizer.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -63,18 +63,18 @@ 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: ...


class _ScalarMappable(_ColorizerInterface):
def __init__(
self,
norm: colors.Normalize | None = ...,
norm: colors.Norm | None = ...,
cmap: str | colors.Colormap | None = ...,
*,
colorizer: Colorizer | None = ...,
Expand Down
106 changes: 97 additions & 9 deletions lib/matplotlib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

import base64
from collections.abc import Sequence, Mapping
from abc import ABC, abstractmethod
import functools
import importlib
import inspect
Expand Down Expand Up @@ -2257,7 +2258,101 @@ def _init(self):
self._isinit = True


class Normalize:
class Norm(ABC):

def __init__(self):
"""
Abstract base class for normalizations.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring seems like it should be on the class, not the __init__.


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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
"""
Expand Down
26 changes: 25 additions & 1 deletion lib/matplotlib/colors.pyi
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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: ...
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions lib/matplotlib/tests/test_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from PIL import Image
import pytest
import base64
import platform

from numpy.testing import assert_array_equal, assert_array_almost_equal

Expand Down Expand Up @@ -1829,3 +1830,49 @@ def test_LinearSegmentedColormap_from_list_value_color_tuple():
cmap([value for value, _ in value_color_tuples]),
to_rgba_array([color for _, color in value_color_tuples]),
)


@image_comparison(['test_norm_abc.png'], remove_text=True,
tol=0 if platform.machine() == 'x86_64' else 0.05)
def test_norm_abc():

class CustomHalfNorm(mcolors.Norm):
def __init__(self):
super().__init__()

@property
def vmin(self):
return 0

@property
def vmax(self):
return 1

@property
def clip(self):
return False

def __call__(self, value, clip=None):
return value / 2

def inverse(self, value):
return 2 * value

def autoscale(self, A):
pass

def autoscale_None(self, A):
pass

def scaled(self):
return True

fig, axes = plt.subplots(2,2)

r = np.linspace(-1, 3, 16*16).reshape((16,16))
norm = CustomHalfNorm()
colorizer = mpl.colorizer.Colorizer(cmap='viridis', norm=norm)
c = axes[0,0].imshow(r, colorizer=colorizer)
axes[0,1].pcolor(r, colorizer=colorizer)
axes[1,0].contour(r, colorizer=colorizer)
axes[1,1].contourf(r, colorizer=colorizer)