Rótulos embutidos no Matplotlib

100

No Matplotlib, não é muito difícil fazer uma lenda ( example_legend()abaixo), mas acho que é melhor colocar rótulos nas curvas sendo plotadas (como em example_inline(), abaixo). Isso pode ser muito complicado, porque tenho que especificar as coordenadas manualmente e, se reformatar o gráfico, provavelmente terei que reposicionar os rótulos. Existe uma maneira de gerar rótulos automaticamente em curvas no Matplotlib? Pontos extras por poder orientar o texto em um ângulo correspondente ao ângulo da curva.

import numpy as np
import matplotlib.pyplot as plt

def example_legend():
    plt.clf()
    x = np.linspace(0, 1, 101)
    y1 = np.sin(x * np.pi / 2)
    y2 = np.cos(x * np.pi / 2)
    plt.plot(x, y1, label='sin')
    plt.plot(x, y2, label='cos')
    plt.legend()

Figura com legenda

def example_inline():
    plt.clf()
    x = np.linspace(0, 1, 101)
    y1 = np.sin(x * np.pi / 2)
    y2 = np.cos(x * np.pi / 2)
    plt.plot(x, y1, label='sin')
    plt.plot(x, y2, label='cos')
    plt.text(0.08, 0.2, 'sin')
    plt.text(0.9, 0.2, 'cos')

Figura com rótulos embutidos

Alex Szatmary
fonte

Respostas:

28

Boa pergunta, há um tempo eu experimentei um pouco com isso, mas não usei muito porque ainda não é à prova de balas. Dividi a área do gráfico em uma grade de 32x32 e calculei um 'campo potencial' para a melhor posição de um rótulo para cada linha de acordo com as seguintes regras:

  • o espaço em branco é um bom lugar para um rótulo
  • A etiqueta deve estar perto da linha correspondente
  • A etiqueta deve estar longe das outras linhas

O código era mais ou menos assim:

import matplotlib.pyplot as plt
import numpy as np
from scipy import ndimage


def my_legend(axis = None):

    if axis == None:
        axis = plt.gca()

    N = 32
    Nlines = len(axis.lines)
    print Nlines

    xmin, xmax = axis.get_xlim()
    ymin, ymax = axis.get_ylim()

    # the 'point of presence' matrix
    pop = np.zeros((Nlines, N, N), dtype=np.float)    

    for l in range(Nlines):
        # get xy data and scale it to the NxN squares
        xy = axis.lines[l].get_xydata()
        xy = (xy - [xmin,ymin]) / ([xmax-xmin, ymax-ymin]) * N
        xy = xy.astype(np.int32)
        # mask stuff outside plot        
        mask = (xy[:,0] >= 0) & (xy[:,0] < N) & (xy[:,1] >= 0) & (xy[:,1] < N)
        xy = xy[mask]
        # add to pop
        for p in xy:
            pop[l][tuple(p)] = 1.0

    # find whitespace, nice place for labels
    ws = 1.0 - (np.sum(pop, axis=0) > 0) * 1.0 
    # don't use the borders
    ws[:,0]   = 0
    ws[:,N-1] = 0
    ws[0,:]   = 0  
    ws[N-1,:] = 0  

    # blur the pop's
    for l in range(Nlines):
        pop[l] = ndimage.gaussian_filter(pop[l], sigma=N/5)

    for l in range(Nlines):
        # positive weights for current line, negative weight for others....
        w = -0.3 * np.ones(Nlines, dtype=np.float)
        w[l] = 0.5

        # calculate a field         
        p = ws + np.sum(w[:, np.newaxis, np.newaxis] * pop, axis=0)
        plt.figure()
        plt.imshow(p, interpolation='nearest')
        plt.title(axis.lines[l].get_label())

        pos = np.argmax(p)  # note, argmax flattens the array first 
        best_x, best_y =  (pos / N, pos % N) 
        x = xmin + (xmax-xmin) * best_x / N       
        y = ymin + (ymax-ymin) * best_y / N       


        axis.text(x, y, axis.lines[l].get_label(), 
                  horizontalalignment='center',
                  verticalalignment='center')


plt.close('all')

x = np.linspace(0, 1, 101)
y1 = np.sin(x * np.pi / 2)
y2 = np.cos(x * np.pi / 2)
y3 = x * x
plt.plot(x, y1, 'b', label='blue')
plt.plot(x, y2, 'r', label='red')
plt.plot(x, y3, 'g', label='green')
my_legend()
plt.show()

E o enredo resultante: insira a descrição da imagem aqui

Jan Kuiken
fonte
Muito agradável. No entanto, tenho um exemplo que não funciona completamente: 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.
egpbos
Desculpe, esqueci x2 = np.linspace(0,0.5,100).
egpbos
Existe alguma maneira de usar isso sem scipy? No meu sistema atual, é difícil instalar.
AnnanFay
Isso não funciona para mim em Python 3.6.4, Matplotlib 2.1.2 e Scipy 1.0.0. Depois de atualizar o printcomando, ele executa e cria 4 gráficos, 3 dos quais parecem ser rabiscos pixelados (provavelmente algo a ver com o 32x32) e o quarto com rótulos em lugares estranhos.
Y Davis
84

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:

