Seaborn: Not clear how to reposition seaborn.histplot legend

Created on 14 Sep 2020  路  1Comment  路  Source: mwaskom/seaborn

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

annoyance question

Most helpful comment

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: https://github.com/mwaskom/seaborn/issues/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")

>All comments

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: https://github.com/mwaskom/seaborn/issues/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")
Was this page helpful?
0 / 5 - 0 ratings

Related issues

sofiatti picture sofiatti  路  4Comments

ConstantinoSchillebeeckx picture ConstantinoSchillebeeckx  路  4Comments

amelio-vazquez-reina picture amelio-vazquez-reina  路  4Comments

amelio-vazquez-reina picture amelio-vazquez-reina  路  3Comments

stonebig picture stonebig  路  4Comments