remove legend handles and labels completely - python

Some plotting tools such as seaborn automatically label plots and add a legend. Removing the legend is fairly easy with ax.get_legend().remove(), but the handles and labels are still stored somewhere in the axes object. Thus when adding another line or other plot type, for which I want to have a legend, the "old" legend handles and labels are also displayed. In other words: ax.get_legend_handles_labels() still returns the labels and handles introduced by f.i. seaborn.
Is there any way to completely remove the handles and labels to be used for a legend from an axis?
Such that ax.get_legend_handles_labels() returns ([], [])?
I know that I can set sns.lineplot(..., legend=False) in seaborn. I am just using seaborn to produce a nice example. The question is how to remove existing legend labels and handles in general.
Here is a minimum working example:
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
df = pd.DataFrame(data={
'x': [0, 1, 0, 1],
'y': [7.2, 10., 11.2, 3.],
'id': [4, 4, 7, 7]
})
# get mean per x location:
grpd = df.groupby('x').mean()
ax = sns.lineplot(data=df, x='x', y='y', hue='id', palette=['b', 'g'])
ax.get_legend().remove()
# this still returns the handles and labels:
print(ax.get_legend_handles_labels())
# Out:
# ([<matplotlib.lines.Line2D at 0x17c1c208f88>,
# <matplotlib.lines.Line2D at 0x17c1c1f8888>,
# <matplotlib.lines.Line2D at 0x17c1c20af88>],
# ['id', '4', '7'])
# plot some other lines, for which I want to have a legend:
ax.plot(grpd.index, grpd['y'], label='mean')
# add legend, ONLY FOR MEAN, if possible:
ax.legend()
I tried manipulating the axes ax.get_legend() and ax.legend_ objects, f.i. with ax.legend_.legendHandles = [], setting the labels, texts and everything else, but the "old" seaborn entries keep reappiering each time.
Since this is meant to be used within a module, it is no applicable to explicitly set a legend only containing the "mean"-entry. I need to "clear" everything happening in the backend to provide a clean API to the user.

Change:
ax = sns.lineplot(data=df, x='x', y='y', hue='id', palette=['b', 'g'])
to:
ax = sns.lineplot(data=df, x='x', y='y', hue='id', palette=['b', 'g'], legend=False)
That way you also don't need to make the call to remove the legend as none is created in the first place.
EDIT UPDATE:
If you want to clear the legend handles and label, I think you'll have to do something like:
for line in ax.lines: # put this before you call the 'mean' plot function.
line.set_label(s='')
This will loop over the lines draw on the plot and then set their label to be empty. That way the legend doesn't consider them and the print(ax.get_legend_handles_labels()) will return empty iterables.

Thanks at mullinscr for the answer considering line plots. To remove legend labels in general, the labels of lines, collections (f.i. scatter plots), patches (boxplots etc) and images (imshow etc.) must be cleared:
for _artist in ax.lines + ax.collections + ax.patches + ax.images:
_artist.set_label(s=None)
Please tell me, if I forgot to mention a specific kind of plot.

