In Matplotlib ist es nicht allzu schwierig, eine Legende zu erstellen (example_legend()
, siehe unten), aber ich denke, es ist besser, Beschriftungen direkt auf den Kurven zu platzieren, die gezeichnet werden (wie in example_inline()
, siehe unten) ). Dies kann sehr umständlich sein, da ich die Koordinaten von Hand angeben und die Beschriftungen wahrscheinlich neu positionieren muss, wenn ich die Zeichnung neu formatiere. Gibt es eine Möglichkeit, Beschriftungen auf Kurven in Matplotlib automatisch zu generieren? Bonuspunkte, um den Text in einem Winkel ausrichten zu können, der dem Winkel der Kurve entspricht.
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')
Schöne Frage, vor einiger Zeit habe ich ein bisschen damit experimentiert, es aber nicht oft benutzt, weil es immer noch nicht kugelsicher ist. Ich habe die Plotfläche in ein 32x32-Raster unterteilt und für jede Zeile ein 'potentielles Feld' für die beste Position einer Beschriftung nach folgenden Regeln berechnet:
Der Code war ungefähr so:
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()
Und die resultierende Handlung:
Update: Benutzer cphyc hat freundlicherweise ein Github-Repository für den Code in dieser Antwort erstellt (siehe hier =) und bündelte den Code in einem Paket, das mit pip install matplotlib-label-lines
installiert werden kann.
Schönes Bild:
In matplotlib
ist es ziemlich einfach Beschriftungskonturdiagramme (entweder automatisch oder durch manuelles Platzieren von Beschriftungen mit Mausklicks). Es scheint (noch) keine vergleichbare Möglichkeit zu geben, Datenreihen auf diese Weise zu kennzeichnen! Es kann einen semantischen Grund dafür geben, dieses Feature nicht aufzunehmen, den ich vermisse.
Unabhängig davon habe ich das folgende Modul geschrieben, das eine halbautomatische Plotbeschriftung zulässt. Es werden nur numpy
und einige Funktionen aus der Standardbibliothek math
benötigt.
Das Standardverhalten der Funktion labelLines
besteht darin, die Beschriftungen gleichmäßig entlang der Achse x
zu verteilen (wobei natürlich automatisch der richtige Wert für y
angegeben wird). Wenn Sie möchten, können Sie einfach ein Array der x-Koordinaten der einzelnen Beschriftungen übergeben. Sie können sogar die Position eines Etiketts optimieren (siehe Grafik unten rechts) und den Rest gleichmäßig verteilen, wenn Sie möchten.
Außerdem berücksichtigt die Funktion label_lines
Nicht die Zeilen, denen im Befehl plot
keine Beschriftung zugewiesen wurde (oder genauer gesagt, wenn die Beschriftung '_line'
Enthält).
An labelLines
oder labelLine
übergebene Schlüsselwortargumente werden an den Funktionsaufruf text
übergeben (einige Schlüsselwortargumente werden festgelegt, wenn der aufrufende Code keine Angabe macht).
1
Und 10
Oben links dargestellt. Ich bin mir nicht mal sicher, ob das vermieden werden kann.y
-Position anzugeben.x
- Achse float
s sindlabelLines
davon aus, dass sich alle Datenreihen über den durch die Achsengrenzen festgelegten Bereich erstrecken. Schauen Sie sich die blaue Kurve im oberen linken Diagramm des hübschen Bildes an. Wenn es nur Daten für den Bereich x
0.5
- 1
Gäbe, dann könnten wir möglicherweise keine Beschriftung an der gewünschten Stelle platzieren (was etwas weniger als 0.2
). Siehe diese Frage für ein besonders unangenehmes Beispiel. Im Moment identifiziert der Code dieses Szenario nicht intelligent und ordnet die Beschriftungen neu an, es gibt jedoch eine angemessene Problemumgehung. Die Funktion labelLines verwendet das Argument xvals
. Eine Liste der vom Benutzer angegebenen x
- Werte anstelle der linearen Standardverteilung über die Breite. Auf diese Weise kann der Benutzer entscheiden, welche x
- Werte für die Beschriftung der einzelnen Datenreihen verwendet werden sollen.Ich glaube auch, dass dies die erste Antwort ist, um das Bonus Ziel zu erreichen, die Beschriftungen an der Kurve auszurichten, auf der sie liegen. :)
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)
Testcode, um das hübsche Bild oben zu generieren:
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()
Die Antwort von @Jan Kuiken ist sicherlich gut durchdacht und gründlich, aber es gibt einige Vorbehalte:
Ein viel einfacherer Ansatz besteht darin, den letzten Punkt jedes Diagramms mit Anmerkungen zu versehen. Der Punkt kann zur Hervorhebung auch eingekreist werden. Dies kann mit einer zusätzlichen Zeile erreicht werden:
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))
Eine Variante wäre ax.annotate
.