Legends are difficult to modify

One aspect of matplotlib that makes it really nice for supoprting higher-level interfaces it that most aspects of the plot can be tweaked after they’ve been defined. When users want to customize a plot beyond that is offered by the API of the higher level library, they can drop down to the matplotlib layer and tweak away.

But this is not really the case for legends. Once a legend is created, the API for modifying it is unfriendly, compared to the Axes and Artist APIs.

The biggest pain point is that legends are very difficult to move after they’ve been created. There is not, as far as I can tell, a public API for changing the location of the legend that is analogous to the parameter you use when creating it (loc). Legend._set_loc is private. There is Legend.set_bbox_to_anchor, which does allow you to move the legend around. But there’s no public access to the anchor point, and if the library used loc="best" to make the original legend, I don’t think there’s any public API for getting a predictable result.

One also sometimes reaches for a method like Legend.set_labels. Unlike moving the legend, changing the labels post-hoc is possible to achieve, because the Legend.texts list is public and the text attributes can be modified in place. But it’s clumsy. And while less common, having post-hoc access to the various legend parameters (e.g. handlelength, handletextpad) would make it easier for users to customize existing legends.

Writing this post here to start a discussion rather than opening a formal feature request issue.

2 Likes

I don’t think there is any religious objection to making the legend API more flexible. I think the practical issues are that the code is quite non-trivial so making adding things to the API w/o breaking the existing code can be daunting.

Yes, one of the reasons I didn’t open an issue is because the legend code scares me :slight_smile:

I wrote these issues in descending order of importance, and also (it seems) descending order of ease of implementation. But I don’t know the history of why so much of the legend API is private, so I might be missing something.

A related (but distinct, so I’ll just mention it here) is that the Legend seems very difficult to sub-class in any useful way because of how much happens in the constructor using private methods. Something to keep in mind…

So to prioritize, making it possible to predictably move a legend post-doc seems like the most important thing, because it fixes something that I don’t believe is currently possible. Perhaps I’ll open a targeted issue about that?

Well, I struggled a bit with customizing a legend lately. What you want can be done by making the legend ‘draggable’. This makes it possible to change the legend position before saving the figure as a svg or png (that is via the window ‘save-button’ that is visible when plotting from a program run in a python shell; not when using a jupyter notebook with the %matplotlib inline option). The most leeway you’ll get with having a legend for the figure (leg=fig.legend(); leg.set_draggable(True)) (in most examples it is linked to a particular axis containing the lines/bars the legend refers to).But I guess you are aware of this all, altrhough it is lacking from the legend guide

If one’s data is organised in the form of a pandas.DataFrame (df), it would be great if a draggable legend could be set in the plotting options, as df.plot() brings in a setting for legend. But alas, this is not the case at the moment. But when implemented, by whom would this be done? At the mpl side of things or is this for the pandas people to do and should this be discussed at their end?

Otherwise, when fiddling with specifics of a figure, one quickly gets into the matplot-lib side of things and from that point onwards, once one has dived in, setting the legend draggable is not difficult as long as you are aware of the possibility…

I hope this is helpful feedback; there is a lot of info out there how to make and customize legends but maybe use cases need to be added that describe the plotting from other perspectives (like by starting from a pandas.dataframe).

Thanks @brobr but a dragabble legend doesn’t solve the problem; I need to change its position programmatically.

But if you do want to make a pandas legend dragabble, why isn’t

ax = df.plot()
ax.legend_.set_draggable(True)

sufficient?

Ok, but where to? The bbox_to_anchor gives a lot of room… Alternatively you could split the figure up in a grid and place the legend in a space for itself.

Instead of ax.legend_.set_draggable(True), did you try ax.get_legend().set_draggable(True)? I found that assessing properties directly not always give the expected result… (I had this with ax.lines which did not work for me when ax.get_lines() did)

ax.get_legend() just does return self.legend_; both are fine and I guess it’s a matter of taste as to which you use.

Quoting from original post:

There is Legend.set_bbox_to_anchor , which does allow you to move the legend around. But there’s no public access to the anchor point, and if the library used loc="best" to make the original legend, I don’t think there’s any public API for getting a predictable result.

I am not a developer, more of an ‘advancing user beginner’ if that’s possible; I’ve been speaking on the basis of what I figured out by trial and error;

I wanted a consistent placement of the legend with ability to correct this afterwards (before saving the plot to svg so that less work is needed in say inkscape) and thus ended up with a Figure.legend with loc='upper right', bbox_to_anchor=(0.88, 0.92). If I recall this correctly, the loc gives the reference corner of the legend placed on the given (relative) figure coordinates. This results in a placement independent of anything underneath, such as the ax.lines, and thus not based on these (as would be with using loc='best' for an Axes.legend). Is this not a way to set an achor point (and orientation) of the legend programmatically? Changing the numbers changes the position of the legend. Or do you need/think of something else?

For me the main difficulty is to figure out the usage of the various options; this is not always clear. A recent API change illustrates this:

Legend and Table no longer allow invalid locations
This affects legends produced on an Axes (Axes.legend and pyplot.legend) and on a Figure (Figure.legend and pyplot.figlegend). Figure legends also no longer accept the unsupported ‘best’ location. Previously, invalid Axes locations would use ‘best’ and invalid Figure locations would used ‘upper right’.

