Skip to content

Commit d131534

Browse files
committed
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.
1 parent f42b24f commit d131534

File tree

2 files changed

+49
-1
lines changed

2 files changed

+49
-1
lines changed

lib/matplotlib/backends/backend_pdf.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1618,7 +1618,14 @@ def markerObject(self, path, trans, fill, stroke, lw, joinstyle,
16181618
def writeMarkers(self):
16191619
for ((pathops, fill, stroke, joinstyle, capstyle),
16201620
(name, ob, bbox, lw)) in self.markers.items():
1621-
bbox = bbox.padded(lw * 0.5)
1621+
# bbox wraps the exact limits of the control points, so half a line
1622+
# will appear outside it. If the join style is miter and the line
1623+
# is not parallel to the edge, then the line will extend even
1624+
# further. From the PDF specification, Section 8.4.3.5, the miter
1625+
# limit is miterLength / lineWidth and from Table 52, the default
1626+
# is 10. With half the miter length outside, that works out to the
1627+
# following padding:
1628+
bbox = bbox.padded(lw * 5)
16221629
self.beginStream(
16231630
ob.id, None,
16241631
{'Type': Name('XObject'), 'Subtype': Name('Form'),

lib/matplotlib/tests/test_marker.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,44 @@ def draw_ref_marker(y, style, size):
139139

140140
ax_test.set(xlim=(-0.5, 1.5), ylim=(-0.5, 1.5))
141141
ax_ref.set(xlim=(-0.5, 1.5), ylim=(-0.5, 1.5))
142+
143+
144+
@check_figures_equal()
145+
def test_marker_clipping(fig_ref, fig_test):
146+
# Plotting multiple markers can trigger different optimized paths in
147+
# backends, so compare single markers vs multiple to ensure they are
148+
# clipped correctly.
149+
marker_count = len(markers.MarkerStyle.markers)
150+
marker_size = 50
151+
ncol = 7
152+
nrow = marker_count // ncol + 1
153+
154+
width = 2 * marker_size * ncol
155+
height = 2 * marker_size * nrow * 2
156+
fig_ref.set_size_inches((width / fig_ref.dpi, height / fig_ref.dpi))
157+
ax_ref = fig_ref.add_axes([0, 0, 1, 1])
158+
fig_test.set_size_inches((width / fig_test.dpi, height / fig_ref.dpi))
159+
ax_test = fig_test.add_axes([0, 0, 1, 1])
160+
161+
for i, marker in enumerate(markers.MarkerStyle.markers):
162+
x = i % ncol
163+
y = i // ncol * 2
164+
165+
# Singular markers per call.
166+
ax_ref.plot([x, x], [y, y + 1], c='k', linestyle='-', lw=3)
167+
ax_ref.plot(x, y, c='k',
168+
marker=marker, markersize=marker_size, markeredgewidth=10,
169+
fillstyle='full', markerfacecolor='white')
170+
ax_ref.plot(x, y + 1, c='k',
171+
marker=marker, markersize=marker_size, markeredgewidth=10,
172+
fillstyle='full', markerfacecolor='white')
173+
174+
# Multiple markers in a single call.
175+
ax_test.plot([x, x], [y, y + 1], c='k', linestyle='-', lw=3,
176+
marker=marker, markersize=marker_size, markeredgewidth=10,
177+
fillstyle='full', markerfacecolor='white')
178+
179+
ax_ref.set(xlim=(-0.5, ncol), ylim=(-0.5, 2 * nrow))
180+
ax_test.set(xlim=(-0.5, ncol), ylim=(-0.5, 2 * nrow))
181+
ax_ref.axis('off')
182+
ax_test.axis('off')

0 commit comments

Comments
 (0)