From d13153401b1d2153e55a786cf7a8636eb7cb7c10 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 16 Apr 2020 21:47:12 -0400 Subject: [PATCH] Fix clipping of markers in PDF backend. The bbox only contains the points of the marker, but the line will extend outside by half the line width, unless it's mitered. The PDF miter limit is 10, so pad by 5*line width (half the miter length). This fixes corners on 'v', '^', '<', '>', 'p', 'h', 'H', 'D', 'd', 'X'. Fixes #9829. --- lib/matplotlib/backends/backend_pdf.py | 9 +++++- lib/matplotlib/tests/test_marker.py | 41 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 5ebb5a969225..ed97330cdea5 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -1618,7 +1618,14 @@ def markerObject(self, path, trans, fill, stroke, lw, joinstyle, def writeMarkers(self): for ((pathops, fill, stroke, joinstyle, capstyle), (name, ob, bbox, lw)) in self.markers.items(): - bbox = bbox.padded(lw * 0.5) + # bbox wraps the exact limits of the control points, so half a line + # will appear outside it. If the join style is miter and the line + # is not parallel to the edge, then the line will extend even + # further. From the PDF specification, Section 8.4.3.5, the miter + # limit is miterLength / lineWidth and from Table 52, the default + # is 10. With half the miter length outside, that works out to the + # following padding: + bbox = bbox.padded(lw * 5) self.beginStream( ob.id, None, {'Type': Name('XObject'), 'Subtype': Name('Form'), diff --git a/lib/matplotlib/tests/test_marker.py b/lib/matplotlib/tests/test_marker.py index 982f74bf8aee..1ac7aaa0d0d8 100644 --- a/lib/matplotlib/tests/test_marker.py +++ b/lib/matplotlib/tests/test_marker.py @@ -139,3 +139,44 @@ def draw_ref_marker(y, style, size): ax_test.set(xlim=(-0.5, 1.5), ylim=(-0.5, 1.5)) ax_ref.set(xlim=(-0.5, 1.5), ylim=(-0.5, 1.5)) + + +@check_figures_equal() +def test_marker_clipping(fig_ref, fig_test): + # Plotting multiple markers can trigger different optimized paths in + # backends, so compare single markers vs multiple to ensure they are + # clipped correctly. + marker_count = len(markers.MarkerStyle.markers) + marker_size = 50 + ncol = 7 + nrow = marker_count // ncol + 1 + + width = 2 * marker_size * ncol + height = 2 * marker_size * nrow * 2 + fig_ref.set_size_inches((width / fig_ref.dpi, height / fig_ref.dpi)) + ax_ref = fig_ref.add_axes([0, 0, 1, 1]) + fig_test.set_size_inches((width / fig_test.dpi, height / fig_ref.dpi)) + ax_test = fig_test.add_axes([0, 0, 1, 1]) + + for i, marker in enumerate(markers.MarkerStyle.markers): + x = i % ncol + y = i // ncol * 2 + + # Singular markers per call. + ax_ref.plot([x, x], [y, y + 1], c='k', linestyle='-', lw=3) + ax_ref.plot(x, y, c='k', + marker=marker, markersize=marker_size, markeredgewidth=10, + fillstyle='full', markerfacecolor='white') + ax_ref.plot(x, y + 1, c='k', + marker=marker, markersize=marker_size, markeredgewidth=10, + fillstyle='full', markerfacecolor='white') + + # Multiple markers in a single call. + ax_test.plot([x, x], [y, y + 1], c='k', linestyle='-', lw=3, + marker=marker, markersize=marker_size, markeredgewidth=10, + fillstyle='full', markerfacecolor='white') + + ax_ref.set(xlim=(-0.5, ncol), ylim=(-0.5, 2 * nrow)) + ax_test.set(xlim=(-0.5, ncol), ylim=(-0.5, 2 * nrow)) + ax_ref.axis('off') + ax_test.axis('off')