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

11 minutos de lectura

avatar de usuario
bolsillo lleno de queso

Estoy trazando el mismo tipo de información, pero para diferentes países, con múltiples subtramas con Matplotlib. Es decir, tengo nueve parcelas en una cuadrícula de 3×3, 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 nueve tramas secundarias tienen las mismas líneas) en la figura solo una vez.

¿Cómo puedo hacer eso?

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

handles, labels = ax.get_legend_handles_labels()
fig.legend(handles, labels, loc="upper center")

  • ¿Cómo elimino la leyenda de las tramas secundarias?

    – BND

    21 de abril de 2019 a las 13:18

  • Solo para agregar a esta gran respuesta. Si tiene un eje y secundario en sus gráficos y necesita fusionarlos, use esto: handles, labels = [(a + b) for a, b in zip(ax1.get_legend_handles_labels(), ax2.get_legend_handles_labels())]

    – Factura

    4 sep 2019 a las 21:32

  • plt.gca().get_legend_handles_labels() trabajó para mi.

    – Stephen Witkowski

    27 de marzo de 2020 a las 21:51

  • ¿Estás seguro de que esto elimina la leyenda de las tramas secundarias?

    – gentil

    16 de abril de 2020 a las 15:54

  • para compañeros conspiradores de pandas, pase legend=0 en la función de trama para ocultar las leyendas de sus subtramas.

    – ShouravBR

    22 de septiembre de 2020 a las 18:47

figlegend puede ser lo que buscas: matplotlib.pyplot.figlegend

Un ejemplo está en Demostración de la leyenda de la figura.

Otro ejemplo:

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

O:

fig.legend(lines, labels, loc = (0.5, 0), ncol=5)

  • Sé las líneas que quiero poner en la leyenda, pero ¿cómo obtengo el lines variable para poner en el argumento de legend ?

    – patapouf_ai

    10 de abril de 2017 a las 12:51

  • @patapouf_ai lines es una lista de resultados que se devuelven de axes.plot() (es decir, cada axes.plot o una rutina similar devuelve una “línea”). Véase también el ejemplo vinculado.

    usuario707650

    10 de abril de 2017 a las 20:13

avatar de usuario
gboffi

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)

He notado que ninguna de las respuestas muestra una imagen con una sola leyenda que hace referencia a muchas curvas en diferentes subparcelas, así que tengo que mostrarte una… para que sientas curiosidad…

Ingrese la descripción de la imagen aquí

Ahora tu desear para 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; con este objetivo, he encapsulado la parte difícil en una función, solo cuatro líneas de código, pero fuertemente 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])

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

PD: lo reconozco sum(list_of_lists, []) es un método realmente ineficiente para aplanar una lista de listas, pero ① Me encanta su compacidad, ② por lo general son algunas curvas en algunas subparcelas y ③ ¿Matplotlib y eficiencia? 😉

Si quieres seguir con la API oficial de Matplotlib, esto es perfecto, de verdad.

Por otro lado, si no te importa usar un método privado del matplotlib.legend módulo … es realmente mucho, mucho, mucho más fácil

from matplotlib.legend import _get_legend_handles_labels
...

fig.legend(*_get_legend_handles_and_labels(fig.axes), ...)

Una explicación completa se puede encontrar en el código fuente de Axes.get_legend_handles_labels en .../matplotlib/axes/_axes.py

  • la línea con sum(lol, ...) me da un TypeError: 'list' object cannot be interpreted as an integer (usando la versión 3.3.4 de matplotlib)

    – duff18

    29 de marzo de 2021 a las 14:31

  • @ duff18 Parece que ha olvidado el argumento opcional para sumes decir, la lista nula []. Por favor mira sum documentación para una explicación.

    – gboffi

    30 de marzo de 2021 a las 8:30

  • no, solo copié y pegué tu código. para ser más claro, la línea que da el error es lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]

    – duff18

    31 de marzo de 2021 a las 9:04


  • @ duff18 No tengo una explicación inmediata, dada también la escasez de información que se ha proporcionado. Solo puedo sugerir que proporcione todo el contexto relevante (un ejemplo reproducible y un seguimiento completo) en una nueva pregunta. Por favor envíeme un comentario si decide hacer una nueva pregunta.

    – gboffi

    31 de marzo de 2021 a las 13:18

  • @ duff18 Acabo de verificar con Matplotlib 3.3.4 y me sorprendió descubrir que todo sigue bien, como estaba bien en agosto de 2019, cuando escribí mi respuesta. No sé qué sale mal en su situación, podría renovar mi sugerencia, publique una nueva pregunta que detalle su contexto. Estaré encantado de intentar ayudarte si me haces un ping.

    – gboffi

    2 abr 2021 a las 20:22

