Skip to content

Commit 50eae5b

Browse files
committed
ENH: Support mixed shading in pcolor/pcolormesh (Closes #31607)
- Allow specifying shading style individually for x and y axes as a 2-tuple. - Resolve 'auto' individually per axis. - Added validation for 2-tuple configurations in rcsetup. - Added comprehensive tests covering correct resolution, shape checks, and error cases.
1 parent ca8df27 commit 50eae5b

4 files changed

Lines changed: 245 additions & 68 deletions

File tree

lib/matplotlib/axes/_axes.py

Lines changed: 126 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -6462,23 +6462,52 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs):
64626462
# - reset shading if shading='auto' to flat or nearest
64636463
# depending on size;
64646464

6465+
# Check and normalize shading parameter:
6466+
if isinstance(shading, str):
6467+
shading = shading.lower()
6468+
shading_x = shading_y = shading
6469+
else:
6470+
try:
6471+
shading_x, shading_y = shading
6472+
if isinstance(shading_x, str):
6473+
shading_x = shading_x.lower()
6474+
if isinstance(shading_y, str):
6475+
shading_y = shading_y.lower()
6476+
except (ValueError, TypeError) as e:
6477+
raise ValueError(
6478+
"shading must be a string or a 2-tuple of strings"
6479+
) from e
6480+
64656481
_valid_shading = ['gouraud', 'nearest', 'flat', 'auto']
6466-
try:
6467-
_api.check_in_list(_valid_shading, shading=shading)
6468-
except ValueError:
6469-
_api.warn_external(f"shading value '{shading}' not in list of "
6470-
f"valid values {_valid_shading}. Setting "
6471-
"shading='auto'.")
6472-
shading = 'auto'
6482+
_api.check_in_list(_valid_shading, shading_x=shading_x)
6483+
_api.check_in_list(_valid_shading, shading_y=shading_y)
6484+
6485+
if (
6486+
(shading_x == 'gouraud' or shading_y == 'gouraud')
6487+
and shading_x != shading_y
6488+
):
6489+
raise ValueError(
6490+
"shading='gouraud' cannot be mixed with other shading types."
6491+
)
64736492

64746493
if len(args) == 1:
64756494
C = np.asanyarray(args[0])
64766495
nrows, ncols = C.shape[:2]
6477-
if shading in ['gouraud', 'nearest']:
6478-
X, Y = np.meshgrid(np.arange(ncols), np.arange(nrows))
6496+
6497+
grid_shading_x = 'flat' if shading_x == 'auto' else shading_x
6498+
grid_shading_y = 'flat' if shading_y == 'auto' else shading_y
6499+
6500+
if grid_shading_x in ['gouraud', 'nearest']:
6501+
x_grid = np.arange(ncols)
64796502
else:
6480-
X, Y = np.meshgrid(np.arange(ncols + 1), np.arange(nrows + 1))
6481-
shading = 'flat'
6503+
x_grid = np.arange(ncols + 1)
6504+
6505+
if grid_shading_y in ['gouraud', 'nearest']:
6506+
y_grid = np.arange(nrows)
6507+
else:
6508+
y_grid = np.arange(nrows + 1)
6509+
6510+
X, Y = np.meshgrid(x_grid, y_grid)
64826511
elif len(args) == 3:
64836512
# Check x and y for bad data...
64846513
C = np.asanyarray(args[2])
@@ -6509,61 +6538,81 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs):
65096538
raise TypeError(f'Incompatible X, Y inputs to {funcname}; '
65106539
f'see help({funcname})')
65116540