Another option is to do the following:
ax.legend(handles=[], labels= [])
In this case, everything will be removed.
If for example you would like to only remove the first handle, you can do the following:
fetch the handles and labels of your plot:
h, l = ax.get_legend_handles_labels()*
select the ones you want (here all except the first):
ax.legend(handles=[item for item in h[1:], labels= [item for item in l[1:])
This way, you can sort out the things you want.

Related

Customizing legend in Seaborn histplot subplots

I am trying to generate a figure with 4 subplots, each of which is a Seaborn histplot. The figure definition lines are:
fig,axes=plt.subplots(2,2,figsize=(6.3,7),sharex=True,sharey=True)
(ax1,ax2),(ax3,ax4)=axes
fig.subplots_adjust(wspace=0.1,hspace=0.2)
I would like to define strings for legend entries in each of the subplots. As an example, I am using the following code for the first subplot:
sp1=sns.histplot(df_dn,x="ktau",hue="statind",element="step", stat="density",common_norm=True,fill=False,palette=colvec,ax=ax1)
ax1.set_title(r'$d_n$')
ax1.set_xlabel(r'max($F_{a,max}$)')
ax1.set_ylabel(r'$\tau_{ken}$')
legend_labels,_=ax1.get_legend_handles_labels()
ax1.legend(legend_labels,['dep-','ind-','ind+','dep+'],title='Stat.ind.')
The legend is not showing correctly (legend entries are not plotted and the legend title is the name of the hue variable ("statind"). Please note I have successfully used the same code for other figures in which I used Seaborn relplots instead of histplots.
The main problem is that ax1.get_legend_handles_labels() returns empty lists (note that the first return value are the handles, the second would be the labels). At least for the current (0.11.1) version of seaborn's histplot().
To get the handles, you can do legend = ax1.get_legend(); handles = legend.legendHandles.
To recreate the legend, first the existing legend needs to be removed. Then, the new legend can be created starting from some handles.
Also note that to be sure of the order of the labels, it helps to set hue_order. Here is some example code to show the ideas:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
df_dn = pd.DataFrame({'ktau': np.random.randn(4000).cumsum(),
'statind': np.repeat([*'abcd'], 1000)})
fig, ax1 = plt.subplots()
sp1 = sns.histplot(df_dn, x="ktau", hue="statind", hue_order=['a', 'b', 'c', 'd'],
element="step", stat="density", common_norm=True, fill=False, ax=ax1)
ax1.set_title(r'$d_n$')
ax1.set_xlabel(r'max($F_{a,max}$)')
ax1.set_ylabel(r'$\tau_{ken}$')
legend = ax1.get_legend()
handles = legend.legendHandles
legend.remove()
ax1.legend(handles, ['dep-', 'ind-', 'ind+', 'dep+'], title='Stat.ind.')
plt.show()

Edit legend title and labels of Seaborn scatterplot and countplot

I am using seaborn scatterplot and countplot on titanic dataset.
Here is my code to draw scatter plot. I also tried to edit legend label.
ax = seaborn.countplot(x='class', hue='who', data=titanic)
legend_handles, _ = ax.get_legend_handles_labels()
plt.show();
To edit legend label, I did this. In this case, there is no legend title anymore. How can I rename this title from 'who' to 'who1'?
ax = seaborn.countplot(x='class', hue='who', data=titanic)
legend_handles, _= ax.get_legend_handles_labels()
ax.legend(legend_handles, ['man1','woman1','child1'], bbox_to_anchor=(1,1))
plt.show()
I used the same method to edit legend labels on scatter plot and the result is different here. It uses 'dead' as legend title and use 'survived' as first legend label.
ax = seaborn.scatterplot(x='age', y='fare', data=titanic, hue = 'survived')
legend_handles, _= ax.get_legend_handles_labels()
ax.legend(legend_handles, ['dead', 'survived'],bbox_to_anchor=(1.26,1))
plt.show()
Is there a parameter to delete and add legend title?
I used same codes on two different graphs and outcome of legend is different. Why is that?
Try using
ax.legend(legend_handles, ['man1','woman1','child1'],
bbox_to_anchor=(1,1),
title='whatever title you want to use')
With seaborn v0.11.2 or later, use the move_legend() function.
From the FAQs page:
With seaborn v0.11.2 or later, use the move_legend() function.
On older versions, a common pattern was to call ax.legend(loc=...) after plotting. While this appears to move the legend, it actually replaces it with a new one, using any labeled artists that happen to be attached to the axes. This does not consistently work across plot types. And it does not propagate the legend title or positioning tweaks that are used to format a multi-variable legend.
The move_legend() function is actually more powerful than its name suggests, and it can also be used to modify other legend parameters (font size, handle length, etc.) after plotting.
Why does the legend order sometimes differ?
You can force the order of the legend via hue_order=['man', 'woman', 'child']. By default, the order is either the order in which they appear in the dataframe (when the values are just strings), or the order imposed by pd.Categorical.
How to rename the legend entries
The surest way is to rename the column values, e.g.
titanic["who"] = titanic["who"].map({'man': 'Man1', 'woman': 'Woman1', 'child': 'Child1'})
If the entries of the column exist of numbers in the range 0,1,..., you can use pd.Categorical.from_codes(...). This also forces an order.
Specific colors for specific hue values
There are many options to specify the colors to be used (via palette=). To assign a specific color to a specific hue value, the palette can be a dictionary, e.g.
palette = {'Man1': 'cornflowerblue', 'Woman1': 'fuchsia', 'Child1': 'limegreen'}
Renaming or removing the legend title
sns.move_legend(ax, title=..., loc='best') sets a new title. Setting the title to an empty string removes it (this is useful when the entries are self-explaining).
A code example
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
titanic = sns.load_dataset('titanic')
# titanic['survived'] = titanic['survived'].map({0:'Dead', 1:'Survived'})
titanic['survived'] = pd.Categorical.from_codes(titanic['survived'], ['Dead', 'Survived'])
palette = {'Dead': 'navy', 'Survived': 'turquoise'}
ax = sns.scatterplot(data=titanic, x='age', y='fare', hue='survived', palette=palette)
sns.move_legend(ax, title='', loc='best') # remove the title
plt.show()

Changing pointplot legend in seaborn

I would like to change the label for the legend and items in the legend for this plot. Right now the label for the legend is "Heart" and the items are 0 and 1. I would like to be able to change all of these to something else, but am unsure how. Here is what I have so far.
sns.set_context("talk",font_scale=3)
ax =sns.pointplot(x="Heart", y="FirstPersonPronouns", hue="Speech", data=df)
ax.set(xlabel='Condition', ylabel='First Person Pronouns')
ax.set(xticklabels=["Control", "Heart"])
Any help would be appreciated! Also, I'm assuming this is a set parameter that I don't know about, is there a comprehensive list of these? I can't seem to find one in the documentation.
An alternative to changing the column names of the data frame, is to create a new legend using the same legend handles (this is what determines the colored markers), but with new text labels:
import seaborn as sns
tips = sns.load_dataset('tips')
ax = sns.pointplot(x='sex', y='total_bill', hue='time', data=tips)
leg_handles = ax.get_legend_handles_labels()[0]
ax.legend(leg_handles, ['Blue', 'Orange'], title='New legend')

Controlling tick labels alignment in pandas Boxplot within subplots

For a single boxplot, the tick labels alignment can be controlled like so:
import matplotlib.pyplot as plt
import matplotlib as mpl
%matplotlib inline
fig,ax = plt.subplots()
df.boxplot(column='col1',by='col2',rot=45,ax=ax)
plt.xticks(ha='right')
This is necessary because when the tick labels are long, it is impossible to read the plot if the tick labels are centered (the default behavior).
Now on to the case of multiple subplots. (I am sorry I am not posting a complete code example). I build the main figure first:
fig,axarr = plt.subplots(ny,nx,sharex=True,sharey=True,figsize=(12,6),squeeze=False)
then comes a for loop that iterates over all the subplot axes and calls a function that draws a boxplot in each of the axes objects:
for key,gr in grouped:
ix = i/ny # Python 2
iy = i%ny
add_box_plot(gr,xcol,axarr[iy,ix])
where
def add_box_plot(gs,xcol,ax):
gs.boxplot(column=xcol,by=keyCol,rot=45,ax=ax)
I have not found a way to get properly aligned tick labels.
If I add
plt.xticks(ha='right')
after the boxplot command in the function, only the last subplot gets the ticks aligned correctly (why?).
If I add plt.xticks(ha='right') after the boxplot command in the function, only the last subplot gets the ticks aligned correctly (why?).
This happens because plt.xticks refers to the last active axes. When you crate subplots, the one created last is active. You then access the axes opbjects directly(although they are called gs or gr in your code, whatever that means). However, this does not change the active axis.
Solution 1:
Use plt.sca() to set the current axis:
def add_box_plot(gs, xcol, ax):
gs.boxplot(column=xcol, by=keyCol, rot=45, ax=ax)
plt.sca(ax)
plt.xticks(ha='right')
Solution 2:
Use Axes.set_xticklabels() instead:
def add_box_plot(gs, xcol, ax):
gs.boxplot(column=xcol,by=keyCol,rot=45,ax=ax)
plt.draw() # This may be required to update the labels
labels = [l.get_text() for l in ax.get_xticklabels()]
ax.set_xticklabels(labels, ha='right')
I'm not sure if the call to plt.draw() is always required, but if I leave it out I only get empty labels.
Since you are using the mpl object oriented interface, you can set the tick parameters for each axis individually.
add a line to set the xticklabels within your add_box_plot function (after gs.boxplot). Unlike plt.xticks, you cannot just give set_xticklabels the ha keyword, it also requires you to give it a list of tick labels. Here, we can just grab the existing labels with get_xticklabels:
def add_box_plot(gs,xcol,ax):
gs.boxplot(column=xcol,by=keyCol,rot=45,ax=ax)
ax.set_xticklabels(ax.get_xticklabels(),ha='right')
Here's a minimal example to show this working:
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
# We'll create two subplots, to test out different alignments
fig,(ax1,ax2) = plt.subplots(2)
# A sample dataframe
df = pd.DataFrame(np.random.rand(10, 5), columns=['A', 'B', 'C', 'D', 'E'])
# Boxplot on first subplot
df.boxplot(ax=ax1)
# Boxplot on second subplot
df.boxplot(ax=ax2)
# Set xticklabels to right alignment
ax1.set_xticklabels(ax1.get_xticklabels(),ha='right')
# Set xticklabels to left alignment
ax2.set_xticklabels(ax2.get_xticklabels(),ha='left')
plt.show()
Notice the xticklabels are right-aligned on the top subplot, and left-aligned on the bottom.

How do I make a single legend for many subplots?

I am plotting the same type of information, but for different countries, with multiple subplots with Matplotlib. That is, I have nine plots on a 3x3 grid, all with the same for lines (of course, different values per line).
However, I have not figured out how to put a single legend (since all nine subplots have the same lines) on the figure just once.
How do I do that?
There is also a nice function get_legend_handles_labels() you can call on the last axis (if you iterate over them) that would collect everything you need from label= arguments:
handles, labels = ax.get_legend_handles_labels()
fig.legend(handles, labels, loc='upper center')
figlegend may be what you're looking for: matplotlib.pyplot.figlegend
An example is at Figure legend demo.
Another example:
plt.figlegend(lines, labels, loc = 'lower center', ncol=5, labelspacing=0.)
Or:
fig.legend(lines, labels, loc = (0.5, 0), ncol=5)
TL;DR
lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
fig.legend(lines, labels)
I have noticed that none of the other answers displays an image with a single legend referencing many curves in different subplots, so I have to show you one... to make you curious...
Now, if I've teased you enough, here it is the code
from numpy import linspace
import matplotlib.pyplot as plt
# each Axes has a brand new prop_cycle, so to have differently
# colored curves in different Axes, we need our own prop_cycle
# Note: we CALL the axes.prop_cycle to get an itertoools.cycle
color_cycle = plt.rcParams['axes.prop_cycle']()
# I need some curves to plot
x = linspace(0, 1, 51)
functs = [x*(1-x), x**2*(1-x),
0.25-x*(1-x), 0.25-x**2*(1-x)]
labels = ['$x-x²$', '$x²-x³$',
'$\\frac{1}{4} - (x-x²)$', '$\\frac{1}{4} - (x²-x³)$']
# the plot,
fig, (a1,a2) = plt.subplots(2)
for ax, f, l, cc in zip((a1,a1,a2,a2), functs, labels, color_cycle):
ax.plot(x, f, label=l, **cc)
ax.set_aspect(2) # superfluos, but nice
# So far, nothing special except the managed prop_cycle. Now the trick:
lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
# Finally, the legend (that maybe you'll customize differently)
fig.legend(lines, labels, loc='upper center', ncol=4)
plt.show()
If you want to stick with the official Matplotlib API, this is
perfect, otherwise see note no.1 below (there is a private
method...)
The two lines
lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
deserve an explanation, see note 2 below.
I tried the method proposed by the most up-voted and accepted answer,
# fig.legend(lines, labels, loc='upper center', ncol=4)
fig.legend(*a2.get_legend_handles_labels(),
loc='upper center', ncol=4)
and this is what I've got
Note 1
If you don't mind using a private method of the matplotlib.legend module ... it's really much much much easier
from matplotlib.legend import _get_legend_handles_labels
...
fig.legend(*_get_legend_handles_and_labels(fig.axes), ...)
Note 2
I have encapsulated the two tricky lines in a function, just four lines of code, but heavily commented
def fig_legend(fig, **kwdargs):
# Generate a sequence of tuples, each contains
# - a list of handles (lohand) and
# - a list of labels (lolbl)
tuples_lohand_lolbl = (ax.get_legend_handles_labels() for ax in fig.axes)
# E.g., a figure with two axes, ax0 with two curves, ax1 with one curve
# yields: ([ax0h0, ax0h1], [ax0l0, ax0l1]) and ([ax1h0], [ax1l0])
# The legend needs a list of handles and a list of labels,
# so our first step is to transpose our data,
# generating two tuples of lists of homogeneous stuff(tolohs), i.e.,
# we yield ([ax0h0, ax0h1], [ax1h0]) and ([ax0l0, ax0l1], [ax1l0])
tolohs = zip(*tuples_lohand_lolbl)
# Finally, we need to concatenate the individual lists in the two
# lists of lists: [ax0h0, ax0h1, ax1h0] and [ax0l0, ax0l1, ax1l0]
# a possible solution is to sum the sublists - we use unpacking
handles, labels = (sum(list_of_lists, []) for list_of_lists in tolohs)
# Call fig.legend with the keyword arguments, return the legend object
return fig.legend(handles, labels, **kwdargs)
I recognize that sum(list_of_lists, []) is a really inefficient method to flatten a list of lists, but ① I love its compactness, ② usually is a few curves in a few subplots and ③ Matplotlib and efficiency? ;-)
For the automatic positioning of a single legend in a figure with many axes, like those obtained with subplots(), the following solution works really well:
plt.legend(lines, labels, loc = 'lower center', bbox_to_anchor = (0, -0.1, 1, 1),
bbox_transform = plt.gcf().transFigure)
With bbox_to_anchor and bbox_transform=plt.gcf().transFigure, you are defining a new bounding box of the size of your figureto be a reference for loc. Using (0, -0.1, 1, 1) moves this bounding box slightly downwards to prevent the legend to be placed over other artists.
OBS: Use this solution after you use fig.set_size_inches() and before you use fig.tight_layout()
You just have to ask for the legend once, outside of your loop.
For example, in this case I have 4 subplots, with the same lines, and a single legend.
from matplotlib.pyplot import *
ficheiros = ['120318.nc', '120319.nc', '120320.nc', '120321.nc']
fig = figure()
fig.suptitle('concentration profile analysis')
for a in range(len(ficheiros)):
# dados is here defined
level = dados.variables['level'][:]
ax = fig.add_subplot(2,2,a+1)
xticks(range(8), ['0h','3h','6h','9h','12h','15h','18h','21h'])
ax.set_xlabel('time (hours)')
ax.set_ylabel('CONC ($\mu g. m^{-3}$)')
for index in range(len(level)):
conc = dados.variables['CONC'][4:12,index] * 1e9
ax.plot(conc,label=str(level[index])+'m')
dados.close()
ax.legend(bbox_to_anchor=(1.05, 0), loc='lower left', borderaxespad=0.)
# it will place the legend on the outer right-hand side of the last axes
show()
If you are using subplots with bar charts, with a different colour for each bar, it may be faster to create the artefacts yourself using mpatches.
Say you have four bars with different colours as r, m, c, and k, you can set the legend as follows:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
labels = ['Red Bar', 'Magenta Bar', 'Cyan Bar', 'Black Bar']
#####################################
# Insert code for the subplots here #
#####################################
# Now, create an artist for each color
red_patch = mpatches.Patch(facecolor='r', edgecolor='#000000') # This will create a red bar with black borders, you can leave out edgecolor if you do not want the borders
black_patch = mpatches.Patch(facecolor='k', edgecolor='#000000')
magenta_patch = mpatches.Patch(facecolor='m', edgecolor='#000000')
cyan_patch = mpatches.Patch(facecolor='c', edgecolor='#000000')
fig.legend(handles = [red_patch, magenta_patch, cyan_patch, black_patch], labels=labels,
loc="center right",
borderaxespad=0.1)
plt.subplots_adjust(right=0.85) # Adjust the subplot to the right for the legend
To build on top of gboffi's and Ben Usman's answer:
In a situation where one has different lines in different subplots with the same color and label, one can do something along the lines of:
labels_handles = {
label: handle for ax in fig.axes for handle, label in zip(*ax.get_legend_handles_labels())
}
fig.legend(
labels_handles.values(),
labels_handles.keys(),
loc = "upper center",
bbox_to_anchor = (0.5, 0),
bbox_transform = plt.gcf().transFigure,
)
Using Matplotlib 2.2.2, this can be achieved using the gridspec feature.
In the example below, the aim is to have four subplots arranged in a 2x2 fashion with the legend shown at the bottom. A 'faux' axis is created at the bottom to place the legend in a fixed spot. The 'faux' axis is then turned off so only the legend shows. Result:
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
# Gridspec demo
fig = plt.figure()
fig.set_size_inches(8, 9)
fig.set_dpi(100)
rows = 17 # The larger the number here, the smaller the spacing around the legend
start1 = 0
end1 = int((rows-1)/2)
start2 = end1
end2 = int(rows-1)
gspec = gridspec.GridSpec(ncols=4, nrows=rows)
axes = []
axes.append(fig.add_subplot(gspec[start1:end1, 0:2]))
axes.append(fig.add_subplot(gspec[start2:end2, 0:2]))
axes.append(fig.add_subplot(gspec[start1:end1, 2:4]))
axes.append(fig.add_subplot(gspec[start2:end2, 2:4]))
axes.append(fig.add_subplot(gspec[end2, 0:4]))
line, = axes[0].plot([0, 1], [0, 1], 'b') # Add some data
axes[-1].legend((line,), ('Test',), loc='center') # Create legend on bottommost axis
axes[-1].set_axis_off() # Don't show the bottom-most axis
fig.tight_layout()
plt.show()
This answer is a complement to user707650's answer on the legend position.
My first try on user707650's solution failed due to overlaps of the legend and the subplot's title.
In fact, the overlaps are caused by fig.tight_layout(), which changes the subplots' layout without considering the figure legend. However, fig.tight_layout() is necessary.
In order to avoid the overlaps, we can tell fig.tight_layout() to leave spaces for the figure's legend by fig.tight_layout(rect=(0,0,1,0.9)).
Description of tight_layout() parameters.
All of the previous answers are way over my head, at this state of my coding journey, so I just added another Matplotlib aspect called patches:
import matplotlib.patches as mpatches
first_leg = mpatches.Patch(color='red', label='1st plot')
second_leg = mpatches.Patch(color='blue', label='2nd plot')
thrid_leg = mpatches.Patch(color='green', label='3rd plot')
plt.legend(handles=[first_leg ,second_leg ,thrid_leg ])
The patches aspect put all the data i needed on my final plot (it was a line plot that combined three different line plots all in the same cell in Jupyter Notebook).
Result
(I changed the names form what I named my own legend.)

Categories