Skip to content
Open
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
42 changes: 42 additions & 0 deletions doc/release/next_whats_new/pcolormesh_centered_gouraud.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Centered Gouraud shading in ``pcolormesh``
------------------------------------------

`~.Axes.pcolormesh` gained a new shading, ``centered-gouraud``.
Unlike ``gouraud``, which expects data values at the corners of
quadrilaterals, ``centered-gouraud`` operates on data defined at
the centers of the quadrilaterals.

With this addition, `~.Axes.pcolormesh` now provides
both discrete and continuous shading for each data semantics:

=============== =========== ====================
.. discrete continuous
=============== =========== ====================
data at corners ``nearest`` ``gouraud``
data at centers ``flat`` ``centered-gouraud``
=============== =========== ====================

Shadings can be switched within the same data semantics,
allowing switching between ``nearest`` and ``gouraud``,
and now also between ``flat`` and ``centered-gouraud``.

For example::

import matplotlib.pyplot as plt
import numpy as np

nrows = 3
ncols = 5
Z = np.arange(nrows * ncols).reshape(nrows, ncols)
x = np.arange(ncols + 1)
y = np.arange(nrows + 1)

fig, ax = plt.subplots()

ax.pcolormesh(x, y, Z, shading='flat')

# raises TypeError as X and Y are not the same shape as Z
# since Gouraud shading expects data at the corners of quadrilaterals
# ax.pcolormesh(x, y, Z, shading='gouraud')

ax.pcolormesh(x, y, Z, shading='centered-gouraud')
28 changes: 28 additions & 0 deletions galleries/examples/images_contours_and_fields/pcolormesh_grids.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,34 @@ def _annotate(ax, x, y, title):
ax.pcolormesh(x, y, Z, shading='gouraud', vmin=Z.min(), vmax=Z.max())
_annotate(ax, x, y, "shading='gouraud'; X, Y same shape as Z")

# %%
# Centered Gouraud Shading
# ------------------------
#
# In some cases, the user has data defined at the centers of quadrilaterals,
# for example when using ``shading='flat'``, and wants the continuous
# shading of Gouraud. ``shading='gouraud'`` does not support this and will
# raise an error, because it expects data at the corners of quadrilaterals,
# requiring *X* and *Y* to have the same shape as *Z*. The new Gouraud shading
# variant ``shading='centered-gouraud'`` provides this. It expects data at
# the centers of quadrilaterals, and therefore *X* and *Y* to be one dimension
# larger than *Z*. Internally, it converts the grid to match the shape of
# *Z* by replacing each quadrilateral with a single point at its center,
# computed as the average of its four corners.

fig, axs = plt.subplots(1, 2, layout='constrained')
ax = axs[0]
x = np.array([0, 1, 2, 3, 4, 5])
y = np.array([0, 1, 2, 3])
ax.pcolormesh(x, y, Z, shading='flat')
_annotate(ax, x, y, "shading='flat'")

ax = axs[1]
ax.pcolormesh(x, y, Z, shading='centered-gouraud')
nx = np.array([0.5, 1.5, 2.5, 3.5, 4.5])
ny = np.array([0.5, 1.5, 2.5])
_annotate(ax, nx, ny, "shading='centered-gouraud'")

plt.show()
# %%
#
Expand Down
35 changes: 28 additions & 7 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6393,7 +6393,7 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs):
# - reset shading if shading='auto' to flat or nearest
# depending on size;

_valid_shading = ['gouraud', 'nearest', 'flat', 'auto']
_valid_shading = ['centered-gouraud', 'gouraud', 'nearest', 'flat', 'auto']
try:
_api.check_in_list(_valid_shading, shading=shading)
except ValueError:
Expand All @@ -6405,8 +6405,10 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs):
if len(args) == 1:
C = np.asanyarray(args[0])
nrows, ncols = C.shape[:2]
if shading in ['gouraud', 'nearest']:
if shading in ['centered-gouraud', 'gouraud', 'nearest']:
X, Y = np.meshgrid(np.arange(ncols), np.arange(nrows))
if shading == 'centered-gouraud':
shading = 'gouraud'
else:
X, Y = np.meshgrid(np.arange(ncols + 1), np.arange(nrows + 1))
shading = 'flat'
Expand Down Expand Up @@ -6452,11 +6454,16 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs):
f" be one smaller than X({Nx}) and Y({Ny})"
f" while using shading='flat'"
f" see help({funcname})")
else: # ['nearest', 'gouraud']:
else: # ['nearest', 'gouraud', 'centered-gouraud']:
if (Nx, Ny) != (ncols, nrows):
raise TypeError('Dimensions of C %s are incompatible with'
' X (%d) and/or Y (%d); see help(%s)' % (
C.shape, Nx, Ny, funcname))
if shading == 'centered-gouraud' and (Nx, Ny) == (ncols + 1, nrows + 1):
# the center of each quad is the average of its four corners
X = 0.25 * (X[:-1, :-1] + X[:-1, 1:] + X[1:, 1:] + X[1:, :-1])
Y = 0.25 * (Y[:-1, :-1] + Y[:-1, 1:] + Y[1:, 1:] + Y[1:, :-1])
shading = 'gouraud'
else:
raise TypeError(f'Dimensions of C {C.shape} are incompatible with'
f' X ({Nx}) and/or Y ({Ny}); see help({funcname})')
if shading == 'nearest':
# grid is specified at the center, so define corners
# at the midpoints between the grid centers and then use the
Expand Down Expand Up @@ -6757,6 +6764,13 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
centered on ``(X[i, j], Y[i, j])``. For ``'gouraud'``, a smooth
interpolation is carried out between the quadrilateral corners.

