Mover la leyenda matplotlib fuera del eje hace que se corte por el cuadro de figura

222

Estoy familiarizado con las siguientes preguntas:

Matplotlib savefig con una leyenda fuera de la trama

Cómo sacar la leyenda de la trama

Parece que las respuestas en estas preguntas tienen el lujo de poder jugar con la reducción exacta del eje para que la leyenda encaje.

Sin embargo, la reducción de los ejes no es una solución ideal porque hace que los datos sean más pequeños y que en realidad sea más difícil de interpretar; particularmente cuando es complejo y hay muchas cosas sucediendo ... por lo tanto, necesita una gran leyenda

El ejemplo de una leyenda compleja en la documentación demuestra la necesidad de esto porque la leyenda en su diagrama en realidad oscurece por completo múltiples puntos de datos.

http://matplotlib.sourceforge.net/users/legend_guide.html#legend-of-complex-plots

Lo que me gustaría poder hacer es expandir dinámicamente el tamaño del cuadro de figura para acomodar la leyenda de la figura en expansión.

import matplotlib.pyplot as plt
import numpy as np

x = np.arange(-2*np.pi, 2*np.pi, 0.1)
fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.plot(x, np.sin(x), label='Sine')
ax.plot(x, np.cos(x), label='Cosine')
ax.plot(x, np.arctan(x), label='Inverse tan')
lgd = ax.legend(loc=9, bbox_to_anchor=(0.5,0))
ax.grid('on')

Observe cómo la etiqueta final 'Inverse tan' está realmente fuera del cuadro de la figura (y se ve muy mal, ¡no es la calidad de publicación!) ingrese la descripción de la imagen aquí

Finalmente, me han dicho que este es un comportamiento normal en R y LaTeX, así que estoy un poco confundido por qué esto es tan difícil en Python ... ¿Hay alguna razón histórica? ¿Matlab es igualmente pobre en este asunto?

Tengo la versión (solo un poco) más larga de este código en pastebin http://pastebin.com/grVjc007

jbbiomed
fuente
10
En cuanto al por qué, es porque matplotlib está orientado a tramas interactivas, mientras que R, etc., no lo son. (Y sí, Matlab es "igualmente pobre" en este caso en particular). Para hacerlo correctamente, debe preocuparse por cambiar el tamaño de los ejes cada vez que se cambia el tamaño, el zoom o la posición de la leyenda se actualiza. (Efectivamente, esto significa verificar cada vez que se dibuja la trama, lo que conduce a desaceleraciones). Ggplot, etc., son estáticos, por eso tienden a hacerlo de manera predeterminada, mientras que matplotlib y matlab no lo hacen. Dicho esto, tight_layout()debe cambiarse para tener en cuenta las leyendas.
Joe Kington
3
También estoy discutiendo esta pregunta en la lista de correo de usuarios de matplotlib. Así que tengo las sugerencias de ajustar la línea savefig a: fig.savefig ('samplefigure', bbox_extra_artists = (lgd,), bbox = 'tight')
jbbiomed
3
Sé que a matplotlib le gusta decir que todo está bajo el control del usuario, pero todo esto con las leyendas es demasiado bueno. Si pongo la leyenda afuera, obviamente quiero que siga siendo visible. La ventana debería ajustarse para ajustarse en lugar de crear esta gran molestia de reescalado. Como mínimo, debería haber una opción True predeterminada para controlar este comportamiento de autoescalado. Obligar a los usuarios a pasar por un número ridículo de re-renderizaciones para tratar de obtener los números de escala en el nombre del control logra lo contrario.
Elliot

Respuestas:

300

Lo siento EMS, pero en realidad acabo de recibir otra respuesta de la lista de correo matplotlib (Gracias a Benjamin Root).

El código que estoy buscando está ajustando la llamada savefig a:

fig.savefig('samplefigure', bbox_extra_artists=(lgd,), bbox_inches='tight')
#Note that the bbox_extra_artists must be an iterable

