Creación de figura con tamaño exacto y sin relleno (y leyenda fuera de los ejes)

Estoy tratando de hacer algunas figuras para un artículo científico, así que quiero que mis figuras tengan un tamaño específico. También veo que Matplotlib por defecto agrega mucho relleno en el borde de las figuras, que no necesito (ya que las figuras estarán en un fondo blanco de todos modos).

Para establecer un tamaño de figura específico, simplemente uso plt.figure(figsize = [w, h]) , y agrego el argumento tight_layout = {'pad': 0} para eliminar el relleno. Esto funciona perfectamente, e incluso funciona si agrego un título, y / x-labels, etc. Ejemplo:

 fig = plt.figure( figsize = [3,2], tight_layout = {'pad': 0} ) ax = fig.add_subplot(111) plt.title('title') ax.set_ylabel('y label') ax.set_xlabel('x label') plt.savefig('figure01.pdf') 

Esto crea un archivo pdf con un tamaño exacto de 3×2 (pulgadas).

figure01.png

El problema que tengo es que cuando, por ejemplo, agrego un cuadro de texto fuera del eje (generalmente un recuadro de leyenda), Matplotlib no deja espacio para el cuadro de texto como lo hace al agregar títulos / tags de ejes. Normalmente, el cuadro de texto se corta o no aparece en la figura guardada. Ejemplo:

 plt.close('all') fig = plt.figure( figsize = [3,2], tight_layout = {'pad': 0} ) ax = fig.add_subplot(111) plt.title('title') ax.set_ylabel('y label') ax.set_xlabel('x label') t = ax.text(0.7, 1.1, 'my text here', bbox = dict(boxstyle = 'round')) plt.savefig('figure02.pdf') 

figure02.png

Una solución que encontré en otra parte de SO fue agregar el argumento bbox_inches = 'tight' al comando savefig. El cuadro de texto ahora está incluido como yo quería, pero el pdf ahora tiene el tamaño incorrecto. Parece que Matplotlib solo hace la figura más grande, en lugar de reducir el tamaño de los ejes como lo hace al agregar títulos y tags x / y.

Ejemplo:

 plt.close('all') fig = plt.figure( figsize = [3,2], tight_layout = {'pad': 0} ) ax = fig.add_subplot(111) plt.title('title') ax.set_ylabel('y label') ax.set_xlabel('x label') t = ax.text(0.7, 1.1, 'my text here', bbox = dict(boxstyle = 'round')) plt.savefig('figure03.pdf', bbox_inches = 'tight') 

figure03.png

(Esta cifra es 3.307×2.248)

¿Hay alguna solución a esto que cubra la mayoría de los casos con una leyenda justo fuera de los ejes?

Entonces los requisitos son:

  1. Tener un tamaño fijo y predefinido
  2. Agregar una etiqueta de texto o una leyenda fuera de los ejes
  3. Los ejes y el texto no se pueden superponer
  4. Los ejes, junto con las tags del título y del eje, se ubican firmemente contra el borde de la figura.

Entonces tight_layout con pad = 0 , resuelve 1. y 4. pero contradice 2.

Uno podría pensar en establecer el pad a un valor mayor. Esto resolvería 2. Sin embargo, dado que es simétrico en todas las direcciones, estaría en contradicción con 4.

Usar bbox_inches = 'tight' cambia el tamaño de la figura. Contradictos 1.

Entonces, creo que no hay una solución genérica para este problema.

