Skip to content

Commit 1510047

Browse files
authored
Avoid title overlap with abc labels (#442)
* Shrink title to avoid abc overlap * Skip title auto-scaling when fontsize is set * Add tests for abc/title auto-scaling * Fix title overlap tests and zero-size axes draw * Shrink titles when abc overlaps across locations * update tests * re-add tests * Clarify padding variable names in _update_title_position Renamed local variables to better reflect their purpose: - abcpad -> abc_title_sep_pts (abc-title separation in points) - pad -> abc_title_sep (abc-title separation in axes coords) - abc_pad -> abc_offset (user's horizontal offset in axes coords) Added comprehensive inline comments explaining: - The difference between abc-title separation (spacing when co-located) and abc offset (user's horizontal shift via abcpad parameter) - Unit conversions from points to axes coordinates - Source and purpose of each variable Updated documentation: - Enhanced abcpad parameter docstring to clarify it's a horizontal offset - Added inline comments to instance variables at initialization This addresses co-author feedback requesting clarification of the relationship between abcpad, self._abc_pad, pad, and abc_pad variables. No API changes - all modifications are internal refactoring only.
1 parent a80b002 commit 1510047

File tree

2 files changed

+209
-12
lines changed

2 files changed

+209
-12
lines changed

ultraplot/axes/base.py

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,9 @@
354354
inside the axes. This can help them stand out on top of artists plotted
355355
inside the axes.
356356
abcpad : float or unit-spec, default: :rc:`abc.pad`
357-
The padding for the inner and outer titles and a-b-c labels.
357+
Horizontal offset to shift the a-b-c label position. Positive values move
358+
the label right, negative values move it left. This is separate from
359+
`abctitlepad`, which controls spacing between abc and title when co-located.
358360
%(units.pt)s
359361
abc_kw, title_kw : dict-like, optional
360362
Additional settings used to update the a-b-c label and title
@@ -846,8 +848,10 @@ def __init__(self, *args, **kwargs):
846848
self._auto_format = None # manipulated by wrapper functions
847849
self._abc_border_kwargs = {}
848850
self._abc_loc = None
849-
self._abc_pad = 0
850-
self._abc_title_pad = rc["abc.titlepad"]
851+
self._abc_pad = 0 # User's horizontal offset for abc label (in points)
852+
self._abc_title_pad = rc[
853+
"abc.titlepad"
854+
] # Spacing between abc and title when co-located
851855
self._title_above = rc["title.above"]
852856
self._title_border_kwargs = {} # title border properties
853857
self._title_loc = None
@@ -2986,6 +2990,8 @@ def _update_title(self, loc, title=None, **kwargs):
29862990
kw["text"] = title[self.number - 1]
29872991
else:
29882992
raise ValueError(f"Invalid title {title!r}. Must be string(s).")
2993+
if any(key in kwargs for key in ("size", "fontsize")):
2994+
self._title_dict[loc]._ultraplot_manual_size = True
29892995
kw.update(kwargs)
29902996
self._title_dict[loc].update(kw)
29912997

@@ -2998,6 +3004,8 @@ def _update_title_position(self, renderer):
29983004
# NOTE: Critical to do this every time in case padding changes or
29993005
# we added or removed an a-b-c label in the same position as a title
30003006
width, height = self._get_size_inches()
3007+
if width <= 0 or height <= 0:
3008+
return
30013009
x_pad = self._title_pad / (72 * width)
30023010
y_pad = self._title_pad / (72 * height)
30033011
for loc, obj in self._title_dict.items():
@@ -3010,7 +3018,8 @@ def _update_title_position(self, renderer):
30103018
# This is known matplotlib problem but especially annoying with top panels.
30113019
# NOTE: See axis.get_ticks_position for inspiration
30123020
pad = self._title_pad
3013-
abcpad = self._abc_title_pad
3021+
# Horizontal separation between abc label and title when co-located (in points)
3022+
abc_title_sep_pts = self._abc_title_pad
30143023
if self.xaxis.get_visible() and any(
30153024
tick.tick2line.get_visible() and not tick.label2.get_visible()
30163025
for tick in self.xaxis.majorTicks
@@ -3038,11 +3047,19 @@ def _update_title_position(self, renderer):
30383047

30393048
# Offset title away from a-b-c label
30403049
# NOTE: Title texts all use axes transform in x-direction
3041-
3042-
# Offset title away from a-b-c label
3050+
# We need to convert padding values from points to axes coordinates (0-1 normalized)
30433051
atext, ttext = aobj.get_text(), tobj.get_text()
30443052
awidth = twidth = 0
3045-
pad = (abcpad / 72) / self._get_size_inches()[0]
3053+
width_inches = self._get_size_inches()[0]
3054+
3055+
# Convert abc-title separation from points to axes coordinates
3056+
# This is the spacing BETWEEN abc and title when they share the same location
3057+
abc_title_sep = (abc_title_sep_pts / 72) / width_inches
3058+
3059+
# Convert user's horizontal offset from points to axes coordinates
3060+
# This is the user-specified shift for the abc label position (via abcpad parameter)
3061+
abc_offset = (self._abc_pad / 72) / width_inches
3062+
30463063
ha = aobj.get_ha()
30473064

30483065
# Get dimensions of non-empty elements
@@ -3059,27 +3076,96 @@ def _update_title_position(self, renderer):
30593076
.width
30603077
)
30613078

3079+
# Shrink the title font if both texts share a location and would overflow
3080+
if (
3081+
atext
3082+
and ttext
3083+
and self._abc_loc == self._title_loc
3084+
and twidth > 0
3085+
and not getattr(tobj, "_ultraplot_manual_size", False)
3086+
):
3087+
scale = 1
3088+
base_x = tobj.get_position()[0]
3089+
if ha == "left":
3090+
available = 1 - (base_x + awidth + abc_title_sep)
3091+
if available < twidth and available > 0:
3092+
scale = available / twidth
3093+
elif ha == "right":
3094+
available = base_x + abc_offset - abc_title_sep - awidth
3095+
if available < twidth and available > 0:
3096+
scale = available / twidth
3097+
elif ha == "center":
3098+
# Conservative fit for centered titles sharing the abc location
3099+
left_room = base_x - 0.5 * (awidth + abc_title_sep)
3100+
right_room = 1 - (base_x + 0.5 * (awidth + abc_title_sep))
3101+
max_room = min(left_room, right_room)
3102+
if max_room < twidth / 2 and max_room > 0:
3103+
scale = (2 * max_room) / twidth
3104+
3105+
if scale < 1:
3106+
tobj.set_fontsize(tobj.get_fontsize() * scale)
3107+
twidth *= scale
3108+
30623109
# Calculate offsets based on alignment and content
30633110
aoffset = toffset = 0
30643111
if atext and ttext:
30653112
if ha == "left":
3066-
toffset = awidth + pad
3113+
toffset = awidth + abc_title_sep
30673114
elif ha == "right":
3068-
aoffset = -(twidth + pad)
3115+
aoffset = -(twidth + abc_title_sep)
30693116
elif ha == "center":
3070-
toffset = 0.5 * (awidth + pad)
3071-
aoffset = -0.5 * (twidth + pad)
3117+
toffset = 0.5 * (awidth + abc_title_sep)
3118+
aoffset = -0.5 * (twidth + abc_title_sep)
30723119

30733120
# Apply positioning adjustments
3121+
# For abc label: apply offset from co-located title + user's horizontal offset
30743122
if atext:
30753123
aobj.set_x(
30763124
aobj.get_position()[0]
30773125
+ aoffset
3078-
+ (self._abc_pad / 72) / (self._get_size_inches()[0])
3126+
+ abc_offset # User's horizontal shift (from abcpad parameter)
30793127
)
30803128
if ttext:
30813129
tobj.set_x(tobj.get_position()[0] + toffset)
30823130

3131+
# Shrink title if it overlaps the abc label at a different location
3132+
if (
3133+
atext
3134+
and self._abc_loc != self._title_loc
3135+
and not getattr(
3136+
self._title_dict[self._title_loc], "_ultraplot_manual_size", False
3137+
)
3138+
):
3139+
title_obj = self._title_dict[self._title_loc]
3140+
title_text = title_obj.get_text()
3141+
if title_text:
3142+
abc_bbox = aobj.get_window_extent(renderer).transformed(
3143+
self.transAxes.inverted()
3144+
)
3145+
title_bbox = title_obj.get_window_extent(renderer).transformed(
3146+
self.transAxes.inverted()
3147+
)
3148+
ax0, ax1 = abc_bbox.x0, abc_bbox.x1
3149+
tx0, tx1 = title_bbox.x0, title_bbox.x1
3150+
if tx0 < ax1 + abc_title_sep and tx1 > ax0 - abc_title_sep:
3151+
base_x = title_obj.get_position()[0]
3152+
ha = title_obj.get_ha()
3153+
max_width = 0
3154+
if ha == "left":
3155+
if base_x <= ax0 - abc_title_sep:
3156+
max_width = (ax0 - abc_title_sep) - base_x
3157+
elif ha == "right":
3158+
if base_x >= ax1 + abc_title_sep:
3159+
max_width = base_x - (ax1 + abc_title_sep)
3160+
elif ha == "center":
3161+
if base_x >= ax1 + abc_title_sep:
3162+
max_width = 2 * (base_x - (ax1 + abc_title_sep))
3163+
elif base_x <= ax0 - abc_title_sep:
3164+
max_width = 2 * ((ax0 - abc_title_sep) - base_x)
3165+
if 0 < max_width < title_bbox.width:
3166+
scale = max_width / title_bbox.width
3167+
title_obj.set_fontsize(title_obj.get_fontsize() * scale)
3168+
30833169
def _update_super_title(self, suptitle=None, **kwargs):
30843170
"""
30853171
Update the figure super title.

ultraplot/tests/test_axes.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,117 @@ def test_dualx_log_transform_is_finite():
148148
assert np.isfinite(transformed).all()
149149

150150

151+
def test_title_manual_size_ignores_auto_shrink():
152+
"""
153+
Ensure explicit title sizes bypass auto-scaling.
154+
"""
155+
fig, axs = uplt.subplots(figsize=(2, 2))
156+
axs.format(
157+
abc=True,
158+
title="X" * 200,
159+
titleloc="left",
160+
abcloc="left",
161+
title_kw={"size": 20},
162+
)
163+
title_obj = axs[0]._title_dict["left"]
164+
fig.canvas.draw()
165+
assert title_obj.get_fontsize() == 20
166+
167+
168+
def test_title_shrinks_when_abc_overlaps_different_loc():
169+
"""
170+
Ensure long titles shrink when overlapping abc at a different location.
171+
"""
172+
fig, axs = uplt.subplots(figsize=(3, 2))
173+
axs.format(abc=True, title="X" * 200, titleloc="center", abcloc="left")
174+
title_obj = axs[0]._title_dict["center"]
175+
original_size = title_obj.get_fontsize()
176+
fig.canvas.draw()
177+
assert title_obj.get_fontsize() < original_size
178+
179+
180+
def test_title_shrinks_right_aligned_same_location():
181+
"""
182+
Test that right-aligned titles shrink when they would overflow with abc label.
183+
"""
184+
fig, axs = uplt.subplots(figsize=(2, 2))
185+
axs.format(abc=True, title="X" * 100, titleloc="right", abcloc="right")
186+
title_obj = axs[0]._title_dict["right"]
187+
original_size = title_obj.get_fontsize()
188+
fig.canvas.draw()
189+
assert title_obj.get_fontsize() < original_size
190+
191+
192+
def test_title_shrinks_centered_same_location():
193+
"""
194+
Test that centered titles shrink when they would overflow with abc label.
195+
"""
196+
fig, axs = uplt.subplots(figsize=(2, 2))
197+
axs.format(abc=True, title="X" * 150, titleloc="center", abcloc="center")
198+
title_obj = axs[0]._title_dict["center"]
199+
original_size = title_obj.get_fontsize()
200+
fig.canvas.draw()
201+
assert title_obj.get_fontsize() < original_size
202+
203+
204+
def test_title_shrinks_right_aligned_different_location():
205+
"""
206+
Test that right-aligned titles shrink when overlapping abc at different location.
207+
"""
208+
fig, axs = uplt.subplots(figsize=(3, 2))
209+
axs.format(abc=True, title="X" * 100, titleloc="right", abcloc="left")
210+
title_obj = axs[0]._title_dict["right"]
211+
original_size = title_obj.get_fontsize()
212+
fig.canvas.draw()
213+
assert title_obj.get_fontsize() < original_size
214+
215+
216+
def test_title_shrinks_left_aligned_different_location():
217+
"""
218+
Test that left-aligned titles shrink when overlapping abc at different location.
219+
"""
220+
fig, axs = uplt.subplots(figsize=(3, 2))
221+
axs.format(abc=True, title="X" * 100, titleloc="left", abcloc="right")
222+
title_obj = axs[0]._title_dict["left"]
223+
original_size = title_obj.get_fontsize()
224+
fig.canvas.draw()
225+
assert title_obj.get_fontsize() < original_size
226+
227+
228+
def test_title_no_shrink_when_no_overlap():
229+
"""
230+
Test that titles don't shrink when there's no overlap with abc label.
231+
"""
232+
fig, axs = uplt.subplots(figsize=(4, 2))
233+
axs.format(abc=True, title="Short Title", titleloc="left", abcloc="right")
234+
title_obj = axs[0]._title_dict["left"]
235+
original_size = title_obj.get_fontsize()
236+
fig, ax = uplt.subplots()
237+
ax.set_xscale("log")
238+
ax.set_xlim(0.1, 10)
239+
sec = ax.dualx(lambda x: 1 / x)
240+
fig.canvas.draw()
241+
assert title_obj.get_fontsize() == original_size
242+
243+
244+
def test_title_shrinks_centered_left_of_abc():
245+
"""
246+
Test that centered titles shrink when they are to the left of abc label.
247+
This covers the specific case where base_x <= ax0 - pad for centered titles.
248+
"""
249+
fig, axs = uplt.subplots(figsize=(3, 2))
250+
axs.format(abc=True, title="X" * 100, titleloc="center", abcloc="right")
251+
title_obj = axs[0]._title_dict["center"]
252+
original_size = title_obj.get_fontsize()
253+
fig.canvas.draw()
254+
assert title_obj.get_fontsize() < original_size
255+
ticks = axs[0].get_xticks()
256+
assert ticks.size > 0
257+
xy = np.column_stack([ticks, np.zeros_like(ticks)])
258+
transformed = axs[0].transData.transform(xy)
259+
assert np.isfinite(transformed).all()
260+
261+
151262
def test_axis_access():
152263
# attempt to access the ax object 2d and linearly
153264
fig, ax = uplt.subplots(ncols=2, nrows=2)

0 commit comments

Comments
 (0)