Skip to content

Align x and y labels between axes #9652

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions doc/users/next_whats_new/2017-11-1_figure_align_labels.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
xlabels and ylabels can now be automatically aligned
----------------------------------------------------

Subplot axes ``ylabels`` can be misaligned horizontally if the tick labels
are very different widths. The same can happen to ``xlabels`` if the
ticklabels are rotated on one subplot (for instance). The new methods
on the `Figure` class: `Figure.align_xlabels` and `Figure.align_ylabels`
will now align these labels horizontally or vertically. If the user only
wants to align some axes, a list of axes can be passed. If no list is
passed, the algorithm looks at all the labels on the figure.

Only labels that have the same subplot locations are aligned. i.e. the
ylabels are aligned only if the subplots are in the same column of the
subplot layout.

Alignemnt is persistent and automatic after these are called.

A convenience wrapper `Figure.align_labels` calls both functions at once.

.. plot::

import matplotlib.gridspec as gridspec

fig = plt.figure(figsize=(5, 3), tight_layout=True)
gs = gridspec.GridSpec(2, 2)

ax = fig.add_subplot(gs[0,:])
ax.plot(np.arange(0, 1e6, 1000))
ax.set_ylabel('Test')
for i in range(2):
ax = fig.add_subplot(gs[1, i])
ax.set_ylabel('Booooo')
ax.set_xlabel('Hello')
if i == 0:
for tick in ax.get_xticklabels():
tick.set_rotation(45)
fig.align_labels()
37 changes: 37 additions & 0 deletions examples/subplots_axes_and_figures/align_labels_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
===============
Aligning Labels
===============

Aligning xlabel and ylabel using `Figure.align_xlabels` and
`Figure.align_ylabels`

`Figure.align_labels` wraps these two functions.

Note that the xlabel "XLabel1 1" would normally be much closer to the
x-axis, and "YLabel1 0" would be much closer to the y-axis of their
respective axes.
"""
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.gridspec as gridspec

fig = plt.figure(tight_layout=True)
gs = gridspec.GridSpec(2, 2)

ax = fig.add_subplot(gs[0, :])
ax.plot(np.arange(0, 1e6, 1000))
ax.set_ylabel('YLabel0')
ax.set_xlabel('XLabel0')

for i in range(2):
ax = fig.add_subplot(gs[1, i])
ax.plot(np.arange(1., 0., -0.1) * 2000., np.arange(1., 0., -0.1))
ax.set_ylabel('YLabel1 %d' % i)
ax.set_xlabel('XLabel1 %d' % i)
if i == 0:
for tick in ax.get_xticklabels():
tick.set_rotation(55)
fig.align_labels() # same as fig.align_xlabels(); fig.align_ylabels()

plt.show()
74 changes: 66 additions & 8 deletions lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -1113,10 +1113,12 @@ def get_tightbbox(self, renderer):
return

ticks_to_draw = self._update_ticks(renderer)
ticklabelBoxes, ticklabelBoxes2 = self._get_tick_bboxes(ticks_to_draw,
renderer)

self._update_label_position(ticklabelBoxes, ticklabelBoxes2)
self._update_label_position(renderer)

# go back to just this axis's tick labels
ticklabelBoxes, ticklabelBoxes2 = self._get_tick_bboxes(
ticks_to_draw, renderer)

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

self._update_label_position(ticklabelBoxes, ticklabelBoxes2)
self._update_label_position(renderer)

self.label.draw(renderer)

Expand Down Expand Up @@ -1670,7 +1672,16 @@ def set_ticks(self, ticks, minor=False):
self.set_major_locator(mticker.FixedLocator(ticks))
return self.get_major_ticks(len(ticks))

def _update_label_position(self, bboxes, bboxes2):
def _get_tick_boxes_siblings(self, xdir, renderer):
"""
Get the bounding boxes for this `.axis` and its siblings
as set by `.Figure.align_xlabels` or `.Figure.align_ylablels`.

By default it just gets bboxes for self.
"""
raise NotImplementedError('Derived must override')

def _update_label_position(self, renderer):
"""
Update the label position based on the bounding box enclosing
all the ticklabels and axis spine
Expand Down Expand Up @@ -1846,13 +1857,37 @@ def set_label_position(self, position):
self.label_position = position
self.stale = True

