更新: 用户cphyc友情提供了此答案中代码的Github仓库(请查看这里),并将代码捆绑成一个包,可以使用pip install matplotlib-label-lines
进行安装。
漂亮图片:
![semi-automatic plot-labeling](https://istack.dev59.com/Onujs.webp)
在
matplotlib
中,
轮廓图标签(可以自动或手动通过鼠标单击放置标签)非常容易。但是,似乎还没有类似的功能来以这种方式标记数据系列!我可能错过了某些语义上不包括此功能的原因。
无论如何,我编写了以下模块,允许半自动绘图标注。它只需要
numpy
和标准
math
库中的一些函数。
描述
labelLines
函数的默认行为是沿着
x
轴均匀分布标签(当然自动放置在正确的
y
值)。如果您想要,您只需传递每个标签的x坐标数组即可。如果您喜欢,您甚至可以调整一个标签的位置(如右下角的图所示),并将其余标签均匀分布。
此外,
label_lines
函数没有考虑在
plot
命令中未分配标签的行(或更准确地说,如果标签包含
'_line'
)。
传递给
labelLines
或
labelLine
的关键字参数会传递到
text
函数调用中(如果调用代码选择不指定,则会设置某些关键字参数)。
问题
- 注释边界框有时会与其他曲线产生不希望的干扰。 如左上图中的
1
和10
注释所示。我甚至不确定这能否避免。
- 有时候指定
y
位置会很好。
- 将注释放置在正确位置仍然是一个迭代的过程
- 只有当
x
轴值为float
时才有效
陷阱
默认情况下,
labelLines
函数假定所有数据系列跨越轴限定的范围。请看漂亮图片左上角的蓝色曲线。如果只有
x
范围为
0.5
-
1
的数据可用,那么我们不可能在所需位置放置标签(这个位置略小于
0.2
)。参见
this question,了解一个特别恶劣的例子。目前,代码无法智能识别此场景并重新排列标签,但是有一个合理的解决方法。labelLines函数接受
xvals
参数;用户指定的
x
值列表,而不是默认的宽度上的线性分布。因此,用户可以决定使用哪些
x
值来放置每个数据系列的标签。
此外,我相信这是第一个完成将标签与其所在曲线对齐的
奖励目标的答案。 :)
label_lines.py:
from math import atan2,degrees
import numpy as np
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
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:
dx = xdata[ip] - xdata[ip-1]
dy = ydata[ip] - ydata[ip-1]
ang = degrees(atan2(dy,dx))
pt = np.array([x,y]).reshape((1,2))
trans_angle = ax.transData.transform_angles(np.array((ang,)),pt)[0]
else:
trans_angle = 0
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 = []
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()
xvals
的列表,否则你可能需要稍微修改一下labelLines
代码:在if xvals is None:
范围内的代码中根据其他条件来创建一个列表。你可以从xvals = [(np.min(l.get_xdata())+np.max(l.get_xdata()))/2 for l in lines]
开始。 - NauticalMile.get_axes()
和.get_axis_bgcolor()
已被弃用。请分别替换为.axes
和.get_facecolor()
。 - Jiageng