Встроенные ярлыки в Matplotlib
В Matplotlib, это не слишком сложно сделать легенду (example_legend()
, ниже), но я считаю, что лучше стирать метки непосредственно на кривых, построенных (как в example_inline()
, ниже). Это может быть очень сложно, потому что я должен указать координаты вручную, и, если я переформатирую сюжет, мне, вероятно, придется переместить метки. Есть ли способ автоматически генерировать метки на кривых в Matplotlib? Бонусные точки за возможность ориентировать текст под углом, соответствующим углу кривой.
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()
![Figure with 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')
![Figure with inline labels]()
Ответы
Ответ 1
Хороший вопрос, некоторое время назад я немного экспериментировал с этим, но не использовал его много, потому что он все еще не пуленепробиваемый. Я разделил область графика на сетку 32x32 и вычислил "потенциальное поле" для лучшего положения метки для каждой строки в соответствии со следующими правилами:
- пробел является хорошим местом для ярлыка
- Метка должна быть рядом с соответствующей строкой
- Этикетка должна быть вдалеке от других линий.
Код был примерно таким:
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()
И получившийся сюжет:
![enter image description here]()
Ответ 2
Обновление: пользователь cphyc любезно создал Github-репозиторий для кода в этом ответе (см. Здесь) и собрал код в пакет, который можно установить с помощью pip install matplotlib-label-lines
.
Приятная картина:
![semi-automatic plot-labeling]()
В matplotlib
довольно легко маркировать контурные графики (либо автоматически, либо вручную размещая метки щелчками мыши). Похоже, что (пока) не существует какой-либо эквивалентной возможности обозначать серии данных таким образом! Может быть некоторая семантическая причина не включать эту функцию, которую я пропускаю.
Несмотря на это, я написал следующий модуль, который принимает любой позволяет полуавтоматическую маркировку графика. Требуется только numpy
и пара функций из стандартной math
библиотеки.
Описание
Поведение функции labelLines
по labelLines
состоит в том, чтобы равномерно размещать метки вдоль оси x
(разумеется, автоматически помещая в правильный y
-value). Если вы хотите, вы можете просто передать массив координат X каждой из меток. Вы можете даже настроить расположение одной метки (как показано на нижнем правом графике) и равномерно распределить остальные, если хотите.
Кроме того, функция label_lines
не учитывает строки, для которых не была назначена метка в команде plot
(или, точнее, если метка содержит '_line'
).
Аргументы ключевых слов, передаваемые в labelLines
или labelLine
, передаются вызову text
функции (некоторые аргументы ключевого слова устанавливаются, если вызывающий код решает не указывать).
вопросы
- Ограничивающие рамки аннотации иногда нежелательно мешают другим кривым. Как показано
1
и 10
аннотации в верхнем левом графике. Я даже не уверен, что этого можно избежать. - Было бы неплохо иногда указывать позицию
y
. - Это все еще итеративный процесс, чтобы получить аннотации в нужном месте
- Работает только тогда, когда значения
x
-axis являются числами с float
Gotchas
- По умолчанию функция
labelLines
предполагает, что все серии данных охватывают диапазон, заданный пределами оси. Взгляните на синюю кривую на верхнем левом графике красивой картинки. Если бы были доступны только данные для диапазона x
0.5
- 1
то мы не смогли бы разместить метку в желаемом месте (которое чуть меньше 0.2
). Смотрите этот вопрос для особенно неприятного примера. В настоящее время код не определяет разумно этот сценарий и не переупорядочивает метки, однако существует разумный обходной путь. Функция labelLines принимает аргумент xvals
; список x
-value, указанных пользователем вместо линейного распределения по ширине по умолчанию. Таким образом, пользователь может решить, какие x
-value использовать для размещения меток в каждой серии данных.
Кроме того, я считаю, что это первый ответ, который завершает бонусную задачу по выравниванию меток с кривой, на которой они находятся. :)
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)
Тестовый код для генерации красивой картинки выше:
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()
Ответ 3
Ответ @Jan Kuiken, конечно, продуман и тщателен, но есть некоторые оговорки:
- это работает не во всех случаях
- это требует изрядного количества дополнительного кода
- это может значительно отличаться от одного участка к другому
Гораздо более простой подход состоит в том, чтобы аннотировать последнюю точку каждого графика. Точка также может быть обведена, для акцента. Это можно сделать с помощью одной дополнительной строки:
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))
Вариант будет использовать ax.annotate
.