2012-04-11 16 views
139

jestem zaznajomiony z następującymi pytaniami:Moving matplotlib legenda zewnątrz osi sprawia, że ​​odcięcie przez pole figury

Matplotlib savefig with a legend outside the plot

How to put the legend out of the plot

Wydaje się, że odpowiedzi na te pytania mają luksus bycia na skrzypcach z dokładnym kurczeniem się osi, tak aby legenda pasowała.

Zmniejszenie osi nie jest jednak rozwiązaniem idealnym, ponieważ powoduje, że dane są mniejsze, co utrudnia interpretację; szczególnie, gdy jest to skomplikowane i wiele się dzieje ... dlatego potrzeba dużej legendy.

Przykład złożonej legendy w dokumentacji pokazuje, że jest to potrzebne, ponieważ legenda na ich wykresie faktycznie całkowicie zasłania wiele punktów danych .

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

Co chciałbym być w stanie zrobić to dynamicznie rozwijać rozmiaru okna rysunku, aby pomieścić rozszerzający postać legendy.

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

Wskazówki jak Ostateczna etykieta „Inverse tan” jest rzeczywiście poza pole figury (i wygląda źle odcięcia - nie jakość publikacji) enter image description here

W końcu powiedziano mi, że jest to normalne zachowanie w R i LaTeX, więc jestem trochę zdezorientowany, dlaczego to jest tak trudne w python ... Czy istnieje historyczny powód? Czy Matlab jest równie biedny w tej sprawie?

Mam (tylko nieznacznie) już wersję tego kodu na pastebin http://pastebin.com/grVjc007

+6

Jeśli chodzi o to dlaczego to dlatego matplotlib jest ukierunkowany interaktywnych wykresów, natomiast R, etc, nie są. (I tak, Matlab jest "tak samo biedny" w tym konkretnym przypadku.) Aby zrobić to poprawnie, musisz martwić się o zmianę rozmiaru osi za każdym razem, gdy figura jest zmieniana, powiększana lub pozycja legendy jest aktualizowana. (Skutecznie oznacza to sprawdzanie za każdym razem, gdy narysowany jest wykres, co prowadzi do spowolnień.) Ggplot itp. Są statyczne, dlatego zazwyczaj robią to domyślnie, podczas gdy matplotlib i matlab nie. To powiedziawszy, 'tight_layout()' należy zmienić, aby uwzględnić legendy. –

+2

Omawiam również to pytanie na liście dyskusyjnej użytkowników matplotlib. Mam więc sugestie dotyczące dostosowania linii savefig do: fig.savefig ('samplefigure', bbox_extra_artists = (lgd,), bbox = 'tight') – jbbiomed

+3

Wiem, że matplotlib lubi twierdzić, że wszystko jest pod kontrolą użytkownik, ale cała ta historia z legendami jest zbyt dobra. Jeśli odłożę legendę na zewnątrz, oczywiście chcę, aby nadal była widoczna. Okno powinno po prostu skalować się tak, aby pasowało, zamiast tworzyć ten ogromny problem z przesuwaniem.Przynajmniej powinna istnieć domyślna opcja True kontrolująca to zachowanie autoskalowania. Zmuszanie użytkowników do przejścia przez absurdalną liczbę ponownych renderowań, aby spróbować uzyskać numery skali w imieniu kontroli, działa odwrotnie. – Elliot

Odpowiedz

190

Przepraszam, EMS, ale tak naprawdę dostałem kolejną odpowiedź z listy maplotlib mailing (podziękowania dla Benjamina Roota).

Kod szukam jest dostosowanie połączenia savefig do:

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

To pozornie podobna do wywoływania tight_layout, ale zamiast pozwolić savefig rozważyć dodatkowe artystów w obliczeniach. W rzeczywistości zmieniło to rozmiar ramki na figurze.

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') 
handles, labels = ax.get_legend_handles_labels() 
lgd = ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5,-0.1)) 
ax.grid('on') 
fig.savefig('samplefigure', bbox_extra_artists=(lgd,), bbox_inches='tight') 

ta produkuje:

enter image description here

+1

/! \ Wydaje się działać tylko od Matplotlib> = 1.0 (przeciążenie Debiana ma 0.99 i to nie działa) –

+1

Nie można tego uruchomić :(Przechodzę w lgd, aby zapisać, ale nadal nie zmienia się rozmiar. może być, ja nie używam podpunktu – 6005

+4

Ah! Właśnie potrzebowałem użyć bbox_inches = "ciasny" jak ty. Dzięki! – 6005

13

Dodano: znalazłem coś, co powinno załatwić sprawę od razu, ale reszta kodu poniżej oferuje również alternatywę.

Użyj funkcji subplots_adjust() przenieść dno poletko up:

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

Następnie grać z przesunięciem w legendzie bbox_to_anchor część polecenia legendy, aby uzyskać pole legendy gdzie chcesz. Niektóre kombinacje ustawień figsize i korzystania z subplots_adjust(bottom=...) powinny wytworzyć dla Ciebie jakość wydruku.

Alternatywa: po prostu zmienił linię:

fig = plt.figure(1) 

do:

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

i zmienił

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

do

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

i pokazuje się dobrze na moim ekranie (24-calowy monitor CRT).

Tutaj figsize=(M,N) ustawia okno rysunku na M cale o N cali. Po prostu baw się tym, dopóki nie będzie dla ciebie odpowiedni. Przekształć go w bardziej skalowalny format obrazu i użyj GIMP do edycji, jeśli to konieczne, lub po prostu przycinaj przy użyciu opcji LaTeX viewport, gdy dołączasz grafikę.

+0

Wygląda na to, że jest to najlepsze rozwiązanie w obecnej chwili, mimo że nadal wymaga "gry, dopóki nie wygląda dobrze", co nie jest dobrym rozwiązaniem dla generatora autoreport. Właściwie to już używam tego rozwiązania, prawdziwym problemem jest to, że matplotlib nie rekompensuje dynamicznie legendy znajdującej się poza bboxem osi. Jak powiedział @Joe, tight_layout * powinien * brać pod uwagę więcej funkcji niż tylko oś, tytuły i lables. Mogę dodać to jako żądanie funkcji na matplotlib. – jbbiomed

+0

działa również dla mnie, aby uzyskać wystarczająco duży obraz, aby pasował do uprzednio odciętych xlabeli –

+1

[tutaj] (http://matplotlib.org/examples/pylab_examples/subplots_adjust.html) to dokumentacja z przykładowym kodem z matplotlib.org – Yojimbo

11

Oto kolejny, bardzo instrukcja rozwiązanie. Możesz zdefiniować rozmiar osi i paddings są odpowiednio uwzględniane (w tym legenda i tickmarks). Mam nadzieję, że przyda się komuś.

Przykład (rozmiar osie są takie same!):

enter image description here

Kod:

#================================================== 
# 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() 
#-------------------------------------------------- 
#================================================== 
+0

To nie działało dla mnie, dopóki nie zmieniłem pierwszego 'plt.draw()' na 'ax.figure.canvas.draw()'. Nie jestem pewien dlaczego, ale przed tą zmianą rozmiar legendy nie był aktualizowany. –

+0

Jeśli próbujesz użyć tego w oknie GUI, musisz zmienić 'fig.set_size_inches (widthTot, heightTot)' na 'fig.set_size_inches (widthTot, heightTot, forward = True)'. –

Powiązane problemy