Skip to content

Commit c50f062

Browse files
committed
FIX: long titles x/ylabel layout
1 parent cc5ddce commit c50f062

File tree

8 files changed

+60
-21
lines changed

8 files changed

+60
-21
lines changed

doc/api/api_changes_3.3/behaviour.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,19 @@ wx Timer interval
181181
~~~~~~~~~~~~~~~~~
182182
Setting the timer interval on a not-yet-started ``TimerWx`` won't start it
183183
anymore.
184+
185+
tight/constrained_layout no longer worry about titles that are too wide
186+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
187+
188+
*tight_layout* and *constrained_layout* shrink axes to accommodate
189+
"decorations" on the axes. However, if an xlabel or title is too long in the
190+
x direction, making the axes smaller in the x-direction doesn't help. The
191+
behavior of both has been changed to ignore the width of the title and
192+
xlabel and the height of the ylabel in the layout logic.
193+
194+
This also means there is a new keyword argument for `.axes.Axes.get_tightbbox`:
195+
``for_layout_only``, which defaults to *False*, but if *True* returns a
196+
bounding box using the rules above. `.axis.Axis.get_tightbbox` gets an
197+
``ignore_label`` keyword argument, which is *None* by default, but which can
198+
also be 'x' or 'y'. These API changes are public, but are meant to be
199+
mostly used internally.

lib/matplotlib/_constrained_layout.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ 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+
tightbbox = ax.get_tightbbox(renderer=renderer, for_layout_only=True)
262262
if tightbbox is None:
263263
bbox = pos
264264
else:

lib/matplotlib/axes/_base.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4087,11 +4087,15 @@ def get_default_bbox_extra_artists(self):
40874087
for _axis in self._get_axis_list():
40884088
artists.remove(_axis)
40894089

4090+
artists.remove(self.title)
4091+
artists.remove(self._left_title)
4092+
artists.remove(self._right_title)
4093+
40904094
return [artist for artist in artists
40914095
if (artist.get_visible() and artist.get_in_layout())]
40924096

40934097
def get_tightbbox(self, renderer, call_axes_locator=True,
4094-
bbox_extra_artists=None):
4098+
bbox_extra_artists=None, *, for_layout_only=False):
40954099
"""
40964100
Return the tight bounding box of the axes, including axis and their
40974101
decorators (xlabel, title, etc).
@@ -4117,6 +4121,10 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
41174121
caller is only interested in the relative size of the tightbbox
41184122
compared to the axes bbox.
41194123
4124+
for_layout_only : default: False
4125+
The bounding box will *not* include the x-extent of the title and
4126+
the xlabel, or the y-extent of the ylabel.
4127+
41204128
Returns
41214129
-------
41224130
`.BboxBase`
@@ -4142,22 +4150,26 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
41424150
self.apply_aspect()
41434151

41444152
if self.axison:
4145-
bb_xaxis = self.xaxis.get_tightbbox(renderer)
4153+
igl = 'x' if for_layout_only else None
4154+
bb_xaxis = self.xaxis.get_tightbbox(renderer, ignore_label=igl)
41464155
if bb_xaxis:
41474156
bb.append(bb_xaxis)
41484157

4149-
bb_yaxis = self.yaxis.get_tightbbox(renderer)
4158+
igl = 'y' if for_layout_only else None
4159+
bb_yaxis = self.yaxis.get_tightbbox(renderer, ignore_label=igl)
41504160
if bb_yaxis:
41514161
bb.append(bb_yaxis)
4152-
41534162
self._update_title_position(renderer)
4154-
41554163
axbbox = self.get_window_extent(renderer)
41564164
bb.append(axbbox)
41574165

41584166
for title in [self.title, self._left_title, self._right_title]:
41594167
if title.get_visible():
4160-
bb.append(title.get_window_extent(renderer))
4168+
bt = title.get_window_extent(renderer)
4169+
if for_layout_only and bt.width > 0:
4170+
bt.x0 = (bt.x0 + bt.x1) / 2 - 0.001
4171+
bt.x1 = bt.x0 + 0.002
4172+
bb.append(bt)
41614173

41624174
bbox_artists = bbox_extra_artists
41634175
if bbox_artists is None:
@@ -4180,7 +4192,6 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
41804192
and 0 < bbox.width < np.inf
41814193
and 0 < bbox.height < np.inf):
41824194
bb.append(bbox)
4183-
41844195
return mtransforms.Bbox.union(
41854196
[b for b in bb if b.width != 0 or b.height != 0])
41864197

lib/matplotlib/axis.py

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

1080-
def get_tightbbox(self, renderer):
1080+
def get_tightbbox(self, renderer, *, ignore_label=None):
10811081
"""
10821082
Return a bounding box that encloses the axis. It only accounts
10831083
tick labels, axis label, and offsetText.
1084+
1085+
If ``ignore_label`` is 'x', then the width of the label is collapsed
1086+
to near zero. If 'y', then the height is collapsed to near zero. This
1087+
is for tight/constrained_layout to be able to ignore too-long labels
1088+
when doing their layout.
10841089
"""
10851090
if not self.get_visible():
10861091
return
@@ -1098,11 +1103,23 @@ def get_tightbbox(self, renderer):
10981103

10991104
bboxes = [
11001105
*(a.get_window_extent(renderer)
1101-
for a in [self.label, self.offsetText]
1106+
for a in [self.offsetText]
11021107
if a.get_visible()),
11031108
*ticklabelBoxes,
11041109
*ticklabelBoxes2,
11051110
]
1111+
# take care of label
1112+
if self.label.get_visible():
1113+
bb = self.label.get_window_extent(renderer)
1114+
# for constrained/tight_layout, we want to ignore the label's
1115+
# width because the adjustments they make can't be improved....
1116+
if ignore_label == 'x' and bb.width > 0:
1117+
bb.x0 = (bb.x0 + bb.x1) / 2 - 0.01
1118+
bb.x1 = bb.x0 + 0.02
1119+
elif ignore_label == 'y' and bb.height > 0:
1120+
bb.y0 = (bb.y0 + bb.y1) / 2 - 0.01
1121+
bb.y1 = bb.y0 + 0.02
1122+
bboxes.append(bb)
11061123
bboxes = [b for b in bboxes
11071124
if 0 < b.width < np.inf and 0 < b.height < np.inf]
11081125
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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ def auto_adjust_subplotpars(
7878
continue
7979

8080
tight_bbox_raw = Bbox.union([
81-
ax.get_tightbbox(renderer) for ax in subplots if ax.get_visible()])
81+
ax.get_tightbbox(renderer, for_layout_only=True)
82+
for ax in subplots if ax.get_visible()])
8283
tight_bbox = TransformedBbox(tight_bbox_raw,
8384
fig.transFigure.inverted())
8485

0 commit comments

Comments
 (0)