Movatterモバイル変換


[0]ホーム

URL:


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

Sign up
Appearance settings

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Closed

Conversation

jklymak
Copy link
Member

@jklymakjklymak commentedJan 1, 2019
edited
Loading

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

fig,axs=plt.subplots(1,2,constrained_layout=True)fori,axinenumerate(axs):ax.plot(range(10),label=f'Boo{i}')lg=fig.legend(loc='upper right',outside=True)plt.show()

yields:

simple

Nested gridspec example

fig=plt.figure(constrained_layout=True)gs0=fig.add_gridspec(1,2)gs=gs0[0].subgridspec(1,1)foriinrange(1):ax=fig.add_subplot(gs[i,0])ax.plot(range(10),label=f'Boo{i}')lg=fig.legend(ax=[ax],loc='upper right',outside=True,borderaxespad=4)gs2=gs0[1].subgridspec(3,1)axx= []foriinrange(3):ax=fig.add_subplot(gs2[i,0])ax.plot(range(10),label=f'Who{i}',color=f'C{i+1}')ifi<2:ax.set_xticklabels('')axx+= [ax]lg2=fig.legend(ax=axx[:-1],loc='upper right',outside=True,borderaxespad=4)plt.show()

yields:

complicated

Added ability to vertically lay-out the corners

Seeoutside='vertical' below...

importmatplotlib.pyplotaspltfig=plt.figure(constrained_layout=True)gs0=fig.add_gridspec(1,2)gs=gs0[0].subgridspec(1,1)foriinrange(1):ax=fig.add_subplot(gs[i,0])ax.plot(range(10),label=f'Boo{i}')lg=fig.legend(ax=[ax],loc='upper right',outside=True,borderaxespad=4)gs2=gs0[1].subgridspec(3,1)axx= []foriinrange(3):ax=fig.add_subplot(gs2[i,0])ax.plot(range(10),label=f'Who{i}',color=f'C{i+1}')ifi<2:ax.set_xticklabels('')axx+= [ax]lg2=fig.legend(ax=axx[:-1],loc='upper right',outside='vertical',borderaxespad=4,ncol=2)plt.show()

yields:

figure_1

  • needs more tests

PR Checklist

  • Has Pytest style unit tests
  • Code isFlake 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

zertrin and MaozGelbart reacted with thumbs up emoji
@jklymakjklymak added this to thev3.1 milestoneJan 1, 2019
@jklymakjklymak added the topic: geometry managerLayoutEngine, Constrained layout, Tight layout labelJan 1, 2019
@timhoffm
Copy link
Member

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)

@jklymak
Copy link
MemberAuthor

jklymak commentedJan 1, 2019
edited
Loading

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.

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,

  1. fig.legend_outside(loc='upper right')
  2. fig.legend(loc='outside upper right'), but what about numerical codesfig.legend(loc=1)?
  3. fig.legend(loc='upper right', outside=True) andfig.legend(loc=1, outside=True) with an error raised ifconstrained_layout == False orbbox_to_anchor is notNone.

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 aaxs keyword argument to fig.legend, but thats not too bad - its nicer than collecting the handles and labels from the subplots of interest, and maybe a nice enhancement anyway.

pharshalp reacted with thumbs up emoji

@dcherian
Copy link

I like 3. I'd be more likely to discover it by looking at the docstring for figure.legend

@pharshalp
Copy link
Contributor

pharshalp commentedJan 1, 2019
edited
Loading

+1.0 for option 3, -0.01 for option 2, -1.0 for option 1.

@anntzer
Copy link
Contributor

+1 for option 3.

@jklymak
Copy link
MemberAuthor

... option 3 it is.

@jklymakjklymak changed the titleENH: add figure.legend_outsideENH: add figure.legend; outside kwarg for better layout outside subplotsJan 2, 2019
@anntzer
Copy link
Contributor

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.
Looks like right now it's always to the right? Should perhaps introduce new locations "right-upper" etc. so that "upper-right" means "UR corner + above"? (Perhaps would read better using compass locations (#12679), then one can have "eastnortheast"/"ENE" vs "northnortheast"/"NNE"...)
A quick look suggests that MATLAB only has "northeastoutside" which behaves like "upper-right" does right now.

@jklymak
Copy link
MemberAuthor

Well since we now have the outside kwarg perhaps it could take a string value outside=“vertical”?

@anntzer
Copy link
Contributor

anntzer commentedJan 2, 2019
edited
Loading