def _update_label_position(self, bboxes, bboxes2):
def _get_tick_boxes_siblings(self, renderer):
"""
Get the bounding boxes for this `.axis` and its siblings
as set by `.Figure.align_xlabels` or `.Figure.align_ylablels`.

By default it just gets bboxes for self.
"""
bboxes = []
bboxes2 = []
# get the Grouper that keeps track of x-label groups for this figure
grp = self.figure._align_xlabel_grp
# if we want to align labels from other axes:
for nn, axx in enumerate(grp.get_siblings(self.axes)):
ticks_to_draw = axx.xaxis._update_ticks(renderer)
tlb, tlb2 = axx.xaxis._get_tick_bboxes(ticks_to_draw, renderer)
bboxes.extend(tlb)
bboxes2.extend(tlb2)
return bboxes, bboxes2

def _update_label_position(self, renderer):
"""
Update the label position based on the bounding box enclosing
all the ticklabels and axis spine
"""
if not self._autolabelpos:
return

# get bounding boxes for this axis and any siblings
# that have been set by `fig.align_xlabels()`
bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)

x, y = self.label.get_position()
if self.label_position == 'bottom':
try:
Expand Down Expand Up @@ -2191,13 +2226,37 @@ def set_label_position(self, position):
self.label_position = position
self.stale = True

def _update_label_position(self, bboxes, bboxes2):
def _get_tick_boxes_siblings(self, renderer):
"""
Get the bounding boxes for this `.axis` and its siblings
as set by `.Figure.align_xlabels` or `.Figure.align_ylablels`.

By default it just gets bboxes for self.
"""
bboxes = []
bboxes2 = []
# get the Grouper that keeps track of y-label groups for this figure
grp = self.figure._align_ylabel_grp
# if we want to align labels from other axes:
for axx in grp.get_siblings(self.axes):
ticks_to_draw = axx.yaxis._update_ticks(renderer)
tlb, tlb2 = axx.yaxis._get_tick_bboxes(ticks_to_draw, renderer)
bboxes.extend(tlb)
bboxes2.extend(tlb2)
return bboxes, bboxes2

def _update_label_position(self, renderer):
"""
Update the label position based on the bounding box enclosing
all the ticklabels and axis spine
"""
if not self._autolabelpos:
return

# get bounding boxes for this axis and any siblings
# that have been set by `fig.align_ylabels()`
bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)

x, y = self.label.get_position()
if self.label_position == 'left':
try:
Expand All @@ -2209,7 +2268,6 @@ def _update_label_position(self, bboxes, bboxes2):
spinebbox = self.axes.bbox
bbox = mtransforms.Bbox.union(bboxes + [spinebbox])
left = bbox.x0