avatar de usuario
Saullo GP Castro

Para el posicionamiento automático de una sola leyenda en un figure con muchas hachas, como las obtenidas 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_anchor y bbox_transform=plt.gcf().transFigureestá definiendo un nuevo cuadro delimitador del tamaño de su figureser una referencia para loc. Usando (0, -0.1, 1, 1) mueve este cuadro delimitador ligeramente hacia abajo para evitar que la leyenda se coloque sobre otros artistas.

OBS: Usa esta solución después tu usas fig.set_size_inches() y antes de tu usas fig.tight_layout()

avatar de usuario
carla

Solo tiene que pedir la leyenda una vez, fuera de su bucle.

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

  • figlegendcomo sugirió Evert, parece ser una solución mucho mejor;)

    – carla

    23 de marzo de 2012 a las 11:06

  • el problema de fig.legend() es que requiere identificación para todas las líneas (parcelas)… ya que, para cada subparcela, estoy usando un ciclo para generar las líneas, la única solución que encontré para superar esto es crear una lista vacía antes del segundo ciclo , y luego agrego las líneas a medida que se crean… Luego uso esta lista como argumento para el fig.legend() función.

    – carla

    23 de marzo de 2012 a las 12:06

  • Una pregunta similar aquí

    – Yushan Zhang

    2 de agosto de 2017 a las 7:34

  • Que es dados allá ?

    – Shyamkkhadka

    30 de enero de 2018 a las 14:48

  • @Shyamkkhadka, en mi guión original dados era un conjunto de datos de un archivo netCDF4 (para cada uno de los archivos definidos en la lista ficheiros). En cada ciclo, se lee un archivo diferente y se agrega una subparcela a la figura.

    – carla

    31 de enero de 2018 a las 11:08

avatar de usuario
Pedro Mortensen

Si está utilizando subparcelas con gráficos de barras, con un color diferente para cada barra, puede ser más rápido crear los artefactos usted mismo usando mpatches.

Digamos que tienes cuatro barras con diferentes colores como r, m, cy kpuede 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

  • figlegendcomo sugirió Evert, parece ser una solución mucho mejor;)

    – carla

    23 de marzo de 2012 a las 11:06

  • el problema de fig.legend() es que requiere identificación para todas las líneas (parcelas)… ya que, para cada subparcela, estoy usando un ciclo para generar las líneas, la única solución que encontré para superar esto es crear una lista vacía antes del segundo ciclo , y luego agrego las líneas a medida que se crean… Luego uso esta lista como argumento para el fig.legend() función.

    – carla

    23 de marzo de 2012 a las 12:06

  • Una pregunta similar aquí

    – Yushan Zhang

    2 de agosto de 2017 a las 7:34

  • Que es dados allá ?

    – Shyamkkhadka

    30 de enero de 2018 a las 14:48

  • @Shyamkkhadka, en mi guión original dados era un conjunto de datos de un archivo netCDF4 (para cada uno de los archivos definidos en la lista ficheiros). En cada ciclo, se lee un archivo diferente y se agrega una subparcela a la figura.

    – carla

    31 de enero de 2018 a las 11:08

avatar de usuario
Pedro Mortensen

Usando Matplotlib 2.2.2, esto se puede lograr usando la función gridspec.

En el siguiente ejemplo, el objetivo es tener cuatro subparcelas dispuestas en forma de 2×2 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 para que solo se muestre la leyenda. Resultado:

Algunas tramas producidas por Matplotlib

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

¿Ha sido útil esta solución?