Создание фигуры с точным размером и без дополнений (и легенда за пределами осей)

Я пытаюсь сделать некоторые цифры для научной статьи, поэтому хочу, чтобы мои цифры имели определенный размер. Я также вижу, что Matplotlib по умолчанию добавляет много дополнений на границу цифр, которые мне не нужны (так как цифры будут на белом фоне в любом случае).

Чтобы установить определенный размер фигуры, я просто использую plt.figure(figsize = [w, h]), и добавляю аргумент tight_layout = {'pad': 0}, чтобы удалить дополнение. Это работает отлично и даже работает, если я добавляю заголовок, y/x-метки и т.д. Пример:

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

Это создает файл PDF с точным размером 3x2 (дюймы).

figure01.png

Проблема заключается в том, что когда я, например, добавляю текстовое поле за пределами оси (как правило, это поле с условным обозначением), Matplotlib не освобождает место для текстового поля, как при добавлении заголовков/осей. Обычно текстовое поле отключается или вообще не отображается на сохраненной фигуре. Пример:

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

Решение, которое я нашел в другом месте на SO, заключалось в том, чтобы добавить аргумент bbox_inches = 'tight' в команду savefig. Текстовое окно теперь включено, как хотелось, но формат pdf теперь неправильный. Похоже, что Matplotlib просто делает фигуру больше, а не уменьшает размер осей, как при добавлении заголовков и x/y-меток.

Пример:

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

(Эта цифра 3,307x2,248)

Есть ли какое-либо решение для этого, которое охватывает большинство случаев с легендой, находящейся за пределами осей?

Ответ 1

Таким образом, требования:

  • Наличие фиксированного предопределенного размера фигуры
  • Добавление текстовой метки или легенды вне осей
  • Оси и текст не могут перекрываться
  • Оси вместе с метками названия и оси сидят снова на границе рисунка.

So tight_layout с pad = 0, решает 1 и 4. но противоречит 2.

Можно подумать, что при установке pad на большее значение. Это позволило бы решить 2. Однако, поскольку он симметричен во всех направлениях, это противоречило бы 4.

Использование bbox_inches = 'tight' изменяет размер фигуры. Противоречит 1.

Итак, я думаю, что нет общего решения этой проблемы.

Что-то, что я могу придумать, следующее: он устанавливает текст в координатах фигур, а затем изменяет размеры осей либо в горизонтальном, либо в вертикальном направлении, так что между осями и текстом нет совпадения.

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

случай 1 (слева) и случай 2 (справа):
введите описание изображения здесь введите описание изображения здесь


Делать то же самое с легендой немного легче, потому что мы можем напрямую использовать аргумент bbox_to_anchor и не нуждаемся в управлении причудливым полем вокруг легенды.
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()

введите описание изображения здесь введите описание изображения здесь


Почему 72? 72 - количество точек на дюйм (ppi). Это фиксированная типографская единица, например. шрифты всегда указываются в точках (например, 12pt). Поскольку matplotlib определяет заполнение текстового поля в единицах относительно fontsize, которое является точками, нам нужно использовать 72 для преобразования обратно в дюймы (а затем для отображения координат). Точки по умолчанию на дюйм (dpi) здесь не затрагиваются, но учитываются в fig.dpi_scale_trans. Если вы хотите изменить dpi, вам нужно убедиться, что рисунок dpi задан при создании рисунка, а также при его сохранении (используйте dpi=.. в вызове plt.figure(), а также plt.savefig()).