Skip to content

Enh: DivergingNorm Fair #15333

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

Closed
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: 27 additions & 0 deletions doc/users/next_whats_new/2019-09-24_divergingnorm_fair.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Fair DivergingNorm
------------------
`~.DivergingNorm` now has an argument ``fair``, which can be set to ``True``
in order to create an off-centered normalization with equally spaced colors.

..plot::

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import DivergingNorm

np.random.seed(19680801)
data = np.random.rand(4, 11)

fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(7, 2))

norm1 = DivergingNorm(0.25, vmin=0, vmax=1, fair=False)
im = ax1.imshow(data, cmap='RdBu', norm=norm1)
cbar = fig.colorbar(im, ax=ax1, orientation="horizontal", aspect=15)

norm2 = DivergingNorm(0.25, vmin=0, vmax=1, fair=True)
im = ax2.imshow(data, cmap='RdBu', norm=norm2)
cbar = fig.colorbar(im, ax=ax2, orientation="horizontal", aspect=15)

ax1.set_title("DivergingNorm(.., fair=False)")
ax2.set_title("DivergingNorm(.., fair=True)")
plt.show()
55 changes: 46 additions & 9 deletions examples/userdemo/colormap_normalizations_diverging.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@
=====================================
DivergingNorm colormap normalization
=====================================

Sometimes we want to have a different colormap on either side of a
conceptual center point, and we want those two colormaps to have
different linear scales. An example is a topographic map where the land
and ocean have a center at zero, but land typically has a greater
elevation range than the water has depth range, and they are often
represented by a different colormap.
"""

##############################################################################
# .. _divergingnorm-diffmap:
#
# Different mapping on either side of a center
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Sometimes we want to have a different colormap on either side of a
# conceptual center point, and we want those two colormaps to have
# different linear scales. An example is a topographic map where the land
# and ocean have a center at zero, but land typically has a greater
# elevation range than the water has depth range, and they are often
# represented by a different colormap.
# This achieved with a `~.DivergingNorm` and by setting its ``vcenter``
# argument to zero.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cbook as cbook
Expand All @@ -29,16 +37,45 @@
colors_land = plt.cm.terrain(np.linspace(0.25, 1, 256))
all_colors = np.vstack((colors_undersea, colors_land))
terrain_map = colors.LinearSegmentedColormap.from_list('terrain_map',
all_colors)
all_colors)

# make the norm: Note the center is offset so that the land has more
# dynamic range:
divnorm = colors.DivergingNorm(vmin=-500, vcenter=0, vmax=4000)

pcm = ax.pcolormesh(longitude, latitude, topo, rasterized=True, norm=divnorm,
cmap=terrain_map,)
cmap=terrain_map)
ax.set_xlabel('Lon $[^o E]$')
ax.set_ylabel('Lat $[^o N]$')
ax.set_aspect(1 / np.cos(np.deg2rad(49)))
fig.colorbar(pcm, shrink=0.6, extend='both', label='Elevation [m]')
plt.show()


##############################################################################
# .. _divergingnorm-fairmap:
#
# Fair mapping on either side of a center
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# On other occasions it may be useful to preserve the linear mapping to colors,
# but still define a center point, such that the colormap extends to both sides
# of the center equally. This can be achieved by using the ``fair=True``
# argument of the `~.DivergingNorm`.

np.random.seed(19680801)
data = np.random.rand(11, 11)

fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(8, 3.5))

norm1 = colors.DivergingNorm(0.25, vmin=0, vmax=1, fair=False)
im = ax1.imshow(data, cmap='RdBu', norm=norm1)
cbar = fig.colorbar(im, ax=ax1, ticks=[0, 0.25, 0.5, 0.75, 1])

norm2 = colors.DivergingNorm(0.25, vmin=0, vmax=1, fair=True)
im = ax2.imshow(data, cmap='RdBu', norm=norm2)
cbar = fig.colorbar(im, ax=ax2, ticks=[0, 0.25, 0.5, 0.75, 1])

ax1.set_title("DivergingNorm(.., fair=False)")
ax2.set_title("DivergingNorm(.., fair=True)")
plt.show()
67 changes: 41 additions & 26 deletions lib/matplotlib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1061,11 +1061,11 @@ def scaled(self):


class DivergingNorm(Normalize):
def __init__(self, vcenter, vmin=None, vmax=None):
def __init__(self, vcenter, vmin=None, vmax=None, fair=False):
Copy link
Member

Choose a reason for hiding this comment

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

I'll block on this being kwarg. Skptical about the normalization on the whole, but very against it being a kwarg for DivergingNorm

Copy link
Member

@story645 story645 Sep 24, 2019

Choose a reason for hiding this comment

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

But I think this belongs in DivergingNorm since that's the use case for truncated around a center colormap. Can we pitch this to the call? (which I'm probably going to be late to).

Copy link
Member

Choose a reason for hiding this comment

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

From a user point of view norm1 = OffcenterNorm(0.0, vmin=-0.25, vmax=0.75) is easier to type, and can get its own section in the docs, rather than making the docs and code for DivergingNorm have a confusing kwarg that does something quite different from what fair=False does. I don't understand the desire to make this a kwarg.

"""
Normalize data with a set center.

