How to Make a Single Legend for Many Subplots with Matplotlib

How do I make a single legend for many subplots?

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

Matplotlib how to add global legend for subplot of histograms

Legends for multiple graphs can be set with fig.legend(). The placement criteria can be fig with bbox_transform, and the display in three columns can be set with ncol. I set it to the lower right, but you can set it to the lower left with loc='lower left'.

fig.legend(labels, loc='lower right', bbox_to_anchor=(1,-0.1), ncol=len(labels), bbox_transform=fig.transFigure)

Sample Image

How do I make a SINGLE legend using subplots? _get_legend_handles_labels is not working

First, I would suggest to save all information into lists, so the plot can be made via a large loop. That way, if some detail changes, it only needs to be changed at one spot.

To create a legend, graphical elements that have a "label" will be added automatically. Normally, a complete bar plot only gets one label. By diving into the generated bars, individual labels can be assigned.

The code first creates a dummy legend, so fig.tight_layout() can adapt all the spacings and leave some place for the legend. After calling fig.tight_layout(), the real legend is created. (With the real legend, fig.tight_layout() would try to assign it completely to one subplot, and create a wide gap between the two columns of subplots).

import matplotlib.pyplot as plt
import numpy as np

colors = ['red', 'blue', 'darkblue', 'purple', 'orange', 'brown', 'pink', 'darkgreen', 'gray']
models = ['Logistic Regression', 'SVM', 'Decision Tree', 'Random Forest', 'XGBoost', 'ADABoost', 'Gaussian NB', 'KNN', 'MLP']

titles = ["F1 score", "accuracy score", "recall score", "precision score"]

N = len(models)
en_f1_scores = np.random.rand(N)
en_acc_scores = np.random.rand(N)
en_recall_scores = np.random.rand(N)
en_precision_scores = np.random.rand(N)
en_scores = [en_f1_scores, en_acc_scores, en_recall_scores, en_precision_scores]
ar_f1_scores = np.random.rand(N)
ar_acc_scores = np.random.rand(N)
ar_recall_scores = np.random.rand(N)
ar_precision_scores = np.random.rand(N)
ar_scores = [ar_f1_scores, ar_acc_scores, ar_recall_scores, ar_precision_scores]

fig, axs = plt.subplots(4, 2, figsize=(25, 15), sharex=True, sharey='row')
fig.suptitle('Performance measures for English and Arabic data', fontsize=25)

for axs_row, en_score, ar_score, title in zip(axs, en_scores, ar_scores, titles):
for language, score, ax in zip(['English', 'Arabic'], [en_score, ar_score], axs_row):
ax.bar(models, score, color=colors)
ax.set_title(language + ' ' + title, fontsize=20)
ax.set_xticks([]) # remove the x tick and their labels
ax.grid(axis='y', ls=':', color='black') # add some gridlines
ax.set_axisbelow(True) # gridlines behind the bars
for spine in ['top', 'right', 'left']: # remove part of the surrounding box, as it gets busy with the grid lines
ax.spines[spine].set_visible(False)
ax.margins(x=0.01) # less white space left and right

# the legend is created for each graphical element that has a "label"
for bar, model in zip(axs[0, 0].containers[0], models):
bar.set_label(model)
# first create a dummy legend, so fig.tight_layout() makes enough space
axs[0, 0].legend(handles=axs[0, 0].containers[0][:1],
bbox_to_anchor=(0, 1.12), loc='lower left')
fig.tight_layout(pad=3.0)
# now create the real legend; if fig.tight_layout() were called on this,
# it would create a large empty space between the columns of subplots
# as it wants the legend to belong to only one of the subplots
axs[0, 0].legend(handles=axs[0, 0].containers[0], ncol=len(models),
bbox_to_anchor=(1.03, 1.12), loc='lower center', fontsize=18)
plt.show()

common legend for 8 subplots

Create a single legend for multiple plot in matplotlib, seaborn