If ``shading='centered-gouraud'`` the dimensions of *X* and *Y*
should be one greater than those of *C* or be the same as those of
*C*, otherwise a ValueError is raised. If the dimensions of the
grid are one greater, *X* and *Y* are modified to match the shape
of *C* In both cases a smooth interpolation is carried out
between the quadrilateral corners.

If *X* and/or *Y* are 1-D arrays or column vectors they will be
expanded as needed into the appropriate 2D arrays, making a
rectangular grid.
Expand All @@ -6783,7 +6797,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
alpha : float, default: None
The alpha blending value, between 0 (transparent) and 1 (opaque).

shading : {'flat', 'nearest', 'gouraud', 'auto'}, optional
shading : {'flat', 'nearest', 'gouraud', 'centered-gouraud', 'auto'}, optional
The fill style for the quadrilateral; defaults to
:rc:`pcolor.shading`. Possible values:

Expand All @@ -6801,6 +6815,13 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
the area in between is interpolated from the corner values.
The dimensions of *X* and *Y* must be the same as *C*. When
Gouraud shading is used, *edgecolors* is ignored.
- 'centered-gouraud': A ``'gouraud'`` variant that also supports
grids with dimensions one greater than those of *C*, which is
useful when switching from ``'flat'`` to ``'gouraud'`` without having
to change *X* and *Y*. In this case, the grid is converted to match
the shape of *C* by using the centers of the quadrilaterals, computed
as the average of their four corners. When *X* and *Y* have the same
dimensions as *C*, this is equivalent to ``'gouraud'`` shading.
- 'auto': Choose 'flat' if dimensions of *X* and *Y* are one
larger than *C*. Choose 'nearest' if dimensions are the same.

Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/axes/_axes.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ class Axes(_AxesBase):
vmin: float | None = ...,
vmax: float | None = ...,
colorizer: Colorizer | None = ...,
shading: Literal["flat", "nearest", "gouraud", "auto"] | None = ...,
shading: Literal["flat", "nearest", "gouraud", "centered-gouraud", "auto"] | None = ...,
antialiased: bool = ...,
data=...,
**kwargs
Expand Down
4 changes: 3 additions & 1 deletion lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3910,7 +3910,9 @@ def pcolormesh(
vmin: float | None = None,
vmax: float | None = None,
colorizer: Colorizer | None = None,
shading: Literal["flat", "nearest", "gouraud", "auto"] | None = None,
shading: (
Literal["flat", "nearest", "gouraud", "centered-gouraud", "auto"] | None
) = None,
antialiased: bool = False,
data=None,
**kwargs,
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,7 @@ def _convert_validator_spec(key, conv):
"markers.fillstyle": validate_fillstyle,

## pcolor(mesh) props:
"pcolor.shading": ["auto", "flat", "nearest", "gouraud"],
"pcolor.shading": ["auto", "flat", "nearest", "gouraud", "centered-gouraud"],
"pcolormesh.snap": validate_bool,

## patch props
Expand Down Expand Up @@ -1652,7 +1652,7 @@ class _Subsection:
_Param(
"pcolor.shading",
default="auto",
validator=["auto", "flat", "nearest", "gouraud"]
validator=["auto", "flat", "nearest", "gouraud", "centered-gouraud"]
),
_Param(
"pcolormesh.snap",
Expand Down
40 changes: 40 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1596,6 +1596,46 @@ def test_pcolor_log_scale(fig_test, fig_ref):
ax.set_xscale('log')


def test_pcolormesh_centered_gouraud():
nrows = 3
ncols = 4
C = np.arange(nrows * ncols).reshape(nrows, ncols)
x = np.arange(ncols + 1)
y = np.arange(nrows + 1)

_, ax = plt.subplots()

ax.pcolormesh(x, y, C, shading="flat")
with pytest.raises(TypeError):
ax.pcolormesh(x, y, C, shading="gouraud")
ax.pcolormesh(x, y, C, shading="centered-gouraud")

ax.pcolormesh(x[:-1], y[:-1], C, shading="gouraud")
ax.pcolormesh(x[:-1], y[:-1], C, shading="centered-gouraud")

with pytest.raises(TypeError):
ax.pcolormesh(x, y, C[:-1, :-1], shading="centered-gouraud")
with pytest.raises(TypeError):
ax.pcolormesh(x[:-2], y[:-2], C, shading="centered-gouraud")

ax.pcolormesh(C, shading="centered-gouraud")


@check_figures_equal()
def test_pcolormesh_centered_gouraud_matches_gouraud(fig_test, fig_ref):
C = np.arange(6).reshape(2, 3)

x_test = np.array([0, 2, 8, 12])
y_test = np.array([0, 2, 6])
ax_test = fig_test.subplots()
ax_test.pcolormesh(x_test, y_test, C, shading="centered-gouraud")

x_ref = np.array([1, 5, 10])
y_ref = np.array([1, 4])
ax_ref = fig_ref.subplots()
ax_ref.pcolormesh(x_ref, y_ref, C, shading="gouraud")


def test_pcolorargs():
n = 12
x = np.linspace(-1.5, 1.5, n)
Expand Down
Loading