Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Make pcolor(mesh) preserve all data #9629

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
wants to merge 1 commit into from
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
17 changes: 17 additions & 0 deletions doc/users/next_whats_new/2019-12-18-pcolordropdata.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Pcolor and Pcolormesh now have *dropdata* kwarg and rcParam
-----------------------------------------------------------

Previously `.axes.Axes.pcolor` and `.axes.Axes.pcolormesh` handled
the situation where *x* and *y* have the same (respective) size as *Z* by
dropping the last row and column of *Z*, and *x* and *y* are regarded as the
edges of the remaining rows and columns in *Z*. However, most users probably
really want *x* and *y* centered on the rows and columns of *Z*, so if
they specify *dropdata* as True, both methods will now linearly interpolate to
get the edges of the bins, and *x* and *y* will specify the (linear) center of
each gridcell in the pseudocolor plot.

Users can also specify this by the new :rc:`pcolor.dropdata` in their
``.matplotlibrc`` or via `.rcParams`.

See :doc:`pcolormesh </gallery/images_contours_and_fields/pcolormesh_levels>`
for an example.
70 changes: 64 additions & 6 deletions examples/images_contours_and_fields/pcolormesh_levels.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""
==========
pcolormesh
Pcolormesh
==========

Shows how to combine Normalization and Colormap instances to draw "levels" in
`~.axes.Axes.pcolor`, `~.axes.Axes.pcolormesh` and `~.axes.Axes.imshow` type
plots in a similar way to the levels keyword argument to contour/contourf.
`.axes.Axes.pcolormesh` allows you to generate 2-D image-style plots. Note it
is somewhat faster than `~.axes.Axes.pcolor`.

