From 3632b1c8e0c3c75caa7c3e8d8b9df60956b57c74 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Mon, 9 Feb 2026 11:33:06 +1000 Subject: [PATCH] Fix suptitle spacing for non-bottom vertical alignment --- ultraplot/figure.py | 11 ++++++++--- ultraplot/tests/test_figure.py | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index aebb9e777..835b2fe85 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -2322,18 +2322,23 @@ def _align_super_title(self, renderer): ha = self._suptitle.get_ha() va = self._suptitle.get_va() - # Use original centering algorithm for positioning (regardless of alignment) + # Use original centering algorithm for horizontal positioning. x, _ = self._get_align_coord( "top", axs, includepanels=self._includepanels, align=ha, ) - y = self._get_offset_coord("top", axs, renderer, pad=pad, extra=labs) + y_target = self._get_offset_coord("top", axs, renderer, pad=pad, extra=labs) - # Set final position and alignment on the suptitle + # Place suptitle so its *bbox bottom* sits at the target offset. + # This preserves spacing for all vertical alignments (e.g. va='top'). self._suptitle.set_ha(ha) self._suptitle.set_va(va) + self._suptitle.set_position((x, 0)) + bbox = self._suptitle.get_window_extent(renderer) + y_bbox = self.transFigure.inverted().transform((0, bbox.ymin))[1] + y = y_target - y_bbox self._suptitle.set_position((x, y)) def _update_axis_label(self, side, axs): diff --git a/ultraplot/tests/test_figure.py b/ultraplot/tests/test_figure.py index afcd259a3..ecd3fc1a9 100644 --- a/ultraplot/tests/test_figure.py +++ b/ultraplot/tests/test_figure.py @@ -299,6 +299,33 @@ def test_suptitle_kw_position_reverted(ha, expectation): uplt.close("all") +@pytest.mark.parametrize("va", ["bottom", "center", "top"]) +def test_suptitle_vertical_alignment_preserves_top_spacing(va): + """ + Suptitle vertical alignment should not reduce the spacing above top content. + """ + fig, axs = uplt.subplots(ncols=2) + fig.format( + suptitle="Long figure title\nsecond line", + suptitle_kw={"va": va}, + toplabels=("left", "right"), + ) + fig.canvas.draw() + renderer = fig.canvas.get_renderer() + + axs_top = fig._get_align_axes("top") + labs = tuple(t for t in fig._suplabel_dict["top"].values() if t.get_text()) + pad = (fig._suptitle_pad / 72) / fig.get_size_inches()[1] + y_expected = fig._get_offset_coord("top", axs_top, renderer, pad=pad, extra=labs) + + bbox = fig._suptitle.get_window_extent(renderer) + y_actual = fig.transFigure.inverted().transform((0, bbox.ymin))[1] + y_tol = 1.5 / (fig.dpi * fig.get_size_inches()[1]) # ~1.5 px tolerance + assert y_actual >= y_expected - y_tol + + uplt.close("all") + + def test_subplots_pixelsnap_aligns_axes_bounds(): with uplt.rc.context({"subplots.pixelsnap": True}): fig, axs = uplt.subplots(ncols=2, nrows=2)