Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Don't clip PowerNorm inputs < vmin #27589

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 10, 2024
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
5 changes: 5 additions & 0 deletions 5 doc/api/next_api_changes/behavior/27589-DS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
PowerNorm no longer clips values below vmin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When ``clip=False`` is set (the default) on `~matplotlib.colors.PowerNorm`,
values below ``vmin`` are now linearly normalised. Previously they were clipped
to zero. This fixes issues with the display of colorbars associated with a power norm.
32 changes: 20 additions & 12 deletions 32 lib/matplotlib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1953,10 +1953,10 @@ class PowerNorm(Normalize):
Determines the behavior for mapping values outside the range
``[vmin, vmax]``.

If clipping is off, values outside the range ``[vmin, vmax]`` are also
transformed by the power function, resulting in values outside ``[0, 1]``.
This behavior is usually desirable, as colormaps can mark these *under*
and *over* values with specific colors.
If clipping is off, values above *vmax* are transformed by the power
function, resulting in values above 1, and values below *vmin* are linearly
transformed resulting in values below 0. This behavior is usually desirable, as
colormaps can mark these *under* and *over* values with specific colors.

If clipping is on, values below *vmin* are mapped to 0 and values above
*vmax* are mapped to 1. Such values become indistinguishable from
Expand All @@ -1969,6 +1969,8 @@ class PowerNorm(Normalize):
.. math::

\left ( \frac{x - v_{min}}{v_{max} - v_{min}} \right )^{\gamma}

For input values below *vmin*, gamma is set to one.
"""
def __init__(self, gamma, vmin=None, vmax=None, clip=False):
super().__init__(vmin, vmax, clip)
Expand All @@ -1994,9 +1996,8 @@ def __call__(self, value, clip=None):
mask=mask)
resdat = result.data
resdat -= vmin
resdat[resdat < 0] = 0
np.power(resdat, gamma, resdat)
resdat /= (vmax - vmin) ** gamma
resdat /= (vmax - vmin)
resdat[resdat > 0] = np.power(resdat[resdat > 0], gamma)

result = np.ma.array(resdat, mask=result.mask, copy=False)
if is_scalar:
Expand All @@ -2006,14 +2007,21 @@ def __call__(self, value, clip=None):
def inverse(self, value):
if not self.scaled():
raise ValueError("Not invertible until scaled")

result, is_scalar = self.process_value(value)

gamma = self.gamma
vmin, vmax = self.vmin, self.vmax

if np.iterable(value):
val = np.ma.asarray(value)
return np.ma.power(val, 1. / gamma) * (vmax - vmin) + vmin
else:
return pow(value, 1. / gamma) * (vmax - vmin) + vmin
resdat = result.data
resdat[resdat > 0] = np.power(resdat[resdat > 0], 1 / gamma)
resdat *= (vmax - vmin)
resdat += vmin

result = np.ma.array(resdat, mask=result.mask, copy=False)
if is_scalar:
result = result[0]
return result


class BoundaryNorm(Normalize):
Expand Down
8 changes: 4 additions & 4 deletions 8 lib/matplotlib/tests/test_colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,10 +552,10 @@ def test_colorbar_lognorm_extension(extend):

def test_colorbar_powernorm_extension():
# Test that colorbar with powernorm is extended correctly
f, ax = plt.subplots()
cb = Colorbar(ax, norm=PowerNorm(gamma=0.5, vmin=0.0, vmax=1.0),
orientation='vertical', extend='both')
assert cb._values[0] >= 0.0
Copy link
Member Author

@dstansby dstansby Jan 6, 2024

Choose a reason for hiding this comment

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

This check came in with #11610, but from that PR it seems like it's enough just to add the colorbar and check no error or warning is raised. It's expected that _values now has a value < 0 with this PR.

# Just a smoke test that adding the colorbar doesn't raise an error or warning
fig, ax = plt.subplots()
Colorbar(ax, norm=PowerNorm(gamma=0.5, vmin=0.0, vmax=1.0),
orientation='vertical', extend='both')


def test_colorbar_axes_kw():
Expand Down
17 changes: 15 additions & 2 deletions 17 lib/matplotlib/tests/test_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,12 +555,16 @@ def test_PowerNorm():
assert_array_almost_equal(norm(a), pnorm(a))

a = np.array([-0.5, 0, 2, 4, 8], dtype=float)
expected = [0, 0, 1/16, 1/4, 1]
expected = [-1/16, 0, 1/16, 1/4, 1]
QuLogic marked this conversation as resolved.
Show resolved Hide resolved
pnorm = mcolors.PowerNorm(2, vmin=0, vmax=8)
assert_array_almost_equal(pnorm(a), expected)
assert pnorm(a[0]) == expected[0]
assert pnorm(a[2]) == expected[2]
assert_array_almost_equal(a[1:], pnorm.inverse(pnorm(a))[1:])
# Check inverse
a_roundtrip = pnorm.inverse(pnorm(a))
assert_array_almost_equal(a, a_roundtrip)
# PowerNorm inverse adds a mask, so check that is correct too
assert_array_equal(a_roundtrip.mask, np.zeros(a.shape, dtype=bool))

# Clip = True
a = np.array([-0.5, 0, 1, 8, 16], dtype=float)
Expand Down Expand Up @@ -591,6 +595,15 @@ def test_PowerNorm_translation_invariance():
assert_array_almost_equal(pnorm(a - 2), expected)


def test_powernorm_cbar_limits():
fig, ax = plt.subplots()
vmin, vmax = 300, 1000
data = np.arange(10*10).reshape(10, 10) + vmin
im = ax.imshow(data, norm=mcolors.PowerNorm(gamma=0.2, vmin=vmin, vmax=vmax))
cbar = fig.colorbar(im)
assert cbar.ax.get_ylim() == (vmin, vmax)


def test_Normalize():
norm = mcolors.Normalize()
vals = np.arange(-10, 10, 1, dtype=float)
Expand Down
Morty Proxy This is a proxified and sanitized view of the page, visit original site.