Skip to content

Commit 67a987e

Browse files
authored
Merge pull request #17222 from jklymak/fix-long-titles-layout
FIX: long titles x/ylabel layout
2 parents 1567063 + 474a90c commit 67a987e

File tree

8 files changed

+82
-23
lines changed

8 files changed

+82
-23
lines changed

doc/api/api_changes_3.3/behaviour.rst

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,6 @@ no longer accept the unsupported ``'best'`` location. Previously, invalid Axes
201201
locations would use ``'best'`` and invalid Figure locations would used ``'upper
202202
right'``.
203203

204-
205204
Passing Line2D's *drawstyle* together with *linestyle* is removed
206205
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
207206

@@ -214,3 +213,18 @@ Upper case color strings
214213

215214
Support for passing single-letter colors (one of "rgbcmykw") as UPPERCASE
216215
characters is removed; these colors are now case-sensitive (lowercase).
216+
217+
tight/constrained_layout no longer worry about titles that are too wide
218+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
219+
220+
*tight_layout* and *constrained_layout* shrink axes to accommodate
221+
"decorations" on the axes. However, if an xlabel or title is too long in the
222+
x direction, making the axes smaller in the x-direction doesn't help. The
223+
behavior of both has been changed to ignore the width of the title and
224+
xlabel and the height of the ylabel in the layout logic.
225+
226+
This also means there is a new keyword argument for `.axes.Axes.get_tightbbox`:
227+
``for_layout_only``, which defaults to *False*, but if *True* returns a
228+
bounding box using the rules above. `.axis.Axis.get_tightbbox` gets an
229+
``ignore_label`` keyword argument, which is *None* by default, but which can
230+
also be 'x' or 'y'.

lib/matplotlib/_constrained_layout.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,11 @@ def _make_layout_margins(ax, renderer, h_pad, w_pad):
258258
fig = ax.figure
259259
invTransFig = fig.transFigure.inverted().transform_bbox
260260
pos = ax.get_position(original=True)
261-
tightbbox = ax.get_tightbbox(renderer=renderer)
261+
try:
262+
tightbbox = ax.get_tightbbox(renderer=renderer, for_layout_only=True)
263+
except TypeError:
264+
tightbbox = ax.get_tightbbox(renderer=renderer)
265+
262266
if tightbbox is None:
263267
bbox = pos
264268
else:

lib/matplotlib/axes/_base.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4080,11 +4080,15 @@ def get_default_bbox_extra_artists(self):
40804080
for _axis in self._get_axis_list():
40814081
artists.remove(_axis)
40824082

4083+
artists.remove(self.title)
4084+
artists.remove(self._left_title)
4085+
artists.remove(self._right_title)
4086+
40834087
return [artist for artist in artists
40844088
if (artist.get_visible() and artist.get_in_layout())]
40854089

