如何在Matplotlib中指定类似箭头的线条样式?

时间:2011-11-23 19:28:45

标签: python matplotlib

我想在Matplotlib中以一种指示特定路径的方式显示一组xy数据。理想情况下,linestyle会被修改为使用类似箭头的补丁。我创建了一个模型,如下所示(使用Omnigraphsketcher)。看起来我应该能够覆盖其中一个常见的linestyle声明('-''--'':'等)。

请注意,我不想简单地用一个箭头连接每个数据点---实际数据点的间距不均匀,我需要一致的箭头间距。

enter image description here

5 个答案:

答案 0 :(得分:7)

这是一个起点:

  1. 按固定步骤(在我下面的示例中为aspace)沿着您的线行走。

    一个。这涉及沿着由两组点(x1y1)和(x2y2)创建的线段采取步骤。

    B中。如果您的步长比线段长,请转到下一组点。

  2. 此时确定线的角度。

  3. 绘制一个倾斜度与该角度相对应的箭头。

  4. 我写了一个小脚本来证明这一点:

    import numpy as np
    import matplotlib.pyplot as plt
    
    fig = plt.figure()
    axes = fig.add_subplot(111)
    
    # my random data
    scale = 10 
    np.random.seed(101)
    x = np.random.random(10)*scale
    y = np.random.random(10)*scale
    
    # spacing of arrows
    aspace = .1 # good value for scale of 1
    aspace *= scale
    
    # r is the distance spanned between pairs of points
    r = [0]
    for i in range(1,len(x)):
        dx = x[i]-x[i-1]
        dy = y[i]-y[i-1]
        r.append(np.sqrt(dx*dx+dy*dy))
    r = np.array(r)
    
    # rtot is a cumulative sum of r, it's used to save time
    rtot = []
    for i in range(len(r)):
        rtot.append(r[0:i].sum())
    rtot.append(r.sum())
    
    arrowData = [] # will hold tuples of x,y,theta for each arrow
    arrowPos = 0 # current point on walk along data
    rcount = 1 
    while arrowPos < r.sum():
        x1,x2 = x[rcount-1],x[rcount]
        y1,y2 = y[rcount-1],y[rcount]
        da = arrowPos-rtot[rcount] 
        theta = np.arctan2((x2-x1),(y2-y1))
        ax = np.sin(theta)*da+x1
        ay = np.cos(theta)*da+y1
        arrowData.append((ax,ay,theta))
        arrowPos+=aspace
        while arrowPos > rtot[rcount+1]: 
            rcount+=1
            if arrowPos > rtot[-1]:
                break
    
    # could be done in above block if you want
    for ax,ay,theta in arrowData:
        # use aspace as a guide for size and length of things
        # scaling factors were chosen by experimenting a bit
        axes.arrow(ax,ay,
                   np.sin(theta)*aspace/10,np.cos(theta)*aspace/10, 
                   head_width=aspace/8)
    
    
    axes.plot(x,y)
    axes.set_xlim(x.min()*.9,x.max()*1.1)
    axes.set_ylim(y.min()*.9,y.max()*1.1)
    
    plt.show()
    

    此示例导致此图: enter image description here

    对于初学者来说,这里还有很大的改进空间:

    1. 可以使用FancyArrowPatch自定义箭头的外观。
    2. 创建箭头时可以添加进一步的测试,以确保它们不会超出线条。这与在顶点处或顶点附近创建的箭头相关,其中线条明显改变方向。对于上面最正确的观点,就是这种情况。
    3. 可以从这个脚本中创建一个方法,该方法适用于更广泛的案例,即使其更具可移植性。
    4. 在研究这个时,我发现了quiver绘图方法。它可能能够取代上述工作,但并不是很明显,这是有保证的。

答案 1 :(得分:6)

Yann非常好的答案,但是使用箭头时,生成的箭头会受到轴纵横比和限制的影响。我创建了一个使用axes.annotate()而不是axes.arrow()的版本。我把它包含在这里供其他人使用。

简而言之,这用于在matplotlib中沿着你的线绘制箭头。代码如下所示。通过添加具有不同箭头的可能性仍然可以改进它。在这里,我只包括控制箭头的宽度和长度。