Esto es aparentemente similar a llamar a tight_layout, pero en su lugar, permite que savefig considere artistas adicionales en el cálculo. De hecho, esto redimensionó el cuadro de la figura como se desea.

import matplotlib.pyplot as plt
import numpy as np

plt.gcf().clear()
x = np.arange(-2*np.pi, 2*np.pi, 0.1)
fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.plot(x, np.sin(x), label='Sine')
ax.plot(x, np.cos(x), label='Cosine')
ax.plot(x, np.arctan(x), label='Inverse tan')
handles, labels = ax.get_legend_handles_labels()
lgd = ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5,-0.1))
text = ax.text(-0.2,1.05, "Aribitrary text", transform=ax.transAxes)
ax.set_title("Trigonometry")
ax.grid('on')
fig.savefig('samplefigure', bbox_extra_artists=(lgd,text), bbox_inches='tight')

Esto produce:

[editar] La intención de esta pregunta era evitar por completo el uso de ubicaciones de coordenadas arbitrarias de texto arbitrario como era la solución tradicional a estos problemas. A pesar de esto, numerosas ediciones han insistido recientemente en incluirlas, a menudo en formas que llevaron al código a generar un error. Ahora he solucionado los problemas y he ordenado el texto arbitrario para mostrar cómo estos también se consideran dentro del algoritmo bbox_extra_artists.

