Skip to content

Commit 23ea909

Browse files
authored
Merge pull request matplotlib#27952 from AnsonTran/align-titles
ENH: Align titles
2 parents d7f37d2 + 0be9cca commit 23ea909

File tree

9 files changed

+124
-24
lines changed

9 files changed

+124
-24
lines changed

doc/api/figure_api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ Annotating
7171
Figure.align_labels
7272
Figure.align_xlabels
7373
Figure.align_ylabels
74+
Figure.align_titles
7475
Figure.autofmt_xdate
7576

7677

@@ -264,6 +265,7 @@ Annotating
264265
SubFigure.align_labels
265266
SubFigure.align_xlabels
266267
SubFigure.align_ylabels
268+
SubFigure.align_titles
267269

268270
Adding and getting Artists
269271
--------------------------
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
subplot titles can now be automatically aligned
2+
-----------------------------------------------
3+
4+
Subplot axes titles can be misaligned vertically if tick labels or
5+
xlabels are placed at the top of one subplot. The new method on the
6+
`.Figure` class: `.Figure.align_titles` will now align the titles
7+
vertically.
Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,43 @@
11
"""
2-
===============
3-
Aligning Labels
4-
===============
2+
==========================
3+
Aligning Labels and Titles
4+
==========================
55
6-
Aligning xlabel and ylabel using `.Figure.align_xlabels` and
7-
`.Figure.align_ylabels`
6+
Aligning xlabel, ylabel, and title using `.Figure.align_xlabels`,
7+
`.Figure.align_ylabels`, and `.Figure.align_titles`.
88
9-
`.Figure.align_labels` wraps these two functions.
9+
`.Figure.align_labels` wraps the x and y label functions.
1010
1111
Note that the xlabel "XLabel1 1" would normally be much closer to the
12-
x-axis, and "YLabel1 0" would be much closer to the y-axis of their
13-
respective axes.
12+
x-axis, "YLabel0 0" would be much closer to the y-axis, and title
13+
"Title0 0" would be much closer to the top of their respective axes.
1414
"""
1515
import matplotlib.pyplot as plt
1616
import numpy as np
1717

18-
import matplotlib.gridspec as gridspec
18+
fig, axs = plt.subplots(2, 2, layout='constrained')
1919

20-
fig = plt.figure(tight_layout=True)
21-
gs = gridspec.GridSpec(2, 2)
22-
23-
ax = fig.add_subplot(gs[0, :])
20+
ax = axs[0][0]
2421
ax.plot(np.arange(0, 1e6, 1000))
25-
ax.set_ylabel('YLabel0')
26-
ax.set_xlabel('XLabel0')
22+
ax.set_title('Title0 0')
23+
ax.set_ylabel('YLabel0 0')
24+
25+
ax = axs[0][1]
26+
ax.plot(np.arange(1., 0., -0.1) * 2000., np.arange(1., 0., -0.1))
27+
ax.set_title('Title0 1')
28+
ax.xaxis.tick_top()
29+
ax.tick_params(axis='x', rotation=55)
30+
2731

2832
for i in range(2):
29-
ax = fig.add_subplot(gs[1, i])
33+
ax = axs[1][i]
3034
ax.plot(np.arange(1., 0., -0.1) * 2000., np.arange(1., 0., -0.1))
3135
ax.set_ylabel('YLabel1 %d' % i)
3236
ax.set_xlabel('XLabel1 %d' % i)
3337
if i == 0:
3438
ax.tick_params(axis='x', rotation=55)
39+
3540
fig.align_labels() # same as fig.align_xlabels(); fig.align_ylabels()
41+
fig.align_titles()
3642

3743
plt.show()

lib/matplotlib/axes/_base.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2985,8 +2985,13 @@ def _update_title_position(self, renderer):
29852985

29862986
titles = (self.title, self._left_title, self._right_title)
29872987

2988-
# Need to check all our twins too, and all the children as well.
2989-
axs = self._twinned_axes.get_siblings(self) + self.child_axes
2988+
# Need to check all our twins too, aligned axes, and all the children
2989+
# as well.
2990+
axs = set()
2991+
axs.update(self.child_axes)
2992+
axs.update(self._twinned_axes.get_siblings(self))
2993+
axs.update(self.figure._align_label_groups['title'].get_siblings(self))
2994+
29902995
for ax in self.child_axes: # Child positions must be updated first.
29912996
locator = ax.get_axes_locator()
29922997
ax.apply_aspect(locator(self, renderer) if locator else None)

lib/matplotlib/figure.py

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,14 @@ def __init__(self, **kwargs):
132132
self._supxlabel = None
133133
self._supylabel = None
134134

135-
# groupers to keep track of x and y labels we want to align.
136-
# see self.align_xlabels and self.align_ylabels and
137-
# axis._get_tick_boxes_siblings
138-
self._align_label_groups = {"x": cbook.Grouper(), "y": cbook.Grouper()}
135+
# groupers to keep track of x, y labels and title we want to align.
136+
# see self.align_xlabels, self.align_ylabels,
137+
# self.align_titles, and axis._get_tick_boxes_siblings
138+
self._align_label_groups = {
139+
"x": cbook.Grouper(),
140+
"y": cbook.Grouper(),
141+
"title": cbook.Grouper()
142+
}
139143