How would it combine with "east"(/"left")?
Would be a bit ugly to have "outside" be True/False for "90°" directions and "vertical"/"horizontal"/False for the 45° ones.
Sorry for the nitpicking...

@jklymak
Copy link
MemberAuthor

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)

anntzer reacted with thumbs up emoji

@jklymakjklymakforce-pushed theenh-add-gridspec-legend branch fromb419198 to8e21c15CompareJanuary 2, 2019 18:20
be changed by specifying a string
``outside="vertical", loc="upper right"``.

axs : sequence of `~.axes.Axes`

Choose a reason for hiding this comment

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

does this need to beaxs instead ofaxes?

Copy link
Contributor

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

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

I guesscolorbar usesax. Since this is supposed to be in parallel to that maybe I should change to that?

Copy link
Contributor

Choose a reason for hiding this comment

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

sure, sounds fair

Choose a reason for hiding this comment

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

ya that sounds good.

@anntzer
Copy link
Contributor

Haven't read the other threads, but you may also want to check#3745#3857#6182.

@jklymak
Copy link
MemberAuthor

@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 thatcolorbar does, but that could be the next step of this.

@jklymak
Copy link
MemberAuthor

jklymak commentedJan 4, 2019
edited
Loading

This latest push lets axes.legend have an outside=True as well. Itdoesn'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 (?).

importmatplotlib.pyplotaspltfig,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()

axfigure_1

anntzer reacted with heart emoji

