From d5d1c7dd443d7fe64d53478553efe0e0aa4235cb Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 16 Apr 2026 14:19:23 +1000 Subject: [PATCH 1/5] Implement fix for 689 --- ultraplot/axes/plot.py | 5 ++++- ultraplot/tests/test_colorbar.py | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index ae387927f..c35ddf587 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -4122,7 +4122,10 @@ def _parse_cmap( norm_kw = norm_kw or {} # If norm is given we use it to set vmin and vmax if (vmin is not None or vmax is not None) and norm is not None: - raise ValueError("If 'norm' is given, 'vmin' and 'vmax' must not be set.") + if not isinstance(norm, str): + raise ValueError( + "If 'norm' is given, 'vmin' and 'vmax' must not be set." + ) if isinstance(norm, mcolors.Normalize): vmin = norm.vmin vmax = norm.vmax diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 683388910..519b4a5ab 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -982,3 +982,27 @@ def test_colorbar_span_position_matches_target_rows(): ), f"Panel y0={panel_pos.y0:.3f} != row1 y0={row1_pos.y0:.3f}" # Sanity: panel must be taller than a single row assert panel_pos.height > row0_pos.height * 1.5 + + +def test_colorbar_norm_str_with_limits(): + """ " + Should allow to pass vmin or vmax when we are passing a str formatter + """ + data = np.random.rand(10, 10) + fig, ax = uplt.subplots() + ax.pcolormesh(data, vmin=0.1, norm="log", vmax=1) + return fig + + +def test_colorbar_norm_with_limits(): + """ " + Should allow to pass vmin or vmax when we are passing a str formatter + """ + data = np.random.rand(10, 10) + fig = None + with pytest.raises(ValueError): + fig, ax = uplt.subplots() + ax.pcolormesh( + data, vmin=0, norm=uplt.colors.mcolors.Normalize(vmin=0, vmax=1), vmax=1 + ) + return fig From d82711fc0b853105feec75014304ce688ee537b6 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 16 Apr 2026 14:21:33 +1000 Subject: [PATCH 2/5] Clean up logic --- ultraplot/axes/plot.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index c35ddf587..a71a641d3 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -4121,11 +4121,10 @@ def _parse_cmap( cmap_kw = cmap_kw or {} norm_kw = norm_kw or {} # If norm is given we use it to set vmin and vmax - if (vmin is not None or vmax is not None) and norm is not None: - if not isinstance(norm, str): - raise ValueError( - "If 'norm' is given, 'vmin' and 'vmax' must not be set." - ) + if (vmin is not None or vmax is not None) and isinstance( + norm, mcolors.Normalize + ): + raise ValueError("If 'norm' is given, 'vmin' and 'vmax' must not be set.") if isinstance(norm, mcolors.Normalize): vmin = norm.vmin vmax = norm.vmax From 6d778f23ce6d3ec6ac91a00ce2c13650e98f2a59 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 16 Apr 2026 14:25:31 +1000 Subject: [PATCH 3/5] Update tests --- ultraplot/tests/test_colorbar.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 5e0078177..caa252359 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -1234,7 +1234,9 @@ def test_colorbar_norm_str_with_limits(): """ data = np.random.rand(10, 10) fig, ax = uplt.subplots() - ax.pcolormesh(data, vmin=0.1, norm="log", vmax=1) + cm = ax.pcolormesh(data, vmin=0.1, norm="linear", vmax=1) + assert cm.norm.vmin == pytest.approx(0.1) + assert cm.norm.vmax == pytest.approx(1) return fig From c87f72db5553dc35f499c38d357c74f7b938a586 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 16 Apr 2026 16:47:48 +1000 Subject: [PATCH 4/5] Allow tuple and list parsing --- ultraplot/axes/plot.py | 23 +++++++++++++++++++++-- ultraplot/tests/test_colorbar.py | 28 +++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index a71a641d3..39e55a896 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -4120,11 +4120,30 @@ def _parse_cmap( # Parse keyword args cmap_kw = cmap_kw or {} norm_kw = norm_kw or {} - # If norm is given we use it to set vmin and vmax + # Tuple/list specs like ``('linear', 0, 1)`` pack positional args for + # ``constructor.Norm``. Build the Normalize now so downstream code can + # treat it uniformly with pre-constructed Normalize instances instead + # of risking a positional/kwarg collision when vmin/vmax are forwarded. + if ( + np.iterable(norm) + and not isinstance(norm, str) + and not isinstance(norm, mcolors.Normalize) + and len(norm) > 1 + ): + norm = constructor.Norm(norm, **norm_kw) + norm_kw = {} + # A ``Normalize`` instance already carries vmin/vmax, so combining it + # with explicit vmin/vmax is ambiguous. String / single-element list or + # tuple specs are just names for ``constructor.Norm`` and accept + # vmin/vmax as kwargs. if (vmin is not None or vmax is not None) and isinstance( norm, mcolors.Normalize ): - raise ValueError("If 'norm' is given, 'vmin' and 'vmax' must not be set.") + raise ValueError( + "If 'norm' is a Normalize instance, 'vmin' and 'vmax' must not be " + "set. Pass them through the Normalize constructor, or specify " + "'norm' as a string / list / tuple to let vmin and vmax apply." + ) if isinstance(norm, mcolors.Normalize): vmin = norm.vmin vmax = norm.vmax diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index caa252359..41a202c1a 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -1228,16 +1228,34 @@ def test_colorbar_span_position_matches_target_rows(): assert panel_pos.height > row0_pos.height * 1.5 -def test_colorbar_norm_str_with_limits(): - """ " - Should allow to pass vmin or vmax when we are passing a str formatter +@pytest.mark.parametrize( + "norm", + ["linear", ["linear"], ("linear",)], +) +def test_colorbar_norm_str_with_limits(norm): + """ + Should allow to pass vmin or vmax when we are passing a norm specification + as a string, list, or tuple (per the ``constructor.Norm`` contract). """ data = np.random.rand(10, 10) fig, ax = uplt.subplots() - cm = ax.pcolormesh(data, vmin=0.1, norm="linear", vmax=1) + cm = ax.pcolormesh(data, vmin=0.1, norm=norm, vmax=1) + assert cm.norm.vmin == pytest.approx(0.1) + assert cm.norm.vmax == pytest.approx(1) + + +@pytest.mark.parametrize("norm", [("linear", 0.1, 1), ["linear", 0.1, 1]]) +def test_colorbar_norm_tuple_positional_limits(norm): + """ + Tuple / list form ``(name, vmin, vmax)`` should construct the normalizer + with the positional arguments and not collide with implicit vmin/vmax + kwargs when the user does not separately specify them. + """ + data = np.random.rand(10, 10) + fig, ax = uplt.subplots() + cm = ax.pcolormesh(data, norm=norm) assert cm.norm.vmin == pytest.approx(0.1) assert cm.norm.vmax == pytest.approx(1) - return fig def test_colorbar_norm_with_limits(): From 0141a097ee3758226605ce9294b84eadcd26cc6b Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 16 Apr 2026 16:49:18 +1000 Subject: [PATCH 5/5] Allow tuple and list parsing --- ultraplot/tests/test_colorbar.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py index 41a202c1a..cbeb410c2 100644 --- a/ultraplot/tests/test_colorbar.py +++ b/ultraplot/tests/test_colorbar.py @@ -1244,7 +1244,9 @@ def test_colorbar_norm_str_with_limits(norm): assert cm.norm.vmax == pytest.approx(1) -@pytest.mark.parametrize("norm", [("linear", 0.1, 1), ["linear", 0.1, 1]]) +@pytest.mark.parametrize( + "norm", [("linear", 0.1, 1), ["linear", 0.1, 1], ("linear", 0.1, 1, False)] +) def test_colorbar_norm_tuple_positional_limits(norm): """ Tuple / list form ``(name, vmin, vmax)`` should construct the normalizer