Wenn Sie die Matplotlib-Legende außerhalb der Achse verschieben, wird sie durch das Figurenfeld abgeschnitten

222

Ich kenne folgende Fragen:

Matplotlib savefig mit einer Legende außerhalb des Plots

Wie man die Legende aus der Handlung entfernt

Es scheint, dass die Antworten auf diese Fragen den Luxus haben, mit dem exakten Schrumpfen der Achse herumspielen zu können, damit die Legende passt.

Das Verkleinern der Achsen ist jedoch keine ideale Lösung, da dadurch die Daten kleiner werden und die Interpretation tatsächlich schwieriger wird. besonders wenn es komplex ist und viele Dinge vor sich gehen ... daher braucht es eine große Legende

Das Beispiel einer komplexen Legende in der Dokumentation zeigt, dass dies erforderlich ist, da die Legende in ihrem Diagramm tatsächlich mehrere Datenpunkte vollständig verdeckt.

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

Ich möchte in der Lage sein, die Größe des Figurenfelds dynamisch zu erweitern, um die expandierende Figurenlegende aufzunehmen.

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

Beachten Sie, dass sich das endgültige Etikett 'Inverse Tan' tatsächlich außerhalb des Zahlenfelds befindet (und schlecht abgeschnitten aussieht - keine Veröffentlichungsqualität!) Geben Sie hier die Bildbeschreibung ein

Schließlich wurde mir gesagt, dass dies in R und LaTeX normal ist, daher bin ich ein wenig verwirrt, warum dies in Python so schwierig ist ... Gibt es einen historischen Grund? Ist Matlab in dieser Angelegenheit genauso arm?

Ich habe die (nur geringfügig) längere Version dieses Codes auf Pastebin http://pastebin.com/grVjc007

jbbiomed
quelle
10
Was das Warum betrifft, liegt es daran, dass matplotlib auf interaktive Plots ausgerichtet ist, während R usw. dies nicht sind. (Und ja, Matlab ist in diesem speziellen Fall "gleich schlecht".) Um dies richtig zu machen, müssen Sie sich jedes Mal um die Größenänderung der Achsen kümmern, wenn die Größe der Figur geändert, gezoomt oder die Position der Legende aktualisiert wird. (Tatsächlich bedeutet dies, jedes Mal zu überprüfen, wenn der Plot gezeichnet wird, was zu Verlangsamungen führt.) Ggplot usw. sind statisch, weshalb sie dies standardmäßig tun, Matplotlib und Matlab dagegen nicht. Dies tight_layout()sollte geändert werden, um Legenden zu berücksichtigen.
Joe Kington
3
Ich diskutiere diese Frage auch auf der Mailingliste der matplotlib-Benutzer. Ich habe also die Vorschläge, die savefig-Zeile an folgende Adresse anzupassen: fig.savefig ('samplefigure', bbox_extra_artists = (lgd,), bbox = 'tight')
jbbiomed
3
Ich weiß, dass matplotlib gerne ankündigt, dass alles unter der Kontrolle des Benutzers steht, aber diese ganze Sache mit den Legenden ist zu viel des Guten. Wenn ich die Legende nach draußen stelle, möchte ich natürlich, dass sie immer noch sichtbar ist. Das Fenster sollte sich nur selbst skalieren, um zu passen, anstatt diesen großen Aufwand beim erneuten Skalieren zu verursachen. Zumindest sollte es eine Standardoption True geben, um dieses automatische Skalierungsverhalten zu steuern. Wenn Benutzer gezwungen werden, eine lächerliche Anzahl von Renderings zu durchlaufen, um zu versuchen, die Skalennummern im Namen der Steuerung richtig zu machen, wird das Gegenteil erreicht.
Elliot

Antworten:

300

Tut mir leid, EMS, aber ich habe gerade eine weitere Antwort von der Matplotlib-Mailling-Liste erhalten (Dank geht an Benjamin Root).

Der gesuchte Code passt den savefig-Aufruf an:

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

Dies ähnelt anscheinend dem Aufruf von tight_layout, aber stattdessen erlauben Sie savefig, zusätzliche Künstler bei der Berechnung zu berücksichtigen. Dadurch wurde die Größe des Figurenfelds tatsächlich wie gewünscht geändert.

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

Dies erzeugt:

