Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3d3472c

Browse files
authored
Merge pull request #9652 from jklymak/alignxylabels
Align x and y labels between axes
2 parents 06a79d7 + 2d47811 commit 3d3472c

File tree

9 files changed

+3090
-10
lines changed

9 files changed

+3090
-10
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
Alignemnt is persistent and automatic after these are called.
17+
18+
A convenience wrapper `Figure.align_labels` calls both functions at once.
19+
20+
.. plot::
21+
22+
import matplotlib.gridspec as gridspec
23+
24+
fig = plt.figure(figsize=(5, 3), tight_layout=True)
25+
gs = gridspec.GridSpec(2, 2)
26+
27+
ax = fig.add_subplot(gs[0,:])
28+
ax.plot(np.arange(0, 1e6, 1000))
29+
ax.set_ylabel('Test')
30+
for i in range(2):
31+
ax = fig.add_subplot(gs[1, i])
32+
ax.set_ylabel('Booooo')
33+
ax.set_xlabel('Hello')
34+
if i == 0:
35+
for tick in ax.get_xticklabels():
36+
tick.set_rotation(45)
37+
fig.align_labels()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
===============
3+
Aligning Labels
4+
===============
5+
6+
Aligning xlabel and ylabel using `Figure.align_xlabels` and
7+
`Figure.align_ylabels`
8+
9+
`Figure.align_labels` wraps these two functions.
10+
11+
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.
14+
"""
15+
import matplotlib.pyplot as plt
16+
import numpy as np
17+
import matplotlib.gridspec as gridspec
18+
19+
fig = plt.figure(tight_layout=True)
20+
gs = gridspec.GridSpec(2, 2)
21+
22+
ax = fig.add_subplot(gs[0, :])
23+
ax.plot(np.arange(0, 1e6, 1000))
24+
ax.set_ylabel('YLabel0')
25+
ax.set_xlabel('XLabel0')
26+
27+
for i in range(2):
28+
ax = fig.add_subplot(gs[1, i])
29+
ax.plot(np.arange(1., 0., -0.1) * 2000., np.arange(1., 0., -0.1))
30+
ax.set_ylabel('YLabel1 %d' % i)
31+
ax.set_xlabel('XLabel1 %d' % i)
32+
if i == 0:
33+
for tick in ax.get_xticklabels():
34+
tick.set_rotation(55)
35+
fig.align_labels() # same as fig.align_xlabels(); fig.align_ylabels()
36+
37+
plt.show()

lib/matplotlib/axis.py

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,10 +1111,12 @@ def get_tightbbox(self, renderer):
11111111
return
11121112

11131113
ticks_to_draw = self._update_ticks(renderer)
1114-
ticklabelBoxes, ticklabelBoxes2 = self._get_tick_bboxes(ticks_to_draw,
1115-
renderer)
11161114

1117-
self._update_label_position(ticklabelBoxes, ticklabelBoxes2)
1115+
self._update_label_position(renderer)
1116+
1117+
# go back to just this axis's tick labels
1118+
ticklabelBoxes, ticklabelBoxes2 = self._get_tick_bboxes(
1119+
ticks_to_draw, renderer)
11181120

11191121
self._update_offset_text_position(ticklabelBoxes, ticklabelBoxes2)
11201122
self.offsetText.set_text(self.major.formatter.get_offset())
@@ -1165,7 +1167,7 @@ def draw(self, renderer, *args, **kwargs):
11651167
# *copy* of the axis label box because we don't wan't to scale
11661168
# the actual bbox
11671169

1168-
self._update_label_position(ticklabelBoxes, ticklabelBoxes2)
1170+
self._update_label_position(renderer)
11691171

11701172
self.label.draw(renderer)
11711173

@@ -1655,7 +1657,16 @@ def set_ticks(self, ticks, minor=False):
16551657
self.set_major_locator(mticker.FixedLocator(ticks))
16561658
return self.get_major_ticks(len(ticks))
16571659

1658-
def _update_label_position(self, bboxes, bboxes2):
1660+
def _get_tick_boxes_siblings(self, xdir, renderer):
1661+
"""
1662+
Get the bounding boxes for this `.axis` and its siblings
1663+
as set by `.Figure.align_xlabels` or `.Figure.align_ylablels`.
1664+
1665+
By default it just gets bboxes for self.
1666+
"""
1667+
raise NotImplementedError('Derived must override')
1668+
1669+
def _update_label_position(self, renderer):
16591670
"""
16601671
Update the label position based on the bounding box enclosing
16611672
all the ticklabels and axis spine
@@ -1831,13 +1842,37 @@ def set_label_position(self, position):
18311842
self.label_position = position
18321843
self.stale = True
18331844

