Przesunięcie legendy matplotlib poza oś powoduje odcięcie jej od ramki rysunku

222

Znam następujące pytania:

Savepl Matplotlib z legendą poza fabułą

Jak wykluczyć legendę z fabuły

Wydaje się, że odpowiedzi na te pytania mają luksus polegający na tym, że mogą bawić się dokładnie skurczeniem osi, aby legenda pasowała.

Zmniejszenie osi nie jest jednak idealnym rozwiązaniem, ponieważ powoduje zmniejszenie danych i utrudnienie interpretacji; szczególnie gdy jest złożony i dzieje się wiele rzeczy ... dlatego potrzebna jest duża legenda

Przykład złożonej legendy w dokumentacji pokazuje, że jest to konieczne, 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

To, co chciałbym móc zrobić, to dynamicznie powiększać rozmiar pudełka z figurkami, aby uwzględnić legendę rozwijających się figur.

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

Zwróć uwagę, że końcowa etykieta „Inverse tan” faktycznie znajduje się poza ramką z rysunkami (i wygląda na źle odciętą - nie jakość publikacji!) wprowadź opis zdjęcia tutaj

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

Mam (tylko nieznacznie) dłuższą wersję tego kodu na pastebin http://pastebin.com/grVjc007

jbbiomed
źródło
10
O ile jest tak, ponieważ matplotlib jest nastawiony na interaktywne wykresy, podczas gdy R itp. Nie. (I tak, Matlab jest „równie słaby” w tym konkretnym przypadku.) Aby zrobić to poprawnie, musisz się martwić o zmianę rozmiaru osi za każdym razem, gdy zmienia się rozmiar, przybliża lub aktualizuje pozycję legendy. (W efekcie oznacza to sprawdzanie przy każdym narysowaniu wykresu, co prowadzi do spowolnienia). 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.
Joe Kington,
3
Omawiam również to pytanie na liście mailingowej użytkowników matplotlib. Mam więc sugestie dostosowania linii savefig do: fig.savefig ('samplefigure', bbox_extra_artists = (lgd,), bbox = 'tight')
jbbiomed
3
Wiem, że matplotlib lubi reklamować, że wszystko jest pod kontrolą użytkownika, ale cała ta legenda jest zbyt dobra. Jeśli wystawię legendę na zewnątrz, oczywiście chcę, aby była nadal widoczna. Okno powinno po prostu skalować się, aby pasowało, zamiast tworzyć ten ogromny problem przeskalowania. Przynajmniej powinna istnieć domyślna opcja True do kontrolowania tego zachowania automatycznego skalowania. Zmuszenie użytkowników do przejścia przez absurdalną liczbę ponownych renderowań w celu uzyskania liczb skalowanych w imieniu kontroli powoduje odwrotną sytuację.
Elliot

Odpowiedzi:

300

Przepraszam EMS, ale właściwie otrzymałem kolejną odpowiedź z listy mailling Matbliblib (dzięki dla Benjamina Roota).

Kod, którego szukam, dostosowuje wywołanie savefig do:

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

Jest to podobno podobne do wywoływania tight_layout, ale zamiast tego pozwalasz savefig na uwzględnienie dodatkowych wykonawców w obliczeniach. To faktycznie zmieniło rozmiar ramki rysunku według potrzeb.

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

Daje to:

[edytuj] Celem tego pytania było całkowite uniknięcie stosowania arbitralnego umieszczania współrzędnych dowolnego tekstu, podobnie jak tradycyjne rozwiązanie tych problemów. Mimo to ostatnio wiele edycji nalegało na ich wprowadzenie, często w sposób, który doprowadził do błędu w kodzie. Naprawiłem teraz problemy i uporządkowałem dowolny tekst, aby pokazać, w jaki sposób są one uwzględniane w algorytmie bbox_extra_artists.

jbbiomed
źródło
1
/! \ Wydaje się, że działa tylko od matplotlib> = 1.0 (squeeze Debiana ma 0.99 i to nie działa)
Julien Palard
1
Nie mogę tego uruchomić :( Przekazuję lgd do savefig, ale nadal nie zmienia rozmiaru. Problemem może być to, że nie używam podplotu.
6005
8
Ach! Po prostu musiałem użyć bbox_inches = "tight" tak jak ty. Dzięki!
6005
7
To miłe, ale wciąż próbuję wyciąć swoją sylwetkę plt.show(). Masz jakieś rozwiązanie?
Agostino,
23

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

Użyj subplots_adjust()funkcji, aby przesunąć dolną część wykresu w górę:

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

Następnie zagraj z przesunięciem w bbox_to_anchorczęści legendy polecenia legendy, aby uzyskać pole legendy tam, gdzie chcesz. Pewna kombinacja ustawienia figsizei używania subplots_adjust(bottom=...)powinna dać ci wykres jakości.

Alternatywnie: po prostu zmieniłem linię:

fig = plt.figure(1)

do:

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

i zmienił się

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 figury na M cali na N cali. Po prostu baw się tym, aż będzie to odpowiednie dla Ciebie. Przekształć go na bardziej skalowalny format obrazu i użyj GIMP do edycji, jeśli to konieczne, lub po prostu przytnij za pomocą viewportopcji LaTeX , jeśli dołączasz grafikę.

Ely
źródło
Wydawałoby się, że jest to obecnie najlepsze rozwiązanie, mimo że nadal wymaga „odtwarzania, aż będzie dobrze wyglądać”, co nie jest dobrym rozwiązaniem dla generatora autoreportu. Właściwie już używam tego rozwiązania, prawdziwy problem polega na tym, że matplotlib nie kompensuje dynamicznie legendy znajdującej się poza ramką osi. Jak powiedział @Joe, tight_layout powinno uwzględniać więcej funkcji niż tylko oś, tytuły i etykiety. Mogę dodać to jako żądanie funkcji na matplotlib.
jbbiomed
działa również dla mnie, aby uzyskać wystarczająco duży obraz, aby pasował do wcześniej odciętych xlabels
Frederick Nord
1
oto dokumentacja z przykładowym kodem z matplotlib.org
Yojimbo
14

Oto kolejne, bardzo ręczne rozwiązanie. Można zdefiniować rozmiar osi, a wypełnienia są odpowiednio uwzględniane (w tym legenda i znaczniki). Mam nadzieję, że komuś się przyda.

Przykład (rozmiar osi jest taki sam!):

wprowadź opis zdjęcia tutaj

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()
#--------------------------------------------------
#==================================================
gebbissimo
źródło
Nie działało to 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.
ws_e_c421
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).
ws_e_c421