Etykiety Inline w Matplotlib
W Matplotlib nie jest zbyt trudno stworzyć legendę (example_legend()
, poniżej), ale myślę, że lepszym stylem jest umieszczanie etykiet bezpośrednio na wykreślanych krzywych (jak w example_inline()
, poniżej). Może to być bardzo skrzypiące, ponieważ muszę ręcznie określać współrzędne, a jeśli przeformatuję Wykres, prawdopodobnie będę musiał zmienić położenie etykiet. Czy istnieje sposób na automatyczne generowanie etykiet na krzywych w Matplotlib? Punkty bonusowe za możliwość ustawienia tekstu pod kątem odpowiadającym kątowi krzywa.
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')
3 answers
[1]] ładne pytanie, jakiś czas temu trochę z tym eksperymentowałem, ale nie używałem go zbyt często, bo nadal nie jest kuloodporny. Podzieliłem obszar wykresu na siatkę 32x32 i obliczyłem "pole potencjału" dla najlepszej pozycji etykiety dla każdej linii zgodnie z następującymi zasadami:
- biała przestrzeń jest dobrym miejscem na etykietę
- etykieta powinna znajdować się w pobliżu odpowiedniego wiersza
- etykieta powinna być oddalona od pozostałych linii
Kod był czymś w rodzaju to:
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()
I wynikowy Wykres:
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-08 14:47:09
Update: użytkownik cphyc uprzejmie utworzył repozytorium Github dla kodu w tej odpowiedzi (zobacz tutaj ) i połączył kod w pakiet, który można zainstalować za pomocą pip install matplotlib-label-lines
.
Ładne Zdjęcie:
W matplotlib
bardzo łatwo jest etykietować wykresy konturu (automatycznie lub ręcznie umieszczając etykiety za pomocą kliknięć myszką). Nie ma (jeszcze) żadnej równoważnej możliwości oznaczania seria danych w ten sposób! Może być jakiś semantyczny powód, dla którego nie włączam tej funkcji, której mi brakuje.
Niezależnie od tego, napisałem następujący moduł, który przyjmuje dowolne pozwala na półautomatyczne etykietowanie Wykresów. Wymaga tylko numpy
i kilku funkcji ze standardowej biblioteki math
.
Opis
Domyślnym zachowaniem funkcji labelLines
jest równomierne rozmieszczanie etykiet wzdłuż osi x
(automatyczne umieszczanie na właściwej wartości y
oczywiście). Jeśli chcesz, możesz po prostu przekazać tablicę współrzędnych x każdej z etykiet. Możesz nawet dostosować lokalizację jednej etykiety (jak pokazano na dolnym prawym wykresie), a resztę równomiernie rozmieścić, jeśli chcesz.
Dodatkowo, funkcja label_lines
nie uwzględnia linii, które nie miały etykiety przypisanej w poleceniu plot
(lub dokładniej, jeśli etykieta zawiera '_line'
).
Argumenty słów kluczowych przekazywane do labelLines
lub labelLine
są przekazywane do funkcji text
wywołanie (niektóre argumenty słów kluczowych są ustawiane, jeśli kod wywołujący nie chce podać).
Problemy
- obramowania adnotacji czasami zakłócają niepożądanie inne krzywe. Jak pokazano w adnotacjach
1
i10
w lewym górnym rogu wykresu. Nie jestem nawet pewien, czy można tego uniknąć. - byłoby miło czasem określić
y
pozycję.
To wciąż iteracyjny proces, aby uzyskać adnotacje we właściwym miejscu.]}
- działa tylko wtedy, gdy wartości osi
x
tofloat
s
Gotchas
- domyślnie funkcja
labelLines
zakłada, że wszystkie serie danych obejmują zakres określony przez granice osi. Spójrz na niebieską krzywą w lewym górnym rogu ładnego obrazu. Gdyby dostępne były tylko dane dla zakresux
0.5
-1
następnie nie moglibyśmy umieścić etykiety w pożądanym miejscu(które jest nieco mniejsze niż0.2
). Zobacz to pytanie dla szczególnie paskudnego przykładu. W tej chwili kod nie identyfikuje inteligentnie tego scenariusza i nie układa etykiet, jednak istnieje rozsądne obejście. Funkcja labelLines przyjmuje argumentxvals
; listęx
- wartości podanych przez użytkownika zamiast domyślnego rozkładu liniowego na całej szerokości. W ten sposób użytkownik może zdecydować, które wartościx
- użyć do umieszczenia etykiet dla każdej serii danych.
Również, uważam, że jest to pierwsza odpowiedź, aby zakończyć bonus cel wyrównanie etykiet do krzywej, na której się znajdują. :)
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)
Kod testowy do wygenerowania pięknego zdjęcia powyżej:
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()
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-16 13:17:09
@ Jan Kuiken odpowiedź jest z pewnością przemyślana i dokładna, ale są pewne zastrzeżenia:
- nie działa we wszystkich przypadkach
- wymaga sporej ilości dodatkowego kodu
- może się znacznie różnić od jednej działki do następnej
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))
Wariantem byłoby użycie ax.annotate
.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-14 10:59:15