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

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 1 commit
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
35 changes: 35 additions & 0 deletions 35 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,35 @@
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.

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()
38 changes: 38 additions & 0 deletions 38 examples/subplots_axes_and_figures/align_labels_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
===============
Aligning Labels
===============

Aligning xlabel and ylabel using
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something wrong with the wrapping her

`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() and fig.align_ylabels()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace and by (semicolon) (fig.align_xlabels() and fig.align_ylabels() is actually legal python but will only execute the first half :-))


plt.show()
45 changes: 37 additions & 8 deletions 45 lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ def __init__(self, axes, pickradius=15):

self._autolabelpos = True
self._smart_bounds = False
self._align_label_siblings = [self]

self.label = self._get_label()
self.labelpad = rcParams['axes.labelpad']
Expand Down Expand Up @@ -1113,10 +1114,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 +1170,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 +1673,24 @@ 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, renderer):
"""
Get the bounding boxes for this axis and its sibblings
as set by `Figure.align_xlabels` or ``Figure.align_ylables`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

markup, typo "sibblings" above


By default it just gets bboxes for self.
"""
bboxes = []
bboxes2 = []
# if we want to align labels from other axes:
for axx in self._align_label_siblings:
ticks_to_draw = axx._update_ticks(renderer)
tlb, tlb2 = axx._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
Expand Down Expand Up @@ -1846,13 +1866,18 @@ def set_label_position(self, position):
self.label_position = position
self.stale = True

def _update_label_position(self, 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)

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

def _update_label_position(self, 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)

x, y = self.label.get_position()
if self.label_position == 'left':
try:
Expand All @@ -2209,7 +2239,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
210 changes: 210 additions & 0 deletions 210 lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -2084,6 +2084,216 @@ 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, renderer=None):
"""
Align the xlabels of subplots in this figure.

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 `~matplotlib.axes.Axes` to align
the xlabels.

renderer : (None)
Optional renderer to do the adjustment on.

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()

"""

from .tight_layout import get_renderer

if renderer is None:
renderer = get_renderer(self)

if axs is None:
axs = self.axes

axs = np.asarray(np.array(axs)).flatten().tolist()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You really really wanted to make sure that you got a list heh :-)
Currently this will also accept Axes instances (rather than Axes in a single-element list) and multi-dimensional arrays. Was that intended? Just making sure, I would as usual prefer a simpler interface but am only -0 on being more flexible.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too much?

Ummm, I can't remember why I went so nuts on this.

  • I agree a singleton would be silly, though letting a list w/ one element would be fine.
  • It needs to be flattened, but before I can do that, it needs to be an np.array...
  • but, I have no idea why I have np.array and np.asarray in there.

Fixed?

axs = np.asarray(axs).flatten().tolist()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for ax in np.asarray(axs).flat: is enough (that'll give you an 1D iterator that directly refers to the original data).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't quite get that to work, and I want to save the list so I don't need the awkward construct twice. Using axs = np.asarray(axs).flatten().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TO be honest, not sure why flat doesn't work. It basically dropped one out of three axes on the figure in align_labels_demo.py so that third axis didn't get aligned. I don't really get why it doesn't work, but flatten() does, so...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed the fact that you had a nested loop also iterating over axs. In that case it is normal that the inner loop also advances the outer iterator and that won't work indeed.

As a side side note, using .ravel() instead of .flatten() is usually more efficient, because the latter always copies the data into a new array, whereas the former only does so when needed (i.e. when the data of the original array is not regularly spaced in memory). We're unlikely to ever have so many axes to run out of memory :-) but still a good habit IMO.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. Interesting. I never understood the difference between flatten() and ravel() and just use flatten because the word means something to me, whereas I'm not sure what ravel is supposed to mean.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"ravel" is apparently a synonym of "unravel" :) https://www.merriam-webster.com/dictionary/ravel?utm_campaign=sd&utm_medium=serp&utm_source=jsonld
[OT: reminds me of the day where my colleague explained to me that "being down for sthg" and "being up for sthg" meant essentially the same thing. English is sometimes weird :)]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made even better by numpy's use of unravel to mean the opposite of ravel: https://docs.scipy.org/doc/numpy-1.13.0/reference/generated/numpy.unravel_index.html


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()
same = [ax]
labpo = ax.xaxis.get_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):
same += [axc]
elif (labpo == 'top') and (rowc0 == row0):
same += [axc]

for axx in same:
_log.debug(' Same: %s', axx.xaxis.label)
axx.xaxis._align_label_siblings += [ax.xaxis]

def align_ylabels(self, axs=None, renderer=None):
"""
Align the ylabels of subplots in this figure.

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 of `~matplotlib.axes.Axes` to align
the ylabels.

renderer : (None)
Optional renderer to do the adjustment on.

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()

"""

from .tight_layout import get_renderer

if renderer is None:
renderer = get_renderer(self)

if axs is None:
axs = self.axes

axs = np.asarray(np.array(axs)).flatten().tolist()
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()
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):
same += [axc]
elif (labpo == 'right') and (colc1 == col1):
same += [axc]
for axx in same:
_log.debug(' Same: %s', axx.yaxis.label)
axx.yaxis._align_label_siblings += [ax.yaxis]

# place holder until #9498 is merged...
def align_titles(self, axs=None, renderer=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is independent of #9498 (on the feature side; the implementations may be overlapping...), or did I miss somthing?

Copy link
Member Author

@jklymak jklymak Jan 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I remember now...

This code (align_titles) is not needed now, and hence the call to it is commented out below, because titles are always in the same position relative to the top of the axis (if placed automatically). So, going through this function is needlessly expensive (though for practical purposes its not that expensive).

If #9498 gets merged, the y-position of titles will be different, and this code will become useful.

No test, because if the title is placed manaully this shouldn't run.... EDIT: Ooops, no I don't offer that feature ;-)

"""
Align the titles of subplots in this figure.

Parameters
----------
axs : list of `~matplotlib.axes.Axes` (None)
Optional list of axes to align the xlabels.

renderer : (None)
Optional renderer to do the adjustment on.

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

matplotlib.figure.Figure.align_ylabels
"""

from .tight_layout import get_renderer

if renderer is None:
renderer = get_renderer(self)

if axs is None:
axs = self.axes

while len(axs):
ax = axs.pop()
ax._update_title_position(renderer)
same = [ax]
if ax._autolabelpos:
ss = ax.get_subplotspec()
nrows, ncols, row0, row1, col0, col1 = ss.get_rows_columns()
labpo = ax.xaxis.get_label_position()
for axc in axs:
axc._update_title_position(renderer)
if axc._autolabelpos:
ss = axc.get_subplotspec()
nrows, ncols, rowc0, rowc1, colc, col1 = \
ss.get_rows_columns()
if (rowc0 == row0):
same += [axc]

x0, y0 = ax.title.get_position()
for axx in same:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks easier (and cheaper) to get the max value of y in a first pass and then apply it throughout?

x, y = axx.title.get_position()
if y > y0:
ax.title.set_position(x0, y)
y0 = y
elif y0 > y:
axx.title.set_positions(x, y0)

def align_labels(self, axs=None, renderer=None):
"""
Align the xlabels and ylabels of subplots with the same subplots
row or column (respectively).

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

renderer : (None)
Optional renderer to do the adjustment on.

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

matplotlib.figure.Figure.align_ylabels
"""
self.align_xlabels(axs=axs, renderer=renderer)
self.align_ylabels(axs=axs, renderer=renderer)
# self.align_titles(axs=axs, renderer=renderer)


def figaspect(arg):
"""
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.