Skip to content

Commit 15d8932

Browse files
committed
Add centered-gouraud shading to pcolormesh
Implement a variant of Gouraud shading that supports grids with dimensions one greater than the data in each dimension. Add tests and documentation. Update type hints and rcParams validation. Include a What's New entry. Fixes #8422
1 parent e6a833f commit 15d8932

File tree

7 files changed

+131
-11
lines changed

7 files changed

+131
-11
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Centered Gouraud Shading in pcolormesh
2+
--------------------------------------
3+
4+
Adds the ``centered-gouraud`` shading to ``pcolormesh``. This new
5+
shading is a variant of ``gouraud`` that also supports grids with
6+
dimensions one greater than the data in each dimension.
7+
8+
For example::
9+
10+
import matplotlib.pyplot as plt
11+
import numpy as np
12+
13+
nrows = 3
14+
ncols = 5
15+
Z = np.arange(nrows * ncols).reshape(nrows, ncols)
16+
x = np.arange(ncols + 1)
17+
y = np.arange(nrows + 1)
18+
19+
fig, ax = plt.subplots()
20+
21+
ax.pcolormesh(x, y, Z, shading='flat')
22+
23+
# raises TypeError as X and Y are not the same shape as Z
24+
# ax.pcolormesh(x, y, Z, shading='gouraud')
25+
26+
ax.pcolormesh(x, y, Z, shading='centered-gouraud')
27+
28+
will be able to switch between the two shadings without needing to
29+
change the grid, as the new shading automatically converts
30+
it to match the data dimensions.

galleries/examples/images_contours_and_fields/pcolormesh_grids.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,32 @@ def _annotate(ax, x, y, title):
116116
ax.pcolormesh(x, y, Z, shading='gouraud', vmin=Z.min(), vmax=Z.max())
117117
_annotate(ax, x, y, "shading='gouraud'; X, Y same shape as Z")
118118

119+
# %%
120+
# Centered Gouraud Shading
121+
# ------------------------
122+
#
123+
# In some cases, the user has a grid that is one larger than the data in each
124+
# dimension, for example when using ``shading='flat'``, and wants the smooth
125+
# color interpolation of Gouraud. While ``shading='gouraud'`` does not
126+
# support this and will raise an error, the Gouraud shading variant
127+
# ``shading='centered-gouraud'`` provides this. It allows *X* and *Y*
128+
# to be one dimension larger than *Z*, and internally converts
129+
# the grid to match the shape of *Z* by replacing each quadrilateral
130+
# with a single point at its center, computed as the average of its four corners.
131+
132+
fig, axs = plt.subplots(1, 2, layout='constrained')
133+
ax = axs[0]
134+
x = np.array([0, 1, 2, 3, 4, 5])
135+
y = np.array([0, 1, 2, 3])
136+
ax.pcolormesh(x, y, Z, shading='flat')
137+
_annotate(ax, x, y, "shading='flat'")
138+
139+
ax = axs[1]
140+
ax.pcolormesh(x, y, Z, shading='centered-gouraud')
141+
nx = np.array([0.5, 1.5, 2.5, 3.5, 4.5])
142+
ny = np.array([0.5, 1.5, 2.5])
143+
_annotate(ax, nx, ny, "shading='centered-gouraud'")
144+
119145
plt.show()
120146
# %%
121147
#

lib/matplotlib/axes/_axes.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6393,7 +6393,7 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs):
63936393
# - reset shading if shading='auto' to flat or nearest
63946394
# depending on size;
63956395

