diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index e90c110c193b..9ec6451a42a7 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -205,7 +205,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path, def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, + linewidths, linestyles, antialiaseds, hatches, urls, offset_position): """ Draw a collection of *paths*. @@ -236,7 +236,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, list(path_ids), offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, - antialiaseds, urls, offset_position): + antialiaseds, hatches, urls, offset_position): path, transform = path_id # Only apply another translation if we have an offset, else we # reuse the initial transform. @@ -250,7 +250,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, coordinates, offsets, offsetTrans, facecolors, - antialiased, edgecolors): + antialiased, edgecolors, hatches): """ Draw a quadmesh. @@ -267,7 +267,7 @@ def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, return self.draw_path_collection( gc, master_transform, paths, [], offsets, offsetTrans, facecolors, - edgecolors, linewidths, [], [antialiased], [None], 'screen') + edgecolors, linewidths, [], [antialiased], hatches, [None], 'screen') def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): @@ -335,7 +335,7 @@ def _iter_collection_uses_per_path(self, paths, all_transforms, def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, - antialiaseds, urls, offset_position): + antialiaseds, hatches, urls, offset_position): """ Helper method (along with `_iter_collection_raw_paths`) to implement `draw_path_collection` in a memory-efficient manner. @@ -365,6 +365,7 @@ def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors, Nedgecolors = len(edgecolors) Nlinewidths = len(linewidths) Nlinestyles = len(linestyles) + Nhatches = len(hatches) Nurls = len(urls) if (Nfacecolors == 0 and Nedgecolors == 0) or Npaths == 0: @@ -385,13 +386,14 @@ def cycle_or_default(seq, default=None): lws = cycle_or_default(linewidths) lss = cycle_or_default(linestyles) aas = cycle_or_default(antialiaseds) + hchs = cycle_or_default(hatches) urls = cycle_or_default(urls) if Nedgecolors == 0: gc0.set_linewidth(0.0) - for pathid, (xo, yo), fc, ec, lw, ls, aa, url in itertools.islice( - zip(pathids, toffsets, fcs, ecs, lws, lss, aas, urls), N): + for pathid, (xo, yo), fc, ec, lw, ls, aa, hch, url in itertools.islice( + zip(pathids, toffsets, fcs, ecs, lws, lss, aas, hchs, urls), N): if not (np.isfinite(xo) and np.isfinite(yo)): continue if Nedgecolors: @@ -406,6 +408,8 @@ def cycle_or_default(seq, default=None): if fc is not None and len(fc) == 4 and fc[3] == 0: fc = None gc0.set_antialiased(aa) + if Nhatches: + gc0.set_hatch(hch) if Nurls: gc0.set_url(url) yield xo, yo, pathid, gc0, fc diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 7e3e09f034f5..caf7f5249290 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2026,7 +2026,7 @@ def draw_path(self, gc, path, transform, rgbFace=None): def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, + linewidths, linestyles, antialiaseds, hatches, urls, offset_position): # We can only reuse the objects if the presence of fill and # stroke (and the amount of alpha for each) is the same for @@ -2068,7 +2068,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, return RendererBase.draw_path_collection( self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, + linewidths, linestyles, antialiaseds, hatches, urls, offset_position) padding = np.max(linewidths) @@ -2085,7 +2085,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, path_codes, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, - antialiaseds, urls, offset_position): + antialiaseds, hatches, urls, offset_position): self.check_gc(gc0, rgbFace) dx, dy = xo - lastx, yo - lasty diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index d760bef04d19..ba6efd13a0c0 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -514,7 +514,7 @@ def draw_markers( @_log_if_debug_on def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, + linewidths, linestyles, antialiaseds, hatches, urls, offset_position): # Is the optimization worth it? Rough calculation: # cost of emitting a path in-line is @@ -530,7 +530,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, return RendererBase.draw_path_collection( self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, + linewidths, linestyles, antialiaseds, hatches, urls, offset_position) path_codes = [] @@ -550,7 +550,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, path_codes, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, - antialiaseds, urls, offset_position): + antialiaseds, hatches, urls, offset_position): ps = f"{xo:g} {yo:g} {path_id}" self._draw_ps(ps, gc0, rgbFace) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 72354b81862b..9c77f8e23a71 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -714,7 +714,7 @@ def draw_markers( def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, + linewidths, linestyles, antialiaseds, hatches, urls, offset_position): # Is the optimization worth it? Rough calculation: # cost of emitting a path in-line is @@ -730,7 +730,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, return super().draw_path_collection( gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, + linewidths, linestyles, antialiaseds, hatches, urls, offset_position) writer = self.writer @@ -749,7 +749,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, path_codes, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, - antialiaseds, urls, offset_position): + antialiaseds, hatches, urls, offset_position): url = gc0.get_url() if url is not None: writer.start('a', attrib={'xlink:href': url}) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index fd6cc4339d64..21361e9d4534 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -360,9 +360,7 @@ def draw(self, renderer): self._set_gc_clip(gc) gc.set_snap(self.get_snap()) - if self._hatch: - gc.set_hatch(self._hatch) - gc.set_hatch_color(self._hatch_color) + gc.set_hatch_color(self._hatch_color) if self.get_sketch_params() is not None: gc.set_sketch_params(*self.get_sketch_params()) @@ -386,7 +384,7 @@ def draw(self, renderer): len(self._linewidths) == 1 and all(ls[1] is None for ls in self._linestyles) and len(self._antialiaseds) == 1 and len(self._urls) == 1 and - self.get_hatch() is None): + all(h is None for h in self._hatch)): if len(trans): combined_transform = transforms.Affine2D(trans[0]) + transform else: @@ -412,6 +410,10 @@ def draw(self, renderer): gc, paths[0], combined_transform.frozen(), mpath.Path(offsets), offset_trf, tuple(facecolors[0])) else: + hatches = self.get_hatch() + if isinstance(renderer, mpl.backends.backend_agg.RendererAgg): + hatches = [mpath.Path.hatch(h) for h in hatches] + if self._gapcolor is not None: # First draw paths within the gaps. ipaths, ilinestyles = self._get_inverse_paths_linestyles() @@ -420,7 +422,7 @@ def draw(self, renderer): self.get_transforms(), offsets, offset_trf, [mcolors.to_rgba("none")], self._gapcolor, self._linewidths, ilinestyles, - self._antialiaseds, self._urls, + self._antialiaseds, hatches, self._urls, "screen") renderer.draw_path_collection( @@ -428,7 +430,7 @@ def draw(self, renderer): self.get_transforms(), offsets, offset_trf, self.get_facecolor(), self.get_edgecolor(), self._linewidths, self._linestyles, - self._antialiaseds, self._urls, + self._antialiaseds, hatches, self._urls, "screen") # offset_position, kept for backcompat. gc.restore() @@ -533,7 +535,9 @@ def set_hatch(self, hatch): hatch : {'/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'} """ # Use validate_hatch(list) after deprecation. - mhatch._validate_hatch_pattern(hatch) + hatch = np.atleast_1d(hatch) + for h in hatch: + mhatch._validate_hatch_pattern(h) self._hatch = hatch self.stale = True @@ -1846,8 +1850,9 @@ def __init__(self, patches, *, match_original=False, **kwargs): a heterogeneous assortment of different patch types. match_original : bool, default: False - If True, use the colors and linewidths of the original - patches. If False, new colors may be assigned by + If True, use the colors, linewidths, linestyles + and the hatch of the original patches. + If False, new colors may be assigned by providing the standard collection arguments, facecolor, edgecolor, linewidths, norm or cmap. @@ -1867,16 +1872,12 @@ def __init__(self, patches, *, match_original=False, **kwargs): """ if match_original: - def determine_facecolor(patch): - if patch.get_fill(): - return patch.get_facecolor() - return [0, 0, 0, 0] - - kwargs['facecolors'] = [determine_facecolor(p) for p in patches] + kwargs['facecolors'] = [p.get_facecolor() for p in patches] kwargs['edgecolors'] = [p.get_edgecolor() for p in patches] kwargs['linewidths'] = [p.get_linewidth() for p in patches] kwargs['linestyles'] = [p.get_linestyle() for p in patches] kwargs['antialiaseds'] = [p.get_antialiased() for p in patches] + kwargs['hatch'] = [p.get_hatch() for p in patches] super().__init__(**kwargs) @@ -2206,7 +2207,8 @@ def draw(self, renderer): coordinates, offsets, offset_trf, # Backends expect flattened rgba arrays (n*m, 4) for fc and ec self.get_facecolor().reshape((-1, 4)), - self._antialiased, self.get_edgecolors().reshape((-1, 4))) + self._antialiased, self.get_edgecolors().reshape((-1, 4)), + self.get_hatch()) gc.restore() renderer.close_group(self.__class__.__name__) self.stale = False diff --git a/lib/matplotlib/hatch.py b/lib/matplotlib/hatch.py index 9ec88776cfd3..d91ccb0a3243 100644 --- a/lib/matplotlib/hatch.py +++ b/lib/matplotlib/hatch.py @@ -180,7 +180,7 @@ def __init__(self, hatch, density): def _validate_hatch_pattern(hatch): - valid_hatch_patterns = set(r'-+|/\xXoO.*') + valid_hatch_patterns = set(r'-+|/\xXoO.*').union({None}) if hatch is not None: invalids = set(hatch).difference(valid_hatch_patterns) if invalids: diff --git a/src/_backend_agg.h b/src/_backend_agg.h index 470d459de341..44eb3b6ceb0b 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -173,9 +173,10 @@ class RendererAgg ColorArray &edgecolors, LineWidthArray &linewidths, DashesVector &linestyles, - AntialiasedArray &antialiaseds); + AntialiasedArray &antialiaseds, + PathGenerator &hatch_paths); - template + template void draw_quad_mesh(GCAgg &gc, agg::trans_affine &master_transform, unsigned int mesh_width, @@ -185,7 +186,8 @@ class RendererAgg agg::trans_affine &offset_trans, ColorArray &facecolors, bool antialiased, - ColorArray &edgecolors); + ColorArray &edgecolors, + PathGenerator &hatch_paths); template void draw_gouraud_triangles(GCAgg &gc, @@ -267,6 +269,7 @@ class RendererAgg LineWidthArray &linewidths, DashesVector &linestyles, AntialiasedArray &antialiaseds, + mpl::PathGenerator &hatch_paths, bool check_snap, bool has_codes); @@ -904,6 +907,7 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, LineWidthArray &linewidths, DashesVector &linestyles, AntialiasedArray &antialiaseds, + mpl::PathGenerator &hatch_paths, bool check_snap, bool has_codes) { @@ -923,6 +927,7 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, size_t Nedgecolors = safe_first_shape(edgecolors); size_t Nlinewidths = safe_first_shape(linewidths); size_t Nlinestyles = std::min(linestyles.size(), N); + size_t Nhatchs = hatch_paths.num_paths(); size_t Naa = safe_first_shape(antialiaseds); if ((Nfacecolors == 0 && Nedgecolors == 0) || Npaths == 0) { @@ -988,6 +993,10 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, } } + if (Nhatchs) { + gc.hatchpath = hatch_paths(i % Nhatchs); + } + if (check_snap) { gc.isaa = antialiaseds(i % Naa); @@ -1034,7 +1043,8 @@ inline void RendererAgg::draw_path_collection(GCAgg &gc, ColorArray &edgecolors, LineWidthArray &linewidths, DashesVector &linestyles, - AntialiasedArray &antialiaseds) + AntialiasedArray &antialiaseds, + PathGenerator &hatch_paths) { _draw_path_collection_generic(gc, master_transform, @@ -1050,6 +1060,7 @@ inline void RendererAgg::draw_path_collection(GCAgg &gc, linewidths, linestyles, antialiaseds, + hatch_paths, true, true); } @@ -1127,7 +1138,7 @@ class QuadMeshGenerator } }; -template +template inline void RendererAgg::draw_quad_mesh(GCAgg &gc, agg::trans_affine &master_transform, unsigned int mesh_width, @@ -1137,7 +1148,8 @@ inline void RendererAgg::draw_quad_mesh(GCAgg &gc, agg::trans_affine &offset_trans, ColorArray &facecolors, bool antialiased, - ColorArray &edgecolors) + ColorArray &edgecolors, + PathGenerator &hatch_paths) { QuadMeshGenerator path_generator(mesh_width, mesh_height, coordinates); @@ -1160,6 +1172,7 @@ inline void RendererAgg::draw_quad_mesh(GCAgg &gc, linewidths, linestyles, antialiaseds, + hatch_paths, true, // check_snap false); } diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index eaf4bf6f5f9d..c05ed8f0650c 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -301,11 +301,12 @@ PyRendererAgg_draw_path_collection(PyRendererAgg *self, PyObject *args) numpy::array_view linewidths; DashesVector dashes; numpy::array_view antialiaseds; + mpl::PathGenerator hatch_paths; PyObject *ignored; PyObject *offset_position; // offset position is no longer used if (!PyArg_ParseTuple(args, - "O&O&O&O&O&O&O&O&O&O&O&OO:draw_path_collection", + "O&O&O&O&O&O&O&O&O&O&O&O&OO:draw_path_collection", &convert_gcagg, &gc, &convert_trans_affine, @@ -328,6 +329,8 @@ PyRendererAgg_draw_path_collection(PyRendererAgg *self, PyObject *args) &dashes, &antialiaseds.converter, &antialiaseds, + &convert_pathgen, + &hatch_paths, &ignored, &offset_position)) { return NULL; @@ -344,7 +347,8 @@ PyRendererAgg_draw_path_collection(PyRendererAgg *self, PyObject *args) edgecolors, linewidths, dashes, - antialiaseds))); + antialiaseds, + hatch_paths))); Py_RETURN_NONE; } @@ -361,9 +365,10 @@ static PyObject *PyRendererAgg_draw_quad_mesh(PyRendererAgg *self, PyObject *arg numpy::array_view facecolors; bool antialiased; numpy::array_view edgecolors; + mpl::PathGenerator hatch_paths; if (!PyArg_ParseTuple(args, - "O&O&IIO&O&O&O&O&O&:draw_quad_mesh", + "O&O&IIO&O&O&O&O&O&O&:draw_quad_mesh", &convert_gcagg, &gc, &convert_trans_affine, @@ -381,7 +386,9 @@ static PyObject *PyRendererAgg_draw_quad_mesh(PyRendererAgg *self, PyObject *arg &convert_bool, &antialiased, &convert_colors, - &edgecolors)) { + &edgecolors, + &convert_pathgen, + &hatch_paths)) { return NULL; } @@ -395,7 +402,8 @@ static PyObject *PyRendererAgg_draw_quad_mesh(PyRendererAgg *self, PyObject *arg offset_trans, facecolors, antialiased, - edgecolors))); + edgecolors, + hatch_paths))); Py_RETURN_NONE; }