Algo que se me ocurre es lo siguiente: establece el texto en coordenadas de figura y luego cambia el tamaño de los ejes, ya sea en dirección horizontal o vertical, de modo que no haya superposición entre los ejes y el texto.

 import matplotlib.pyplot as plt import matplotlib.transforms fig = plt.figure(figsize = [3,2]) ax = fig.add_subplot(111) plt.title('title') ax.set_ylabel('y label') ax.set_xlabel('x label') def text_legend(ax, x0, y0, text, direction = "v", padpoints = 3, margin=1.,**kwargs): ha = kwargs.pop("ha", "right") va = kwargs.pop("va", "top") t = ax.figure.text(x0, y0, text, ha=ha, va=va, **kwargs) otrans = ax.figure.transFigure plt.tight_layout(pad=0) ax.figure.canvas.draw() plt.tight_layout(pad=0) offs = t._bbox_patch.get_boxstyle().pad * t.get_size() + margin # adding 1pt trans = otrans + \ matplotlib.transforms.ScaledTranslation(-offs/72.,-offs/72.,fig.dpi_scale_trans) t.set_transform(trans) ax.figure.canvas.draw() ppar = [0,-padpoints/72.] if direction == "v" else [-padpoints/72.,0] trans2 = matplotlib.transforms.ScaledTranslation(ppar[0],ppar[1],fig.dpi_scale_trans) + \ ax.figure.transFigure.inverted() tbox = trans2.transform(t._bbox_patch.get_window_extent()) bbox = ax.get_position() if direction=="v": ax.set_position([bbox.x0, bbox.y0,bbox.width, tbox[0][1]-bbox.y0]) else: ax.set_position([bbox.x0, bbox.y0,tbox[0][0]-bbox.x0, bbox.height]) # case 1: place text label at top right corner of figure (1,1). Adjust axes height. #text_legend(ax, 1,1, 'my text here', bbox = dict(boxstyle = 'round'), ) # case 2: place text left of axes, (1, y), direction=="v" text_legend(ax, 1., 0.8, 'my text here', margin=2., direction="h", bbox = dict(boxstyle = 'round') ) plt.savefig(__file__+'.pdf') plt.show() 

caso 1 (izquierda) y caso 2 (derecha):
enter image description here enter image description here


Hacer lo mismo con una leyenda es un poco más fácil, porque podemos usar directamente el argumento bbox_to_anchor y no necesitamos controlar el cuadro de fantasía alrededor de la leyenda.

 import matplotlib.pyplot as plt import matplotlib.transforms fig = plt.figure(figsize = [3.5,2]) ax = fig.add_subplot(111) ax.set_title('title') ax.set_ylabel('y label') ax.set_xlabel('x label') ax.plot([1,2,3], marker="o", label="quantity 1") ax.plot([2,1.7,1.2], marker="s", label="quantity 2") def legend(ax, x0=1,y0=1, direction = "v", padpoints = 3,**kwargs): otrans = ax.figure.transFigure t = ax.legend(bbox_to_anchor=(x0,y0), loc=1, bbox_transform=otrans,**kwargs) plt.tight_layout(pad=0) ax.figure.canvas.draw() plt.tight_layout(pad=0) ppar = [0,-padpoints/72.] if direction == "v" else [-padpoints/72.,0] trans2=matplotlib.transforms.ScaledTranslation(ppar[0],ppar[1],fig.dpi_scale_trans)+\ ax.figure.transFigure.inverted() tbox = t.get_window_extent().transformed(trans2 ) bbox = ax.get_position() if direction=="v": ax.set_position([bbox.x0, bbox.y0,bbox.width, tbox.y0-bbox.y0]) else: ax.set_position([bbox.x0, bbox.y0,tbox.x0-bbox.x0, bbox.height]) # case 1: place text label at top right corner of figure (1,1). Adjust axes height. #legend(ax, borderaxespad=0) # case 2: place text left of axes, (1, y), direction=="h" legend(ax,y0=0.8, direction="h", borderaxespad=0.2) plt.savefig(__file__+'.pdf') plt.show() 

enter image description here enter image description here


¿Por qué 72 ? El 72 es el número de puntos por pulgada (ppi). Esta es una unidad tipográfica fija, por ejemplo, los tamaños de letra siempre se dan en puntos (como 12 puntos). Debido a que matplotlib define el relleno del cuadro de texto en unidades relativas al tamaño de fuente, que son los puntos, necesitamos usar 72 para transformar de nuevo a pulgadas (y luego para mostrar las coordenadas). Los puntos predeterminados por pulgada (ppp) no se tocan aquí, pero se contabilizan en fig.dpi_scale_trans . Si desea cambiar los ppp, debe asegurarse de que la ppp de la figura esté configurada tanto al crear la figura como al guardarla (utilice dpi=.. en la llamada a plt.figure() y plt.savefig() ) .