web-dev-qa-db-fra.com

Déplacer la légende de matplotlib en dehors de l'axe la coupe par la boîte de la figure

Je connais les questions suivantes:

Matplotlib savefig avec une légende en dehors du tracé

Comment sortir la légende de l'intrigue

Il semble que les réponses à ces questions ont le luxe de pouvoir jouer avec le rétrécissement exact de l’axe pour que la légende s’intègre.

Réduire les axes, cependant, n'est pas une solution idéale car cela réduit la taille des données, ce qui rend leur interprétation plus difficile. surtout quand c'est complexe et qu'il y a beaucoup de choses qui se passent ... donc nécessitant 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 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 de manière dynamique la taille de la boîte de caractères afin de l'adapter à la légende des chiffres 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')

Notez que l'étiquette finale "Inverse tan" est en dehors de la zone des chiffres (et a l'air mal coupée - pas de qualité de publication!) enter image description here

Enfin, on m'a dit que c'était un comportement normal dans R et LaTeX, alors je suis un peu confus pourquoi c'est si difficile en python ... Y a-t-il une raison historique? Matlab est-il également pauvre en la matière?

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

197
jbbiomed

Désolé, EMS, mais je viens tout juste de recevoir une autre réponse de la liste de diffusion matplotlib (Merci à Benjamin Root).

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

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

Cela ressemble apparemment à appeler tight_layout, mais vous permettez à savefig de prendre en compte des artistes supplémentaires dans le calcul. Cela a effectivement redimensionné la boîte de la figure comme vous le souhaitez.

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] L'intention de cette question était d'éviter complètement l'utilisation d'emplacements 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 intégrer, souvent de manière à ce que le code génère une erreur. J'ai maintenant corrigé les problèmes et mis de l'ordre dans le texte arbitraire pour montrer comment ils sont également pris en compte dans l'algorithme bbox_extra_artists.

266
jbbiomed

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

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

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

Puis jouez avec le décalage dans la légende bbox_to_anchor partie de la commande de légende, pour obtenir la boîte de légende où vous le souhaitez. Une combinaison du réglage de figsize et de l’utilisation de subplots_adjust(bottom=...) devrait produire un tracé de qualité pour vous.

Alternative: J'ai simplement changé la 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 bien sur mon écran (un moniteur CRT de 24 pouces).

Ici, figsize=(M,N) définit la fenêtre de figure sur M pouces sur N pouces. Il suffit de jouer avec cela jusqu'à ce que cela vous convient Convertissez-le en un format d'image plus évolutif et utilisez GIMP pour le modifier si nécessaire, ou recadrez-le simplement avec l'option LaTeX viewport lorsque vous incluez des graphiques.

21
ely

Voici une autre solution très manuelle. Vous pouvez définir la taille de l'axe et les marges sont considérées 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!):

enter image description here

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