From a1eef6526b69220ecc77e91e81ac00a5abb6a917 Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Thu, 25 Dec 2025 09:38:44 +0000 Subject: [PATCH 1/2] fix(patches): honor dash offset --- doc/users/next_whats_new/patch_dashoffset.rst | 6 ++ lib/matplotlib/patches.py | 6 +- lib/matplotlib/tests/test_patches.py | 59 +++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 doc/users/next_whats_new/patch_dashoffset.rst diff --git a/doc/users/next_whats_new/patch_dashoffset.rst b/doc/users/next_whats_new/patch_dashoffset.rst new file mode 100644 index 000000000000..63bfc390692b --- /dev/null +++ b/doc/users/next_whats_new/patch_dashoffset.rst @@ -0,0 +1,6 @@ +Patch dash offsets honored on patches +------------------------------------- + +Patches now respect the dash offset specified via +`~matplotlib.patches.Patch.set_linestyle`, so edge stroking matches the +behaviour of lines across both vector (e.g. SVG) and raster (Agg) backends. diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index e062249589e2..2332051a1f8a 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -414,6 +414,8 @@ def set_linestyle(self, ls): (offset, onoffseq) where ``onoffseq`` is an even length tuple of on and off ink in points. + When ``rcParams['lines.scale_dashes']`` is True (the default), both the + offset and the on/off lengths are scaled by the linewidth. Parameters ---------- @@ -587,9 +589,7 @@ def draw(self, renderer): if not self.get_visible(): return # Patch has traditionally ignored the dashoffset. - with cbook._setattr_cm( - self, _dash_pattern=(0, self._dash_pattern[1])), \ - self._bind_draw_path_function(renderer) as draw_path: + with self._bind_draw_path_function(renderer) as draw_path: path = self.get_path() transform = self.get_transform() tpath = transform.transform_path_non_affine(path) diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index 7064d0dd3b19..24a6171ea03a 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -1,16 +1,22 @@ """ Tests specific to the patches module. """ +import io +import re + import numpy as np from numpy.testing import assert_almost_equal, assert_array_equal import pytest import matplotlib as mpl +from matplotlib.backends.backend_agg import RendererAgg +from matplotlib.backends.backend_svg import FigureCanvasSVG from matplotlib.patches import (Annulus, Ellipse, Patch, Polygon, Rectangle, FancyArrowPatch, FancyArrow, BoxStyle) from matplotlib.testing.decorators import image_comparison, check_figures_equal from matplotlib.transforms import Bbox import matplotlib.pyplot as plt +from matplotlib.figure import Figure from matplotlib import ( collections as mcollections, colors as mcolors, patches as mpatches, path as mpath, transforms as mtransforms, rcParams) @@ -337,6 +343,59 @@ def test_patch_linestyle_none(fig_test, fig_ref): ax_ref.set_ylim([-1, i + 1]) +def test_patch_linestyle_respects_dashoffset_svg(): + fig = Figure(figsize=(2, 1)) + ax = fig.add_subplot() + ax.set_axis_off() + ax.set_xlim(0, 3) + ax.set_ylim(0, 1) + + rect_no_offset = Rectangle( + (0.1, 0.1), 1, 0.8, fill=False, linewidth=2, + linestyle=(0, (3, 2))) + rect_with_offset = Rectangle( + (1.5, 0.1), 1, 0.8, fill=False, linewidth=2, + linestyle=(6, (3, 2))) + + ax.add_patch(rect_no_offset) + ax.add_patch(rect_with_offset) + + canvas = FigureCanvasSVG(fig) + buffer = io.StringIO() + canvas.print_svg(buffer) + svg = buffer.getvalue() + + offsets = [ + float(match) + for match in re.findall(r"stroke-dashoffset:\s*([0-9.]+)", svg) + ] + + assert len(offsets) >= 2 + assert rect_no_offset._dash_pattern[0] == 0 + assert offsets.count(rect_no_offset._dash_pattern[0]) >= 1 + assert offsets.count(rect_with_offset._dash_pattern[0]) >= 1 + + +def test_patch_linestyle_dashoffset_prop_agg(monkeypatch): + renderer = RendererAgg(200, 200, 72) + rect = Rectangle( + (0, 0), 1, 1, fill=False, linewidth=2, + linestyle=(6, (3, 2))) + + recorded = [] + original_draw_path = RendererAgg.draw_path + + def capture(self, gc, path, transform, rgbFace=None): + recorded.append(gc.get_dashes()) + return original_draw_path(self, gc, path, transform, rgbFace) + + monkeypatch.setattr(RendererAgg, "draw_path", capture) + rect.draw(renderer) + + assert recorded + assert rect._dash_pattern in recorded + + def test_wedge_movement(): param_dict = {'center': ((0, 0), (1, 1), 'set_center'), 'r': (5, 8, 'set_radius'), From 8513bad94fae6ae4299d8b4e04d130842f297579 Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Thu, 25 Dec 2025 09:51:51 +0000 Subject: [PATCH 2/2] chore(patches): refresh dash offset comment --- lib/matplotlib/patches.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 2332051a1f8a..04c3c96fb1a6 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -588,7 +588,7 @@ def draw(self, renderer): # docstring inherited if not self.get_visible(): return - # Patch has traditionally ignored the dashoffset. + # Preserve the computed dash offset via the bound draw_path. with self._bind_draw_path_function(renderer) as draw_path: path = self.get_path() transform = self.get_transform()