Skip to content

Add DivergingNorm (again, again, again) #12419

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 1 commit into from
Mar 18, 2019

Conversation

jklymak
Copy link
Member

@jklymak jklymak commented Oct 6, 2018

PR Summary

Edit: see documentation here (and examples linked therein)

https://19203-1385122-gh.circle-artifacts.com/0/home/circleci/project/doc/build/html/api/_as_gen/matplotlib.colors.DivergingNorm.html#matplotlib.colors.DivergingNorm

This PR is re-invigorating #5054, which re-invigorated #4666, which re-invigorated #3858. Who knows, maybe it went back before that. Hat tip to @dopplershift, @OceanWolf , @jkseppan, @phobson who wrote this, not me. EDIT: forgot #5061....

It (re-) introduces DivergingNorm:

This seems like a pretty straightforward and helpful enhancement, so not sure why this is the fourth PR. OTOH, please note the todo-list is still active, this needs docs, whats new, and more thorough tests and I've not yet double checked the code. But throwing it out there in case any of the original authors want their PR back.

Mostly ready to go, but no doubt some edge cases...

    x = np.linspace(-2, 7)
    y = np.linspace(-1*np.pi, np.pi)
    X, Y = np.meshgrid(x, y)
    Z = x * np.sin(Y)**2

    fig, (ax1, ax2) = plt.subplots(ncols=2)
    cmap = plt.cm.coolwarm
    norm = mcolors.DivergingNorm(vmin=-2, vcenter=0, vmax=7)

    img1 = ax1.pcolormesh(Z, cmap=cmap, norm=None)
    cbar1 = fig.colorbar(img1, ax=ax1)

    img2 = ax2.pcolormesh(Z, cmap=cmap, norm=norm)
    cbar2 = fig.colorbar(img2, ax=ax2)

test_offset_norm

PR Checklist

  • Has Pytest style unit tests
  • Code is Flake 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

This was referenced Oct 6, 2018
@jklymak
Copy link
Member Author

jklymak commented Oct 6, 2018

Actually in the light of day I’m not going to champion this. If someone tried to publish in my field with negative anomalies on a different colour-to-data ratio than positive anomalies they would be in for a drubbing. Maybe there are applications that I don’t know about where this feature would be desirable and legitimate so feel free to reopen this or one of its predecessors. But, I don’t know the use case for encouraging this sort of data mapping by default.

@jklymak jklymak closed this Oct 6, 2018
@ImportanceOfBeingErnest
Copy link
Member

The use of such Norm is clearly that you may work with one single colormap and adapt the norm to the data in use.

The following image is taken from this stackoverflow question where the idea is to define a "sealevel", such that the turquoise part of the colormap is always at 0 elevation.

image

The alternative to this approach would be to create a new colormap for each individual dataset, which is surely undesireable.

@jklymak
Copy link
Member Author

jklymak commented Oct 6, 2018

Ahh. Thanks the sea level one is a great example where this is ok. I’ll change the examples and tests to that 😉

@jklymak jklymak reopened this Oct 6, 2018
@ImportanceOfBeingErnest
Copy link
Member

There is by the way a part about this in the docs: https://matplotlib.org/users/colormapnorms.html#custom-normalization-two-linear-ranges which tells that "This may appear soon as colors.OffsetNorm()"

and it has the warning and the use case mentionned:

As above, non-symmetric mapping of data to color is non-standard practice for quantitative data, and should only be used advisedly. A practical example is having an ocean/land colormap where the land and ocean data span different ranges.

@jklymak
Copy link
Member Author

jklymak commented Oct 6, 2018

Ha ha, I wrote that 😉

@ImportanceOfBeingErnest
Copy link
Member

In the current version that example has somehow become useless, as it suddenly uses data that is symmetric about 0 anyways. It also lost the warning. There is also an error in the BoundaryNorm section. I suppose that was edited in a hurry and would need to be refurbished anyways.

import numpy as np
import pytest

from numpy.testing import assert_array_equal, assert_array_almost_equal
from numpy.testing import (assert_array_equal, assert_array_almost_equal,
assert_equal)
Copy link
Member

Choose a reason for hiding this comment

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

I think we've generally moved away from using assert_equal. Especially because you're just comparing single values, just assert that the two things are equal.

vmax = np.ma.max(A)

if vmin > vmax:
raise ValueError('vmin must be less than vmax')
Copy link
Member

Choose a reason for hiding this comment

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

less than or equal (or change condition)?

