Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3d3472c

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

File tree

9 files changed

+3090
-10
lines changed
Filter options

9 files changed

+3090
-10
lines changed
+37Lines 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()
+37Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/axis.py
+66-8Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/figure.py
+165Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.