1834-
def _update_label_position(self, bboxes, bboxes2):
1845+
def _get_tick_boxes_siblings(self, renderer):
1846+
"""
1847+
Get the bounding boxes for this `.axis` and its siblings
1848+
as set by `.Figure.align_xlabels` or `.Figure.align_ylablels`.
1849+
1850+
By default it just gets bboxes for self.
1851+
"""
1852+
bboxes = []
1853+
bboxes2 = []
1854+
# get the Grouper that keeps track of x-label groups for this figure
1855+
grp = self.figure._align_xlabel_grp
1856+
# if we want to align labels from other axes:
1857+
for nn, axx in enumerate(grp.get_siblings(self.axes)):
1858+
ticks_to_draw = axx.xaxis._update_ticks(renderer)
1859+
tlb, tlb2 = axx.xaxis._get_tick_bboxes(ticks_to_draw, renderer)
1860+
bboxes.extend(tlb)
1861+
bboxes2.extend(tlb2)
1862+
return bboxes, bboxes2
1863+
1864+
def _update_label_position(self, renderer):
18351865
"""
18361866
Update the label position based on the bounding box enclosing
18371867
all the ticklabels and axis spine
18381868
"""
18391869
if not self._autolabelpos:
18401870
return
1871+
1872+
# get bounding boxes for this axis and any siblings
1873+
# that have been set by `fig.align_xlabels()`
1874+
bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
1875+
18411876
x, y = self.label.get_position()
18421877
if self.label_position == 'bottom':
18431878
try:
@@ -2176,13 +2211,37 @@ def set_label_position(self, position):
21762211
self.label_position = position
21772212
self.stale = True
21782213

2179-
def _update_label_position(self, bboxes, bboxes2):
2214+
def _get_tick_boxes_siblings(self, renderer):
2215+
"""
2216+
Get the bounding boxes for this `.axis` and its siblings
2217+
as set by `.Figure.align_xlabels` or `.Figure.align_ylablels`.
2218+
2219+
By default it just gets bboxes for self.
2220+
"""
2221+
bboxes = []
2222+
bboxes2 = []
2223+
# get the Grouper that keeps track of y-label groups for this figure
2224+
grp = self.figure._align_ylabel_grp
2225+
# if we want to align labels from other axes:
2226+
for axx in grp.get_siblings(self.axes):
2227+
ticks_to_draw = axx.yaxis._update_ticks(renderer)
2228+
tlb, tlb2 = axx.yaxis._get_tick_bboxes(ticks_to_draw, renderer)
2229+
bboxes.extend(tlb)
2230+
bboxes2.extend(tlb2)
2231+
return bboxes, bboxes2
2232+
2233+
def _update_label_position(self, renderer):
21802234
"""
21812235
Update the label position based on the bounding box enclosing
21822236
all the ticklabels and axis spine
21832237
"""
21842238
if not self._autolabelpos:
21852239
return
2240+
2241+
# get bounding boxes for this axis and any siblings
2242+
# that have been set by `fig.align_ylabels()`
2243+
bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
2244+
21862245
x, y = self.label.get_position()
21872246
if self.label_position == 'left':
21882247
try:
@@ -2194,7 +2253,6 @@ def _update_label_position(self, bboxes, bboxes2):
21942253
spinebbox = self.axes.bbox
21952254
bbox = mtransforms.Bbox.union(bboxes + [spinebbox])
21962255
left = bbox.x0
2197-
21982256
self.label.set_position(
21992257
(left - self.labelpad * self.figure.dpi / 72.0, y)
22002258
)

