Move legend outside figure in seaborn tsplot [duplicate] - python

This question already has answers here:
Move seaborn plot legend to a different position
(8 answers)
How to put the legend outside the plot
(18 answers)
Closed 8 months ago.
I would like to create a time series plot using seaborn.tsplot like in this example from tsplot documentation, but with the legend moved to the right, outside the figure.
Based on the lines 339-340 in seaborn's timeseries.py, it looks like seaborn.tsplot currently doesn't allow direct control of legend placement:
if legend:
ax.legend(loc=0, title=legend_name)
Is there a matplotlib workaround?
I'm using seaborn 0.6-dev.

20220916 Update
Since version v0.11.2 of seaborn, there is an in-built control of the legend position, see seaborn.move_legend. To put the legend outside:
ax = sns.histplot(penguins, x="bill_length_mm", hue="species")
sns.move_legend(ax, "upper left", bbox_to_anchor=(1, 1))
Old Answer
Indeed, seaborn doesn't handle legends well so far. You can use plt.legend() to control legend properties directly through matplotlib, in accordance with Matplotlib Legend Guide.
Note that in Seaborn 0.10.0 tsplot was removed, and you may replicate (with different values for the estimation if you please) the plots with lineplot instead of tsplot.
Snippet
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="darkgrid")
# Load the long-form example gammas dataset
gammas = sns.load_dataset("gammas")
# Plot the response with standard error
sns.lineplot(data=gammas, x="timepoint", y="BOLD signal", hue="ROI")
# Put the legend out of the figure
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
Output

Existing solutions seem to be making things unnecessarily complicated by using the "wrong" thing for the location parameter; think about it terms of where the legend is in relation to an anchor. For example, if you want a legend on the right, then the anchor location is center left of it.
We can simplify Sergey Antopolskiy's answer down to:
import seaborn as sns
# Load the long-form example gammas dataset
g = sns.lineplot(data=gammas, x="timepoint", y="BOLD signal", hue="ROI")
# Put the legend out of the figure
g.legend(loc='center left', bbox_to_anchor=(1, 0.5))
bbox_to_anchor says we want the anchor on the right (i.e. 1 on the x-axis) and vertically centered (0.5 on the y-axis). loc says we want the legend center-left of this anchor.
In Seaborn version 0.11.0, this gives me something like:

I tried to apply T.W.'s answer for seaborn lineplot, without success. A few modifications to his answer did the job... in case anyone is looking for the lineplot version as I was!
import seaborn as sns
import pandas as pd
# load data
df = sns.load_dataset("gammas")
# EDIT: I Needed to ad the fig
fig, ax1 = plt.subplots(1,1)
# EDIT:
# T.W.' answer said: "create with hue but without legend" <- # I needed to include it!
# So, removed: legend=False
g = sns.lineplot(x="timepoint", y="BOLD signal", hue="ROI", data=df, ax=ax1)
# EDIT:
# Removed 'ax' from T.W.'s answer here aswell:
box = g.get_position()
g.set_position([box.x0, box.y0, box.width * 0.85, box.height]) # resize position
# Put a legend to the right side
g.legend(loc='center right', bbox_to_anchor=(1.25, 0.5), ncol=1)
plt.show()

The answer by Sergey worked great for me using a seaborn.tsplot but I was not able to get it working for an seaborn.lmplot so I looked a bit deeper and found another solution:
Example:
import seaborn as sns
import pandas as pd
# load data
df = pd.DataFrame.from_csv('mydata.csv')
# create with hue but without legend
g = sns.lmplot(x="x_data", y="y_data", hue="condition", legend=False, data=df)
# resize figure box to -> put the legend out of the figure
box = g.ax.get_position() # get position of figure
g.ax.set_position([box.x0, box.y0, box.width * 0.85, box.height]) # resize position
# Put a legend to the right side
g.ax.legend(loc='center right', bbox_to_anchor=(1.25, 0.5), ncol=1)
sns.plt.show(g)
Maybe you have to play around with the values to fit them to your legend.
This answer will also be helpful if you need more examples.

A pure seaborn solution:
FacetGrid-based Seaborn plots can do this automatically using the legend_out kwarg. Using relplot, pass legend_out to the FacetGrid constructor via the facet_kws dictionary:
import seaborn as sns
sns.set(style="darkgrid")
gammas = sns.load_dataset("gammas")
sns.relplot(
data=gammas,
x="timepoint",
y="BOLD signal",
hue="ROI",
kind="line",
facet_kws={"legend_out": True}
)

Related

How to customiza Seaborn/Matplotlib heatmap colorbars

