Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion ultraplot/axes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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():
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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.
Expand Down
111 changes: 111 additions & 0 deletions ultraplot/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,117 @@ def test_dualx_log_transform_is_finite():
assert np.isfinite(transformed).all()


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_title_shrinks_right_aligned_same_location():
"""
Test that right-aligned titles shrink when they would overflow with abc label.
"""
fig, axs = uplt.subplots(figsize=(2, 2))
axs.format(abc=True, title="X" * 100, titleloc="right", abcloc="right")
title_obj = axs[0]._title_dict["right"]
original_size = title_obj.get_fontsize()
fig.canvas.draw()
assert title_obj.get_fontsize() < original_size


def test_title_shrinks_centered_same_location():
"""
Test that centered titles shrink when they would overflow with abc label.
"""
fig, axs = uplt.subplots(figsize=(2, 2))
axs.format(abc=True, title="X" * 150, titleloc="center", abcloc="center")
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_title_shrinks_right_aligned_different_location():
"""
Test that right-aligned titles shrink when overlapping abc at different location.
"""
fig, axs = uplt.subplots(figsize=(3, 2))
axs.format(abc=True, title="X" * 100, titleloc="right", abcloc="left")
title_obj = axs[0]._title_dict["right"]
original_size = title_obj.get_fontsize()
fig.canvas.draw()
assert title_obj.get_fontsize() < original_size


def test_title_shrinks_left_aligned_different_location():
"""
Test that left-aligned titles shrink when overlapping abc at different location.
"""
fig, axs = uplt.subplots(figsize=(3, 2))
axs.format(abc=True, title="X" * 100, titleloc="left", abcloc="right")
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_no_shrink_when_no_overlap():
"""
Test that titles don't shrink when there's no overlap with abc label.
"""
fig, axs = uplt.subplots(figsize=(4, 2))
axs.format(abc=True, title="Short Title", titleloc="left", abcloc="right")
title_obj = axs[0]._title_dict["left"]
original_size = title_obj.get_fontsize()
fig, ax = uplt.subplots()
ax.set_xscale("log")
ax.set_xlim(0.1, 10)
sec = ax.dualx(lambda x: 1 / x)
fig.canvas.draw()
assert title_obj.get_fontsize() == original_size


def test_title_shrinks_centered_left_of_abc():
"""
Test that centered titles shrink when they are to the left of abc label.
This covers the specific case where base_x <= ax0 - pad for centered titles.
"""
fig, axs = uplt.subplots(figsize=(3, 2))
axs.format(abc=True, title="X" * 100, titleloc="center", abcloc="right")
title_obj = axs[0]._title_dict["center"]
original_size = title_obj.get_fontsize()
fig.canvas.draw()
assert title_obj.get_fontsize() < original_size
ticks = axs[0].get_xticks()
assert ticks.size > 0
xy = np.column_stack([ticks, np.zeros_like(ticks)])
transformed = axs[0].transData.transform(xy)
assert np.isfinite(transformed).all()


def test_axis_access():
# attempt to access the ax object 2d and linearly
fig, ax = uplt.subplots(ncols=2, nrows=2)
Expand Down