Переміщення легенди про 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 ... Чи є історична причина? Чи настільки бідний Матлаб з цього питання?

У мене (лише трохи) довша версія цього коду на пастібі http://pastebin.com/grVjc007


10
Що стосується того, що це тому, що matplotlib орієнтований на інтерактивні сюжети, тоді як R тощо не є. (І так, Матлаб "настільки ж бідний" в даному конкретному випадку.) Щоб зробити це належним чином, вам потрібно потурбуватися про зміну розміру осей кожного разу, коли фігура змінюється, змінюється масштаб або змінюється положення легенди. (Фактично, це означає перевірку кожного разу, коли малюється сюжет, що призводить до уповільнення.) Ggplot тощо є статичними, тому вони, як правило, роблять це за замовчуванням, тоді як matplotlib і matlab не роблять. Це, як було сказано, tight_layout()слід змінити, щоб врахувати легенди.
Джо Кінгтон

3
Я також обговорюю це питання в списку розсилки користувачів matplotlib. Тож у мене є пропозиції змінити рядок savefig до: fig.savefig ('samplefigure', bbox_extra_artists = (lgd,), bbox = '
тяжкий

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.


1
/! \ Здається, працює тільки з matplotlib> = 1,0 (у Debian видавленні є 0,99, а це не працює)
Жульєн Палард

1
Не можу це зробити :( Я передаю lgd для savefig, але він все ще не змінює розмір. Проблема може бути в тому, що я не використовую subplot.
6005

8
Ах! Мені просто потрібно було використовувати bbox_inches = "тугий", як ти. Дякую!
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 на дюйм на M дюймів. Просто пограйте з цим, поки це не здасться вам правильним. Перетворіть його у більш масштабований формат зображення та використовуйте GIMP для редагування, якщо потрібно, або просто обріжте за допомогою viewportпараметра LaTeX, включаючи графіку.


Здавалося б, це найкраще рішення на даний момент, хоча воно все ще вимагає "гри, поки воно не виглядає добре", що не є хорошим рішенням для генератора автопорту. Я фактично вже використовую це рішення, справжня проблема полягає в тому, що 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()
#--------------------------------------------------
#==================================================

Це не працює для мене , поки я не змінив перший plt.draw()в ax.figure.canvas.draw(). Я не знаю чому, але до цієї зміни розмір легенди не оновлювався.
ws_e_c421

Якщо ви намагаєтеся використовувати це у вікні GUI, вам потрібно змінити fig.set_size_inches(widthTot,heightTot)в fig.set_size_inches(widthTot,heightTot, forward=True).
ws_e_c421
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.