diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index a0e30f68..28177177 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) @@ -2998,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(): @@ -3042,7 +3046,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 +3065,36 @@ 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 + and not getattr(tobj, "_ultraplot_manual_size", False) + ): + 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: @@ -3080,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 370f2c52..92fe4eb4 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,47 @@ 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, 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 + + +def test_title_manual_size_ignores_auto_shrink(): + """ + Ensure explicit title sizes bypass auto-scaling. + """ + fig, axs = uplt.subplots(figsize=(2, 2)) + axs.format( + abc=True, + title="X" * 200, + titleloc="left", + abcloc="left", + title_kw={"size": 20}, + ) + title_obj = axs[0]._title_dict["left"] + fig.canvas.draw() + 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)