6512-
if shading == 'auto':
6513-
if ncols == Nx and nrows == Ny:
6514-
shading = 'nearest'
6541+
if shading_x == 'auto':
6542+
if ncols == Nx:
6543+
shading_x = 'nearest'
65156544
else:
6516-
shading = 'flat'
6517-
6518-
if shading == 'flat':
6519-
if (Nx, Ny) != (ncols + 1, nrows + 1):
6520-
raise TypeError(f"Dimensions of C {C.shape} should"
6521-
f" be one smaller than X({Nx}) and Y({Ny})"
6522-
f" while using shading='flat'"
6523-
f" see help({funcname})")
6524-
else: # ['nearest', 'gouraud']:
6545+
shading_x = 'flat'
6546+
if shading_y == 'auto':
6547+
if nrows == Ny:
6548+
shading_y = 'nearest'
6549+
else:
6550+
shading_y = 'flat'
6551+
6552+
if shading_x == 'gouraud':
65256553
if (Nx, Ny) != (ncols, nrows):
65266554
raise TypeError('Dimensions of C %s are incompatible with'
65276555
' X (%d) and/or Y (%d); see help(%s)' % (
65286556
C.shape, Nx, Ny, funcname))
6529-
if shading == 'nearest':
6530-
# grid is specified at the center, so define corners
6531-
# at the midpoints between the grid centers and then use the
6532-
# flat algorithm.
6533-
def _interp_grid(X, require_monotonicity=False):
6534-
# helper for below. To ensure the cell edges are calculated
6535-
# correctly, when expanding columns, the monotonicity of
6536-
# X coords needs to be checked. When expanding rows, the
6537-
# monotonicity of Y coords needs to be checked.
6538-
if np.shape(X)[1] > 1:
6539-
dX = np.diff(X, axis=1) * 0.5
6540-
if (require_monotonicity and
6541-
not (np.all(dX >= 0) or np.all(dX <= 0))):
6542-
_api.warn_external(
6543-
f"The input coordinates to {funcname} are "
6544-
"interpreted as cell centers, but are not "
6545-
"monotonically increasing or decreasing. "
6546-
"This may lead to incorrectly calculated cell "
6547-
"edges, in which case, please supply "
6548-
f"explicit cell edges to {funcname}.")
6549-
6550-
hstack = np.ma.hstack if np.ma.isMA(X) else np.hstack
6551-
X = hstack((X[:, [0]] - dX[:, [0]],
6552-
X[:, :-1] + dX,
6553-
X[:, [-1]] + dX[:, [-1]]))
6554-
else:
6555-
# This is just degenerate, but we can't reliably guess
6556-
# a dX if there is just one value.
6557-
X = np.hstack((X, X))
6558-
return X
6559-
6560-
if ncols == Nx:
6561-
X = _interp_grid(X, require_monotonicity=True)
6562-
Y = _interp_grid(Y)
6563-
if nrows == Ny:
6564-
X = _interp_grid(X.T).T
6565-
Y = _interp_grid(Y.T, require_monotonicity=True).T
6566-
shading = 'flat'
6557+
shading = 'gouraud'
6558+
else:
6559+
if shading_x == 'flat' and Nx != ncols + 1:
6560+
raise TypeError(f"Dimensions of C {C.shape} should be one smaller than "
6561+
f"X ({Nx}) along the X-axis while using shading='flat'")
6562+
if shading_x == 'nearest' and Nx != ncols:
6563+
raise TypeError(
6564+
f"Dimensions of C {C.shape} should be equal to "
6565+
f"X ({Nx}) along the X-axis while using "
6566+
"shading='nearest'"
6567+
)
6568+
6569+
if shading_y == 'flat' and Ny != nrows + 1:
6570+
raise TypeError(f"Dimensions of C {C.shape} should be one smaller than "
6571+
f"Y ({Ny}) along the Y-axis while using shading='flat'")
6572+
if shading_y == 'nearest' and Ny != nrows:
6573+
raise TypeError(
6574+
f"Dimensions of C {C.shape} should be equal to "
6575+
f"Y ({Ny}) along the Y-axis while using "
6576+
"shading='nearest'"
6577+
)
6578+
6579+
# grid is specified at the center, so define corners
6580+
# at the midpoints between the grid centers and then use the
6581+
# flat algorithm.
6582+
def _interp_grid(X, require_monotonicity=False):
6583+
# helper for below. To ensure the cell edges are calculated
6584+
# correctly, when expanding columns, the monotonicity of
6585+
# X coords needs to be checked. When expanding rows, the
6586+
# monotonicity of Y coords needs to be checked.
6587+
if np.shape(X)[1] > 1:
6588+
dX = np.diff(X, axis=1) * 0.5
6589+
if (require_monotonicity and
6590+
not (np.all(dX >= 0) or np.all(dX <= 0))):
6591+
_api.warn_external(
6592+
f"The input coordinates to {funcname} are "
6593+
"interpreted as cell centers, but are not "
6594+
"monotonically increasing or decreasing. "
6595+
"This may lead to incorrectly calculated cell "
6596+
"edges, in which case, please supply "
6597+
f"explicit cell edges to {funcname}.")
6598+
6599+
hstack = np.ma.hstack if np.ma.isMA(X) else np.hstack
6600+
X = hstack((X[:, [0]] - dX[:, [0]],
6601+
X[:, :-1] + dX,
6602+
X[:, [-1]] + dX[:, [-1]]))
6603+
else:
6604+
# This is just degenerate, but we can't reliably guess
6605+
# a dX if there is just one value.
6606+
X = np.hstack((X, X))
6607+
return X
6608+
6609+
if shading_x == 'nearest':
6610+
X = _interp_grid(X, require_monotonicity=True)
6611+
Y = _interp_grid(Y)
6612+
if shading_y == 'nearest':
6613+
X = _interp_grid(X.T).T
6614+
Y = _interp_grid(Y.T, require_monotonicity=True).T
6615+
shading = 'flat'
65676616

