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 matplotlibque 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 numpyun par de funciones de la mathbiblioteca estándar .
Descripción
El comportamiento predeterminado de la labelLinesfunción es espaciar las etiquetas de manera uniforme a lo largo del xeje (colocándolas automáticamente en el yvalor 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_linesfunción no tiene en cuenta las líneas que no han tenido una etiqueta asignada en el plotcomando (o más exactamente si la etiqueta contiene '_line').
Los argumentos de palabra clave pasados labelLineso labelLinese pasan a la textllamada 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
1y 10en el gráfico superior izquierdo. Ni siquiera estoy seguro de que esto pueda evitarse.
- En su lugar, sería bueno especificar una
yposición a veces.
- Sigue siendo un proceso iterativo obtener anotaciones en la ubicación correcta.
- Solo funciona cuando los
xvalores de -axis son floats
Gotchas
- De forma predeterminada, la
labelLinesfunció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 xrango 0.5, 1entonces 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 xvalsargumento; una lista de xvalores 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.