-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
""" | ||
=============== | ||
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() and fig.align_ylabels() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. replace |
||
|
||
plt.show() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'] | ||
|
@@ -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()) | ||
|
@@ -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) | ||
|
||
|
@@ -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`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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: | ||
|
@@ -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: | ||
|
@@ -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) | ||
) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :-) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Fixed? axs = np.asarray(axs).flatten().tolist() There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TO be honest, not sure why There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I see. Interesting. I never understood the difference between There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made even better by numpy's use of |
||
|
||
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I remember now... This code ( If #9498 gets merged, the y-position of titles will be different, and this code will become useful.
|
||
""" | ||
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
""" | ||
|
There was a problem hiding this comment.
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