Cómo colocar etiquetas en línea en un gráfico de líneas

13 minutos de lectura

Avatar de usuario de Alex Szatmary
Alex Szatmary

En Matplotlib, no es demasiado difícil hacer una leyenda (example_legend()a continuación), pero creo que es mejor poner etiquetas en las curvas que se están trazando (como en example_inline(), abajo). Esto puede ser muy complicado, porque tengo que especificar las coordenadas a mano y, si cambio el formato de la trama, probablemente tenga que cambiar la posición de las etiquetas. ¿Hay alguna forma de generar automáticamente etiquetas en curvas en Matplotlib? Puntos de bonificación por poder orientar el texto en un ángulo correspondiente al ángulo de la 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 con leyenda

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 con etiquetas en línea

Avatar de usuario de NauticalMile
Milla nautica

Actualizar: El usuario cphyc ha creado amablemente un repositorio de Github para el código en esta respuesta (ver aquí), y agrupó el código en un paquete que se puede instalar usando pip install matplotlib-label-lines.


Bonita imagen:

etiquetado semiautomático de parcelas

En matplotlib es bastante fácil de diagramas de contorno de etiquetas (ya sea automáticamente o colocando etiquetas manualmente con clics del mouse). ¡No parece haber (todavía) ninguna capacidad equivalente para etiquetar series de datos de esta manera! Puede haber alguna razón semántica para no incluir esta característica que me falta.

Independientemente, he escrito el siguiente módulo que permite el etiquetado semiautomático de parcelas. Requiere solo numpy y un par de funciones del estándar math biblioteca.

Descripción

El comportamiento predeterminado del labelLines La función es espaciar las etiquetas uniformemente a lo largo de la x eje (colocándose automáticamente en el y-valor por supuesto). Si lo desea, puede simplemente pasar una matriz de las coordenadas x de cada una de las etiquetas. Incluso puede modificar la ubicación de una etiqueta (como se muestra en el gráfico inferior derecho) y espaciar el resto de manera uniforme si lo desea.

además, el label_lines función no tiene en cuenta las líneas que no han tenido una etiqueta asignada en el plot comando (o más exactamente si la etiqueta contiene '_line').

Argumentos de palabras clave pasados ​​a labelLines o labelLine se pasan a la text llamada de función (algunos argumentos de palabras clave se establecen si el código de llamada elige no especificar).

Asuntos

  • Los cuadros delimitadores de anotaciones a veces interfieren de forma no deseada con otras curvas. Como lo muestra el 1 y 10 anotaciones en el gráfico superior izquierdo. Ni siquiera estoy seguro de que esto se pueda evitar.
  • Sería bueno especificar un y posición en cambio a veces.
  • Todavía es un proceso iterativo obtener anotaciones en la ubicación correcta
  • Solo funciona cuando el x-los valores del eje son floats

trampas

  • Por defecto, el labelLines La función asume que todas las series de datos abarcan el rango especificado por los límites del eje. Eche un vistazo a la curva azul en la parte superior izquierda de la imagen bonita. Si sólo hubiera datos disponibles para el x rango 0.51 entonces no podríamos colocar una etiqueta en la ubicación deseada (que es un poco menos de 0.2). Vea esta pregunta para un ejemplo particularmente desagradable. En este momento, el código no identifica de manera inteligente este escenario y reorganiza las etiquetas; sin embargo, existe una solución alternativa razonable. La función labelLines toma la xvals argumento; una lista de x-valores especificados por el usuario en lugar de la distribución lineal predeterminada en todo el ancho. Así el usuario puede decidir qué x-valores a utilizar para la ubicación de la etiqueta de cada serie de datos.

Además, creo que esta es la primera respuesta para completar la prima objetivo de alinear las etiquetas con la curva en la que se encuentran. 🙂

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)

Código de prueba para generar la bonita imagen de arriba:

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

  • @blujay Me alegro de que hayas podido adaptarlo a tus necesidades. Agregaré esa restricción como un problema.

    – Milla nautica

    11 de octubre de 2016 a las 1:07

  • @Liza Lea mi Gotcha que acabo de agregar para saber por qué sucede esto. Para su caso (supongo que es como el de esta pregunta), a menos que desee crear manualmente una lista de xvalses posible que desee modificar el labelLines codifique un poco: cambie el código debajo del if xvals is None: alcance para crear una lista basada en otros criterios. Podrías empezar con xvals = [(np.min(l.get_xdata())+np.max(l.get_xdata()))/2 for l in lines]

    – Milla nautica

    22 de junio de 2017 a las 16:34

  • @Liza Sin embargo, tu gráfico me intriga. El problema es que sus datos no se distribuyen uniformemente en el gráfico y tiene muchas curvas que están casi una encima de la otra. Con mi solución, puede ser muy difícil diferenciar las etiquetas en muchos casos. Creo que la mejor solución es tener bloques de etiquetas apiladas en diferentes partes vacías de tu parcela. Ver este gráfico para un ejemplo con dos bloques de etiquetas apiladas (un bloque con 1 etiqueta y otro bloque con 4). Implementar esto sería un poco de trabajo preliminar, podría hacerlo en algún momento en el futuro.

    – Milla nautica

    22 de junio de 2017 a las 16:51

  • Nota: desde Matplotlib 2.0, .get_axes() y .get_axis_bgcolor() han quedado en desuso. Por favor reemplace con .axes y .get_facecolor()resp.

    – Jiageng

    14 mayo 2018 a las 11:14

  • Otra cosa asombrosa sobre labellines es que las propiedades relacionadas con plt.text o ax.text se aplica a ella. Lo que significa que puedes establecer fontsize y bbox parámetros en el labelLines() función.

    – tionichm

    4 julio 2019 a las 14:00


