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')