6396-
_valid_shading = ['gouraud', 'nearest', 'flat', 'auto']
6396+
_valid_shading = ['centered-gouraud', 'gouraud', 'nearest', 'flat', 'auto']
63976397
try:
63986398
_api.check_in_list(_valid_shading, shading=shading)
63996399
except ValueError:
@@ -6405,8 +6405,10 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs):
64056405
if len(args) == 1:
64066406
C = np.asanyarray(args[0])
64076407
nrows, ncols = C.shape[:2]
6408-
if shading in ['gouraud', 'nearest']:
6408+
if shading in ['centered-gouraud', 'gouraud', 'nearest']:
64096409
X, Y = np.meshgrid(np.arange(ncols), np.arange(nrows))
6410+
if shading == 'centered-gouraud':
6411+
shading = 'gouraud'
64106412
else:
64116413
X, Y = np.meshgrid(np.arange(ncols + 1), np.arange(nrows + 1))
64126414
shading = 'flat'
@@ -6452,11 +6454,17 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs):
64526454
f" be one smaller than X({Nx}) and Y({Ny})"
64536455
f" while using shading='flat'"
64546456
f" see help({funcname})")
6455-
else: # ['nearest', 'gouraud']:
6457+
else: # ['nearest', 'gouraud', 'centered-gouraud']:
64566458
if (Nx, Ny) != (ncols, nrows):
6457-
raise TypeError('Dimensions of C %s are incompatible with'
6458-
' X (%d) and/or Y (%d); see help(%s)' % (
6459-
C.shape, Nx, Ny, funcname))
6459+
if shading == 'centered-gouraud' and (Nx, Ny) == (ncols + 1, nrows + 1):
6460+
# the center of each quad is the average of its four corners
6461+
X = 0.25 * (X[:-1, :-1] + X[:-1, 1:] + X[1:, 1:] + X[1:, :-1])
6462+
Y = 0.25 * (Y[:-1, :-1] + Y[:-1, 1:] + Y[1:, 1:] + Y[1:, :-1])
6463+
shading = 'gouraud'
6464+
else:
6465+
raise TypeError('Dimensions of C %s are incompatible with'
6466+
' X (%d) and/or Y (%d); see help(%s)' % (
6467+
C.shape, Nx, Ny, funcname))
64606468
if shading == 'nearest':
64616469
# grid is specified at the center, so define corners
64626470
# at the midpoints between the grid centers and then use the
@@ -6757,6 +6765,13 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
67576765
centered on ``(X[i, j], Y[i, j])``. For ``'gouraud'``, a smooth
67586766
interpolation is carried out between the quadrilateral corners.
67596767
6768+
If ``shading='centered-gouraud'`` the dimensions of *X* and *Y*
6769+
should be one greater than those of *C* or be the same as those of
6770+
*C*, otherwise a ValueError is raised. If the dimensions of the
6771+
grid are one greater, *X* and *Y* are modified to match the shape
6772+
of *C* In both cases a smooth interpolation is carried out
6773+
between the quadrilateral corners.
6774+
67606775
If *X* and/or *Y* are 1-D arrays or column vectors they will be
67616776
expanded as needed into the appropriate 2D arrays, making a
67626777
rectangular grid.
@@ -6783,7 +6798,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
67836798
alpha : float, default: None
67846799
The alpha blending value, between 0 (transparent) and 1 (opaque).
67856800
6786-
shading : {'flat', 'nearest', 'gouraud', 'auto'}, optional
6801+
shading : {'flat', 'nearest', 'gouraud', 'centered-gouraud', 'auto'}, optional
67876802
The fill style for the quadrilateral; defaults to
67886803
:rc:`pcolor.shading`. Possible values:
67896804
@@ -6801,6 +6816,13 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
68016816
the area in between is interpolated from the corner values.
68026817
The dimensions of *X* and *Y* must be the same as *C*. When
68036818
Gouraud shading is used, *edgecolors* is ignored.
6819+
- 'centered-gouraud': A ``'gouraud'`` variant that also supports
6820+
grids with dimensions one greater than those of *C*, which is
6821+
useful when switching from ``'flat'`` to ``'gouraud'`` without having
6822+
to change *X* and *Y*. In this case, the grid is converted to match
6823+
the shape of *C* by using the centers of the quadrilaterals, computed
6824+
as the average of their four corners. When *X* and *Y* have the same
6825+
dimensions as *C*, this is equivalent to ``'gouraud'`` shading.
68046826
- 'auto': Choose 'flat' if dimensions of *X* and *Y* are one
68056827
larger than *C*. Choose 'nearest' if dimensions are the same.
68066828

lib/matplotlib/axes/_axes.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ class Axes(_AxesBase):
541541
vmin: float | None = ...,
542542
vmax: float | None = ...,
543543
colorizer: Colorizer | None = ...,
544-
shading: Literal["flat", "nearest", "gouraud", "auto"] | None = ...,
544+
shading: Literal["flat", "nearest", "gouraud", "centered-gouraud", "auto"] | None = ...,
545545
antialiased: bool = ...,
546546
data=...,
547547
**kwargs

lib/matplotlib/pyplot.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3910,7 +3910,9 @@ def pcolormesh(
39103910
vmin: float | None = None,
39113911
vmax: float | None = None,
39123912
colorizer: Colorizer | None = None,
3913-
shading: Literal["flat", "nearest", "gouraud", "auto"] | None = None,
3913+
shading: (
3914+
Literal["flat", "nearest", "gouraud", "centered-gouraud", "auto"] | None
3915+
) = None,
39143916
antialiased: bool = False,
39153917
data=None,
39163918
**kwargs,

lib/matplotlib/rcsetup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -995,7 +995,7 @@ def _convert_validator_spec(key, conv):
995995
"markers.fillstyle": validate_fillstyle,
996996

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

10011001
## patch props
@@ -1652,7 +1652,7 @@ class _Subsection:
16521652
_Param(
16531653
"pcolor.shading",
16541654
default="auto",
1655-
validator=["auto", "flat", "nearest", "gouraud"]
1655+
validator=["auto", "flat", "nearest", "gouraud", "centered-gouraud"]
16561656
),
16571657
_Param(
16581658
"pcolormesh.snap",

lib/matplotlib/tests/test_axes.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,6 +1596,46 @@ def test_pcolor_log_scale(fig_test, fig_ref):
15961596
ax.set_xscale('log')
15971597

15981598

1599+
def test_pcolormesh_centered_gouraud():
1600+
nrows = 3
1601+
ncols = 4
1602+
C = np.arange(nrows * ncols).reshape(nrows, ncols)
1603+
x = np.arange(ncols + 1)
1604+
y = np.arange(nrows + 1)
1605+
1606+
_, ax = plt.subplots()
1607+
1608+
ax.pcolormesh(x, y, C, shading="flat")
1609+
with pytest.raises(TypeError):
1610+
ax.pcolormesh(x, y, C, shading="gouraud")
1611+
ax.pcolormesh(x, y, C, shading="centered-gouraud")
1612+
1613+
ax.pcolormesh(x[:-1], y[:-1], C, shading="gouraud")
1614+
ax.pcolormesh(x[:-1], y[:-1], C, shading="centered-gouraud")
1615+
1616+
with pytest.raises(TypeError):
1617+
ax.pcolormesh(x, y, C[:-1, :-1], shading="centered-gouraud")
1618+
with pytest.raises(TypeError):
1619+
ax.pcolormesh(x[:-2], y[:-2], C, shading="centered-gouraud")
1620+
1621+
ax.pcolormesh(C, shading="centered-gouraud")
1622+
1623+
1624+
@check_figures_equal()
1625+
def test_pcolormesh_centered_gouraud_matches_gouraud(fig_test, fig_ref):
1626+
C = np.arange(6).reshape(2, 3)
1627+
1628+
x_test = np.array([0, 2, 8, 12])
1629+
y_test = np.array([0, 2, 6])
1630+
ax_test = fig_test.subplots()
1631+
ax_test.pcolormesh(x_test, y_test, C, shading="centered-gouraud")
1632+
1633+
refX = np.array([1, 5, 10])
1634+
refY = np.array([1, 4])
1635+
ax_ref = fig_ref.subplots()
1636+
ax_ref.pcolormesh(refX, refY, C, shading="gouraud")
1637+
1638+
15991639
def test_pcolorargs():
16001640
n = 12
16011641
x = np.linspace(-1.5, 1.5, n)

0 commit comments

Comments
 (0)