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

178

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

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

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

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

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

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

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

What I would like to be able to do is dynamically expand the size of the figure box to accommodate the expanding figure legend.

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

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

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

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

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

Ваш Ответ

3   ответа
248

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.3,1, "test", transform=ax.transAxes)
ax.set_title("Trigonometry")
ax.grid('on')
fig.savefig('samplefigure', bbox_extra_artists=(lgd,text), bbox_inches='tight')

Это производит:https: //imgur.com/xzd8G8

Не могу заставить это работать :( Я передаю lgd для savefig, но он все еще не меняет размер. Возможно, проблема в том, что я не использую подзагово 6005
Ah! Мне просто нужно было использовать bbox_inches = "thin", как вы это и сделали. Благодарность 6005
/! \ Кажется, работает только с matplotlib> = 1.0 (в Debian squeeze 0,99, и это не работает) Julien Palard
Это хорошо, но я все равно обрезаю свою фигуру, когда пытаюсьplt.show() Это. Любое исправление для этого? Agostino
19

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

Использоватьsubplots_adjust()ункция @ для перемещения нижней части подзадачи вверх:

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

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

Alternative: Я просто изменил строку:

fig = plt.figure(1)

to:

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 для редактирования при необходимости или просто обрежьте с помощью LaTeXviewport опция при включении графики.

Может показаться, что это лучшее решение в настоящее время, хотя для этого все еще требуется «играть до тех пор, пока оно не будет выглядеть хорошо», что не является хорошим решением для генератора автоотчета. Я на самом деле уже использую это решение, реальная проблема в том, что matplotlib динамически не компенсирует легенду, находящуюся вне bbox оси. Как сказал Джо, тудолже учитывать больше функций, чем просто ось, заголовки и метки. Я мог бы добавить это как запрос функции на matplotlib. jbbiomed
Во - документация с примером кода от matplotlib.org Yojimbo
Кроме того, я работаю над тем, чтобы получить достаточно большую картинку, чтобы она соответствовала ранее отрезанным xlabels Frederick Nord
13

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

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

Код

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

Похожие вопросы