140144
self._localaxes = [] # track all Axes
141145
self.artists = []
@@ -1293,7 +1297,7 @@ def subplots_adjust(self, left=None, bottom=None, right=None, top=None,
12931297

12941298
def align_xlabels(self, axs=None):
12951299
"""
1296-
Align the xlabels of subplots in the same subplot column if label
1300+
Align the xlabels of subplots in the same subplot row if label
12971301
alignment is being done automatically (i.e. the label position is
12981302
not manually set).
12991303
@@ -1314,6 +1318,7 @@ def align_xlabels(self, axs=None):
13141318
See Also
13151319
--------
13161320
matplotlib.figure.Figure.align_ylabels
1321+
matplotlib.figure.Figure.align_titles
13171322
matplotlib.figure.Figure.align_labels
13181323
13191324
Notes
@@ -1375,6 +1380,7 @@ def align_ylabels(self, axs=None):
13751380
See Also
13761381
--------
13771382
matplotlib.figure.Figure.align_xlabels
1383+
matplotlib.figure.Figure.align_titles
13781384
matplotlib.figure.Figure.align_labels
13791385
13801386
Notes
@@ -1412,6 +1418,53 @@ def align_ylabels(self, axs=None):
14121418
# grouper for groups of ylabels to align
14131419
self._align_label_groups['y'].join(ax, axc)
14141420

1421+
def align_titles(self, axs=None):
1422+
"""
1423+
Align the titles of subplots in the same subplot row if title
1424+
alignment is being done automatically (i.e. the title position is
1425+
not manually set).
1426+
1427+
Alignment persists for draw events after this is called.
1428+
1429+
Parameters
1430+
----------
1431+
axs : list of `~matplotlib.axes.Axes`
1432+
Optional list of (or ndarray) `~matplotlib.axes.Axes`
1433+
to align the titles.
1434+
Default is to align all Axes on the figure.
1435+
1436+
See Also
1437+
--------
1438+
matplotlib.figure.Figure.align_xlabels
1439+
matplotlib.figure.Figure.align_ylabels
1440+
matplotlib.figure.Figure.align_labels
1441+
1442+
Notes
1443+
-----
1444+
This assumes that ``axs`` are from the same `.GridSpec`, so that
1445+
their `.SubplotSpec` positions correspond to figure positions.
1446+
1447+
Examples
1448+
--------
1449+
Example with titles::
1450+
1451+
fig, axs = plt.subplots(1, 2)
1452+
axs[0].set_aspect('equal')
1453+
axs[0].set_title('Title 0')
1454+
axs[1].set_title('Title 1')
1455+
fig.align_titles()
1456+
"""
1457+
if axs is None:
1458+
axs = self.axes
1459+
axs = [ax for ax in np.ravel(axs) if ax.get_subplotspec() is not None]
1460+
for ax in axs:
1461+
_log.debug(' Working on: %s', ax.get_title())
1462+
rowspan = ax.get_subplotspec().rowspan
1463+
for axc in axs:
1464+
rowspanc = axc.get_subplotspec().rowspan
1465+
if (rowspan.start == rowspanc.start):
1466+
self._align_label_groups['title'].join(ax, axc)
1467+
14151468
def align_labels(self, axs=None):
14161469
"""
14171470
Align the xlabels and ylabels of subplots with the same subplots
@@ -1430,8 +1483,8 @@ def align_labels(self, axs=None):
14301483
See Also
14311484
--------
14321485
matplotlib.figure.Figure.align_xlabels
1433-
14341486
matplotlib.figure.Figure.align_ylabels
1487+
matplotlib.figure.Figure.align_titles
14351488
"""
14361489
self.align_xlabels(axs=axs)
14371490
self.align_ylabels(axs=axs)

lib/matplotlib/figure.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ class FigureBase(Artist):
161161
) -> None: ...
162162
def align_xlabels(self, axs: Iterable[Axes] | None = ...) -> None: ...
163163
def align_ylabels(self, axs: Iterable[Axes] | None = ...) -> None: ...
164+
def align_titles(self, axs: Iterable[Axes] | None = ...) -> None: ...
164165
def align_labels(self, axs: Iterable[Axes] | None = ...) -> None: ...
165166
def add_gridspec(self, nrows: int = ..., ncols: int = ..., **kwargs) -> GridSpec: ...
166167
@overload
Loading
Loading

lib/matplotlib/tests/test_figure.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,32 @@ def test_align_labels():
6666
fig.align_labels()
6767

6868

69+
@image_comparison(['figure_align_titles_tight.png',
70+
'figure_align_titles_constrained.png'],
71+
tol=0 if platform.machine() == 'x86_64' else 0.022,
72+
style='mpl20')
73+
def test_align_titles():
74+
for layout in ['tight', 'constrained']:
75+
fig, axs = plt.subplots(1, 2, layout=layout, width_ratios=[2, 1])
76+
77+
ax = axs[0]
78+
ax.plot(np.arange(0, 1e6, 1000))
79+
ax.set_title('Title0 left', loc='left')
80+
ax.set_title('Title0 center', loc='center')
81+
ax.set_title('Title0 right', loc='right')
82+
83+
ax = axs[1]
84+
ax.plot(np.arange(0, 1e4, 100))
85+
ax.set_title('Title1')
86+
ax.set_xlabel('Xlabel0')
87+
ax.xaxis.set_label_position("top")
88+
ax.xaxis.tick_top()
89+
for tick in ax.get_xticklabels():
90+
tick.set_rotation(90)
91+
92+
fig.align_titles()
93+
94+
6995
def test_align_labels_stray_axes():
7096
fig, axs = plt.subplots(2, 2)
7197
for nn, ax in enumerate(axs.flat):

0 commit comments

Comments
 (0)