Atualização: O usuário cphyc gentilmente criou um repositório Github para o código desta resposta (veja aqui ) e agrupou o código em um pacote que pode ser instalado usando pip install matplotlib-label-lines
.
Bonita foto:
Em matplotlib
que é bastante fácil de rotular gráficos de contorno (automaticamente ou manualmente, colocando etiquetas com cliques do mouse). Não parece (ainda) haver qualquer capacidade equivalente para rotular séries de dados desta forma! Pode haver alguma razão semântica para não incluir esse recurso que estou perdendo.
Independentemente disso, escrevi o módulo a seguir, que permite a rotulagem semiautomática de plotagem. Requer apenas numpy
e algumas funções da math
biblioteca padrão .
Descrição
O comportamento padrão da labelLines
função é espaçar os rótulos uniformemente ao longo do x
eixo (posicionando automaticamente noy
-valor claro). Se você quiser, pode simplesmente passar um array das coordenadas x de cada um dos rótulos. Você pode até ajustar a localização de um rótulo (conforme mostrado no gráfico inferior direito) e espaçar o resto uniformemente, se desejar.
Além disso, a label_lines
função não leva em conta as linhas que não tiveram um rótulo atribuído no plot
comando (ou mais precisamente se o rótulo contiver '_line'
).
Argumentos de palavra- chave passados para labelLines
ou labelLine
são passados para a text
chamada de função (alguns argumentos de palavra-chave são definidos se o código de chamada optar por não especificar).
Problemas
- As caixas delimitadoras de anotação às vezes interferem de forma indesejável com outras curvas. Conforme mostrado pelas anotações
1
e 10
no gráfico superior esquerdo. Nem tenho certeza se isso pode ser evitado.
- Seria bom especificar uma
y
posição às vezes.
- Ainda é um processo iterativo para obter anotações no local certo
- Só funciona quando os
x
valores -axis são float
s
Pegadinhas
- Por padrão, a
labelLines
função assume que todas as séries de dados abrangem o intervalo especificado pelos limites do eixo. Dê uma olhada na curva azul no gráfico superior esquerdo da bela imagem. Se houvesse únicos dados disponíveis para a x
gama 0.5
- 1
, em seguida, em seguida, não poderíamos colocar uma etiqueta no local desejado (que é um pouco menos do que 0.2
). Veja esta pergunta para um exemplo particularmente desagradável. No momento, o código não identifica de forma inteligente esse cenário e reorganiza os rótulos, no entanto, há uma solução alternativa razoável. A função labelLines leva o xvals
argumento; uma lista dex
-valores especificados pelo usuário em vez da distribuição linear padrão ao longo da largura. Assim, o usuário pode decidir qualx
-valores a serem usados para o posicionamento do rótulo de cada série de dados.
Além disso, acredito que esta seja a primeira resposta para completar o objetivo bônus de alinhar os rótulos com a curva em que estão. :)
label_lines.py:
from math import atan2,degrees
import numpy as np
#Label line with line2D label data
def labelLine(line,x,label=None,align=True,**kwargs):
ax = line.axes
xdata = line.get_xdata()
ydata = line.get_ydata()
if (x < xdata[0]) or (x > xdata[-1]):
print('x label location is outside data range!')
return
#Find corresponding y co-ordinate and angle of the line
ip = 1
for i in range(len(xdata)):
if x < xdata[i]:
ip = i
break
y = ydata[ip-1] + (ydata[ip]-ydata[ip-1])*(x-xdata[ip-1])/(xdata[ip]-xdata[ip-1])
if not label:
label = line.get_label()
if align:
#Compute the slope
dx = xdata[ip] - xdata[ip-1]
dy = ydata[ip] - ydata[ip-1]
ang = degrees(atan2(dy,dx))
#Transform to screen co-ordinates
pt = np.array([x,y]).reshape((1,2))
trans_angle = ax.transData.transform_angles(np.array((ang,)),pt)[0]
else:
trans_angle = 0
#Set a bunch of keyword arguments
if 'color' not in kwargs:
kwargs['color'] = line.get_color()
if ('horizontalalignment' not in kwargs) and ('ha' not in kwargs):
kwargs['ha'] = 'center'
if ('verticalalignment' not in kwargs) and ('va' not in kwargs):
kwargs['va'] = 'center'
if 'backgroundcolor' not in kwargs:
kwargs['backgroundcolor'] = ax.get_facecolor()
if 'clip_on' not in kwargs:
kwargs['clip_on'] = True
if 'zorder' not in kwargs:
kwargs['zorder'] = 2.5
ax.text(x,y,label,rotation=trans_angle,**kwargs)
def labelLines(lines,align=True,xvals=None,**kwargs):
ax = lines[0].axes
labLines = []
labels = []
#Take only the lines which have labels other than the default ones
for line in lines:
label = line.get_label()
if "_line" not in label:
labLines.append(line)
labels.append(label)
if xvals is None:
xmin,xmax = ax.get_xlim()
xvals = np.linspace(xmin,xmax,len(labLines)+2)[1:-1]
for line,x,label in zip(labLines,xvals,labels):
labelLine(line,x,label,align,**kwargs)
Teste o código para gerar a bela imagem acima:
from matplotlib import pyplot as plt
from scipy.stats import loglaplace,chi2
from labellines import *
X = np.linspace(0,1,500)
A = [1,2,5,10,20]
funcs = [np.arctan,np.sin,loglaplace(4).pdf,chi2(5).pdf]
plt.subplot(221)
for a in A:
plt.plot(X,np.arctan(a*X),label=str(a))
labelLines(plt.gca().get_lines(),zorder=2.5)
plt.subplot(222)
for a in A:
plt.plot(X,np.sin(a*X),label=str(a))
labelLines(plt.gca().get_lines(),align=False,fontsize=14)
plt.subplot(223)
for a in A:
plt.plot(X,loglaplace(4).pdf(a*X),label=str(a))
xvals = [0.8,0.55,0.22,0.104,0.045]
labelLines(plt.gca().get_lines(),align=False,xvals=xvals,color='k')
plt.subplot(224)
for a in A:
plt.plot(X,chi2(5).pdf(a*X),label=str(a))
lines = plt.gca().get_lines()
l1=lines[-1]
labelLine(l1,0.6,label=r'$Re=${}'.format(l1.get_label()),ha='left',va='bottom',align = False)
labelLines(lines[:-1],align=False)
plt.show()
plt.plot(x2, 3*x2**2, label="3x*x"); plt.plot(x2, 2*x2**2, label="2x*x"); plt.plot(x2, 0.5*x2**2, label="0.5x*x"); plt.plot(x2, -1*x2**2, label="-x*x"); plt.plot(x2, -2.5*x2**2, label="-2.5*x*x"); my_legend();
Isso coloca um dos rótulos no canto superior esquerdo. Alguma ideia de como consertar isso? Parece que o problema pode ser que as linhas estão muito próximas.