Skip to content

Commit ce91095

Browse files
committed
ENH: aligning x and y labels across axes
1 parent ad27247 commit ce91095

File tree

9 files changed

+3105
-10
lines changed

9 files changed

+3105
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
xlabels and ylabels can now be automatically aligned
2+
----------------------------------------------------
3+
4+
Subplot axes ``ylabels`` can be misaligned horizontally if the tick labels
5+
are very different widths. The same can happen to ``xlabels`` if the
6+
ticklabels are rotated on one subplot (for instance). The new methods
7+
on the `Figure` class: `Figure.align_xlabels` and `Figure.align_ylabels`
8+
will now align these labels horizontally or vertically. If the user only
9+
wants to align some axes, a list of axes can be passed. If no list is
10+
passed, the algorithm looks at all the labels on the figure.
11+
12+
Only labels that have the same subplot locations are aligned. i.e. the
13+
ylabels are aligned only if the subplots are in the same column of the
14+
subplot layout.
15+
16+
A convenience wrapper `Figure.align_labels` calls both functions at once.
17+
18+
.. plot::
19+
20+
import matplotlib.gridspec as gridspec
21+
22+
fig = plt.figure(figsize=(5, 3), tight_layout=True)
23+
gs = gridspec.GridSpec(2, 2)
24+
25+
ax = fig.add_subplot(gs[0,:])
26+
ax.plot(np.arange(0, 1e6, 1000))
27+
ax.set_ylabel('Test')
28+
for i in range(2):
29+
ax = fig.add_subplot(gs[1, i])
30+
ax.set_ylabel('Booooo')
31+
ax.set_xlabel('Hello')
32+
if i == 0:
33+
for tick in ax.get_xticklabels():
34+
tick.set_rotation(45)
35+
fig.align_labels()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
===============
3+
Aligning Labels
4+
===============
5+
6+
Aligning xlabel and ylabel using
7+
`Figure.align_xlabels` and
8+
`Figure.align_ylabels`
9+
10+
`Figure.align_labels` wraps these two functions.
11+
12+
Note that
13+
the xlabel "XLabel1 1" would normally be much closer to the x-axis, and
14+
"YLabel1 0" would be much closer to the y-axis of their respective axes.
15+
"""
16+
import matplotlib.pyplot as plt
17+
import numpy as np
18+
import matplotlib.gridspec as gridspec
19+
20+
fig = plt.figure(tight_layout=True)
21+
gs = gridspec.GridSpec(2, 2)
22+
23+
ax = fig.add_subplot(gs[0, :])
24+
ax.plot(np.arange(0, 1e6, 1000))
25+
ax.set_ylabel('YLabel0')
26+
ax.set_xlabel('XLabel0')
27+
28+
for i in range(2):
29+
ax = fig.add_subplot(gs[1, i])
30+
ax.plot(np.arange(1., 0., -0.1) * 2000., np.arange(1., 0., -0.1))
31+
ax.set_ylabel('YLabel1 %d' % i)
32+
ax.set_xlabel('XLabel1 %d' % i)
33+
if i == 0:
34+
for tick in ax.get_xticklabels():
35+
tick.set_rotation(55)
36+
fig.align_labels() # same as fig.align_xlabels() and fig.align_ylabels()
37+
38+
plt.show()

lib/matplotlib/axis.py

+37-8
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,7 @@ def __init__(self, axes, pickradius=15):
692692

693693
self._autolabelpos = True
694694
self._smart_bounds = False
695+
self._align_label_siblings = [self]
695696

696697
self.label = self._get_label()
697698
self.labelpad = rcParams['axes.labelpad']
@@ -1113,10 +1114,12 @@ def get_tightbbox(self, renderer):
11131114
return
11141115

11151116
ticks_to_draw = self._update_ticks(renderer)
1116-
ticklabelBoxes, ticklabelBoxes2 = self._get_tick_bboxes(ticks_to_draw,
1117-
renderer)
11181117

1119-
self._update_label_position(ticklabelBoxes, ticklabelBoxes2)
1118+
self._update_label_position(renderer)
1119+
1120+
# go back to just this axis's tick labels
1121+
ticklabelBoxes, ticklabelBoxes2 = self._get_tick_bboxes(
1122+
ticks_to_draw, renderer)
11201123

11211124
self._update_offset_text_position(ticklabelBoxes, ticklabelBoxes2)
11221125
self.offsetText.set_text(self.major.formatter.get_offset())
@@ -1167,7 +1170,7 @@ def draw(self, renderer, *args, **kwargs):
11671170
# *copy* of the axis label box because we don't wan't to scale
11681171
# the actual bbox
11691172

1170-
self._update_label_position(ticklabelBoxes, ticklabelBoxes2)
1173+
self._update_label_position(renderer)
11711174

11721175
self.label.draw(renderer)
11731176

@@ -1670,7 +1673,24 @@ def set_ticks(self, ticks, minor=False):
16701673
self.set_major_locator(mticker.FixedLocator(ticks))
16711674
return self.get_major_ticks(len(ticks))
16721675

1673-
def _update_label_position(self, bboxes, bboxes2):
1676+
def _get_tick_boxes_siblings(self, renderer):
1677+
"""
1678+
Get the bounding boxes for this axis and its sibblings
1679+
as set by `Figure.align_xlabels` or ``Figure.align_ylables`.
1680+
1681+
By default it just gets bboxes for self.
1682+
"""
1683+
bboxes = []
1684+
bboxes2 = []
1685+
# if we want to align labels from other axes:
1686+
for axx in self._align_label_siblings:
1687+
ticks_to_draw = axx._update_ticks(renderer)
1688+
tlb, tlb2 = axx._get_tick_bboxes(ticks_to_draw, renderer)
1689+
bboxes.extend(tlb)
1690+
bboxes2.extend(tlb2)
1691+
return bboxes, bboxes2
1692+
1693+
def _update_label_position(self, renderer):
16741694
"""
16751695
Update the label position based on the bounding box enclosing
16761696
all the ticklabels and axis spine
@@ -1846,13 +1866,18 @@ def set_label_position(self, position):
18461866
self.label_position = position
18471867
self.stale = True
18481868

1849-
def _update_label_position(self, bboxes, bboxes2):
1869+
def _update_label_position(self, renderer):
18501870
"""
18511871
Update the label position based on the bounding box enclosing
18521872
all the ticklabels and axis spine
18531873
"""
18541874
if not self._autolabelpos:
18551875
return
1876+
1877+
# get bounding boxes for this axis and any siblings
1878+
# that have been set by `fig.align_xlabels()`
1879+
bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer)
1880+
18561881
x, y = self.label.get_position()
18571882
if self.label_position == 'bottom':
18581883
try:
@@ -2191,13 +2216,18 @@ def set_label_position(self, position):
21912216
self.label_position = position
21922217
self.stale = True
21932218

2194-
def _update_label_position(self, bboxes, bboxes2):
2219+
def _update_label_position(self, renderer):
21952220
"""
21962221
Update the label position based on the bounding box enclosing
21972222
all the ticklabels and axis spine
21982223
"""
21992224
if not self._autolabelpos:
22002225
return
2226+
2227+
# get bounding boxes for this axis and any siblings
2228+
# that have been set by `fig.align_ylabels()`
2229+
bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer)
2230+
22012231
x, y = self.label.get_position()
22022232
if self.label_position == 'left':
22032233
try:
@@ -2209,7 +2239,6 @@ def _update_label_position(self, bboxes, bboxes2):
22092239
spinebbox = self.axes.bbox
22102240
bbox = mtransforms.Bbox.union(bboxes + [spinebbox])
22112241
left = bbox.x0
2212-
22132242
self.label.set_position(
22142243
(left - self.labelpad * self.figure.dpi / 72.0, y)
22152244
)

lib/matplotlib/figure.py

+210
Original file line numberDiff line numberDiff line change
@@ -2084,6 +2084,216 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None,
20842084
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
20852085
self.subplots_adjust(**kwargs)
20862086

2087+
def align_xlabels(self, axs=None, renderer=None):
2088+
"""
2089+
Align the xlabels of subplots in this figure.
2090+
2091+
If a label is on the bottom, it is aligned with labels on axes that
2092+
also have their label on the bottom and that have the same
2093+
bottom-most subplot row. If the label is on the top,
2094+
it is aligned with labels on axes with the same top-most row.
2095+
2096+
Parameters
2097+
----------
2098+
axs : list of `~matplotlib.axes.Axes` (None)
2099+
Optional list of `~matplotlib.axes.Axes` to align
2100+
the xlabels.
2101+
2102+
renderer : (None)
2103+
Optional renderer to do the adjustment on.
2104+
2105+
See Also
2106+
--------
2107+
matplotlib.figure.Figure.align_ylabels
2108+
2109+
matplotlib.figure.Figure.align_labels
2110+
2111+
Example
2112+
-------
2113+
Example with rotated xtick labels::
2114+
2115+
fig, axs = plt.subplots(1, 2)
2116+
for tick in axs[0].get_xticklabels():
2117+
tick.set_rotation(55)
2118+
axs[0].set_xlabel('XLabel 0')
2119+
axs[1].set_xlabel('XLabel 1')
2120+
fig.align_xlabels()
2121+
2122+
"""
2123+
2124+
from .tight_layout import get_renderer
2125+
2126+
if renderer is None:
2127+
renderer = get_renderer(self)
2128+
2129+
if axs is None:
2130+
axs = self.axes
2131+
2132+
axs = np.asarray(np.array(axs)).flatten().tolist()
2133+
2134+
for ax in axs:
2135+
_log.debug(' Working on: %s', ax.get_xlabel())
2136+
ss = ax.get_subplotspec()
2137+
nrows, ncols, row0, row1, col0, col1 = ss.get_rows_columns()
2138+
same = [ax]
2139+
labpo = ax.xaxis.get_label_position()
2140+
for axc in axs:
2141+
if axc.xaxis.get_label_position() == labpo:
2142+
ss = axc.get_subplotspec()
2143+
nrows, ncols, rowc0, rowc1, colc, col1 = \
2144+
ss.get_rows_columns()
2145+
if (labpo == 'bottom') and (rowc1 == row1):
2146+
same += [axc]
2147+
elif (labpo == 'top') and (rowc0 == row0):
2148+
same += [axc]
2149+
2150+
for axx in same:
2151+
_log.debug(' Same: %s', axx.xaxis.label)
2152+
axx.xaxis._align_label_siblings += [ax.xaxis]
2153+
2154+
def align_ylabels(self, axs=None, renderer=None):
2155+
"""
2156+
Align the ylabels of subplots in this figure.
2157+
2158+
If a label is on the left, it is aligned with labels on axes that
2159+
also have their label on the left and that have the same
2160+
left-most subplot column. If the label is on the right,
2161+
it is aligned with labels on axes with the same right-most column.
2162+
2163+
Parameters
2164+
----------
2165+
axs : list of `~matplotlib.axes.Axes` (None)
2166+
Optional list of `~matplotlib.axes.Axes` to align
2167+
the ylabels.
2168+
2169+
renderer : (None)
2170+
Optional renderer to do the adjustment on.
2171+
2172+
See Also
2173+
--------
2174+
matplotlib.figure.Figure.align_xlabels
2175+
2176+
matplotlib.figure.Figure.align_labels
2177+
2178+
Example
2179+
-------
2180+
Example with large yticks labels::
2181+
2182+
fig, axs = plt.subplots(2, 1)
2183+
axs[0].plot(np.arange(0, 1000, 50))
2184+
axs[0].set_ylabel('YLabel 0')
2185+
axs[1].set_ylabel('YLabel 1')
2186+
fig.align_ylabels()
2187+
2188+
"""
2189+
2190+
from .tight_layout import get_renderer
2191+
2192+
if renderer is None:
2193+
renderer = get_renderer(self)
2194+
2195+
if axs is None:
2196+
axs = self.axes
2197+
2198+
axs = np.asarray(np.array(axs)).flatten().tolist()
2199+
for ax in axs:
2200+
_log.debug(' Working on: %s', ax.get_ylabel())
2201+
ss = ax.get_subplotspec()
2202+
nrows, ncols, row0, row1, col0, col1 = ss.get_rows_columns()
2203+
same = [ax]
2204+
labpo = ax.yaxis.get_label_position()
2205+
for axc in axs:
2206+
if axc != ax:
2207+
if axc.yaxis.get_label_position() == labpo:
2208+
ss = axc.get_subplotspec()
2209+
nrows, ncols, row0, row1, colc0, colc1 = \
2210+
ss.get_rows_columns()
2211+
if (labpo == 'left') and (colc0 == col0):
2212+
same += [axc]
2213+
elif (labpo == 'right') and (colc1 == col1):
2214+
same += [axc]
2215+
for axx in same:
2216+
_log.debug(' Same: %s', axx.yaxis.label)
2217+
axx.yaxis._align_label_siblings += [ax.yaxis]
2218+
2219+
# place holder until #9498 is merged...
2220+
def align_titles(self, axs=None, renderer=None):
2221+
"""
2222+
Align the titles of subplots in this figure.
2223+
2224+
Parameters
2225+
----------
2226+
axs : list of `~matplotlib.axes.Axes` (None)
2227+
Optional list of axes to align the xlabels.
2228+
2229+
renderer : (None)
2230+
Optional renderer to do the adjustment on.
2231+
2232+
See Also
2233+
--------
2234+
matplotlib.figure.Figure.align_xlabels
2235+
2236+
matplotlib.figure.Figure.align_ylabels
2237+
"""
2238+
2239+
from .tight_layout import get_renderer
2240+
2241+
if renderer is None:
2242+
renderer = get_renderer(self)
2243+
2244+
if axs is None:
2245+
axs = self.axes
2246+
2247+
while len(axs):
2248+
ax = axs.pop()
2249+
ax._update_title_position(renderer)
2250+
same = [ax]
2251+
if ax._autolabelpos:
2252+
ss = ax.get_subplotspec()
2253+
nrows, ncols, row0, row1, col0, col1 = ss.get_rows_columns()
2254+
labpo = ax.xaxis.get_label_position()
2255+
for axc in axs:
2256+
axc._update_title_position(renderer)
2257+
if axc._autolabelpos:
2258+
ss = axc.get_subplotspec()
2259+
nrows, ncols, rowc0, rowc1, colc, col1 = \
2260+
ss.get_rows_columns()
2261+
if (rowc0 == row0):
2262+
same += [axc]
2263+
2264+
x0, y0 = ax.title.get_position()
2265+
for axx in same:
2266+
x, y = axx.title.get_position()
2267+
if y > y0:
2268+
ax.title.set_position(x0, y)
2269+
y0 = y
2270+
elif y0 > y:
2271+
axx.title.set_positions(x, y0)
2272+
2273+
def align_labels(self, axs=None, renderer=None):
2274+
"""
2275+
Align the xlabels and ylabels of subplots with the same subplots
2276+
row or column (respectively).
2277+
2278+
Parameters
2279+
----------
2280+
axs : list of `~matplotlib.axes.Axes` (None)
2281+
Optional list (or ndarray) of `~matplotlib.axes.Axes` to
2282+
align the labels.
2283+
2284+
renderer : (None)
2285+
Optional renderer to do the adjustment on.
2286+
2287+
See Also
2288+
--------
2289+
matplotlib.figure.Figure.align_xlabels
2290+
2291+
matplotlib.figure.Figure.align_ylabels
2292+
"""
2293+
self.align_xlabels(axs=axs, renderer=renderer)
2294+
self.align_ylabels(axs=axs, renderer=renderer)
2295+
# self.align_titles(axs=axs, renderer=renderer)
2296+
20872297

20882298
def figaspect(arg):
20892299
"""

0 commit comments

Comments
 (0)