Skip to content

custom legends example #9451

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 1 commit into from
Oct 17, 2017
Merged

custom legends example #9451

merged 1 commit into from
Oct 17, 2017

Conversation

choldgraf
Copy link
Contributor

This is an example to demonstrate how to create legends from scratch using Matplotlib objects. This is often useful if you add a lot of elements to a plot but only want to point out a couple in your legend.

Some stuff that could be improved:

  1. Link to other places in the docs that show how to do this. Right now I only know of one bit at the end of the legend tutorial.
  2. Add other common elements to add to a legend in the way that it's done in this example.

@QuLogic
Copy link
Member

QuLogic commented Oct 17, 2017

At the moment, I'm not quite sure how this is different from the legend tutorial?

@choldgraf
Copy link
Contributor Author

It's the same content in a concise, example format. I don't see a problem with having the same content spread across the examples and the tutorials...i think it makes the content more discoverable...

Copy link
Contributor

@afvincent afvincent left a comment

Choose a reason for hiding this comment

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

A few remarks.


fig, ax = plt.subplots()
lines = ax.plot(data)
ax.legend(lines, loc=(1.05, .05), )
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't the rightest part of the legend cut out if one doesn't tweak the layout (and calling plt.tight_layout() is currently even worse if I am correct)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah yeah that's a good point - ugh that's such annoying behavior from matplotlib :-)`

Copy link
Contributor

Choose a reason for hiding this comment

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

Winter Layout manager is coming (#9082)! This should help to get rid of this kind of issues if I understood correctly.

Copy link
Member

Choose a reason for hiding this comment

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

Well actually it can be gotten rid of with #9164 which is a two liner instead of 20-file fix :-).


fig, ax = plt.subplots()
lines = ax.plot(data)
ax.legend(lines, loc=(1.05, .05), )
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the “tuple notation” really mandatory, or could the trailing coma be removed?


fig, ax = plt.subplots()
lines = ax.plot(data)
ax.legend(custom_lines, ['Cold', 'Medium', 'Hot'], loc=(1.05, .8),)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same remark as before about the legend being possibly cut out.

legend_elements = {'Line': Line2D([0], [0], color='b', lw=4),
'Scatter': Line2D([0], [0], marker='o', color='w',
markerfacecolor='g', markersize=15),
'Color Patch': Patch(facecolor='orange', edgecolor='r')}
Copy link
Contributor

Choose a reason for hiding this comment

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

More a note for the future: if #8292 is some day merged in (I hope so!), it would be nice to remember updating this example to include one case or two.

# is a list of common ones.

from matplotlib.patches import Patch
from matplotlib.lines import Line2D
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe a dumb comment, but Line2D has already been loaded, hasn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah here I was loading it again just so that it's easier for people to trace the imports in case they wanna do it themselves. Think it should be removed? I agree it's not standard PEP style

Copy link
Contributor

Choose a reason for hiding this comment

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

Well I do not know what exactly are our guidelines about this kind of things. I am not particularly pushing for removing it, I just wanted to be sure it had been done on purpose.

About PEP8, there are other imports that are not done at the top of the file anyway, so I guess that it does not really matter anyway ^^.

=> my call would be to keep it this way except if somebody more involved in the docs than I am complains ;)

Copy link
Member

@QuLogic QuLogic Oct 17, 2017

Choose a reason for hiding this comment

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

I think @choldgraf might be the one most involved in the docs. 😛

Copy link
Contributor

Choose a reason for hiding this comment

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

@QuLogic I know ^^, but some other core devs are also quite involved about the docs and do care about what are the relevant guidelines that the docs should follow (hint: from my perspective, you are counted in that group :P)

markerfacecolor='g', markersize=15),
'Color Patch': Patch(facecolor='orange', edgecolor='r')}
fig, ax = plt.subplots()
ax.legend(legend_elements.values(), legend_elements.keys(), loc=(1.05, .8),)
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, when I am trying this locally, the right part of the legend is cut out (by the figure right border).


fig, ax = plt.subplots()
lines = ax.plot(data)
ax.legend(lines, loc=(1.05, .05), )
Copy link
Contributor

Choose a reason for hiding this comment

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

I may be wrong but I think that one tries to use the leading zeros wherever possible: (1.05, .05) <- (1.05, 0.05)?


fig, ax = plt.subplots()
lines = ax.plot(data)
ax.legend(custom_lines, ['Cold', 'Medium', 'Hot'], loc=(1.05, .8),)
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly to previous plot:

  • is the trailing coma mandatory?
  • (1.05, .8) <- (1.05, 0.8)?

markerfacecolor='g', markersize=15),
'Color Patch': Patch(facecolor='orange', edgecolor='r')}
fig, ax = plt.subplots()
ax.legend(legend_elements.values(), legend_elements.keys(), loc=(1.05, .8),)
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly to previous plot:

  • is the trailing coma mandatory?
  • (1.05, .8) <- (1.05, 0.8)?

'Scatter': Line2D([0], [0], marker='o', color='w',
markerfacecolor='g', markersize=15),
'Color Patch': Patch(facecolor='orange', edgecolor='r')}
fig, ax = plt.subplots()
Copy link
Contributor

Choose a reason for hiding this comment

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

Purely cosmetic remark if it was not done on purpose: in the example above, there isn't a blank line after the imports but there is one before instantiating the figure, while in the current example it is the opposite

@afvincent
Copy link
Contributor

afvincent commented Oct 17, 2017

Well, I would say that the current legend tutorial may be quite scary for beginners: it quickly becomes quite advanced (at least from my point of view). This example keep things rather simple and should already cover most of the usual "custom legend" needs (apart maybe from the "multiple legends on the same Axes instance" one). Maybe it could point toward the “advanced” legend tutorial with an hyperlink at the end: most of the end-users (, who I know ^^,) start scrolling the example gallery before even trying to look for a proper tutorial.

@choldgraf
Copy link
Contributor Author

For sure the tutorial should be linked from the example. I'll include that w/ the next round of edits when I address your comments @afvincent ... thanks for the feedback!

markerfacecolor='g', markersize=15),
'Color Patch': Patch(facecolor='orange', edgecolor='r')}
fig, ax = plt.subplots()
ax.legend(legend_elements.values(), legend_elements.keys(), loc=(1.05, .8),)
Copy link
Contributor

@anntzer anntzer Oct 17, 2017

Choose a reason for hiding this comment

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

Note that the order of iteration over values()/keys() is not guaranteed (in fact it will change between iterations on Py3.4/3.5), so please use lists.

Also I think it is much better style to set the label on the artist itself:

legend_elements = [
    Line2D(..., label="foo"),
    Patch(..., label="bar)
]
ax.legend(legend_elements)

The comments apply throughout the tutorial of course.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point re: lists

for setting the label on the artist, IIRC doing that didn't automatically add labels when I called ax.legend but I could be wrong...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ya, when I do

legend_elements = [Line2D([0], [0], color='b', lw=4, label='Line'),
                   Line2D([0], [0], marker='o', color='w', label='Scatter',
                                     markerfacecolor='g', markersize=15),
                   Patch(facecolor='orange', edgecolor='r',
                         label='Color Patch')]
fig, ax = plt.subplots()
ax.legend(legend_elements)

the result is
image

is that a bug?

Copy link
Contributor

Choose a reason for hiding this comment

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

Of course :/, this has to be plt.legend(handles=[...]), otherwise it thinks you're passing labels for some nonexistent artists. Stupid API, refactoring it is somewhere on my (thick) todo.

Copy link
Member

Choose a reason for hiding this comment

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

Well we could do it here: #9324 I agree its a backwards API.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would suggest having this on a separate PR. Basically, if only one positional argument is passed and it's a list of artists, then consider it as handles instead of labels.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

in the meantime, I'll just use plt

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmmm, I actually just tried it w/ plt and had the same behavior...

Copy link
Contributor Author

@choldgraf choldgraf Oct 17, 2017

Choose a reason for hiding this comment

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

ah nvm, I see what you mean about handles=. That works for ax.legend as well actually, but just giving it as a positional argument works for neither.

Copy link
Member

Choose a reason for hiding this comment

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

Right - the positional argument is for labels believe it or not. i.e. ax.legend({'a', 'b', 'c'}) will label the first three artists it finds on ax` 'a', 'b', and 'c'. I guess artists are stored in the order they are drawn, but it seems like a strange choice to me. Once #9324 gets merged it'll be easy to check for a list of handles instead and make the call you were trying to make work.

