matplotlib - Crear figura con tamaño exacto y sin relleno(y leyenda fuera de los ejes)
(1)
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, lo 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, etiquetas y / x, 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 3x2 (pulgadas).
El problema que tengo es que cuando, por ejemplo, agrego un cuadro de texto fuera del eje (generalmente un cuadro de leyenda), Matplotlib no deja espacio para el cuadro de texto como lo hace al agregar títulos / etiquetas de eje. Normalmente, el cuadro de texto está cortado o no se muestra 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'')
Una solución que encontré en otro lugar en SO fue agregar el argumento
bbox_inches = ''tight''
al comando savefig.
El cuadro de texto ahora se incluye como quería, pero el pdf ahora tiene el tamaño incorrecto.
Parece que Matplotlib solo hace que la figura sea más grande, en lugar de reducir el tamaño de los ejes como lo hace al agregar títulos y etiquetas 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'')
(Esta cifra es 3.307x2.248)
¿Hay alguna solución para esto que cubra la mayoría de los casos con una leyenda justo fuera de los ejes?
Entonces los requisitos son:
- Tener un tamaño de figura fijo y predefinido
- Agregar una etiqueta de texto o leyenda fuera de los ejes
- Los ejes y el texto no pueden solaparse
- Los ejes, junto con las etiquetas de título y eje, se asientan 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 configurar 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.
Así que 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 en dirección horizontal o vertical para 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):
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 elegante 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()
¿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 fuente siempre se dan en puntos (como 12pt).
Debido a que matplotlib define el relleno del cuadro de texto en unidades relativas al tamaño de fuente, que son puntos, necesitamos usar
72
para transformar de nuevo a pulgadas (y luego para mostrar las coordenadas).
Los puntos por pulgada (ppp) predeterminados no se tocan aquí, pero se contabilizan en
fig.dpi_scale_trans
.
Si desea cambiar el dpi, debe asegurarse de que el dpi de la figura esté configurado al crear la figura y al guardarla (use
dpi=..
en la llamada a
plt.figure()
así como
plt.savefig()
) .