To position the legend, it is important to set the loc parameter, being the anchor point. (The default loc is 'best' which means you don't know beforehand where it would end up). The positions are measured from 0,0 being the lower left of the current ax, to 1,1: the upper left of the current ax. This doesn't include the padding for titles etc., so the values can go a bit outside the 0, 1 range. The "current ax" is the last one that was activated.

Note that instead of plt.legend (which uses an axes), you could also use plt.gcf().legend which uses the "figure". Then, the coordinates are 0,0 in lower left corner of the complete plot (the "figure") and 1,1 in the upper right. A drawback would be that no extra space would be created for the legend, so you'd need to manually set a top padding (e.g. plt.gcf().subplots_adjust(top=0.8)). A drawback would be that you can't use plt.tight_layout() anymore, and that it would be harder to align the legend with the axes.

import seaborn as sns
from matplotlib import pyplot as plt
from matplotlib import patches as mpatches
import pandas as pd

dataset = sns.load_dataset("iris")

# Reindex the dataset by species so it can be pivoted for each species
reindexed_dataset = dataset.set_index(dataset.groupby('species').cumcount())
cols_to_pivot = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width']

# empty dataframe
reshaped_dataset = pd.DataFrame()
for var_name in cols_to_pivot:
pivoted_dataset = reindexed_dataset.pivot(columns='species', values=var_name).rename_axis(None, axis=1)
pivoted_dataset['measurement'] = var_name
reshaped_dataset = reshaped_dataset.append(pivoted_dataset, ignore_index=True)

## Now, lets spit the dataframe into groups by-measurements.
grouped_dfs_02 = []
for group in reshaped_dataset.groupby('measurement'):
grouped_dfs_02.append(group[1])

## make the box plot of several measured variables, compared between species
plt.figure(figsize=(20, 5), dpi=80)
plt.suptitle('Distribution of floral traits in the species of iris')

sp_name = ['Iris-setosa', 'Iris-versicolor', 'Iris-virginica']
setosa = mpatches.Patch(color='red')
versi = mpatches.Patch(color='green')
virgi = mpatches.Patch(color='blue')

my_pal = {"versicolor": "g", "setosa": "r", "virginica": "b"}
plt_index = 0

# for i, df in enumerate(grouped_dfs_02):
for group_name, df in reshaped_dataset.groupby('measurement'):
axi = plt.subplot(1, len(grouped_dfs_02), plt_index + 1)
sp_name = ['Iris-setosa', 'Iris-versicolor', 'Iris-virginica']
df_melt = df.melt('measurement', var_name='species', value_name='values')

sns.boxplot(data=df_melt, x='species', y='values', ax=axi, orient="v", palette=my_pal)
plt.title(group_name)
plt_index += 1

# Move the legend to an empty part of the plot
plt.legend(title='species', labels=sp_name,
handles=[setosa, versi, virgi], bbox_to_anchor=(1, 1.23),
fancybox=True, shadow=True, ncol=5, loc='upper right')
plt.tight_layout()
plt.show()

resulting plot

One legend for all subplots in pyplot

This worked for me, you essentially capture the patch handles for each graph plotted and manually create a legend at the end.

import pylab as plt
import numpy as NP

plt.figure(figsize=(14,8), dpi=72)
plt.gcf().suptitle(r'Difference between TI and $\lambda$D', size=16)
# Subplot 1
ax1 = plt.subplot2grid((1,3),(0,0),colspan=2)
N = 100
LE_x = NP.random.rand(N)
LE_y = NP.random.rand(N)
MD_x = NP.random.rand(N)
MD_y = NP.random.rand(N)

# Plot scattered data in first subplot
s1 = plt.scatter(LE_x, LE_y, s=40, lw=0, color='gold', marker='o', label=r'$\lambda$D')
s2 = plt.scatter(MD_x, MD_y, s=40, lw=0, color='blue', marker='^', label=r'TI')

data = NP.random.randn(1000)
LE_hist, bins2 = NP.histogram(data, 50)

data = NP.random.randn(1000)
MD_hist, bins2 = NP.histogram(data, 50)
# Subplot 2
ax2 = plt.subplot2grid((1,3),(0,2))
vpos1 = NP.arange(0, len(LE_hist))
vpos2 = NP.arange(0, len(MD_hist)) + 0.5
h1 = plt.barh(vpos1, LE_hist, height=0.5, color='gold', label=r'$\lambda$D')
h2 = plt.barh(vpos2, MD_hist, height=0.5, color='blue', label=r'TI')

# Legend
#legend = plt.legend()
lgd = plt.legend((s1, s2, h1, h2), (r'$\lambda$D', r'TI', r'$\lambda$D', r'TI'), loc='upper center')
plt.show()

Result

Common legend for subplot matplotlib

Of course you need to show the legend on one of the subplots. It's your decision which one you chose.

In order to show all four lines in the legend, you need to provide a reference to the lines to the legend

plt.legend(handles = [line1, line2, ...])

See also the Matplotlib legend guide.

So here is a working example

import numpy as np
import matplotlib.pyplot as plt

x = np.random.randint(0,12,size=(12,4))
y = np.random.randint(0,8,size=(12,4))

fig, (ax, ax2, ax3) = plt.subplots(3, 1, sharex=True, figsize=(5,5))

l, = ax.plot(x[:,0],y[:,0], marker = 'o', label='1')
l2, =ax2.plot(x[:,1],y[:,1], marker = 'o', label='2',color='r')
l3, =ax2.plot(x[:,2],y[:,2], marker = 'o', label='3',color='turquoise')
l4, =ax3.plot(x[:,3],y[:,3], marker = 'o', label='4',color='g')

plt.legend( handles=[l, l2, l3, l4],loc="upper left", bbox_to_anchor=[0, 1],
ncol=2, shadow=True, title="Legend", fancybox=True)

plt.show()

Sample Image

2 Plots sharing a common and single legend

This is an alternate way to do it. The idea is to create two axis objects and then pass them to plot the two DataFrames. Then use legend=False for the first plot as the legends of both the plots are the same. Now you can set the position of the common legend outside the figure using loc.


Complete working answer

import pandas as pd
import matplotlib.pyplot as plt

fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(9, 3))

df1=pd.DataFrame({'Dates' : pd.date_range('2002-02-04',periods=3),
'fac1' : [1,1.02,0.98],'fac2':[2,2.05,2.06]})

df2=pd.DataFrame({'Dates' : pd.date_range('2002-02-04',periods=3),
'fac1' : [1,0.95,1.10], 'fac2':[0.5,0.6,0.55]})

df1.plot(x=df1.iloc[:,0].name, y=df1.iloc[:,1:3].columns, legend=False, title='jup', ax=ax[0])

df2.plot(x=df2.iloc[:,0].name, y=df2.iloc[:,1:3].columns, legend=True, title='tit', ax=ax[1])

ax[1].legend(loc=(1.1, 0.5))
plt.show()

Sample Image



Related Topics



Leave a reply



Submit