como faço para criar uma única legenda para muitas subtramas com matplotlib?

166

Estou plotando o mesmo tipo de informação, mas para países diferentes, com várias subparcelas com matplotlib. Ou seja, tenho 9 gráficos em uma grade 3x3, todos iguais para linhas (é claro, valores diferentes por linha).

No entanto, eu não descobri como colocar uma única legenda (uma vez que todas as 9 subparcelas têm as mesmas linhas) na figura apenas uma vez.

Como faço isso?

pocketfullofcheese
fonte

Respostas:

160

Também há uma função interessante que get_legend_handles_labels()você pode chamar no último eixo (se você iterar sobre eles) que coletaria tudo o que você precisa dos label=argumentos:

handles, labels = ax.get_legend_handles_labels()
fig.legend(handles, labels, loc='upper center')
Ben Usman
fonte
13
Essa deve ser a resposta principal.
precisa saber é o seguinte
1
Esta é realmente uma resposta muito mais útil! Funcionou assim em um caso mais complicado para mim.
gmaravel
1
resposta perfeita!
Dorgham
4
Como removo a legenda das subparcelas?
BND
5
Apenas para adicionar a esta ótima resposta. Se você tem um eixo y secundário em suas plotagens e precisa mesclar os dois, use o seguinte: #handles, labels = [(a + b) for a, b in zip(ax1.get_legend_handles_labels(), ax2.get_legend_handles_labels())]
Bill Bill
114

figlegend pode ser o que você está procurando: http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.figlegend

Exemplo aqui: http://matplotlib.org/examples/pylab_examples/figlegend_demo.html

Outro exemplo:

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

ou:

fig.legend( lines, labels, loc = (0.5, 0), ncol=5 )
Nathan Musoke
fonte
1
Conheço as linhas que quero colocar na legenda, mas como faço para obter a linesvariável para colocar no argumento legend?
patapouf_ai
1
@patapouf_ai linesé uma lista de resultados retornados axes.plot()(por exemplo, cada axes.plotrotina semelhante ou semelhante retorna uma "linha"). Veja também o exemplo vinculado.
17

Para o posicionamento automático de uma única legenda em um figurecom muitos eixos, como os obtidos com subplots(), a seguinte solução funciona muito bem:

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

Com bbox_to_anchore bbox_transform=plt.gcf().transFigurevocê está definindo uma nova caixa delimitadora do tamanho do seu figurepara ser uma referência loc. O uso (0,-0.1,1,1)move esta caixa de empacotamento levemente para baixo para impedir que a legenda seja colocada sobre outros artistas.

OBS: use esta solução DEPOIS de usar fig.set_size_inches()e ANTES de usarfig.tight_layout()

Saullo GP Castro
fonte
1
Ou simpy loc='upper center', bbox_to_anchor=(0.5, 0), bbox_transform=plt.gcf().transFiguree não se sobrepõe com certeza.
Davor Josipovic
2
Ainda não sei por que, mas a solução de Evert não funcionou para mim - a lenda continuava sendo cortada. Esta solução (juntamente com o comentário de davor) funcionou muito bem - a legenda foi colocada conforme o esperado e totalmente visível. Obrigado!
sudo make install
16

Você só precisa pedir a legenda uma vez, fora do seu loop.

Por exemplo, neste caso, tenho 4 subparcelas, com as mesmas linhas e uma única legenda.

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
fonte
3
figlegend, Como sugerido por Evert, parece ser uma solução muito melhor;)
carla
11
o problema fig.legend()é que ele requer identificação para todas as linhas (plotagens) ... como, para cada subtrama, estou usando um loop para gerar as linhas, a única solução que descobri para superar isso é criar uma lista vazia antes o segundo loop e, em seguida, acrescente as linhas à medida que elas estão sendo criadas ... Então eu uso essa lista como argumento para a fig.legend()função.
carla
Uma pergunta semelhante aqui
emmmphd
O que tem dadosai?
Shyamkkhadka
1
@Shyamkkhadka, no meu script original, dadoshavia um conjunto de dados de um arquivo netCDF4 (para cada um dos arquivos definidos na lista ficheiros). Em cada loop, um arquivo diferente é lido e uma subparcela é adicionada à figura.
Carla
13

Notei que nenhuma resposta exibe uma imagem com uma única legenda referenciando muitas curvas em diferentes subparcelas, por isso tenho que mostrar uma ... para deixar você curioso ...

insira a descrição da imagem aqui

Agora, você quer ver o código, não é?

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

As duas linhas

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

merecem uma explicação - para esse objetivo, encapsulei a parte complicada de uma função, apenas 4 linhas de código, mas muito comentadas

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 Eu reconheço que sum(list_of_lists, [])é um método realmente ineficiente para achatar uma lista de listas, mas eu amo sua compactação, geralmente há algumas curvas em algumas subparcelas e Matplotlib e eficiência? ;-)

gboffi
fonte
3

Embora seja um pouco tarde para o jogo, darei outra solução aqui, pois esse ainda é um dos primeiros links para aparecer no google. Usando o matplotlib 2.2.2, isso pode ser alcançado usando o recurso gridspec. No exemplo abaixo, o objetivo é ter quatro subparcelas organizadas de forma 2x2 com a legenda mostrada na parte inferior. Um eixo 'falso' é criado na parte inferior para colocar a legenda em um ponto fixo. O eixo 'falso' é desativado para que apenas a legenda seja exibida. Resultado: 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
fonte
3

se você estiver usando subtramas com gráficos de barras, com cores diferentes para cada barra. pode ser mais rápido criar você mesmo os artefatos usandompatches

Digamos que você tenha quatro barras com cores diferentes, pois r m c kvocê pode definir a legenda da seguinte maneira

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
fonte
1
+1 O melhor! Usei-o dessa maneira adicionando diretamente ao arquivo para plt.legendter uma legenda para todas as minhas subparcelas #
User User
É mais rápido combinar as alças automáticas e as etiquetas artesanais handles, _ = plt.gca().get_legend_handles_labels()fig.legend(handles, labels)
:,
1

Esta resposta é um complemento para o @ Evert na posição da legenda.

Minha primeira tentativa na solução do @ Evert falhou devido a sobreposições da legenda e do título da subtrama.

De fato, as sobreposições são causadas por fig.tight_layout(), o que altera o layout das subparcelas sem considerar a legenda da figura. No entanto, fig.tight_layout()é necessário.

Para evitar sobreposições, podemos dizer fig.tight_layout()para deixar espaços para a legenda da figura fig.tight_layout(rect=(0,0,1,0.9)).

Descrição dos parâmetros tight_layout () .

laven_qa
fonte
1

Para aproveitar as respostas de @ gboffi e Ben Usman:

Em uma situação em que se tem linhas diferentes em subparcelas diferentes com a mesma cor e rótulo, pode-se fazer algo ao longo das linhas de

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
fonte