Actualización: el usuario cphyc ha creado amablemente un repositorio de Github para el código de esta respuesta (ver aquí ) y ha empaquetado el código en un paquete que puede instalarse usando pip install matplotlib-label-lines
.
Bonita imagen:
En matplotlib
que es bastante fácil de parcelas etiqueta de contorno (ya sea de forma automática o manualmente mediante la colocación de etiquetas con clics del ratón). ¡No parece (todavía) haber 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 admite cualquier etiquetado de trazado semiautomático. Requiere solo numpy
un par de funciones de la math
biblioteca estándar .
Descripción
El comportamiento predeterminado de la labelLines
función es espaciar las etiquetas de manera uniforme a lo largo del x
eje (colocándolas automáticamente en el y
valor correcto, 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, la 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'
).
Los argumentos de palabra clave pasados labelLines
o labelLine
se pasan a la text
llamada de función (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 de manera no deseada con otras curvas. Como se muestra en las anotaciones
1
y 10
en el gráfico superior izquierdo. Ni siquiera estoy seguro de que esto pueda evitarse.
- En su lugar, sería bueno especificar una
y
posición a veces.
- Sigue siendo un proceso iterativo obtener anotaciones en la ubicación correcta.
- Solo funciona cuando los
x
valores de -axis son float
s
Gotchas
- De forma predeterminada, la
labelLines
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 el gráfico superior izquierdo de la bonita imagen. Si solo hubiera datos disponibles para el x
rango 0.5
, 1
entonces no podríamos colocar una etiqueta en la ubicación deseada (que es un poco menos que 0.2
). Consulte esta pregunta para ver un ejemplo particularmente desagradable. En este momento, el código no identifica inteligentemente este escenario y reorganiza las etiquetas, sin embargo, hay una solución razonable. La función labelLines toma el xvals
argumento; una lista de x
valores especificados por el usuario en lugar de la distribución lineal predeterminada a lo ancho. Para que el usuario pueda decidir quéx
-valores que se utilizarán para la ubicación de la etiqueta de cada serie de datos.
Además, creo que esta es la primera respuesta para completar el objetivo adicional 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)
Pruebe el código 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()
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.