Mover a legenda do matplotlib para fora do eixo faz com que seja cortado pela caixa de figuras

222

Estou familiarizado com as seguintes perguntas:

Matplotlib savefig com uma legenda fora da plotagem

Como colocar a lenda fora da trama

Parece que as respostas nessas perguntas têm o luxo de poder mexer com o encolhimento exato do eixo, para que a legenda se encaixe.

Diminuir os eixos, no entanto, não é uma solução ideal, pois diminui os dados, dificultando a interpretação; particularmente quando é complexo e há muitas coisas acontecendo ... portanto, precisamos de uma grande lenda

O exemplo de uma legenda complexa na documentação demonstra a necessidade disso, porque a legenda em sua plotagem na verdade obscurece completamente vários pontos de dados.

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

O que eu gostaria de poder fazer é expandir dinamicamente o tamanho da caixa de figuras para acomodar a legenda da figura em expansão.

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

Observe como o rótulo final 'bronzeado inverso' está realmente fora da caixa de figuras (e parece muito ruim - não qualidade de publicação!) insira a descrição da imagem aqui

Finalmente, me disseram que esse é um comportamento normal no R e no LaTeX, então estou um pouco confuso por que isso é tão difícil em python ... Existe uma razão histórica? O Matlab é igualmente pobre nesse assunto?

Eu tenho a versão (apenas um pouco) mais longa desse código em pastebin http://pastebin.com/grVjc007

jbbiomed
fonte
10
Quanto ao porquê, é porque o matplotlib é voltado para gráficos interativos, enquanto R, etc, não são. (E sim, o Matlab é "igualmente pobre" nesse caso em particular.) Para fazer isso corretamente, você precisa se preocupar em redimensionar os eixos sempre que a figura for redimensionada, ampliada ou a posição da legenda for atualizada. (Efetivamente, isso significa verificar todas as vezes que o gráfico é desenhado, o que leva a lentidão.) Ggplot, etc, é estático; é por isso que eles tendem a fazer isso por padrão, enquanto matplotlib e matlab não. Dito isto, tight_layout()deve ser alterado para levar em consideração as lendas.
21812 Joe Kington
3
Também estou discutindo esta questão na lista de discussão dos usuários do matplotlib. Então, eu tenho as sugestões de ajuste da linha savefig para: fig.savefig ( 'samplefigure', bbox_extra_artists = (LGD,), bbox = 'apertado')
jbbiomed
3
Eu sei que o matplotlib gosta de dizer que tudo está sob o controle do usuário, mas essa coisa toda com as lendas é muito boa. Se eu colocar a lenda do lado de fora, obviamente quero que ela ainda seja visível. A janela deve ser dimensionada para caber em vez de criar esse enorme aborrecimento de redimensionamento. No mínimo, deve haver uma opção True padrão para controlar esse comportamento de dimensionamento automático. Forçar os usuários a passar por um número ridículo de repetições para tentar acertar os números da escala em nome do controle é o contrário.
Elliot

Respostas:

300

Desculpe o EMS, mas na verdade recebi outra resposta da lista de emails matplotlib (Agradecemos a Benjamin Root).

O código que estou procurando é ajustar a chamada savefig para:

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

Aparentemente, isso é semelhante a chamar tight_layout, mas você permite que o savefig considere artistas extras no cálculo. Na verdade, isso redimensionou a caixa de figuras conforme desejado.

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

Isso produz:

[editar] O objetivo desta pergunta era evitar completamente o uso de posicionamentos arbitrários de coordenadas de texto arbitrário, como era a solução tradicional para esses problemas. Apesar disso, várias edições recentemente insistiram em inseri-las, geralmente de maneiras que levaram o código a gerar um erro. Corrigi os problemas e arrumei o texto arbitrário para mostrar como eles também são considerados no algoritmo bbox_extra_artists.

jbbiomed
fonte
1
/! \ Parece funcionar somente desde matplotlib> = 1.0 (o squeeze do Debian tem 0,99 e isso não funciona) #
Julien Palard
1
Não é possível fazer isso funcionar :( Eu passo o lgd para savefig, mas ele ainda não é redimensionado. O problema pode ser que eu não estou usando uma subtrama.
6005
8
Ah! Eu só precisava usar bbox_inches = "tight" como você fez. Obrigado!
6005
7
Isso é legal, mas continuo cortando minha figura quando tento plt.show(). Alguma correção para isso?
Agostino
23

Adicionado: Encontrei algo que deve resolver o problema imediatamente, mas o restante do código abaixo também oferece uma alternativa.

Use a subplots_adjust()função para mover a parte inferior da subparcela para cima:

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

Em seguida, brinque com o deslocamento na bbox_to_anchorparte da legenda do comando legend, para obter a caixa de legenda onde desejar. Alguma combinação de definir figsizee usar o subplots_adjust(bottom=...)deve produzir um gráfico de qualidade para você.

Alternativo: eu simplesmente mudei a linha:

fig = plt.figure(1)

para:

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

e mudou

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

para

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

e aparece bem na minha tela (um monitor CRT de 24 polegadas).

Aqui figsize=(M,N)define a janela da figura como M polegadas por N polegadas. Apenas brinque com isso até parecer certo para você. Converta-o para um formato de imagem mais escalável e use o GIMP para editar, se necessário, ou apenas recorte com a viewportopção LaTeX ao incluir gráficos.

ely
fonte
Parece que esta é a melhor solução no momento atual, mesmo que ainda exija 'jogar até ficar com boa aparência', o que não é uma boa solução para um gerador de relatório automático. Na verdade, eu já uso essa solução, o problema real é que o matplotlib não compensa dinamicamente a legenda estar fora da bbox do eixo. Como o @Joe disse, tight_layout deve levar em conta mais recursos do que apenas eixos, títulos e etiquetas. Eu posso adicionar isso como uma solicitação de recurso no matplotlib.
Jbbiomed
também funciona para que eu tenha uma imagem grande o suficiente para caber nos xlabels que estavam sendo cortados anteriormente
Frederick Nord
1
aqui é a documentação com o exemplo de código a partir de matplotlib.org
Yojimbo
14

Aqui está outra solução muito manual. Você pode definir o tamanho do eixo e os paddings são considerados adequadamente (incluindo legenda e marcas de escala). Espero que seja útil para alguém.

Exemplo (o tamanho dos eixos é o mesmo!):

insira a descrição da imagem aqui

Código:

#==================================================
# 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
fonte
Isso não funcionou para mim até que eu mudei o primeiro plt.draw()para ax.figure.canvas.draw(). Não sei por que, mas antes dessa alteração, o tamanho da legenda não era atualizado.
precisa saber é o seguinte
Se você estiver tentando usar isso em uma janela da GUI, precisará mudar fig.set_size_inches(widthTot,heightTot)para fig.set_size_inches(widthTot,heightTot, forward=True).
precisa saber é o seguinte