Skip to content
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

ENH: add shading='nearest' and 'auto' to pcolormesh #16258

Merged
merged 1 commit into from
Feb 9, 2020

Conversation

jklymak
Copy link
Member

@jklymak jklymak commented Jan 18, 2020

PR Summary

Redo of #9629. Lots of discussion there.

See built example here: https://29535-1385122-gh.circle-artifacts.com/0/home/circleci/project/doc/build/html/gallery/images_contours_and_fields/pcolormesh_grids.html#sphx-glr-gallery-images-contours-and-fields-pcolormesh-grids-py

Se new pcolormesh docs here:

https://29535-1385122-gh.circle-artifacts.com/0/home/circleci/project/doc/build/html/api/_as_gen/matplotlib.axes.Axes.pcolormesh.html#matplotlib.axes.Axes.pcolormesh

Suppose len(x) is N, len(y) is M, and size(C) is M, N. Previously, if shading='flat' then pcolor/mesh would silently drop the last row and column of C and x and y would specify the edges of the colorer quadrilaterals.

While compatible with Matlab, this is both arbitrary, and probably not what the user wanted.

This PR changes a few related things:

  1. if shading='flat' and the dimensions of x, y and C match, a Deprecation Warning is raised the data is no longer silently dropped.
  2. A new shading='nearest' is provided that only allows for x, y and C to match, and centers the colors on the grid points.
  3. A new shading='auto' is provided that will choose 'flat' or 'nearest' depending on the size of the data.
  4. A new rcParam['pcolor.shading'] is provided that defaults to 'flat' (for back compatibility).

Demo:

import numpy as np
import matplotlib.pyplot as plt

fig, axs = plt.subplots(3, 2, sharex=True, sharey=True)

nx = 4
ny = 5
x = np.arange(nx)
y = np.arange(ny)

X, Y = np.meshgrid(x, y)
np.random.seed(19680808)

C = np.random.rand(ny, nx)

# deprecation warning:
axs[0, 0].pcolormesh(x, y, C, vmin=0, vmax=1, shading='flat')
axs[0, 0].scatter(X[:], Y[:], c=C.flat, ec='w')
axs[0, 0].set_title('flat: Emits deprecation warning')

# no deprecation warning
axs[0, 1].pcolormesh(x, y, C[:-1, :-1], vmin=0, vmax=1, shading='flat')
axs[0, 1].scatter(X[:], Y[:], c=C.flat, ec='w')
axs[0, 1].set_title('flat: No warning')

# no deprecation warning
axs[1, 0].pcolormesh(x, y, C, shading='nearest', vmin=0, vmax=1)
axs[1, 0].scatter(X[:], Y[:], c=C.flat, ec='w')
axs[1, 0].set_title('nearest')

# no deprecation warning
axs[1, 1].pcolormesh(x, y, C, shading='gouraud', vmin=0, vmax=1)
axs[1, 1].scatter(X[:], Y[:], c=C.flat, ec='w')
axs[1, 1].set_title('gouraud')

axs[1, 1].set_xlim([-0.5, 3.5])
axs[1, 1].set_ylim([-0.5, 4.5])

# no deprecation warning:
axs[2, 0].pcolormesh(x, y, C, vmin=0, vmax=1, shading='auto')
axs[2, 0].scatter(X[:], Y[:], c=C.flat, ec='w')
axs[2, 0].set_title('auto: matching size')

# no deprecation warning
axs[2, 1].pcolormesh(x, y, C[:-1, :-1], vmin=0, vmax=1, shading='auto')
axs[2, 1].scatter(X[:], Y[:], c=C.flat, ec='w')
axs[2, 1].set_title('auto: c smaller')

The first call emits:

testpcolormesh.py:17: MatplotlibDeprecationWarning: shading='flat' when X and Y have 
the same dimensions as C is deprecated since 3.3.  Either specify the corners of 
the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud'
or set rcParams['pcolor.shading']
  axs[0, 0].pcolormesh(x, y, C, vmin=0, vmax=1, shading='flat')

pcolor

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

@jklymak jklymak mentioned this pull request Jan 18, 2020
7 tasks
@jklymak
Copy link
Member Author

jklymak commented Jan 18, 2020

Note this fails a bunch of tests because apparently we often call x, y, C with same size and the new Deprecation Warning causes the tests to fail.

