matplotlib - font - subplot python
Etiquetas en línea en Matplotlib (3)
En Matplotlib, no es demasiado difícil hacer una leyenda ( example_legend()
, más abajo), pero creo que es mejor estilo poner las etiquetas 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 etiquetas. ¿Hay alguna manera de generar automáticamente etiquetas 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()
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'')
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 32x32 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:
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 diagrama a otro
Un enfoque mucho más simple es anotar el último punto de cada parcela. 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
.
Actualización: el usuario cphyc ha creado gentilmente un repositorio de Github para el código en esta respuesta (ver here ), y ha incluido el código en un paquete que puede instalarse usando la pip install matplotlib-label-lines
.
Bonita imagen:
En matplotlib
es bastante fácil etiquetar los diagramas de contorno (ya sea automáticamente o colocando manualmente las etiquetas 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 parcela 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 etiquetas 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 etiquetas. Incluso puede ajustar 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 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
y10
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
sonfloat
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. Echa un vistazo a la curva azul en el gráfico superior izquierdo de la bonita imagen. Si solo hubiera datos disponibles para el rangox
0.5
-1
, entonces posiblemente no podríamos colocar una etiqueta en la ubicación deseada (que es un poco menos de0.2
). Vea esta pregunta para 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 argumentoxvals
; 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é valoresx
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 objetivo extra de alinear las etiquetas 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()