Skip to content
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

Not clear how to reposition seaborn.histplot legend #2280

Closed
mortonjt opened this issue Sep 14, 2020 · 7 comments
Closed

Not clear how to reposition seaborn.histplot legend #2280

mortonjt opened this issue Sep 14, 2020 · 7 comments

Comments

@mortonjt
Copy link

I was pretty excited about seeing the histplot functionality coming out, and I'm getting beautiful figures with it already.

I'm trying to finetune my figures and reposition the legends - but am running into issues when moving the legend.
Below is toy example

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
fig, ax = plt.subplots()
x = np.hstack((np.random.normal(0, 1, 100), np.random.normal(-0.5, 1, 100), np.random.normal(0.5, 1, 100)))
data = pd.DataFrame({'x': x, 'd' : ['a'] * 100 + ['b'] * 100 + ['c'] * 100})
g = sns.histplot(data, x='x', hue="d", element="step", stat="probability", ax=ax)

image

When I move the legend to the upper left, the legend disappears with the warning No handles with labels found to put in legend..

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
fig, ax = plt.subplots()
x = np.hstack((np.random.normal(0, 1, 100), np.random.normal(-0.5, 1, 100), np.random.normal(0.5, 1, 100)))
data = pd.DataFrame({'x': x, 'd' : ['a'] * 100 + ['b'] * 100 + ['c'] * 100})
g = sns.histplot(data, x='x', hue="d", element="step", stat="probability", ax=ax, legend=False)
ax.legend(loc='upper-left')

image

This is interesting, because it doesn't appear to be an issue with the other plots
https://stackoverflow.com/questions/27019079/move-seaborn-plot-legend-to-a-different-position
https://stackoverflow.com/questions/53733755/how-to-move-legend-to-outside-of-a-seaborn-scatterplot/53737271

I'm curious if this is unique to histplot (or if I'm overlooking something).

@mwaskom
Copy link
Owner

mwaskom commented Sep 14, 2020

Hi, yes, legends do work a little bit differently in the distribution plots, and they're also still a little rough around the edges.

In short, seaborn currently has two options: add a bunch of phantom artists to the axes with labels (meaning that calling ax.legend() again will pick them up and replace the existing legend) or pass handles and labels directly to ax.legend (meaning that inspecting or modifying the artists is easier, but "moving" the legend is harder).

IMO the root problem is that matplotlib legend objects lack a public-facing API for modifying them once they exist. I'm pushing on matplotlib to fix this limitation in core, which would be better than implementing a hacky work around in seaborn (thread here).

Here's also a issue tracking some known ongoing difficulties with legends in seaborn, some (but not all) of which are attributable to the matplotlib-level API: #2231. Solving some of these will require moving way from "option 1" above.

Right now, your best bet is probably something like this:

def move_legend(ax, new_loc, **kws):
    old_legend = ax.legend_
    handles = old_legend.legendHandles
    labels = [t.get_text() for t in old_legend.get_texts()]
    title = old_legend.get_title().get_text()
    ax.legend(handles, labels, loc=new_loc, title=title, **kws)
    
move_legend(ax, "upper left")

@felipezeiser
Copy link

Hi, yes, legends do work a little bit differently in the distribution plots, and they're also still a little rough around the edges.

In short, seaborn currently has two options: add a bunch of phantom artists to the axes with labels (meaning that calling ax.legend() again will pick them up and replace the existing legend) or pass handles and labels directly to ax.legend (meaning that inspecting or modifying the artists is easier, but "moving" the legend is harder).

IMO the root problem is that matplotlib legend objects lack a public-facing API for modifying them once they exist. I'm pushing on matplotlib to fix this limitation in core, which would be better than implementing a hacky work around in seaborn (thread here).

Here's also a issue tracking some known ongoing difficulties with legends in seaborn, some (but not all) of which are attributable to the matplotlib-level API: #2231. Solving some of these will require moving way from "option 1" above.

Right now, your best bet is probably something like this:

def move_legend(ax, new_loc, **kws):
    old_legend = ax.legend_
    handles = old_legend.legendHandles
    labels = [t.get_text() for t in old_legend.get_texts()]
    title = old_legend.get_title().get_text()
    ax.legend(handles, labels, loc=new_loc, title=title, **kws)
    
move_legend(ax, "upper left")

this worked for me. thank you so much

mwaskom added a commit that referenced this issue Aug 15, 2021
This addresses issues discussed in #2280, along with some of the issues in #2231

It is a somewhat hack-ish solution. Because matplotlib legends don't offer public
control over their location, this copies data from an existing legend to a new
object, and then removes the original legend. I am hopeful that there will be
upstream changes that make legend repositioning more natural, but this is
a reasonable stopgap measure to alleviate a common seaborn pain-point.
mwaskom added a commit that referenced this issue Aug 15, 2021
This addresses issues discussed in #2280, along with some of the issues in #2231

It is a somewhat hack-ish solution. Because matplotlib legends don't offer public
control over their location, this copies data from an existing legend to a new
object, and then removes the original legend. I am hopeful that there will be
upstream changes that make legend repositioning more natural, but this is
a reasonable stopgap measure to alleviate a common seaborn pain-point.
@mwaskom
Copy link
Owner

mwaskom commented Aug 15, 2021

I'm going to consider this closed with #2643, which codifies the move_legend hack as a helper function in seaborn. It doesn't solve the "histplot works differently from other plots" problem, but I think that's unlikely to change in the future (or rather, for reasons discussed above, other functions will work more like histplot). Legend inconsistencies / difficulties are otherwise tracked in #2231.

So, official recommendation is to move (and update) the legend, for any seaborn plot, like this:

ax = sns.histplot(data=penguins, x="bill_length_mm", hue="species")
sns.move_legend(ax, "lower center", bbox_to_anchor=(.5, 1), ncol=3, title_fontsize=14)

image

@mwaskom mwaskom closed this as completed Aug 15, 2021
mwaskom added a commit that referenced this issue Aug 15, 2021
This addresses issues discussed in #2280, along with some of the issues in #2231

It is a somewhat hack-ish solution. Because matplotlib legends don't offer public
control over their location, this copies data from an existing legend to a new
object, and then removes the original legend. I am hopeful that there will be
upstream changes that make legend repositioning more natural, but this is
a reasonable stopgap measure to alleviate a common seaborn pain-point.

(cherry picked from commit f4a5076)
@erinboyle
Copy link

Thank you!! Big improvement!

@jofa974
Copy link

jofa974 commented Sep 16, 2021

I was about to go crazy... until I found this post. Thanks a lot !

@ditag
Copy link

ditag commented Oct 13, 2021

I was about to go crazy... until I found this post. Thanks a lot !

Same here!

@ravshanovbek
Copy link

I'm going to consider this closed with #2643, which codifies the move_legend hack as a helper function in seaborn. It doesn't solve the "histplot works differently from other plots" problem, but I think that's unlikely to change in the future (or rather, for reasons discussed above, other functions will work more like histplot). Legend inconsistencies / difficulties are otherwise tracked in #2231.

So, official recommendation is to move (and update) the legend, for any seaborn plot, like this:

ax = sns.histplot(data=penguins, x="bill_length_mm", hue="species")
sns.move_legend(ax, "lower center", bbox_to_anchor=(.5, 1), ncol=3, title_fontsize=14)

image
Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants