¿Cómo hago una sola leyenda para muchas subtramas con matplotlib?

166

Estoy trazando el mismo tipo de información, pero para diferentes países, con múltiples subtramas con matplotlib. Es decir, tengo 9 parcelas en una cuadrícula de 3x3, todas con las mismas líneas (por supuesto, diferentes valores por línea).

Sin embargo, no he descubierto cómo poner una sola leyenda (ya que las 9 subtramas tienen las mismas líneas) en la figura solo una vez.

¿Cómo puedo hacer eso?

Pocketfullofcheese
fuente

Respuestas:

160

También hay una buena función get_legend_handles_labels()que puede invocar en el último eje (si itera sobre ellos) que recopilaría todo lo que necesita de los label=argumentos:

handles, labels = ax.get_legend_handles_labels()
fig.legend(handles, labels, loc='upper center')
Ben Usman
fuente
13
Esta debería ser la mejor respuesta.
naught101
1
¡Esta es una respuesta mucho más útil! Funcionó así en un caso más complicado para mí.
gmaravel
1
¡Respuesta perfecta!
Dorgham
44
¿Cómo elimino la leyenda de las subtramas?
BND
55
Solo para agregar a esta gran respuesta. Si tiene un eje y secundario en sus parcelas y necesita fusionarlos, use esto:handles, labels = [(a + b) for a, b in zip(ax1.get_legend_handles_labels(), ax2.get_legend_handles_labels())]
Bill
114

figlegend puede ser lo que estás buscando: http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.figlegend

Ejemplo aquí: http://matplotlib.org/examples/pylab_examples/figlegend_demo.html

Otro ejemplo:

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

o:

fig.legend( lines, labels, loc = (0.5, 0), ncol=5 )
Nathan Musoke
fuente
1
Sé las líneas que quiero poner en la leyenda, pero ¿cómo consigo la linesvariable para poner en el argumento legend?
patapouf_ai
1
@patapouf_ai lineses una lista de resultados que se devuelven axes.plot()(es decir, cada axes.plotrutina o una rutina similar devuelve una "línea"). Ver también el ejemplo vinculado.
17

Para el posicionamiento automático de una sola leyenda en un figurecon muchos ejes, como los obtenidos con subplots(), la siguiente solución funciona muy bien:

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

Con bbox_to_anchory bbox_transform=plt.gcf().transFigureestá definiendo un nuevo cuadro delimitador del tamaño de su figurepara ser una referencia loc. Usando (0,-0.1,1,1)mueve este cuadro de bouding ligeramente hacia abajo para evitar que la leyenda se coloque sobre otros artistas.

OBS: use esta solución DESPUÉS de usar fig.set_size_inches()y ANTES de usarfig.tight_layout()

Saullo GP Castro
fuente
1
O simple loc='upper center', bbox_to_anchor=(0.5, 0), bbox_transform=plt.gcf().transFigurey no se superpondrá con seguridad.
Davor Josipovic el
2
Todavía no estoy seguro de por qué, pero la solución de Evert no funcionó para mí: la leyenda seguía cortando. Esta solución (junto con el comentario de davor) funcionó de manera muy limpia: la leyenda se colocó como se esperaba y fue completamente visible. ¡Gracias!
sudo make install
16

Solo tienes que pedir la leyenda una vez, fuera de tu circuito.

Por ejemplo, en este caso tengo 4 subtramas, con las mismas líneas y una sola leyenda.

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
fuente
3
figlegend, como sugiere Evert, parece ser una solución mucho mejor;)
carla
11
El problema fig.legend()es que requiere la identificación de todas las líneas (tramas) ... ya que, para cada subtrama, estoy usando un bucle para generar las líneas, la única solución que descubrí para superar esto es crear una lista vacía antes el segundo ciclo, y luego agregue las líneas a medida que se crean ... Luego uso esta lista como argumento de la fig.legend()función.
carla
Una pregunta similar aquí
emmmphd
Que dadoshay
Shyamkkhadka
1
@Shyamkkhadka, en mi script original dadoshabía un conjunto de datos de un archivo netCDF4 (para cada uno de los archivos definidos en la lista ficheiros). En cada bucle, se lee un archivo diferente y se agrega una subtrama a la figura.
carla
14

He notado que ninguna respuesta muestra una imagen con una sola leyenda que hace referencia a muchas curvas en diferentes subtramas, así que tengo que mostrarle una ... para que tenga curiosidad ...

ingrese la descripción de la imagen aquí

Ahora, quieres mirar el código, ¿no?

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

Las dos lineas

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

Merecen una explicación: para este objetivo, he encapsulado la parte difícil en una función, solo 4 líneas de código, pero muy comentado

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)

PD: Reconozco que sum(list_of_lists, [])es un método realmente ineficiente para aplanar una lista de listas, pero love Me encanta su tamaño compacto, ② generalmente hay algunas curvas en algunas subtramas y ③ ¿Matplotlib y eficiencia? ;-)

gboffi
fuente
3

Si bien es bastante tarde para el juego, daré otra solución aquí, ya que este sigue siendo uno de los primeros enlaces en aparecer en Google. Usando matplotlib 2.2.2, esto se puede lograr usando la función gridspec. En el ejemplo a continuación, el objetivo es tener cuatro subtramas organizadas de forma 2x2 con la leyenda que se muestra en la parte inferior. Se crea un eje 'falso' en la parte inferior para colocar la leyenda en un lugar fijo. El eje 'falso' se apaga, por lo que solo se muestra la leyenda. 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
fuente
3

si está utilizando subtramas con gráficos de barras, con diferentes colores para cada barra. puede ser más rápido crear los artefactos usted mismo usandompatches

Digamos que tiene cuatro barras con diferentes colores, ya r m c kque puede establecer la leyenda de la siguiente manera

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
fuente
1
+1 ¡Lo mejor! Lo utilicé de esta manera agregando directamente a plt.legendtener una leyenda para todas mis subtramas
Usuario
Es más rápido combinar las manijas automáticas y las etiquetas hechas a mano: handles, _ = plt.gca().get_legend_handles_labels()luegofig.legend(handles, labels)
smcs
1

Esta respuesta es un complemento de @ Evert en la posición de leyenda.

Mi primer intento con la solución de @ Evert falló debido a la superposición de la leyenda y el título de la subtrama.

De hecho, las superposiciones son causadas por fig.tight_layout(), lo que cambia el diseño de las subtramas sin tener en cuenta la leyenda de la figura. Sin embargo,fig.tight_layout() es necesario.

Para evitar las superposiciones, podemos decir fig.tight_layout()que deje espacios para la leyenda de la figura fig.tight_layout(rect=(0,0,1,0.9)).

Descripción de los parámetros tight_layout () .

laven_qa
fuente
1

Para construir sobre la respuesta de @ gboffi y Ben Usman:

En una situación en la que uno tiene diferentes líneas en diferentes subtramas con el mismo color y etiqueta, uno puede hacer algo en la línea 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
fuente