I need to customize the labels and ticks of an
heatmap colorbar obtained by using matplotlib with no success so far.
My data have been already posted and can be found here: 1
My working code:
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
df = pd.read_csv("deltaGdata.csv")
df1 = df[['VARIANT', 'DDgun','mCSM', 'SDM', 'DeepDDG', 'DynaMut2']]
df1.set_index(['VARIANT'],inplace=True)
sns.set(rc = {'figure.figsize':(7, 20)}) # (width_inches, width_height)
ax = sns.heatmap(df1, cmap='rocket')
ax.set_yticks(np.arange(len(df1)) + .5)
ax.set_yticklabels(df1.index,fontname='DejaVu Sans', fontsize=14.5, fontweight='550' )
ax.set_xticklabels(df1,fontname='DejaVu Sans', fontsize=20, fontweight='550', rotation=90)
ax.set_title("ΔΔG (Kcal/mole)", fontname='DejaVu Sans', fontsize=24, fontweight='700')
figure = ax.get_figure()
figure.savefig('fig.png', dpi=300)
figure.savefig('fig.svg', dpi=300, format="svg")
This code produces an heatmap with a colorbar having very tiny ticks and numbers compared with the others
in the final figure.
I found that there is very little documentation about
how to customize colorbars and nothing useful to fix
my problem. I hope to get help also because I think it
would be beneficial for others Matplotlib/Seaborn users.
My understanding is that it consists of a heatmap and a color bar subplot, with the last subplot specifying the size of the label attribute.
print(ax.figure.axes)
[<AxesSubplot:title={'center':'ΔΔG (Kcal/mole)'}, ylabel='VARIANT'>, <AxesSubplot:label='<colorbar>'>]
# Add the following code
ax.figure.axes[-1].tick_params(labelsize=20)

Legend not showing with barless histogram plot in python

I am trying to plot a kde plot in seaborn using the histplot function, and removing later the bars of the histogram in the following way (see last part of the accepted answer here):
fig, ax = plt.subplots()
sns.histplot(data, kde=True, binwidth=5, stat="probability", label='data1', kde_kws={'cut': 3})
The reason for using histplot instead of kdeplot is that I need to set a specific binwidth. The problem I have that I cannot print out the legend, meaning that
ax.legend(loc='best')
does nothing, and I receive the following message: No handles with labels found to put in legend.
I have also tried with
handles, labels = ax.get_legend_handles_labels()
plt.legend(handles, labels, loc='best')
but without results. Does anybody have an idea of what is going on here? Thanks in advance!
You can add the label for the kde line via the line_kws={'label': ...} parameter.
sns.kdeplot can't be used directly, because currently the only option is the default scaling (density).
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
data = np.random.normal(0.01, 0.1, size=10000).cumsum()
ax = sns.histplot(data, kde=True, binwidth=5, stat="probability", label='data1',
kde_kws={'cut': 3}, line_kws={'label': 'kde scaled to probability'})
ax.containers[0].remove() # remove the bars of the histogram
ax.legend()
plt.show()

Difficulty combining and repositioning the legends of two charts in matplotlib and pandas

I am trying to plot two charts onto one figure, with both charts coming from the same dataframe, but one represented as a stacked bar chart and the other a simple line plot.
When I create the plot using the following code:
combined.iloc[:, 1:10].plot(kind='bar', stacked=True, figsize=(20,10))
combined.iloc[:, 0].plot(kind='line', secondary_y=True, use_index=False, linestyle='-', marker='o')
plt.legend(loc='upper left', fancybox=True, framealpha=1, shadow=True, borderpad=1)
plt.show()
With the combined data frame looking like this:
I get the following image:
I am trying to combine both legends into one, and position the legend in the upper left hand corner so all the chart is visible.
Can someone explain why plt.legend() only seems to be editing the line chart corresponding to the combined.iloc[:, 0] slice of my combined dataframe? If anyone can see a quick and easy way to combine and reposition the legends please let me know! I'd be most grateful.
Passing True for the argument secondary_y means that the plot will be created on a separate axes instance with twin x-axis, since this creates a different axes instance the solution is generally to create the legend manually, as in the answers to the question linked by #ImportanceOfBeingErnest. If you don't want to create the legend directly you can get around this issue by calling plt.legend() between calls to pandas.DataFrame.plot and storing the result. You can then recover the handles and labels from the two axes instances. The following code is a complete example of this
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
df = pd.DataFrame({'x' : np.random.random(25),
'y' : np.random.random(25)*5,
'z' : np.random.random(25)*2.5})
df.iloc[:, 1:10].plot(kind='bar', stacked=True)
leg = plt.legend()
df.iloc[:, 0].plot(kind='line', y='x', secondary_y=True)
leg2 = plt.legend()
plt.legend(leg.get_patches()+leg2.get_lines(),
[text.get_text() for text in leg.get_texts()+leg2.get_texts()],
loc='upper left', fancybox=True, framealpha=1, shadow=True, borderpad=1)
leg.remove()
plt.show()
This will produce
and should be fairly easy to modify to suit your specific use case.
Alternatively, you can use matplotlib.pyplot.figlegend(), but you will need to pass legend = False in all calls to pandas.DataFrame.plot(), i.e.
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
df = pd.DataFrame({'x' : np.random.random(25),
'y' : np.random.random(25)*5,
'z' : np.random.random(25)*2.5})
df.iloc[:, 1:10].plot(kind='bar', stacked=True, legend=False)
df.iloc[:, 0].plot(kind='line', y='x', secondary_y=True, legend=False)
plt.figlegend(loc='upper left', fancybox=True, framealpha=1, shadow=True, borderpad=1)
plt.show()
This will however default to positioning the legend outside the axes, but you can override the automatic positioning via the bbox_to_anchor argument in calling plt.figlegend().

