Repository: matplotlib/matplotlib

Issue:
[ENH]: inset_axes has borderpadding, but not x/y individually.
### Problem

axislabels (ticks labels or labels) of inset_axes feature can sometimes overlap the outer axes of the main plot when location is set to upper left, lower left. borderpad in inset_axes allows the user to offset the axes from the main axes, but will in both x and y. When I place an inset in the upper left corner, it should not move down, but only to the right to not overlap with the main plot axis labels, and not move into the figure more than it should.

It would be a great feature to have borderpadding in the form of a tuple, to offset the inset axes by only x- or y-padding.

### Proposed solution

Allow inset_axes(borderpad=(0.5, 0.5))
This will also require some backpropagation into the upper classes and methods.



Discussion:

Patch:
diff --git a/doc/users/next_whats_new/updated_borderpad_parameter.rst b/doc/users/next_whats_new/updated_borderpad_parameter.rst
new file mode 100644
index 000000000000..5acf075f7b51
--- /dev/null
+++ b/doc/users/next_whats_new/updated_borderpad_parameter.rst
@@ -0,0 +1,18 @@
+``borderpad`` accepts a tuple for separate x/y padding
+-------------------------------------------------------
+
+The ``borderpad`` parameter used for placing anchored artists (such as inset axes) now accepts a tuple of ``(x_pad, y_pad)``.
+
+This allows for specifying separate padding values for the horizontal and
+vertical directions, providing finer control over placement. For example, when
+placing an inset in a corner, one might want horizontal padding to avoid
+overlapping with the main plot's axis labels, but no vertical padding to keep
+the inset flush with the plot area edge.
+
+Example usage with :func:`~mpl_toolkits.axes_grid1.inset_locator.inset_axes`:
+
+.. code-block:: python
+
+    ax_inset = inset_axes(
+        ax, width="30%", height="30%", loc='upper left',
+        borderpad=(4, 0))
diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py
index 2fb14e52c58c..933b3f7c9eaa 100644
--- a/lib/matplotlib/legend.py
+++ b/lib/matplotlib/legend.py
@@ -1140,9 +1140,10 @@ def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer):
         parentbbox : `~matplotlib.transforms.Bbox`
             A parent box which will contain the bbox, in display coordinates.
         """
+        pad = self.borderaxespad * renderer.points_to_pixels(self._fontsize)
         return offsetbox._get_anchored_bbox(
             loc, bbox, parentbbox,
-            self.borderaxespad * renderer.points_to_pixels(self._fontsize))
+            pad, pad)
 
     def _find_best_position(self, width, height, renderer):
         """Determine the best location to place the legend."""
diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py
index 974cc4f2db05..39035e0b785a 100644
--- a/lib/matplotlib/offsetbox.py
+++ b/lib/matplotlib/offsetbox.py
@@ -946,8 +946,13 @@ def __init__(self, loc, *,
             See the parameter *loc* of `.Legend` for details.
         pad : float, default: 0.4
             Padding around the child as fraction of the fontsize.
-        borderpad : float, default: 0.5
+        borderpad : float or (float, float), default: 0.5
             Padding between the offsetbox frame and the *bbox_to_anchor*.
+            If a float, the same padding is used for both x and y.
+            If a tuple of two floats, it specifies the (x, y) padding.
+
+            .. versionadded:: 3.11
+               The *borderpad* parameter now accepts a tuple of (x, y) paddings.
         child : `.OffsetBox`
             The box that will be anchored.
         prop : `.FontProperties`
@@ -1054,12 +1059,22 @@ def set_bbox_to_anchor(self, bbox, transform=None):
     @_compat_get_offset
     def get_offset(self, bbox, renderer):
         # docstring inherited
-        pad = (self.borderpad
-               * renderer.points_to_pixels(self.prop.get_size_in_points()))
+        fontsize_in_pixels = renderer.points_to_pixels(self.prop.get_size_in_points())
+        try:
+            borderpad_x, borderpad_y = self.borderpad
+        except TypeError:
+            borderpad_x = self.borderpad
+            borderpad_y = self.borderpad
+        pad_x_pixels = borderpad_x * fontsize_in_pixels
+        pad_y_pixels = borderpad_y * fontsize_in_pixels
         bbox_to_anchor = self.get_bbox_to_anchor()
         x0, y0 = _get_anchored_bbox(
-            self.loc, Bbox.from_bounds(0, 0, bbox.width, bbox.height),
-            bbox_to_anchor, pad)
+            self.loc,
+            Bbox.from_bounds(0, 0, bbox.width, bbox.height),
+            bbox_to_anchor,
+            pad_x_pixels,
+            pad_y_pixels
+        )
         return x0 - bbox.x0, y0 - bbox.y0
 
     def update_frame(self, bbox, fontsize=None):
@@ -1084,15 +1099,15 @@ def draw(self, renderer):
         self.stale = False
 
 
-def _get_anchored_bbox(loc, bbox, parentbbox, borderpad):
+def _get_anchored_bbox(loc, bbox, parentbbox, pad_x, pad_y):
     """
     Return the (x, y) position of the *bbox* anchored at the *parentbbox* with
