comment créer une seule légende pour plusieurs sous-parcelles avec matplotlib?

166

Je trace le même type d'informations, mais pour différents pays, avec plusieurs sous-graphiques avec matplotlib. Autrement dit, j'ai 9 tracés sur une grille 3x3, tous avec la même chose pour les lignes (bien sûr, des valeurs différentes par ligne).

Cependant, je n'ai pas compris comment mettre une seule légende (puisque les 9 sous-graphiques ont les mêmes lignes) sur la figure une seule fois.

Comment je fais ça?

poche de fromage
la source

Réponses:

161

Il existe également une fonction intéressante que get_legend_handles_labels()vous pouvez appeler sur le dernier axe (si vous les parcourez) qui collecterait tout ce dont vous avez besoin à partir des label=arguments:

handles, labels = ax.get_legend_handles_labels()
fig.legend(handles, labels, loc='upper center')
Ben Usman
la source
13
Cela devrait être la meilleure réponse.
naught101
1
C'est en effet une réponse bien plus utile! Cela a fonctionné comme ça dans un cas plus compliqué pour moi.
gmaravel
1
réponse parfaite!
Dorgham
4
Comment supprimer la légende des sous-graphiques?
BND
5
Juste pour ajouter à cette excellente réponse. Si vous avez un axe y secondaire sur vos parcelles et que vous devez les fusionner, utilisez ceci:handles, labels = [(a + b) for a, b in zip(ax1.get_legend_handles_labels(), ax2.get_legend_handles_labels())]
Bill
114

figlegend est peut-être ce que vous recherchez: http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.figlegend

Exemple ici: http://matplotlib.org/examples/pylab_examples/figlegend_demo.html

Un autre exemple:

plt.figlegend( lines, labels, loc = 'lower center', ncol=5, labelspacing=0. )

ou:

fig.legend( lines, labels, loc = (0.5, 0), ncol=5 )
Nathan Musoke
la source
1
Je connais les lignes que je veux mettre dans la légende, mais comment obtenir la linesvariable pour laquelle placer l'argument legend?
patapouf_ai
1
@patapouf_ai linesest une liste de résultats renvoyés par axes.plot()(c'est-à-dire que chaque axes.plotroutine ou similaire renvoie une "ligne"). Voir également l'exemple lié.
17

Pour le positionnement automatique d'une seule légende dans un figureà plusieurs axes, comme ceux obtenus avec subplots(), la solution suivante fonctionne très bien:

plt.legend( lines, labels, loc = 'lower center', bbox_to_anchor = (0,-0.1,1,1),
            bbox_transform = plt.gcf().transFigure )

Avec bbox_to_anchoret, bbox_transform=plt.gcf().transFigurevous définissez une nouvelle boîte englobante de la taille de votre figurecomme référence loc. L'utilisation (0,-0.1,1,1)déplace cette boîte de boudinage légèrement vers le bas pour éviter que la légende ne soit placée sur d'autres artistes.

OBS: utilisez cette solution APRÈS avoir utilisé fig.set_size_inches()et AVANT d'utiliserfig.tight_layout()

Saullo GP Castro
la source
1
Ou simpy loc='upper center', bbox_to_anchor=(0.5, 0), bbox_transform=plt.gcf().transFigureet il ne se chevauchera pas à coup sûr.
Davor Josipovic
2
Je ne sais toujours pas pourquoi, mais la solution d'Evert n'a pas fonctionné pour moi - la légende n'arrêtait pas de se couper. Cette solution (avec le commentaire de davor) a fonctionné très proprement - la légende a été placée comme prévu et entièrement visible. Merci!
sudo make install
16

Vous n'avez qu'à demander la légende une fois, en dehors de votre boucle.

Par exemple, dans ce cas, j'ai 4 sous-graphiques, avec les mêmes lignes et une seule légende.

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()
carla
la source
3
figlegend, comme suggéré par Evert, semble être une bien meilleure solution;)
carla
11
le problème fig.legend()est qu'il nécessite une identification pour toutes les lignes (parcelles) ... car, pour chaque sous-graphique, j'utilise une boucle pour générer les lignes, la seule solution que j'ai trouvée pour surmonter cela est de créer une liste vide avant la deuxième boucle, puis ajoutez les lignes au fur et à mesure de leur création ... Ensuite, j'utilise cette liste comme argument de la fig.legend()fonction.
carla
Une question similaire ici
emmmphd
Qu'y a-t- dadosil?
Shyamkkhadka
1
@Shyamkkhadka, dans mon script d'origine dadosétait un ensemble de données d'un fichier netCDF4 (pour chacun des fichiers définis dans la liste ficheiros). Dans chaque boucle, un fichier différent est lu et un sous-tracé est ajouté à la figure.
carla
14