Yes that is how you place a legend when you’re calling ax.legend(). The discussion is about the API for modifying a legend artist that already exists: for instance, because a third-party library (like pandas) created the plot and then returned the Axes object to the user. Because the matplotlib Legend object exposes neither get_loc nor set_loc, it is somewhere between not easy and actually impossible to move an existing legend to a specific location in a plot by modifying its properties through a public API.

1 Like

Thanks for the explanation; Wonder whether something along the lines of setting the legend draggable and then have a method that does the ‘dragging’ programmatically (.i.e. by setting new position coordinates) would be an approach?

I just looked at the API:; the last bit seems possible:-)

set_draggable(self, state, use_blit=False, update=‘loc’)[source]¶

update{‘loc’, ‘bbox’}, optional
The legend parameter to be changed when dragged:

‘loc’: update the loc parameter of the legend
‘bbox’: update the bbox_to_anchor parameter of the legend

Would this do it?

EDIT:
There is the possibility

leg.set_bbox_to_anchor((0.08, 0.12))

That worked for me after having set the legend originally somewhere else…

Of course one has to know about bbox_to_anchor in order to do something like this. That’s what makes mpl difficult, to know/find what options cover what one has in mind

Wonder whether something along the lines of setting the legend draggable and then have a method that does the ‘dragging’ programmatically (.i.e. by setting new position coordinates) would be an approach?

Dragabillity is neither here nor there. There are _get_loc and _set_loc methods, but they are not part of the public API. The proposal here is to make them part of the public API. This will take a little bit of work, because internally matplotlib tracks the location using an index, but Legend.set_loc should accept a string (e.g. "upper right"), and there would need to be a decision about what get_loc should return, along with potential changes in the internal code that uses it.

The broader point is that, in this and other ways (some mentioned in the original post), legends do not adhere to the same “fully-tweakable after creation” design principle as most other components of matplotlib.

Of course one has to know about bbox_to_anchor in order to do something like this

As I mentioned earlier,

There is Legend.set_bbox_to_anchor , which does allow you to move the legend around. But there’s no public access to the anchor point, and if the library used loc=“best” to make the original legend, I don’t think there’s any public API for getting a predictable result.

Hi, I did not realise you are developing seaborn. Thanks for that, amazing and helpful package to get all kinds of stuff done. Maybe you’re not waiting to extend the conversation with a noob, but see it as feedback from a user; someone working with, not creating the code (my main work is in molecular biology).

I’ve just been trying out to place/replace a legend made via sns.catplot(...., legend_out=False).
The placement obtained with the default ‘legend_out’ is not alweays satisfactory. (But that is not a big problem; edit the figure in an svg-editor; that works often faster than finding that one option that might do the job). But trying to understand the stuff, searching the web or the documentation, one runs into quite some overlap of how things can be done.

The above out-of-figure legend-placement can be done outwith seaborn, possibly derived from
move-seaborn-plot-legend-to-a-different-position, while someone else is even writing a book chapter about setting a figure size only.

All these various ways are good in the sense that a lot is possible; but for a (not-a-full-time programmer) beginner it is enormously confusing. It took quite some time to appreciate the level one is operating at (‘Axes’, ‘Figure’??; using plt. interface or the object-orientation?? ) and how functions cross-refer. And the realisation that one has to inactivate an item that is well presented (say a legend) and built it completely over (say via matplot.lib) is also not that obvious. This involves quite a learning curve. From the seaborn documentation:

g = sns.catplot(x="total_bill", y="day", hue="time",
                height=3.5, aspect=1.5,
                kind="box", legend=False, data=tips);
g.add_legend(title="Meal")

So after the plot, a legend can be created in seaborn. And as you will know, with extra matplotlib code this can be placed somewhere else (at least in the figure I was working on):

g.add_legend(title=‘Meal’,loc=‘upper right’, bbox_to_anchor=(0.98, 0.90))

As referred to in the seaborn documentation, this ‘mixing’ is not always obvious; but this works (possibly because it is at the figure level) too:

leg = g._legend  #just a guess ;-)
leg.set_bbox_to_anchor((0.08, 0.12))
leg.set_draggable(True)

So what I do not understand is the need to change the matplotlib api, where maybe providing more transparent access to an element from seaborn that would allow the direct use of a matplotlib function could help a lot of users.

Being built on top of matplotlib it makes sense that various settings in seaborn (or pandas.plot) translate/overlap matplotlib functions. Also that this overlap is not complete. But as a user one would like to have a quick way of getting to an aspect of a figure whithout the need to learn all the multiple ways to accomplish something (e.g. “move that legend”).

If there is a documented matplotlib solution for fine-tuning (a part of) a figure absent from pandas or seaborn, ideally one would be able to use that solution without needing to rebuild that (part of the) figure in matplotlib.

Say, instead of g._legend above, a function like g.get_legend() in the list of seaborn methods would help access to using matplotlib in a more seamless manner (at least until the proposed legend-api change is working/implemented).

I think you’re basically making my point for me here … the fact that legends cannot be easily repositoned once they exist forces users of third-party tools into non-obvious workarounds like plotting without a legend, then calling .legend() themselves with their desired location.

Say, instead of g._legend above, a function like g.get_legend() in the list of seaborn methods would help access to using matplotlib in a more seamless manner (at least until the proposed legend-api change is working/implemented).

seaborn v0.11 will have FacetGrid.legend as a public attribute but, again, without a good API at the matplotlib level for programmatically modifying it, that’s of somewhat limited use. Setting the legend to draggable and manually adjusting works for some cases, but it’s not reproducible and it’s not helpful if you’re using a non-interactive backend.