-    the *loc* code with the *borderpad*.
+    the *loc* code with the *borderpad* and padding *pad_x*, *pad_y*.
     """
     # This is only called internally and *loc* should already have been
     # validated.  If 0 (None), we just let ``bbox.anchored`` raise.
     c = [None, "NE", "NW", "SW", "SE", "E", "W", "E", "S", "N", "C"][loc]
-    container = parentbbox.padded(-borderpad)
+    container = parentbbox.padded(-pad_x, -pad_y)
     return bbox.anchored(c, container=container).p0
 
 
diff --git a/lib/matplotlib/offsetbox.pyi b/lib/matplotlib/offsetbox.pyi
index 8a2016c0320a..36f31908eebf 100644
--- a/lib/matplotlib/offsetbox.pyi
+++ b/lib/matplotlib/offsetbox.pyi
@@ -157,7 +157,7 @@ class AnchoredOffsetbox(OffsetBox):
         loc: str,
         *,
         pad: float = ...,
-        borderpad: float = ...,
+        borderpad: float | tuple[float, float] = ...,
         child: OffsetBox | None = ...,
         prop: FontProperties | None = ...,
         frameon: bool = ...,
@@ -185,7 +185,7 @@ class AnchoredText(AnchoredOffsetbox):
         loc: str,
         *,
         pad: float = ...,
-        borderpad: float = ...,
+        borderpad: float | tuple[float, float] = ...,
         prop: dict[str, Any] | None = ...,
         **kwargs
     ) -> None: ...
diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py
index 52fe6efc0618..a1a9cc8df591 100644
--- a/lib/mpl_toolkits/axes_grid1/inset_locator.py
+++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py
@@ -341,11 +341,16 @@ def inset_axes(parent_axes, width, height, loc='upper right',
 
         %(Axes:kwdoc)s
 
-    borderpad : float, default: 0.5
+    borderpad : float or (float, float), default: 0.5
         Padding between inset axes and the bbox_to_anchor.
+        If a float, the same padding is used for both x and y.
+        If a tuple of two floats, it specifies the (x, y) padding.
         The units are axes font size, i.e. for a default font size of 10 points
         *borderpad = 0.5* is equivalent to a padding of 5 points.
 
+        .. versionadded:: 3.11
+           The *borderpad* parameter now accepts a tuple of (x, y) paddings.
+
     Returns
     -------
     inset_axes : *axes_class*


Test patch:
diff --git a/lib/matplotlib/tests/test_offsetbox.py b/lib/matplotlib/tests/test_offsetbox.py
index bd353ffc719b..f126b1cbb466 100644
--- a/lib/matplotlib/tests/test_offsetbox.py
+++ b/lib/matplotlib/tests/test_offsetbox.py
@@ -470,3 +470,40 @@ def test_draggable_in_subfigure():
     bbox = ann.get_window_extent()
     MouseEvent("button_press_event", fig.canvas, bbox.x1+2, bbox.y1+2)._process()
     assert not ann._draggable.got_artist
+
+
+def test_anchored_offsetbox_tuple_and_float_borderpad():
+    """
+    Test AnchoredOffsetbox correctly handles both float and tuple for borderpad.
+    """
+
+    fig, ax = plt.subplots()
+
+    # Case 1: Establish a baseline with float value
+    text_float = AnchoredText("float", loc='lower left', borderpad=5)
+    ax.add_artist(text_float)
+
+    # Case 2: Test that a symmetric tuple gives the exact same result.
+    text_tuple_equal = AnchoredText("tuple", loc='lower left', borderpad=(5, 5))
+    ax.add_artist(text_tuple_equal)
+
+    # Case 3: Test that an asymmetric tuple with different values works as expected.
+    text_tuple_asym = AnchoredText("tuple_asym", loc='lower left', borderpad=(10, 4))
+    ax.add_artist(text_tuple_asym)
+
+    # Draw the canvas to calculate final positions
+    fig.canvas.draw()
+
+    pos_float = text_float.get_window_extent()
+    pos_tuple_equal = text_tuple_equal.get_window_extent()
+    pos_tuple_asym = text_tuple_asym.get_window_extent()
+
+    # Assertion 1: Prove that borderpad=5 is identical to borderpad=(5, 5).
+    assert pos_tuple_equal.x0 == pos_float.x0
+    assert pos_tuple_equal.y0 == pos_float.y0
+
+    # Assertion 2: Prove that the asymmetric padding moved the box
+    # further from the origin than the baseline in the x-direction and less far
+    # in the y-direction.
+    assert pos_tuple_asym.x0 > pos_float.x0
+    assert pos_tuple_asym.y0 < pos_float.y0