Separate seaborn legend into two distinct boxes

2019-08-20 13:55发布

问题:

I'm using Seaborn to generate many types of graphs, but will use just a simple example here for illustration purposes based on an included dataset:

import seaborn
tips = seaborn.load_dataset("tips")
axes = seaborn.scatterplot(x="day", y="tip", size="sex", hue="time", data=tips)

In this result, the single legend box contains two titles "time" and "sex", each with sub-elements.

How could I easily separate the legend into two boxes, each with a single title? I.e. one for legend box indicating color codes (that could be placed at the left), and one legend box indicating size codes (that would be placed at the right).

回答1:

The following code works well because there is the same number of time categories as sex categories. If it is not necessarily the case, you would have to calculate a priori how many lines of legend are required by each "label"

fig = plt.figure()
tips = seaborn.load_dataset("tips")
axes = seaborn.scatterplot(x="day", y="tip", size="sex", hue="time", data=tips)
h,l = axes.get_legend_handles_labels()
l1 = axes.legend(h[:int(len(h)/2)],l[:int(len(l)/2)], loc='upper left')
l2 = axes.legend(h[int(len(h)/2):],l[int(len(l)/2):], loc='upper right')
axes.add_artist(l1) # we need this because the 2nd call to legend() erases the first



回答2:

If you want to use matplotlib instead of seaborn,

import matplotlib.pyplot as plt
import seaborn
tips = seaborn.load_dataset("tips")

tips["time_int"] = tips["time"].cat.codes
tips["sex_int"] = (tips["sex"].cat.codes*5+5)**2

sc = plt.scatter(x="day", y="tip", s="sex_int", c="time_int", data = tips, cmap="bwr")

leg1 = plt.legend(sc.legend_elements("colors")[0], tips["time"].cat.categories,
                  title="Time", loc="upper right")
leg2 = plt.legend(sc.legend_elements("sizes")[0], tips["sex"].cat.categories,
                  title="Sex", loc="upper left")
plt.gca().add_artist(leg1)
plt.show()



回答3:

I took Diziet's answer and expanded on it. He produced the necessary syntax I was needing, but as he pointed out, was missing a way to calculate how many lines of legend are required for splitting the legend. I have added this, and wrote a complete script:

# Modules #
import seaborn
from matplotlib import pyplot

# Plot #
tips = seaborn.load_dataset("tips")
axes = seaborn.scatterplot(x="day", y="tip", size="sex", hue="time", data=tips)

# Legend split and place outside #
num_of_colors   = len(tips['time'].unique()) + 1
handles, labels = axes.get_legend_handles_labels()
color_hl = handles[:num_of_colors], labels[:num_of_colors]
sizes_hl = handles[num_of_colors:], labels[num_of_colors:]

# Call legend twice #
color_leg = axes.legend(*color_hl,
                        bbox_to_anchor = (1.05, 1),
                        loc            = 'upper left',
                        borderaxespad  = 0.)
sizes_leg = axes.legend(*sizes_hl,
                        bbox_to_anchor = (1.05, 0),
                        loc            = 'lower left',
                        borderaxespad  = 0.)

# We need this because the 2nd call to legend() erases the first #
axes.add_artist(color_leg)

# Adjust #
pyplot.subplots_adjust(right=0.75)

# Display #
pyplot.ion()
pyplot.show()