0 _ avatar de usuario
0 _

La respuesta de @Jan Kuiken ciertamente está bien pensada y es exhaustiva, pero hay algunas advertencias:

  • no funciona en todos los casos
  • requiere una buena cantidad de código adicional
  • puede variar considerablemente de una parcela a otra

Un enfoque mucho más simple es anotar el último punto de cada gráfico. El punto también se puede encerrar en un círculo para enfatizar. Esto se puede lograr con una línea adicional:

import matplotlib.pyplot as plt

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

una variante seria usar el método matplotlib.axes.Axes.annotate.

  • +1! Parece una solución agradable y simple. Perdón por la pereza, pero ¿cómo se vería esto? ¿Estaría el texto dentro del gráfico o encima del eje y derecho?

    – rocarvaj

    30 de marzo de 2016 a las 14:36

  • @rocarvaj Depende de otras configuraciones. Es posible que las etiquetas sobresalgan del cuadro de trazado. Dos formas de evitar este comportamiento son: 1) usar un índice diferente a -12) establecer límites de eje apropiados para dejar espacio para las etiquetas.

    – 0 _

    14/09/2017 a las 11:05

  • También se convierte en un desastre, si las gráficas se concentran en algún valor y: los puntos finales se acercan demasiado para que el texto se vea bien.

    – Gato perezoso

    08/03/2018 a las 20:48

  • @LazyCat: Eso es cierto. Para arreglar esto, uno puede hacer que las anotaciones se puedan arrastrar. Un poco de dolor, supongo, pero haría el truco.

    – PlacidLush

    20 de abril de 2020 a las 20:08

  • Dale una medalla a este tipo.

    – Philippe Rémy

    15 de enero de 2022 a las 4:36

Buena pregunta, hace un tiempo experimenté un poco con esto, pero no lo he usado mucho porque todavía no es a prueba de balas. Dividí el área de la trama en una cuadrícula de 32×32 y calculé un ‘campo potencial’ para la mejor posición de una etiqueta para cada línea de acuerdo con las siguientes reglas:

  • el espacio en blanco es un buen lugar para una etiqueta
  • La etiqueta debe estar cerca de la línea correspondiente
  • La etiqueta debe estar lejos de las otras líneas.

El código era algo como esto:

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

Y la trama resultante:
ingrese la descripción de la imagen aquí

  • Muy lindo. Sin embargo, tengo un ejemplo que no 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(); Esto coloca una de las etiquetas en la esquina superior izquierda. ¿Alguna idea sobre cómo solucionar este problema? Parece que el problema puede ser que las líneas están demasiado juntas.

    – egpbos

    12 de marzo de 2014 a las 12:58


  • lo siento, lo olvidé x2 = np.linspace(0,0.5,100).

    – egpbos

    12 de marzo de 2014 a las 13:06

  • ¿Hay alguna forma de usar esto sin scipy? En mi sistema actual es un dolor de instalar.

    – Annan Fay

    15/03/2016 a las 21:44

  • Esto no me funciona con Python 3.6.4, Matplotlib 2.1.2 y Scipy 1.0.0. Después de actualizar el print comando, se ejecuta y crea 4 gráficos, 3 de los cuales parecen ser un galimatías pixelado (probablemente algo relacionado con el 32×32), y el cuarto con etiquetas en lugares extraños.

    – Y Davis

    27 de febrero de 2018 a las 13:46


Avatar de usuario de Nico Schlömer
Nico Schlömer

matplotx (que yo escribí) tiene line_labels() que traza las etiquetas a la derecha de las líneas. También es lo suficientemente inteligente como para evitar superposiciones cuando se concentran demasiadas líneas en un solo lugar. (Ver gráfico estelar por ejemplo.) Lo hace resolviendo un problema particular de mínimos cuadrados no negativos en las posiciones objetivo de las etiquetas. De todos modos, en muchos casos en los que, para empezar, no hay superposición, como en el ejemplo siguiente, ni siquiera es necesario.

import matplotlib.pyplot as plt
import matplotx
import numpy as np

# create data
rng = np.random.default_rng(0)
offsets = [1.0, 1.50, 1.60]
labels = ["no balancing", "CRV-27", "CRV-27*"]
x0 = np.linspace(0.0, 3.0, 100)
y = [offset * x0 / (x0 + 1) + 0.1 * rng.random(len(x0)) for offset in offsets]

# plot
with plt.style.context(matplotx.styles.dufte):
    for yy, label in zip(y, labels):
        plt.plot(x0, yy, label=label)
    plt.xlabel("distance [m]")
    matplotx.ylabel_top("voltage [V]")  # move ylabel to the top, rotate
    matplotx.line_labels()  # line labels to the right
    plt.show()
    # plt.savefig("out.png", bbox_inches="tight")

ingrese la descripción de la imagen aquí

Un enfoque más simple como el que hace 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()

código python 3 en sageCell

¿Ha sido útil esta solución?