Skip to content
Merged
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
18 changes: 16 additions & 2 deletions ultraplot/axes/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

__all__ = ["ExternalAxesContainer"]

_ABOVE_AXES_TITLE_LOCS = {"left", "center", "right"}


class ExternalAxesContainer(CartesianAxes):
"""
Expand Down Expand Up @@ -399,7 +401,9 @@ def _ensure_external_fits_within_container(self, renderer):
container_bbox = container_pos.transformed(self.figure.transFigure)
# Reserve vertical space for titles/abc labels.
title_pad_px = 0.0
for obj in self._title_dict.values():
for loc, obj in self._title_dict.items():
if not self._title_reserves_external_space(loc):
continue
if not obj.get_visible():
continue
if not obj.get_text():
Expand Down Expand Up @@ -548,14 +552,24 @@ def _update_title_position(self, renderer):
container_bbox = self.get_position().transformed(fig.transFigure)
if container_bbox.height <= 0:
return
for obj in self._title_dict.values():
for loc, obj in self._title_dict.items():
if not self._title_reserves_external_space(loc):
continue
bbox = obj.get_window_extent(renderer)
overflow = bbox.y1 - container_bbox.y1
if overflow > 0:
x, y = obj.get_position()
y -= overflow / container_bbox.height
obj.set_position((x, y))

def _title_reserves_external_space(self, loc):
"""
Return whether a title-like artist needs room above an external axes.
"""
if loc == "abc":
loc = self._abc_loc
return loc in _ABOVE_AXES_TITLE_LOCS

def _iter_axes(self, hidden=True, children=True, panels=True):
"""
Override to only yield the container itself, not the external axes.
Expand Down
47 changes: 47 additions & 0 deletions ultraplot/tests/test_external_container_edge_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,53 @@ def points_to_pixels(self, value):
assert new_pos.height <= initial_pos.height


class ContainerTightBboxAxes(MinimalExternalAxes):
"""External axes whose tight bbox exactly matches its current position."""

def get_tightbbox(self, renderer):
return self._position.transformed(self.figure.transFigure)


def _external_position_after_abc_fit(abcloc):
fig = uplt.figure()
ax = ExternalAxesContainer(
fig,
1,
1,
1,
external_axes_class=ContainerTightBboxAxes,
external_axes_kwargs={},
external_padding=0.0,
external_shrink_factor=1.0,
)
ax.number = 1
ax.format(abc="A", abcloc=abcloc)

child = ax.get_external_child()
child.set_position(ax.get_position())
initial_pos = child.get_position().frozen()
renderer = fig.canvas.get_renderer()
ax._update_title_position(renderer)
ax._ensure_external_fits_within_container(renderer)
return initial_pos, child.get_position()


def test_inside_abc_location_does_not_reserve_external_title_space():
"""Inside abc labels should not shrink the external axes title area."""
initial_pos, new_pos = _external_position_after_abc_fit("upper left")

assert new_pos.height == pytest.approx(initial_pos.height)
assert new_pos.y0 == pytest.approx(initial_pos.y0)


def test_above_axes_abc_location_reserves_external_title_space():
"""Above-axes abc labels should still leave room above external axes."""
initial_pos, new_pos = _external_position_after_abc_fit("left")

assert new_pos.height < initial_pos.height
assert new_pos.y0 > initial_pos.y0


def test_external_axes_fallback_to_rect_on_typeerror():
"""Test fallback to rect init when subplotspec is unsupported."""
fig = uplt.figure()
Expand Down