[Bearbeiten] Mit dieser Frage sollte die Verwendung beliebiger Koordinatenplatzierungen von beliebigem Text vollständig vermieden werden, wie dies bei der herkömmlichen Lösung dieser Probleme der Fall war. Trotzdem haben in letzter Zeit zahlreiche Änderungen darauf bestanden, diese einzufügen, oft auf eine Weise, die dazu führte, dass der Code einen Fehler auslöste. Ich habe jetzt die Probleme behoben und den beliebigen Text aufgeräumt, um zu zeigen, wie diese auch im Algorithmus bbox_extra_artists berücksichtigt werden.

jbbiomed
quelle
1
/! \ Scheint nur zu funktionieren, da matplotlib> = 1.0 (Debian Squeeze hat 0.99 und das funktioniert nicht)
Julien Palard
1
Ich kann das nicht zum Laufen bringen :( Ich übergebe lgd an savefig, aber die Größe wird immer noch nicht geändert. Das Problem kann sein, dass ich keine Nebenhandlung verwende.
6005
8
Ah! Ich musste nur bbox_inches = "tight" verwenden, wie Sie es getan haben. Vielen Dank!
6005
7
Das ist schön, aber ich bekomme immer noch meine Figur geschnitten, wenn ich es versuche plt.show(). Irgendeine Lösung dafür?
Agostino
23

Hinzugefügt: Ich habe etwas gefunden, das den Trick sofort ausführen sollte, aber der Rest des folgenden Codes bietet auch eine Alternative.

Verwenden Sie die subplots_adjust()Funktion, um den unteren Rand des Unterplots nach oben zu verschieben:

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

Spielen Sie dann mit dem Versatz im Legendenteil bbox_to_anchordes Legendenbefehls, um das Legendenfeld an die gewünschte Stelle zu bringen. Eine Kombination aus Einstellen figsizeund Verwenden von subplots_adjust(bottom=...)sollte ein Qualitätsdiagramm für Sie erstellen.

Alternative: Ich habe einfach die Zeile geändert:

fig = plt.figure(1)

zu:

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

und geändert

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

zu

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

und es wird gut auf meinem Bildschirm angezeigt (ein 24-Zoll-CRT-Monitor).

Hier wird figsize=(M,N)das Figurenfenster auf M Zoll mal N Zoll eingestellt. Spielen Sie einfach damit, bis es für Sie richtig aussieht. Konvertieren Sie es in ein skalierbareres Bildformat und verwenden Sie GIMP, um es bei Bedarf zu bearbeiten, oder schneiden Sie es einfach mit der LaTeX- viewportOption zu, wenn Sie Grafiken einfügen.

ely
quelle
Es scheint, dass dies derzeit die beste Lösung ist, obwohl es immer noch erforderlich ist, "zu spielen, bis es gut aussieht", was für einen Autoreport-Generator keine gute Lösung ist. Ich verwende diese Lösung bereits. Das eigentliche Problem ist, dass matplotlib die Legende außerhalb der bbox der Achse nicht dynamisch kompensiert. Wie @Joe sagte, sollte tight_layout mehr Funktionen berücksichtigen als nur Achsen, Titel und Etiketten. Ich könnte dies als Feature-Anfrage auf der Matplotlib hinzufügen.
jbbiomed
funktioniert auch für mich, um ein Bild zu bekommen, das groß genug ist, um zu den zuvor abgeschnittenen xlabels zu passen
Frederick Nord
1
Hier ist die Dokumentation mit Beispielcode von matplotlib.org
Yojimbo
14

Hier ist eine andere, sehr manuelle Lösung. Sie können die Größe der Achse definieren und die Auffüllungen werden entsprechend berücksichtigt (einschließlich Legende und Häkchen). Hoffe, es ist für jemanden von Nutzen.

Beispiel (Achsengröße sind gleich!):

Geben Sie hier die Bildbeschreibung ein

Code:

#==================================================
# 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
quelle
Das hat bei mir nicht funktioniert, bis ich das erste geändert plt.draw()habe ax.figure.canvas.draw(). Ich bin mir nicht sicher warum, aber vor dieser Änderung wurde die Legendengröße nicht aktualisiert.
ws_e_c421
Wenn Sie versuchen , diese auf einem GUI - Fenster zu verwenden, müssen Sie Änderungen fig.set_size_inches(widthTot,heightTot)an fig.set_size_inches(widthTot,heightTot, forward=True).
ws_e_c421