Eixo secundário com twinx (): como adicionar à legenda?

288

Eu tenho um gráfico com dois eixos y, usando twinx(). Também dou rótulos às linhas e quero mostrá-las legend(), mas só consigo obter os rótulos de um eixo na legenda:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rc
rc('mathtext', default='regular')

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(time, Swdown, '-', label = 'Swdown')
ax.plot(time, Rn, '-', label = 'Rn')
ax2 = ax.twinx()
ax2.plot(time, temp, '-r', label = 'temp')
ax.legend(loc=0)
ax.grid()
ax.set_xlabel("Time (h)")
ax.set_ylabel(r"Radiation ($MJ\,m^{-2}\,d^{-1}$)")
ax2.set_ylabel(r"Temperature ($^\circ$C)")
ax2.set_ylim(0, 35)
ax.set_ylim(-20,100)
plt.show()

Portanto, só obtenho os rótulos do primeiro eixo na legenda, e não o rótulo 'temp' do segundo eixo. Como eu poderia adicionar esse terceiro rótulo à legenda?

insira a descrição da imagem aqui

joris
fonte
4
[ Não faça isso em lugar nenhum remotamente próximo a qualquer código de produção ] Quando meu único objetivo é gerar um belo enredo com a lenda apropriada o mais rápido possível, eu uso um feio truque de plotar um array vazio axcom o estilo que uso ax2: in seu caso ax.plot([], [], '-r', label = 'temp'). É muito mais rápido e mais simples do que fazê-lo corretamente ...
Neinstein

Respostas:

370

Você pode adicionar facilmente uma segunda legenda adicionando a linha:

ax2.legend(loc=0)

Você receberá isso:

insira a descrição da imagem aqui

Mas se você quiser todos os rótulos em uma legenda, faça algo assim:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rc
rc('mathtext', default='regular')

time = np.arange(10)
temp = np.random.random(10)*30
Swdown = np.random.random(10)*100-10
Rn = np.random.random(10)*100-10

fig = plt.figure()
ax = fig.add_subplot(111)

lns1 = ax.plot(time, Swdown, '-', label = 'Swdown')
lns2 = ax.plot(time, Rn, '-', label = 'Rn')
ax2 = ax.twinx()
lns3 = ax2.plot(time, temp, '-r', label = 'temp')

# added these three lines
lns = lns1+lns2+lns3
labs = [l.get_label() for l in lns]
ax.legend(lns, labs, loc=0)

ax.grid()
ax.set_xlabel("Time (h)")
ax.set_ylabel(r"Radiation ($MJ\,m^{-2}\,d^{-1}$)")
ax2.set_ylabel(r"Temperature ($^\circ$C)")
ax2.set_ylim(0, 35)
ax.set_ylim(-20,100)
plt.show()

O que lhe dará o seguinte:

insira a descrição da imagem aqui

Paulo
fonte
2
Isso falha com errorbarplotagens. Para uma solução que os manipule corretamente, consulte abaixo: stackoverflow.com/a/10129461/1319447
Davide
1
Para evitar duas legendas sobrepostas, como no meu caso, quando especifiquei duas .legend (loc = 0), você deve especificar dois valores diferentes para o valor da localização da legenda (ambos diferentes de 0). Veja: matplotlib.org/api/legend_api.html
Roalt 4/16/16
Ocorreu um problema ao adicionar uma única linha a uma subtrama com várias linhas ax1. Nesse caso, use lns1=ax1.linese depois anexe lns2a esta lista.
Little Bobby Tables
Os diferentes valores usados por locsão explicados aqui
Dror
1
Veja a resposta abaixo de um modo mais automático (com matplotlib> = 2,1): stackoverflow.com/a/47370214/653364
Joris
183

Não tenho certeza se essa funcionalidade é nova, mas você também pode usar o método get_legend_handles_labels () em vez de controlar as linhas e os rótulos:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rc
rc('mathtext', default='regular')

pi = np.pi

# fake data
time = np.linspace (0, 25, 50)
temp = 50 / np.sqrt (2 * pi * 3**2) \
        * np.exp (-((time - 13)**2 / (3**2))**2) + 15
Swdown = 400 / np.sqrt (2 * pi * 3**2) * np.exp (-((time - 13)**2 / (3**2))**2)
Rn = Swdown - 10

fig = plt.figure()
ax = fig.add_subplot(111)