65686617
C = cbook.safe_masked_invalid(C, copy=True)
65696618
return X, Y, C, shading
@@ -6623,7 +6672,7 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None,
66236672
expanded as needed into the appropriate 2D arrays, making a
66246673
rectangular grid.
66256674
6626-
shading : {'flat', 'nearest', 'auto'}, default: :rc:`pcolor.shading`
6675+
shading : {'flat', 'nearest', 'auto'} or 2-tuple, default: :rc:`pcolor.shading`
66276676
The fill style for the quadrilateral. Possible values:
66286677
66296678
- 'flat': A solid color is used for each quad. The color of the
@@ -6636,6 +6685,10 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None,
66366685
- 'auto': Choose 'flat' if dimensions of *X* and *Y* are one
66376686
larger than *C*. Choose 'nearest' if dimensions are the same.
66386687
6688+
Additionally, a 2-tuple of strings `(shading_x, shading_y)` can be
6689+
passed to specify different shading types for the X and Y axes
6690+
individually.
6691+
66396692
See :doc:`/gallery/images_contours_and_fields/pcolormesh_grids`
66406693
for more description.
66416694
@@ -6717,7 +6770,8 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None,
67176770

67186771
if shading is None:
67196772
shading = mpl.rcParams['pcolor.shading']
6720-
shading = shading.lower()
6773+
if isinstance(shading, str):
6774+
shading = shading.lower()
67216775
X, Y, C, shading = self._pcolorargs('pcolor', *args, shading=shading,
67226776
kwargs=kwargs)
67236777
linewidths = (0.25,)
@@ -6850,7 +6904,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
68506904
alpha : float, default: None
68516905
The alpha blending value, between 0 (transparent) and 1 (opaque).
68526906
6853-
shading : {'flat', 'nearest', 'gouraud', 'auto'}, optional
6907+
shading : {'flat', 'nearest', 'gouraud', 'auto'} or 2-tuple, optional
68546908
The fill style for the quadrilateral; defaults to
68556909
:rc:`pcolor.shading`. Possible values:
68566910
@@ -6869,6 +6923,11 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
68696923
- 'auto': Choose 'flat' if dimensions of *X* and *Y* are one
68706924
larger than *C*. Choose 'nearest' if dimensions are the same.
68716925
6926+
Additionally, a 2-tuple of strings `(shading_x, shading_y)` can be
6927+
passed to specify different shading types for the X and Y axes
6928+
individually. Note that 'gouraud' cannot be mixed with other shading
6929+
types.
6930+
68726931
See :doc:`/gallery/images_contours_and_fields/pcolormesh_grids`
68736932
for more description.
68746933
@@ -6953,7 +7012,9 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None,
69537012
`~.Axes.pcolormesh`, which is not available with `~.Axes.pcolor`.
69547013
69557014
"""
6956-
shading = mpl._val_or_rc(shading, 'pcolor.shading').lower()
7015+
shading = mpl._val_or_rc(shading, 'pcolor.shading')
7016+
if isinstance(shading, str):
7017+
shading = shading.lower()
69577018
kwargs.setdefault('edgecolors', 'none')
69587019

69597020
X, Y, C, shading = self._pcolorargs('pcolormesh', *args,

lib/matplotlib/axes/_axes.pyi

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,12 @@ class Axes(_AxesBase):
526526
def pcolor(
527527
self,
528528
*args: ArrayLike,
529-
shading: Literal["flat", "nearest", "auto"] | None = ...,
529+
shading: Literal["flat", "nearest", "auto"]
530+
| tuple[
531+
Literal["flat", "nearest", "auto"],
532+
Literal["flat", "nearest", "auto"],
533+
]
534+
| None = ...,
530535
alpha: float | None = ...,
531536
norm: str | Normalize | None = ...,
532537
cmap: str | Colormap | None = ...,
@@ -545,7 +550,12 @@ class Axes(_AxesBase):
545550
vmin: float | None = ...,
546551
vmax: float | None = ...,
547552
colorizer: Colorizer | None = ...,
548-
shading: Literal["flat", "nearest", "gouraud", "auto"] | None = ...,
553+
shading: Literal["flat", "nearest", "gouraud", "auto"]
554+
| tuple[
555+
Literal["flat", "nearest", "gouraud", "auto"],
556+
Literal["flat", "nearest", "gouraud", "auto"],
557+
]
558+
| None = ...,
549559
antialiased: bool = ...,
550560
data: DataParamType = ...,
551561
**kwargs

lib/matplotlib/rcsetup.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,46 @@ def validate_hist_bins(s):
979979
" a sequence of floats")
980980

981981

982+
def validate_pcolor_shading(s):
983+
valid = ["auto", "flat", "nearest", "gouraud"]
984+
if isinstance(s, str):
985+
s = s.lower()
986+
if s in valid:
987+
return s
988+
raise ValueError(f"shading {s!r} must be one of {valid}")
989+
# Otherwise, check if it's an iterable of size 2
990+
try:
991+
s_tuple = tuple(s)
992+
except TypeError as e:
993+
raise ValueError(
994+
f"shading {s!r} must be a string or a 2-tuple of strings"
995+
) from e
996+
if len(s_tuple) != 2:
997+
raise ValueError(
998+
f"shading {s!r} must be a string or a 2-tuple of strings"
999+
)
1000+
1001+
normalized = []
1002+
for item in s_tuple:
1003+
if isinstance(item, str):
1004+
item = item.lower()
1005+
if item in valid:
1006+
normalized.append(item)
1007+
continue
1008+
raise ValueError(
1009+
f"shading components must be one of {valid}, got {item!r}"
1010+
)
1011+
if (
1012+
(normalized[0] == 'gouraud' or normalized[1] == 'gouraud')
1013+
and normalized[0] != normalized[1]
1014+
):
1015+
raise ValueError(
1016+
"shading='gouraud' cannot be mixed with other shading types."
1017+
)
1018+
return tuple(normalized)
1019+
1020+
1021+
9821022
class _ignorecase(list):
9831023
"""A marker class indicating that a list-of-str is case-insensitive."""
9841024

@@ -1032,7 +1072,7 @@ def _convert_validator_spec(key, conv):
10321072
"markers.fillstyle": validate_fillstyle,
10331073

10341074
## pcolor(mesh) props:
1035-
"pcolor.shading": ["auto", "flat", "nearest", "gouraud"],
1075+
"pcolor.shading": validate_pcolor_shading,
10361076
"pcolormesh.snap": validate_bool,
10371077

10381078
## patch props

0 commit comments

Comments
 (0)