Déplacer la légende de matplotlib en dehors de l'axe la rend coupée par la boîte à chiffres

222

Je connais les questions suivantes:

Matplotlib savefig avec une légende en dehors de l'intrigue

Comment sortir la légende de l'intrigue

Il semble que les réponses à ces questions aient le luxe de pouvoir jouer avec le rétrécissement exact de l'axe pour que la légende rentre.

Réduire les axes, cependant, n'est pas une solution idéale car il rend les données plus petites, ce qui les rend en fait plus difficiles à interpréter; surtout quand c'est complexe et qu'il y a beaucoup de choses qui se passent ... donc besoin d'une grande légende

L'exemple d'une légende complexe dans la documentation en démontre la nécessité car la légende de leur tracé masque en réalité complètement plusieurs points de données.

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

Ce que j'aimerais pouvoir faire, c'est agrandir dynamiquement la taille de la boîte de figurine pour accueillir la légende de la figure en expansion.

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

Remarquez comment l'étiquette finale 'Inverse tan' est en fait en dehors de la boîte à chiffres (et semble mal coupée - pas la qualité de la publication!) entrez la description de l'image ici

Enfin, on m'a dit que c'est un comportement normal dans R et LaTeX, donc je suis un peu confus pourquoi c'est si difficile en python ... Y a-t-il une raison historique? Matlab est-il également pauvre à ce sujet?

J'ai la version (seulement légèrement) plus longue de ce code sur pastebin http://pastebin.com/grVjc007

jbbiomed
la source
10
En ce qui concerne le pourquoi, c'est parce que matplotlib est orienté vers des graphiques interactifs, contrairement à R, etc. (Et oui, Matlab est "tout aussi pauvre" dans ce cas particulier.) Pour le faire correctement, vous devez vous soucier de redimensionner les axes chaque fois que la figure est redimensionnée, zoomée ou que la position de la légende est mise à jour. (En effet, cela signifie vérifier chaque fois que le tracé est tracé, ce qui conduit à des ralentissements.) Ggplot, etc., sont statiques, c'est pourquoi ils ont tendance à le faire par défaut, contrairement à matplotlib et matlab. Cela étant dit, tight_layout()il faudrait le modifier pour tenir compte des légendes.
Joe Kington
3
Je discute également de cette question sur la liste de diffusion des utilisateurs de matplotlib. J'ai donc les suggestions d'ajuster la ligne savefig à: fig.savefig ('samplefigure', bbox_extra_artists = (lgd,), bbox = 'tight')
jbbiomed
3
Je sais que matplotlib aime à vanter que tout est sous le contrôle de l'utilisateur, mais tout cela avec les légendes est trop une bonne chose. Si je mets la légende à l'extérieur, je veux évidemment qu'elle soit toujours visible. La fenêtre devrait simplement s'ajuster pour s'adapter au lieu de créer cet énorme problème de redimensionnement. Au minimum, il devrait y avoir une option True par défaut pour contrôler ce comportement de mise à l'échelle automatique. Forcer les utilisateurs à passer par un nombre ridicule de nouveaux rendus pour essayer d'obtenir les bons numéros d'échelle au nom du contrôle accomplit le contraire.
Elliot

Réponses:

300

Désolé EMS, mais je viens de recevoir une autre réponse de la liste de distribution de matplotlib (merci à Benjamin Root).

Le code que je recherche ajuste l'appel savefig à:

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

Ceci est apparemment similaire à l'appel de tight_layout, mais à la place, vous autorisez savefig à prendre en compte des artistes supplémentaires dans le calcul. Cela a en fait redimensionné la boîte de chiffres comme souhaité.

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

Cela produit:

[modifier] Le but de cette question était d'éviter complètement l'utilisation de placements de coordonnées arbitraires de texte arbitraire comme c'était la solution traditionnelle à ces problèmes. Malgré cela, de nombreuses modifications ont récemment insisté pour les insérer, souvent de manière à ce que le code génère une erreur. J'ai maintenant corrigé les problèmes et rangé le texte arbitraire pour montrer comment ceux-ci sont également pris en compte dans l'algorithme bbox_extra_artists.

jbbiomed
la source
1
/! \ Semble fonctionner uniquement depuis matplotlib> = 1.0 (Debian squeeze a 0.99 et cela ne fonctionne pas)
Julien Palard
1
Impossible de faire fonctionner ceci :( Je passe lgd à savefig mais il ne redimensionne toujours pas. Le problème est peut-être que je n'utilise pas de sous-intrigue.
6005
8
Ah! J'avais juste besoin d'utiliser bbox_inches = "tight" comme vous l'avez fait. Merci!
6005
7
C'est bien, mais je coupe toujours ma silhouette quand j'essaye plt.show(). Une solution pour ça?
Agostino
23

Ajouté: J'ai trouvé quelque chose qui devrait faire l'affaire immédiatement, mais le reste du code ci-dessous propose également une alternative.

Utilisez la subplots_adjust()fonction pour déplacer le bas de la sous-intrigue vers le haut:

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

Jouez ensuite avec le décalage dans la bbox_to_anchorpartie légende de la commande légende, pour obtenir la boîte de légende où vous le souhaitez. Une combinaison de la définition figsizeet de l'utilisation de subplots_adjust(bottom=...)devrait produire un tracé de qualité pour vous.

Alternative: j'ai simplement changé de ligne:

fig = plt.figure(1)

à:

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

et changé

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

à

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

et il apparaît très bien sur mon écran (un moniteur CRT de 24 pouces).

Ici figsize=(M,N), la fenêtre de la figure doit être M pouces par N pouces. Jouez avec ceci jusqu'à ce qu'il vous convienne. Convertissez-le en un format d'image plus évolutif et utilisez GIMP pour le modifier si nécessaire, ou recadrez simplement avec l' viewportoption LaTeX lorsque vous incluez des graphiques.

ely
la source
Il semblerait que ce soit la meilleure solution à l'heure actuelle, même si cela nécessite encore de «jouer jusqu'à ce qu'il paraisse bien», ce qui n'est pas une bonne solution pour un générateur de rapport automatique. En fait, j'utilise déjà cette solution, le vrai problème est que matplotlib ne compense pas dynamiquement la légende en dehors de la bbox de l'axe. Comme l'a dit @Joe, tight_layout devrait prendre en compte plus de fonctionnalités que les axes, les titres et les étiquettes. Je pourrais ajouter cela comme une demande de fonctionnalité sur le matplotlib.
jbbiomed
fonctionne également pour moi pour obtenir une image suffisamment grande pour s'adapter aux xlabels précédemment coupés
Frederick Nord
1
voici la documentation avec un exemple de code de matplotlib.org
Yojimbo
14

Voici une autre solution très manuelle. Vous pouvez définir la taille de l'axe et les rembourrages sont considérés en conséquence (y compris la légende et les graduations). J'espère que c'est utile à quelqu'un.

Exemple (la taille des axes est la même!):

entrez la description de l'image ici

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
la source
Cela n'a pas fonctionné pour moi jusqu'à ce que je change le premier plt.draw()en ax.figure.canvas.draw(). Je ne sais pas pourquoi, mais avant ce changement, la taille de la légende n'était pas mise à jour.
ws_e_c421
Si vous essayez de l'utiliser sur une fenêtre GUI, vous devez passer fig.set_size_inches(widthTot,heightTot)à fig.set_size_inches(widthTot,heightTot, forward=True).
ws_e_c421