@choldgraf
Copy link
Contributor Author

ok, comments addressed in latest push.

Copy link
Contributor

@afvincent afvincent left a comment

Choose a reason for hiding this comment

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

Mainly an outdated comment to remove. But otherwise, my previous comments have indeed been addressed :).

Do not forget to include a link toward the legend tutorial somewhere.

# Create the figure
# If you use `ax.legend`, you'll also need to give the labels manually.
fig, ax = plt.subplots()
ax.legend(handles=legend_elements, loc=(0.5, 0.5))
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand that one kind of gave up on the idea on using out-of-axes legend before the aforementioned @jklymak's PRs are merged, but then is there a peculiar reason not to simply use loc="center" instead of loc=(0.5, 0.5). Except of course if one really want to align the lower left of the legend with the center of the axes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nope, I'm just being dumb :-)

label='Color Patch')]

# Create the figure
# If you use `ax.legend`, you'll also need to give the labels manually.
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 that the line above should not be here anymore, should it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nope, will remove

@anntzer
Copy link
Contributor

anntzer commented Oct 17, 2017

I had a quick skim through the legend related examples.

Tutorial:

Examples:

@jklymak
Copy link
Member

jklymak commented Oct 17, 2017

Just to "me too" @anntzer's comment. It'd be great if this new tutorial could find a nice home in one of the other tutorials.

