diff --git a/doc/release/next_whats_new/pcolormesh_gouraud.rst b/doc/release/next_whats_new/pcolormesh_gouraud.rst new file mode 100644 index 000000000000..089e4a8bc639 --- /dev/null +++ b/doc/release/next_whats_new/pcolormesh_gouraud.rst @@ -0,0 +1,52 @@ +``gouraud`` shading supports data at quadrilaterals centers +----------------------------------------------------------- + +`~.Axes.pcolormesh` previously required data at the corners of +quadrilaterals for ``gouraud`` shading. It now also supports data +defined at the centers of quadrilaterals. + +This now allows `~.Axes.pcolormesh` to provide both constant and +linearly interpolated shading for each data location. + +=============== ======== ===================== +.. constant linearly interpolated +=============== ======== ===================== +data at corners nearest gouraud +data at centers flat gouraud +=============== ======== ===================== + +Shading can be switched for the same data location, allowing +switches between ``nearest`` and ``gouraud``, and now also +between ``flat`` and ``gouraud``. + +For example: + +.. plot:: + :include-source: true + :alt: Switching between constant and linearly interpolated shading for data at corners and centers. + + import matplotlib.pyplot as plt + import numpy as np + + nrows, ncols = 3, 5 + Z = np.arange(nrows * ncols).reshape(nrows, ncols) + x = np.arange(ncols + 1) + y = np.arange(nrows + 1) + + fig, axs = plt.subplots(2, 2, layout='constrained') + + # Data at corners, requires X and Y the same shape as Z. + axs[0, 0].pcolormesh(x[:-1], y[:-1], Z, shading='nearest') + axs[0, 0].set_title('nearest: X, Y, Z same shape') + + axs[0, 1].pcolormesh(x[:-1], y[:-1], Z, shading='gouraud') + axs[0, 1].set_title('gouraud: X, Y, Z same shape') + + # Data at centers, requires X and Y one larger than Z. + axs[1, 0].pcolormesh(x, y, Z, shading='flat') + axs[1, 0].set_title('flat: X, Y one larger than Z') + + axs[1, 1].pcolormesh(x, y, Z, shading='gouraud') + axs[1, 1].set_title('gouraud: X, Y one larger than Z') + + plt.show() diff --git a/galleries/examples/images_contours_and_fields/pcolormesh_grids.py b/galleries/examples/images_contours_and_fields/pcolormesh_grids.py index 212b807dbf90..506ddbcb0aa6 100644 --- a/galleries/examples/images_contours_and_fields/pcolormesh_grids.py +++ b/galleries/examples/images_contours_and_fields/pcolormesh_grids.py @@ -108,7 +108,8 @@ def _annotate(ax, x, y, title): # # `Gouraud shading `_ can also # be specified, where the color in the quadrilaterals is linearly interpolated -# between the grid points. The shapes of *X*, *Y*, *Z* must be the same. +# between the grid points. The data is specified at the corners of the +# quadrilaterals, in which case *X*, *Y* and *Z* are all the same shape. fig, ax = plt.subplots(layout='constrained') x = np.arange(ncols) @@ -116,6 +117,21 @@ 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") +# %% +# Gouraud Shading, one larger grid +# -------------------------------- +# +# In some cases, the user has data defined at the centers of the quadrilaterals +# with *X* and *Y* one larger than *Z*. ``shading='gouraud'`` also supports +# this by using the grid quadrilateral centers as the corners of each colored +# quadrilateral. + +fig, ax = plt.subplots(layout='constrained') +x = np.arange(ncols + 1) +y = np.arange(nrows + 1) +ax.pcolormesh(x, y, Z, shading='gouraud', vmin=Z.min(), vmax=Z.max()) +_annotate(ax, x, y, "shading='gouraud'; X, Y one larger than Z") + plt.show() # %% # diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 6102fd3d3ed3..15e9d70faca3 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6523,9 +6523,13 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs): f" see help({funcname})") else: # ['nearest', '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 == '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]) + 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 @@ -6818,11 +6822,17 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, greater than those of *C*, otherwise a TypeError is raised. The quadrilateral is colored due to the value at ``C[i, j]``. - If ``shading='nearest'`` or ``'gouraud'``, the dimensions of *X* - and *Y* should be the same as those of *C* (if not, a TypeError - will be raised). For ``'nearest'`` the color ``C[i, j]`` is - centered on ``(X[i, j], Y[i, j])``. For ``'gouraud'``, a smooth - interpolation is carried out between the quadrilateral corners. + If ``shading='nearest'`` the dimensions of *X* and *Y* should be + the same as those of *C*, otherwise a TypeError is raised. The + color ``C[i, j]`` is centered on ``(X[i, j], Y[i, j])``. + + If ``shading='gouraud'`` the dimensions of *X* and *Y* should be + the same as those of *C* or be one greater than those of *C*, + otherwise a TypeError is raised. If the dimensions of *X* and *Y* + are one greater, they are internally converted to match the shape + of *C* by replacing each quadrilateral with a point at its center, + computed as the average of their four corners. 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 @@ -6861,11 +6871,16 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, - 'nearest': Each grid point will have a color centered on it, extending halfway between the adjacent grid centers. The dimensions of *X* and *Y* must be the same as *C*. - - 'gouraud': Each quad will be Gouraud shaded: The color of the - corners (i', j') are given by ``C[i', j']``. The color values of - 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. + - 'gouraud': If the mesh data is defined at the corners of grid + quadrilaterals, with *X*, *Y* and *C* having the same dimensions, + each grid quad will be Gouraud shaded. If the color values + are specified at the centers of grid quadrilaterals, *X* and *Y* + have dimensions one greater than those of *C*, and each colored + quadrilateral will use the grid quad centers as its corners, so + that it can be Gouraud shaded: The color of the corners (i', j') + are given by ``C[i', j']``, and the color values of the area + in between are interpolated from the corner values. + When Gouraud shading is used, *edgecolors* is ignored. - 'auto': Choose 'flat' if dimensions of *X* and *Y* are one larger than *C*. Choose 'nearest' if dimensions are the same. diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 2e3abec8363e..ac3653f031b5 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1623,6 +1623,40 @@ def test_pcolor_log_scale(fig_test, fig_ref): ax.set_xscale('log') +def test_pcolormesh_switching_shadings(): + nrows, ncols = 3, 4 + Z = np.arange(nrows * ncols).reshape(nrows, ncols) + x = np.arange(ncols + 1) + y = np.arange(nrows + 1) + + _, ax = plt.subplots() + + ax.pcolormesh(x, y, Z, shading='flat') + ax.pcolormesh(x, y, Z, shading='gouraud') + with pytest.raises(TypeError): + ax.pcolormesh(x, y, Z, shading='nearest') + + ax.pcolormesh(x[:-1], y[:-1], Z, shading='nearest') + ax.pcolormesh(x[:-1], y[:-1], Z, shading='gouraud') + with pytest.raises(TypeError): + ax.pcolormesh(x[:-1], y[:-1], Z, shading='flat') + + +@check_figures_equal() +def test_pcolormesh_gouraud_grid_conversion(fig_test, fig_ref): + Z = 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, Z, shading='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, Z, shading='gouraud') + + def test_pcolorargs(): n = 12 x = np.linspace(-1.5, 1.5, n) @@ -1636,9 +1670,9 @@ def test_pcolorargs(): with pytest.raises(TypeError): ax.pcolormesh(X, Y, Z.T) with pytest.raises(TypeError): - ax.pcolormesh(x, y, Z[:-1, :-1], shading="gouraud") + ax.pcolormesh(x, y, Z[:-2, :-2], shading='gouraud') with pytest.raises(TypeError): - ax.pcolormesh(X, Y, Z[:-1, :-1], shading="gouraud") + ax.pcolormesh(X, Y, Z[:-2, :-2], shading='gouraud') x[0] = np.nan with pytest.raises(ValueError): ax.pcolormesh(x, y, Z[:-1, :-1])