"""

import matplotlib
Expand All @@ -14,6 +14,66 @@
from matplotlib.ticker import MaxNLocator
import numpy as np

###############################################################################
# Basic Pcolormesh
# ----------------
#
# We usually specify a pcolormesh by defining the edge of quadrilaterals and
# the value of the quadrilateral. Note that here *x* and *y* each have one
# extra element than Z in the respective dimension.

np.random.seed(19680801)
Z = np.random.rand(6, 10)
x = np.arange(-0.5, 10, 1) # len = 11
y = np.arange(4.5, 11, 1) # len = 7

fig, ax = plt.subplots()
ax.pcolormesh(x, y, Z)

###############################################################################
# Non-rectilinear Pcolormesh
# --------------------------
#
# Note that we can also specify matrices for *X* and *Y* and have
# non-rectilinear quadrilaterals.

x = np.arange(-0.5, 10, 1) # len = 11
y = np.arange(4.5, 11, 1) # len = 7
X, Y = np.meshgrid(x, y)
X = X + 0.2 * Y # tilt the co-ordinates.
Y = Y + 0.3 * X

fig, ax = plt.subplots()
ax.pcolormesh(X, Y, Z)

###############################################################################
# Centered Co-ordinates
# ---------------------
#
# Often a user wants to pass *X* and *Y* with the same sizes as *Z* to
# `.axes.Axes.pcolormesh`. Matplotlib will either drop the last row and
# column of *Z* (default, h=istorically compatible with Matlab), or if
# ``dropdata=True`` assume the user wanted *X* and *Y* centered on the
# quadrilateral and linearly interpolate to get the edges.

x = np.arange(10) # len = 10
y = np.arange(6) # len = 6
X, Y = np.meshgrid(x, y)

fig, axs = plt.subplots(2, 1, sharex=True, sharey=True)
axs[0].pcolormesh(X, Y, Z, vmin=np.min(Z), vmax=np.max(Z))
axs[0].set_title('dropdata=True (default)')
axs[1].pcolormesh(X, Y, Z, vmin=np.min(Z), vmax=np.max(Z), dropdata=False)
axs[1].set_title('dropdata=False')

###############################################################################
# Making levels using Norms
# -------------------------
#
# Shows how to combine Normalization and Colormap instances to draw
# "levels" in `.axes.Axes.pcolor`, `.axes.Axes.pcolormesh`
# and `.axes.Axes.imshow` type plots in a similar
# way to the levels keyword argument to contour/contourf.

# make these smaller to increase the resolution
dx, dy = 0.05, 0.05
Expand Down Expand Up @@ -54,8 +114,6 @@
# don't overlap
fig.tight_layout()

plt.show()

#############################################################################
#
# ------------
Expand Down
76 changes: 63 additions & 13 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5603,7 +5603,7 @@ def imshow(self, X, cmap=None, norm=None, aspect=None,
return im

@staticmethod
def _pcolorargs(funcname, *args, allmatch=False):
def _pcolorargs(funcname, *args, allmatch=False, dropdata=True):
# If allmatch is True, then the incoming X, Y, C must have matching
# dimensions, taking into account that X and Y can be 1-D rather than
# 2-D. This perfect match is required for Gouraud shading. For flat
Expand Down Expand Up @@ -5665,14 +5665,34 @@ def _pcolorargs(funcname, *args, allmatch=False):
raise TypeError('Dimensions of C %s are incompatible with'
' X (%d) and/or Y (%d); see help(%s)' % (
C.shape, Nx, Ny, funcname))
C = C[:Ny - 1, :Nx - 1]

if dropdata:
C = C[:Ny - 1, :Nx - 1]
else:
def _interp_grid(X):
Copy link
Contributor

@pharshalp pharshalp Oct 21, 2018

Choose a reason for hiding this comment

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

Suggested change
def _interp_grid(X):
def _interp_grid(X):
# helper for below
dX = np.diff(X, axis=1)/2.
if np.allclose(dX[:, :], dX[0, [0]]):
X = np.hstack((X[:, [0]] - dX[:, [0]],
X[:, :-1] + dX,
X[:, [-1]] + dX[:, [-1]])
)
else:
X_ratio = np.sqrt(X[:, 1:]/X[:, 0:-1])
if np.allclose(X_ratio[:, :], X_ratio[0, [0]]):
X = np.hstack((X[:, [0]]/X_ratio[:, [0]],
X[:, :-1]*X_ratio,
X[:, [-1]]*X_ratio[:, [-1]])
)
else:
pass
# Warn the user that the data is neither linearly
# nor logarithmically spaced.
return X

The changes I am suggesting take care of the case when X and/or Y are logarithmically spaced.
(e.g. visualizing a time series of fractional octave band sound pressure level data. See the following example)

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

time = np.array([0, 1, 2, 3])
freq = np.array([500, 1000, 2000, 4000])
time_mesh, freq_mesh = np.meshgrid(time, freq)
C = np.arange(16).reshape(4,4)

fig, ax = plt.subplots()
im = ax.pcolormesh(time_mesh, freq_mesh, C, dropdata=False)
cbar = fig.colorbar(im)
cbar.set_label('Octave band SPL [dB$_{ref: 20 \mu Pa}$]')

ax.set_xticks(time)
ax.set_yscale('log')
ax.set_yticks(freq)
ax.set_yticklabels(freq)
ax.minorticks_off()
ax.set_xlabel('Time [s]')
ax.set_ylabel('Octave band center frequency  [Hz]')

plt.show()

figure_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 like this logarithmic addition! The only thing I'd add is that you can still use the method and calculate dX's even if the points aren't evenly spaced, so the first if statement shouldn't be a requirement. It could just be the default after checking for the ratio.

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 don't think this will apply generally enough to be a good approach. If someone needs precision in the cell edges, they should calculate them themselves as they need, not count on matplotlib to do it. This is just a rough fix which is not quite as rough as just dropping the data.

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, there are a whole bunch of cases where this will fail/error out (x = [10, 9, 8]) that we could check for, but I'm not convinced its worth the extra code.

Copy link
Contributor

@anntzer anntzer Dec 18, 2019

Choose a reason for hiding this comment

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

I guess a possibility would be to do the grid interpolation in screen space? Something like... (assuming dropdata=False and the shapes are such that grid interp is necessary)

  • convert the x values using the current scale
  • if the scaled values are uniformly spaced, do the interp, convert back to data space, use the interp'd grid.
  • if not (and an interpolation is necessary), emit a warning (so that users will know to call set_xscale("log") before passing their log-scaled x values

(very rough sketch)

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 seems way too fancy and hard to explain to the user. And I'm not even convinced it makes sense for curvilinear grids etc. Lets keep this simple and easy to explain. If the user wants something fancier they can calculate their own edges...

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 a possibility would be to do the grid interpolation in screen space? Something like... (assuming dropdata=False and the shapes are such that grid interp is necessary)

* convert the x values using the current scale

* if the scaled values are uniformly spaced, do the interp, convert back to data space, use the interp'd grid.

* if not (and an interpolation is necessary), emit a warning (so that users will know to call set_xscale("log") before passing their log-scaled x values

This process could be converted into a helper function which could be showed off in a gallery example/matplotlib blog. If the OP is fine with it, I am interested in working on a PR demonstrating the process outlined here. Does this sound a like a good idea?

# helper for below
dX = np.diff(X, axis=1)/2.
X = np.hstack((X[:, [0]] - dX[:, [0]],
X[:, :-1] + dX,
X[:, [-1]] + dX[:, [-1]])
)
return X

if ncols == Nx:
X = _interp_grid(X)
Y = _interp_grid(Y)

if nrows == Ny:
X = _interp_grid(X.T).T
Y = _interp_grid(Y.T).T

C = cbook.safe_masked_invalid(C)
return X, Y, C

@_preprocess_data()
@docstring.dedent_interpd
def pcolor(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
vmax=None, **kwargs):
vmax=None, dropdata=None, **kwargs):
r"""
Create a pseudocolor plot with a non-regular rectangular grid.