self.label.set_position(
(left - self.labelpad * self.figure.dpi / 72.0, y)
)
Expand Down
165 changes: 165 additions & 0 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,12 @@ def __init__(self,
self.clf()
self._cachedRenderer = None

# groupers to keep track of x and y labels we want to align.
# see self.align_xlabels and self.align_ylabels and
# axis._get_tick_boxes_siblings
self._align_xlabel_grp = cbook.Grouper()
self._align_ylabel_grp = cbook.Grouper()

@property
@cbook.deprecated("2.1", alternative="Figure.patch")
def figurePatch(self):
Expand Down Expand Up @@ -2084,6 +2090,165 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None,
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
self.subplots_adjust(**kwargs)

def align_xlabels(self, axs=None):
"""
Align the ylabels of subplots in the same subplot column if label
alignment is being done automatically (i.e. the label position is
not manually set).

Alignment persists for draw events after this is called.

If a label is on the bottom, it is aligned with labels on axes that
also have their label on the bottom and that have the same
bottom-most subplot row. If the label is on the top,
it is aligned with labels on axes with the same top-most row.

Parameters
----------
axs : list of `~matplotlib.axes.Axes` (None)
Optional list of (or ndarray) `~matplotlib.axes.Axes` to align
the xlabels. Default is to align all axes on the figure.

Note
----
This assumes that ``axs`` are from the same `~.GridSpec`, so that
their `~.SubplotSpec` positions correspond to figure positions.

See Also
--------
matplotlib.figure.Figure.align_ylabels

matplotlib.figure.Figure.align_labels

Example
-------
Example with rotated xtick labels::

fig, axs = plt.subplots(1, 2)
for tick in axs[0].get_xticklabels():
tick.set_rotation(55)
axs[0].set_xlabel('XLabel 0')
axs[1].set_xlabel('XLabel 1')
fig.align_xlabels()

"""

if axs is None:
axs = self.axes
axs = np.asarray(axs).ravel()
for ax in axs:
_log.debug(' Working on: %s', ax.get_xlabel())
ss = ax.get_subplotspec()
nrows, ncols, row0, row1, col0, col1 = ss.get_rows_columns()
labpo = ax.xaxis.get_label_position() # top or bottom

# loop through other axes, and search for label positions
# that are same as this one, and that share the appropriate
# row number.
# Add to a grouper associated with each axes of sibblings.
# This list is inspected in `axis.draw` by
# `axis._update_label_position`.
for axc in axs:
if axc.xaxis.get_label_position() == labpo:
ss = axc.get_subplotspec()
nrows, ncols, rowc0, rowc1, colc, col1 = \
ss.get_rows_columns()
if (labpo == 'bottom' and rowc1 == row1 or
labpo == 'top' and rowc0 == row0):
# grouper for groups of xlabels to align
self._align_xlabel_grp.join(ax, axc)

def align_ylabels(self, axs=None):
"""
Align the ylabels of subplots in the same subplot column if label
alignment is being done automatically (i.e. the label position is
not manually set).

Alignment persists for draw events after this is called.

If a label is on the left, it is aligned with labels on axes that
also have their label on the left and that have the same
left-most subplot column. If the label is on the right,
it is aligned with labels on axes with the same right-most column.

Parameters
----------
axs : list of `~matplotlib.axes.Axes` (None)
Optional list (or ndarray) of `~matplotlib.axes.Axes` to align
the ylabels. Default is to align all axes on the figure.

Note
----
This assumes that ``axs`` are from the same `~.GridSpec`, so that
their `~.SubplotSpec` positions correspond to figure positions.

See Also
--------
matplotlib.figure.Figure.align_xlabels

matplotlib.figure.Figure.align_labels

Example
-------
Example with large yticks labels::

fig, axs = plt.subplots(2, 1)
axs[0].plot(np.arange(0, 1000, 50))
axs[0].set_ylabel('YLabel 0')
axs[1].set_ylabel('YLabel 1')
fig.align_ylabels()

"""

if axs is None:
axs = self.axes
axs = np.asarray(axs).ravel()
for ax in axs:
_log.debug(' Working on: %s', ax.get_ylabel())
ss = ax.get_subplotspec()
nrows, ncols, row0, row1, col0, col1 = ss.get_rows_columns()
same = [ax]
labpo = ax.yaxis.get_label_position() # left or right
# loop through other axes, and search for label positions
# that are same as this one, and that share the appropriate
# column number.
# Add to a list associated with each axes of sibblings.
# This list is inspected in `axis.draw` by
# `axis._update_label_position`.
for axc in axs:
if axc != ax:
if axc.yaxis.get_label_position() == labpo:
ss = axc.get_subplotspec()
nrows, ncols, row0, row1, colc0, colc1 = \
ss.get_rows_columns()
if (labpo == 'left' and colc0 == col0 or
labpo == 'right' and colc1 == col1):
# grouper for groups of ylabels to align
self._align_ylabel_grp.join(ax, axc)

def align_labels(self, axs=None):
"""
Align the xlabels and ylabels of subplots with the same subplots
row or column (respectively) if label alignment is being
done automatically (i.e. the label position is not manually set).

Alignment persists for draw events after this is called.

Parameters
----------
axs : list of `~matplotlib.axes.Axes` (None)
Optional list (or ndarray) of `~matplotlib.axes.Axes` to
align the labels. Default is to align all axes on the figure.

See Also
--------
matplotlib.figure.Figure.align_xlabels

matplotlib.figure.Figure.align_ylabels
"""
self.align_xlabels(axs=axs)
self.align_ylabels(axs=axs)


def figaspect(arg):
"""
Expand Down
Loading