GeezerVison is showing github.com — Abstract base class for Normalize (#30178) · matplotlib/matplotlib@7edd74a
File treeExpand file treeCollapse file tree Open diff view settings baseline_images/test_colors
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/colors_api.rst +1Lines changed: 1 addition & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -21,6 +21,7 @@ Color norms | 21 | 21 | :toctree: _as_gen/ | 22 | 22 | :template: autosummary.rst | 23 | 23 |
| | 24 | + Norm | 24 | 25 | Normalize | 25 | 26 | NoNorm | 26 | 27 | AsinhNorm | |
Collapse file Copy file name to clipboardExpand all lines: lib/matplotlib/colorizer.py +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -90,7 +90,7 @@ def norm(self): | 90 | 90 |
| 91 | 91 | @norm.setter | 92 | 92 | def norm(self, norm): | 93 | | - _api.check_isinstance((colors.Normalize, str, None), norm=norm) | | 93 | + _api.check_isinstance((colors.Norm, str, None), norm=norm) | 94 | 94 | if norm is None: | 95 | 95 | norm = colors.Normalize() | 96 | 96 | elif isinstance(norm, str): | |
Collapse file Copy file name to clipboardExpand all lines: lib/matplotlib/colorizer.pyi +7-7Lines changed: 7 additions & 7 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -10,12 +10,12 @@ class Colorizer: | 10 | 10 | def __init__( | 11 | 11 | self, | 12 | 12 | cmap: str | colors.Colormap | None = ..., | 13 | | - norm: str | colors.Normalize | None = ..., | | 13 | + norm: str | colors.Norm | None = ..., | 14 | 14 | ) -> None: ... | 15 | 15 | @property | 16 | | - def norm(self) -> colors.Normalize: ... | | 16 | + def norm(self) -> colors.Norm: ... | 17 | 17 | @norm.setter | 18 | | - def norm(self, norm: colors.Normalize | str | None) -> None: ... | | 18 | + def norm(self, norm: colors.Norm | str | None) -> None: ... | 19 | 19 | def to_rgba( | 20 | 20 | self, | 21 | 21 | x: np.ndarray, | @@ -63,18 +63,18 @@ class _ColorizerInterface: | 63 | 63 | def get_cmap(self) -> colors.Colormap: ... | 64 | 64 | def set_cmap(self, cmap: str | colors.Colormap) -> None: ... | 65 | 65 | @property | 66 | | - def norm(self) -> colors.Normalize: ... | | 66 | + def norm(self) -> colors.Norm: ... | 67 | 67 | @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: ... | 70 | 70 | def autoscale(self) -> None: ... | 71 | 71 | def autoscale_None(self) -> None: ... | 72 | 72 |
| 73 | 73 |
| 74 | 74 | class _ScalarMappable(_ColorizerInterface): | 75 | 75 | def __init__( | 76 | 76 | self, | 77 | | - norm: colors.Normalize | None = ..., | | 77 | + norm: colors.Norm | None = ..., | 78 | 78 | cmap: str | colors.Colormap | None = ..., | 79 | 79 | *, | 80 | 80 | colorizer: Colorizer | None = ..., | |
Collapse file Copy file name to clipboardExpand all lines: lib/matplotlib/colors.py +98-45Lines changed: 98 additions & 45 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 41 | 41 |
| 42 | 42 | import base64 | 43 | 43 | from collections.abc import Sequence, Mapping | | 44 | +from abc import ABC, abstractmethod | 44 | 45 | import functools | 45 | 46 | import importlib | 46 | 47 | import inspect | @@ -2257,7 +2258,87 @@ def _init(self): | 2257 | 2258 | self._isinit = True | 2258 | 2259 |
| 2259 | 2260 |
| 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): | 2261 | 2342 | """ | 2262 | 2343 | A class which, when called, maps values within the interval | 2263 | 2344 | ``[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): | 2307 | 2388 | ----- | 2308 | 2389 | If ``vmin == vmax``, input data will be mapped to 0. | 2309 | 2390 | """ | | 2391 | + super().__init__() | 2310 | 2392 | self._vmin = _sanitize_extrema(vmin) | 2311 | 2393 | self._vmax = _sanitize_extrema(vmax) | 2312 | 2394 | self._clip = clip | 2313 | 2395 | self._scale = None | 2314 | | - self.callbacks = cbook.CallbackRegistry(signals=["changed"]) | 2315 | 2396 |
| 2316 | 2397 | @property | 2317 | 2398 | def vmin(self): | 2318 | | - """Lower limit of the input data interval; maps to 0.""" | | 2399 | + # docstring inherited | 2319 | 2400 | return self._vmin | 2320 | 2401 |
| 2321 | 2402 | @vmin.setter | @@ -2327,7 +2408,7 @@ def vmin(self, value): | 2327 | 2408 |
| 2328 | 2409 | @property | 2329 | 2410 | def vmax(self): | 2330 | | - """Upper limit of the input data interval; maps to 1.""" | | 2411 | + # docstring inherited | 2331 | 2412 | return self._vmax | 2332 | 2413 |
| 2333 | 2414 | @vmax.setter | @@ -2339,11 +2420,7 @@ def vmax(self, value): | 2339 | 2420 |
| 2340 | 2421 | @property | 2341 | 2422 | 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 | 2347 | 2424 | return self._clip | 2348 | 2425 |
| 2349 | 2426 | @clip.setter | @@ -2352,13 +2429,6 @@ def clip(self, value): | 2352 | 2429 | self._clip = value | 2353 | 2430 | self._changed() | 2354 | 2431 |
| 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 | | - | 2362 | 2432 | @staticmethod | 2363 | 2433 | def process_value(value): | 2364 | 2434 | """ | @@ -2400,24 +2470,7 @@ def process_value(value): | 2400 | 2470 | return result, is_scalar | 2401 | 2471 |
| 2402 | 2472 | 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 | 2421 | 2474 | if clip is None: | 2422 | 2475 | clip = self.clip | 2423 | 2476 |
| @@ -2468,7 +2521,7 @@ def inverse(self, value): | 2468 | 2521 | return vmin + value * (vmax - vmin) | 2469 | 2522 |
| 2470 | 2523 | def autoscale(self, A): | 2471 | | - """Set *vmin*, *vmax* to min, max of *A*.""" | | 2524 | + # docstring inherited | 2472 | 2525 | with self.callbacks.blocked(): | 2473 | 2526 | # Pause callbacks while we are updating so we only get | 2474 | 2527 | # a single update signal at the end | @@ -2477,7 +2530,7 @@ def autoscale(self, A): | 2477 | 2530 | self._changed() | 2478 | 2531 |
| 2479 | 2532 | 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 | 2481 | 2534 | A = np.asanyarray(A) | 2482 | 2535 |
| 2483 | 2536 | if isinstance(A, np.ma.MaskedArray): | @@ -2491,7 +2544,7 @@ def autoscale_None(self, A): | 2491 | 2544 | self.vmax = A.max() | 2492 | 2545 |
| 2493 | 2546 | def scaled(self): | 2494 | | - """Return whether *vmin* and *vmax* are both set.""" | | 2547 | + # docstring inherited | 2495 | 2548 | return self.vmin is not None and self.vmax is not None | 2496 | 2549 |
| 2497 | 2550 |
| @@ -2775,7 +2828,7 @@ def _make_norm_from_scale( | 2775 | 2828 | unlike to arbitrary lambdas. | 2776 | 2829 | """ | 2777 | 2830 |
| 2778 | | - class Norm(base_norm_cls): | | 2831 | + class ScaleNorm(base_norm_cls): | 2779 | 2832 | def __reduce__(self): | 2780 | 2833 | cls = type(self) | 2781 | 2834 | # If the class is toplevel-accessible, it is possible to directly | @@ -2855,15 +2908,15 @@ def autoscale_None(self, A): | 2855 | 2908 | return super().autoscale_None(in_trf_domain) | 2856 | 2909 |
| 2857 | 2910 | 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" | 2860 | 2913 | 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__ | 2865 | 2918 |
| 2866 | | - return Norm | | 2919 | + return ScaleNorm | 2867 | 2920 |
| 2868 | 2921 |
| 2869 | 2922 | def _create_empty_object_of_class(cls): | |
Collapse file Copy file name to clipboardExpand all lines: lib/matplotlib/colors.pyi +23-1Lines changed: 23 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence | | 2 | +from abc import ABC, abstractmethod | 2 | 3 | from matplotlib import cbook, scale | 3 | 4 | import re | 4 | 5 |
| @@ -249,8 +250,29 @@ class BivarColormapFromImage(BivarColormap): | 249 | 250 | origin: Sequence[float] = ..., name: str = ... | 250 | 251 | ) -> None: ... | 251 | 252 |
| 252 | | -class Normalize: | | 253 | +class Norm(ABC): | 253 | 254 | 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): | 254 | 276 | def __init__( | 255 | 277 | self, vmin: float | None = ..., vmax: float | None = ..., clip: bool = ... | 256 | 278 | ) -> None: ... | |
Collapse file Copy file name to clipboard 15.8 KB
- Display the source diff
- Display the rich diff
Collapse file Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_colors.py +47Lines changed: 47 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 7 | 7 | from PIL import Image | 8 | 8 | import pytest | 9 | 9 | import base64 | | 10 | +import platform | 10 | 11 |
| 11 | 12 | from numpy.testing import assert_array_equal, assert_array_almost_equal | 12 | 13 |
| @@ -1829,3 +1830,49 @@ def test_LinearSegmentedColormap_from_list_value_color_tuple(): | 1829 | 1830 | cmap([value for value, _ in value_color_tuples]), | 1830 | 1831 | to_rgba_array([color for _, color in value_color_tuples]), | 1831 | 1832 | ) | | 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) |
|