diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index df1d27277..591aa9c2e 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -4168,6 +4168,7 @@ def _parse_cmap( extend=None, vmin=None, vmax=None, + vcenter=None, discrete=None, default_cmap=None, default_discrete=True, @@ -4193,6 +4194,8 @@ def _parse_cmap( The colormap extend setting. vmin, vmax : float, optional The normalization range. + vcenter : float, optional + The center value for diverging normalizers. sequential, diverging, cyclic, qualitative : bool, optional Toggle various colormap types. discrete : bool, optional @@ -4227,19 +4230,26 @@ def _parse_cmap( # 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( + if (vmin is not None or vmax is not None or vcenter is not None) and isinstance( norm, mcolors.Normalize ): raise ValueError( - "If 'norm' is a Normalize instance, 'vmin' and 'vmax' must not be " + "If 'norm' is a Normalize instance, 'vmin', 'vmax', and 'vcenter' must not be " "set. Pass them through the Normalize constructor, or specify " - "'norm' as a string / list / tuple to let vmin and vmax apply." + "'norm' as a string / list / tuple to let vmin, vmax, and vcenter apply." ) if isinstance(norm, mcolors.Normalize): vmin = norm.vmin vmax = norm.vmax vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop("vmin", None)) vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop("vmax", None)) + vcenter = _not_none( + vcenter=vcenter, norm_kw_vcenter=norm_kw.pop("vcenter", None) + ) + if vcenter is not None: + norm_kw["vcenter"] = vcenter + if norm is None: + norm = "diverging" extend = _not_none(extend, "neither") modes = { key: kwargs.pop(key, None) diff --git a/ultraplot/tests/test_2dplots.py b/ultraplot/tests/test_2dplots.py index ce12b0bd1..f3bda1ad5 100644 --- a/ultraplot/tests/test_2dplots.py +++ b/ultraplot/tests/test_2dplots.py @@ -11,18 +11,39 @@ import ultraplot as uplt, warnings -@pytest.mark.skip("not sure what this does") -@pytest.mark.mpl_image_compare def test_colormap_vcenter(rng): """ - Test colormap vcenter. + Test that explicit `vcenter` configures a diverging normalizer centered at `vcenter`. """ fig, axs = uplt.subplots(ncols=3) data = 10 * rng.random((10, 10)) - 3 - axs[0].pcolor(data, vcenter=0) - axs[1].pcolor(data, vcenter=1) - axs[2].pcolor(data, vcenter=2) - return fig + m0 = axs[0].pcolor(data, vcenter=0) + m1 = axs[1].pcolor(data, vcenter=1) + m2 = axs[2].pcolor(data, vcenter=2) + + # In discrete mode (default), m.norm is DiscreteNorm wrapping DivergingNorm (_norm) + assert m0.norm._norm.vcenter == pytest.approx(0) + assert m1.norm._norm.vcenter == pytest.approx(1) + assert m2.norm._norm.vcenter == pytest.approx(2) + + # The underlying diverging norm maps vcenter to 0.5 (center of colormap) + assert m0.norm._norm(0) == pytest.approx(0.5) + assert m1.norm._norm(1) == pytest.approx(0.5) + assert m2.norm._norm(2) == pytest.approx(0.5) + + # Verify continuous mode (discrete=False) where DivergingNorm is directly used + _, ax = uplt.subplots() + m_cont = ax.pcolor(data, vcenter=1.5, discrete=False) + assert isinstance(m_cont.norm, uplt.DivergingNorm) + assert m_cont.norm.vcenter == pytest.approx(1.5) + assert m_cont.norm(1.5) == pytest.approx(0.5) + + # Verify passing vcenter via norm_kw works identically + _, ax_kw = uplt.subplots() + m_kw = ax_kw.pcolor(data, norm_kw={"vcenter": 1.5}, discrete=False) + assert isinstance(m_kw.norm, uplt.DivergingNorm) + assert m_kw.norm.vcenter == pytest.approx(1.5) + assert m_kw.norm(1.5) == pytest.approx(0.5) @pytest.mark.mpl_image_compare