@choldgraf
Copy link
Contributor Author

choldgraf commented Oct 17, 2017

re: creating a new example vs. merging it with pre-existing examples, I'm of two minds on this one. On one hand, I agree that we don't want content to be spread too thinly. On the other hand, I think that appending this content to the currently-existing legend examples isn't a great idea, because

A. those examples are already long enough that they should be in the legend tutorial, not as examples,
and
B. the content in those examples is complex enough that many people will probably just not read it (e.g. the legend example involves subclassing HandlerLineCollection and has a really low SNR in general)

I think most people treat the examples gallery by quickly scanning the page of ctrl+fing for the thing that they want to do. To that extent, IMO, examples should be short and demonstrate a single kind of functionality or show off a single more complex viz, which is why I was keeping this as its separate file rather than glomming it on to something already there.

That said, I totally agree w/ you that we the way legends are handled in the docs should be improved, and I agree some should be removed. We should open an issue on streamlining the legend content using the list that you put together.

@choldgraf
Copy link
Contributor Author

latest push addresses @afvincent 's comments

@anntzer
Copy link
Contributor

anntzer commented Oct 17, 2017

We should open an issue on streamlining the legend content using the list that you put together.

you get to do it :-)

@choldgraf choldgraf force-pushed the custom_legend branch 2 times, most recently from d991533 to 0c6fca9 Compare October 17, 2017 16:44
@choldgraf
Copy link
Contributor Author

ok, #9458 created

@afvincent you're currently blocking on requested changes...anything else I need to do?

@afvincent
Copy link
Contributor

LGTM.

A remaining comment, but non-blocking from my side, is the following: should one add a fixed seed to make the random traces reproducible (like @NelleV did a while ago for all the examples that were drawing random data), or are this kind of things now directly handled by SG?

@choldgraf
Copy link
Contributor Author

good call, just added randomness fixer

@NelleV NelleV merged commit 6657fe1 into matplotlib:master Oct 17, 2017
@QuLogic QuLogic added this to the v2.2 milestone Oct 17, 2017
@QuLogic QuLogic removed this from the needs sorting milestone Feb 12, 2018
@QuLogic QuLogic added this to the v2.2.0 milestone Feb 12, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants