Lo spostamento della legenda del matplotlib all'esterno dell'asse lo interrompe dalla casella della figura


222

Conosco le seguenti domande:

Matplotlib savefig con una legenda fuori dalla trama

Come mettere la leggenda fuori dalla trama

Sembra che le risposte a queste domande abbiano il lusso di poter giocherellare con l'esatto restringimento dell'asse in modo che la leggenda si adatti.

Ridurre gli assi, tuttavia, non è una soluzione ideale perché rende i dati più piccoli rendendoli in realtà più difficili da interpretare; in particolare quando è complesso e ci sono molte cose in corso ... quindi necessita di una grande leggenda

L'esempio di una legenda complessa nella documentazione dimostra la necessità di ciò perché la legenda nel loro diagramma in realtà oscura completamente più punti dati.

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

Quello che vorrei poter fare è espandere dinamicamente le dimensioni della scatola delle figure per adattarle alla legenda delle figure in espansione.

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

Notate come l'etichetta finale 'Abbronzatura inversa' sia effettivamente fuori dalla scatola delle figure (e sembra gravemente tagliata, non di qualità di pubblicazione!) inserisci qui la descrizione dell'immagine

Infine, mi è stato detto che questo è un comportamento normale in R e LaTeX, quindi sono un po 'confuso perché questo è così difficile in Python ... C'è una ragione storica? Matlab è altrettanto povero su questo argomento?

Ho la versione (solo leggermente) più lunga di questo codice su pastebin http://pastebin.com/grVjc007


10
Per quanto riguarda il perché è perché matplotlib è orientato verso grafici interattivi, mentre R, ecc. Non lo sono. (E sì, Matlab è "ugualmente povero" in questo caso particolare.) Per farlo correttamente, devi preoccuparti di ridimensionare gli assi ogni volta che la figura viene ridimensionata, ingrandita o la posizione della legenda viene aggiornata. (In effetti, questo significa controllare ogni volta che viene disegnata la trama, il che porta a rallentamenti.) Ggplot, ecc., Sono statici, quindi è per questo che tendono a farlo di default, mentre matplotlib e matlab no. Ciò detto, tight_layout()dovrebbe essere modificato per tener conto delle leggende.
Joe Kington,

3
Sto anche discutendo questa domanda sulla mailing list degli utenti matplotlib. Quindi ho i suggerimenti per regolare la riga savefig su: fig.savefig ('samplefigure', bbox_extra_artists = (lgd,), bbox = 'tight')
jbbiomed

3
So che a matplotlib piace propagandare che tutto è sotto il controllo dell'utente, ma l'intera cosa con le leggende è troppo positiva. Se metto fuori la leggenda, ovviamente voglio che sia ancora visibile. La finestra dovrebbe ridimensionarsi per adattarsi invece di creare questa enorme seccatura di riscatto. Per lo meno dovrebbe esserci un'opzione True predefinita per controllare questo comportamento del ridimensionamento automatico. Costringere gli utenti a passare attraverso un numero ridicolo di rendering di nuovo per cercare di ottenere i numeri di scala proprio in nome del controllo compie il contrario.
Elliot

Risposte:


300

Mi dispiace EMS, ma in realtà ho appena ricevuto un'altra risposta dalla lista di mailling matplotlib (Grazie a Benjamin Root).

Il codice che sto cercando sta regolando la chiamata savefig su:

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

Questo è apparentemente simile al richiamo di tight_layout, ma invece consenti a savefig di considerare artisti extra nel calcolo. In effetti, questo ha ridimensionato la casella delle figure come desiderato.

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

Questo produce:

[modifica] L'intento di questa domanda era di evitare completamente l'uso di posizionamenti di coordinate arbitrarie di testo arbitrario, così come la soluzione tradizionale a questi problemi. Ciononostante, recentemente numerose modifiche hanno insistito per inserirle, spesso in modi che hanno portato all'errore del codice. Ora ho risolto i problemi e riordinato il testo arbitrario per mostrare come questi vengano considerati anche nell'algoritmo bbox_extra_artists.


1
/! \ Sembra funzionare solo da matplotlib> = 1.0 (la compressione Debian ha 0,99 e questo non funziona)
Julien Palard

1
Non riesco a farlo funzionare :( Passo lgd a savefig ma non si ridimensiona ancora. Il problema potrebbe essere che non sto usando un subplot.
6005

8
Ah! Avevo solo bisogno di usare bbox_inches = "tight" come hai fatto tu. Grazie!
6005

7
Questo è carino, ma continuo a farmi tagliare la figura quando provo a plt.show()farlo. Qualche correzione per quello?
Agostino,


23

Aggiunto: ho trovato qualcosa che dovrebbe fare subito il trucco, ma il resto del codice qui sotto offre anche un'alternativa.

Utilizzare la subplots_adjust()funzione per spostare la parte inferiore della sottotrama in alto:

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

Quindi gioca con l'offset nella bbox_to_anchorparte della legenda del comando legenda, per ottenere il riquadro della legenda dove lo desideri. Una combinazione di impostazione figsizee utilizzo di subplots_adjust(bottom=...)dovrebbe produrre una trama di qualità per te.

Alternativa: ho semplicemente cambiato la riga:

fig = plt.figure(1)

per:

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

e cambiato

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

per

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

e si presenta bene sul mio schermo (un monitor CRT da 24 pollici).

Qui figsize=(M,N)imposta la finestra della figura su M pollici per N pollici. Gioca con questo fino a quando non ti sembra giusto. Convertilo in un formato immagine più scalabile e usa GIMP per modificarlo se necessario, oppure ritaglia semplicemente con l' viewportopzione LaTeX quando includi la grafica.


Sembrerebbe che questa sia la soluzione migliore al momento, anche se richiede ancora 'suonare fino a quando non sembra buono' che non è una buona soluzione per un generatore di autoreport. In realtà utilizzo già questa soluzione, il vero problema è che matplotlib non compensa dinamicamente la legenda che si trova all'esterno della bbox dell'asse. Come ha detto @Joe, tight_layout dovrebbe tener conto di più funzioni rispetto ad assi, titoli e lables. Potrei aggiungere questo come una richiesta di funzionalità sul matplotlib.
jbbiomed

funziona anche per me ottenere un'immagine abbastanza grande da adattarsi agli xlabel precedentemente tagliati
Frederick Nord

1
ecco la documentazione con il codice di esempio da matplotlib.org
Yojimbo,

14

Ecco un'altra soluzione molto manuale. È possibile definire la dimensione dell'asse e le imbottiture sono considerate di conseguenza (compresi legenda e segni di spunta). Spero che sia utile a qualcuno.

Esempio (le dimensioni degli assi sono le stesse!):

inserisci qui la descrizione dell'immagine

Codice:

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

Questo non ha funzionato per me fino a quando ho cambiato il primo plt.draw()a ax.figure.canvas.draw(). Non sono sicuro del perché, ma prima di questa modifica la dimensione della legenda non veniva aggiornata.
ws_e_c421

Se si sta tentando di utilizzarlo in una finestra della GUI, è necessario passare fig.set_size_inches(widthTot,heightTot)a fig.set_size_inches(widthTot,heightTot, forward=True).
ws_e_c421
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.