From 6aafb5622e6d62acc13da0b028da282ea82b6c6d Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Wed, 31 Dec 2025 20:05:22 +1000 Subject: [PATCH 1/5] Shrink title to avoid abc overlap --- ultraplot/axes/base.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index a0e30f68..bb98587e 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3042,7 +3042,9 @@ def _update_title_position(self, renderer): # Offset title away from a-b-c label atext, ttext = aobj.get_text(), tobj.get_text() awidth = twidth = 0 - pad = (abcpad / 72) / self._get_size_inches()[0] + width_inches = self._get_size_inches()[0] + pad = (abcpad / 72) / width_inches + abc_pad = (self._abc_pad / 72) / width_inches ha = aobj.get_ha() # Get dimensions of non-empty elements @@ -3059,6 +3061,30 @@ def _update_title_position(self, renderer): .width ) + # Shrink the title font if both texts share a location and would overflow + if atext and ttext and self._abc_loc == self._title_loc and twidth > 0: + scale = 1 + base_x = tobj.get_position()[0] + if ha == "left": + available = 1 - (base_x + awidth + pad) + if available < twidth and available > 0: + scale = available / twidth + elif ha == "right": + available = base_x + abc_pad - pad - awidth + if available < twidth and available > 0: + scale = available / twidth + elif ha == "center": + # Conservative fit for centered titles sharing the abc location + left_room = base_x - 0.5 * (awidth + pad) + right_room = 1 - (base_x + 0.5 * (awidth + pad)) + max_room = min(left_room, right_room) + if max_room < twidth / 2 and max_room > 0: + scale = (2 * max_room) / twidth + + if scale < 1: + tobj.set_fontsize(tobj.get_fontsize() * scale) + twidth *= scale + # Calculate offsets based on alignment and content aoffset = toffset = 0 if atext and ttext: From b6be117e94b7dda8e6ead020e55135180c28d930 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Wed, 31 Dec 2025 20:14:50 +1000 Subject: [PATCH 2/5] Skip title auto-scaling when fontsize is set --- ultraplot/axes/base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index bb98587e..c00edf30 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -2986,6 +2986,8 @@ def _update_title(self, loc, title=None, **kwargs): kw["text"] = title[self.number - 1] else: raise ValueError(f"Invalid title {title!r}. Must be string(s).") + if any(key in kwargs for key in ("size", "fontsize")): + self._title_dict[loc]._ultraplot_manual_size = True kw.update(kwargs) self._title_dict[loc].update(kw) @@ -3062,7 +3064,13 @@ def _update_title_position(self, renderer): ) # Shrink the title font if both texts share a location and would overflow - if atext and ttext and self._abc_loc == self._title_loc and twidth > 0: + if ( + atext + and ttext + and self._abc_loc == self._title_loc + and twidth > 0 + and not getattr(tobj, "_ultraplot_manual_size", False) + ): scale = 1 base_x = tobj.get_position()[0] if ha == "left": From 5660475f3b6d56a59196eed923c8dc14528b7c0b Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Wed, 31 Dec 2025 20:16:19 +1000 Subject: [PATCH 3/5] Add tests for abc/title auto-scaling --- ultraplot/tests/test_axes.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/ultraplot/tests/test_axes.py b/ultraplot/tests/test_axes.py index 370f2c52..aaae190e 100644 --- a/ultraplot/tests/test_axes.py +++ b/ultraplot/tests/test_axes.py @@ -4,6 +4,7 @@ """ import numpy as np import pytest + import ultraplot as uplt from ultraplot.internals.warnings import UltraPlotWarning @@ -130,6 +131,35 @@ def test_cartesian_format_all_units_types(): ax.format(**kwargs) +def test_title_shrinks_when_abc_overlaps(): + """ + Ensure long titles shrink when they would overlap the abc label. + """ + fig, ax = uplt.subplots(figsize=(2, 2)) + ax.format(abc=True, title="X" * 200, titleloc="left", abcloc="left") + title_obj = ax._title_dict["left"] + original_size = title_obj.get_fontsize() + fig.canvas.draw() + assert title_obj.get_fontsize() < original_size + + +def test_title_manual_size_ignores_auto_shrink(): + """ + Ensure explicit title sizes bypass auto-scaling. + """ + fig, ax = uplt.subplots(figsize=(2, 2)) + ax.format( + abc=True, + title="X" * 200, + titleloc="left", + abcloc="left", + title_kw={"size": 20}, + ) + title_obj = ax._title_dict["left"] + fig.canvas.draw() + assert title_obj.get_fontsize() == 20 + + def test_axis_access(): # attempt to access the ax object 2d and linearly fig, ax = uplt.subplots(ncols=2, nrows=2) From de5bc3ba03c053fccd941364770c532699baca69 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 1 Jan 2026 14:21:27 +1000 Subject: [PATCH 4/5] Fix title overlap tests and zero-size axes draw --- ultraplot/axes/base.py | 2 ++ ultraplot/tests/test_axes.py | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index c00edf30..48af4bc5 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3000,6 +3000,8 @@ def _update_title_position(self, renderer): # NOTE: Critical to do this every time in case padding changes or # we added or removed an a-b-c label in the same position as a title width, height = self._get_size_inches() + if width <= 0 or height <= 0: + return x_pad = self._title_pad / (72 * width) y_pad = self._title_pad / (72 * height) for loc, obj in self._title_dict.items(): diff --git a/ultraplot/tests/test_axes.py b/ultraplot/tests/test_axes.py index aaae190e..7eb804b8 100644 --- a/ultraplot/tests/test_axes.py +++ b/ultraplot/tests/test_axes.py @@ -135,9 +135,9 @@ def test_title_shrinks_when_abc_overlaps(): """ Ensure long titles shrink when they would overlap the abc label. """ - fig, ax = uplt.subplots(figsize=(2, 2)) - ax.format(abc=True, title="X" * 200, titleloc="left", abcloc="left") - title_obj = ax._title_dict["left"] + fig, axs = uplt.subplots(figsize=(2, 2)) + axs.format(abc=True, title="X" * 200, titleloc="left", abcloc="left") + title_obj = axs[0]._title_dict["left"] original_size = title_obj.get_fontsize() fig.canvas.draw() assert title_obj.get_fontsize() < original_size @@ -147,15 +147,15 @@ def test_title_manual_size_ignores_auto_shrink(): """ Ensure explicit title sizes bypass auto-scaling. """ - fig, ax = uplt.subplots(figsize=(2, 2)) - ax.format( + fig, axs = uplt.subplots(figsize=(2, 2)) + axs.format( abc=True, title="X" * 200, titleloc="left", abcloc="left", title_kw={"size": 20}, ) - title_obj = ax._title_dict["left"] + title_obj = axs[0]._title_dict["left"] fig.canvas.draw() assert title_obj.get_fontsize() == 20 From 2b92a065ae27fbf8cf1f2614ed417d08e399ca2f Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 1 Jan 2026 14:28:19 +1000 Subject: [PATCH 5/5] Shrink titles when abc overlaps across locations --- ultraplot/axes/base.py | 38 ++++++++++++++++++++++++++++++++++++ ultraplot/tests/test_axes.py | 12 ++++++++++++ 2 files changed, 50 insertions(+) diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 48af4bc5..28177177 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -3116,6 +3116,44 @@ def _update_title_position(self, renderer): if ttext: tobj.set_x(tobj.get_position()[0] + toffset) + # Shrink title if it overlaps the abc label at a different location + if ( + atext + and self._abc_loc != self._title_loc + and not getattr( + self._title_dict[self._title_loc], "_ultraplot_manual_size", False + ) + ): + title_obj = self._title_dict[self._title_loc] + title_text = title_obj.get_text() + if title_text: + abc_bbox = aobj.get_window_extent(renderer).transformed( + self.transAxes.inverted() + ) + title_bbox = title_obj.get_window_extent(renderer).transformed( + self.transAxes.inverted() + ) + ax0, ax1 = abc_bbox.x0, abc_bbox.x1 + tx0, tx1 = title_bbox.x0, title_bbox.x1 + if tx0 < ax1 + pad and tx1 > ax0 - pad: + base_x = title_obj.get_position()[0] + ha = title_obj.get_ha() + max_width = 0 + if ha == "left": + if base_x <= ax0 - pad: + max_width = (ax0 - pad) - base_x + elif ha == "right": + if base_x >= ax1 + pad: + max_width = base_x - (ax1 + pad) + elif ha == "center": + if base_x >= ax1 + pad: + max_width = 2 * (base_x - (ax1 + pad)) + elif base_x <= ax0 - pad: + max_width = 2 * ((ax0 - pad) - base_x) + if 0 < max_width < title_bbox.width: + scale = max_width / title_bbox.width + title_obj.set_fontsize(title_obj.get_fontsize() * scale) + def _update_super_title(self, suptitle=None, **kwargs): """ Update the figure super title. diff --git a/ultraplot/tests/test_axes.py b/ultraplot/tests/test_axes.py index 7eb804b8..92fe4eb4 100644 --- a/ultraplot/tests/test_axes.py +++ b/ultraplot/tests/test_axes.py @@ -160,6 +160,18 @@ def test_title_manual_size_ignores_auto_shrink(): assert title_obj.get_fontsize() == 20 +def test_title_shrinks_when_abc_overlaps_different_loc(): + """ + Ensure long titles shrink when overlapping abc at a different location. + """ + fig, axs = uplt.subplots(figsize=(3, 2)) + axs.format(abc=True, title="X" * 200, titleloc="center", abcloc="left") + title_obj = axs[0]._title_dict["center"] + original_size = title_obj.get_fontsize() + fig.canvas.draw() + assert title_obj.get_fontsize() < original_size + + def test_axis_access(): # attempt to access the ax object 2d and linearly fig, ax = uplt.subplots(ncols=2, nrows=2)