From 569ed898aa89c39fb4cd2b6161beec2e58f0935b Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 12 Sep 2026 15:26:22 +0100 Subject: [PATCH] ENH: AnchoredOffsetbox for (Sub)Figures --- .../next_whats_new/fig_anchored_offsetbox.rst | 32 +++++++++++++++++++ galleries/examples/misc/anchored_artists.py | 6 ++-- lib/matplotlib/offsetbox.py | 15 ++++++--- lib/matplotlib/tests/test_offsetbox.py | 23 +++++++++++++ 4 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 doc/release/next_whats_new/fig_anchored_offsetbox.rst diff --git a/doc/release/next_whats_new/fig_anchored_offsetbox.rst b/doc/release/next_whats_new/fig_anchored_offsetbox.rst new file mode 100644 index 000000000000..244b253517ca --- /dev/null +++ b/doc/release/next_whats_new/fig_anchored_offsetbox.rst @@ -0,0 +1,32 @@ +``AnchoredOffsetbox`` and ``AnchoredText`` for ``Figure`` and ``SubFigure`` +--------------------------------------------------------------------------- + +The `.AnchoredOffsetbox` and `.AnchoredText` artists may now be directly added to a +`.Figure` or `.SubFigure` and by default will be anchored relative to their parent +``(Sub)Figure``. + + +.. plot:: + :include-source: true + :alt: The left half of the figure contains two text boxes with black outlines. One in the top left of the figure reads "Anchored to Figure". One at the bottom and to the left the vertical center line reads "Anchored to SubFigure". A third box containing a blue circle is halfway up and to the right of the vertical center line. + + import matplotlib.pyplot as plt + import matplotlib.patches as mpatches + from matplotlib.offsetbox import AnchoredText, AnchoredOffsetbox, DrawingArea + + fig = plt.figure() + + sfig1, sfig2 = fig.subfigures(ncols=2) + sfig1.set_facecolor("lemonchiffon") + sfig2.set_facecolor("lightcyan") + + fig_text = AnchoredText("Anchored to Figure", loc="upper left") + fig.add_artist(fig_text) + + sfig_text = AnchoredText("Anchored to SubFigure", loc="lower right") + sfig1.add_artist(sfig_text) + + area = DrawingArea(width=30, height=30) + area.add_artist(mpatches.Circle((15, 15), 15, fc="tab:blue")) + box = AnchoredOffsetbox(child=area, loc="center left") + sfig2.add_artist(box) diff --git a/galleries/examples/misc/anchored_artists.py b/galleries/examples/misc/anchored_artists.py index be600449bba6..af807eecdb60 100644 --- a/galleries/examples/misc/anchored_artists.py +++ b/galleries/examples/misc/anchored_artists.py @@ -22,12 +22,12 @@ from matplotlib.patches import Circle, Ellipse -def draw_text(ax): +def draw_text(fig): """Draw a text-box anchored to the upper-left corner of the figure.""" box = AnchoredOffsetbox(child=TextArea("Figure 1a"), loc="upper left", frameon=True) box.patch.set_boxstyle("round,pad=0.,rounding_size=0.2") - ax.add_artist(box) + fig.add_artist(box) def draw_circles(ax): @@ -68,7 +68,7 @@ def draw_sizebar(ax): fig, ax = plt.subplots() ax.set_aspect(1) -draw_text(ax) +draw_text(fig) draw_circles(ax) draw_ellipse(ax) draw_sizebar(ax) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index ca19a26f2b17..37589ad73749 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -909,10 +909,14 @@ class AnchoredOffsetbox(OffsetBox): """ An OffsetBox placed according to location *loc*. - AnchoredOffsetbox has a single child. When multiple children are needed, - use an extra OffsetBox to enclose them. By default, the offset box is - anchored against its parent Axes. You may explicitly specify the - *bbox_to_anchor*. + AnchoredOffsetbox has a single child. When multiple children are needed, use an + extra OffsetBox to enclose them. By default, the offset box is anchored against its + parent Axes, SubFigure or Figure. You may explicitly specify the *bbox_to_anchor*. + + .. versionadded:: 3.12 + `.AnchoredOffsetbox` can now be added directly to a `.Figure` or `.SubFigure`, + and will by default be anchored to that parent. + """ zorder = 5 # zorder of the legend @@ -1024,7 +1028,8 @@ def get_bbox(self, renderer): def get_bbox_to_anchor(self): """Return the bbox that the box is anchored to.""" if self._bbox_to_anchor is None: - return self.axes.bbox + return (self.axes.bbox if self.axes is not None else + self.get_figure(root=False).bbox) else: transform = self._bbox_to_anchor_transform if transform is None: diff --git a/lib/matplotlib/tests/test_offsetbox.py b/lib/matplotlib/tests/test_offsetbox.py index 4ea6688d8651..514e0c10e556 100644 --- a/lib/matplotlib/tests/test_offsetbox.py +++ b/lib/matplotlib/tests/test_offsetbox.py @@ -507,3 +507,26 @@ def test_anchored_offsetbox_tuple_and_float_borderpad(): # in the y-direction. assert pos_tuple_asym.x0 > pos_float.x0 assert pos_tuple_asym.y0 < pos_float.y0 + + +def test_figure_anchored_offsetbox(): + fig = plt.figure() + sfig1, _ = fig.subfigures(ncols=2) + + box1 = AnchoredText("wibble", loc="upper left", borderpad=0) + fig.add_artist(box1) + + box2 = AnchoredText("meep", loc="lower right", borderpad=0) + sfig1.add_artist(box2) + + fig.draw_without_rendering() + + # Upper left of figure + tight_bb1 = box1.get_tightbbox() + assert tight_bb1.x0 == pytest.approx(0) + assert tight_bb1.y1 == pytest.approx(480) + + # lower right of lefthand subfigure + tight_bb2 = box2.get_tightbbox() + assert tight_bb2.x1 == pytest.approx(320) + assert tight_bb2.y0 == pytest.approx(0)