diff --git a/doc/api/next_api_changes/deprecations/28048-PR.rst b/doc/api/next_api_changes/deprecations/28048-PR.rst new file mode 100644 index 000000000000..4e90a3aced19 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/28048-PR.rst @@ -0,0 +1,4 @@ +``PdfFile.hatchPatterns`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +... is deprecated. diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 95ed49612b35..80e57311d6de 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -961,6 +961,10 @@ def get_hatch_linewidth(self): """Get the hatch linewidth.""" return self._hatch_linewidth + def set_hatch_linewidth(self, hatch_linewidth): + """Set the hatch linewidth.""" + self._hatch_linewidth = hatch_linewidth + def get_sketch_params(self): """ Return the sketch parameters for the artist. diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index 70be504666fc..12b4d06aa68c 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -167,6 +167,7 @@ class GraphicsContextBase: def get_hatch_color(self) -> ColorType: ... def set_hatch_color(self, hatch_color: ColorType) -> None: ... def get_hatch_linewidth(self) -> float: ... + def set_hatch_linewidth(self, hatch_linewidth: float) -> None: ... def get_sketch_params(self) -> tuple[float, float, float] | None: ... def set_sketch_params( self, diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 44b85cd3136d..86cbbe4d9a41 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -732,7 +732,7 @@ def __init__(self, filename, metadata=None): self._soft_mask_states = {} self._soft_mask_seq = (Name(f'SM{i}') for i in itertools.count(1)) self._soft_mask_groups = [] - self.hatchPatterns = {} + self._hatch_patterns = {} self._hatch_pattern_seq = (Name(f'H{i}') for i in itertools.count(1)) self.gouraudTriangles = [] @@ -1535,26 +1535,29 @@ def _write_soft_mask_groups(self): def hatchPattern(self, hatch_style): # The colors may come in as numpy arrays, which aren't hashable - if hatch_style is not None: - edge, face, hatch = hatch_style - if edge is not None: - edge = tuple(edge) - if face is not None: - face = tuple(face) - hatch_style = (edge, face, hatch) - - pattern = self.hatchPatterns.get(hatch_style, None) + edge, face, hatch, lw = hatch_style + if edge is not None: + edge = tuple(edge) + if face is not None: + face = tuple(face) + hatch_style = (edge, face, hatch, lw) + + pattern = self._hatch_patterns.get(hatch_style, None) if pattern is not None: return pattern name = next(self._hatch_pattern_seq) - self.hatchPatterns[hatch_style] = name + self._hatch_patterns[hatch_style] = name return name + hatchPatterns = _api.deprecated("3.10")(property(lambda self: { + k: (e, f, h) for k, (e, f, h, l) in self._hatch_patterns.items() + })) + def writeHatches(self): hatchDict = dict() sidelen = 72.0 - for hatch_style, name in self.hatchPatterns.items(): + for hatch_style, name in self._hatch_patterns.items(): ob = self.reserveObject('hatch pattern') hatchDict[name] = ob res = {'Procsets': @@ -1569,7 +1572,7 @@ def writeHatches(self): # Change origin to match Agg at top-left. 'Matrix': [1, 0, 0, 1, 0, self.height * 72]}) - stroke_rgb, fill_rgb, hatch = hatch_style + stroke_rgb, fill_rgb, hatch, lw = hatch_style self.output(stroke_rgb[0], stroke_rgb[1], stroke_rgb[2], Op.setrgb_stroke) if fill_rgb is not None: @@ -1578,7 +1581,7 @@ def writeHatches(self): 0, 0, sidelen, sidelen, Op.rectangle, Op.fill) - self.output(mpl.rcParams['hatch.linewidth'], Op.setlinewidth) + self.output(lw, Op.setlinewidth) self.output(*self.pathOperations( Path.hatch(hatch), @@ -2509,14 +2512,14 @@ def alpha_cmd(self, alpha, forced, effective_alphas): name = self.file.alphaState(effective_alphas) return [name, Op.setgstate] - def hatch_cmd(self, hatch, hatch_color): + def hatch_cmd(self, hatch, hatch_color, hatch_linewidth): if not hatch: if self._fillcolor is not None: return self.fillcolor_cmd(self._fillcolor) else: return [Name('DeviceRGB'), Op.setcolorspace_nonstroke] else: - hatch_style = (hatch_color, self._fillcolor, hatch) + hatch_style = (hatch_color, self._fillcolor, hatch, hatch_linewidth) name = self.file.hatchPattern(hatch_style) return [Name('Pattern'), Op.setcolorspace_nonstroke, name, Op.setcolor_nonstroke] @@ -2581,8 +2584,8 @@ def clip_cmd(self, cliprect, clippath): (('_dashes',), dash_cmd), (('_rgb',), rgb_cmd), # must come after fillcolor and rgb - (('_hatch', '_hatch_color'), hatch_cmd), - ) + (('_hatch', '_hatch_color', '_hatch_linewidth'), hatch_cmd), + ) def delta(self, other): """ @@ -2610,11 +2613,11 @@ def delta(self, other): break # Need to update hatching if we also updated fillcolor - if params == ('_hatch', '_hatch_color') and fill_performed: + if cmd.__name__ == 'hatch_cmd' and fill_performed: different = True if different: - if params == ('_fillcolor',): + if cmd.__name__ == 'fillcolor_cmd': fill_performed = True theirs = [getattr(other, p) for p in params] cmds.extend(cmd(self, *theirs)) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 893c660c0b24..78473afa0f06 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -505,12 +505,11 @@ def set_font(self, fontname, fontsize, store=True): self.fontname = fontname self.fontsize = fontsize - def create_hatch(self, hatch): + def create_hatch(self, hatch, linewidth): sidelen = 72 if hatch in self._hatches: return self._hatches[hatch] name = 'H%d' % len(self._hatches) - linewidth = mpl.rcParams['hatch.linewidth'] pageheight = self.height * 72 self._pswriter.write(f"""\ << /PatternType 1 @@ -933,7 +932,7 @@ def _draw_ps(self, ps, gc, rgbFace, *, fill=True, stroke=True): write("grestore\n") if hatch: - hatch_name = self.create_hatch(hatch) + hatch_name = self.create_hatch(hatch, gc.get_hatch_linewidth()) write("gsave\n") write(_nums_to_str(*gc.get_hatch_color()[:3])) write(f" {hatch_name} setpattern fill grestore\n") diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index dbf056613696..2193dc6b6cdc 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -500,11 +500,12 @@ def _get_hatch(self, gc, rgbFace): edge = gc.get_hatch_color() if edge is not None: edge = tuple(edge) - dictkey = (gc.get_hatch(), rgbFace, edge) + lw = gc.get_hatch_linewidth() + dictkey = (gc.get_hatch(), rgbFace, edge, lw) oid = self._hatchd.get(dictkey) if oid is None: oid = self._make_id('h', dictkey) - self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge), oid) + self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge, lw), oid) else: _, oid = oid return oid @@ -515,7 +516,7 @@ def _write_hatches(self): HATCH_SIZE = 72 writer = self.writer writer.start('defs') - for (path, face, stroke), oid in self._hatchd.values(): + for (path, face, stroke, lw), oid in self._hatchd.values(): writer.start( 'pattern', id=oid, @@ -539,7 +540,7 @@ def _write_hatches(self): hatch_style = { 'fill': rgb2hex(stroke), 'stroke': rgb2hex(stroke), - 'stroke-width': str(mpl.rcParams['hatch.linewidth']), + 'stroke-width': str(lw), 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter' } diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index e668308abc82..94aa6b315141 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -174,6 +174,7 @@ def __init__(self, *, self._edge_is_mapped = None self._mapped_colors = None # calculated in update_scalarmappable self._hatch_color = mcolors.to_rgba(mpl.rcParams['hatch.color']) + self._hatch_linewidth = mpl.rcParams['hatch.linewidth'] self.set_facecolor(facecolors) self.set_edgecolor(edgecolors) self.set_linewidth(linewidths) @@ -364,6 +365,7 @@ 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: gc.set_sketch_params(*self.get_sketch_params()) @@ -542,6 +544,14 @@ def get_hatch(self): """Return the current hatching pattern.""" return self._hatch + def set_hatch_linewidth(self, lw): + """Set the hatch linewidth.""" + self._hatch_linewidth = lw + + def get_hatch_linewidth(self): + """Return the hatch linewidth.""" + return self._hatch_linewidth + def set_offsets(self, offsets): """ Set the offsets for the collection. diff --git a/lib/matplotlib/collections.pyi b/lib/matplotlib/collections.pyi index 06d8676867ee..7237b5846d10 100644 --- a/lib/matplotlib/collections.pyi +++ b/lib/matplotlib/collections.pyi @@ -48,6 +48,8 @@ class Collection(artist.Artist, cm.ScalarMappable): def get_urls(self) -> Sequence[str | None]: ... def set_hatch(self, hatch: str) -> None: ... def get_hatch(self) -> str: ... + def set_hatch_linewidth(self, lw: float) -> None: ... + def get_hatch_linewidth(self) -> float: ... def set_offsets(self, offsets: ArrayLike) -> None: ... def get_offsets(self) -> ArrayLike: ... def set_linewidth(self, lw: float | Sequence[float]) -> None: ... diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 2db678587ec7..e7e56c853849 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -72,6 +72,7 @@ def __init__(self, *, joinstyle = JoinStyle.miter self._hatch_color = colors.to_rgba(mpl.rcParams['hatch.color']) + self._hatch_linewidth = mpl.rcParams['hatch.linewidth'] self._fill = bool(fill) # needed for set_facecolor call if color is not None: if edgecolor is not None or facecolor is not None: @@ -571,6 +572,14 @@ def get_hatch(self): """Return the hatching pattern.""" return self._hatch + def set_hatch_linewidth(self, lw): + """Set the hatch linewidth.""" + self._hatch_linewidth = lw + + def get_hatch_linewidth(self): + """Return the hatch linewidth.""" + return self._hatch_linewidth + def _draw_paths_with_artist_properties( self, renderer, draw_path_args_list): """ @@ -605,6 +614,7 @@ def _draw_paths_with_artist_properties( 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: gc.set_sketch_params(*self.get_sketch_params()) diff --git a/lib/matplotlib/patches.pyi b/lib/matplotlib/patches.pyi index f6c9ddf75839..0645479ee5e7 100644 --- a/lib/matplotlib/patches.pyi +++ b/lib/matplotlib/patches.pyi @@ -59,6 +59,8 @@ class Patch(artist.Artist): def set_joinstyle(self, s: JoinStyleType) -> None: ... def get_joinstyle(self) -> Literal["miter", "round", "bevel"]: ... def set_hatch(self, hatch: str) -> None: ... + def set_hatch_linewidth(self, lw: float) -> None: ... + def get_hatch_linewidth(self) -> float: ... def get_hatch(self) -> str: ... def get_path(self) -> Path: ... diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 19af38f3b522..11934cfca2c3 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -1361,3 +1361,26 @@ def test_striped_lines(fig_test, fig_ref, gapcolor): for x, gcol, ls in zip(x, itertools.cycle(gapcolor), itertools.cycle(linestyles)): ax_ref.axvline(x, 0, 1, linewidth=20, linestyle=ls, gapcolor=gcol, alpha=0.5) + + +@check_figures_equal(extensions=['png', 'pdf', 'svg', 'eps']) +def test_hatch_linewidth(fig_test, fig_ref): + ax_test = fig_test.add_subplot() + ax_ref = fig_ref.add_subplot() + + lw = 2.0 + + polygons = [ + [(0.1, 0.1), (0.1, 0.4), (0.4, 0.4), (0.4, 0.1)], + [(0.6, 0.6), (0.6, 0.9), (0.9, 0.9), (0.9, 0.6)], + ] + ref = PolyCollection(polygons, hatch="x") + ref.set_hatch_linewidth(lw) + + with mpl.rc_context({"hatch.linewidth": lw}): + test = PolyCollection(polygons, hatch="x") + + ax_ref.add_collection(ref) + ax_test.add_collection(test) + + assert test.get_hatch_linewidth() == ref.get_hatch_linewidth() == lw diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index 3544ce8cb10c..edbafa074de5 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -960,3 +960,20 @@ def test_arrow_set_data(): ) arrow.set_data(x=.5, dx=3, dy=8, width=1.2) assert np.allclose(expected2, np.round(arrow.get_verts(), 2)) + + +@check_figures_equal(extensions=["png", "pdf", "svg", "eps"]) +def test_set_and_get_hatch_linewidth(fig_test, fig_ref): + ax_test = fig_test.add_subplot() + ax_ref = fig_ref.add_subplot() + + lw = 2.0 + + with plt.rc_context({"hatch.linewidth": lw}): + ax_ref.add_patch(mpatches.Rectangle((0, 0), 1, 1, hatch="x")) + + ax_test.add_patch(mpatches.Rectangle((0, 0), 1, 1, hatch="x")) + ax_test.patches[0].set_hatch_linewidth(lw) + + assert ax_ref.patches[0].get_hatch_linewidth() == lw + assert ax_test.patches[0].get_hatch_linewidth() == lw