Skip to content

Commit 4f070ac

Browse files
ENH: Allow tuple for borderpad in AnchoredOffsetbox (matplotlib#30359)
1 parent 5c0d055 commit 4f070ac

File tree

6 files changed

+88
-12
lines changed

6 files changed

+88
-12
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
``borderpad`` accepts a tuple for separate x/y padding
2+
-------------------------------------------------------
3+
4+
The ``borderpad`` parameter used for placing anchored artists (such as inset axes) now accepts a tuple of ``(x_pad, y_pad)``.
5+
6+
This allows for specifying separate padding values for the horizontal and
7+
vertical directions, providing finer control over placement. For example, when
8+
placing an inset in a corner, one might want horizontal padding to avoid
9+
overlapping with the main plot's axis labels, but no vertical padding to keep
10+
the inset flush with the plot area edge.
11+
12+
Example usage with :func:`~mpl_toolkits.axes_grid1.inset_locator.inset_axes`:
13+
14+
.. code-block:: python
15+
16+
ax_inset = inset_axes(
17+
ax, width="30%", height="30%", loc='upper left',
18+
borderpad=(4, 0))

lib/matplotlib/legend.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1140,9 +1140,10 @@ def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer):
11401140
parentbbox : `~matplotlib.transforms.Bbox`
11411141
A parent box which will contain the bbox, in display coordinates.
11421142
"""
1143+
pad = self.borderaxespad * renderer.points_to_pixels(self._fontsize)
11431144
return offsetbox._get_anchored_bbox(
11441145
loc, bbox, parentbbox,
1145-
self.borderaxespad * renderer.points_to_pixels(self._fontsize))
1146+
pad, pad)
11461147

11471148
def _find_best_position(self, width, height, renderer):
11481149
"""Determine the best location to place the legend."""

lib/matplotlib/offsetbox.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -946,8 +946,13 @@ def __init__(self, loc, *,
946946
See the parameter *loc* of `.Legend` for details.
947947
pad : float, default: 0.4
948948
Padding around the child as fraction of the fontsize.
949-
borderpad : float, default: 0.5
949+
borderpad : float or (float, float), default: 0.5
950950
Padding between the offsetbox frame and the *bbox_to_anchor*.
951+
If a float, the same padding is used for both x and y.
952+
If a tuple of two floats, it specifies the (x, y) padding.
953+
954+
.. versionadded:: 3.11
955+
The *borderpad* parameter now accepts a tuple of (x, y) paddings.
951956
child : `.OffsetBox`
952957
The box that will be anchored.
953958
prop : `.FontProperties`
@@ -1054,12 +1059,22 @@ def set_bbox_to_anchor(self, bbox, transform=None):
10541059
@_compat_get_offset
10551060
def get_offset(self, bbox, renderer):
10561061
# docstring inherited
1057-
pad = (self.borderpad
1058-
* renderer.points_to_pixels(self.prop.get_size_in_points()))
1062+
fontsize_in_pixels = renderer.points_to_pixels(self.prop.get_size_in_points())
1063+
try:
1064+
borderpad_x, borderpad_y = self.borderpad
1065+
except TypeError:
1066+
borderpad_x = self.borderpad
1067+
borderpad_y = self.borderpad
1068+
pad_x_pixels = borderpad_x * fontsize_in_pixels
1069+
pad_y_pixels = borderpad_y * fontsize_in_pixels
10591070
bbox_to_anchor = self.get_bbox_to_anchor()
10601071
x0, y0 = _get_anchored_bbox(
1061-
self.loc, Bbox.from_bounds(0, 0, bbox.width, bbox.height),
1062-
bbox_to_anchor, pad)
1072+
self.loc,
1073+
Bbox.from_bounds(0, 0, bbox.width, bbox.height),
1074+
bbox_to_anchor,
1075+
pad_x_pixels,
1076+
pad_y_pixels
1077+
)
10631078
return x0 - bbox.x0, y0 - bbox.y0
10641079

10651080
def update_frame(self, bbox, fontsize=None):
@@ -1084,15 +1099,15 @@ def draw(self, renderer):
10841099
self.stale = False
10851100

10861101

1087-
def _get_anchored_bbox(loc, bbox, parentbbox, borderpad):
1102+
def _get_anchored_bbox(loc, bbox, parentbbox, pad_x, pad_y):
10881103
"""
10891104
Return the (x, y) position of the *bbox* anchored at the *parentbbox* with
1090-
the *loc* code with the *borderpad*.
1105+
the *loc* code with the *borderpad* and padding *pad_x*, *pad_y*.
10911106
"""
10921107
# This is only called internally and *loc* should already have been
10931108
# validated. If 0 (None), we just let ``bbox.anchored`` raise.
10941109
c = [None, "NE", "NW", "SW", "SE", "E", "W", "E", "S", "N", "C"][loc]
1095-
container = parentbbox.padded(-borderpad)
1110+
container = parentbbox.padded(-pad_x, -pad_y)
10961111
return bbox.anchored(c, container=container).p0
10971112

10981113

lib/matplotlib/offsetbox.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ class AnchoredOffsetbox(OffsetBox):
157157
loc: str,
158158
*,
159159
pad: float = ...,
160-
borderpad: float = ...,
160+
borderpad: float | tuple[float, float] = ...,
161161
child: OffsetBox | None = ...,
162162
prop: FontProperties | None = ...,
163163
frameon: bool = ...,
@@ -185,7 +185,7 @@ class AnchoredText(AnchoredOffsetbox):
185185
loc: str,
186186
*,
187187
pad: float = ...,
188-
borderpad: float = ...,
188+
borderpad: float | tuple[float, float] = ...,
189189
prop: dict[str, Any] | None = ...,
190190
**kwargs
191191
) -> None: ...

lib/matplotlib/tests/test_offsetbox.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,3 +470,40 @@ def test_draggable_in_subfigure():
470470
bbox = ann.get_window_extent()
471471
MouseEvent("button_press_event", fig.canvas, bbox.x1+2, bbox.y1+2)._process()
472472
assert not ann._draggable.got_artist
473+
474+
475+
def test_anchored_offsetbox_tuple_and_float_borderpad():
476+
"""
477+
Test AnchoredOffsetbox correctly handles both float and tuple for borderpad.
478+
"""
479+
480+
fig, ax = plt.subplots()
481+
482+
# Case 1: Establish a baseline with float value
483+
text_float = AnchoredText("float", loc='lower left', borderpad=5)
484+
ax.add_artist(text_float)
485+
486+
# Case 2: Test that a symmetric tuple gives the exact same result.
487+
text_tuple_equal = AnchoredText("tuple", loc='lower left', borderpad=(5, 5))
488+
ax.add_artist(text_tuple_equal)
489+
490+
# Case 3: Test that an asymmetric tuple with different values works as expected.
491+
text_tuple_asym = AnchoredText("tuple_asym", loc='lower left', borderpad=(10, 4))
492+
ax.add_artist(text_tuple_asym)
493+
494+
# Draw the canvas to calculate final positions
495+
fig.canvas.draw()
496+
497+
pos_float = text_float.get_window_extent()
498+
pos_tuple_equal = text_tuple_equal.get_window_extent()
499+
pos_tuple_asym = text_tuple_asym.get_window_extent()
500+
501+
# Assertion 1: Prove that borderpad=5 is identical to borderpad=(5, 5).
502+
assert pos_tuple_equal.x0 == pos_float.x0
503+
assert pos_tuple_equal.y0 == pos_float.y0
504+
505+
# Assertion 2: Prove that the asymmetric padding moved the box
506+
# further from the origin than the baseline in the x-direction and less far
507+
# in the y-direction.
508+
assert pos_tuple_asym.x0 > pos_float.x0
509+
assert pos_tuple_asym.y0 < pos_float.y0

lib/mpl_toolkits/axes_grid1/inset_locator.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,11 +341,16 @@ def inset_axes(parent_axes, width, height, loc='upper right',
341341
342342
%(Axes:kwdoc)s
343343
344-
borderpad : float, default: 0.5
344+
borderpad : float or (float, float), default: 0.5
345345
Padding between inset axes and the bbox_to_anchor.
346+
If a float, the same padding is used for both x and y.
347+
If a tuple of two floats, it specifies the (x, y) padding.
346348
The units are axes font size, i.e. for a default font size of 10 points
347349
*borderpad = 0.5* is equivalent to a padding of 5 points.
348350
351+
.. versionadded:: 3.11
352+
The *borderpad* parameter now accepts a tuple of (x, y) paddings.
353+
349354
Returns
350355
-------
351356
inset_axes : *axes_class*

0 commit comments

Comments
 (0)