-
-
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
Conversation
Ooh, this looks nice. I also approve of having this as a function rather than another kwarg option. |
Very nice indeed, something I've always wanted to have. One minor design point: I know that right now we tend to pass the renderer instance down the call stack, but AFAICT we really don't need to do that (we can just get it from |
Well frankly it’s an attempt to head off calls for this to be in a geometry manager like #9082. You can do it but if you can do it more simply then why not. @anntzer I’ll look at the renderer stuff. I was really just copying the _update_label_pos function that also passed the renderer so I assumed there were good reasons. I think there are cases where the figure canvas renderer is not defined so the wrapper in tight_layout is useful. |
As long as you keep renderer the last argument it remains possible to later deprecate it without too much pain so I don't mind keeping this discussion for later... |
@@ -4,10 +4,10 @@ | |||
=============== | |||
|
|||
Aligning xlabel and ylabel using | |||
:func:`~matplotlib.figure.align_xlabels` and | |||
:func:`~matplotlib.figure.align_xlabels` | |||
:func:`matplotlib.figure.align_xlabels` and |
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.
for future reference, you don't need the :func:
(or :meth:
or :class:
, etc.) here: the default role resolves any python object.
Subplot axes ``ylabels`` can not line up 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: ``fig.align_xlabels()`` and ``fig.align_ylabels()`` |
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.
rst syntax police here:
on the `Figure` class: `Figure.align_xlabels` and `Figure.align_ylabels`
or
on the `Figure` class: `~Figure.align_xlabels` and `~Figure.align_ylabels`
These are refering to things that have their own entries in the docs (the class and the method. The tilde is documented at http://www.sphinx-doc.org/en/stable/domains.html#role-py:obj and quite useful.
=============== | ||
|
||
Aligning xlabel and ylabel using | ||
:func:`matplotlib.figure.align_xlabels` and |
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.
see above re: syntax. the functions don't actually exist in the figure
module.
lib/matplotlib/axis.py
Outdated
def _get_tick_boxes_siblings(self, renderer): | ||
""" | ||
Get the bounding boxes for this axis and its sibblings | ||
as set by `fig.align_xlabels` or ``fig.align_ylables`. |
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.
markup
lib/matplotlib/figure.py
Outdated
Choose the left-most label to align to. Axes with the same subplot | ||
column are aligned. | ||
|
||
Parameters: |
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.
I'll channel the numpydoc police here.
Also seems normal to define align_xlabels first?
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('XLaleb1 %d' % i) |
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.
"XLabel1" rather than "XLaleb1"?
PS: I am not an expert of this portion of the code but FWIW, I am very excited by such a functionality landing in Matplotlib :)
dd8e05e
to
44b36df
Compare
squashed and rebased with master... |
@jklymak Can you please not include my name with the |
How annoying; my apologies! |
44b36df
to
86a6e32
Compare
86a6e32
to
05e8fc2
Compare
Rebase. Ping @anntzer, @dstansby, @afvincent who seemed interested; Pretty sure this is a non-breaking change. Happy to move to 3.0 if folks can't take a closer look next couple weeks. |
05e8fc2
to
ce91095
Compare
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 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 :-))
Aligning Labels | ||
=============== | ||
|
||
Aligning xlabel and ylabel using |
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
lib/matplotlib/axis.py
Outdated
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 comment
The reason will be displayed to describe this comment to others. Learn more.
markup, typo "sibblings" above
lib/matplotlib/figure.py
Outdated
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 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?
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.
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 ;-)
lib/matplotlib/figure.py
Outdated
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 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?
import numpy as np | ||
import warnings | ||
import pytest | ||
|
||
|
||
@image_comparison(baseline_images=['figure_align_labels']) | ||
def test_align_labels(): |
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.
also test for titles? not sure what's the status of that part
One thing that's perhaps not obvious is that calling align_foo has a persistent effect (in other words it does not just do the alignment "for this single draw"). Which is very reasonable, but should be mentioned somewhere. Otherwise, generally looks good (minor comments above). |
5d225e9
to
1194941
Compare
lib/matplotlib/axis.py
Outdated
@@ -1675,8 +1675,8 @@ def set_ticks(self, ticks, minor=False): | |||
|
|||
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`. | |||
Get the bounding boxes for this `.axis` and it's siblings |
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.
its :)
1194941
to
7928ecc
Compare
lib/matplotlib/figure.py
Outdated
for axx in same: | ||
_log.debug(' Same: %s', axx.xaxis.label) | ||
axx.xaxis._align_label_siblings += [ax.xaxis] | ||
if (((labpo == 'bottom') and (rowc1 == row1)) or |
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.
not a big deal but that's a lot of extra parentheses (only the outermost are needed)
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.
I guess somewhere in my head I knew that and
precedes or
, but I'm never sure, and I'm never sure from language to language, so...
Thanks @anntzer Removed title alignment for now. That depends on #9498, which is the only automatic way to have titles not be aligned; right now they are aligned by default. Of course if the user manually places one title, I assume they can align the other ones by hand because they know the position. I'll put the I did make a couple of small improvements (got rid of a loop), and tried to indicate what was going on in the comments (because even I was confused looking back at it). Added the fact that this alignment is persistent to the docs. Also that it only applies to automatically applied label positions. Manually-placed labels don't get aligned. Though thats a bit subtle - I think the way it is now, if a manually aligned label is the outlier, others will get aligned to it. If its an inlier, then the others will not be aligned to it, and the manual label will also stay an inlier. I actually think thats desirable, but could change that behaviour if others disagree. I think its a bit of an edge case; again, if someone is lining things up by hand, they probaby shouldn't be calling this helper. |
7928ecc
to
1447ed9
Compare
1447ed9
to
a8f72f3
Compare
Would it make sense to store the alignedness info on a Grouper (https://matplotlib.org/api/cbook_api.html#matplotlib.cbook.Grouper) stored on the Figure (one per axis), instead of in Axis._align_label_siblings? The main advantage would be that this way, if we ever figure out proper axes unsharing semantics (right now, shared axes cannot be unshared), hopefully the solution would also apply to unaligning (I don't think it's critical, just pointing out the possibility). |
The axes need not be shared if we align the labels (and indeed it would be strange for the labels to be misaligned in that case). OTOH, (I think) I can see the advantage of a pair of |
The point is not to reuse the same grouper instance, just to use the same data structure. |
lib/matplotlib/figure.py
Outdated
|
||
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 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.
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.
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
andnp.asarray
in there.
Fixed?
axs = np.asarray(axs).flatten().tolist()
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.
for ax in np.asarray(axs).flat:
is enough (that'll give you an 1D iterator that directly refers to the original data).
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.
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()
.
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.
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...
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.
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.
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.
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.
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.
"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 :)]
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.
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
23273c3
to
64b270b
Compare
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.
Very minor comments still being discussed but their resolution is not strictly required for this PR to go in.
64b270b
to
9f00a79
Compare
9f00a79
to
2d47811
Compare
PR Summary
Aligns the x and y labels automatically at draw time.
Example:
Before
After
PR Checklist