ax.plot(time, Swdown, '-', label = 'Swdown')
ax.plot(time, Rn, '-', label = 'Rn')
ax2 = ax.twinx()
ax2.plot(time, temp, '-r', label = 'temp')

# ask matplotlib for the plotted objects and their labels
lines, labels = ax.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax2.legend(lines + lines2, labels + labels2, loc=0)

ax.grid()
ax.set_xlabel("Time (h)")
ax.set_ylabel(r"Radiation ($MJ\,m^{-2}\,d^{-1}$)")
ax2.set_ylabel(r"Temperature ($^\circ$C)")
ax2.set_ylim(0, 35)
ax.set_ylim(-20,100)
plt.show()
zgana
fonte
1
Esta é a única solução que pode lidar com os eixos em que as tramas se sobrepõem com as legendas (os últimos eixos é a que deve traçar as legendas)
Amelio Vazquez-Reina
5
Essa solução também funciona com errorbargráficos, enquanto o aceito falha (mostrando uma linha e suas barras de erro separadamente, e nenhuma delas com o rótulo correto). Além disso, é mais simples.
Davide
ligeiro problema: ele não funciona se você deseja substituir o rótulo ax2e não tem um conjunto desde o início
Ciprian Tomoiagă
Observação: para plotagens clássicas, você não precisa especificar o argumento do rótulo. Mas para outros, por exemplo. barras que você precisa.
22719 belka
Isso também facilita tudo se você não souber de antemão quantas linhas serão plotadas.
Vegard Jervell
77

A partir da versão 2.1 do matplotlib, você pode usar uma legenda da figura . Em vez de ax.legend(), que produz uma legenda com as alças dos eixosax , pode-se criar uma legenda de figura

fig.legend (loc = "canto superior direito")

que reunirá todas as alças de todas as subparcelas na figura. Como é uma legenda da figura, ela será colocada no canto da figura e o locargumento será relativo à figura.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0,10)
y = np.linspace(0,10)
z = np.sin(x/3)**2*98

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x,y, '-', label = 'Quantity 1')

ax2 = ax.twinx()
ax2.plot(x,z, '-r', label = 'Quantity 2')
fig.legend(loc="upper right")

ax.set_xlabel("x [units]")
ax.set_ylabel(r"Quantity 1")
ax2.set_ylabel(r"Quantity 2")

plt.show()

insira a descrição da imagem aqui

Para colocar a legenda de volta nos eixos, seria necessário fornecer a bbox_to_anchore a bbox_transform. O último seria a transformação dos eixos dos eixos em que a legenda deve residir. Os primeiros podem ser as coordenadas da aresta definidas por locdadas nas coordenadas dos eixos.

fig.legend(loc="upper right", bbox_to_anchor=(1,1), bbox_transform=ax.transAxes)

insira a descrição da imagem aqui

ImportanceOfBeingErnest
fonte
Então, a versão 2.1 já foi lançada? Mas no Anaconda 3, tentei conda upgrade matplotlibnão encontrar versões mais recentes, ainda estou usando a v.2.0.2
StayFoolish
1
Essa é uma maneira mais limpa de alcançar o resultado final.
Goutham
1
bonito e pythônico
DanGoodrick
1
Isso não parece funcionar quando você tem muitas subparcelas. Ele adiciona uma única legenda para todas as subparcelas. Normalmente, é necessário uma legenda para cada subparcela, contendo séries nos eixos primário e secundário de cada legenda.
Sancho.s ReinstateMonicaCellio
@ sancho Correto, é o que está escrito na terceira frase desta resposta, "... que reunirá todas as alças de todas as subparcelas da figura".
ImportanceOfBeingErnest
38

Você pode facilmente obter o que deseja adicionando a linha no machado:

ax.plot([], [], '-r', label = 'temp')

ou

ax.plot(np.nan, '-r', label = 'temp')

Isso plotaria nada além de adicionar um rótulo à legenda de machado.

Eu acho que essa é uma maneira muito mais fácil. Não é necessário rastrear linhas automaticamente quando você tem apenas algumas linhas nos segundos eixos, pois a fixação manual como acima seria bastante fácil. Enfim, depende do que você precisa.

O código inteiro é como abaixo:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rc
rc('mathtext', default='regular')

time = np.arange(22.)
temp = 20*np.random.rand(22)
Swdown = 10*np.random.randn(22)+40
Rn = 40*np.random.rand(22)