import numpy as np
import matplotlib.pyplot as plt


def arrowplot(axes, x, y, narrs=30, dspace=0.5, direc='pos', \
                          hl=0.3, hw=6, c='black'): 
    ''' narrs  :  Number of arrows that will be drawn along the curve

        dspace :  Shift the position of the arrows along the curve.
                  Should be between 0. and 1.

        direc  :  can be 'pos' or 'neg' to select direction of the arrows

        hl     :  length of the arrow head 

        hw     :  width of the arrow head        

        c      :  color of the edge and face of the arrow head  
    '''

    # r is the distance spanned between pairs of points
    r = [0]
    for i in range(1,len(x)):
        dx = x[i]-x[i-1] 
        dy = y[i]-y[i-1] 
        r.append(np.sqrt(dx*dx+dy*dy))
    r = np.array(r)

    # rtot is a cumulative sum of r, it's used to save time
    rtot = []
    for i in range(len(r)):
        rtot.append(r[0:i].sum())
    rtot.append(r.sum())

    # based on narrs set the arrow spacing
    aspace = r.sum() / narrs

    if direc is 'neg':
        dspace = -1.*abs(dspace) 
    else:
        dspace = abs(dspace)

    arrowData = [] # will hold tuples of x,y,theta for each arrow
    arrowPos = aspace*(dspace) # current point on walk along data
                                 # could set arrowPos to 0 if you want
                                 # an arrow at the beginning of the curve

    ndrawn = 0
    rcount = 1 
    while arrowPos < r.sum() and ndrawn < narrs:
        x1,x2 = x[rcount-1],x[rcount]
        y1,y2 = y[rcount-1],y[rcount]
        da = arrowPos-rtot[rcount]
        theta = np.arctan2((x2-x1),(y2-y1))
        ax = np.sin(theta)*da+x1
        ay = np.cos(theta)*da+y1
        arrowData.append((ax,ay,theta))
        ndrawn += 1
        arrowPos+=aspace
        while arrowPos > rtot[rcount+1]: 
            rcount+=1
            if arrowPos > rtot[-1]:
                break

    # could be done in above block if you want
    for ax,ay,theta in arrowData:
        # use aspace as a guide for size and length of things
        # scaling factors were chosen by experimenting a bit

        dx0 = np.sin(theta)*hl/2. + ax
        dy0 = np.cos(theta)*hl/2. + ay
        dx1 = -1.*np.sin(theta)*hl/2. + ax
        dy1 = -1.*np.cos(theta)*hl/2. + ay

        if direc is 'neg' :
          ax0 = dx0 
          ay0 = dy0
          ax1 = dx1
          ay1 = dy1 
        else:
          ax0 = dx1 
          ay0 = dy1
          ax1 = dx0
          ay1 = dy0 

        axes.annotate('', xy=(ax0, ay0), xycoords='data',
                xytext=(ax1, ay1), textcoords='data',
                arrowprops=dict( headwidth=hw, frac=1., ec=c, fc=c))


    axes.plot(x,y, color = c)
    axes.set_xlim(x.min()*.9,x.max()*1.1)
    axes.set_ylim(y.min()*.9,y.max()*1.1)


if __name__ == '__main__':
    fig = plt.figure()
    axes = fig.add_subplot(111)

    # my random data
    scale = 10 
    np.random.seed(101)
    x = np.random.random(10)*scale
    y = np.random.random(10)*scale
    arrowplot(axes, x, y ) 

    plt.show()

结果图可以在这里看到:

enter image description here

答案 2 :(得分:1)

Yann的回答的矢量化版本:

timesAsPyDt = (spy0030Df['dt']).apply(lambda d: pd.to_datetime(str(d)))

答案 3 :(得分:0)

以下是Duarte代码的修改和简化版本。当我运行具有各种数据集和宽高比的代码时,我遇到了问题,因此我将其清理干净并使用FancyArrowPatches作为箭头。请注意,示例图的x值与y轴相差1,000,000倍。

我也改为在显示坐标中绘制箭头,因此x和y轴上的不同缩放不会改变箭头长度。

