Перемещение легенды matplotlib за пределы оси делает ее обрезкой по фигуре

222

Я знаком со следующими вопросами:

Matplotlib savefig с легендой вне сюжета

Как убрать легенду из сюжета

Кажется, что ответы на эти вопросы могут позволить себе роскошь возиться с точным сжатием оси, чтобы легенда подходила.

Однако сжатие осей не является идеальным решением, поскольку оно уменьшает размер данных, затрудняя их интерпретацию; особенно когда это сложно и происходит много всего ... отсюда и нужна большая легенда

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

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

То, что я хотел бы сделать, это динамически увеличивать размер поля рисунка, чтобы приспособить расширяющуюся легенду фигуры.

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

Обратите внимание, что окончательный ярлык «Обратный загар» на самом деле находится за рамкой рисунка (и выглядит плохо обрезанным, а не качеством публикации!) введите описание изображения здесь

Наконец, мне сказали, что это нормальное поведение в R и LaTeX, поэтому я немного запутался, почему это так сложно в python ... Есть ли историческая причина? Матлаб одинаково беден в этом вопросе?

У меня есть (только немного) более длинная версия этого кода на pastebin http://pastebin.com/grVjc007

jbbiomed
источник
10
Насколько это объясняется тем, что matplotlib ориентирован на интерактивные сюжеты, а R и т. Д. - нет. (И да, Matlab «одинаково беден» в данном конкретном случае.) Чтобы сделать это правильно, вам нужно беспокоиться о изменении размера осей каждый раз, когда фигура изменяется, масштабируется или обновляется положение легенды. (По сути, это означает проверку при каждом построении графика, что приводит к замедлению.) Ggplot и т. Д. Являются статическими, поэтому они, как правило, делают это по умолчанию, тогда как matplotlib и matlab этого не делают. Сказанное tight_layout()должно быть изменено, чтобы принять во внимание легенды.
Джо Кингтон
3
Я также обсуждаю этот вопрос в списке рассылки пользователей matplotlib. Таким образом , у меня есть предложения корректировки savefig строки: fig.savefig ( 'samplefigure', bbox_extra_artists = (LGD,), BBox = 'плотно')
jbbiomed
3
Я знаю, что matplotlib любит говорить, что все находится под контролем пользователя, но вся эта вещь с легендами - слишком хорошая вещь. Если я поставлю легенду снаружи, я, очевидно, хочу, чтобы она все еще была видна. Окно должно просто масштабироваться, чтобы соответствовать, а не создавать эту огромную проблему с масштабированием. По крайней мере, должна быть опция True по умолчанию для управления этим автоматическим масштабированием. Заставляя пользователей проходить смешное количество повторных рендеров, чтобы попытаться получить правильные номера весов во имя контроля, можно добиться обратного.
Эллиот

Ответы:

300

Извините, EMS, но на самом деле я только что получил другой ответ из списка рассылки matplotlib (Спасибо Бенджамину Руту).

Код, который я ищу, настраивает вызов savefig на:

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

Похоже, это похоже на вызов тугой-тройки, но вместо этого вы позволяете savefig учитывать дополнительных исполнителей в расчете. Это на самом деле изменило размер фигуры по желанию.

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

Это производит:

[править] Цель этого вопроса состояла в том, чтобы полностью избежать использования произвольных координатных размещений произвольного текста, что было традиционным решением этих проблем. Несмотря на это, в последнее время многочисленные исправления настаивали на их внесении, часто так, чтобы код вызывал ошибку. Теперь я исправил ошибки и исправил произвольный текст, чтобы показать, как они также учитываются в алгоритме bbox_extra_artists.

jbbiomed
источник
1
/! \ Кажется, работает только начиная с matplotlib> = 1.0 (в Debian есть сжатие 0.99, и это не работает)
Julien Palard
1
Не могу заставить это работать :( Я передаю lgd для savefig, но он все еще не меняет размер. Возможно, проблема в том, что я не использую подзаговор.
6005
8
Ах! Мне просто нужно было использовать bbox_inches = "stretch", как вы и сделали. Спасибо!
6005
7
Это хорошо, но я все равно обрезаю свою фигуру, когда пытаюсь plt.show()это сделать. Любое исправление для этого?
Агостино,
23

Добавлено: я нашел кое-что, что должно было бы помочь, но остальная часть кода ниже также предлагает альтернативу.

Используйте subplots_adjust()функцию, чтобы переместить нижнюю часть субплота вверх:

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

Затем поиграйте со смещением в bbox_to_anchorчасти легенды команды легенды, чтобы получить поле легенды там, где вы хотите. Некоторая комбинация установки figsizeи использования subplots_adjust(bottom=...)должна создать качественный график для вас.

Альтернатива: я просто изменил строку:

fig = plt.figure(1)

чтобы:

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

и изменился

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

в

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

и это хорошо отображается на моем экране (24-дюймовый ЭЛТ-монитор).

Здесь figsize=(M,N)устанавливает окно рисунка, чтобы быть M дюймов на N дюймов. Просто играйте с этим, пока он не станет подходящим для вас. Преобразуйте его в более масштабируемый формат изображения и используйте GIMP для редактирования, если это необходимо, или просто обрежьте его с помощью viewportопции LaTeX при включении графики.

Ely
источник
Казалось бы, это лучшее решение в настоящее время, хотя для этого все еще требуется «играть, пока он не выглядит хорошо», что не является хорошим решением для генератора автоотчета. Я на самом деле уже использую это решение, реальная проблема в том, что matplotlib динамически не компенсирует легенду, находящуюся вне bbox оси. Как сказал @Joe, тугое_приятие должно учитывать больше функций, чем просто ось, заголовки и метки. Я мог бы добавить это как запрос функции на matplotlib.
jbbiomed
Я также работаю над тем, чтобы получить достаточно большую картинку, чтобы она соответствовала ранее отрезанным xlabels
Фредерик Норд
1
Вот документация с примером кода от matplotlib.org
Yojimbo
14

Вот еще одно, очень ручное решение. Вы можете определить размер оси, и отступы учитываются соответственно (включая условные обозначения и метки). Надеюсь, это кому-нибудь пригодится.

Пример (размер осей одинаковый!):

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

Код:

#==================================================
# 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
источник
Это не сработало для меня, пока я не сменил первое plt.draw()на ax.figure.canvas.draw(). Я не уверен почему, но до этого изменения размер легенды не обновлялся.
ws_e_c421
Если вы пытаетесь использовать это в окне графического интерфейса, вам нужно изменить fig.set_size_inches(widthTot,heightTot)на fig.set_size_inches(widthTot,heightTot, forward=True).
ws_e_c421