40864090
def get_tightbbox(self, renderer, call_axes_locator=True,
4087-
bbox_extra_artists=None):
4091+
bbox_extra_artists=None, *, for_layout_only=False):
40884092
"""
40894093
Return the tight bounding box of the axes, including axis and their
40904094
decorators (xlabel, title, etc).
@@ -4110,6 +4114,10 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
41104114
caller is only interested in the relative size of the tightbbox
41114115
compared to the axes bbox.
41124116
4117+
for_layout_only : default: False
4118+
The bounding box will *not* include the x-extent of the title and
4119+
the xlabel, or the y-extent of the ylabel.
4120+
41134121
Returns
41144122
-------
41154123
`.BboxBase`
@@ -4135,22 +4143,37 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
41354143
self.apply_aspect()
41364144

41374145
if self.axison:
4138-
bb_xaxis = self.xaxis.get_tightbbox(renderer)
4146+
igl = 'x' if for_layout_only else None
4147+
try:
4148+
bb_xaxis = self.xaxis.get_tightbbox(renderer, ignore_label=igl)
4149+
except TypeError:
4150+
# in case downstream library has redefined axis:
4151+
bb_xaxis = self.xaxis.get_tightbbox(renderer)
41394152
if bb_xaxis:
41404153
bb.append(bb_xaxis)
41414154

4142-
bb_yaxis = self.yaxis.get_tightbbox(renderer)
4155+
igl = 'y' if for_layout_only else None
4156+
try:
4157+
bb_yaxis = self.yaxis.get_tightbbox(renderer, ignore_label=igl)
4158+
except TypeError:
4159+
# in case downstream library has redefined axis:
4160+
bb_xaxis = self.yaxis.get_tightbbox(renderer)
41434161
if bb_yaxis:
41444162
bb.append(bb_yaxis)
4145-
41464163
self._update_title_position(renderer)
4147-
41484164
axbbox = self.get_window_extent(renderer)
41494165
bb.append(axbbox)
41504166

41514167
for title in [self.title, self._left_title, self._right_title]:
41524168
if title.get_visible():
4153-
bb.append(title.get_window_extent(renderer))
4169+
bt = title.get_window_extent(renderer)
4170+
if for_layout_only and bt.width > 0:
4171+
# make the title bbox 1 pixel wide so its width
4172+
# is not accounted for in bbox calculations in
4173+
# tight/constrained_layout
4174+
bt.x0 = (bt.x0 + bt.x1) / 2 - 0.5
4175+
bt.x1 = bt.x0 + 1.0
4176+
bb.append(bt)
41544177

41554178
bbox_artists = bbox_extra_artists
41564179
if bbox_artists is None:
@@ -4173,7 +4196,6 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
41734196
and 0 < bbox.width < np.inf
41744197
and 0 < bbox.height < np.inf):
41754198
bb.append(bbox)
4176-
41774199
return mtransforms.Bbox.union(
41784200
[b for b in bb if b.width != 0 or b.height != 0])
41794201

lib/matplotlib/axis.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,10 +1079,15 @@ def _get_tick_bboxes(self, ticks, renderer):
10791079
[tick.label2.get_window_extent(renderer)
10801080
for tick in ticks if tick.label2.get_visible()])
10811081

1082-
def get_tightbbox(self, renderer):
1082+
def get_tightbbox(self, renderer, *, ignore_label=None):
10831083
"""
10841084
Return a bounding box that encloses the axis. It only accounts
10851085
tick labels, axis label, and offsetText.
1086+
1087+
If ``ignore_label`` is 'x', then the width of the label is collapsed
1088+
to near zero. If 'y', then the height is collapsed to near zero. This
1089+
is for tight/constrained_layout to be able to ignore too-long labels
1090+
when doing their layout.
10861091
"""
10871092
if not self.get_visible():
10881093
return
@@ -1100,11 +1105,24 @@ def get_tightbbox(self, renderer):
11001105

11011106
bboxes = [
11021107
*(a.get_window_extent(renderer)
1103-
for a in [self.label, self.offsetText]
1108+
for a in [self.offsetText]
11041109
if a.get_visible()),
11051110
*ticklabelBoxes,
11061111
*ticklabelBoxes2,
11071112
]
1113+
# take care of label
1114+
if self.label.get_visible():
1115+
bb = self.label.get_window_extent(renderer)
1116+
# for constrained/tight_layout, we want to ignore the label's
1117+
# width because the adjustments they make can't be improved.
1118+
# this code collapses the relevant direction
1119+
if ignore_label == 'x' and bb.width > 0:
1120+
bb.x0 = (bb.x0 + bb.x1) / 2 - 0.5
1121+
bb.x1 = bb.x0 + 1.0
1122+
elif ignore_label == 'y' and bb.height > 0:
1123+
bb.y0 = (bb.y0 + bb.y1) / 2 - 0.5
1124+
bb.y1 = bb.y0 + 1.0
1125+
bboxes.append(bb)
11081126
bboxes = [b for b in bboxes
11091127
if 0 < b.width < np.inf and 0 < b.height < np.inf]
11101128
if bboxes:

lib/matplotlib/tests/test_constrainedlayout.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def test_constrained_layout10():
176176
@image_comparison(['constrained_layout11.png'])
177177
def test_constrained_layout11():
178178
"""Test for multiple nested gridspecs"""
179-
fig = plt.figure(constrained_layout=True, figsize=(10, 3))
179+
fig = plt.figure(constrained_layout=True, figsize=(13, 3))
180180
gs0 = gridspec.GridSpec(1, 2, figure=fig)
181181
gsl = gridspec.GridSpecFromSubplotSpec(1, 2, gs0[0])
182182
gsl0 = gridspec.GridSpecFromSubplotSpec(2, 2, gsl[1])

lib/matplotlib/tests/test_tightlayout.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -258,29 +258,23 @@ def test_empty_layout():
258258

259259
@pytest.mark.parametrize("label", ["xlabel", "ylabel"])
260260
def test_verybig_decorators(label):
261-
"""Test that warning emitted when xlabel/ylabel too big."""
261+
"""Test that no warning emitted when xlabel/ylabel too big."""
262262
fig, ax = plt.subplots(figsize=(3, 2))
263263
ax.set(**{label: 'a' * 100})
264-
with pytest.warns(UserWarning):
265-
fig.tight_layout()
266264

267265

268266
def test_big_decorators_horizontal():
269-
"""Test that warning emitted when xlabel too big."""
267+
"""Test that doesn't warn when xlabel too big."""
270268
fig, axs = plt.subplots(1, 2, figsize=(3, 2))
271269
axs[0].set_xlabel('a' * 30)
272270
axs[1].set_xlabel('b' * 30)
273-
with pytest.warns(UserWarning):
274-
fig.tight_layout()
275271

276272

277273
def test_big_decorators_vertical():
278-
"""Test that warning emitted when xlabel too big."""
274+
"""Test that doesn't warn when ylabel too big."""
279275
fig, axs = plt.subplots(2, 1, figsize=(3, 2))
280276
axs[0].set_ylabel('a' * 20)
281277
axs[1].set_ylabel('b' * 20)
282-
with pytest.warns(UserWarning):
283-
fig.tight_layout()
284278

285279

286280
def test_badsubplotgrid():

lib/matplotlib/tight_layout.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,15 @@ def auto_adjust_subplotpars(
7777
if all(not ax.get_visible() for ax in subplots):
7878
continue
7979

80-
tight_bbox_raw = Bbox.union([
81-
ax.get_tightbbox(renderer) for ax in subplots if ax.get_visible()])
80+
bb = []
81+
for ax in subplots:
82+
if ax.get_visible():
83+
try:
84+
bb += [ax.get_tightbbox(renderer, for_layout_only=True)]
85+
except TypeError:
86+
bb += [ax.get_tightbbox(renderer)]
87+
88+
tight_bbox_raw = Bbox.union(bb)
8289
tight_bbox = TransformedBbox(tight_bbox_raw,
8390
fig.transFigure.inverted())
8491

0 commit comments

Comments
 (0)