在此过程中,我发现了matplotlib的FancyArrowPatch中的一个错误,该错误在绘制纯垂直箭头时会发生爆炸。我在我的代码中找到了解决办法。

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches


def arrowplot(axes, x, y, nArrs=30, mutateSize=10, color='gray', markerStyle='o'): 
    '''arrowplot : plots arrows along a path on a set of axes
        axes   :  the axes the path will be plotted on
        x      :  list of x coordinates of points defining path
        y      :  list of y coordinates of points defining path
        nArrs  :  Number of arrows that will be drawn along the path
        mutateSize :  Size parameter for arrows
        color  :  color of the edge and face of the arrow head
        markerStyle : Symbol

        Bugs: If a path is straight vertical, the matplotlab FanceArrowPatch bombs out.
          My kludge is to test for a vertical path, and perturb the second x value
          by 0.1 pixel. The original x & y arrays are not changed

        MHuster 2016, based on code by 
    '''
    # recast the data into numpy arrays
    x = np.array(x, dtype='f')
    y = np.array(y, dtype='f')
    nPts = len(x)

    # Plot the points first to set up the display coordinates
    axes.plot(x,y, markerStyle, ms=5, color=color)

    # get inverse coord transform
    inv = ax.transData.inverted()

    # transform x & y into display coordinates
    # Variable with a 'D' at the end are in display coordinates
    xyDisp = np.array(axes.transData.transform(zip(x,y)))
    xD = xyDisp[:,0]
    yD = xyDisp[:,1]

    # drD is the distance spanned between pairs of points
    # in display coordinates
    dxD = xD[1:] - xD[:-1]
    dyD = yD[1:] - yD[:-1]
    drD = np.sqrt(dxD**2 + dyD**2)

    # Compensating for matplotlib bug
    dxD[np.where(dxD==0.0)] = 0.1


    # rtotS is the total path length
    rtotD = np.sum(drD)

    # based on nArrs, set the nominal arrow spacing
    arrSpaceD = rtotD / nArrs

    # Loop over the path segments
    iSeg = 0
    while iSeg < nPts - 1:
        # Figure out how many arrows in this segment.
        # Plot at least one.
        nArrSeg = max(1, int(drD[iSeg] / arrSpaceD + 0.5))
        xArr = (dxD[iSeg]) / nArrSeg # x size of each arrow
        segSlope = dyD[iSeg] / dxD[iSeg]
        # Get display coordinates of first arrow in segment
        xBeg = xD[iSeg]
        xEnd = xBeg + xArr
        yBeg = yD[iSeg]
        yEnd = yBeg + segSlope * xArr
        # Now loop over the arrows in this segment
        for iArr in range(nArrSeg):
            # Transform the oints back to data coordinates
            xyData = inv.transform(((xBeg, yBeg),(xEnd,yEnd)))
            # Use a patch to draw the arrow
            # I draw the arrows with an alpha of 0.5
            p = patches.FancyArrowPatch( 
                xyData[0], xyData[1], 
                arrowstyle='simple',
                mutation_scale=mutateSize,
                color=color, alpha=0.5)
            axes.add_patch(p)
            # Increment to the next arrow
            xBeg = xEnd
            xEnd += xArr
            yBeg = yEnd
            yEnd += segSlope * xArr
        # Increment segment number
        iSeg += 1

if __name__ == '__main__':
    import numpy as np
    import matplotlib.pyplot as plt
    fig = plt.figure()
    ax = fig.add_subplot(111)
    # my random data
    xScale = 1e6
    np.random.seed(1)
    x = np.random.random(10) * xScale
    y = np.random.random(10)
    arrowplot(ax, x, y, nArrs=4*(len(x)-1), mutateSize=10, color='red')
    xRng = max(x) - min(x)
    ax.set_xlim(min(x) - 0.05*xRng, max(x) + 0.05*xRng)
    yRng = max(y) - min(y)
    ax.set_ylim(min(y) - 0.05*yRng, max(y) + 0.05*yRng)
    plt.show()

enter image description here

答案 4 :(得分:0)

如果你可以不用那些花哨的绕边/固定长度的箭头,这里有一个穷人版本,将这些段细分为大约fork() 长段。如果 exec() 不是太大,我眼中的方差可以忽略不计。

ds

plot