"Either specify the corners of the quadrilaterals "
"with X and Y, or pass shading='auto', 'nearest' "
"or 'gouraud', or set rcParams['pcolor.shading']")
C = C[:Ny - 1, :Nx - 1]
Copy link
Member

Choose a reason for hiding this comment

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

It looks like you are missing the mixed cases like C.shape == (Ny+1, Nx). Did we handle them before?

Copy link
Member Author

Choose a reason for hiding this comment

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

No we didn't handle that, and I think thats OK for simplicity's sake. If someone has a more complicated grid I feel they can wrangle it themselves....

Copy link
Member

Choose a reason for hiding this comment

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

Actually, we did handle it, because in the "flat" case we always did the indexing (your line 5661), which "worked" with the mixed case. In the code you have here, the mixed case will fail somewhere downstream instead of being caught with an exception, or simply handled as it was originally.

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 you could fix it by changing "and" to "or" in your line 5654, and then out-denting line 5661.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, 5654 could also change "and" to "or". Warn if either dimension isn't right.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep that now emits the warning for:
ax.pcolormesh(x, y[:-1], C[:-1, :-1], vmin=0, vmax=1, shading='flat')

@efiring
Copy link
Member

efiring commented Jan 19, 2020

I've only taken a quick look so far, but I think this is on the right track. I'm OK with keeping the "shading" kwarg and adding the options as you have done. For the test failures, it sounds like the thing to do is modify the tests such that the test images don't change.

@jklymak
Copy link
Member Author

jklymak commented Jan 19, 2020

The fact that so many tests are hitting the deprecation warning should tell us that even the folks who write examples and tests for Matplotlib make X, Y and C the same size.

@jklymak jklymak force-pushed the fixpcolor2 branch 4 times, most recently from e05a6ae to e9dbec1 Compare January 19, 2020 23:03
Copy link
Contributor

@greglucas greglucas left a comment

Choose a reason for hiding this comment

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

I think this is a great addition! I agree with your decision to update the shading keyword argument rather than adding another keyword for this use case, I think it makes logical sense.

I've just made some minor comments in the code below, but it is close to being ready in my opinion.

examples/images_contours_and_fields/pcolormesh_grids.py Outdated Show resolved Hide resolved
examples/images_contours_and_fields/pcolormesh_grids.py Outdated Show resolved Hide resolved
examples/images_contours_and_fields/pcolormesh_grids.py Outdated Show resolved Hide resolved
examples/images_contours_and_fields/pcolormesh_grids.py Outdated Show resolved Hide resolved
examples/images_contours_and_fields/pcolormesh_levels.py Outdated Show resolved Hide resolved
examples/images_contours_and_fields/pcolormesh_levels.py Outdated Show resolved Hide resolved
lib/matplotlib/axes/_axes.py Outdated Show resolved Hide resolved
lib/matplotlib/colorbar.py Show resolved Hide resolved
@jklymak jklymak force-pushed the fixpcolor2 branch 3 times, most recently from 2ea4fe5 to 9241c89 Compare January 21, 2020 16:50
@jklymak
Copy link
Member Author

jklymak commented Jan 21, 2020

The fact that so many tests are hitting the deprecation warning should tell us that even the folks who write examples and tests for Matplotlib make X, Y and C the same size.

Though I must admit I was responsible for about half of them 😉

@jklymak jklymak added this to the v3.3.0 milestone Jan 21, 2020
@jklymak jklymak marked this pull request as ready for review January 21, 2020 17:42
@jklymak
Copy link
Member Author

jklymak commented Jan 21, 2020

@efiring, and anyone else, this is ready for review I think, but happy to make more changes.

`.axes.Axes.pcolormesh` and `~.axes.Axes.pcolor` have a few options for
how grids are laid out and the shading between the grid points.

Generally, if *Z* is size *MxN* then the grid *X* and *Y* can be
Copy link
Contributor

@anntzer anntzer Jan 21, 2020

Choose a reason for hiding this comment

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

