Matplotlib中的内联标签


100

在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')

带有内联标签的图

Answers:


28

很好的问题,前一阵子我对此进行了一些试验,但是由于它仍然不是防弹的,所以并没有使用很多。我将绘图区域划分为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()

和结果情节: 在此处输入图片说明


非常好。但是,我有一个无法完全正常工作的示例:plt.plot(x2, 3*x2**2, label="3x*x"); plt.plot(x2, 2*x2**2, label="2x*x"); plt.plot(x2, 0.5*x2**2, label="0.5x*x"); plt.plot(x2, -1*x2**2, label="-x*x"); plt.plot(x2, -2.5*x2**2, label="-2.5*x*x"); my_legend();这会将标签之一放在左上角。有想法该怎么解决这个吗?似乎问题出在,这些线条太靠近了。
egpbos 2014年

对不起,忘了x2 = np.linspace(0,0.5,100)
egpbos 2014年

有什么办法可以不加掩饰地使用它吗?在我当前的系统上,安装很麻烦。
AnnanFay

在Python 3.6.4,Matplotlib 2.1.2和Scipy 1.0.0下,这对我不起作用。更新print命令后,它会运行并创建4个图,其中3个图看起来像是乱七八糟的(可能与32x32有关),第四个图的标签在奇数处。
Y戴维斯

84

更新:cphyc用户已经为此答案中的代码创建了一个Github存储库(请参阅此处),并将代码捆绑到可以使用进行安装的软件包中pip install matplotlib-label-lines


美丽的照片:

半自动地块标记

matplotlib其中标记轮廓图非常容易(自动或通过单击鼠标手动放置标签)。似乎还没有以这种方式标记数据系列的等效功能!可能有一些语义上的原因不包括此功能,而我却没有。

无论如何,我已经编写了以下模块,该模块采用了任何允许半自动绘图的标签。它仅需要numpy标准math库中的几个功能。

描述

labelLines功能的默认行为是使标签沿x轴均匀分布(自动以正确y的当然值放置)。如果需要,可以只传递每个标签的x坐标的数组。您甚至可以调整一个标签的位置(如右下图所示),并根据需要均匀分布其余标签。

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

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

问题

  • 注释边界框有时会不希望地干扰其他曲线。如左上图的110注释所示。我什至不确定这是可以避免的。
  • y有时最好指定一个职位。
  • 在正确的位置获取批注仍然是一个反复的过程
  • 仅当x-axis值为floats 时才有效

陷阱

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

1
@blujay我很高兴您能够适应您的需求。我将添加该约束作为一个问题。
NauticalMile

1
@Liza阅读我刚刚添加的内容,了解发生这种情况的原因。对于您的情况(我假设它与该问题中的问题类似),除非您想要手动创建的列表,否则您xvals可能需要labelLines稍作修改:在if xvals is None:范围内更改代码以创建基于其他条件的列表。您可以开始xvals = [(np.min(l.get_xdata())+np.max(l.get_xdata()))/2 for l in lines]
-NauticalMile

1
@Liza您的图表吸引了我。问题在于您的数据在整个图中分布不均匀,并且有很多曲线几乎彼此重叠。使用我的解决方案,在很多情况下很难区分标签。我认为最好的解决方案是在绘图的不同空白部分中放置堆叠的标签块。请参见此图,以获取具有两个堆叠标签块的示例(一个块带有1个标签,另一个块带有4个标签)。实施此操作会花费很多时间,我可能会在将来的某个时候这样做。
NauticalMile

1
注:因为Matplotlib 2.0,.get_axes()并且.get_axis_bgcolor()已被弃用。请用.axes和代替.get_facecolor()
贾金

1
另一个很棒的事情labellines是与它相关plt.textax.text适用的属性。意味着您可以在函数中设置fontsizebbox参数labelLines()
tionichm

52

@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


1
+1!它看起来像一个不错的简单解决方案。对不起,这很懒惰,但是看起来怎么样?文本是在图内还是在y轴右上方?
rocarvaj

1
@rocarvaj这取决于其他设置。标签可能会突出到绘图框之外。避免此行为的两种方法是:1)使用不同于的索引-1,2)设置适当的轴限制以为标签留出空间。
Ioannis Filippidis

1
如果曲线图集中在某些y值上,也会变得一团糟-端点变得太靠近以至于文本看起来
不太

@LazyCat:是的。要解决此问题,可以使注释可拖动。我猜有点痛苦,但是可以解决问题。
PlacidLush

1

像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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.