diff --git a/doc/api/next_api_changes/deprecations/25027-GL.rst b/doc/api/next_api_changes/deprecations/25027-GL.rst new file mode 100644 index 000000000000..30137ee091df --- /dev/null +++ b/doc/api/next_api_changes/deprecations/25027-GL.rst @@ -0,0 +1,12 @@ +The object returned by ``pcolor()`` has changed to a ``PolyQuadMesh`` class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The old object was a `.PolyCollection` with flattened vertices and array data. +The new `.PolyQuadMesh` class subclasses `.PolyCollection`, but adds in better +2D coordinate and array handling in alignment with `.QuadMesh`. Previously, if +a masked array was input, the list of polygons within the collection would shrink +to the size of valid polygons and users were required to keep track of which +polygons were drawn and call ``set_array()`` with the smaller "compressed" array size. +Passing the "compressed" and flattened array values is now deprecated and the +full 2D array of values (including the mask) should be passed +to `.PolyQuadMesh.set_array`. diff --git a/doc/missing-references.json b/doc/missing-references.json index 0e33c97dab1d..1061b08b7fe6 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -23,11 +23,11 @@ "lib/matplotlib/colorbar.py:docstring of matplotlib.colorbar:1" ], "matplotlib.axes.Axes.patch": [ - "doc/tutorials/intermediate/artists.rst:184", - "doc/tutorials/intermediate/artists.rst:423" + "doc/tutorials/artists.rst:177", + "doc/tutorials/artists.rst:405" ], "matplotlib.axes.Axes.patches": [ - "doc/tutorials/intermediate/artists.rst:461" + "doc/tutorials/artists.rst:443" ], "matplotlib.axes.Axes.transAxes": [ "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredDirectionArrows:4" @@ -38,23 +38,25 @@ "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredSizeBar:4" ], "matplotlib.axes.Axes.xaxis": [ - "doc/tutorials/intermediate/artists.rst:607" + "doc/tutorials/artists.rst:589", + "doc/users/explain/axes/index.rst:133" ], "matplotlib.axes.Axes.yaxis": [ - "doc/tutorials/intermediate/artists.rst:607" + "doc/tutorials/artists.rst:589", + "doc/users/explain/axes/index.rst:133" ], "matplotlib.axis.Axis.label": [ - "doc/tutorials/intermediate/artists.rst:654" + "doc/tutorials/artists.rst:636" ], "matplotlib.colors.Colormap.name": [ "lib/matplotlib/cm.py:docstring of matplotlib.cm:10" ], "matplotlib.figure.Figure.patch": [ - "doc/tutorials/intermediate/artists.rst:184", - "doc/tutorials/intermediate/artists.rst:317" + "doc/tutorials/artists.rst:177", + "doc/tutorials/artists.rst:310" ], "matplotlib.figure.Figure.transFigure": [ - "doc/tutorials/intermediate/artists.rst:366" + "doc/tutorials/artists.rst:359" ], "max": [ "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:1" @@ -106,6 +108,7 @@ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.boxplot:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.clabel:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.csd:1", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.ecdf:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.errorbar:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.eventplot:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figimage:1", @@ -130,22 +133,13 @@ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.specgram:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.spy:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.stairs:1", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.stem:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.step:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.subplot_mosaic:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.thetagrids:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.violinplot:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.vlines:1" ], - "Axes": [ - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.axes:1", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.colorbar:1", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.delaxes:1", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.subplot2grid:1", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.subplot_mosaic:1", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.subplots:1", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.twinx:1", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.twiny:1" - ], "ColorType": [ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.errorbar:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.eventplot:1", @@ -166,9 +160,6 @@ "MarkerType": [ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.scatter:1" ], - "SubplotBase": [ - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.subplots:1" - ], "_AxesBase": [ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.twinx:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.twiny:1" @@ -177,13 +168,13 @@ "doc/api/artist_api.rst:202" ], "matplotlib.backend_bases.FigureCanvas": [ - "doc/tutorials/intermediate/artists.rst:32", - "doc/tutorials/intermediate/artists.rst:34", - "doc/tutorials/intermediate/artists.rst:39" + "doc/tutorials/artists.rst:36", + "doc/tutorials/artists.rst:38", + "doc/tutorials/artists.rst:43" ], "matplotlib.backend_bases.Renderer": [ - "doc/tutorials/intermediate/artists.rst:34", - "doc/tutorials/intermediate/artists.rst:39" + "doc/tutorials/artists.rst:38", + "doc/tutorials/artists.rst:43" ], "matplotlib.backend_bases._Backend": [ "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases:1" @@ -212,6 +203,11 @@ "doc/api/collections_api.rst:13", "lib/matplotlib/collections.py:docstring of matplotlib.collections:1" ], + "matplotlib.collections._MeshData": [ + "doc/api/artist_api.rst:202", + "doc/api/collections_api.rst:13", + "lib/matplotlib/collections.py:docstring of matplotlib.collections:1" + ], "matplotlib.image._ImageBase": [ "doc/api/artist_api.rst:202", "lib/matplotlib/image.py:docstring of matplotlib.image:1" @@ -247,15 +243,12 @@ ], "mpl_toolkits.axes_grid1.axes_size._Base": [ "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.Add:1", - "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.AddList:1", "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.AxesX:1", "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.AxesY:1", "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.Fixed:1", "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.Fraction:1", "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.MaxExtent:1", - "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.Padded:1", - "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.Scaled:1", - "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.SizeFromFunc:1" + "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.Scaled:1" ], "mpl_toolkits.axes_grid1.parasite_axes.AxesHostAxes": [ "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:30::1", @@ -303,25 +296,21 @@ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.phase_spectrum:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.psd:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.specgram:1", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.subplots:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.xcorr:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.xticks:1", "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.yticks:1" ], "numpy.uint8": [ "lib/matplotlib/path.py:docstring of matplotlib.path:1" - ], - "unittest.case.TestCase": [ - "lib/matplotlib/testing/decorators.py:docstring of matplotlib.testing.decorators:1" ] }, "py:data": { "matplotlib.axes.Axes.transAxes": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:240", - "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.add_axes:18", - "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:107", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:249", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:240" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:238", + "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.add_artist:1", + "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:105", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:242", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:238" ] }, "py:meth": { @@ -329,11 +318,11 @@ "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:26", "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:28", "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:35", - "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:39", - "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:44" + "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:40", + "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:41" ], "IPython.terminal.interactiveshell.TerminalInteractiveShell.inputhook": [ - "doc/users/explain/interactive_guide.rst:420" + "doc/users/explain/figure/interactive_guide.rst:420" ], "_find_tails": [ "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:5" @@ -366,26 +355,31 @@ "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver:38", "lib/mpl_toolkits/mplot3d/art3d.py:docstring of mpl_toolkits.mplot3d.art3d.Path3DCollection:39", "lib/mpl_toolkits/mplot3d/art3d.py:docstring of mpl_toolkits.mplot3d.art3d.Poly3DCollection:37" + ], + "matplotlib.collections._MeshData.set_array": [ + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolormesh:155", + "lib/matplotlib/collections.py:docstring of matplotlib.collections.AsteriskPolygonCollection:1", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolormesh:155" ] }, "py:obj": { "Artist.stale_callback": [ - "doc/users/explain/interactive_guide.rst:323" + "doc/users/explain/figure/interactive_guide.rst:323" ], "Artist.sticky_edges": [ - "doc/api/axes_api.rst:353::1", + "doc/api/axes_api.rst:354::1", "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes.Axes.use_sticky_edges:1" ], "Axes.dataLim": [ - "doc/api/axes_api.rst:292::1", + "doc/api/axes_api.rst:293::1", "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.update_datalim:1" ], "AxesBase": [ - "doc/api/axes_api.rst:445::1", + "doc/api/axes_api.rst:446::1", "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.add_child_axes:1" ], "Figure.stale_callback": [ - "doc/users/explain/interactive_guide.rst:333" + "doc/users/explain/figure/interactive_guide.rst:333" ], "Glyph": [ "doc/gallery/misc/ftface_props.rst:28" @@ -397,7 +391,7 @@ "lib/matplotlib/testing/decorators.py:docstring of matplotlib.testing.decorators:1" ], "Line2D.pick": [ - "doc/users/explain/event_handling.rst:567" + "doc/users/explain/figure/event_handling.rst:568" ], "QuadContourSet.changed()": [ "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contour:147", @@ -406,7 +400,7 @@ "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contourf:147" ], "Rectangle.contains": [ - "doc/users/explain/event_handling.rst:279" + "doc/users/explain/figure/event_handling.rst:280" ], "Size.from_any": [ "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:53", @@ -445,11 +439,11 @@ "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.inset_axes:6" ], "axes.bbox": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:137", - "lib/matplotlib/figure.py:docstring of matplotlib.figure.Figure:126", - "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:4", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:146", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:137" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:136", + "lib/matplotlib/figure.py:docstring of matplotlib.figure.Figure:69", + "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:3", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:140", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:136" ], "can_composite": [ "lib/matplotlib/image.py:docstring of matplotlib.image:5" @@ -461,11 +455,11 @@ "lib/matplotlib/backends/backend_agg.py:docstring of matplotlib.backends.backend_agg:1" ], "figure.bbox": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:137", - "lib/matplotlib/figure.py:docstring of matplotlib.figure.Figure:126", - "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:4", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:146", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:137" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:136", + "lib/matplotlib/figure.py:docstring of matplotlib.figure.Figure:69", + "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:3", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:140", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:136" ], "fmt_xdata": [ "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.format_xdata:1" @@ -489,7 +483,7 @@ "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_ylim:19" ], "ipykernel.pylab.backend_inline": [ - "doc/users/explain/interactive.rst:255" + "doc/users/explain/figure/interactive.rst:264" ], "kde.covariance_factor": [ "lib/matplotlib/mlab.py:docstring of matplotlib.mlab:40" @@ -629,9 +623,6 @@ "matplotlib.animation.ImageMagickFileWriter.bin_path": [ "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:27::1" ], - "matplotlib.animation.ImageMagickFileWriter.delay": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickFileWriter.input_names:1::1" - ], "matplotlib.animation.ImageMagickFileWriter.finish": [ "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:27::1" ], @@ -647,9 +638,6 @@ "matplotlib.animation.ImageMagickFileWriter.isAvailable": [ "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:27::1" ], - "matplotlib.animation.ImageMagickFileWriter.output_args": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickFileWriter.input_names:1::1" - ], "matplotlib.animation.ImageMagickFileWriter.saving": [ "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:27::1" ], @@ -659,9 +647,6 @@ "matplotlib.animation.ImageMagickWriter.bin_path": [ "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:27::1" ], - "matplotlib.animation.ImageMagickWriter.delay": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickWriter.input_names:1::1" - ], "matplotlib.animation.ImageMagickWriter.finish": [ "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:27::1" ], @@ -674,9 +659,6 @@ "matplotlib.animation.ImageMagickWriter.isAvailable": [ "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:27::1" ], - "matplotlib.animation.ImageMagickWriter.output_args": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickWriter.input_names:1::1" - ], "matplotlib.animation.ImageMagickWriter.saving": [ "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:27::1" ], diff --git a/doc/users/next_whats_new/polyquadmesh.rst b/doc/users/next_whats_new/polyquadmesh.rst new file mode 100644 index 000000000000..de72ba0980ca --- /dev/null +++ b/doc/users/next_whats_new/polyquadmesh.rst @@ -0,0 +1,25 @@ +``PolyQuadMesh`` is a new class for drawing quadrilateral meshes +---------------------------------------------------------------- + +`~.Axes.pcolor` previously returned a flattened `.PolyCollection` with only +the valid polygons (unmasked) contained within it. Now, we return a `.PolyQuadMesh`, +which is a mixin incorporating the usefulness of 2D array and mesh coordinates +handling, but still inheriting the draw methods of `.PolyCollection`, which enables +more control over the rendering properties than a normal `.QuadMesh` that is +returned from `~.Axes.pcolormesh`. The new class subclasses `.PolyCollection` and thus +should still behave the same as before. This new class keeps track of the mask for +the user and updates the Polygons that are sent to the renderer appropriately. + +.. plot:: + + arr = np.arange(12).reshape((3, 4)) + + fig, ax = plt.subplots() + pc = ax.pcolor(arr) + + # Mask one element and show that the hatch is also not drawn + # over that region + pc.set_array(np.ma.masked_equal(arr, 5)) + pc.set_hatch('//') + + plt.show() diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 46fb69852286..6ed51e8573d7 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -5783,13 +5783,7 @@ def _pcolorargs(self, funcname, *args, shading='auto', **kwargs): raise ValueError( 'x and y arguments to pcolormesh cannot have ' 'non-finite values or be of type ' - 'numpy.ma.core.MaskedArray with masked values') - # safe_masked_invalid() returns an ndarray for dtypes other - # than floating point. - if isinstance(X, np.ma.core.MaskedArray): - X = X.data # strip mask as downstream doesn't like it... - if isinstance(Y, np.ma.core.MaskedArray): - Y = Y.data + 'numpy.ma.MaskedArray with masked values') nrows, ncols = C.shape[:2] else: raise _api.nargs_error(funcname, takes="1 or 3", given=len(args)) @@ -5839,9 +5833,11 @@ def _interp_grid(X): "This may lead to incorrectly calculated cell " "edges, in which case, please supply " f"explicit cell edges to {funcname}.") - X = np.hstack((X[:, [0]] - dX[:, [0]], - X[:, :-1] + dX, - X[:, [-1]] + dX[:, [-1]])) + + hstack = np.ma.hstack if np.ma.isMA(X) else np.hstack + X = hstack((X[:, [0]] - dX[:, [0]], + X[:, :-1] + dX, + X[:, [-1]] + dX[:, [-1]])) else: # This is just degenerate, but we can't reliably guess # a dX if there is just one value. @@ -5959,7 +5955,7 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, Returns ------- - `matplotlib.collections.Collection` + `matplotlib.collections.PolyQuadMesh` Other Parameters ---------------- @@ -5977,7 +5973,7 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, **kwargs Additionally, the following arguments are allowed. They are passed - along to the `~matplotlib.collections.PolyCollection` constructor: + along to the `~matplotlib.collections.PolyQuadMesh` constructor: %(PolyCollection:kwdoc)s @@ -6011,35 +6007,6 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, shading = shading.lower() X, Y, C, shading = self._pcolorargs('pcolor', *args, shading=shading, kwargs=kwargs) - Ny, Nx = X.shape - - # convert to MA, if necessary. - C = ma.asarray(C) - X = ma.asarray(X) - Y = ma.asarray(Y) - - mask = ma.getmaskarray(X) + ma.getmaskarray(Y) - xymask = (mask[0:-1, 0:-1] + mask[1:, 1:] + - mask[0:-1, 1:] + mask[1:, 0:-1]) - # don't plot if C or any of the surrounding vertices are masked. - mask = ma.getmaskarray(C) + xymask - - unmask = ~mask - X1 = ma.filled(X[:-1, :-1])[unmask] - Y1 = ma.filled(Y[:-1, :-1])[unmask] - X2 = ma.filled(X[1:, :-1])[unmask] - Y2 = ma.filled(Y[1:, :-1])[unmask] - X3 = ma.filled(X[1:, 1:])[unmask] - Y3 = ma.filled(Y[1:, 1:])[unmask] - X4 = ma.filled(X[:-1, 1:])[unmask] - Y4 = ma.filled(Y[:-1, 1:])[unmask] - npoly = len(X1) - - xy = np.stack([X1, Y1, X2, Y2, X3, Y3, X4, Y4, X1, Y1], axis=-1) - verts = xy.reshape((npoly, 5, 2)) - - C = ma.filled(C[:Ny - 1, :Nx - 1])[unmask] - linewidths = (0.25,) if 'linewidth' in kwargs: kwargs['linewidths'] = kwargs.pop('linewidth') @@ -6053,19 +6020,29 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, # unless the boundary is not stroked, in which case the # default will be False; with unstroked boundaries, aa # makes artifacts that are often disturbing. - if 'antialiased' in kwargs: - kwargs['antialiaseds'] = kwargs.pop('antialiased') - if 'antialiaseds' not in kwargs and cbook._str_lower_equal(ec, "none"): - kwargs['antialiaseds'] = False + if 'antialiaseds' in kwargs: + kwargs['antialiased'] = kwargs.pop('antialiaseds') + if 'antialiased' not in kwargs and cbook._str_lower_equal(ec, "none"): + kwargs['antialiased'] = False kwargs.setdefault('snap', False) - collection = mcoll.PolyCollection( - verts, array=C, cmap=cmap, norm=norm, alpha=alpha, **kwargs) - collection._scale_norm(norm, vmin, vmax) + if np.ma.isMaskedArray(X) or np.ma.isMaskedArray(Y): + stack = np.ma.stack + X = np.ma.asarray(X) + Y = np.ma.asarray(Y) + # For bounds collections later + x = X.compressed() + y = Y.compressed() + else: + stack = np.stack + x = X + y = Y + coords = stack([X, Y], axis=-1) - x = X.compressed() - y = Y.compressed() + collection = mcoll.PolyQuadMesh( + coords, array=C, cmap=cmap, norm=norm, alpha=alpha, **kwargs) + collection._scale_norm(norm, vmin, vmax) # Transform from native to data coordinates? t = collection._transform @@ -6258,7 +6235,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, The main difference lies in the created object and internal data handling: - While `~.Axes.pcolor` returns a `.PolyCollection`, `~.Axes.pcolormesh` + While `~.Axes.pcolor` returns a `.PolyQuadMesh`, `~.Axes.pcolormesh` returns a `.QuadMesh`. The latter is more specialized for the given purpose and thus is faster. It should almost always be preferred. @@ -6267,12 +6244,13 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, for *C*. However, only `~.Axes.pcolor` supports masked arrays for *X* and *Y*. The reason lies in the internal handling of the masked values. `~.Axes.pcolor` leaves out the respective polygons from the - PolyCollection. `~.Axes.pcolormesh` sets the facecolor of the masked + PolyQuadMesh. `~.Axes.pcolormesh` sets the facecolor of the masked elements to transparent. You can see the difference when using edgecolors. While all edges are drawn irrespective of masking in a QuadMesh, the edge between two adjacent masked quadrilaterals in `~.Axes.pcolor` is not drawn as the corresponding polygons do not - exist in the PolyCollection. + exist in the PolyQuadMesh. Because PolyQuadMesh draws each individual + polygon, it also supports applying hatches and linestyles to the collection. Another difference is the support of Gouraud shading in `~.Axes.pcolormesh`, which is not available with `~.Axes.pcolor`. diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index e61c3773d953..da8f5bc8ce14 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -875,7 +875,7 @@ def update_scalarmappable(self): # Allow possibility to call 'self.set_array(None)'. if self._A is not None: # QuadMesh can map 2d arrays (but pcolormesh supplies 1d array) - if self._A.ndim > 1 and not isinstance(self, QuadMesh): + if self._A.ndim > 1 and not isinstance(self, _MeshData): raise ValueError('Collections can only map rank 1 arrays') if np.iterable(self._alpha): if self._alpha.size != self._A.size: @@ -1944,9 +1944,11 @@ def draw(self, renderer): renderer.close_group(self.__class__.__name__) -class QuadMesh(Collection): +class _MeshData: r""" - Class for the efficient drawing of a quadrilateral mesh. + Class for managing the two dimensional coordinates of Quadrilateral meshes + and the associated data with them. This class is a mixin and is intended to + be used with another collection that will implement the draw separately. A quadrilateral mesh is a grid of M by N adjacent quadrilaterals that are defined via a (M+1, N+1) grid of vertices. The quadrilateral (m, n) is @@ -1966,42 +1968,12 @@ class QuadMesh(Collection): The vertices. ``coordinates[m, n]`` specifies the (x, y) coordinates of vertex (m, n). - antialiased : bool, default: True - shading : {'flat', 'gouraud'}, default: 'flat' - - Notes - ----- - Unlike other `.Collection`\s, the default *pickradius* of `.QuadMesh` is 0, - i.e. `~.Artist.contains` checks whether the test point is within any of the - mesh quadrilaterals. - """ - - def __init__(self, coordinates, *, antialiased=True, shading='flat', - **kwargs): - kwargs.setdefault("pickradius", 0) - # end of signature deprecation code - + def __init__(self, coordinates, *, shading='flat'): _api.check_shape((None, None, 2), coordinates=coordinates) self._coordinates = coordinates - self._antialiased = antialiased self._shading = shading - self._bbox = transforms.Bbox.unit() - self._bbox.update_from_data_xy(self._coordinates.reshape(-1, 2)) - # super init delayed after own init because array kwarg requires - # self._coordinates and self._shading - super().__init__(**kwargs) - self.set_mouseover(False) - - def get_paths(self): - if self._paths is None: - self.set_paths() - return self._paths - - def set_paths(self): - self._paths = self._convert_mesh_to_paths(self._coordinates) - self.stale = True def set_array(self, A): """ @@ -2040,9 +2012,6 @@ def set_array(self, A): f"{' or '.join(map(str, ok_shapes))}, not {A.shape}") return super().set_array(A) - def get_datalim(self, transData): - return (self.get_transform() - transData).transform_bbox(self._bbox) - def get_coordinates(self): """ Return the vertices of the mesh as an (M+1, N+1, 2) array. @@ -2053,6 +2022,18 @@ def get_coordinates(self): """ return self._coordinates + def get_edgecolor(self): + # docstring inherited + # Note that we want to return an array of shape (N*M, 4) + # a flattened RGBA collection + return super().get_edgecolor().reshape(-1, 4) + + def get_facecolor(self): + # docstring inherited + # Note that we want to return an array of shape (N*M, 4) + # a flattened RGBA collection + return super().get_facecolor().reshape(-1, 4) + @staticmethod def _convert_mesh_to_paths(coordinates): """ @@ -2116,6 +2097,64 @@ def _convert_mesh_to_triangles(self, coordinates): tmask = np.isnan(colors[..., 2, 3]) return triangles[~tmask], colors[~tmask] + +class QuadMesh(_MeshData, Collection): + r""" + Class for the efficient drawing of a quadrilateral mesh. + + A quadrilateral mesh is a grid of M by N adjacent quadrilaterals that are + defined via a (M+1, N+1) grid of vertices. The quadrilateral (m, n) is + defined by the vertices :: + + (m+1, n) ----------- (m+1, n+1) + / / + / / + / / + (m, n) -------- (m, n+1) + + The mesh need not be regular and the polygons need not be convex. + + Parameters + ---------- + coordinates : (M+1, N+1, 2) array-like + The vertices. ``coordinates[m, n]`` specifies the (x, y) coordinates + of vertex (m, n). + + antialiased : bool, default: True + + shading : {'flat', 'gouraud'}, default: 'flat' + + Notes + ----- + Unlike other `.Collection`\s, the default *pickradius* of `.QuadMesh` is 0, + i.e. `~.Artist.contains` checks whether the test point is within any of the + mesh quadrilaterals. + + """ + + def __init__(self, coordinates, *, antialiased=True, shading='flat', + **kwargs): + kwargs.setdefault("pickradius", 0) + super().__init__(coordinates=coordinates, shading=shading) + Collection.__init__(self, **kwargs) + + self._antialiased = antialiased + self._bbox = transforms.Bbox.unit() + self._bbox.update_from_data_xy(self._coordinates.reshape(-1, 2)) + self.set_mouseover(False) + + def get_paths(self): + if self._paths is None: + self.set_paths() + return self._paths + + def set_paths(self): + self._paths = self._convert_mesh_to_paths(self._coordinates) + self.stale = True + + def get_datalim(self, transData): + return (self.get_transform() - transData).transform_bbox(self._bbox) + @artist.allow_rasterization def draw(self, renderer): if not self.get_visible(): @@ -2170,3 +2209,161 @@ def get_cursor_data(self, event): if contained and self.get_array() is not None: return self.get_array().ravel()[info["ind"]] return None + + +class PolyQuadMesh(_MeshData, PolyCollection): + """ + Class for drawing a quadrilateral mesh as individual Polygons. + + A quadrilateral mesh is a grid of M by N adjacent quadrilaterals that are + defined via a (M+1, N+1) grid of vertices. The quadrilateral (m, n) is + defined by the vertices :: + + (m+1, n) ----------- (m+1, n+1) + / / + / / + / / + (m, n) -------- (m, n+1) + + The mesh need not be regular and the polygons need not be convex. + + Parameters + ---------- + coordinates : (M+1, N+1, 2) array-like + The vertices. ``coordinates[m, n]`` specifies the (x, y) coordinates + of vertex (m, n). + + Notes + ----- + Unlike `.QuadMesh`, this class will draw each cell as an individual Polygon. + This is significantly slower, but allows for more flexibility when wanting + to add additional properties to the cells, such as hatching. + + Another difference from `.QuadMesh` is that if any of the vertices or data + of a cell are masked, that Polygon will **not** be drawn and it won't be in + the list of paths returned. + """ + + def __init__(self, coordinates, **kwargs): + # We need to keep track of whether we are using deprecated compression + # Update it after the initializers + self._deprecated_compression = False + super().__init__(coordinates=coordinates) + PolyCollection.__init__(self, verts=[], **kwargs) + # Store this during the compression deprecation period + self._original_mask = ~self._get_unmasked_polys() + self._deprecated_compression = np.any(self._original_mask) + # Setting the verts updates the paths of the PolyCollection + # This is called after the initializers to make sure the kwargs + # have all been processed and available for the masking calculations + self._set_unmasked_verts() + + def _get_unmasked_polys(self): + """Get the unmasked regions using the coordinates and array""" + # mask(X) | mask(Y) + mask = np.any(np.ma.getmaskarray(self._coordinates), axis=-1) + + # We want the shape of the polygon, which is the corner of each X/Y array + mask = (mask[0:-1, 0:-1] | mask[1:, 1:] | mask[0:-1, 1:] | mask[1:, 0:-1]) + + if (getattr(self, "_deprecated_compression", False) and + np.any(self._original_mask)): + return ~(mask | self._original_mask) + # Take account of the array data too, temporarily avoiding + # the compression warning and resetting the variable after the call + with cbook._setattr_cm(self, _deprecated_compression=False): + arr = self.get_array() + if arr is not None: + arr = np.ma.getmaskarray(arr) + if arr.ndim == 3: + # RGB(A) case + mask |= np.any(arr, axis=-1) + elif arr.ndim == 2: + mask |= arr + else: + mask |= arr.reshape(self._coordinates[:-1, :-1, :].shape[:2]) + return ~mask + + def _set_unmasked_verts(self): + X = self._coordinates[..., 0] + Y = self._coordinates[..., 1] + + unmask = self._get_unmasked_polys() + X1 = np.ma.filled(X[:-1, :-1])[unmask] + Y1 = np.ma.filled(Y[:-1, :-1])[unmask] + X2 = np.ma.filled(X[1:, :-1])[unmask] + Y2 = np.ma.filled(Y[1:, :-1])[unmask] + X3 = np.ma.filled(X[1:, 1:])[unmask] + Y3 = np.ma.filled(Y[1:, 1:])[unmask] + X4 = np.ma.filled(X[:-1, 1:])[unmask] + Y4 = np.ma.filled(Y[:-1, 1:])[unmask] + npoly = len(X1) + + xy = np.ma.stack([X1, Y1, X2, Y2, X3, Y3, X4, Y4, X1, Y1], axis=-1) + verts = xy.reshape((npoly, 5, 2)) + self.set_verts(verts) + + def get_edgecolor(self): + # docstring inherited + # We only want to return the facecolors of the polygons + # that were drawn. + ec = super().get_edgecolor() + unmasked_polys = self._get_unmasked_polys().ravel() + if len(ec) != len(unmasked_polys): + # Mapping is off + return ec + return ec[unmasked_polys, :] + + def get_facecolor(self): + # docstring inherited + # We only want to return the facecolors of the polygons + # that were drawn. + fc = super().get_facecolor() + unmasked_polys = self._get_unmasked_polys().ravel() + if len(fc) != len(unmasked_polys): + # Mapping is off + return fc + return fc[unmasked_polys, :] + + def set_array(self, A): + # docstring inherited + prev_unmask = self._get_unmasked_polys() + # MPL <3.8 compressed the mask, so we need to handle flattened 1d input + # until the deprecation expires, also only warning when there are masked + # elements and thus compression occurring. + if self._deprecated_compression and np.ndim(A) == 1: + _api.warn_deprecated("3.8", message="Setting a PolyQuadMesh array using " + "the compressed values is deprecated. " + "Pass the full 2D shape of the original array " + f"{prev_unmask.shape} including the masked elements.") + Afull = np.empty(self._original_mask.shape) + Afull[~self._original_mask] = A + # We also want to update the mask with any potential + # new masked elements that came in. But, we don't want + # to update any of the compression from the original + mask = self._original_mask.copy() + mask[~self._original_mask] |= np.ma.getmask(A) + A = np.ma.array(Afull, mask=mask) + return super().set_array(A) + self._deprecated_compression = False + super().set_array(A) + # If the mask has changed at all we need to update + # the set of Polys that we are drawing + if not np.array_equal(prev_unmask, self._get_unmasked_polys()): + self._set_unmasked_verts() + + def get_array(self): + # docstring inherited + # Can remove this entire function once the deprecation period ends + A = super().get_array() + if A is None: + return + if self._deprecated_compression and np.any(np.ma.getmask(A)): + _api.warn_deprecated("3.8", message=( + "Getting the array from a PolyQuadMesh will return the full " + "array in the future (uncompressed). To get this behavior now " + "set the PolyQuadMesh with a 2D array .set_array(data2d).")) + # Setting an array of a polycollection required + # compressing the array + return np.ma.compressed(A) + return A diff --git a/lib/matplotlib/collections.pyi b/lib/matplotlib/collections.pyi index c8b38f5fac2e..25eece49d755 100644 --- a/lib/matplotlib/collections.pyi +++ b/lib/matplotlib/collections.pyi @@ -204,7 +204,19 @@ class TriMesh(Collection): @staticmethod def convert_mesh_to_paths(tri: Triangulation) -> list[Path]: ... -class QuadMesh(Collection): +class _MeshData: + def __init__( + self, + coordinates: ArrayLike, + *, + shading: Literal["flat", "gouraud"] = ..., + ) -> None: ... + def set_array(self, A: ArrayLike | None) -> None: ... + def get_coordinates(self) -> ArrayLike: ... + def get_facecolor(self) -> ColorType | Sequence[ColorType]: ... + def get_edgecolor(self) -> ColorType | Sequence[ColorType]: ... + +class QuadMesh(_MeshData, Collection): def __init__( self, coordinates: ArrayLike, @@ -216,7 +228,12 @@ class QuadMesh(Collection): def get_paths(self) -> list[Path]: ... # Parent class has an argument, perhaps add a noop arg? def set_paths(self) -> None: ... # type: ignore[override] - def set_array(self, A: ArrayLike | None) -> None: ... def get_datalim(self, transData: transforms.Transform) -> transforms.Bbox: ... - def get_coordinates(self) -> ArrayLike: ... def get_cursor_data(self, event: MouseEvent) -> float: ... + +class PolyQuadMesh(_MeshData, PolyCollection): + def __init__( + self, + coordinates: ArrayLike, + **kwargs + ) -> None: ... diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 56a9c688af1f..2a1002b6df59 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -19,6 +19,11 @@ from matplotlib.testing.decorators import check_figures_equal, image_comparison +@pytest.fixture(params=["pcolormesh", "pcolor"]) +def pcfunc(request): + return request.param + + def generate_EventCollection_plot(): """Generate the initial collection and plot it.""" positions = np.array([0., 1., 2., 3., 5., 8., 13., 21.]) @@ -818,12 +823,12 @@ def test_autolim_with_zeros(transform, expected): np.testing.assert_allclose(ax.get_xlim(), expected) -def test_quadmesh_set_array_validation(): +def test_quadmesh_set_array_validation(pcfunc): x = np.arange(11) y = np.arange(8) z = np.random.random((7, 10)) fig, ax = plt.subplots() - coll = ax.pcolormesh(x, y, z) + coll = getattr(ax, pcfunc)(x, y, z) with pytest.raises(ValueError, match=re.escape( "For X (11) and Y (8) with flat shading, A should have shape " @@ -866,12 +871,65 @@ def test_quadmesh_set_array_validation(): coll = ax.pcolormesh(x, y, z, shading='gouraud') -def test_quadmesh_get_coordinates(): +def test_polyquadmesh_masked_vertices_array(): + xx, yy = np.meshgrid([0, 1, 2], [0, 1, 2, 3]) + # 2 x 3 mesh data + zz = (xx*yy)[:-1, :-1] + quadmesh = plt.pcolormesh(xx, yy, zz) + quadmesh.update_scalarmappable() + quadmesh_fc = quadmesh.get_facecolor()[1:, :] + # Mask the origin vertex in x + xx = np.ma.masked_where((xx == 0) & (yy == 0), xx) + polymesh = plt.pcolor(xx, yy, zz) + polymesh.update_scalarmappable() + # One cell should be left out + assert len(polymesh.get_paths()) == 5 + # Poly version should have the same facecolors as the end of the quadmesh + assert_array_equal(quadmesh_fc, polymesh.get_facecolor()) + + # Mask the origin vertex in y + yy = np.ma.masked_where((xx == 0) & (yy == 0), yy) + polymesh = plt.pcolor(xx, yy, zz) + polymesh.update_scalarmappable() + # One cell should be left out + assert len(polymesh.get_paths()) == 5 + # Poly version should have the same facecolors as the end of the quadmesh + assert_array_equal(quadmesh_fc, polymesh.get_facecolor()) + + # Mask the origin cell data + zz = np.ma.masked_where((xx[:-1, :-1] == 0) & (yy[:-1, :-1] == 0), zz) + polymesh = plt.pcolor(zz) + polymesh.update_scalarmappable() + # One cell should be left out + assert len(polymesh.get_paths()) == 5 + # Poly version should have the same facecolors as the end of the quadmesh + assert_array_equal(quadmesh_fc, polymesh.get_facecolor()) + + # Setting array with 1D compressed values is deprecated + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match="Setting a PolyQuadMesh"): + polymesh.set_array(np.ones(5)) + + # We should also be able to call set_array with a new mask and get + # updated polys + # Remove mask, should add all polys back + zz = np.arange(6).reshape((3, 2)) + polymesh.set_array(zz) + polymesh.update_scalarmappable() + assert len(polymesh.get_paths()) == 6 + # Add mask should remove polys + zz = np.ma.masked_less(zz, 2) + polymesh.set_array(zz) + polymesh.update_scalarmappable() + assert len(polymesh.get_paths()) == 4 + + +def test_quadmesh_get_coordinates(pcfunc): x = [0, 1, 2] y = [2, 4, 6] z = np.ones(shape=(2, 2)) xx, yy = np.meshgrid(x, y) - coll = plt.pcolormesh(xx, yy, z) + coll = getattr(plt, pcfunc)(xx, yy, z) # shape (3, 3, 2) coords = np.stack([xx.T, yy.T]).T @@ -908,23 +966,23 @@ def test_quadmesh_set_array(): assert np.array_equal(coll.get_array(), np.ones(16)) -def test_quadmesh_vmin_vmax(): +def test_quadmesh_vmin_vmax(pcfunc): # test when vmin/vmax on the norm changes, the quadmesh gets updated fig, ax = plt.subplots() cmap = mpl.colormaps['plasma'] norm = mpl.colors.Normalize(vmin=0, vmax=1) - coll = ax.pcolormesh([[1]], cmap=cmap, norm=norm) + coll = getattr(ax, pcfunc)([[1]], cmap=cmap, norm=norm) fig.canvas.draw() - assert np.array_equal(coll.get_facecolors()[0, 0, :], cmap(norm(1))) + assert np.array_equal(coll.get_facecolors()[0, :], cmap(norm(1))) # Change the vmin/vmax of the norm so that the color is from # the bottom of the colormap now norm.vmin, norm.vmax = 1, 2 fig.canvas.draw() - assert np.array_equal(coll.get_facecolors()[0, 0, :], cmap(norm(1))) + assert np.array_equal(coll.get_facecolors()[0, :], cmap(norm(1))) -def test_quadmesh_alpha_array(): +def test_quadmesh_alpha_array(pcfunc): x = np.arange(4) y = np.arange(4) z = np.arange(9).reshape((3, 3)) @@ -932,26 +990,26 @@ def test_quadmesh_alpha_array(): alpha_flat = alpha.ravel() # Provide 2-D alpha: fig, (ax0, ax1) = plt.subplots(2) - coll1 = ax0.pcolormesh(x, y, z, alpha=alpha) - coll2 = ax1.pcolormesh(x, y, z) + coll1 = getattr(ax0, pcfunc)(x, y, z, alpha=alpha) + coll2 = getattr(ax0, pcfunc)(x, y, z) coll2.set_alpha(alpha) plt.draw() - assert_array_equal(coll1.get_facecolors()[..., -1], alpha) - assert_array_equal(coll2.get_facecolors()[..., -1], alpha) + assert_array_equal(coll1.get_facecolors()[:, -1], alpha_flat) + assert_array_equal(coll2.get_facecolors()[:, -1], alpha_flat) # Or provide 1-D alpha: fig, (ax0, ax1) = plt.subplots(2) - coll1 = ax0.pcolormesh(x, y, z, alpha=alpha) - coll2 = ax1.pcolormesh(x, y, z) + coll1 = getattr(ax0, pcfunc)(x, y, z, alpha=alpha) + coll2 = getattr(ax1, pcfunc)(x, y, z) coll2.set_alpha(alpha) plt.draw() - assert_array_equal(coll1.get_facecolors()[..., -1], alpha) - assert_array_equal(coll2.get_facecolors()[..., -1], alpha) + assert_array_equal(coll1.get_facecolors()[:, -1], alpha_flat) + assert_array_equal(coll2.get_facecolors()[:, -1], alpha_flat) -def test_alpha_validation(): +def test_alpha_validation(pcfunc): # Most of the relevant testing is in test_artist and test_colors. fig, ax = plt.subplots() - pc = ax.pcolormesh(np.arange(12).reshape((3, 4))) + pc = getattr(ax, pcfunc)(np.arange(12).reshape((3, 4))) with pytest.raises(ValueError, match="^Data array shape"): pc.set_alpha([0.5, 0.6]) pc.update_scalarmappable() @@ -985,15 +1043,15 @@ def test_legend_inverse_size_label_relationship(): @mpl.style.context('default') -@pytest.mark.parametrize('pcfunc', [plt.pcolor, plt.pcolormesh]) def test_color_logic(pcfunc): + pcfunc = getattr(plt, pcfunc) z = np.arange(12).reshape(3, 4) # Explicitly set an edgecolor. pc = pcfunc(z, edgecolors='red', facecolors='none') pc.update_scalarmappable() # This is called in draw(). # Define 2 reference "colors" here for multiple use. face_default = mcolors.to_rgba_array(pc._get_default_facecolor()) - mapped = pc.get_cmap()(pc.norm(z.ravel() if pcfunc == plt.pcolor else z)) + mapped = pc.get_cmap()(pc.norm(z.ravel())) # GitHub issue #1302: assert mcolors.same_color(pc.get_edgecolor(), 'red') # Check setting attributes after initialization: @@ -1023,7 +1081,7 @@ def test_color_logic(pcfunc): assert mcolors.same_color(pc.get_edgecolor(), 'none') assert mcolors.same_color(pc.get_facecolor(), face_default) # not mapped # Turn it back on by restoring the array (must be 1D!): - pc.set_array(z.ravel() if pcfunc == plt.pcolor else z) + pc.set_array(z) pc.update_scalarmappable() assert np.array_equal(pc.get_facecolor(), mapped) assert mcolors.same_color(pc.get_edgecolor(), 'none') @@ -1068,18 +1126,20 @@ def test_LineCollection_args(): assert mcolors.same_color(lc.get_facecolor(), 'none') -def test_array_wrong_dimensions(): +def test_array_dimensions(pcfunc): + # Make sure we can set the 1D, 2D, and 3D array shapes z = np.arange(12).reshape(3, 4) - pc = plt.pcolor(z) - with pytest.raises(ValueError, match="^Collections can only map"): - pc.set_array(z) - pc.update_scalarmappable() - pc = plt.pcolormesh(z) - pc.set_array(z) # 2D is OK for Quadmesh + pc = getattr(plt, pcfunc)(z) + # 1D + pc.set_array(z.ravel()) + pc.update_scalarmappable() + # 2D + pc.set_array(z) pc.update_scalarmappable() # 3D RGB is OK as well - z = np.arange(36).reshape(3, 4, 3) + z = np.arange(36, dtype=np.uint8).reshape(3, 4, 3) pc.set_array(z) + pc.update_scalarmappable() def test_get_segments():