From d7da1fce96e11e31c24700fe798fcaabe193f109 Mon Sep 17 00:00:00 2001 From: anTon <138380708+r3kste@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:39:44 +0530 Subject: [PATCH 01/16] add hatchcolors param for collections --- lib/matplotlib/backend_bases.py | 32 +++++++---- lib/matplotlib/backend_bases.pyi | 2 + lib/matplotlib/backends/backend_pdf.py | 9 ++-- lib/matplotlib/backends/backend_ps.py | 8 +-- lib/matplotlib/backends/backend_svg.py | 8 +-- lib/matplotlib/collections.py | 54 +++++++++++++------ lib/matplotlib/collections.pyi | 5 ++ lib/matplotlib/legend_handler.py | 3 +- lib/matplotlib/tests/test_collections.py | 69 ++++++++++++++++++++++++ src/_backend_agg.h | 32 +++++++---- src/_backend_agg_wrapper.cpp | 19 ++++--- 11 files changed, 190 insertions(+), 51 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 62c26a90a91c..494f40b1ff06 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -208,7 +208,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, - offset_position): + offset_position, hatchcolors=None): """ Draw a collection of *paths*. @@ -217,8 +217,8 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, *master_transform*. They are then translated by the corresponding entry in *offsets*, which has been first transformed by *offset_trans*. - *facecolors*, *edgecolors*, *linewidths*, *linestyles*, and - *antialiased* are lists that set the corresponding properties. + *facecolors*, *edgecolors*, *linewidths*, *linestyles*, *antialiased* + and *hatchcolors* are lists that set the corresponding properties. *offset_position* is unused now, but the argument is kept for backwards compatibility. @@ -235,10 +235,13 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, path_ids = self._iter_collection_raw_paths(master_transform, paths, all_transforms) + if hatchcolors is None: + hatchcolors = [] + 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, urls, offset_position, hatchcolors): path, transform = path_id # Only apply another translation if we have an offset, else we # reuse the initial transform. @@ -252,7 +255,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, hatchcolors=None): """ Draw a quadmesh. @@ -265,11 +268,13 @@ def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, if edgecolors is None: edgecolors = facecolors + if hatchcolors is None: + hatchcolors = [] linewidths = np.array([gc.get_linewidth()], float) return self.draw_path_collection( gc, master_transform, paths, [], offsets, offsetTrans, facecolors, - edgecolors, linewidths, [], [antialiased], [None], 'screen') + edgecolors, linewidths, [], [antialiased], [None], 'screen', hatchcolors) def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): @@ -337,7 +342,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, urls, offset_position, hatchcolors=None): """ Helper method (along with `_iter_collection_raw_paths`) to implement `draw_path_collection` in a memory-efficient manner. @@ -360,16 +365,20 @@ def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors, *path_ids*; *gc* is a graphics context and *rgbFace* is a color to use for filling the path. """ + if hatchcolors is None: + hatchcolors = [] + Npaths = len(path_ids) Noffsets = len(offsets) N = max(Npaths, Noffsets) Nfacecolors = len(facecolors) Nedgecolors = len(edgecolors) + Nhatchcolors = len(hatchcolors) Nlinewidths = len(linewidths) Nlinestyles = len(linestyles) Nurls = len(urls) - if (Nfacecolors == 0 and Nedgecolors == 0) or Npaths == 0: + if (Nfacecolors == 0 and Nedgecolors == 0 and Nhatchcolors == 0) or Npaths == 0: return gc0 = self.new_gc() @@ -384,6 +393,7 @@ def cycle_or_default(seq, default=None): toffsets = cycle_or_default(offset_trans.transform(offsets), (0, 0)) fcs = cycle_or_default(facecolors) ecs = cycle_or_default(edgecolors) + hcs = cycle_or_default(hatchcolors) lws = cycle_or_default(linewidths) lss = cycle_or_default(linestyles) aas = cycle_or_default(antialiaseds) @@ -392,8 +402,8 @@ def cycle_or_default(seq, default=None): 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, hc, lw, ls, aa, url in itertools.islice( + zip(pathids, toffsets, fcs, ecs, hcs, lws, lss, aas, urls), N): if not (np.isfinite(xo) and np.isfinite(yo)): continue if Nedgecolors: @@ -405,6 +415,8 @@ def cycle_or_default(seq, default=None): gc0.set_linewidth(0) else: gc0.set_foreground(ec) + if Nhatchcolors: + gc0.set_hatch_color(hc) if fc is not None and len(fc) == 4 and fc[3] == 0: fc = None gc0.set_antialiased(aa) diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index 898d6fb0e7f5..33c10adf9c2a 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -63,6 +63,7 @@ class RendererBase: antialiaseds: bool | Sequence[bool], urls: str | Sequence[str], offset_position: Any, + hatchcolors: ColorType | Sequence[ColorType] | None = None, ) -> None: ... def draw_quad_mesh( self, @@ -76,6 +77,7 @@ class RendererBase: facecolors: Sequence[ColorType], antialiased: bool, edgecolors: Sequence[ColorType] | ColorType | None, + hatchcolors: Sequence[ColorType] | ColorType | None = None, ) -> None: ... def draw_gouraud_triangles( self, diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index c1c5eb8819be..0607b5455414 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2030,7 +2030,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, - offset_position): + offset_position, hatchcolors=None): # We can only reuse the objects if the presence of fill and # stroke (and the amount of alpha for each) is the same for # all of them @@ -2038,6 +2038,9 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, facecolors = np.asarray(facecolors) edgecolors = np.asarray(edgecolors) + if hatchcolors is None: + hatchcolors = [] + if not len(facecolors): filled = False can_do_optimization = not gc.get_hatch() @@ -2072,7 +2075,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, - offset_position) + offset_position, hatchcolors) padding = np.max(linewidths) path_codes = [] @@ -2088,7 +2091,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, urls, offset_position, hatchcolors): 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 f1f914ae5420..18d69c8684b8 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -674,7 +674,9 @@ def draw_markers( def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, - offset_position): + offset_position, hatchcolors=None): + if hatchcolors is None: + hatchcolors = [] # Is the optimization worth it? Rough calculation: # cost of emitting a path in-line is # (len_path + 2) * uses_per_path @@ -690,7 +692,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, - offset_position) + offset_position, hatchcolors) path_codes = [] for i, (path, transform) in enumerate(self._iter_collection_raw_paths( @@ -709,7 +711,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, urls, offset_position, hatchcolors): 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 2193dc6b6cdc..9ae079dd2260 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -736,7 +736,9 @@ def draw_markers( def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, - offset_position): + offset_position, hatchcolors=None): + if hatchcolors is None: + hatchcolors = [] # Is the optimization worth it? Rough calculation: # cost of emitting a path in-line is # (len_path + 5) * uses_per_path @@ -752,7 +754,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, - offset_position) + offset_position, hatchcolors) writer = self.writer path_codes = [] @@ -770,7 +772,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, urls, offset_position, hatchcolors): 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 a3f245bbc2c8..a29ec87f07fd 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -79,6 +79,7 @@ class Collection(mcolorizer.ColorizingArtist): def __init__(self, *, edgecolors=None, facecolors=None, + hatchcolors=None, linewidths=None, linestyles='solid', capstyle=None, @@ -174,13 +175,6 @@ def __init__(self, *, self._face_is_mapped = None self._edge_is_mapped = None self._mapped_colors = None # calculated in update_scalarmappable - - # Temporary logic to set hatchcolor. This eager resolution is temporary - # and will be replaced by a proper mechanism in a follow-up PR. - hatch_color = mpl.rcParams['hatch.color'] - if hatch_color == 'edge': - hatch_color = mpl.rcParams['patch.edgecolor'] - self._hatch_color = mcolors.to_rgba(hatch_color) self._hatch_linewidth = mpl.rcParams['hatch.linewidth'] self.set_facecolor(facecolors) self.set_edgecolor(edgecolors) @@ -190,6 +184,7 @@ def __init__(self, *, self.set_pickradius(pickradius) self.set_urls(urls) self.set_hatch(hatch) + self.set_hatchcolor(hatchcolors) self.set_zorder(zorder) if capstyle: @@ -371,7 +366,6 @@ def draw(self, renderer): if self._hatch: gc.set_hatch(self._hatch) - gc.set_hatch_color(self._hatch_color) gc.set_hatch_linewidth(self._hatch_linewidth) if self.get_sketch_params() is not None: @@ -431,7 +425,7 @@ def draw(self, renderer): [mcolors.to_rgba("none")], self._gapcolor, self._linewidths, ilinestyles, self._antialiaseds, self._urls, - "screen") + "screen", self.get_hatchcolor()) renderer.draw_path_collection( gc, transform.frozen(), paths, @@ -439,7 +433,8 @@ def draw(self, renderer): self.get_facecolor(), self.get_edgecolor(), self._linewidths, self._linestyles, self._antialiaseds, self._urls, - "screen") # offset_position, kept for backcompat. + "screen", # offset_position, kept for backcompat. + self.get_hatchcolor()) gc.restore() renderer.close_group(self.__class__.__name__) @@ -814,8 +809,15 @@ def _get_default_edgecolor(self): # This may be overridden in a subclass. return mpl.rcParams['patch.edgecolor'] + def get_hatchcolor(self): + if cbook._str_equal(self._hatchcolors, 'edge'): + if self.get_edgecolor().size == 0: + return mpl.colors.to_rgba_array(self._get_default_edgecolor(), + self._alpha) + return self.get_edgecolor() + return self._hatchcolors + def _set_edgecolor(self, c): - set_hatch_color = True if c is None: if (mpl.rcParams['patch.force_edgecolor'] or self._edge_default @@ -823,14 +825,11 @@ def _set_edgecolor(self, c): c = self._get_default_edgecolor() else: c = 'none' - set_hatch_color = False if cbook._str_lower_equal(c, 'face'): self._edgecolors = 'face' self.stale = True return self._edgecolors = mcolors.to_rgba_array(c, self._alpha) - if set_hatch_color and len(self._edgecolors): - self._hatch_color = tuple(self._edgecolors[0]) self.stale = True def set_edgecolor(self, c): @@ -851,6 +850,29 @@ def set_edgecolor(self, c): self._original_edgecolor = c self._set_edgecolor(c) + def _set_hatchcolor(self, c): + c = mpl._val_or_rc(c, 'hatch.color') + if c == 'edge': + self._hatchcolors = 'edge' + else: + self._hatchcolors = mcolors.to_rgba_array(c, self._alpha) + self.stale = True + + def set_hatchcolor(self, c): + """ + Set the hatchcolor(s) of the collection. + + Parameters + ---------- + c : :mpltype:`color` or list of :mpltype:`color` or 'edge' + The collection hatchcolor(s). If a sequence, the patches cycle + through it. + """ + if cbook._str_equal(c, 'edge'): + c = 'edge' + self._original_hatchcolor = c + self._set_hatchcolor(c) + def set_alpha(self, alpha): """ Set the transparency of the collection. @@ -968,6 +990,7 @@ def update_from(self, other): self._us_linestyles = other._us_linestyles self._pickradius = other._pickradius self._hatch = other._hatch + self._hatchcolors = other._hatchcolors # update_from for scalarmappable self._A = other._A @@ -2465,7 +2488,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_hatchcolor().reshape((-1, 4))) gc.restore() renderer.close_group(self.__class__.__name__) self.stale = False diff --git a/lib/matplotlib/collections.pyi b/lib/matplotlib/collections.pyi index 0805adef4293..a7ad264fb59d 100644 --- a/lib/matplotlib/collections.pyi +++ b/lib/matplotlib/collections.pyi @@ -21,6 +21,7 @@ class Collection(colorizer.ColorizingArtist): *, edgecolors: ColorType | Sequence[ColorType] | None = ..., facecolors: ColorType | Sequence[ColorType] | None = ..., + hatchcolors: ColorType | Sequence[ColorType] | None = ..., linewidths: float | Sequence[float] | None = ..., linestyles: LineStyleType | Sequence[LineStyleType] = ..., capstyle: CapStyleType | None = ..., @@ -66,6 +67,10 @@ class Collection(colorizer.ColorizingArtist): def get_facecolor(self) -> ColorType | Sequence[ColorType]: ... def get_edgecolor(self) -> ColorType | Sequence[ColorType]: ... def set_edgecolor(self, c: ColorType | Sequence[ColorType]) -> None: ... + def get_hatchcolor(self) -> ColorType | Sequence[ColorType]: ... + def set_hatchcolor( + self, c: ColorType | Sequence[ColorType] | Literal["edge"] + ) -> None: ... def set_alpha(self, alpha: float | Sequence[float] | None) -> None: ... def get_linewidth(self) -> float | Sequence[float]: ... def get_linestyle(self) -> LineStyleType | Sequence[LineStyleType]: ... diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 97076ad09cb8..263945b050d0 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -790,12 +790,11 @@ def get_first(prop_array): # Directly set Patch color attributes (must be RGBA tuples). legend_handle._facecolor = first_color(orig_handle.get_facecolor()) legend_handle._edgecolor = first_color(orig_handle.get_edgecolor()) + legend_handle._hatch_color = first_color(orig_handle.get_hatchcolor()) legend_handle._original_facecolor = orig_handle._original_facecolor legend_handle._original_edgecolor = orig_handle._original_edgecolor legend_handle._fill = orig_handle.get_fill() legend_handle._hatch = orig_handle.get_hatch() - # Hatch color is anomalous in having no getters and setters. - legend_handle._hatch_color = orig_handle._hatch_color # Setters are fine for the remaining attributes. legend_handle.set_linewidth(get_first(orig_handle.get_linewidths())) legend_handle.set_linestyle(get_first(orig_handle.get_linestyles())) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index db9c4be81a44..17d935536cfd 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -1384,3 +1384,72 @@ def test_hatch_linewidth(fig_test, fig_ref): ax_test.add_collection(test) assert test.get_hatch_linewidth() == ref.get_hatch_linewidth() == lw + + +def test_collection_hatchcolor_inherit_logic(): + from matplotlib.collections import PathCollection + path = mpath.Path.unit_rectangle() + + colors_1 = ['purple', 'red', 'green', 'yellow'] + colors_2 = ['orange', 'cyan', 'blue', 'magenta'] + with mpl.rc_context({'hatch.color': 'edge'}): + # edgecolor and hatchcolor is set + col = PathCollection([path], hatch='//', + edgecolor=colors_1, hatchcolor=colors_2) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_2)) + + # explicitly setting edgecolor and then hatchcolor + col = PathCollection([path], hatch='//') + col.set_edgecolor(colors_1) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) + col.set_hatchcolor(colors_2) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_2)) + + # explicitly setting hatchcolor and then edgecolor + col = PathCollection([path], hatch='//') + col.set_hatchcolor(colors_1) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) + col.set_edgecolor(colors_2) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) + + +def test_collection_hatchcolor_fallback_logic(): + from matplotlib.collections import PathCollection + path = mpath.Path.unit_rectangle() + + colors_1 = ['purple', 'red', 'green', 'yellow'] + colors_2 = ['orange', 'cyan', 'blue', 'magenta'] + + # hatchcolor parameter should take precedence over rcParam + # When edgecolor is not set + with mpl.rc_context({'hatch.color': 'green'}): + col = PathCollection([path], hatch='//', hatchcolor=colors_1) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) + # When edgecolor is set + with mpl.rc_context({'hatch.color': 'green'}): + col = PathCollection([path], hatch='//', + edgecolor=colors_2, hatchcolor=colors_1) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) + + # hatchcolor should not be overridden by edgecolor when + # hatchcolor parameter is not passed and hatch.color rcParam is set to a color + with mpl.rc_context({'hatch.color': 'green'}): + col = PathCollection([path], hatch='//') + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array('green')) + col.set_edgecolor(colors_1) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array('green')) + + # hatchcolor should match edgecolor when + # hatchcolor parameter is not passed and hatch.color rcParam is set to 'edge' + with mpl.rc_context({'hatch.color': 'edge'}): + col = PathCollection([path], hatch='//', edgecolor=colors_1) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) + # hatchcolor parameter is set to 'edge' + col = PathCollection([path], hatch='//', edgecolor=colors_1, hatchcolor='edge') + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) + + # default hatchcolor should be used when hatchcolor parameter is not passed and + # hatch.color rcParam is set to 'edge' and edgecolor is not set + col = PathCollection([path], hatch='//') + assert_array_equal(col.get_hatchcolor(), + mpl.colors.to_rgba_array(mpl.rcParams['patch.edgecolor'])) diff --git a/src/_backend_agg.h b/src/_backend_agg.h index f1fbf11ea4e5..0e33d38dfd93 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -177,7 +177,8 @@ class RendererAgg ColorArray &edgecolors, LineWidthArray &linewidths, DashesVector &linestyles, - AntialiasedArray &antialiaseds); + AntialiasedArray &antialiaseds, + ColorArray &hatchcolors); template void draw_quad_mesh(GCAgg &gc, @@ -189,7 +190,8 @@ class RendererAgg agg::trans_affine &offset_trans, ColorArray &facecolors, bool antialiased, - ColorArray &edgecolors); + ColorArray &edgecolors, + ColorArray &hatchcolors); template void draw_gouraud_triangles(GCAgg &gc, @@ -272,7 +274,8 @@ class RendererAgg DashesVector &linestyles, AntialiasedArray &antialiaseds, bool check_snap, - bool has_codes); + bool has_codes, + ColorArray &hatchcolors); template void _draw_gouraud_triangle(PointArray &points, @@ -917,7 +920,8 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, DashesVector &linestyles, AntialiasedArray &antialiaseds, bool check_snap, - bool has_codes) + bool has_codes, + ColorArray &hatchcolors) { typedef agg::conv_transform transformed_path_t; typedef PathNanRemover nan_removed_t; @@ -937,11 +941,12 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, size_t Ntransforms = safe_first_shape(transforms); size_t Nfacecolors = safe_first_shape(facecolors); size_t Nedgecolors = safe_first_shape(edgecolors); + size_t Nhatchcolors = safe_first_shape(hatchcolors); size_t Nlinewidths = safe_first_shape(linewidths); size_t Nlinestyles = std::min(linestyles.size(), N); size_t Naa = safe_first_shape(antialiaseds); - if ((Nfacecolors == 0 && Nedgecolors == 0) || Npaths == 0) { + if ((Nfacecolors == 0 && Nedgecolors == 0 && Nhatchcolors == 0) || Npaths == 0) { return; } @@ -1004,6 +1009,11 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, } } + if(Nhatchcolors) { + int ic = i % Nhatchcolors; + gc.hatch_color = agg::rgba(hatchcolors(ic, 0), hatchcolors(ic, 1), hatchcolors(ic, 2), hatchcolors(ic, 3)); + } + gc.isaa = antialiaseds(i % Naa); transformed_path_t tpath(path, trans); nan_removed_t nan_removed(tpath, true, has_codes); @@ -1048,7 +1058,8 @@ inline void RendererAgg::draw_path_collection(GCAgg &gc, ColorArray &edgecolors, LineWidthArray &linewidths, DashesVector &linestyles, - AntialiasedArray &antialiaseds) + AntialiasedArray &antialiaseds, + ColorArray &hatchcolors) { _draw_path_collection_generic(gc, master_transform, @@ -1065,7 +1076,8 @@ inline void RendererAgg::draw_path_collection(GCAgg &gc, linestyles, antialiaseds, true, - true); + true, + hatchcolors); } template @@ -1151,7 +1163,8 @@ inline void RendererAgg::draw_quad_mesh(GCAgg &gc, agg::trans_affine &offset_trans, ColorArray &facecolors, bool antialiased, - ColorArray &edgecolors) + ColorArray &edgecolors, + ColorArray &hatchcolors) { QuadMeshGenerator path_generator(mesh_width, mesh_height, coordinates); @@ -1175,7 +1188,8 @@ inline void RendererAgg::draw_quad_mesh(GCAgg &gc, linestyles, antialiaseds, true, // check_snap - false); + false, + hatchcolors); } template diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 269e2aaa9ee5..39d8009748d3 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -146,12 +146,14 @@ PyRendererAgg_draw_path_collection(RendererAgg *self, py::array_t antialiaseds_obj, py::object Py_UNUSED(ignored_obj), // offset position is no longer used - py::object Py_UNUSED(offset_position_obj)) + py::object Py_UNUSED(offset_position_obj), + py::array_t hatchcolors_obj) { auto transforms = convert_transforms(transforms_obj); auto offsets = convert_points(offsets_obj); auto facecolors = convert_colors(facecolors_obj); auto edgecolors = convert_colors(edgecolors_obj); + auto hatchcolors = convert_colors(hatchcolors_obj); auto linewidths = linewidths_obj.unchecked<1>(); auto antialiaseds = antialiaseds_obj.unchecked<1>(); @@ -165,7 +167,8 @@ PyRendererAgg_draw_path_collection(RendererAgg *self, edgecolors, linewidths, dashes, - antialiaseds); + antialiaseds, + hatchcolors); } static void @@ -179,12 +182,14 @@ PyRendererAgg_draw_quad_mesh(RendererAgg *self, agg::trans_affine offset_trans, py::array_t facecolors_obj, bool antialiased, - py::array_t edgecolors_obj) + py::array_t edgecolors_obj, + py::array_t hatchcolors_obj) { auto coordinates = coordinates_obj.mutable_unchecked<3>(); auto offsets = convert_points(offsets_obj); auto facecolors = convert_colors(facecolors_obj); auto edgecolors = convert_colors(edgecolors_obj); + auto hatchcolors = convert_colors(hatchcolors_obj); self->draw_quad_mesh(gc, master_transform, @@ -195,7 +200,8 @@ PyRendererAgg_draw_quad_mesh(RendererAgg *self, offset_trans, facecolors, antialiased, - edgecolors); + edgecolors, + hatchcolors); } static void @@ -229,11 +235,12 @@ PYBIND11_MODULE(_backend_agg, m, py::mod_gil_not_used()) .def("draw_path_collection", &PyRendererAgg_draw_path_collection, "gc"_a, "master_transform"_a, "paths"_a, "transforms"_a, "offsets"_a, "offset_trans"_a, "facecolors"_a, "edgecolors"_a, "linewidths"_a, - "dashes"_a, "antialiaseds"_a, "ignored"_a, "offset_position"_a) + "dashes"_a, "antialiaseds"_a, "ignored"_a, "offset_position"_a, + "hatchcolors"_a = nullptr) .def("draw_quad_mesh", &PyRendererAgg_draw_quad_mesh, "gc"_a, "master_transform"_a, "mesh_width"_a, "mesh_height"_a, "coordinates"_a, "offsets"_a, "offset_trans"_a, "facecolors"_a, - "antialiased"_a, "edgecolors"_a) + "antialiased"_a, "edgecolors"_a, "hatchcolors"_a = nullptr) .def("draw_gouraud_triangles", &PyRendererAgg_draw_gouraud_triangles, "gc"_a, "points"_a, "colors"_a, "trans"_a = nullptr) From 2fd17a09b16a58188efc692e4fbc7a13cb6c4ac5 Mon Sep 17 00:00:00 2001 From: anTon <138380708+r3kste@users.noreply.github.com> Date: Fri, 3 Jan 2025 19:52:13 +0530 Subject: [PATCH 02/16] minor fixes --- lib/matplotlib/collections.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index a29ec87f07fd..2ba655aa99bb 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -105,6 +105,10 @@ def __init__(self, *, facecolor. facecolors : :mpltype:`color` or list of colors, default: :rc:`patch.facecolor` Face color for each patch making up the collection. + hatchcolors : :mpltype:`color` or list of colors, default: :rc:`hatch.color` + Hatch color for each patch making up the collection. The special + value 'edge' can be passed to make the hatchcolor match the + edgecolor. linewidths : float or list of floats, default: :rc:`patch.linewidth` Line width for each patch making up the collection. linestyles : str or tuple or list thereof, default: 'solid' @@ -754,7 +758,7 @@ def _get_default_antialiased(self): def set_color(self, c): """ - Set both the edgecolor and the facecolor. + Sets the edgecolor, facecolor and hatchcolor. Parameters ---------- @@ -767,6 +771,7 @@ def set_color(self, c): """ self.set_facecolor(c) self.set_edgecolor(c) + self.set_hatchcolor(c) def _get_default_facecolor(self): # This may be overridden in a subclass. @@ -811,7 +816,7 @@ def _get_default_edgecolor(self): def get_hatchcolor(self): if cbook._str_equal(self._hatchcolors, 'edge'): - if self.get_edgecolor().size == 0: + if len(self.get_edgecolor()) == 0: return mpl.colors.to_rgba_array(self._get_default_edgecolor(), self._alpha) return self.get_edgecolor() @@ -852,7 +857,7 @@ def set_edgecolor(self, c): def _set_hatchcolor(self, c): c = mpl._val_or_rc(c, 'hatch.color') - if c == 'edge': + if cbook._str_equal(c, 'edge'): self._hatchcolors = 'edge' else: self._hatchcolors = mcolors.to_rgba_array(c, self._alpha) @@ -868,8 +873,6 @@ def set_hatchcolor(self, c): The collection hatchcolor(s). If a sequence, the patches cycle through it. """ - if cbook._str_equal(c, 'edge'): - c = 'edge' self._original_hatchcolor = c self._set_hatchcolor(c) @@ -888,6 +891,7 @@ def set_alpha(self, alpha): artist.Artist._set_alpha_for_array(self, alpha) self._set_facecolor(self._original_facecolor) self._set_edgecolor(self._original_edgecolor) + self._set_hatchcolor(self._original_hatchcolor) set_alpha.__doc__ = artist.Artist._set_alpha_for_array.__doc__ From f5978ceb30016af0feb46a0860958850501e9579 Mon Sep 17 00:00:00 2001 From: anTon <138380708+r3kste@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:47:41 +0530 Subject: [PATCH 03/16] added gallery example --- .../shapes_and_collections/hatchcolor_demo.py | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/galleries/examples/shapes_and_collections/hatchcolor_demo.py b/galleries/examples/shapes_and_collections/hatchcolor_demo.py index 7125ddb57fe7..0b50b5ad825e 100644 --- a/galleries/examples/shapes_and_collections/hatchcolor_demo.py +++ b/galleries/examples/shapes_and_collections/hatchcolor_demo.py @@ -1,7 +1,10 @@ """ -================ -Patch hatchcolor -================ +=============== +Hatchcolor Demo +=============== + +Patch Hatchcolor +---------------- This example shows how to use the *hatchcolor* parameter to set the color of the hatch. The *hatchcolor* parameter is available for `~.patches.Patch`, @@ -11,6 +14,7 @@ import matplotlib.pyplot as plt import numpy as np +import matplotlib.cm as cm from matplotlib.patches import Rectangle fig, (ax1, ax2) = plt.subplots(1, 2) @@ -28,6 +32,49 @@ ax2.set_xlim(0, 5) ax2.set_ylim(0, 5) +# %% +# Collection Hatchcolor +# --------------------- +# +# The following example shows how to use the *hatchcolor* parameter to set the color of +# the hatch in a scatter plot. The *hatchcolor* parameter can also be passed to +# `~.collections.Collection`, child classes of Collection, and methods that pass +# through to Collection. + +fig, ax = plt.subplots() + +num_points_x = 10 +num_points_y = 9 +x = np.linspace(0, 1, num_points_x) +y = np.linspace(0, 1, num_points_y) + +X, Y = np.meshgrid(x, y) +X[1::2, :] += (x[1] - x[0]) / 2 # stagger every alternate row + +# As ax.scatter (PathCollection) is drawn row by row, setting hatchcolors to the +# first row is enough, as the colors will be cycled through for the next rows. +colors = [cm.rainbow(val) for val in x] + +ax.scatter( + X.ravel(), + Y.ravel(), + s=1700, + facecolor="none", + edgecolor="gray", + linewidth=2, + marker="h", # Use hexagon as marker + hatch="xxx", + hatchcolor=colors, +) +ax.set_xlim(0, 1) +ax.set_ylim(0, 1) + +# Remove ticks and labels +ax.set_xticks([]) +ax.set_yticks([]) +ax.set_xticklabels([]) +ax.set_yticklabels([]) + plt.show() # %% @@ -41,3 +88,5 @@ # - `matplotlib.patches.Polygon` # - `matplotlib.axes.Axes.add_patch` # - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` +# - `matplotlib.collections` +# - `matplotlib.axes.Axes.scatter` / `matplotlib.pyplot.scatter` From 5377127fd920130f5969ed4f1c9a063a60fc68d1 Mon Sep 17 00:00:00 2001 From: anTon <138380708+r3kste@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:50:16 +0530 Subject: [PATCH 04/16] documented hatchcolor parameter for collections in next whats new entry --- .../next_whats_new/separated_hatchcolor.rst | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/doc/users/next_whats_new/separated_hatchcolor.rst b/doc/users/next_whats_new/separated_hatchcolor.rst index f3932cf876f8..b8c3f7d36aa8 100644 --- a/doc/users/next_whats_new/separated_hatchcolor.rst +++ b/doc/users/next_whats_new/separated_hatchcolor.rst @@ -57,3 +57,30 @@ Previously, hatch colors were the same as edge colors, with a fallback to xy=(.5, 1.03), xycoords=patch4, ha='center', va='bottom') plt.show() + +For collections, a sequence of colors can be passed to the *hatchcolor* parameter +which will be cycled through for each hatch, similar to *facecolor* and *edgecolor*. + +.. plot:: + :include-source: true + :alt: A scatter plot of a quadratic function with hatches on the markers. The hatches are colored in blue, orange, and green, respectively. After the first three markers, the colors are cycled through again. + + import matplotlib.pyplot as plt + import numpy as np + + fig, ax = plt.subplots() + + x = np.linspace(0, 1, 10) + y = x**2 + colors = ["blue", "orange", "green"] + + ax.scatter( + x, + y, + s=800, + hatch="xxxx", + hatchcolor=colors, + facecolor="none", + edgecolor="black", + ) + plt.show() From 6f18c499c1288d18200779229b1a2681efd11e52 Mon Sep 17 00:00:00 2001 From: r3kste <138380708+r3kste@users.noreply.github.com> Date: Mon, 27 Jan 2025 20:48:11 +0530 Subject: [PATCH 05/16] update whats new for hatchcolor in collections --- doc/users/next_whats_new/separated_hatchcolor.rst | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/users/next_whats_new/separated_hatchcolor.rst b/doc/users/next_whats_new/separated_hatchcolor.rst index b8c3f7d36aa8..4652458083e3 100644 --- a/doc/users/next_whats_new/separated_hatchcolor.rst +++ b/doc/users/next_whats_new/separated_hatchcolor.rst @@ -61,17 +61,25 @@ Previously, hatch colors were the same as edge colors, with a fallback to For collections, a sequence of colors can be passed to the *hatchcolor* parameter which will be cycled through for each hatch, similar to *facecolor* and *edgecolor*. +Previously, if *edgecolor* was not specified, the hatch color would fall back to +:rc:`patch.edgecolor`, but the alpha value would default to **1.0**, regardless of the +alpha value of the collection. This behavior has been changed such that, if both +*hatchcolor* and *edgecolor* are not specified, the hatch color will fall back +to 'patch.edgecolor' with the alpha value of the collection. + .. plot:: :include-source: true - :alt: A scatter plot of a quadratic function with hatches on the markers. The hatches are colored in blue, orange, and green, respectively. After the first three markers, the colors are cycled through again. + :alt: A random scatter plot with hatches on the markers. The hatches are colored in blue, orange, and green, respectively. After the first three markers, the colors are cycled through again. import matplotlib.pyplot as plt import numpy as np + np.random.seed(19680801) + fig, ax = plt.subplots() - x = np.linspace(0, 1, 10) - y = x**2 + x = np.random.rand(20) + y = np.random.rand(20) colors = ["blue", "orange", "green"] ax.scatter( @@ -83,4 +91,5 @@ which will be cycled through for each hatch, similar to *facecolor* and *edgecol facecolor="none", edgecolor="black", ) + plt.show() From 8a7cd656ef1ce15678b5b5f3cf3e4e0c3d9e710e Mon Sep 17 00:00:00 2001 From: r3kste <138380708+r3kste@users.noreply.github.com> Date: Wed, 29 Jan 2025 22:07:06 +0530 Subject: [PATCH 06/16] update contourf hatch test with cmap.with_alpha() --- lib/matplotlib/tests/test_axes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 10b6af8d5883..9dd50e436ea3 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -2649,8 +2649,8 @@ def test_contour_hatching(): x, y, z = contour_dat() fig, ax = plt.subplots() ax.contourf(x, y, z, 7, hatches=['/', '\\', '//', '-'], - cmap=mpl.colormaps['gray'], - extend='both', alpha=0.5) + cmap=mpl.colormaps['gray'].with_alpha(0.5), + extend='both') @image_comparison(['contour_colorbar'], style='mpl20', From e3b03fcb5b068e7f2e9442106d5d0494606bab42 Mon Sep 17 00:00:00 2001 From: Pranav Date: Wed, 5 Feb 2025 16:05:20 +0530 Subject: [PATCH 07/16] grammar nits --- lib/matplotlib/backend_bases.py | 3 +++ lib/matplotlib/collections.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 494f40b1ff06..68bc0441950f 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -220,6 +220,9 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, *facecolors*, *edgecolors*, *linewidths*, *linestyles*, *antialiased* and *hatchcolors* are lists that set the corresponding properties. + .. versionadded:: 3.11 + Allowing *hatchcolors* to be specified. + *offset_position* is unused now, but the argument is kept for backwards compatibility. diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 2ba655aa99bb..46c288be9b58 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -106,8 +106,8 @@ def __init__(self, *, facecolors : :mpltype:`color` or list of colors, default: :rc:`patch.facecolor` Face color for each patch making up the collection. hatchcolors : :mpltype:`color` or list of colors, default: :rc:`hatch.color` - Hatch color for each patch making up the collection. The special - value 'edge' can be passed to make the hatchcolor match the + Hatch color for each patch making up the collection. The color + can be set to the special value 'edge' to make the hatchcolor match the edgecolor. linewidths : float or list of floats, default: :rc:`patch.linewidth` Line width for each patch making up the collection. @@ -758,7 +758,7 @@ def _get_default_antialiased(self): def set_color(self, c): """ - Sets the edgecolor, facecolor and hatchcolor. + Set the edgecolor, facecolor and hatchcolor. Parameters ---------- From 7668535a90725522e33763d812fa17b541d23750 Mon Sep 17 00:00:00 2001 From: r3kste <138380708+r3kste@users.noreply.github.com> Date: Wed, 5 Feb 2025 21:45:56 +0530 Subject: [PATCH 08/16] enhanced tests and made hatchcolors param required in _iter_collection() --- lib/matplotlib/backend_bases.py | 5 +-- lib/matplotlib/tests/test_backend_bases.py | 2 +- lib/matplotlib/tests/test_collections.py | 46 +++++++++++----------- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 68bc0441950f..995278019868 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -345,7 +345,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, hatchcolors=None): + antialiaseds, urls, offset_position, hatchcolors): """ Helper method (along with `_iter_collection_raw_paths`) to implement `draw_path_collection` in a memory-efficient manner. @@ -368,9 +368,6 @@ def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors, *path_ids*; *gc* is a graphics context and *rgbFace* is a color to use for filling the path. """ - if hatchcolors is None: - hatchcolors = [] - Npaths = len(path_ids) Noffsets = len(offsets) N = max(Npaths, Noffsets) diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py index ef5a52d988bb..b1ac109a913d 100644 --- a/lib/matplotlib/tests/test_backend_bases.py +++ b/lib/matplotlib/tests/test_backend_bases.py @@ -38,7 +38,7 @@ def check(master_transform, paths, all_transforms, gc, range(len(raw_paths)), offsets, transforms.AffineDeltaTransform(master_transform), facecolors, edgecolors, [], [], [False], - [], 'screen')] + [], 'screen', [])] uses = rb._iter_collection_uses_per_path( paths, all_transforms, offsets, facecolors, edgecolors) if raw_paths: diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 17d935536cfd..dc6e4e32b342 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -1390,63 +1390,63 @@ def test_collection_hatchcolor_inherit_logic(): from matplotlib.collections import PathCollection path = mpath.Path.unit_rectangle() - colors_1 = ['purple', 'red', 'green', 'yellow'] - colors_2 = ['orange', 'cyan', 'blue', 'magenta'] + edgecolors = ['purple', 'red', 'green', 'yellow'] + hatchcolors = ['orange', 'cyan', 'blue', 'magenta'] with mpl.rc_context({'hatch.color': 'edge'}): # edgecolor and hatchcolor is set col = PathCollection([path], hatch='//', - edgecolor=colors_1, hatchcolor=colors_2) - assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_2)) + edgecolor=edgecolors, hatchcolor=hatchcolors) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(hatchcolors)) # explicitly setting edgecolor and then hatchcolor col = PathCollection([path], hatch='//') - col.set_edgecolor(colors_1) - assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) - col.set_hatchcolor(colors_2) - assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_2)) + col.set_edgecolor(edgecolors) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(edgecolors)) + col.set_hatchcolor(hatchcolors) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(hatchcolors)) # explicitly setting hatchcolor and then edgecolor col = PathCollection([path], hatch='//') - col.set_hatchcolor(colors_1) - assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) - col.set_edgecolor(colors_2) - assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) + col.set_hatchcolor(hatchcolors) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(hatchcolors)) + col.set_edgecolor(edgecolors) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(hatchcolors)) def test_collection_hatchcolor_fallback_logic(): from matplotlib.collections import PathCollection path = mpath.Path.unit_rectangle() - colors_1 = ['purple', 'red', 'green', 'yellow'] - colors_2 = ['orange', 'cyan', 'blue', 'magenta'] + edgecolors = ['purple', 'red', 'green', 'yellow'] + hatchcolors = ['orange', 'cyan', 'blue', 'magenta'] # hatchcolor parameter should take precedence over rcParam # When edgecolor is not set with mpl.rc_context({'hatch.color': 'green'}): - col = PathCollection([path], hatch='//', hatchcolor=colors_1) - assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) + col = PathCollection([path], hatch='//', hatchcolor=hatchcolors) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(hatchcolors)) # When edgecolor is set with mpl.rc_context({'hatch.color': 'green'}): col = PathCollection([path], hatch='//', - edgecolor=colors_2, hatchcolor=colors_1) - assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) + edgecolor=edgecolors, hatchcolor=hatchcolors) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(hatchcolors)) # hatchcolor should not be overridden by edgecolor when # hatchcolor parameter is not passed and hatch.color rcParam is set to a color with mpl.rc_context({'hatch.color': 'green'}): col = PathCollection([path], hatch='//') assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array('green')) - col.set_edgecolor(colors_1) + col.set_edgecolor(edgecolors) assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array('green')) # hatchcolor should match edgecolor when # hatchcolor parameter is not passed and hatch.color rcParam is set to 'edge' with mpl.rc_context({'hatch.color': 'edge'}): - col = PathCollection([path], hatch='//', edgecolor=colors_1) - assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) + col = PathCollection([path], hatch='//', edgecolor=edgecolors) + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(edgecolors)) # hatchcolor parameter is set to 'edge' - col = PathCollection([path], hatch='//', edgecolor=colors_1, hatchcolor='edge') - assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(colors_1)) + col = PathCollection([path], hatch='//', edgecolor=edgecolors, hatchcolor='edge') + assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(edgecolors)) # default hatchcolor should be used when hatchcolor parameter is not passed and # hatch.color rcParam is set to 'edge' and edgecolor is not set From 9d0ec232bfea76929351a7749ef7aa023b6bfc3a Mon Sep 17 00:00:00 2001 From: r3kste <138380708+r3kste@users.noreply.github.com> Date: Sat, 8 Feb 2025 08:37:40 +0530 Subject: [PATCH 09/16] smoke test for hatchcolors coercion --- lib/matplotlib/tests/test_collections.py | 33 ++++++++++++++++++++++++ src/_backend_agg_wrapper.cpp | 4 +-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index dc6e4e32b342..cb29508ec010 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -1453,3 +1453,36 @@ def test_collection_hatchcolor_fallback_logic(): col = PathCollection([path], hatch='//') assert_array_equal(col.get_hatchcolor(), mpl.colors.to_rgba_array(mpl.rcParams['patch.edgecolor'])) + + +@pytest.mark.parametrize('backend', ['agg', 'pdf', 'svg', 'ps']) +def test_draw_path_collection_no_hatchcolor(backend): + from matplotlib.collections import PathCollection + path = mpath.Path.unit_rectangle() + + plt.switch_backend(backend) + fig, ax = plt.subplots() + renderer = fig._get_renderer() + + col = PathCollection([path], hatch='//') + ax.add_collection(col) + + gc = renderer.new_gc() + transform = mtransforms.IdentityTransform() + paths = col.get_paths() + transforms = col.get_transforms() + offsets = col.get_offsets() + offset_trf = col.get_offset_transform() + facecolors = col.get_facecolor() + edgecolors = col.get_edgecolor() + linewidths = col.get_linewidth() + linestyles = col.get_linestyle() + antialiaseds = col.get_antialiased() + urls = col.get_urls() + offset_position = "screen" + + renderer.draw_path_collection( + gc, transform, paths, transforms, offsets, offset_trf, + facecolors, edgecolors, linewidths, linestyles, + antialiaseds, urls, offset_position + ) diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 39d8009748d3..f0c029b96b98 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -236,11 +236,11 @@ PYBIND11_MODULE(_backend_agg, m, py::mod_gil_not_used()) "gc"_a, "master_transform"_a, "paths"_a, "transforms"_a, "offsets"_a, "offset_trans"_a, "facecolors"_a, "edgecolors"_a, "linewidths"_a, "dashes"_a, "antialiaseds"_a, "ignored"_a, "offset_position"_a, - "hatchcolors"_a = nullptr) + "hatchcolors"_a = py::array_t().reshape({0, 4})) .def("draw_quad_mesh", &PyRendererAgg_draw_quad_mesh, "gc"_a, "master_transform"_a, "mesh_width"_a, "mesh_height"_a, "coordinates"_a, "offsets"_a, "offset_trans"_a, "facecolors"_a, - "antialiased"_a, "edgecolors"_a, "hatchcolors"_a = nullptr) + "antialiased"_a, "edgecolors"_a, "hatchcolors"_a = py::array_t().reshape({0, 4})) .def("draw_gouraud_triangles", &PyRendererAgg_draw_gouraud_triangles, "gc"_a, "points"_a, "colors"_a, "trans"_a = nullptr) From 7e46707c29c2c9b9918fab25acb89d0566f89162 Mon Sep 17 00:00:00 2001 From: r3kste <138380708+r3kste@users.noreply.github.com> Date: Sun, 16 Feb 2025 15:00:10 +0530 Subject: [PATCH 10/16] pass hatchcolor arg only if it is supported by the renderer. and made suggested changes. --- lib/matplotlib/backends/backend_pdf.py | 5 ++- lib/matplotlib/collections.py | 47 +++++++++++++++++--------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 0607b5455414..a370330d43eb 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2606,7 +2606,10 @@ def delta(self, other): different = ours is not theirs else: different = bool(ours != theirs) - except ValueError: + except (ValueError, DeprecationWarning): + # numpy version < 1.25 raises DeprecationWarning when array shapes + # mismatch, unlike numpy >= 1.25 which raises ValueError. + # This should be removed when numpy < 1.25 is no longer supported. ours = np.asarray(ours) theirs = np.asarray(theirs) different = (ours.shape != theirs.shape or diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 46c288be9b58..9deba8b741a6 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -420,25 +420,40 @@ def draw(self, renderer): gc, paths[0], combined_transform.frozen(), mpath.Path(offsets), offset_trf, tuple(facecolors[0])) else: - if self._gapcolor is not None: - # First draw paths within the gaps. - ipaths, ilinestyles = self._get_inverse_paths_linestyles() + # Find whether renderer.draw_path_collection() takes hatchcolor parameter + hatchcolors_arg_supported = True + try: renderer.draw_path_collection( - gc, transform.frozen(), ipaths, + gc, transform.frozen(), [], self.get_transforms(), offsets, offset_trf, - [mcolors.to_rgba("none")], self._gapcolor, - self._linewidths, ilinestyles, + self.get_facecolor(), self.get_edgecolor(), + self._linewidths, self._linestyles, self._antialiaseds, self._urls, - "screen", self.get_hatchcolor()) - - renderer.draw_path_collection( - gc, transform.frozen(), paths, - self.get_transforms(), offsets, offset_trf, - self.get_facecolor(), self.get_edgecolor(), - self._linewidths, self._linestyles, - self._antialiaseds, self._urls, - "screen", # offset_position, kept for backcompat. - self.get_hatchcolor()) + "screen", self.get_hatchcolor() + ) + except TypeError: + hatchcolors_arg_supported = False + + if self._gapcolor is not None: + # First draw paths within the gaps. + ipaths, ilinestyles = self._get_inverse_paths_linestyles() + args = [gc, transform.frozen(), ipaths, self.get_transforms(), + offsets, offset_trf, [mcolors.to_rgba("none")], + self._gapcolor, self._linewidths, ilinestyles, + self._antialiaseds, self._urls, "screen"] + if hatchcolors_arg_supported: + args.append(self.get_hatchcolor()) + + renderer.draw_path_collection(*args) + + args = [gc, transform.frozen(), paths, self.get_transforms(), + offsets, offset_trf, self.get_facecolor(), + self.get_edgecolor(), self._linewidths, self._linestyles, + self._antialiaseds, self._urls, "screen"] + if hatchcolors_arg_supported: + args.append(self.get_hatchcolor()) + + renderer.draw_path_collection(*args) gc.restore() renderer.close_group(self.__class__.__name__) From c5e9583a5e78bb1b1af1fefffc33c13a4831aa59 Mon Sep 17 00:00:00 2001 From: r3kste <138380708+r3kste@users.noreply.github.com> Date: Sat, 22 Feb 2025 16:18:10 +0530 Subject: [PATCH 11/16] Fix hatchcolors argument support in third-party backends --- lib/matplotlib/collections.py | 50 +++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 9deba8b741a6..ff3fe4427f15 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -437,23 +437,45 @@ def draw(self, renderer): if self._gapcolor is not None: # First draw paths within the gaps. ipaths, ilinestyles = self._get_inverse_paths_linestyles() - args = [gc, transform.frozen(), ipaths, self.get_transforms(), - offsets, offset_trf, [mcolors.to_rgba("none")], - self._gapcolor, self._linewidths, ilinestyles, - self._antialiaseds, self._urls, "screen"] - if hatchcolors_arg_supported: - args.append(self.get_hatchcolor()) + args = [offsets, offset_trf, [mcolors.to_rgba("none")], self._gapcolor, + self._linewidths, ilinestyles, self._antialiaseds, self._urls, + "screen", self.get_hatchcolor()] - renderer.draw_path_collection(*args) + if hatchcolors_arg_supported: + renderer.draw_path_collection(gc, transform.frozen(), ipaths, + self.get_transforms(), *args) + else: + # If the renderer does not support the hatchcolors argument, + # iterate over the paths and draw them one by one. + path_ids = renderer._iter_collection_raw_paths( + transform.frozen(), ipaths, self.get_transforms()) + for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( + gc, list(path_ids), *args + ): + path, transform = path_id + if xo != 0 or yo != 0: + transform = transform.frozen() + transform.translate(xo, yo) + renderer.draw_path(gc0, path, transform, rgbFace) + + args = [offsets, offset_trf, self.get_facecolor(), self.get_edgecolor(), + self._linewidths, self._linestyles, self._antialiaseds, self._urls, + "screen", self.get_hatchcolor()] - args = [gc, transform.frozen(), paths, self.get_transforms(), - offsets, offset_trf, self.get_facecolor(), - self.get_edgecolor(), self._linewidths, self._linestyles, - self._antialiaseds, self._urls, "screen"] if hatchcolors_arg_supported: - args.append(self.get_hatchcolor()) - - renderer.draw_path_collection(*args) + renderer.draw_path_collection(gc, transform.frozen(), paths, + self.get_transforms(), *args) + else: + path_ids = renderer._iter_collection_raw_paths( + transform.frozen(), paths, self.get_transforms()) + for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( + gc, list(path_ids), *args + ): + path, transform = path_id + if xo != 0 or yo != 0: + transform = transform.frozen() + transform.translate(xo, yo) + renderer.draw_path(gc0, path, transform, rgbFace) gc.restore() renderer.close_group(self.__class__.__name__) From d2b626802e9971aa1b704b61ee79f5c9a707ea09 Mon Sep 17 00:00:00 2001 From: r3kste <138380708+r3kste@users.noreply.github.com> Date: Sun, 23 Feb 2025 12:54:03 +0530 Subject: [PATCH 12/16] Add note about provisional API for draw_path_collection() --- lib/matplotlib/collections.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index ff3fe4427f15..7306e85586ef 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -420,6 +420,9 @@ def draw(self, renderer): gc, paths[0], combined_transform.frozen(), mpath.Path(offsets), offset_trf, tuple(facecolors[0])) else: + # The current new API of draw_path_collection() is provisional + # and will be changed in a future PR. + # Find whether renderer.draw_path_collection() takes hatchcolor parameter hatchcolors_arg_supported = True try: From fee895b8d09d4ecfd6562f1aaf00db3d3b42378f Mon Sep 17 00:00:00 2001 From: r3kste <138380708+r3kste@users.noreply.github.com> Date: Sun, 16 Mar 2025 18:07:42 +0530 Subject: [PATCH 13/16] Refactor draw_path_collection to make `hatchcolors` a keyword-only argument --- lib/matplotlib/backend_bases.py | 5 +++-- lib/matplotlib/backend_bases.pyi | 1 + lib/matplotlib/backends/backend_pdf.py | 4 ++-- lib/matplotlib/backends/backend_ps.py | 4 ++-- lib/matplotlib/backends/backend_svg.py | 4 ++-- lib/matplotlib/collections.py | 12 +++++++----- src/_backend_agg_wrapper.cpp | 2 +- 7 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 995278019868..46af9aaf4af2 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -208,7 +208,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, - offset_position, hatchcolors=None): + offset_position, *, hatchcolors=None): """ Draw a collection of *paths*. @@ -277,7 +277,8 @@ 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', hatchcolors) + edgecolors, linewidths, [], [antialiased], [None], 'screen', + hatchcolors=hatchcolors) def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index 33c10adf9c2a..47c1f1951d01 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -63,6 +63,7 @@ class RendererBase: antialiaseds: bool | Sequence[bool], urls: str | Sequence[str], offset_position: Any, + *, hatchcolors: ColorType | Sequence[ColorType] | None = None, ) -> None: ... def draw_quad_mesh( diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index a370330d43eb..c4a097f8571e 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2030,7 +2030,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, - offset_position, hatchcolors=None): + offset_position, *, hatchcolors=None): # We can only reuse the objects if the presence of fill and # stroke (and the amount of alpha for each) is the same for # all of them @@ -2075,7 +2075,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, - offset_position, hatchcolors) + offset_position, hatchcolors=hatchcolors) padding = np.max(linewidths) path_codes = [] diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 18d69c8684b8..5c8bf7463bbe 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -674,7 +674,7 @@ def draw_markers( def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, - offset_position, hatchcolors=None): + offset_position, *, hatchcolors=None): if hatchcolors is None: hatchcolors = [] # Is the optimization worth it? Rough calculation: @@ -692,7 +692,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, - offset_position, hatchcolors) + offset_position, hatchcolors=hatchcolors) path_codes = [] for i, (path, transform) in enumerate(self._iter_collection_raw_paths( diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 9ae079dd2260..9eb604088836 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -736,7 +736,7 @@ def draw_markers( def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, - offset_position, hatchcolors=None): + offset_position, *, hatchcolors=None): if hatchcolors is None: hatchcolors = [] # Is the optimization worth it? Rough calculation: @@ -754,7 +754,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, gc, master_transform, paths, all_transforms, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, - offset_position, hatchcolors) + offset_position, hatchcolors=hatchcolors) writer = self.writer path_codes = [] diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 7306e85586ef..49a7e5010f66 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -432,7 +432,7 @@ def draw(self, renderer): self.get_facecolor(), self.get_edgecolor(), self._linewidths, self._linestyles, self._antialiaseds, self._urls, - "screen", self.get_hatchcolor() + "screen", hatchcolors=self.get_hatchcolor() ) except TypeError: hatchcolors_arg_supported = False @@ -442,11 +442,12 @@ def draw(self, renderer): ipaths, ilinestyles = self._get_inverse_paths_linestyles() args = [offsets, offset_trf, [mcolors.to_rgba("none")], self._gapcolor, self._linewidths, ilinestyles, self._antialiaseds, self._urls, - "screen", self.get_hatchcolor()] + "screen"] if hatchcolors_arg_supported: renderer.draw_path_collection(gc, transform.frozen(), ipaths, - self.get_transforms(), *args) + self.get_transforms(), *args, + hatchcolors=self.get_hatchcolor()) else: # If the renderer does not support the hatchcolors argument, # iterate over the paths and draw them one by one. @@ -463,11 +464,12 @@ def draw(self, renderer): args = [offsets, offset_trf, self.get_facecolor(), self.get_edgecolor(), self._linewidths, self._linestyles, self._antialiaseds, self._urls, - "screen", self.get_hatchcolor()] + "screen"] if hatchcolors_arg_supported: renderer.draw_path_collection(gc, transform.frozen(), paths, - self.get_transforms(), *args) + self.get_transforms(), *args, + hatchcolors=self.get_hatchcolor()) else: path_ids = renderer._iter_collection_raw_paths( transform.frozen(), paths, self.get_transforms()) diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index f0c029b96b98..3b9afec4a924 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -236,7 +236,7 @@ PYBIND11_MODULE(_backend_agg, m, py::mod_gil_not_used()) "gc"_a, "master_transform"_a, "paths"_a, "transforms"_a, "offsets"_a, "offset_trans"_a, "facecolors"_a, "edgecolors"_a, "linewidths"_a, "dashes"_a, "antialiaseds"_a, "ignored"_a, "offset_position"_a, - "hatchcolors"_a = py::array_t().reshape({0, 4})) + py::kw_only(), "hatchcolors"_a = py::array_t().reshape({0, 4})) .def("draw_quad_mesh", &PyRendererAgg_draw_quad_mesh, "gc"_a, "master_transform"_a, "mesh_width"_a, "mesh_height"_a, "coordinates"_a, "offsets"_a, "offset_trans"_a, "facecolors"_a, From b9ac0fb3250f4f0a1ce865892d7bd7fa26f862e9 Mon Sep 17 00:00:00 2001 From: r3kste <138380708+r3kste@users.noreply.github.com> Date: Sat, 22 Mar 2025 23:53:02 +0530 Subject: [PATCH 14/16] Made suggested changes --- .../shapes_and_collections/hatchcolor_demo.py | 19 ++++++++++--------- lib/matplotlib/backend_bases.py | 8 ++++---- lib/matplotlib/backend_bases.pyi | 1 + lib/matplotlib/backends/backend_pdf.py | 2 +- lib/matplotlib/backends/backend_ps.py | 2 +- lib/matplotlib/backends/backend_svg.py | 2 +- lib/matplotlib/collections.py | 13 ++++++++----- lib/matplotlib/collections.pyi | 2 +- lib/matplotlib/patches.pyi | 2 +- lib/matplotlib/tests/test_backend_bases.py | 2 +- src/_backend_agg_wrapper.cpp | 3 ++- 11 files changed, 31 insertions(+), 25 deletions(-) diff --git a/galleries/examples/shapes_and_collections/hatchcolor_demo.py b/galleries/examples/shapes_and_collections/hatchcolor_demo.py index 0b50b5ad825e..39816207dc48 100644 --- a/galleries/examples/shapes_and_collections/hatchcolor_demo.py +++ b/galleries/examples/shapes_and_collections/hatchcolor_demo.py @@ -3,12 +3,19 @@ Hatchcolor Demo =============== +The color of the hatch can be set using the *hatchcolor* parameter. The following +examples show how to use the *hatchcolor* parameter to set the color of the hatch +in `~.patches.Patch` and `~.collections.Collection`. + +See also :doc:`/gallery/shapes_and_collections/hatch_demo` for more usage examples +of hatching. + Patch Hatchcolor ---------------- This example shows how to use the *hatchcolor* parameter to set the color of -the hatch. The *hatchcolor* parameter is available for `~.patches.Patch`, -child classes of Patch, and methods that pass through to Patch. +the hatch in a rectangle and a bar plot. The *hatchcolor* parameter is available for +`~.patches.Patch`, child classes of Patch, and methods that pass through to Patch. """ import matplotlib.pyplot as plt @@ -62,19 +69,13 @@ facecolor="none", edgecolor="gray", linewidth=2, - marker="h", # Use hexagon as marker + marker="s", # Use hexagon as marker hatch="xxx", hatchcolor=colors, ) ax.set_xlim(0, 1) ax.set_ylim(0, 1) -# Remove ticks and labels -ax.set_xticks([]) -ax.set_yticks([]) -ax.set_xticklabels([]) -ax.set_yticklabels([]) - plt.show() # %% diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 46af9aaf4af2..73b96b102e4f 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -221,7 +221,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, and *hatchcolors* are lists that set the corresponding properties. .. versionadded:: 3.11 - Allowing *hatchcolors* to be specified. + Allow *hatchcolors* to be specified. *offset_position* is unused now, but the argument is kept for backwards compatibility. @@ -244,7 +244,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, hatchcolors): + antialiaseds, urls, offset_position, hatchcolors=hatchcolors): path, transform = path_id # Only apply another translation if we have an offset, else we # reuse the initial transform. @@ -258,7 +258,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, hatchcolors=None): + antialiased, edgecolors, *, hatchcolors=None): """ Draw a quadmesh. @@ -346,7 +346,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, hatchcolors): + antialiaseds, urls, offset_position, *, hatchcolors): """ Helper method (along with `_iter_collection_raw_paths`) to implement `draw_path_collection` in a memory-efficient manner. diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index 47c1f1951d01..fc25ab416a74 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -78,6 +78,7 @@ class RendererBase: facecolors: Sequence[ColorType], antialiased: bool, edgecolors: Sequence[ColorType] | ColorType | None, + *, hatchcolors: Sequence[ColorType] | ColorType | None = None, ) -> None: ... def draw_gouraud_triangles( diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index c4a097f8571e..860f0ad04c39 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2091,7 +2091,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, hatchcolors): + antialiaseds, urls, offset_position, hatchcolors=hatchcolors): 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 5c8bf7463bbe..62952caa32e1 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -711,7 +711,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, hatchcolors): + antialiaseds, urls, offset_position, hatchcolors=hatchcolors): 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 9eb604088836..0cb6430ec823 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -772,7 +772,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, hatchcolors): + antialiaseds, urls, offset_position, hatchcolors=hatchcolors): 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 49a7e5010f66..0c2ea1f66d96 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -454,7 +454,7 @@ def draw(self, renderer): path_ids = renderer._iter_collection_raw_paths( transform.frozen(), ipaths, self.get_transforms()) for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( - gc, list(path_ids), *args + gc, list(path_ids), *args, hatchcolors=self.get_hatchcolor(), ): path, transform = path_id if xo != 0 or yo != 0: @@ -474,7 +474,7 @@ def draw(self, renderer): path_ids = renderer._iter_collection_raw_paths( transform.frozen(), paths, self.get_transforms()) for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( - gc, list(path_ids), *args + gc, list(path_ids), *args, hatchcolors=self.get_hatchcolor(), ): path, transform = path_id if xo != 0 or yo != 0: @@ -802,14 +802,17 @@ def set_color(self, c): """ Set the edgecolor, facecolor and hatchcolor. + .. versionchanged:: 3.11 + Now sets the hatchcolor as well. + Parameters ---------- c : :mpltype:`color` or list of RGBA tuples See Also -------- - Collection.set_facecolor, Collection.set_edgecolor - For setting the edge or face color individually. + Collection.set_facecolor, Collection.set_edgecolor, Collection.set_hatchcolor + For setting the facecolor, edgecolor, and hatchcolor individually. """ self.set_facecolor(c) self.set_edgecolor(c) @@ -2535,7 +2538,7 @@ def draw(self, renderer): # 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.get_hatchcolor().reshape((-1, 4))) + hatchcolors=self.get_hatchcolor().reshape((-1, 4))) gc.restore() renderer.close_group(self.__class__.__name__) self.stale = False diff --git a/lib/matplotlib/collections.pyi b/lib/matplotlib/collections.pyi index a7ad264fb59d..ecd969cfacc6 100644 --- a/lib/matplotlib/collections.pyi +++ b/lib/matplotlib/collections.pyi @@ -21,7 +21,7 @@ class Collection(colorizer.ColorizingArtist): *, edgecolors: ColorType | Sequence[ColorType] | None = ..., facecolors: ColorType | Sequence[ColorType] | None = ..., - hatchcolors: ColorType | Sequence[ColorType] | None = ..., + hatchcolors: ColorType | Sequence[ColorType] | Literal["edge"] | None = ..., linewidths: float | Sequence[float] | None = ..., linestyles: LineStyleType | Sequence[LineStyleType] = ..., capstyle: CapStyleType | None = ..., diff --git a/lib/matplotlib/patches.pyi b/lib/matplotlib/patches.pyi index 3d119cfa076d..c95f20e35812 100644 --- a/lib/matplotlib/patches.pyi +++ b/lib/matplotlib/patches.pyi @@ -25,7 +25,7 @@ class Patch(artist.Artist): fill: bool = ..., capstyle: CapStyleType | None = ..., joinstyle: JoinStyleType | None = ..., - hatchcolor: ColorType | None = ..., + hatchcolor: ColorType | Literal["edge"] | None = ..., **kwargs, ) -> None: ... def get_verts(self) -> ArrayLike: ... diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py index b1ac109a913d..d96ce1ad9433 100644 --- a/lib/matplotlib/tests/test_backend_bases.py +++ b/lib/matplotlib/tests/test_backend_bases.py @@ -38,7 +38,7 @@ def check(master_transform, paths, all_transforms, gc, range(len(raw_paths)), offsets, transforms.AffineDeltaTransform(master_transform), facecolors, edgecolors, [], [], [False], - [], 'screen', [])] + [], 'screen', hatchcolors=[])] uses = rb._iter_collection_uses_per_path( paths, all_transforms, offsets, facecolors, edgecolors) if raw_paths: diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 3b9afec4a924..053606697388 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -240,7 +240,8 @@ PYBIND11_MODULE(_backend_agg, m, py::mod_gil_not_used()) .def("draw_quad_mesh", &PyRendererAgg_draw_quad_mesh, "gc"_a, "master_transform"_a, "mesh_width"_a, "mesh_height"_a, "coordinates"_a, "offsets"_a, "offset_trans"_a, "facecolors"_a, - "antialiased"_a, "edgecolors"_a, "hatchcolors"_a = py::array_t().reshape({0, 4})) + "antialiased"_a, "edgecolors"_a, py::kw_only(), + "hatchcolors"_a = py::array_t().reshape({0, 4})) .def("draw_gouraud_triangles", &PyRendererAgg_draw_gouraud_triangles, "gc"_a, "points"_a, "colors"_a, "trans"_a = nullptr) From 2e4784bad47ae8776a8851b2ac55017b7170b7be Mon Sep 17 00:00:00 2001 From: r3kste <138380708+r3kste@users.noreply.github.com> Date: Fri, 28 Mar 2025 19:20:28 +0530 Subject: [PATCH 15/16] added test and improve comments for third party backend support of hatchcolors arg --- .../next_whats_new/separated_hatchcolor.rst | 6 ++--- .../shapes_and_collections/hatchcolor_demo.py | 2 +- lib/matplotlib/collections.py | 10 +++++--- lib/matplotlib/tests/test_collections.py | 25 +++++++++++++++++++ 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/doc/users/next_whats_new/separated_hatchcolor.rst b/doc/users/next_whats_new/separated_hatchcolor.rst index 4652458083e3..a794fba2e515 100644 --- a/doc/users/next_whats_new/separated_hatchcolor.rst +++ b/doc/users/next_whats_new/separated_hatchcolor.rst @@ -78,9 +78,9 @@ to 'patch.edgecolor' with the alpha value of the collection. fig, ax = plt.subplots() - x = np.random.rand(20) - y = np.random.rand(20) - colors = ["blue", "orange", "green"] + x = [29, 36, 41, 25, 32, 70, 62, 58, 66, 80, 58, 68, 62, 37, 48] + y = [82, 76, 48, 53, 62, 70, 84, 68, 55, 75, 29, 25, 12, 17, 20] + colors = ['tab:blue'] * 5 + ['tab:orange'] * 5 + ['tab:green'] * 5 ax.scatter( x, diff --git a/galleries/examples/shapes_and_collections/hatchcolor_demo.py b/galleries/examples/shapes_and_collections/hatchcolor_demo.py index 39816207dc48..7a51248ffc14 100644 --- a/galleries/examples/shapes_and_collections/hatchcolor_demo.py +++ b/galleries/examples/shapes_and_collections/hatchcolor_demo.py @@ -69,7 +69,7 @@ facecolor="none", edgecolor="gray", linewidth=2, - marker="s", # Use hexagon as marker + marker="h", # Use hexagon as marker hatch="xxx", hatchcolor=colors, ) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 0c2ea1f66d96..edb3446303a7 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -423,7 +423,10 @@ def draw(self, renderer): # The current new API of draw_path_collection() is provisional # and will be changed in a future PR. - # Find whether renderer.draw_path_collection() takes hatchcolor parameter + # Find whether renderer.draw_path_collection() takes hatchcolor parameter. + # Since third-party implementations of draw_path_collection() may not be + # introspectable, e.g. with inspect.signature, the only way is to try and + # call this with the hatchcolors parameter. hatchcolors_arg_supported = True try: renderer.draw_path_collection( @@ -435,6 +438,9 @@ def draw(self, renderer): "screen", hatchcolors=self.get_hatchcolor() ) except TypeError: + # If the renderer does not support the hatchcolors argument, + # it will raise a TypeError. In this case, we will + # iterate over all paths and draw them one by one. hatchcolors_arg_supported = False if self._gapcolor is not None: @@ -449,8 +455,6 @@ def draw(self, renderer): self.get_transforms(), *args, hatchcolors=self.get_hatchcolor()) else: - # If the renderer does not support the hatchcolors argument, - # iterate over the paths and draw them one by one. path_ids = renderer._iter_collection_raw_paths( transform.frozen(), ipaths, self.get_transforms()) for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index cb29508ec010..bbed939c0c07 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -1486,3 +1486,28 @@ def test_draw_path_collection_no_hatchcolor(backend): facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position ) + + +def test_third_party_backend_hatchcolors_arg_fallback(monkeypatch): + fig, ax = plt.subplots() + canvas = fig.canvas + renderer = canvas.get_renderer() + + # monkeypatch the `draw_path_collection` method to simulate a third-party backend + # that does not support the `hatchcolors` argument. + def mock_draw_path_collection(self, gc, master_transform, paths, all_transforms, + offsets, offset_trans, facecolors, edgecolors, + linewidths, linestyles, antialiaseds, urls, + offset_position): + pass + + monkeypatch.setattr(renderer, 'draw_path_collection', mock_draw_path_collection) + + # Create a PathCollection with hatch colors + from matplotlib.collections import PathCollection + path = mpath.Path.unit_rectangle() + coll = PathCollection([path], hatch='//', hatchcolor='red') + + ax.add_collection(coll) + + plt.draw() From 3b43bc65e5de16fd464809f0d152d0060f8465aa Mon Sep 17 00:00:00 2001 From: r3kste <138380708+r3kste@users.noreply.github.com> Date: Sat, 29 Mar 2025 23:29:35 +0530 Subject: [PATCH 16/16] skip unnecessary path iteration when hatchcolors is not passed/needed --- lib/matplotlib/collections.py | 55 ++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index edb3446303a7..92ac5726f26c 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -443,6 +443,12 @@ def draw(self, renderer): # iterate over all paths and draw them one by one. hatchcolors_arg_supported = False + # If the hatchcolors argument is not needed or not passed + # then we can skip the iteration over paths in case the + # argument is not supported by the renderer. + hatchcolors_not_needed = (self.get_hatch() is None or + self._original_hatchcolor is None) + if self._gapcolor is not None: # First draw paths within the gaps. ipaths, ilinestyles = self._get_inverse_paths_linestyles() @@ -455,16 +461,21 @@ def draw(self, renderer): self.get_transforms(), *args, hatchcolors=self.get_hatchcolor()) else: - path_ids = renderer._iter_collection_raw_paths( - transform.frozen(), ipaths, self.get_transforms()) - for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( - gc, list(path_ids), *args, hatchcolors=self.get_hatchcolor(), - ): - path, transform = path_id - if xo != 0 or yo != 0: - transform = transform.frozen() - transform.translate(xo, yo) - renderer.draw_path(gc0, path, transform, rgbFace) + if hatchcolors_not_needed: + renderer.draw_path_collection(gc, transform.frozen(), ipaths, + self.get_transforms(), *args) + else: + path_ids = renderer._iter_collection_raw_paths( + transform.frozen(), ipaths, self.get_transforms()) + for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( + gc, list(path_ids), *args, + hatchcolors=self.get_hatchcolor(), + ): + path, transform = path_id + if xo != 0 or yo != 0: + transform = transform.frozen() + transform.translate(xo, yo) + renderer.draw_path(gc0, path, transform, rgbFace) args = [offsets, offset_trf, self.get_facecolor(), self.get_edgecolor(), self._linewidths, self._linestyles, self._antialiaseds, self._urls, @@ -475,16 +486,20 @@ def draw(self, renderer): self.get_transforms(), *args, hatchcolors=self.get_hatchcolor()) else: - path_ids = renderer._iter_collection_raw_paths( - transform.frozen(), paths, self.get_transforms()) - for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( - gc, list(path_ids), *args, hatchcolors=self.get_hatchcolor(), - ): - path, transform = path_id - if xo != 0 or yo != 0: - transform = transform.frozen() - transform.translate(xo, yo) - renderer.draw_path(gc0, path, transform, rgbFace) + if hatchcolors_not_needed: + renderer.draw_path_collection(gc, transform.frozen(), paths, + self.get_transforms(), *args) + else: + path_ids = renderer._iter_collection_raw_paths( + transform.frozen(), paths, self.get_transforms()) + for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( + gc, list(path_ids), *args, hatchcolors=self.get_hatchcolor(), + ): + path, transform = path_id + if xo != 0 or yo != 0: + transform = transform.frozen() + transform.translate(xo, yo) + renderer.draw_path(gc0, path, transform, rgbFace) gc.restore() renderer.close_group(self.__class__.__name__)