11

I use a relplot with different hue and style and would like to show the respective legend entries besides instead of below each other.

So currently I get a legend like this:

relplot legend with hue and style labels below each other

Instead I would like to have a single legend looking something like this:

relplot legend with hue and style labels besides each other

How can this be done?

I tried setting the following but that had no effect:

plot._legend
leg._ncol = 2
leg.handleheight = 1  # restricting the height

Minimal working example to solve this problem:

import pandas as pd
import seaborn as sns

columns = ['category1', 'category2', 'category3', 'time', 'value']

data = [['content1', 'other1', 'critera1', 0, 0.1], ['content1', 'other1', 'critera1', 1, 0.4], ['content1', 'other1', 'critera1', 2, 0.7], ['content2', 'other1', 'critera1', 0, 0.2], ['content2', 'other1', 'critera1', 1, 0.6], ['content2', 'other1', 'critera1', 2, 0.8], ['content1', 'other2', 'critera1', 0, 0.0], ['content1', 'other2', 'critera1', 1, 0.2], ['content1', 'other2', 'critera1', 2, 0.8], ['content2', 'other2', 'critera1', 0, 0.3], ['content2', 'other2', 'critera1', 1, 0.6], ['content2', 'other2', 'critera1', 2, 0.5], [
    'content1', 'other1', 'critera2', 0, 0.1], ['content1', 'other1', 'critera2', 1, 0.4], ['content1', 'other1', 'critera2', 2, 0.7], ['content2', 'other1', 'critera2', 0, 0.2], ['content2', 'other1', 'critera2', 1, 0.6], ['content2', 'other1', 'critera2', 2, 0.8], ['content1', 'other2', 'critera2', 0, 0.0], ['content1', 'other2', 'critera2', 1, 0.2], ['content1', 'other2', 'critera2', 2, 0.8], ['content2', 'other2', 'critera2', 0, 0.3], ['content2', 'other2', 'critera2', 1, 0.6], ['content2', 'other2', 'critera2', 2, 0.5], ]

df = pd.DataFrame(data, columns=columns)

plot = sns.relplot(x='time', y='value', col='category3', hue='category1', style='category2', kind="line", col_wrap=2, data=df)

leg = plot._legend
leg.set_bbox_to_anchor((0.5, 1.3, 0, 0))
leg._loc = 9

minimal working example output


ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
Spenhouet
  • 6,556
  • 12
  • 51
  • 76
  • 1
    There is no built-in way to do this. And even if it was possible to change the number of columns on the fly, it would look differently than you expect. So you will need to recreate the legend from scratch. If you need an answer on how to do that, it would be nice to provide potential answerers with a [mcve] they can use to showcase the solution. – ImportanceOfBeingErnest Jun 13 '19 at 08:04
  • Possible duplicate of [Separate seaborn legend into two distinct boxes](https://stackoverflow.com/questions/56456956/separate-seaborn-legend-into-two-distinct-boxes) – Diziet Asahi Jun 13 '19 at 08:07
  • @DizietAsahi Thanks for the link but they should still be in the same legend. – Spenhouet Jun 13 '19 at 08:09
  • @ImportanceOfBeingErnest I have added a MWE. – Spenhouet Jun 13 '19 at 08:24

5 Answers5

11

Since you seem to want to place the legend above the plots, I would instruct seaborn to not reserve space on the right for the legend using legend_out=False. Then it's just a matter of getting the handles and labels created by seaborn, and generate a new legend using ncol=2. Note that this will only work well if you have the same number of elements in both columns, otherwise things will get messy.

plot = sns.relplot(x='time', y='value', col='category3', hue='category1', style='category2', kind="line", col_wrap=2, data=df, facet_kws=dict(legend_out=False))
h,l = plot.axes[0].get_legend_handles_labels()
plot.axes[0].legend_.remove()
plot.fig.legend(h,l, ncol=2) # you can specify any location parameter you want here
Diziet Asahi
  • 38,379
  • 7
  • 60
  • 75
6

Use ncol

ax = sns.barplot(x="X", y="Y", data=data)    
ax.legend(loc='upper left',ncol=2, title="Title")
notilas
  • 2,323
  • 4
  • 23
  • 36
4

Final solution thanks to @DizietAsahi

import pandas as pd
import seaborn as sns

columns = ['category1', 'category2', 'category3', 'time', 'value']

data = [['content1', 'other1', 'critera1', 0, 0.1], ['content1', 'other1', 'critera1', 1, 0.4], ['content1', 'other1', 'critera1', 2, 0.7], ['content2', 'other1', 'critera1', 0, 0.2], ['content2', 'other1', 'critera1', 1, 0.6], ['content2', 'other1', 'critera1', 2, 0.8], ['content1', 'other2', 'critera1', 0, 0.0], ['content1', 'other2', 'critera1', 1, 0.2], ['content1', 'other2', 'critera1', 2, 0.8], ['content2', 'other2', 'critera1', 0, 0.3], ['content2', 'other2', 'critera1', 1, 0.6], ['content2', 'other2', 'critera1', 2, 0.5], [
    'content1', 'other1', 'critera2', 0, 0.1], ['content1', 'other1', 'critera2', 1, 0.4], ['content1', 'other1', 'critera2', 2, 0.7], ['content2', 'other1', 'critera2', 0, 0.2], ['content2', 'other1', 'critera2', 1, 0.6], ['content2', 'other1', 'critera2', 2, 0.8], ['content1', 'other2', 'critera2', 0, 0.0], ['content1', 'other2', 'critera2', 1, 0.2], ['content1', 'other2', 'critera2', 2, 0.8], ['content2', 'other2', 'critera2', 0, 0.3], ['content2', 'other2', 'critera2', 1, 0.6], ['content2', 'other2', 'critera2', 2, 0.5], ]

df = pd.DataFrame(data, columns=columns)

plot = sns.relplot(x='time', y='value', col='category3', hue='category1', style='category2', kind="line",
                   col_wrap=2, data=df)

handles, labels = plot.axes[0].get_legend_handles_labels()
plot._legend.remove()
plot.fig.legend(handles, labels, ncol=2, loc='upper center', 
                bbox_to_anchor=(0.5, 1.15), frameon=False)

solution

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
1

The answer given by @DizietAsahi was not working for my case. However, I was able to do this with the sns.move_legend() utility function, which can be used to modify the legend of a plot.

sns.move_legend(ax, "upper center", bbox_to_anchor=(0.5, 1.15), 
                ncol=2)
Timmy Francesco
  • 166
  • 1
  • 6
1
import seaborn as sns
import matplotlib.pyplot as plt

# legend and other functionalities will be controlled by matplotlib.pyplot
fig, ax = plt.subplots()

# your plot (it is generated by seaborn)
sns.relplot(...)

# position your legend
lgd = ax.legend(loc = 'upper center', bbox_to_anchor = (0.5, -0.13),
          fancybox = True, shadow = True, ncol = 2)

# here bbox_extra_artists and bbox_inches = 'tight' will make sure the saved figure is not trimming the legend if it goes outside of the figure
plt.savefig("plot.jpg", bbox_extra_artists = (lgd,), bbox_inches = 'tight')

Note: You may need to adjust bbox_to_anchor if the legend overlaps the axis labels.

References:

hafiz031
  • 2,236
  • 3
  • 26
  • 48