Expand All @@ -5686,7 +5706,9 @@ def pcolor(self, *args, alpha=None, norm=None, cmap=None, vmin=None,

``pcolor()`` can be very slow for large arrays. In most
cases you should use the similar but much faster
`~.Axes.pcolormesh` instead. See there for a discussion of the
`~.Axes.pcolormesh` instead. See
:ref:`Differences between pcolor() and pcolormesh()
<differences-pcolor-pcolormesh>` for a discussion of the
differences.

Parameters
Expand All @@ -5711,7 +5733,8 @@ def pcolor(self, *args, alpha=None, norm=None, cmap=None, vmin=None,

The dimensions of *X* and *Y* should be one greater than those of
*C*. Alternatively, *X*, *Y* and *C* may have equal dimensions, in
which case the last row and column of *C* will be ignored.
which case the last row and column of *C* will be ignored if
*dropdata* is True (see below).

If *X* and/or *Y* are 1-D arrays or column vectors they will be
expanded as needed into the appropriate 2-D arrays, making a
Expand Down Expand Up @@ -5751,6 +5774,13 @@ def pcolor(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
snap : bool, default: False
Whether to snap the mesh to pixel boundaries.

dropdata : bool, default: :rc:`pcolor.dropdata`
If True (default), and *X* and *Y* are the same size as C in their
respective dimensions, drop the last element of C in both
dimensions. If False, *X* and *Y* are assumed to be the midpoints
of the quadrilaterals, and their edges calculated by linear
interpolation.

Returns
-------
collection : `matplotlib.collections.Collection`
Expand Down Expand Up @@ -5801,12 +5831,19 @@ def pcolor(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
``pcolor()`` displays all columns of *C* if *X* and *Y* are not
specified, or if *X* and *Y* have one more column than *C*.
If *X* and *Y* have the same number of columns as *C* then the last
column of *C* is dropped. Similarly for the rows.
column of *C* is dropped if *dropdata* is True. Similarly for the rows.
If *dropdata* is False, then *X* and *Y* are assumed to be at the
middle of the quadrilaterals, and the edges of the quadrilaterals are
linearly interpolated.

Note: This behavior is different from MATLAB's ``pcolor()``, which
This behavior is different from MATLAB's ``pcolor()``, which
always discards the last row and column of *C*.
"""
X, Y, C = self._pcolorargs('pcolor', *args, allmatch=False)
if dropdata is None:
dropdata = rcParams['pcolor.dropdata']

X, Y, C = self._pcolorargs('pcolor', *args, dropdata=dropdata,
allmatch=False)
Ny, Nx = X.shape

# unit conversion allows e.g. datetime objects as axis values
Expand Down Expand Up @@ -5903,7 +5940,8 @@ def pcolor(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
@_preprocess_data()
@docstring.dedent_interpd
def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
vmax=None, shading='flat', antialiased=False, **kwargs):
vmax=None, shading='flat', antialiased=False,
dropdata=None, **kwargs):
"""
Create a pseudocolor plot with a non-regular rectangular grid.

Expand All @@ -5913,9 +5951,9 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,

*X* and *Y* can be used to specify the corners of the quadrilaterals.

.. note::
.. hint::

`~Axes.pcolormesh` is similar to `~Axes.pcolor`. It's much faster
`~Axes.pcolormesh` is similar to `~Axes.pcolor`. It is much faster
and preferred in most cases. For a detailed discussion on the
differences see :ref:`Differences between pcolor() and pcolormesh()
<differences-pcolor-pcolormesh>`.
Expand All @@ -5942,7 +5980,8 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,

The dimensions of *X* and *Y* should be one greater than those of
*C*. Alternatively, *X*, *Y* and *C* may have equal dimensions, in
which case the last row and column of *C* will be ignored.
which case the last row and column of *C* will be ignored, unless
*dropdata* is *False* (see below).

If *X* and/or *Y* are 1-D arrays or column vectors they will be
expanded as needed into the appropriate 2-D arrays, making a
Expand Down Expand Up @@ -5991,6 +6030,13 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
snap : bool, default: False
Whether to snap the mesh to pixel boundaries.

dropdata : bool, default: :rc:`pcolor.dropdata`
If True (default), and *X* and *Y* are the same size as C in their
respective dimensions, drop the last element of C in both
dimensions. If False, *X* and *Y* are assumed to be the midpoints
of the quadrilaterals, and their edges calculated by linear
interpolation.

Returns
-------
mesh : `matplotlib.collections.QuadMesh`
Expand Down Expand Up @@ -6062,7 +6108,11 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,

allmatch = (shading == 'gouraud')

Copy link
Contributor

@pharshalp pharshalp Oct 21, 2018

Choose a reason for hiding this comment

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

Suggested change
dropdata = kwargs.pop('dropdata', True)

Also, I think the method definition for _pcolorargs needs to be updated to include dropdata=True.

Copy link
Contributor

Choose a reason for hiding this comment

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

What about a different kwarg than dropdata? Maybe something like xyvertices=False, xyedges. Something to insinuate that the current xy are grid points or vertices rather than edges.

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, I see what you mean, but not sure what to do about this. We don't want to imply that we will do anything if len(x)==N+1. I'll propose we keep dropdata for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

The problem I see with dropdata as the kwarg is that you're actually not just including all of the data, you're also changing the centers/edges of the cells by calculating a midpoint everywhere. In the examples above, the edges were at integers (0, 1, 2) and with this fix they are now at -0.5, 0.5, 1.5 2.5. So the grid has shifted as well. To me, dropdata=False reads like you would have just extended the previous edges to include a new extrapolation point to capture all of the data (0, 1, 2, 3).

xy_centers, xy_midpoints?

If len(x)==N+1 and this kwarg is present, you could raise/warn a user that the combo doesn't make sense.

Copy link
Member Author

Choose a reason for hiding this comment

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

you're also changing the centers/edges of the cells by calculating a midpoint everywhere.

I'm not calculating the midpoint, I'm calculating the edges, assuming that the given co-ordinates are the midpoint, which I think is what 99.99% of users would expect. The fact is that I think this should be the default behaviour, but thats a whole back compatibility concern.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with what you've said (hence my xy_midpoints suggestion), I wasn't very clear in my description above. I also agree that I think this is what most users would expect. For backwards compatibility, I think it would be fine if this is just an extra option and the default is the previous behaviour.

Copy link
Member

Choose a reason for hiding this comment

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

I agree that the name should be changed. I disagree that this is what users want 99% of the time. Most of the time, imshow() is exactly what those users really need, and is faster to boot. I have been in enough arguments with people about whether coordinates should represent vertices or midpoints to know that it is very dependent upon the particular field you are in and the standard data formats they are usually using most of the time.

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 0.001% of users expect data to be dropped. As for imshow that is useless for unevenly spaced X or Y.

X, Y, C = self._pcolorargs('pcolormesh', *args, allmatch=allmatch)
if dropdata is None:
dropdata = rcParams['pcolor.dropdata']

X, Y, C = self._pcolorargs('pcolormesh', *args,
allmatch=allmatch, dropdata=dropdata)
Ny, Nx = X.shape
X = X.ravel()
Y = Y.ravel()
Expand Down
13 changes: 7 additions & 6 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2515,11 +2515,11 @@ def minorticks_on():
@docstring.copy(Axes.pcolor)
def pcolor(
*args, alpha=None, norm=None, cmap=None, vmin=None,
vmax=None, data=None, **kwargs):
vmax=None, dropdata=None, data=None, **kwargs):
__ret = gca().pcolor(
*args, alpha=alpha, norm=norm, cmap=cmap, vmin=vmin,
vmax=vmax, **({"data": data} if data is not None else {}),
**kwargs)
vmax=vmax, dropdata=dropdata, **({"data": data} if data is not
None else {}), **kwargs)
sci(__ret)
return __ret

Expand All @@ -2528,12 +2528,13 @@ def pcolor(
@docstring.copy(Axes.pcolormesh)
def pcolormesh(
*args, alpha=None, norm=None, cmap=None, vmin=None,
vmax=None, shading='flat', antialiased=False, data=None,
**kwargs):
vmax=None, shading='flat', antialiased=False, dropdata=None,
data=None, **kwargs):
__ret = gca().pcolormesh(
*args, alpha=alpha, norm=norm, cmap=cmap, vmin=vmin,
vmax=vmax, shading=shading, antialiased=antialiased,
**({"data": data} if data is not None else {}), **kwargs)
dropdata=dropdata, **({"data": data} if data is not None else
{}), **kwargs)
sci(__ret)
return __ret

Expand Down
3 changes: 3 additions & 0 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,9 @@ def validate_webagg_address(s):
# marker props
'markers.fillstyle': ['full', validate_fillstyle],

## pcolor(mesh) props:
'pcolor.dropdata': [True, validate_bool],

## patch props
'patch.linewidth': [1.0, validate_float], # line width in points
'patch.edgecolor': ['black', validate_color],
Expand Down
46 changes: 46 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,52 @@ def test_pcolorargs():
ax.pcolormesh(x, y, Z[:-1, :-1])


@check_figures_equal(extensions=["png"])
def test_pcolornodropdata(fig_test, fig_ref):
ax = fig_test.subplots()
x = np.arange(0, 10)
y = np.arange(0, 3)
np.random.seed(19680801)
Z = np.random.randn(2, 9)
ax.pcolormesh(x, y, Z)

ax = fig_ref.subplots()
x2 = x[:-1] + np.diff(x) / 2
y2 = y[:-1] + np.diff(y) / 2
ax.pcolormesh(x2, y2, Z, dropdata=False)


@check_figures_equal(extensions=["png"])
def test_pcolornodropdatarc(fig_test, fig_ref):
matplotlib.rcParams['pcolor.dropdata'] = False
ax = fig_test.subplots()
x = np.arange(0, 10)
y = np.arange(0, 3)
np.random.seed(19680801)
Z = np.random.randn(2, 9)
ax.pcolormesh(x, y, Z)

ax = fig_ref.subplots()
x2 = x[:-1] + np.diff(x) / 2
y2 = y[:-1] + np.diff(y) / 2
ax.pcolormesh(x2, y2, Z)


@check_figures_equal(extensions=["png"])
def test_pcolordropdata(fig_test, fig_ref):
ax = fig_test.subplots()
x = np.arange(0, 10)
y = np.arange(0, 4)
np.random.seed(19680801)
Z = np.random.randn(3, 9)
ax.pcolormesh(x[:-1], y[:-1], Z[:-1, :-1])

ax = fig_ref.subplots()
x2 = x[:-1]
y2 = y[:-1]
ax.pcolormesh(x2, y2, Z)


@image_comparison(['canonical'])
def test_canonical():
fig, ax = plt.subplots()
Expand Down
1 change: 1 addition & 0 deletions matplotlibrc.template
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@

#markers.fillstyle : full ## {full, left, right, bottom, top, none}

# pcolor.dropdata : True

## ***************************************************************************
## * PATCHES *
Expand Down