lib/matplotlib/figure.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,12 @@ def __init__(self,
380380
self.clf()
381381
self._cachedRenderer = None
382382

383+
# groupers to keep track of x and y labels we want to align.
384+
# see self.align_xlabels and self.align_ylabels and
385+
# axis._get_tick_boxes_siblings
386+
self._align_xlabel_grp = cbook.Grouper()
387+
self._align_ylabel_grp = cbook.Grouper()
388+
383389
@property
384390
@cbook.deprecated("2.1", alternative="Figure.patch")
385391
def figurePatch(self):
@@ -2084,6 +2090,165 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None,
20842090
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
20852091
self.subplots_adjust(**kwargs)
20862092

2093+
def align_xlabels(self, axs=None):
2094+
"""
2095+
Align the ylabels of subplots in the same subplot column if label
2096+
alignment is being done automatically (i.e. the label position is
2097+
not manually set).
2098+
2099+
Alignment persists for draw events after this is called.
2100+
2101+
If a label is on the bottom, it is aligned with labels on axes that
2102+
also have their label on the bottom and that have the same
2103+
bottom-most subplot row. If the label is on the top,
2104+
it is aligned with labels on axes with the same top-most row.
2105+
2106+
Parameters
2107+
----------
2108+
axs : list of `~matplotlib.axes.Axes` (None)
2109+
Optional list of (or ndarray) `~matplotlib.axes.Axes` to align
2110+
the xlabels. Default is to align all axes on the figure.
2111+
2112+
Note
2113+
----
2114+
This assumes that ``axs`` are from the same `~.GridSpec`, so that
2115+
their `~.SubplotSpec` positions correspond to figure positions.
2116+
2117+
See Also
2118+
--------
2119+
matplotlib.figure.Figure.align_ylabels
2120+
2121+
matplotlib.figure.Figure.align_labels
2122+
2123+
Example
2124+
-------
2125+
Example with rotated xtick labels::
2126+
2127+
fig, axs = plt.subplots(1, 2)
2128+
for tick in axs[0].get_xticklabels():
2129+
tick.set_rotation(55)
2130+
axs[0].set_xlabel('XLabel 0')
2131+
axs[1].set_xlabel('XLabel 1')
2132+
fig.align_xlabels()
2133+
2134+
"""
2135+
2136+
if axs is None:
2137+
axs = self.axes
2138+
axs = np.asarray(axs).ravel()
2139+
for ax in axs:
2140+
_log.debug(' Working on: %s', ax.get_xlabel())
2141+
ss = ax.get_subplotspec()
2142+
nrows, ncols, row0, row1, col0, col1 = ss.get_rows_columns()
2143+
labpo = ax.xaxis.get_label_position() # top or bottom
2144+
2145+
# loop through other axes, and search for label positions
2146+
# that are same as this one, and that share the appropriate
2147+
# row number.
2148+
# Add to a grouper associated with each axes of sibblings.
2149+
# This list is inspected in `axis.draw` by
2150+
# `axis._update_label_position`.
2151+
for axc in axs:
2152+
if axc.xaxis.get_label_position() == labpo:
2153+
ss = axc.get_subplotspec()
2154+
nrows, ncols, rowc0, rowc1, colc, col1 = \
2155+
ss.get_rows_columns()
2156+
if (labpo == 'bottom' and rowc1 == row1 or
2157+
labpo == 'top' and rowc0 == row0):
2158+
# grouper for groups of xlabels to align
2159+
self._align_xlabel_grp.join(ax, axc)
2160+
2161+
def align_ylabels(self, axs=None):
2162+
"""
2163+
Align the ylabels of subplots in the same subplot column if label
2164+
alignment is being done automatically (i.e. the label position is
2165+
not manually set).
2166+
2167+
Alignment persists for draw events after this is called.
2168+
2169+
If a label is on the left, it is aligned with labels on axes that
2170+
also have their label on the left and that have the same
2171+
left-most subplot column. If the label is on the right,
2172+
it is aligned with labels on axes with the same right-most column.
2173+
2174+
Parameters
2175+
----------
2176+
axs : list of `~matplotlib.axes.Axes` (None)
2177+
Optional list (or ndarray) of `~matplotlib.axes.Axes` to align
2178+
the ylabels. Default is to align all axes on the figure.
2179+
2180+
Note
2181+
----
2182+
This assumes that ``axs`` are from the same `~.GridSpec`, so that
2183+
their `~.SubplotSpec` positions correspond to figure positions.
2184+
2185+
See Also
2186+
--------
2187+
matplotlib.figure.Figure.align_xlabels
2188+
2189+
matplotlib.figure.Figure.align_labels
2190+
2191+
Example
2192+
-------
2193+
Example with large yticks labels::
2194+
2195+
fig, axs = plt.subplots(2, 1)
2196+
axs[0].plot(np.arange(0, 1000, 50))
2197+
axs[0].set_ylabel('YLabel 0')
2198+
axs[1].set_ylabel('YLabel 1')
2199+
fig.align_ylabels()
2200+
2201+
"""
2202+
2203+
if axs is None:
2204+
axs = self.axes
2205+
axs = np.asarray(axs).ravel()
2206+
for ax in axs:
2207+
_log.debug(' Working on: %s', ax.get_ylabel())
2208+
ss = ax.get_subplotspec()
2209+
nrows, ncols, row0, row1, col0, col1 = ss.get_rows_columns()
2210+
same = [ax]
2211+
labpo = ax.yaxis.get_label_position() # left or right
2212+
# loop through other axes, and search for label positions
2213+
# that are same as this one, and that share the appropriate
2214+
# column number.
2215+
# Add to a list associated with each axes of sibblings.
2216+
# This list is inspected in `axis.draw` by
2217+
# `axis._update_label_position`.
2218+
for axc in axs:
2219+
if axc != ax:
2220+
if axc.yaxis.get_label_position() == labpo:
2221+
ss = axc.get_subplotspec()
2222+
nrows, ncols, row0, row1, colc0, colc1 = \
2223+
ss.get_rows_columns()
2224+
if (labpo == 'left' and colc0 == col0 or
2225+
labpo == 'right' and colc1 == col1):
2226+
# grouper for groups of ylabels to align
2227+
self._align_ylabel_grp.join(ax, axc)
2228+
2229+
def align_labels(self, axs=None):
2230+
"""
2231+
Align the xlabels and ylabels of subplots with the same subplots
2232+
row or column (respectively) if label alignment is being
2233+
done automatically (i.e. the label position is not manually set).
2234+
2235+
Alignment persists for draw events after this is called.
2236+
2237+
Parameters
2238+
----------
2239+
axs : list of `~matplotlib.axes.Axes` (None)
2240+
Optional list (or ndarray) of `~matplotlib.axes.Axes` to
2241+
align the labels. Default is to align all axes on the figure.
2242+
2243+
See Also
2244+
--------
2245+
matplotlib.figure.Figure.align_xlabels
2246+
2247+
matplotlib.figure.Figure.align_ylabels
2248+
"""
2249+
self.align_xlabels(axs=axs)
2250+
self.align_ylabels(axs=axs)
2251+
20872252

20882253
def figaspect(arg):
20892254
"""

0 commit comments

Comments
 (0)