@jklymak jklymak force-pushed the add-asym-norm branch 8 times, most recently from db8c3de to 38cb3dc Compare October 7, 2018 05:37
raise ValueError('vmin, vcenter, and vmax must '
'increment monotonically for DivergingNorm '
raise ValueError('vmin, vcenter, and vmax must increment '
'monotonically for DivergingNorm '
Copy link
Member

Choose a reason for hiding this comment

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

minor point, but would it be more clear to say?

DivergingNorm requires that vmin <vcenter < vmax

@jklymak
Copy link
Member Author

jklymak commented Oct 9, 2018

@pharshalp this now doesn't clip the values outside vmin, vmax unless clip is True...

@pharshalp
Copy link
Contributor

pharshalp commented Oct 9, 2018

@pharshalp this now doesn't clip the values outside vmin, vmax unless clip is True...

@jklymak Thanks! Just checked it using a small example and can confirm that it works!

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors

x = np.linspace(-2, 7)
y = np.linspace(-1*np.pi, np.pi)
X, Y = np.meshgrid(x, y)
Z = x * np.sin(Y)**2

fig, (ax1, ax2) = plt.subplots(ncols=2)
cmap = plt.cm.coolwarm
cmap.set_over('orange')
cmap.set_under('steelblue')
norm = mcolors.DivergingNorm(vmin=-1, vcenter=0, vmax=6)

img1 = ax1.pcolormesh(Z, cmap=cmap, norm=None)
cbar1 = fig.colorbar(img1, ax=ax1, extend='neither')

img2 = ax2.pcolormesh(Z, cmap=cmap, norm=norm)
cbar2 = fig.colorbar(img2, ax=ax2, extend='both')

plt.show()

figure_3

@ImportanceOfBeingErnest
Copy link
Member

Any idea why the two new norms do not appear in the "classes" list in the docs?

@jklymak
Copy link
Member Author

jklymak commented Oct 9, 2018

py 3.5 errors are due to slight differences in whether interpolation is under or over in numpy (i.e. does 1.0 interpolate into [0. 1.] as an over value or not - 3.6/3.7 say not, 3.5 says yes).

What is the plan for 3.1 wrt py 3.5? Discuss in #12358

@jklymak jklymak force-pushed the add-asym-norm branch 3 times, most recently from 9fe878e to 3d6c3c6 Compare March 8, 2019 01:47

bunique = b
yunique = self._y
if b[0] == b[1]:
Copy link
Contributor

Choose a reason for hiding this comment

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

I see how the previous code was doing linear interpolation by hand, but where do these "equal first and second point / equal last and nexttolast point" cases come from?

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed comment:

        bunique = b
        yunique = self._y
        # trim extra b values at beginning and end if they are
        # not unique.  These are here for extended colorbars, and are not
        # wanted for the interpolation.  

Copy link
Member

Choose a reason for hiding this comment

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

I don't quite understand this. Souldn't you trim all but one duplicate values?
The current code does b = [0, 0, 0, 1, 2] --> bunique = [0, 0, 1, 2]. Is that intended?

Copy link
Member Author

Choose a reason for hiding this comment

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

That should never happen - if it does, just as well that an error trips. b are the boundaries, passed through the norm should be monotonic, except for, possibly, the first and last values if there are an "extend" either at max or min.

cmap=terrain_map,)
ax.set_xlabel('Lon $[^o E]$')
ax.set_ylabel('Lat $[^o N]$')
ax.set_aspect(1 / np.cos(np.deg2rad(49)))
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess that comes from Mercator? kind of obscure if that's not your field, but probably not a big deal.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added explanation: This is just a local "projection" centered at 49 degrees taking into account how much closer the longitude lines are at that lattitude than at the equator and then assuming the lines of longitude are parallel. Not very sophisticated 😉

Copy link
Contributor

@anntzer anntzer left a comment

Choose a reason for hiding this comment

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

Only minor nitpicks left, nothing blocking the merge.

@jklymak
Copy link
Member Author

jklymak commented Mar 13, 2019

Thanks @anntzer. Reasonable nitpicks that I’ll fix today. EDIT: Fixed!

@jklymak jklymak force-pushed the add-asym-norm branch 2 times, most recently from 1c81602 to 31785c2 Compare March 13, 2019 23:34
Borrows heavily from @Tillsen's solution found on
StackOverflow here: http://goo.gl/RPXMYB

Used with his permission dicussesd on Github here:
https://github.com/matplotlib/matplotlib/pull/3858`

TST: add tests for DivergingNorm

DOC: add to colors_api.rst

DOC: add tutorial

FIX: fix extend=both

DOC: add new example
assert norm.scaled() is True


def test_DivergingNorm_scaleout_center():
Copy link
Contributor

Choose a reason for hiding this comment

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

this and the one below could use pytest.mark.parametrize

assert norm.vmin == -5


def test_DivergingNorm_Even():
Copy link
Contributor

Choose a reason for hiding this comment

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

and these two as well

assert_array_equal(norm(vals), expected)


def test_DivergingNorm_VminEqualsVcenter():
Copy link
Contributor

Choose a reason for hiding this comment

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

and these fives

Copy link
Member Author

Choose a reason for hiding this comment

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

If someone wants to parametrize these post-facto they can. I am not a fan of parameterized tests....

@jklymak
Copy link
Member Author

jklymak commented Mar 18, 2019

Thanks everyone, particularly @phobson for the original PR, and @anntzer for the thorough review

@jklymak jklymak deleted the add-asym-norm branch March 18, 2019 19:49
timhoffm added a commit that referenced this pull request Mar 18, 2019
…419-on-v3.1.x

Backport PR #12419 on branch v3.1.x (Add DivergingNorm (again, again, again))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants