From f5351adc066a7c8be485fd862535878e94f5aad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Fri, 6 Jun 2025 16:42:22 +0200 Subject: [PATCH 1/2] Creation of the Norm Protocol --- lib/matplotlib/colorizer.py | 2 +- lib/matplotlib/colorizer.pyi | 14 ++-- lib/matplotlib/colors.py | 79 ++++++++++++++++++ lib/matplotlib/colors.pyi | 27 +++++- .../test_colors/test_norm_protocol.png | Bin 0 -> 16129 bytes lib/matplotlib/tests/test_colors.py | 52 +++++++++++- 6 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_colors/test_norm_protocol.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 dd5d22130904..5b01f1a01713 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -41,6 +41,7 @@ import base64 from collections.abc import Sequence, Mapping +from typing import Protocol, runtime_checkable import functools import importlib import inspect @@ -2257,6 +2258,84 @@ def _init(self): self._isinit = True +@runtime_checkable +class Norm(Protocol): + + @property + def vmin(self): + """Lower limit of the input data interval; maps to 0.""" + ... + + + @property + def vmax(self): + """Upper limit of the input data interval; maps to 1.""" + ... + + @property + def clip(self): + """ + Determines the behavior for mapping values outside the range ``[vmin, vmax]``. + + See the *clip* parameter in `.Normalize`. + """ + ... + + def _changed(self): + """ + Call this whenever the norm is changed to notify all the + callback listeners to the 'changed' signal. + """ + ... + + + 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)``. + """ + ... + + def inverse(self, value): + """ + Maps the normalized value (i.e., index in the colormap) back to image + data value. + + Parameters + ---------- + value + Normalized value. + """ + ... + + + def autoscale(self, A): + """Set *vmin*, *vmax* to min, max of *A*.""" + ... + + def autoscale_None(self, A): + """If *vmin* or *vmax* are not set, use the min/max of *A* to set them.""" + ... + + def scaled(self): + """Return whether *vmin* and *vmax* are both set.""" + ... + + class Normalize: """ A class which, when called, maps values within the interval diff --git a/lib/matplotlib/colors.pyi b/lib/matplotlib/colors.pyi index eadd759bcaa3..95b964ee5754 100644 --- a/lib/matplotlib/colors.pyi +++ b/lib/matplotlib/colors.pyi @@ -2,7 +2,7 @@ from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence from matplotlib import cbook, scale import re -from typing import Any, Literal, overload +from typing import Any, Literal, overload, Protocol, runtime_checkable from .typing import ColorType import numpy as np @@ -249,6 +249,31 @@ class BivarColormapFromImage(BivarColormap): origin: Sequence[float] = ..., name: str = ... ) -> None: ... +@runtime_checkable +class Norm(Protocol): + callbacks: cbook.CallbackRegistry + @property + def vmin(self) -> float | None: ... + @property + def vmax(self) -> float | None: ... + @property + def clip(self) -> bool: ... + @overload + def __call__(self, value: float, clip: bool | None = ...) -> float: ... + @overload + def __call__(self, value: np.ndarray, clip: bool | None = ...) -> np.ma.MaskedArray: ... + @overload + def __call__(self, value: ArrayLike, clip: bool | None = ...) -> ArrayLike: ... + @overload + def inverse(self, value: float) -> float: ... + @overload + def inverse(self, value: np.ndarray) -> np.ma.MaskedArray: ... + @overload + def inverse(self, value: ArrayLike) -> ArrayLike: ... + def autoscale(self, A: ArrayLike) -> None: ... + def autoscale_None(self, A: ArrayLike) -> None: ... + def scaled(self) -> bool: ... + class Normalize: callbacks: cbook.CallbackRegistry def __init__( diff --git a/lib/matplotlib/tests/baseline_images/test_colors/test_norm_protocol.png b/lib/matplotlib/tests/baseline_images/test_colors/test_norm_protocol.png new file mode 100644 index 0000000000000000000000000000000000000000..b9627bc369fb67edf6642f5c6ee299126c3ba384 GIT binary patch literal 16129 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 m#Xqdm(aEbdoECsDN==M!bm7?pGkNyW9#B1sR literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index df3f65bdb2dc..1a19881e29ec 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -19,7 +19,8 @@ import matplotlib.pyplot as plt import matplotlib.scale as mscale from matplotlib.rcsetup import cycler -from matplotlib.testing.decorators import image_comparison, check_figures_equal +from matplotlib.testing.decorators import (image_comparison, check_figures_equal, + remove_ticks_and_titles) from matplotlib.colors import is_color_like, to_rgba_array, ListedColormap @@ -1829,3 +1830,52 @@ 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_protocol.png']) +def test_norm_protocol(): + class CustomHalfNorm: + def __init__(self): + self.callbacks = mpl.cbook.CallbackRegistry(signals=["changed"]) + + @property + def vmin(self): + return 0 + + @property + def vmax(self): + return 1 + + @property + def clip(self): + return False + + def _changed(self): + self.callbacks.process('changed') + + def __call__(self, value, clip=None): + return value/2 + + def inverse(self, value): + return 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) + remove_ticks_and_titles(fig) From b0c80932851d821f752e3eac6e649388e9a45b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Sat, 7 Jun 2025 20:49:39 +0200 Subject: [PATCH 2/2] updates based on feedback on PR --- lib/matplotlib/colors.py | 2 ++ lib/matplotlib/colors.pyi | 8 ++++---- lib/matplotlib/tests/test_colors.py | 8 ++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 5b01f1a01713..867dce5b1eac 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -2261,6 +2261,8 @@ def _init(self): @runtime_checkable class Norm(Protocol): + callbacks: cbook.CallbackRegistry + @property def vmin(self): """Lower limit of the input data interval; maps to 0.""" diff --git a/lib/matplotlib/colors.pyi b/lib/matplotlib/colors.pyi index 95b964ee5754..756202c5d960 100644 --- a/lib/matplotlib/colors.pyi +++ b/lib/matplotlib/colors.pyi @@ -253,17 +253,17 @@ class BivarColormapFromImage(BivarColormap): class Norm(Protocol): callbacks: cbook.CallbackRegistry @property - def vmin(self) -> float | None: ... + def vmin(self) -> float | tuple[float] | None: ... @property - def vmax(self) -> float | None: ... + def vmax(self) -> float | tuple[float] | None: ... @property - def clip(self) -> bool: ... + def clip(self) -> bool | tuple[bool]: ... @overload def __call__(self, value: float, clip: bool | None = ...) -> float: ... @overload def __call__(self, value: np.ndarray, clip: bool | None = ...) -> np.ma.MaskedArray: ... @overload - def __call__(self, value: ArrayLike, clip: bool | None = ...) -> ArrayLike: ... + def __call__(self, value: ArrayLike, clip: bool | None | list = ...) -> ArrayLike: ... @overload def inverse(self, value: float) -> float: ... @overload diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 1a19881e29ec..e687c414d285 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -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 @@ -19,8 +20,7 @@ import matplotlib.pyplot as plt import matplotlib.scale as mscale from matplotlib.rcsetup import cycler -from matplotlib.testing.decorators import (image_comparison, check_figures_equal, - remove_ticks_and_titles) +from matplotlib.testing.decorators import (image_comparison, check_figures_equal) from matplotlib.colors import is_color_like, to_rgba_array, ListedColormap @@ -1832,7 +1832,8 @@ def test_LinearSegmentedColormap_from_list_value_color_tuple(): ) -@image_comparison(['test_norm_protocol.png']) +@image_comparison(['test_norm_protocol.png'], remove_text=True, + tol=0 if platform.machine() == 'x86_64' else 0.05) def test_norm_protocol(): class CustomHalfNorm: def __init__(self): @@ -1878,4 +1879,3 @@ def scaled(self): axes[0,1].pcolor(r, colorizer=colorizer) axes[1,0].contour(r, colorizer=colorizer) axes[1,1].contourf(r, colorizer=colorizer) - remove_ticks_and_titles(fig)