Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions lib/matplotlib/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -4053,12 +4053,18 @@ def __init__(self, xy, width, height, boxstyle="round", *,
Scaling factor applied to the attributes of the box style
(e.g. pad or rounding_size).

mutation_aspect : float, default: 1
mutation_aspect : float or str, default: 1
The height of the rectangle will be squeezed by this value before
the mutation and the mutated box will be stretched by the inverse
of it. For example, this allows different horizontal and vertical
padding.

The special value ``"screen-proportional"`` resolves the aspect
dynamically from the axes at draw time, so that the box is scaled
proportionally to the display scaling of the data. This keeps
features such as the rounding of ``"Round"`` boxstyles circular
on screen, independent of the axes aspect ratio.

Other Parameters
----------------
**kwargs : `~matplotlib.patches.Patch` properties
Expand Down Expand Up @@ -4140,8 +4146,15 @@ def set_mutation_aspect(self, aspect):

Parameters
----------
aspect : float
aspect : float or "screen-proportional"
A float value, or the string ``"screen-proportional"`` to resolve
the aspect dynamically from the axes at draw time (see
`.FancyBboxPatch` for details).
"""
if (not isinstance(aspect, (int, float))
and aspect != "screen-proportional"):
raise ValueError(
"mutation_aspect must be a number or 'screen-proportional'")
self._mutation_aspect = aspect
self.stale = True

Expand All @@ -4150,10 +4163,26 @@ def get_mutation_aspect(self):
return (self._mutation_aspect if self._mutation_aspect is not None
else 1) # backcompat.

def _get_screen_proportional_aspect(self):
"""Return the mutation aspect that is isotropic in display space."""
axes = self.axes
if axes is None:
return 1
# Display-space size of the Axes and of one data unit in each
# direction, so that the box is scaled like the data on screen.
bbox = axes.get_window_extent()
txmin, txmax = axes.xaxis.get_transform().transform(axes.get_xbound())
tymin, tymax = axes.yaxis.get_transform().transform(axes.get_ybound())
xsize = max(abs(txmax - txmin), 1e-30)
ysize = max(abs(tymax - tymin), 1e-30)
return bbox.width * ysize / (bbox.height * xsize)

def get_path(self):
"""Return the mutated path of the rectangle."""
boxstyle = self.get_boxstyle()
m_aspect = self.get_mutation_aspect()
if m_aspect == "screen-proportional":
m_aspect = self._get_screen_proportional_aspect()
# Call boxstyle with y, height squeezed by aspect_ratio.
path = boxstyle(self._x, self._y / m_aspect,
self._width, self._height / m_aspect,
Expand Down
40 changes: 40 additions & 0 deletions lib/matplotlib/tests/test_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,46 @@ def test_boxstyle_errors(fmt, match):
BoxStyle(fmt)


def test_fancybbox_patch_screen_proportional_mutation_aspect():
fig, ax = plt.subplots(figsize=(10, 5))
w, h = 0.18, 1.0
patch = mpatches.FancyBboxPatch(
(0.3, 0), w, h,
boxstyle=BoxStyle("Round", pad=0, rounding_size=w / 2),
mutation_aspect="screen-proportional")
ax.add_patch(patch)
ax.autoscale_view()
fig.canvas.draw()

# The corner of the Round box must be a circular arc in display space,
# i.e. the rounding is isotropic on screen.
verts = patch.get_transform().transform(patch.get_path().vertices)
# Bottom-right corner: verts[1] = (x1 - dr, y0), verts[2] = (x1, y0),
# verts[3] = (x1, y0 + dr).
np.testing.assert_allclose(verts[2, 0] - verts[1, 0],
verts[3, 1] - verts[2, 1])

# A plain numeric mutation_aspect does not compensate the axes aspect.
patch.set_mutation_aspect(1)
verts = patch.get_transform().transform(patch.get_path().vertices)
assert not np.isclose(verts[2, 0] - verts[1, 0],
verts[3, 1] - verts[2, 1])

with pytest.raises(ValueError, match="mutation_aspect"):
patch.set_mutation_aspect("bogus")


def test_fancybbox_patch_screen_proportional_without_axes():
# Without an Axes the screen aspect cannot be resolved; it falls back to 1.
patch = mpatches.FancyBboxPatch(
(0, 0), 0.2, 0.1,
boxstyle=BoxStyle("Round", pad=0, rounding_size=0.1),
mutation_aspect="screen-proportional")
verts = patch.get_path().vertices
np.testing.assert_allclose(verts[2, 0] - verts[1, 0],
verts[3, 1] - verts[2, 1])


@image_comparison(['annulus.png'], style='mpl20')
def test_annulus():

Expand Down
Loading