Memindahkan legenda matplotlib di luar sumbu membuatnya terpotong oleh kotak gambar

222

Saya akrab dengan pertanyaan-pertanyaan berikut:

Matplotlib savefig dengan legenda di luar plot

Cara mengeluarkan legenda dari plot

Tampaknya jawaban dalam pertanyaan-pertanyaan ini memiliki kemewahan untuk bisa bermain-main dengan penyusutan sumbu yang tepat sehingga legenda cocok.

Mengecilkan sumbu, bagaimanapun, bukanlah solusi yang ideal karena membuat data lebih kecil sehingga sebenarnya lebih sulit untuk ditafsirkan; terutama ketika kompleks dan ada banyak hal yang terjadi ... maka perlu legenda besar

Contoh legenda kompleks dalam dokumentasi menunjukkan perlunya hal ini karena legenda dalam plot mereka benar-benar mengaburkan beberapa titik data.

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

Apa yang ingin saya lakukan adalah secara dinamis memperluas ukuran kotak gambar untuk mengakomodasi legenda angka yang sedang berkembang.

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

Perhatikan bagaimana label akhir 'Inverse tan' sebenarnya di luar kotak gambar (dan terlihat sangat terpotong - bukan kualitas publikasi!) masukkan deskripsi gambar di sini

Akhirnya, saya telah diberitahu bahwa ini adalah perilaku normal dalam R dan LaTeX, jadi saya sedikit bingung mengapa ini sangat sulit dengan python ... Apakah ada alasan historis? Apakah Matlab sama-sama miskin dalam hal ini?

Saya memiliki (hanya sedikit) versi yang lebih panjang dari kode ini di pastebin http://pastebin.com/grVjc007

teringat
sumber
10
Sejauh mengapa itu karena matplotlib diarahkan untuk plot interaktif, sedangkan R, dll, tidak. (Dan ya, Matlab "sama-sama miskin" dalam kasus khusus ini.) Untuk melakukannya dengan benar, Anda perlu khawatir tentang mengubah ukuran sumbu setiap kali gambar diubah ukurannya, diperbesar, atau posisi legenda diperbarui. (Secara efektif, ini berarti memeriksa setiap kali plot dibuat, yang mengarah ke perlambatan.) Ggplot, dll, bersifat statis, jadi itu sebabnya mereka cenderung melakukan ini secara default, sedangkan matplotlib dan matlab tidak. Yang telah dikatakan, tight_layout()harus diubah untuk memperhitungkan legenda.
Joe Kington
3
Saya juga membahas pertanyaan ini di milis pengguna matplotlib. Jadi saya punya saran untuk menyesuaikan baris savefig ke: fig.savefig ('samplefigure', bbox_extra_artists = (lgd,), bbox = 'ketat')
jbbiomed
3
Saya tahu matplotlib suka menggembar-gemborkan bahwa semuanya berada di bawah kendali pengguna, tetapi semua ini dengan legenda terlalu banyak hal yang baik. Jika saya menempatkan legenda di luar, saya jelas ingin itu tetap terlihat. Jendela seharusnya cukup menyesuaikan ukurannya dan bukannya membuat kerepotan yang sangat besar ini. Paling tidak harus ada opsi True true untuk mengontrol perilaku autoscaling ini. Memaksa pengguna untuk melakukan sejumlah rendering ulang yang konyol untuk mencoba dan mendapatkan angka skala tepat atas nama kontrol menghasilkan kebalikannya.
Elliot

Jawaban:

300

Maaf EMS, tapi saya sebenarnya hanya mendapat respons lain dari daftar mailling matplotlib (Terima kasih keluar untuk Benjamin Root).

Kode yang saya cari adalah menyesuaikan panggilan savefig ke:

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

Ini tampaknya mirip dengan memanggil tight_layout, tetapi sebaliknya Anda mengizinkan savefig untuk mempertimbangkan artis tambahan dalam perhitungan. Ini memang mengubah ukuran kotak gambar seperti yang diinginkan.

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

Ini menghasilkan:

[Sunting] Maksud dari pertanyaan ini adalah untuk sepenuhnya menghindari penggunaan penempatan koordinat sewenang-wenang dari teks sewenang-wenang seperti solusi tradisional untuk masalah ini. Meskipun demikian, banyak suntingan baru-baru ini bersikeras menempatkan ini, sering dengan cara yang menyebabkan kode meningkatkan kesalahan. Saya sekarang telah memperbaiki masalah dan merapikan teks arbitrer untuk menunjukkan bagaimana ini juga dipertimbangkan dalam algoritma bbox_extra_artists.

teringat
sumber
1
/! \ Tampaknya hanya berfungsi sejak matplotlib> = 1.0 (Pemerasan Debian memiliki 0,99 dan ini tidak berfungsi)
Julien Palard
1
Tidak bisa menjalankan ini :( Saya memberikan lgd ke savefig tetapi masih belum mengubah ukuran. Masalahnya mungkin saya tidak menggunakan subplot.
6005
8
Ah! Saya hanya perlu menggunakan bbox_inches = "ketat" seperti yang Anda lakukan. Terima kasih!
6005
7
Ini bagus, tetapi saya masih bisa memotong figur saya ketika saya mencobanya plt.show(). Adakah perbaikan untuk itu?
Agostino
23

Ditambahkan: Saya menemukan sesuatu yang harus melakukan trik segera, tetapi sisa kode di bawah ini juga menawarkan alternatif.

Gunakan subplots_adjust()fungsi ini untuk memindahkan bagian bawah subplot ke atas:

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

Kemudian mainkan dengan offset di bagian legenda bbox_to_anchordari perintah legenda, untuk mendapatkan kotak legenda di tempat yang Anda inginkan. Beberapa kombinasi pengaturan figsizedan penggunaan subplots_adjust(bottom=...)harus menghasilkan plot kualitas untuk Anda.

Alternatif: Saya cukup mengubah baris:

fig = plt.figure(1)

untuk:

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

dan berubah

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

untuk

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

dan itu muncul dengan baik di layar saya (monitor CRT 24 inci).

Di sini figsize=(M,N)mengatur jendela gambar menjadi M inci demi N inci. Mainkan saja ini sampai terlihat cocok untuk Anda. Konversikan ke format gambar yang lebih skalabel dan gunakan GIMP untuk mengedit jika perlu, atau potong saja dengan viewportopsi LaTeX saat menyertakan gambar.

Ely
sumber
Tampaknya ini adalah solusi terbaik pada saat ini, meskipun masih membutuhkan 'bermain sampai terlihat bagus' yang bukan solusi yang baik untuk generator autoreport. Saya sebenarnya sudah menggunakan solusi ini, masalah sebenarnya adalah matplotlib tidak secara dinamis mengimbangi legenda yang berada di luar bbox sumbu. Seperti yang dikatakan @Joe, tight_layout harus mempertimbangkan lebih banyak fitur daripada sekadar sumbu, judul, dan label. Saya mungkin menambahkan ini sebagai permintaan fitur di matplotlib.
jbbiomed
juga bekerja bagi saya untuk mendapatkan gambar yang cukup besar agar sesuai dengan xlabel yang sebelumnya terputus
Frederick Nord
1
di sini adalah dokumentasi dengan kode contoh dari matplotlib.org
Yojimbo
14

Berikut ini adalah solusi lain yang sangat manual. Anda dapat menentukan ukuran sumbu dan bantalan dianggap sesuai (termasuk legenda dan tanda centang). Semoga bermanfaat bagi seseorang.

Contoh (ukuran sumbu sama!):

masukkan deskripsi gambar di sini

Kode:

#==================================================
# 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
sumber
Ini tidak berhasil bagi saya sampai saya mengubah yang pertama plt.draw()menjadi ax.figure.canvas.draw(). Saya tidak yakin mengapa, tetapi sebelum perubahan ini ukuran legenda tidak diperbarui.
ws_e_c421
Jika Anda mencoba untuk menggunakan ini pada jendela GUI, Anda perlu perubahan fig.set_size_inches(widthTot,heightTot)untuk fig.set_size_inches(widthTot,heightTot, forward=True).
ws_e_c421