fig = plt.figure()
ax = fig.add_subplot(111)
ax2 = ax.twinx()

#---------- look at below -----------

ax.plot(time, Swdown, '-', label = 'Swdown')
ax.plot(time, Rn, '-', label = 'Rn')

ax2.plot(time, temp, '-r')  # The true line in ax2
ax.plot(np.nan, '-r', label = 'temp')  # Make an agent in ax

ax.legend(loc=0)

#---------------done-----------------

ax.grid()
ax.set_xlabel("Time (h)")
ax.set_ylabel(r"Radiation ($MJ\,m^{-2}\,d^{-1}$)")
ax2.set_ylabel(r"Temperature ($^\circ$C)")
ax2.set_ylim(0, 35)
ax.set_ylim(-20,100)
plt.show()

O enredo é o seguinte:

insira a descrição da imagem aqui


Atualização: adicione uma versão melhor:

ax.plot(np.nan, '-r', label = 'temp')

Isso não fará nada enquanto plot(0, 0)pode alterar a faixa do eixo.


Um exemplo extra para dispersão

ax.scatter([], [], s=100, label = 'temp')  # Make an agent in ax
ax2.scatter(time, temp, s=10)  # The true scatter in ax2

ax.legend(loc=1, framealpha=1)
Syrtis Major
fonte
3
Eu gosto disso. É meio feio na maneira como "engana" o sistema, mas é tão simples de implementar.
Daniel Power
Isso é realmente simples de implementar. Porém, ao usar isso com dispersão, o tamanho da dispersão resultante na legenda é apenas um pequeno ponto.
greeeeeeen 12/09/19
@greeeeeeen Então você só deve especificar o tamanho do marcador ao fazer o gráfico de dispersão :-)
Syrtis Major
@SyrtisMajor Eu, é claro, tentei isso. Mas isso não mudou o tamanho do marcador na legenda.
greeeeeeen
@greeeeeeen Você alterou o tamanho do marcador da dispersão do agente? Veja minha postagem, adicionei um trecho de código de exemplo.
Syrtis Major
7

Um hack rápido que pode atender às suas necessidades.

Retire a moldura da caixa e posicione manualmente as duas legendas uma ao lado da outra. Algo assim..

ax1.legend(loc = (.75,.1), frameon = False)
ax2.legend( loc = (.75, .05), frameon = False)

Onde a tupla local é porcentagens da esquerda para a direita e de baixo para cima que representam o local no gráfico.

user2105997
fonte
5

Encontrei um exemplo oficial do matplotlib a seguir que usa host_subplot para exibir vários eixos y e todos os diferentes rótulos em uma legenda. Nenhuma solução alternativa necessária. Melhor solução que encontrei até agora. http://matplotlib.org/examples/axes_grid/demo_parasite_axes2.html

from mpl_toolkits.axes_grid1 import host_subplot
import mpl_toolkits.axisartist as AA
import matplotlib.pyplot as plt

host = host_subplot(111, axes_class=AA.Axes)
plt.subplots_adjust(right=0.75)

par1 = host.twinx()
par2 = host.twinx()

offset = 60
new_fixed_axis = par2.get_grid_helper().new_fixed_axis
par2.axis["right"] = new_fixed_axis(loc="right",
                                    axes=par2,
                                    offset=(offset, 0))

par2.axis["right"].toggle(all=True)

host.set_xlim(0, 2)
host.set_ylim(0, 2)

host.set_xlabel("Distance")
host.set_ylabel("Density")
par1.set_ylabel("Temperature")
par2.set_ylabel("Velocity")

p1, = host.plot([0, 1, 2], [0, 1, 2], label="Density")
p2, = par1.plot([0, 1, 2], [0, 3, 2], label="Temperature")
p3, = par2.plot([0, 1, 2], [50, 30, 15], label="Velocity")

par1.set_ylim(0, 4)
par2.set_ylim(1, 65)

host.legend()

plt.draw()
plt.show()
gerrit
fonte
Bem-vindo ao Stack Overflow! Cite a parte mais relevante do link, caso o site de destino esteja inacessível ou fique permanentemente offline. Consulte Como escrevo uma boa resposta . Concentre-se em questões mais atuais no futuro, esta tem quase 4 anos.
ByteHamster 5/05
De fato, uma boa descoberta, mas eu gostaria que você tivesse pegado o que aprendeu do exemplo, aplicado no MWE do OP e incluído uma imagem.
aeroNotAuto