Etiquetas en línea en Matplotlib

En Matplotlib, no es demasiado difícil hacer una leyenda ( example_legend() , más abajo), pero creo que es mejor estilo poner las tags en las curvas que se trazan (como en example_inline() , a continuación). Esto puede ser muy complicado, porque tengo que especificar las coordenadas a mano y, si vuelvo a formatear el trazado, probablemente tenga que reposicionar las tags. ¿Hay alguna manera de generar automáticamente tags en las 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

Buena pregunta, hace un tiempo he experimentado un poco con esto, pero no lo he usado mucho porque aún no es a prueba de balas. Dividí el área de trazado 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 ttwig resultante: enter image description here

Actualización: el usuario cphyc ha creado gentilmente un repository de Github para el código en esta respuesta (ver aquí ), y ha incluido el código en un paquete que puede instalarse usando la pip install matplotlib-label-lines .


Bonita imagen:

etiquetado de parcela semiautomático

En matplotlib es bastante fácil etiquetar los diagtwigs de contorno (ya sea automáticamente o colocando manualmente las tags con clics del mouse). ¡No parece haber (aún) una 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.

A pesar de todo, he escrito el siguiente módulo que permite cualquier tipo de etiquetado de plot semiautomático. Solo requiere numpy y un par de funciones de la biblioteca math estándar.

Descripción

El comportamiento predeterminado de la función labelLines es espaciar las tags de manera uniforme a lo largo del eje x (colocando automáticamente en el valor correcto y por supuesto). Si lo desea, puede pasar una serie de coordenadas x de cada una de las tags. Incluso puede ajustar la ubicación de una etiqueta (como se muestra en el gráfico inferior derecho) y espaciar el rest de manera uniforme si lo desea.

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

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

Cuestiones

  • Los cuadros delimitadores de anotaciones a veces interfieren indeseablemente con otras curvas. Como se muestra en las anotaciones 1 y 10 en la gráfica superior izquierda. Ni siquiera estoy seguro de que esto pueda evitarse.
  • A veces sería bueno especificar una posición y .
  • Todavía es un proceso iterativo para obtener anotaciones en el lugar correcto
  • Solo funciona cuando los valores del eje x son float

Gotchas

  • De forma predeterminada, la función labelLines 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 el gráfico superior izquierdo de la bonita imagen. Si solo hubiera datos disponibles para el rango x 0.51 , entonces posiblemente 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 inteligentemente este escenario y reorganiza las tags, sin embargo, hay una solución razonable. La función labelLines toma el argumento xvals ; una lista de valores- x especificados por el usuario en lugar de la distribución lineal predeterminada en todo el ancho. Por lo tanto, el usuario puede decidir qué valores x usar para la colocación de la etiqueta de cada serie de datos.

Además, creo que esta es la primera respuesta para completar el objective extra de alinear las tags con la curva en la que están. 🙂

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

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

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

Un enfoque mucho más simple es anotar el último punto de cada plot. El punto también se puede marcar con un círculo, para enfatizarlo. Esto se puede lograr con una línea adicional:

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

Una variante sería usar ax.annotate .

Intereting Posts