Встроенные метки в 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()
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')
6 ответов
Хороший вопрос, некоторое время назад я немного экспериментировал с этим, но не использовал его много, потому что он все еще не пуленепробиваемый. Я разделил область графика на сетку 32х32 и рассчитал "потенциальное поле" для наилучшего положения метки для каждой линии в соответствии со следующими правилами:
- пустое пространство - хорошее место для этикетки
- Метка должна быть рядом с соответствующей строкой
- Метка должна быть вдали от других строк
Код был примерно таким:
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()
И полученный сюжет:
Обновление: пользователь cphyc любезно создал Github-репозиторий для кода в этом ответе (см. Здесь) и собрал код в пакет, который можно установить с помощью pip install matplotlib-label-lines
,
Приятная картина:
В matplotlib
довольно просто пометить контурные графики (либо автоматически, либо вручную размещая метки с помощью щелчков мыши). Похоже, что (пока) не существует какой-либо эквивалентной возможности помечать серии данных таким образом! Может быть некоторая семантическая причина не включать эту функцию, которую я пропускаю.
Несмотря на это, я написал следующий модуль, который принимает любой позволяет полуавтоматическую маркировку графика. Требуется только numpy
и пара функций из стандарта math
библиотека.
Описание
Поведение по умолчанию labelLines
Функция заключается в равномерном расположении меток вдоль x
ось (автоматически размещается в правильном y
-значение конечно). Если вы хотите, вы можете просто передать массив координат х каждой из меток. Вы можете даже настроить расположение одной метки (как показано на нижнем правом графике) и равномерно распределить остальные, если хотите.
В дополнение label_lines
функция не учитывает строки, которым не присвоена метка в plot
команда (или точнее, если метка содержит '_line'
).
Ключевое слово аргументы переданы labelLines
или же labelLine
передаются на text
вызов функции (некоторые ключевые аргументы устанавливаются, если вызывающий код решает не указывать).
вопросы
- Ограничивающие рамки аннотации иногда нежелательно мешают другим кривым. Как показано
1
а также10
аннотации в верхнем левом сюжете. Я даже не уверен, что этого можно избежать. - Было бы неплохо указать
y
Положение вместо этого иногда. - Это все еще итеративный процесс, чтобы получить аннотации в нужном месте
- Это работает только когда
x
осевые значенияfloat
s
Gotchas
- По умолчанию
labelLines
Функция предполагает, что все серии данных охватывают диапазон, заданный пределами оси. Взгляните на синюю кривую на верхнем левом графике красивой картинки. Если бы были доступны только данные дляx
спектр0.5
-1
тогда мы не могли бы разместить ярлык в нужном месте (что немного меньше, чем0.2
). Смотрите этот вопрос для особенно неприятного примера. В настоящее время код не определяет разумно этот сценарий и не переупорядочивает метки, однако существует разумный обходной путь. Функция labelLines принимаетxvals
аргумент; списокx
-значения, указанные пользователем вместо линейного распределения по ширине по умолчанию. Таким образом, пользователь может решить, какойx
-значения использовать для размещения метки каждого ряда данных.
Кроме того, я считаю, что это первый ответ, который завершает бонусную задачу по выравниванию меток с кривой, на которой они находятся.:)
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 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()
Ответ @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
,
matplotx (который я написал) имеет
line_labels()
который наносит метки справа от линий. Это также достаточно умно, чтобы избежать перекрытий, когда слишком много линий сосредоточено в одном месте. (См . Примеры в звездном графике.) Это достигается путем решения конкретной неотрицательной задачи наименьших квадратов на целевых позициях меток. В любом случае, во многих случаях, когда перекрытия нет, как в приведенном ниже примере, в этом даже нет необходимости.
import matplotlib.pyplot as plt
import matplotx
import numpy as np
# create data
rng = np.random.default_rng(0)
offsets = [1.0, 1.50, 1.60]
labels = ["no balancing", "CRV-27", "CRV-27*"]
x0 = np.linspace(0.0, 3.0, 100)
y = [offset * x0 / (x0 + 1) + 0.1 * rng.random(len(x0)) for offset in offsets]
# plot
with plt.style.context(matplotx.styles.dufte):
for yy, label in zip(y, labels):
plt.plot(x0, yy, label=label)
plt.xlabel("distance [m]")
matplotx.ylabel_top("voltage [V]") # move ylabel to the top, rotate
matplotx.line_labels() # line labels to the right
plt.show()
# plt.savefig("out.png", bbox_inches="tight")
Я применил другой подход на основе идеи @NauticalMile, реализованной @cphyc, добавив автоматическое позиционирование меток с помощью Shapely, чтобы избежать перекрытия. Это может быть полезно, если у вас много графиков со многими линиями, поскольку позволяет избежать позиционирования меток вручную.
С оригинальными примерами он дает:Примеры
Вы можете попробовать это сpip install matplotlib-inline-labels
. Смотрите репо .
Более простой подход, такой как у Иоанниса Филиппидиса:
import matplotlib.pyplot as plt
import numpy as np
# evenly sampled time at 200ms intervals
tMin=-1 ;tMax=10
t = np.arange(tMin, tMax, 0.1)
# red dashes, blue points default
plt.plot(t, 22*t, 'r--', t, t**2, 'b')
factor=3/4 ;offset=20 # text position in view
textPosition=[(tMax+tMin)*factor,22*(tMax+tMin)*factor]
plt.text(textPosition[0],textPosition[1]+offset,'22 t',color='red',fontsize=20)
textPosition=[(tMax+tMin)*factor,((tMax+tMin)*factor)**2+20]
plt.text(textPosition[0],textPosition[1]+offset, 't^2', bbox=dict(facecolor='blue', alpha=0.5),fontsize=20)
plt.show()