Skip to content

Commit 35e658a

Browse files
committed
Fix InsideStrokedRectangle to inset stroke under any transform
The previous implementation shrank the rectangle's bounds by lw/2 in data units, which only happens to be correct in the DrawingArea's points-as-units transform; under transData, transAxes, or any other transform it inset by the wrong amount. It also failed to restore the bounds after drawing, so each redraw shrank the patch further. Replace it with a clip-to-self approach: stroke at 2·lw centred on the path, then clip to the patch's own boundary so the outer half is erased. The visible stroke ends up fully inside the rectangle with its outer edge on the original boundary, in any transform, idempotently across redraws. Snapshot and restore _dash_pattern around the linewidth change so dashed strokes render at the requested length rather than 2× longer.
1 parent efed6a9 commit 35e658a

3 files changed

Lines changed: 31 additions & 6 deletions

File tree

plotnine/_mpl/patches.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,41 @@ def draw(self, renderer):
111111

112112
class InsideStrokedRectangle(Rectangle):
113113
"""
114-
A rectangle whose stroked is fully contained within it
114+
A rectangle whose stroke is fully contained within it
115115
"""
116116

117117
@artist.allow_rasterization
118118
def draw(self, renderer):
119119
"""
120-
Draw with the bounds of the rectangle adjusted to contain the stroke
120+
Draw stroking at 2·lw and clipping to the rectangle's own boundary
121+
122+
The outer half of the stroke is removed by the clip, leaving exactly
123+
`lw` of stroke fully inside the rectangle, with the visible outer edge
124+
of the stroke on the original boundary. This is transform-agnostic
125+
(works under data, axes, identity, and DrawingArea transforms) and
126+
idempotent across redraws.
121127
"""
122-
x, y = self.xy
123-
w, h = self.get_width(), self.get_height()
124128
lw = self.get_linewidth()
125-
self.set_bounds((x + lw / 2), (y + lw / 2), (w - lw), (h - lw))
126-
super().draw(renderer)
129+
if lw <= 0:
130+
return super().draw(renderer)
131+
132+
saved_clip_path = self.get_clip_path()
133+
saved_clip_on = self.get_clip_on()
134+
135+
# Dash lengths track the linewidth: matplotlib stores the dash
136+
# pattern as multipliers and scales them whenever set_linewidth is
137+
# called. Doubling lw below would therefore stretch the dashes 2x.
138+
# Snapshot the pattern at the user's lw now and write it back after
139+
# the doubling so the dashes render at their requested length.
140+
original_dash_pattern = self._dash_pattern
141+
try:
142+
self.set_linewidth(2 * lw)
143+
self._dash_pattern = original_dash_pattern
144+
self.set_clip_path(self.get_path(), self.get_transform())
145+
self.set_clip_on(True)
146+
self.set_snap(False)
147+
super().draw(renderer)
148+
finally:
149+
self.set_linewidth(lw)
150+
self.set_clip_path(saved_clip_path)
151+
self.set_clip_on(saved_clip_on)
117 Bytes
Loading
134 Bytes
Loading

0 commit comments

Comments
 (0)