size -> shape (for an array t, t.shape is its shape, t.size its number of elements, so it's really not the same thing) ("is size" -> "has shape"
(throughout)
I also prefer (M, N) to MxN but won't overly insist on it (but (M+1, N+1) reads nicer that M+1 x N+1, in particular, I think).

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I got all the shape/size changes...

def _interp_grid(X):
# helper for below
dX = np.diff(X, axis=1)/2.
X = np.hstack((X[:, [0]] - dX[:, [0]],
Copy link
Contributor

Choose a reason for hiding this comment

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

Symmetrically expanding the first and last pixels seem fine but needs to be documented below. Also I guess this will crash if X or Y has size 1? (Sure it's degenerate, but I guess you can either use a width of 1 (likewise needs doc), or error out more cleanly.)

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess we should error. Assuming width 1 seems undesirable....

Copy link
Contributor

Choose a reason for hiding this comment

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

erroring is certainly fine with me

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 the general mpl strategy, though, is to try hard not to error out when data are simply missing:

In [2]: plt.plot([])
Out[2]: [<matplotlib.lines.Line2D at 0x119b13898>]

The idea is that if you have a long-running process making plots from a stream of data that might have gaps, it is better to show an empty window during a gap than to require that the user trap the gap condition and handle it manually.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, fair enough. Instead of guessing a dX, I just make it [X, X] and nothing visible gets plotted. Anything else seems liable to just be wrong (dX = X/2, has problems if X=0, and just choosing dX=1/2 seems like it could mess with scaling of some plots inadvertently.

@jklymak jklymak force-pushed the fixpcolor2 branch 2 times, most recently from 5b78d2e to 3d8700e Compare January 22, 2020 05:18
@anntzer
Copy link
Contributor

anntzer commented Feb 8, 2020

There's something funny going on with sticky_edges and sharex/sharey; not sure whether it's due to this PR or something preexisting:

from pylab import *

Z = np.arange(15).reshape(3, 5)
x = np.arange(6)
y = np.arange(4)

fig, axs = plt.subplots(2, 2, sharex="col", sharey="col")
axs[0, 0].pcolormesh(x, y, Z, shading='auto')
axs[1, 1].pcolormesh(x, y, Z, shading='auto')

plt.show()

results in
test
i.e. sticky_edges are correctly applied on the top left ("master", from the PoV of sticky edges) but not on the bottom right ("follower").

dX = np.diff(X, axis=1)/2.
X = np.hstack((X[:, [0]] - dX[:, [0]],
X[:, :-1] + dX,
X[:, [-1]] + dX[:, [-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 guess the whole thing would read slightly nicer if you interpolated rows instead of columns (and then below did the double transpose in the ncols == Nx case and not in the nrows = Ny case):

if len(X) > 1:
    dX = np.diff(X, axis=0) / 2
    X = np.row_stack([X[0] - dX[0], X[:-1] + dX, X[-1] + dX[-1]])
else:
    X = np.row_stack([X, X])

(untested)

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 only reads nicer if you can remember the rule as to whether X[a] refers to that a-th row or column. I prefer the more explicit form.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's not too hard if you think of X as "list of lists" (then X[a] is just the a-th list, which is clearly the a-th row). Anyways, not insisting on it, this is fine too.

Copy link
Member Author

Choose a reason for hiding this comment

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

which is clearly the a-th row

Why is that clear?

I think when possible, its helpful to be explicit that we are using a 2-D array versus a 1-D. If I read X[0]-dX[0] I immediately think X is 1-D.

Copy link
Contributor

Choose a reason for hiding this comment

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

Because

[[1, 2, 3],
 [4, 5, 6]]

is just

[blah,  # row
 blah]  # row

? i.e. literally how numpy writes out the thing?
Anyways, that's a tangent, not really relevant here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, its probably just that I have to switch between python and Fortran too much to keep it straight.

@jklymak
Copy link
Member Author

jklymak commented Feb 8, 2020

i.e. sticky_edges are correctly applied on the top left ("master", from the PoV of sticky edges) but not on the bottom right ("follower").

That is also present in master.

@anntzer
Copy link
Contributor

anntzer commented Feb 8, 2020

Moved that to #16448.

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.

lgtm post-ci.

@anntzer
Copy link
Contributor

anntzer commented Feb 9, 2020

Let's go :)

@anntzer anntzer merged commit 78bee0f into matplotlib:master Feb 9, 2020
@jklymak jklymak deleted the fixpcolor2 branch February 9, 2020 22:15
@jklymak
Copy link
Member Author

jklymak commented Feb 9, 2020

Thanks for everyone's help and patience with this!

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.

4 participants