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

188821 次浏览

问得好,前段时间我用它做过一些实验,但还没用过多少次,因为它还不是防弹的。我把地块区域划分成一个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

@ Jan Kuiken 的回答当然是深思熟虑的,但也有一些警告:

  • 并非所有情况下都有效
  • 它需要相当多的额外代码
  • 每块地的情况可能有很大的不同

一个更简单的方法是对每个情节的最后一点进行注释。为了强调,这个点也可以被圈起来。这可以通过一行额外的代码来实现:

import matplotlib.pyplot as plt


for i, (x, y) in enumerate(samples):
plt.plot(x, y)
plt.text(x[-1], y[-1], f'sample {i}')

一个变体是 使用方法是 matplotlib.axes.Axes.annotate

更新: 用户 死因已经为这个答案中的代码创建了一个 Github 存储库(参见 给你) ,并将代码打包到一个可以使用 pip install matplotlib-label-lines安装的包中。


美丽的图片:

semi-automatic plot-labeling

matplotlib中,标注等高线图非常容易(自动或通过鼠标点击手动放置标签)。似乎还没有任何等效的能力来以这种方式标记数据系列!可能有一些语义上的原因没有包括这个特性,我是缺少的。

无论如何,我已经编写了以下模块,采取任何允许半自动情节标签。它只需要 numpy和几个来自标准 math库的函数。

描述

labelLines函数的默认行为是沿着 x轴均匀地放置标签(当然是自动放置在正确的 y值)。如果需要,可以传递每个标签的 x 坐标数组。您甚至可以调整一个标签的位置(如右下图所示) ,如果您愿意,还可以将其余的标签均匀放置。

此外,label_lines函数没有考虑到在 plot命令中没有分配标签的行(如果标签包含 '_line',则更准确)。

传递给 labelLineslabelLine的关键字参数传递给 text函数调用(如果调用代码选择不指定,则设置一些关键字参数)。

问题

  • 注释边界框有时会不必要地干扰其他曲线。如左上图中的 110注释所示。我甚至不确定这是否可以避免。
  • 有时指定一个 y位置会比较好。
  • 在正确的位置获得注释仍然是一个迭代过程
  • 它只在 x轴的值为 float时起作用

抓到你了

  • 默认情况下,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 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()

一种更简单的方法,就像约阿尼斯 · 菲利皮迪斯(Ioannis Filippidis)所做的那样:

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()

在 sageCell 上编码 python 3

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")

enter image description here