Skip to content
Merged
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
27 changes: 24 additions & 3 deletions ultraplot/axes/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4120,9 +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
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.")
# 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 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
Expand Down
64 changes: 64 additions & 0 deletions ultraplot/tests/test_colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -1226,3 +1226,67 @@ 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


@pytest.mark.parametrize(
"norm",
["linear", ["linear"], ("linear",)],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
["linear", ["linear"], ("linear",)],
["linear", "log", ["linear"], ("linear",)],

What about trying a different norm?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could; I am currently just testing against the explicit inputs which are str, list or tuple

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that we conducted the type testing there.
It is probably confirmed somewhere that parsing a string and converting it into the corresponding normalizer is possible, so it might not be necessary here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will check

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this test

@pytest.mark.parametrize("norm", [uplt.DiscreteNorm, uplt.colors.mcolors.Normalize])
def test_normalize_types(norm):
    data = np.random.rand(10, 10)
    target = norm

    print(norm)
    if norm is uplt.DiscreteNorm:
        norm = uplt.DiscreteNorm(levels=[0, 1])
        discrete = True
    elif norm is uplt.colors.mcolors.Normalize:
        norm = uplt.colors.mcolors.Normalize(vmin=0, vmax=1)
        discrete = False
    else:
        raise ValueError("Norm not understood.")
    fig, ax = uplt.subplots()
    cm = ax.pcolormesh(data, norm=norm, discrete=discrete)
    assert isinstance(cm.norm, target)

)
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=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], ("linear", 0.1, 1, False)]
)
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)


@pytest.mark.parametrize("norm", [uplt.DiscreteNorm, uplt.colors.mcolors.Normalize])
def test_normalize_types(norm):
data = np.random.rand(10, 10)
target = norm

if norm is uplt.DiscreteNorm:
norm = uplt.DiscreteNorm(levels=[0, 1])
discrete = True
elif norm is uplt.colors.mcolors.Normalize:
norm = uplt.colors.mcolors.Normalize(vmin=0, vmax=1)
discrete = False
else:
raise ValueError("Norm not understood.")
fig, ax = uplt.subplots()
cm = ax.pcolormesh(data, norm=norm, discrete=discrete)
assert isinstance(cm.norm, target)


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