From 2f64f07ee200fe601a1ad70e392900c69a55dc82 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 16 Jul 2026 16:00:49 +1000 Subject: [PATCH 1/3] Fix ListedColormap N deprecation --- ultraplot/colors.py | 23 ++++++++++++++++++++++- ultraplot/tests/test_colors.py | 27 ++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/ultraplot/colors.py b/ultraplot/colors.py index 6d1c18cc4..bab2f5c7a 100644 --- a/ultraplot/colors.py +++ b/ultraplot/colors.py @@ -17,6 +17,7 @@ # colors or truncate colors. So we translate the relevant ListedColormaps to # LinearSegmentedColormaps for consistency. See :rc:`cmap.listedthresh` import functools +import itertools import json import os import re @@ -1715,6 +1716,20 @@ def __repr__(self): string += "})" return string + @property + def monochrome(self): + """Whether every color is identical, normalized to a Python boolean.""" + if hasattr(self, "_monochrome"): + return self._monochrome + try: + return bool(super().monochrome) + except AttributeError: + return False + + @monochrome.setter + def monochrome(self, value): + self._monochrome = bool(value) + def __init__(self, colors, name=None, N=None, alpha=None, **kwargs): """ Parameters @@ -1748,7 +1763,13 @@ def __init__(self, colors, name=None, N=None, alpha=None, **kwargs): # identical monochromatic ListedColormaps when it receives scalar colors. N = _not_none(N, len(colors)) name = _not_none(name, DEFAULT_NAME) - super().__init__(colors, name=name, N=N, **kwargs) + if isinstance(colors, str): + colors = [colors] * N + elif np.iterable(colors): + colors = list(itertools.islice(itertools.cycle(colors), N)) + else: + colors = [colors] * N + super().__init__(colors, name=name, **kwargs) if alpha is not None: self.set_alpha(alpha) for i, color in enumerate(self.colors): diff --git a/ultraplot/tests/test_colors.py b/ultraplot/tests/test_colors.py index 407edbd0c..21a9228f7 100644 --- a/ultraplot/tests/test_colors.py +++ b/ultraplot/tests/test_colors.py @@ -1,12 +1,37 @@ import os +import warnings + +import matplotlib as mpl +import matplotlib.colors as mcolors import pytest import numpy as np -import matplotlib.colors as mcolors from ultraplot import colors as pcolors from ultraplot import config +@pytest.mark.parametrize( + ("N", "expected"), + ( + (None, ["#ff0000", "#0000ff"]), + (1, ["#ff0000"]), + (3, ["#ff0000", "#0000ff", "#ff0000"]), + ), +) +def test_discrete_colormap_resizes_without_listed_n_warning(N, expected): + """DiscreteColormap preserves N semantics without deprecated mpl input.""" + with warnings.catch_warnings(): + warnings.filterwarnings( + "error", + message=r"Passing 'N' to ListedColormap.*", + category=mpl.MatplotlibDeprecationWarning, + ) + cmap = pcolors.DiscreteColormap(["red", "blue"], N=N) + + assert cmap.N == len(expected) + assert [mcolors.to_hex(color) for color in cmap.colors] == expected + + @pytest.fixture(autouse=True) def setup_teardown(): """ From 31c016981273074491492a2568d7ab611cdee1b0 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 16 Jul 2026 16:48:05 +1000 Subject: [PATCH 2/3] Add more tests --- ultraplot/tests/test_colors.py | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/ultraplot/tests/test_colors.py b/ultraplot/tests/test_colors.py index 21a9228f7..d392e4e3e 100644 --- a/ultraplot/tests/test_colors.py +++ b/ultraplot/tests/test_colors.py @@ -14,6 +14,7 @@ ("N", "expected"), ( (None, ["#ff0000", "#0000ff"]), + (0, []), (1, ["#ff0000"]), (3, ["#ff0000", "#0000ff", "#ff0000"]), ), @@ -32,6 +33,53 @@ def test_discrete_colormap_resizes_without_listed_n_warning(N, expected): assert [mcolors.to_hex(color) for color in cmap.colors] == expected +@pytest.mark.parametrize( + ("N", "expected_size"), + ( + (None, 3), + (1, 1), + (4, 4), + ), +) +def test_discrete_colormap_expands_scalar_color(N, expected_size): + """Scalar color strings produce the requested monochromatic colormap.""" + cmap = pcolors.DiscreteColormap("red", N=N) + + assert cmap.N == expected_size + assert [mcolors.to_hex(color) for color in cmap.colors] == [ + "#ff0000" + ] * expected_size + assert cmap.monochrome is True + + +def test_discrete_colormap_resizing_preserves_alpha_override(): + """Alpha replacement is applied to every color after cycling the input.""" + cmap = pcolors.DiscreteColormap( + [(1, 0, 0, 0.2), (0, 0, 1, 0.8)], N=3, alpha=0.5 + ) + + assert [mcolors.to_hex(color, keep_alpha=True) for color in cmap.colors] == [ + "#ff000080", + "#0000ff80", + "#ff000080", + ] + assert cmap.monochrome is False + + +@pytest.mark.parametrize( + ("colors", "expected"), + ( + (["red", "red"], True), + (["red", "blue"], False), + ), +) +def test_discrete_colormap_monochrome_is_python_bool(colors, expected): + """Monochrome detection remains available across matplotlib versions.""" + cmap = pcolors.DiscreteColormap(colors) + + assert cmap.monochrome is expected + + @pytest.fixture(autouse=True) def setup_teardown(): """ From 3a9c1f89f584cd3d5534bc22dd504c32562e356a Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 16 Jul 2026 16:50:34 +1000 Subject: [PATCH 3/3] black --- ultraplot/tests/test_colors.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ultraplot/tests/test_colors.py b/ultraplot/tests/test_colors.py index d392e4e3e..b1e78e009 100644 --- a/ultraplot/tests/test_colors.py +++ b/ultraplot/tests/test_colors.py @@ -54,9 +54,7 @@ def test_discrete_colormap_expands_scalar_color(N, expected_size): def test_discrete_colormap_resizing_preserves_alpha_override(): """Alpha replacement is applied to every color after cycling the input.""" - cmap = pcolors.DiscreteColormap( - [(1, 0, 0, 0.2), (0, 0, 1, 0.8)], N=3, alpha=0.5 - ) + cmap = pcolors.DiscreteColormap([(1, 0, 0, 0.2), (0, 0, 1, 0.8)], N=3, alpha=0.5) assert [mcolors.to_hex(color, keep_alpha=True) for color in cmap.colors] == [ "#ff000080",