-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
ENH: add figure.legend; outside kwarg for better layout outside subplots #13072
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
I‘m positive on the idea. But don‘t have the time to look into this in more detail for a few days. So just a quick API thought for now: Is it reasonable to have two separate functions fig.legend and fig.legend_outside for the user? Internally, this is quite different, but it might be worth considering having something like fig.legend(loc=„outside upper right“) and branch internally. That of course depends on how similar the API of both versions would be. (Sorry for the bad formatting - phone keyboard) |
The API can be identical from my point of view, except for specifying "outside" somehow. Its not currently, but thats just because I think we are trying to get away from implicit APIs, and parsing the arguments so that the inputs work at different levels is a bit of a pain, but it can be dealt with. So,
I'd be fine w/ 3, but I'm not sure what is more discoverable for users, 1 or 3. Other opinions? Note that 3 would also mean adding a |
I like 3. I'd be more likely to discover it by looking at the docstring for figure.legend |
+1.0 for option 3, -0.01 for option 2, -1.0 for option 1. |
+1 for option 3. |
... option 3 it is. |
Actually looking at it again... "upper right" can mean one of two things: in the upper-right corner, to the right of the axes, or in the upper-right corner, above the axis. |
Well since we now have the outside kwarg perhaps it could take a string value outside=“vertical”? |
How would it combine with "east"(/"left")? |
It’d be True if any string specified (or True) and the string would be checked only for the ambiguous corners (default to the way I’m doing it now, which I think is 95% of what people want) |
b419198
to
8e21c15
Compare
lib/matplotlib/figure.py
Outdated
be changed by specifying a string | ||
``outside="vertical", loc="upper right"``. | ||
|
||
axs : sequence of `~.axes.Axes` |
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.
does this need to be axs
instead of axes
?
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.
axs would be standard I think (axes is not great as it suggests a single Axes...)
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 colorbar
uses ax
. Since this is supposed to be in parallel to that maybe I should change to that?
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.
sure, sounds fair
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.
ya that sounds good.
Links to docs (as of previous push; noticed a couple of mistakes) |
@anntzer, thanks - those are largely for axes legends, whereas this is for figure legends. OTOH, it would be easy to add an "outside" kwarg for axes legends as well, in fact easier than for figures, I think so long as constrained_layout is being used. Both of them can be done w/o constained_layout as well doing the same things that |
This latest push lets axes.legend have an outside=True as well. It doesn't put the legend outside any axes decorations, so it can overlap tick labels, titles, etc. This is because constrained_layout doesn't do anything below the axes level, and this is an axes level-artist. But it is somewhat nicer than specifying the location by hand (?). import matplotlib.pyplot as plt
fig, ax = plt.subplots(2, 1, constrained_layout=True)
ax[0].plot(range(10), label='boo')
ax[0].legend(loc=1, outside=True)
ax[1].plot(range(10), label='boo')
ax[1].legend(loc=3, outside=True)
plt.show() |
lib/matplotlib/gridspec.py
Outdated
""" | ||
legend for this gridspec, offset from all the subplots. | ||
|
||
See `.Figure.legend_outside` for details on how to call. |
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.
"the outside argument to Figure.legend
"
Dunno whether you want to rename that function now that Figure.legend_outside was renamed, but perhaps not.
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.
Ooops, no this definitely still needs some documentation work. Also the above new stuff is a bit of a mess still, so let me mark WIP.
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.
Fixed....
Sorry for jumping in again at a late stage of the discussion, but I feel this needs thorough consideration to end up with a reasonable API. Parameters
This all tells me that two arguments are arguments are maybe not a great idea. We just have one task: Defining the position of the legend. Splitting this in a direction (like "upper right" or "northeast") and in inside/outside is artificial and even not clear because there is no unambiguous "upper right" for "outside". Also the following combinations do not have a defined meaning:
Also, we already have the meaning of I have to think about the desired API a bit more. AlignmentLooking at the first figure in https://17015-1385122-gh.circle-artifacts.com/0/home/circleci/project/doc/build/html/gallery/text_labels_and_annotations/figlegendoutside_demo.html, I feel that we actually would need some sort of alignment with the axes but outside:
I currently don't have a clear strategy how to do this, but it is at least something to consider when defining the API. |
@timhoffm Not late at all for discussing the API. As pointed out before I'd prefer this was its own function, but that wasn't popular. Given that, indeed some kwargs clash, and get dropped. I don't feel strongly about using the
If you want to align legends on the axes level rather than the figure/gridspec level, the way to do that is with an axes legend. import numpy as np
import matplotlib.pyplot as plt
fig, axs = plt.subplots(1, 2, sharey=True, constrained_layout=True)
x = np.arange(0.0, 2.0, 0.02)
y1 = np.sin(2 * np.pi * x)
y2 = np.exp(-x)
axs[0].plot(x, y1, 'rs-', label='Line1')
h2, = axs[0].plot(x, y2, 'go', label='Line2')
axs[0].set_ylabel('DATA')
y3 = np.sin(4 * np.pi * x)
y4 = np.exp(-2 * x)
axs[1].plot(x, y3, 'yd-', label='Line3')
h4, = axs[1].plot(x, y4, 'k^', label='Line4')
fig.legend(loc='upper center', outside=True, ncol=2)
axs[1].legend(outside=True, loc='lower right')
axs[0].legend(handles=[h2, h4], labels=['curve2', 'curve4'],
loc='center left', outside=True, borderaxespad=6)
plt.show() I don't have a good way to centre the top legend on the pair of the axes, but thats a pretty hard problem in general as you'd need to make assumptions about how the child axes are set up in order to do it. Same with the vertical alignment issues at the figure level. Basically you are saying you'd like the legend centred on the bounding spines? Thats not impossible, but would require building a new bbox_to_anchor out of those spines etc, so would add substantial complexity. And things like colorbars etc would need to be dealt with. Overall, I'm not sure its worth the complexity given that most people will put the legend on the upper right corner. |
4a7e6b4
to
0154ce0
Compare
Are there further comments on the API? Again, my tendency would be to have a different function |
I think the fact that the supported kwargs are effectively different in the inside and outside cases makes me change my vote to be in favor of And then you can actually have a new list of locations that has ENE/NNE (upperupperleft/leftupperleft), and not NE (upperleft) to handle the corners properly without having to guess... |
Ok ENE means upper right beside? NNE means upper right above? Did we want NE ( above and beside?). |
Yes for the first question. I don't know if anyone would want to use "NE" (if that means "both to the right and above the upper-right corner"), so I'd forget about it until someone complains (but certainly, if we have ENE and NNE, then let's not have NE as a shortcut for ENE, that's just silly). Of course the discussion about NSEW vs upper/lower/left/right isn't over yet either :/ |
(@anntzer BTW this failed w the font bug: https://travis-ci.org/matplotlib/matplotlib/jobs/486672509) |
32a1a8a
to
3dbd703
Compare
Still not sure what to do about this one. Seemed that consensus was for a separate method, but @TimHoffman felt strongly that it should be a parsable part of the location kwarg of the main legend function. That’s somewhat held up by the fact that we aren’t entirely happy with our location kwarg strings. I am genuinely curious how often fig.legend is ever called, and if anyone ever wants it to not be outside the axes. I imagine most real world examples are anchoring it outside at axes and then using bbox_inches=tight or adjusting the subplots so the legend fits. I guess so far this is a decent proof of concept. But I’m not sure if I like the fragility if it all. If we were to add this functionality to non constrained layout should we just do the anchoring I suspect most people want? Getting this to work without constrained_layout is maybe a good way to start reinvigorating it. |
This is just to answer the question on frequency of usage (I haven't thought about how this relates to or impacts this PR)
A direct use case is adding a single legend for multiple axes. E.g. in Secondary axis with twinx(): how to add to legend? I proposed to use
to get that legend inside the axes. That answer has become quite popular by now. Also, the most popular solution to How do I make a single legend for many subplots with matplotlib? mentions |
@jklymak wrong user mentioned (#13072 (comment) 😄). I could go with a separate method Do we have a way of introducing experimental/preliminary API? If so, I'd suggest to do so here since we're not quite clear how the API would be best designed. |
I suggest "NUM PAD" style. This makes sense and you will not get confused |
Well that would break back compatibility. It’s also not completely obvious for those of us without a number pad in front of us. Also most phones have the opposite positioning of the keys (with 1,2,3) on the top, so it’s not unique |
I had some initial thoughts, but they don't exactly make sense. I left them here as idea, but see last paragraph. So perhaps trying to overload the one argument If the anchor is on the same side as the location, then it'll be inside the figure, and if it's on the opposite, it'd be outside the figure. A shortcut like 'inside' could exist to mean "pick a reasonable anchor to work like it used to" and be the default. Maybe an 'outside' shortcut could be "pick the opposite anchor choice from location". However, thinking about it some more, this is a |
@QuLogic, thanks for your thoughts. We already have a "AxesGroup"-like object, its a I admit to some ambivalence about the |
Semi-OT random thoughts:
|
FWIW, I'm doing grids of subplots with duplicated curves (i.e. a curve may appear in one or more subplots). In this case, using a figure-level legend seems natural, as the legend is not associated with any particular plot. Also, |
Closed for #19743 |
PR Summary
If constrained_layout is being used, this new kwarg allows the user to automagically place a legend outside the subplots in a grid spec (or all the subplots in a figure for a more straightforward example).
Closes #13023 ping @dcherian
simple example
yields:
Nested gridspec example
yields:
Added ability to vertically lay-out the corners
See
outside='vertical'
below...yields:
PR Checklist