jbbiomed
fuente
1
/! \ Parece funcionar solo desde matplotlib> = 1.0 (la compresión de Debian tiene 0.99 y esto no funciona)
Julien Palard
1
No puedo hacer que esto funcione :( Paso lgd en savefig pero todavía no cambia el tamaño. El problema puede ser que no estoy usando una subtrama.
6005
8
Ah! Solo necesitaba usar bbox_inches = "tight" como lo hiciste. ¡Gracias!
6005
77
Esto es bueno, pero todavía me corto la figura cuando lo intento plt.show(). ¿Alguna solución para eso?
Agostino
2
No funciona si usa el fig.legend()método , realmente extraño.
jdhao
23

Agregado: Encontré algo que debería hacer el truco de inmediato, pero el resto del código a continuación también ofrece una alternativa.

Use la subplots_adjust()función para mover la parte inferior de la subtrama hacia arriba:

fig.subplots_adjust(bottom=0.2) # <-- Change the 0.02 to work for your plot.

Luego juegue con el desplazamiento en la bbox_to_anchorparte de leyenda del comando de leyenda, para obtener el cuadro de leyenda donde lo desee. Alguna combinación de configurar figsizey usar elsubplots_adjust(bottom=...) debería producir un gráfico de calidad para usted.

Alternativa: simplemente cambié la línea:

fig = plt.figure(1)

a:

fig = plt.figure(num=1, figsize=(13, 13), dpi=80, facecolor='w', edgecolor='k')

y cambiado

lgd = ax.legend(loc=9, bbox_to_anchor=(0.5,0))

a

lgd = ax.legend(loc=9, bbox_to_anchor=(0.5,-0.02))

y aparece bien en mi pantalla (un monitor CRT de 24 pulgadas).

Aquí figsize=(M,N)establece que la ventana de la figura sea M pulgadas por N pulgadas. Solo juega con esto hasta que te parezca adecuado. Conviértalo a un formato de imagen más escalable y use GIMP para editar si es necesario, o simplemente recorte con la viewportopción LaTeX cuando incluya gráficos.

ely
fuente
Parece que esta es la mejor solución en el momento actual, a pesar de que todavía requiere "jugar hasta que se vea bien", que no es una buena solución para un generador de informes automáticos. De hecho, ya uso esta solución, el verdadero problema es que matplotlib no compensa dinámicamente la leyenda que se encuentra fuera del bbox del eje. Como @Joe dijo, tight_layout debería tener en cuenta más características que solo ejes, títulos y etiquetas. Podría agregar esto como una solicitud de función en matplotlib.
jbbiomed
también funciona para mí obtener una imagen lo suficientemente grande como para que se ajuste a las etiquetas x que se cortaron previamente
Frederick Nord
1
Aquí está la documentación con el código de ejemplo de matplotlib.org
Yojimbo
14

Aquí hay otra solución muy manual. Puede definir el tamaño del eje y los rellenos se consideran en consecuencia (incluidas la leyenda y las marcas de verificación). Espero que sea de utilidad para alguien.

Ejemplo (¡el tamaño de los ejes es el mismo!):

ingrese la descripción de la imagen aquí

Código:

#==================================================
# Plot table

colmap = [(0,0,1) #blue
         ,(1,0,0) #red
         ,(0,1,0) #green
         ,(1,1,0) #yellow
         ,(1,0,1) #magenta
         ,(1,0.5,0.5) #pink
         ,(0.5,0.5,0.5) #gray
         ,(0.5,0,0) #brown
         ,(1,0.5,0) #orange
         ]


import matplotlib.pyplot as plt
import numpy as np

import collections
df = collections.OrderedDict()
df['labels']        = ['GWP100a\n[kgCO2eq]\n\nasedf\nasdf\nadfs','human\n[pts]','ressource\n[pts]'] 
df['all-petroleum long name'] = [3,5,2]
df['all-electric']  = [5.5, 1, 3]
df['HEV']           = [3.5, 2, 1]
df['PHEV']          = [3.5, 2, 1]

numLabels = len(df.values()[0])
numItems = len(df)-1
posX = np.arange(numLabels)+1
width = 1.0/(numItems+1)

fig = plt.figure(figsize=(2,2))
ax = fig.add_subplot(111)
for iiItem in range(1,numItems+1):
  ax.bar(posX+(iiItem-1)*width, df.values()[iiItem], width, color=colmap[iiItem-1], label=df.keys()[iiItem])
ax.set(xticks=posX+width*(0.5*numItems), xticklabels=df['labels'])

#--------------------------------------------------
# Change padding and margins, insert legend

fig.tight_layout() #tight margins
leg = ax.legend(loc='upper left', bbox_to_anchor=(1.02, 1), borderaxespad=0)
plt.draw() #to know size of legend

padLeft   = ax.get_position().x0 * fig.get_size_inches()[0]
padBottom = ax.get_position().y0 * fig.get_size_inches()[1]
padTop    = ( 1 - ax.get_position().y0 - ax.get_position().height ) * fig.get_size_inches()[1]
padRight  = ( 1 - ax.get_position().x0 - ax.get_position().width ) * fig.get_size_inches()[0]
dpi       = fig.get_dpi()
padLegend = ax.get_legend().get_frame().get_width() / dpi 

widthAx = 3 #inches
heightAx = 3 #inches
widthTot = widthAx+padLeft+padRight+padLegend
heightTot = heightAx+padTop+padBottom

# resize ipython window (optional)
posScreenX = 1366/2-10 #pixel
posScreenY = 0 #pixel
canvasPadding = 6 #pixel
canvasBottom = 40 #pixel
ipythonWindowSize = '{0}x{1}+{2}+{3}'.format(int(round(widthTot*dpi))+2*canvasPadding
                                            ,int(round(heightTot*dpi))+2*canvasPadding+canvasBottom
                                            ,posScreenX,posScreenY)
fig.canvas._tkcanvas.master.geometry(ipythonWindowSize) 
plt.draw() #to resize ipython window. Has to be done BEFORE figure resizing!

# set figure size and ax position
fig.set_size_inches(widthTot,heightTot)
ax.set_position([padLeft/widthTot, padBottom/heightTot, widthAx/widthTot, heightAx/heightTot])
plt.draw()
plt.show()
#--------------------------------------------------
#==================================================
gebbissimo
fuente
Esto no funcionó para mí hasta que cambié el primero plt.draw()a ax.figure.canvas.draw(). No estoy seguro de por qué, pero antes de este cambio, el tamaño de la leyenda no se actualizaba.
ws_e_c421
Si está intentando usar esto en una ventana GUI, debe cambiar fig.set_size_inches(widthTot,heightTot)a fig.set_size_inches(widthTot,heightTot, forward=True).
ws_e_c421