rotulagem semiautomática de plotagem

Em matplotlibque é 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 numpye algumas funções da mathbiblioteca padrão .

Descrição

O comportamento padrão da labelLinesfunção é espaçar os rótulos uniformemente ao longo do xeixo (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_linesfunção não leva em conta as linhas que não tiveram um rótulo atribuído no plotcomando (ou mais precisamente se o rótulo contiver '_line').

Argumentos de palavra- chave passados ​​para labelLinesou labelLinesão passados ​​para a textchamada 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 1e 10no gráfico superior esquerdo. Nem tenho certeza se isso pode ser evitado.
  • Seria bom especificar uma yposição às vezes.
  • Ainda é um processo iterativo para obter anotações no local certo
  • Só funciona quando os xvalores -axis são floats

Pegadinhas

  • Por padrão, a labelLinesfunçã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 xgama 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 xvalsargumento; 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()
Milha náutica
fonte
1
@blujay Fico feliz que você tenha adaptado para atender às suas necessidades. Vou adicionar essa restrição como um problema.
NauticalMile
1
@Liza Leia meu Gotcha que acabei de adicionar para saber por que isso está acontecendo. Para o seu caso (presumo que seja como o desta pergunta ), a menos que você queira criar manualmente uma lista de xvals, convém modificar labelLinesum pouco o código: altere o código no if xvals is None:escopo para criar uma lista com base em outros critérios. Você poderia começar comxvals = [(np.min(l.get_xdata())+np.max(l.get_xdata()))/2 for l in lines]
NauticalMile
1
@Liza Seu gráfico me intriga. O problema é que seus dados não estão uniformemente espalhados pelo gráfico e você tem muitas curvas quase em cima umas das outras. Com a minha solução, pode ser muito difícil distinguir os rótulos em muitos casos. Acho que a melhor solução é ter blocos de etiquetas empilhadas em diferentes partes vazias do seu lote. Veja este gráfico para um exemplo com dois blocos de rótulos empilhados (um bloco com 1 rótulo e outro bloco com 4). Implementar isso seria um pouco de trabalho braçal, posso fazê-lo em algum momento no futuro.
NauticalMile de
1
Nota: desde Matplotlib 2.0, .get_axes()e .get_axis_bgcolor()foram descontinuados. Substitua por .axese .get_facecolor(), resp.
Jiāgěng
1
Outra coisa incrível labellinesé que as propriedades relacionadas plt.textou ax.textse aplicam a ele. Significa que você pode definir fontsizee bboxparâmetros na labelLines()função.
tionichm
52

A resposta de @Jan Kuiken é certamente bem pensada e completa, mas há algumas ressalvas:

  • não funciona em todos os casos
  • requer uma boa quantidade de código extra
  • pode variar consideravelmente de um lote para o outro

Uma abordagem muito mais simples é anotar o último ponto de cada gráfico. O ponto também pode ser circulado, para dar ênfase. Isso pode ser feito com uma linha extra:

from matplotlib import pyplot as plt

for i, (x, y) in enumerate(samples):
    plt.plot(x, y)
    plt.text(x[-1], y[-1], 'sample {i}'.format(i=i))

Uma variante seria usar ax.annotate.

Ioannis Filippidis
fonte
1
+1! Parece uma solução simples e agradável. Desculpe pela preguiça, mas como ficaria isso? O texto estaria dentro do gráfico ou no topo do eixo y direito?
rocarvaj
1
@rocarvaj Depende de outras configurações. É possível que as etiquetas se projetem para fora da caixa de plotagem. Duas maneiras de evitar esse comportamento são: 1) usar um índice diferente de -1, 2) definir limites de eixo apropriados para permitir espaço para os rótulos.
Ioannis Filippidis
1
Também se torna uma bagunça se os gráficos se concentram em algum valor y - os pontos finais ficam muito próximos para que o texto pareça bom
LazyCat
@LazyCat: Isso é verdade. Para corrigir isso, pode-se tornar as anotações arrastáveis. Um pouco doloroso, eu acho, mas daria certo.
PlacidLush
1

Uma abordagem mais simples como a de Ioannis Filippidis:

import matplotlib.pyplot as plt
import numpy as np

# evenly sampled time at 200ms intervals
tMin=-1 ;tMax=10
t = np.arange(tMin, tMax, 0.1)

# red dashes, blue points default
plt.plot(t, 22*t, 'r--', t, t**2, 'b')

factor=3/4 ;offset=20  # text position in view  
textPosition=[(tMax+tMin)*factor,22*(tMax+tMin)*factor]
plt.text(textPosition[0],textPosition[1]+offset,'22  t',color='red',fontsize=20)
textPosition=[(tMax+tMin)*factor,((tMax+tMin)*factor)**2+20]
plt.text(textPosition[0],textPosition[1]+offset, 't^2', bbox=dict(facecolor='blue', alpha=0.5),fontsize=20)
plt.show()

codificar python 3 em sageCell

Tagada
fonte