2012-04-11 32 views
139

Estoy familiarizado con las siguientes preguntas:Moving leyenda matplotlib fuera del eje hace que sea corte por la caja de la figura

Matplotlib savefig with a legend outside the plot

How to put the legend out of the plot

Parece que las respuestas de estas preguntas tienen la lujo de poder jugar con la contracción exacta del eje para que la leyenda se adapte.

La reducción de los ejes, sin embargo, no es una solución ideal porque reduce los datos y hace que 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 trazado en realidad oscurece completamente múltiples puntos de datos .

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

Lo que me gustaría ser capaz de hacer es dinámicamente ampliar el tamaño de la caja de la figura para dar cabida a la leyenda de la figura 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 bronceado' es en realidad fuera de la caja de la figura (y se ve mal corte - no calidad de publicación) enter image description here

Por último, me han dicho que este es 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 (sólo un poco) más larga de este código en Pastebin http://pastebin.com/grVjc007

+6

En cuanto a la razón por la es que es porque matplotlib se orienta hacia parcelas interactivos, mientras que R, etc, no lo son (Y sí, Matlab es "igualmente pobre" en este caso particular). Para hacerlo correctamente, debe preocuparse por cambiar el tamaño de los ejes cada vez que se cambia el tamaño de la figura, se amplía o se actualiza la posición de la leyenda. (Efectivamente, esto significa verificar cada vez que se traza la trama, lo que conduce a ralentizaciones.) Ggplot, etc., son estáticos, por eso tienden a hacer esto de forma predeterminada, mientras que matplotlib y matlab no. Dicho esto, 'tight_layout()' debe cambiarse para tener en cuenta las leyendas. –

+2

También estoy discutiendo esta cuestión en la lista de correo de los usuarios de matplotlib. Así que tengo la sugerencia de ajustar la línea savefig a: fig.savefig ('samplefigure', bbox_extra_artists = (lgd,), bbox = 'tight') – jbbiomed

+3

Sé que matplotlib le gusta tout que todo está bajo el control de la usuario, pero todo esto con las leyendas es demasiado bueno. Si pongo la leyenda afuera, obviamente quiero que aún esté visible. La ventana solo debe escalarse para ajustarse en lugar de crear esta enorme molestia de reajuste.Como mínimo, debe 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 repeticiones para tratar de obtener los números de la escala en nombre del control logra lo contrario. – Elliot

Respuesta

190

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

El código que estoy buscando es el ajuste de la llamada a savefig:

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 tight_layout, pero en su lugar permite savefig a considerar artistas adicionales en el cálculo. De hecho, esto redimensionó el cuadro de figuras según lo deseado.

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') 
handles, labels = ax.get_legend_handles_labels() 
lgd = ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5,-0.1)) 
ax.grid('on') 
fig.savefig('samplefigure', bbox_extra_artists=(lgd,), bbox_inches='tight') 

Esto produce:

enter image description here

+1

/! \ Parece que funciona solo desde matplotlib> = 1.0 (Debian squeeze tiene 0.99 y esto no funciona) –

+1

No se puede hacer que esto funcione :(Paso en lgd a savefig pero aún no cambia de tamaño. puede ser que no estoy usando una subtrama. – 6005

+4

¡Ah! Solo necesitaba usar bbox_inches = "apretado" como lo hiciste. ¡Gracias! – 6005

13

Agregado: encontré algo que debe hacer el truco de inmediato, pero el resto del código de abajo también ofrece una alternativa.

utilizar la función subplots_adjust() para mover la parte inferior de la trama secundaria hasta:

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

luego jugar con el desplazamiento en la parte bbox_to_anchor leyenda del comando leyenda, para obtener el cuadro de la leyenda, donde lo desee. Alguna combinación de configurar el figsize y usar el subplots_adjust(bottom=...) debe producir un diagrama de calidad para usted.

alternativa: simplemente I cambió la línea:

fig = plt.figure(1) 

a:

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

y cambiaron

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 se muestra bien en mi pantalla (un monitor CRT de 24 pulgadas).

Aquí figsize=(M,N) establece que la ventana de la figura sea de M pulgadas por N pulgadas. Solo juega con esto hasta que te quede bien. Conviértalo en un formato de imagen más escalable y use GIMP para editar si es necesario, o simplemente recorte con la opción LaTeX viewport al incluir gráficos.

+0

Parecería que esta es la mejor solución en el momento actual, aunque todavía requiere 'jugar hasta que se vea bien', lo que no es una buena solución para un generador de informe automático. De hecho, ya uso esta solución, el verdadero problema es que matplotlib no compensa dinámicamente la leyenda que está fuera de la bbox del eje. Como dijo @Joe, tight_layout * debería * tener en cuenta más características que solo eje, títulos y etiquetas. Podría agregar esto como una solicitud de función en matplotlib. – jbbiomed

+0

también funciona para que obtenga una imagen lo suficientemente grande como para caber en las xlabels cortadas previamente –

+1

[aquí] (http://matplotlib.org/examples/pylab_examples/subplots_adjust.html) es la documentación con el código de ejemplo de matplotlib.org – Yojimbo

11

Aquí hay otra solución, muy manual. Puede definir el tamaño del eje y las separaciones se consideran en consecuencia (incluida la leyenda y las marcas). Espero que sea útil para alguien.

Ejemplo (tamaño ejes son los mismos!):

enter image description here

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() 
#-------------------------------------------------- 
#================================================== 
+0

Esto no funcionó para mí hasta que cambié el primer '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. –

+0

Si está intentando usar esto en una ventana de GUI, necesita cambiar 'fig.set_size_inches (widthTot, heightTot)' a 'fig.set_size_inches (widthTot, heightTot, forward = True)'. –

Cuestiones relacionadas