From 8f3aa89ae3a8edc54688fce3ce37ec01d7da99f7 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Tue, 3 Jan 2023 12:14:25 -0700 Subject: [PATCH] ENH: Add a PolyQuadMesh for 2d meshes as Polygons This adds a new collection PolyQuadMesh that is a combination of a PolyCollection and QuadMesh, storing all of the coordinate data and arrays through QuadMesh, but drawing each quad individually through the PolyCollection.draw() method. The common mesh attributes and methods are stored in a private mixin class _MeshData that is used for both QuadMesh and PolyQuadMesh. Additionally, this supports RGB(A) arrays for pcolor() calls now. Previously to update a pcolor array you would need to set only the compressed values. This required keeping track of the mask by the user. For consistency with QuadMesh, we should only accept full 2D arrays that include the mask if a user desires. This also allows for the mask to be updated when setting an array. --- .../deprecations/25027-GL.rst | 12 + doc/missing-references.json | 128 ++++----- doc/users/next_whats_new/polyquadmesh.rst | 25 ++ lib/matplotlib/axes/_axes.py | 84 ++---- lib/matplotlib/collections.py | 271 +++++++++++++++--- lib/matplotlib/collections.pyi | 23 +- lib/matplotlib/tests/test_collections.py | 120 ++++++-- 7 files changed, 467 insertions(+), 196 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/25027-GL.rst create mode 100644 doc/users/next_whats_new/polyquadmesh.rst 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():