Useful when mapping data with an unequal rates of change around a
Useful when mapping data around a
conceptual center, e.g., data that range from -2 to 4, with 0 as
the midpoint.

Expand All @@ -1079,6 +1079,12 @@ def __init__(self, vcenter, vmin=None, vmax=None):
vmax : float, optional
The data value that defines ``1.0`` in the normalization.
Defaults to the the max value of the dataset.
fair : bool, optional
If *False* (default), the range between vmin and vmax will be
mapped to the normalized range ``[0,1]``. If *True*, the range
``[vcenter-d, vcenter+d]`` with
``d=max(abs(vcenter-vmin), abs(vmax-vcenter))`` is mapped to
``[0,1]``. This is useful to ensure colors are equally distributed.

Examples
--------
Expand All @@ -1091,40 +1097,49 @@ def __init__(self, vcenter, vmin=None, vmax=None):
>>> data = [-4000., -2000., 0., 2500., 5000., 7500., 10000.]
>>> offset(data)
array([0., 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])

A more detailed example is found in
:doc:`/gallery/userdemo/colormap_normalizations_diverging`
"""

self.vcenter = vcenter
self.vmin = vmin
self.vmax = vmax
if vcenter is not None and vmax is not None and vcenter >= vmax:
raise ValueError('vmin, vcenter, and vmax must be in '
'ascending order')
if vcenter is not None and vmin is not None and vcenter <= vmin:
raise ValueError('vmin, vcenter, and vmax must be in '
'ascending order')

def autoscale_None(self, A):
"""
Get vmin and vmax, and then clip at vcenter
"""
super().autoscale_None(A)
if self.vmin > self.vcenter:
self.vmin = self.vcenter
if self.vmax < self.vcenter:
self.vmax = self.vcenter
self.fair = fair
super().__init__(vmin=vmin, vmax=vmax)

def __call__(self, value, clip=None):
"""
Map value to the interval [0, 1]. The clip argument is unused.
Map value to (a subset of) the interval [0, 1].
The clip argument is unused.
"""
result, is_scalar = self.process_value(value)
self.autoscale_None(result) # sets self.vmin, self.vmax if None

if not self.vmin <= self.vcenter <= self.vmax:
raise ValueError("vmin, vcenter, vmax must increase monotonically")
result = np.ma.masked_array(
np.interp(result, [self.vmin, self.vcenter, self.vmax],
[0, 0.5, 1.]), mask=np.ma.getmask(result))
if self.vmin > self.vmax:
raise ValueError("vmin must be less or equal vmax")
elif self.vmin == self.vmax:
interp_x = [self.vmin, self.vmax]
interp_y = [0.0, 0.0]
elif self.vcenter >= self.vmax:
interp_x = [self.vmin, self.vcenter]
interp_y = [0.0, 0.5]
elif self.vcenter <= self.vmin:
interp_x = [self.vcenter, self.vmax]
interp_y = [0.5, 1.0]
elif self.fair:
maxrange = max(np.abs(self.vcenter - self.vmin),
np.abs(self.vmax - self.vcenter))
interp_x = [self.vcenter - maxrange, self.vcenter + maxrange]
interp_y = [0, 1.]
else:
interp_x = [self.vmin, self.vcenter, self.vmax]
interp_y = [0, 0.5, 1.]

under = result < self.vmin
over = result > self.vmax
interp = np.interp(result, interp_x, interp_y, left=-1., right=2.)
result = np.ma.masked_array(interp, mask=np.ma.getmask(result))
result[under] = -1.
result[over] = 2.
if is_scalar:
result = np.atleast_1d(result)[0]
return result
Expand Down
82 changes: 34 additions & 48 deletions lib/matplotlib/tests/test_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,58 +324,44 @@ def test_DivergingNorm_scale():


def test_DivergingNorm_scaleout_center():
# test the vmin never goes above vcenter
# test vcenter outside [vmin, vmax]
norm = mcolors.DivergingNorm(vcenter=0)
norm([1, 2, 3, 5])
assert norm.vmin == 0
assert norm.vmax == 5

a = norm([1., 2., 3., 5.])
assert norm.vmin == 1.
assert norm.vmax == 5.
assert_array_almost_equal(a, np.array([0.6, 0.7, 0.8, 1.0]))

def test_DivergingNorm_scaleout_center_max():
# test the vmax never goes below vcenter
norm = mcolors.DivergingNorm(vcenter=0)
norm([-1, -2, -3, -5])
assert norm.vmax == 0
assert norm.vmin == -5


def test_DivergingNorm_Even():
norm = mcolors.DivergingNorm(vmin=-1, vcenter=0, vmax=4)
vals = np.array([-1.0, -0.5, 0.0, 1.0, 2.0, 3.0, 4.0])
expected = np.array([0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
assert_array_equal(norm(vals), expected)


def test_DivergingNorm_Odd():
norm = mcolors.DivergingNorm(vmin=-2, vcenter=0, vmax=5)
vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
expected = np.array([0.0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
assert_array_equal(norm(vals), expected)


def test_DivergingNorm_VminEqualsVcenter():
with pytest.raises(ValueError):
mcolors.DivergingNorm(vmin=-2, vcenter=-2, vmax=2)


def test_DivergingNorm_VmaxEqualsVcenter():
with pytest.raises(ValueError):
mcolors.DivergingNorm(vmin=-2, vcenter=2, vmax=2)


def test_DivergingNorm_VminGTVcenter():
with pytest.raises(ValueError):
mcolors.DivergingNorm(vmin=10, vcenter=0, vmax=20)


def test_DivergingNorm_DivergingNorm_VminGTVmax():
with pytest.raises(ValueError):
mcolors.DivergingNorm(vmin=10, vcenter=0, vmax=5)


def test_DivergingNorm_VcenterGTVmax():
a = norm([-1., -2., -3., -5.])
assert norm.vmax == -1.
assert norm.vmin == -5.
assert_array_almost_equal(a, np.array([0.4, 0.3, 0.2, 0.0]))


@pytest.mark.parametrize("vmin,vc,vmax,fair,vals,expect",
[[-1, 0, 4, False, [-1.0, -0.5, 0.0, 1.0, 2.0, 3.0, 4.0],
[0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0]],
[-2, 0, 5, False, [-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0],
[0.0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]],
[-2, -2, 2, False, [-3, -2, -1, 0, 1, 2, 3],
[-1, 0.5, 0.625, 0.75, 0.875, 1., 2.]],
[-2, 2, 2, False, [-3, -2, -1, 0, 1, 2, 3],
[-1., 0., 0.125, 0.25, 0.375, 0.5, 2.0]],
[10, 0, 20, False, [0, 5, 10, 15, 20], [-1, -1, 0.75, 0.875, 1.]],
[10, 30, 20, False, [10, 15, 20, 25, 30], [0., 0.125, 0.25, 2, 2]],
[-4, -4, -4, False, [-8, -6, -4, -2, 0], [-1., -1., 0., 2., 2.]],
[-6, 0, 12, True, [-12, -6, -3, 0, 6, 12],
[-1, 0.25, 0.375, 0.5, 0.75, 1.]]])
def test_DivergingNorm_Misc(vmin, vc, vmax, fair, vals, expect):
norm = mcolors.DivergingNorm(vmin=vmin, vcenter=vc, vmax=vmax, fair=fair)
assert_array_equal(norm(vals), np.array(expect))


def test_DivergingNorm_rasing():
# test for vmin > vmax -> not allowed
with pytest.raises(ValueError):
mcolors.DivergingNorm(vmin=10, vcenter=25, vmax=20)
norm = mcolors.DivergingNorm(vmin=10, vcenter=0, vmax=5)
norm(np.array([-3, -2, -1, 0, 1, 2, 3]))


def test_DivergingNorm_premature_scaling():
Expand Down
Loading