"""
legend for this gridspec, offset from all the subplots.

See `.Figure.legend_outside` for details on how to call.
Copy link
Contributor

Choose a reason for hiding this comment

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

"theoutside argument toFigure.legend"
Dunno whether you want to rename that function now that Figure.legend_outside was renamed, but perhaps not.

Copy link
MemberAuthor

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.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Fixed....

@timhoffm
Copy link
Member

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

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.
Looks like right now it's always to the right? Should perhaps introduce new locations "right-upper" etc. so that "upper-right" means "UR corner + above"? (Perhaps would read better using compass locations (#12679), then one can have "eastnortheast"/"ENE" vs "northnortheast"/"NNE"...)
A quick look suggests that MATLAB only has "northeastoutside" which behaves like "upper-right" does right now.

Well since we now have the outside kwarg perhaps it could take a string value outside=“vertical”?

How would it combine with "east"(/"left")?
Would be a bit ugly to have "outside" be True/False for "90°" directions and "vertical"/"horizontal"/False for the 45° ones.

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:

loc='best', outside=Trueloc='center', outside=Trueloc=(0.1, 0.1), outside=True

Also, we already have the meaning ofloc depend onbbox_to_anchor. It's not getting simpler to document and understand if we have multiple interdependent parameters.

I have to think about the desired API a bit more.

Alignment

Looking at the first figure inhttps://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:

  • The left legend should be centered with the ylabel.
  • The top legend should be centered between the two axes patches.
  • The bottom of the right legend should be aligned with the bottom of the axes patch, not with the bottom of the ticks.

I currently don't have a clear strategy how to do this, but it is at least something to consider when defining the API.

@jklymak
Copy link
MemberAuthor

jklymak commentedJan 6, 2019
edited
Loading

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.

@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 theoutside kwarg; I appreciate that its inelegant, but OTOH, I think its easiest to remember. But further thought is welcome.

Alignment

Looking at the first figure inhttps://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:

  • The left legend should be centered with the ylabel.
  • The top legend should be centered between the two axes patches.
  • The bottom of the right legend should be aligned with the bottom of the axes patch, not with the bottom of the ticks.

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.

importnumpyasnpimportmatplotlib.pyplotaspltfig,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()

dsafigure_1

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.

@jklymak
Copy link
MemberAuthor

Are there further comments on the API? Again, my tendency would be to have a different functionfigure.legend_outside and then we don't have kwarg clashes withfigure.legend. There is no way I can or want to make this work withbbox_to_anchor, and obviouslybest,center etc won't have any meaning. Could have those cases error out.

@anntzer
Copy link
Contributor

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 oflegend_outside.

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...

@jklymak
Copy link
MemberAuthor

Ok ENE means upper right beside? NNE means upper right above? Did we want NE ( above and beside?).

@anntzer
Copy link
Contributor

anntzer commentedJan 31, 2019
edited
Loading

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 :/

@jklymak
Copy link
MemberAuthor

(@anntzer BTW this failed w the font bug:https://travis-ci.org/matplotlib/matplotlib/jobs/486672509)

@jklymakjklymakforce-pushed theenh-add-gridspec-legend branch from32a1a8a to3dbd703CompareJuly 28, 2019 21:57
@jklymak
Copy link
MemberAuthor

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 tonot 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.

@ImportanceOfBeingErnest
Copy link
Member

This is just to answer the question on frequency of usage (I haven't thought about how this relates to or impacts this PR)

I am genuinely curious how oftenfig.legend is ever called, and if anyone ever wants it tonot be outside the axes.

A direct use case is adding a single legend for multiple axes. E.g. inSecondary axis with twinx(): how to add to legend? I proposed to usefig.legend() to get a single legend for twinned axes. Especially,

 fig.legend(loc="upper right", bbox_to_anchor=(1,1), bbox_transform=ax.transAxes)

to get that legend inside the axes. That answer has become quite popular by now.

Also, the most popular solution toHow do I make a single legend for many subplots with matplotlib? mentionsfig.legend() (though it wouldn't be clear from that answer if people put it in- or outside any of the axes).

@timhoffm
Copy link
Member

@jklymak wrong user mentioned (#13072 (comment) 😄). I could go with a separate methodlegend_outside for now. However, the separate method does still not help with the location string.

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.

@naintoo
Copy link

I suggest "NUM PAD" style.
7 8 9
4 5 6
1 2 3
0 - BEST!

This makes sense and you will not get confused

@jklymak
Copy link
MemberAuthor

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

@QuLogic
Copy link
Member

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 argumentloc in this way may be a bit too confusing. It seems like what you're trying to convey is both a location and an anchor, so why not use two arguments (both of which exist somewhere in mpl, if not as arguments, than as concepts at least) instead? I know we havebbox_to_anchor, but a legend location + legend anchor are sufficient to describe all of the options given above without using confusing upper-right/right-upper/etc. (except for the previously mentioned parameter, which could be de-emphasized a bit if necessary.)

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 aFigure method and there isn't really a concept of 'outside' the figure. What this is really working on is a (sub)group ofAxes. If the result ofsubplots was a sort ofAxesGroup on which you could calllegend, then the above location/anchor makes a bit more sense. Or if you could do(ax1 + ax2).legend()... Anyway, now I'm just throwing out random ideas and I'm not sure how to proceed.

@tacaswelltacaswell modified the milestones:v3.2.0,v3.3.0Aug 27, 2019
@jklymak
Copy link
MemberAuthor

@QuLogic, thanks for your thoughts. We already have a "AxesGroup"-like object, its agridspec. As noted above, I'd be happy if this method became agridspec method.

I admit to some ambivalence about theanchor paradigm. Its super flexible, but correspondingly hard to remember how to use.

@timhoffm
Copy link
Member

Semi-OT random thoughts:

  • What "outside" legends really need to do is steal space from one or more axes. We have a similar concept incolorbar where the parameterax can take one or more axes to steal space from. Maybe the concepts there and here are similar enough to have a common approach.
  • Maybe we should have a non-Axes container that can be placed into a gridspec, which then holds the legend (or the legend could be placed inside its own axes - which OTOH sounds like a bad idea). Main point here would be that the legend gets a defined share of the layout.

@jjnurminen
Copy link

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,constrained_layout works very well for me, except it doesn't supportfig.legend. Thus, I've resorted to creating an extra 'legend axis' outside my subplots and manually constructing a legend there, but that's a bit clumsy. So I would be +1 on implementing this.

@QuLogicQuLogic modified the milestones:v3.3.0,v3.4.0May 2, 2020
@jklymakjklymak marked this pull request as draftJuly 23, 2020 16:32
@QuLogicQuLogic modified the milestones:v3.4.0,v3.5.0Jan 21, 2021
@jklymakjklymak mentioned this pull requestMar 19, 2021
7 tasks
@jklymak
Copy link
MemberAuthor

Closed for#19743

@jklymakjklymak deleted the enh-add-gridspec-legend branchMarch 19, 2021 05:16
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@anntzeranntzeranntzer left review comments

@dcheriandcheriandcherian left review comments

Assignees
No one assigned
Projects
None yet
Milestone
v3.5.0
Development

Successfully merging this pull request may close these issues.

constrained_layout support for figure.legend
11 participants
@jklymak@timhoffm@dcherian@pharshalp@anntzer@ImportanceOfBeingErnest@naintoo@QuLogic@jjnurminen@tacaswell@story645

[8]ページ先頭

©2009-2025 Movatter.jp