Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions ultraplot/axes/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4168,6 +4168,7 @@ def _parse_cmap(
extend=None,
vmin=None,
vmax=None,
vcenter=None,
discrete=None,
default_cmap=None,
default_discrete=True,
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
35 changes: 28 additions & 7 deletions ultraplot/tests/test_2dplots.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down