How to reposition title in seaborn.FacetGrid if tight_layout is applied?

I have a simple seaborn FacetGrid() with barplots inside.
I applied tight_layout() to my final plot, as xticks had to be properly positioned on the plot after rotation.
As result, when I want to add the title to the plot it is positioned in the wrong place, basically over the existing axes.
So, my question is how should the title be manipulated in order to be properly positioned in case tight_layout() is applied?
I reproduced the issue with the standard tips dataset:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
tips = sns.load_dataset("tips")
days_dict = {day:day+', a long name' for day in tips['day'].unique()}
tips['day_long'] = tips['day'].map(lambda x: days_dict[x])
grid = sns.FacetGrid(tips,col='size',col_wrap=3,height=4,sharex=False)
grid.map(sns.barplot, 'day_long', 'total_bill').set_titles('{col_name}')
grid.fig.set_size_inches(10,10)
grid.fig.suptitle('Title (it should be placed higher)',fontsize=16)
for ax in grid.axes.flat:
for label in ax.get_xticklabels():
label.set_rotation(90)
plt.tight_layout()
plt.show()
Add (adjust the value to your taste)
grid.fig.subplots_adjust(top=0.90)
after tight_laout() to make some room at the top of the plot for the suptitle()

How to prevent overlapping x-axis labels in sns.countplot

For the plot
sns.countplot(x="HostRamSize",data=df)
I got the following graph with x-axis label mixing together, how do I avoid this? Should I change the size of the graph to solve this problem?
Having a Series ds like this
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np; np.random.seed(136)
l = "1234567890123"
categories = [ l[i:i+5]+" - "+l[i+1:i+6] for i in range(6)]
x = np.random.choice(categories, size=1000,
p=np.diff(np.array([0,0.7,2.8,6.5,8.5,9.3,10])/10.))
ds = pd.Series({"Column" : x})
there are several options to make the axis labels more readable.
Change figure size
plt.figure(figsize=(8,4)) # this creates a figure 8 inch wide, 4 inch high
sns.countplot(x="Column", data=ds)
plt.show()
Rotate the ticklabels
ax = sns.countplot(x="Column", data=ds)
ax.set_xticklabels(ax.get_xticklabels(), rotation=40, ha="right")
plt.tight_layout()
plt.show()
Decrease Fontsize
ax = sns.countplot(x="Column", data=ds)
ax.set_xticklabels(ax.get_xticklabels(), fontsize=7)
plt.tight_layout()
plt.show()
Of course any combination of those would work equally well.
Setting rcParams
The figure size and the xlabel fontsize can be set globally using rcParams
plt.rcParams["figure.figsize"] = (8, 4)
plt.rcParams["xtick.labelsize"] = 7
This might be useful to put on top of a juypter notebook such that those settings apply for any figure generated within. Unfortunately rotating the xticklabels is not possible using rcParams.
I guess it's worth noting that the same strategies would naturally also apply for seaborn barplot, matplotlib bar plot or pandas.bar.
You can rotate the x_labels and increase their font size using the xticks methods of pandas.pyplot.
For Example:
import matplotlib.pyplot as plt
plt.figure(figsize=(10,5))
chart = sns.countplot(x="HostRamSize",data=df)
plt.xticks(
rotation=45,
horizontalalignment='right',
fontweight='light',
fontsize='x-large'
)
For more such modifications you can refer this link:
Drawing from Data
If you just want to make sure xticks labels are not squeezed together, you can set a proper fig size and try fig.autofmt_xdate().
This function will automatically align and rotate the labels.
plt.figure(figsize=(15,10)) #adjust the size of plot
ax=sns.countplot(x=df['Location'],data=df,hue='label',palette='mako')
ax.set_xticklabels(ax.get_xticklabels(), rotation=40, ha="right") #it will rotate text on x axis
plt.tight_layout()
plt.show()
you can try this code & change size & rotation according to your need.
I don't know whether it is an option for you but maybe turning the graphic could be a solution (instead of plotting on x=, do it on y=), such that:
sns.countplot(y="HostRamSize",data=df)

Categories

Resources