J'ai remarqué qu'aucune réponse n'affiche une image avec une seule légende référençant de nombreuses courbes dans différentes sous-parcelles, donc je dois vous en montrer une ... pour vous rendre curieux ...

entrez la description de l'image ici

Maintenant, vous voulez regarder le code, n'est-ce pas?

from numpy import linspace
import matplotlib.pyplot as plt

# Calling the axes.prop_cycle returns an itertoools.cycle

color_cycle = plt.rcParams['axes.prop_cycle']()

# I need some curves to plot

x = linspace(0, 1, 51)
f1 = x*(1-x)   ; lab1 = 'x - x x'
f2 = 0.25-f1   ; lab2 = '1/4 - x + x x' 
f3 = x*x*(1-x) ; lab3 = 'x x - x x x'
f4 = 0.25-f3   ; lab4 = '1/4 - x x + x x x'

# let's plot our curves (note the use of color cycle, otherwise the curves colors in
# the two subplots will be repeated and a single legend becomes difficult to read)
fig, (a13, a24) = plt.subplots(2)

a13.plot(x, f1, label=lab1, **next(color_cycle))
a13.plot(x, f3, label=lab3, **next(color_cycle))
a24.plot(x, f2, label=lab2, **next(color_cycle))
a24.plot(x, f4, label=lab4, **next(color_cycle))

# so far so good, 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 we invoke the legend (that you probably would like to customize...)

fig.legend(lines, labels)
plt.show()

Les deux lignes

lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]

mérite une explication - dans ce but, j'ai encapsulé la partie délicate dans une fonction, juste 4 lignes de code mais fortement commenté

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

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

PS Je reconnais que sum(list_of_lists, [])c'est une méthode vraiment inefficace pour aplatir une liste de listes mais ① j'aime sa compacité, ② est généralement quelques courbes dans quelques sous-graphiques et ③ Matplotlib et l'efficacité? ;-)

gboffi
la source
3

Bien qu'assez tardif dans le jeu, je vais donner une autre solution ici car c'est toujours l'un des premiers liens à apparaître sur Google. En utilisant matplotlib 2.2.2, cela peut être réalisé en utilisant la fonction gridspec. Dans l'exemple ci-dessous, le but est d'avoir quatre sous-graphiques disposés en 2x2 avec la légende affichée en bas. Un axe «faux» est créé en bas pour placer la légende à un endroit fixe. L'axe «faux» est alors désactivé pour que seule la légende s'affiche. Résultat: https://i.stack.imgur.com/5LUWM.png .

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 bottommost axis

fig.tight_layout()
plt.show()
gigo318
la source
3

si vous utilisez des sous-graphiques avec des graphiques à barres, avec une couleur différente pour chaque barre. il peut être plus rapide de créer vous-même les artefacts en utilisantmpatches

Supposons que vous ayez quatre barres de couleurs différentes, car r m c kvous pouvez définir la légende comme suit

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
Chidi
la source
1
+1 Le meilleur! Je l'ai utilisé de cette manière en ajoutant directement au plt.legendpour avoir une légende pour tous mes sous
Utilisateur
Il est plus rapide de combiner les poignées automatiques et les étiquettes faites à la main handles, _ = plt.gca().get_legend_handles_labels()fig.legend(handles, labels)
:,
1

Cette réponse est un complément à @ Evert sur la position de la légende.

Mon premier essai sur la solution de @ Evert a échoué en raison de chevauchements de la légende et du titre de l'intrigue secondaire.

En fait, les chevauchements sont causés par fig.tight_layout(), qui modifie la disposition des sous-graphiques sans tenir compte de la légende de la figure. Cependant, fig.tight_layout()c'est nécessaire.

Afin d'éviter les chevauchements, nous pouvons dire fig.tight_layout()de laisser des espaces pour la légende de la figure par fig.tight_layout(rect=(0,0,1,0.9)).

Description des paramètres tight_layout () .

laven_qa
la source
1

Pour s'appuyer sur la réponse de @ gboffi et de Ben Usman:

Dans une situation où l'on a des lignes différentes dans différentes sous-parcelles avec la même couleur et la même étiquette, on peut faire quelque chose du genre

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,
)
Heiner
la source