Draw rounded fancyarrowpatch with midpoint arrow in matplotlib

十年热恋 提交于 2019-12-31 03:36:08

问题


I've been trying to push the boundaries of matplotlib's patches and instruct it to draw a rounded FancyArrowPatch with a directional arrow on its midpoint. This would prove incredibly useful in a network representation I am trying to create.

My coding hours with python are not yet in the double digit, so I can't say I have a clear understanding of matplotlib's patches.py, but I have narrowed down the solution to two possible strategies:

  1. the smart, possibly pythonic way: create a custom arrowstyle class which further requires a modification of the _get_arrow_wedge() function to include a midpoint coordinates. This may be beyond my possibilities for now, or
  2. the lazy way: extract the midpoint coordinates from an elicited FancyArrowPatch and draw the desired arrowstyle on such coordinates.

Of course, so far I've chosen the lazy way. I did some early experimenting with extracting the midpoint coordinates of a curved FancyArrowPatch using get_path() and get_path_in_displaycoord(), but I can't seem to predict the precise midpoint coordinates. Some help would be very appreciated.

My fiddling so far:

import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch

n1 = (2,3)
n2 = (4,6)


# Try with multiple arc radius sizes, draw a separate plot each time
for rad in range(20):
    #setup figure
    figure = plt.figure()
    ax = plt.subplot(111)
    plt.annotate('rad:' + str(rad/25.),xy=(2,5))

    # create rounded fancyarrowpatch   
    t = FancyArrowPatch(posA=n1,posB=n2,
                        connectionstyle='arc3,rad=%s'%float(rad/25.),
                        arrowstyle='->',
                        shrinkA=0,
                        shrinkB=0,
                        mutation_scale=0.5)

    # extract vertices from get_path: points P#
    path = t.get_path().vertices.tolist()
    lab, px, py = ['P{0}'.format(i) for i in range(len(path))], [u[0] for u in path],[u[1] for u in path]
    for i in range(len(path)):
        plt.annotate(lab[i],xy=(px[i],py[i]))

    # extract vertices from get_path_in_displaycoord (but they are useless) : points G#
    newpath = t.get_path_in_displaycoord()
    a,b = newpath[0][0].vertices.tolist(), newpath[0][1].vertices.tolist()
    a.extend(b)
    glab, gx, gy = ['G{0}'.format(i) for i in range(len(a))], [u[0] for u in a],[u[1] for u in a]
    for i in range(len(a)):
        plt.annotate(glab[i],xy=(gx[i],gy[i]))    

    #point A: start
    x1, y1 = n1
    plt.annotate('A',xy=(x1,y1))    

    #point B:end
    x2, y2 = n2
    plt.annotate('B',xy=(x2,y2))

    #point M: the 'midpoint' as defined by class Arc3, specifically its connect() function
    x12, y12 = (x1 + x2) / 2., (y1 + y2) / 2.
    dx, dy = x2 - x1, y2 - y1
    cx, cy = x12 + (rad/100.) * dy, y12 - (rad/100.) * dx    
    plt.annotate('M',xy=(cx,cy))

    #point O : midpoint between M and P1, the second vertex from get_path   
    mx,my = (cx + px[1])/2., (cy + py[1])/2.
    plt.annotate('O',xy=(mx,my))

    ax.add_patch(t)
    plt.scatter([x1,cx,x2,mx,gx].extend(px),[y1,cy,y2,my,gy].extend(py))


plt.show()

EDIT: taking onboard @cphlewis suggestions: I tried to reconstruct the Bezier curve:

def bezcurv(start,control,end,tau):
    ans = []
    for t in tau:
        B = [(1-t)**2 * start[i] + 2*(1-t)*t*end[i] + (t**2)*control[i] for i in range(len(start))]
        ans.append(tuple(B))
    return ans

I thus add the generated line to the original plot:

tau = [time/100. for time in range(101)]
bezsim = bezcurv(n1,n2,(cx,cy),tau)
simx,simy = [b[0] for b in bezsim], [b[1] for b in bezsim]

The green line below is (should be?) the reconstructed bezier curve, though it's clearly not.


回答1:


After much struggling, I convinced myself that to solve this I had to part away from the FancyArrowPatch suite and create something from scratch. Here is a working solution that, far from fulfilling any perfectionist spirit, satisfied me:

import matplotlib.pyplot as plt
import numpy as np
from numpy.random import seed, randint

# Build function that connects two points with a curved line, 
# and an arrow on the middle of it

seed(1679)

narrow = 3
rad_one = 50
numpoints = 3

random_points = list(randint(1,20,[numpoints,4]))
rpoints = [[(a,b),(c,d)] for a,b,c,d in random_points]

def curvline(start,end,rad,t=100,arrows=1,push=0.8):
    #Compute midpoint
    rad = rad/100.    
    x1, y1 = start
    x2, y2 = end
    y12 = (y1 + y2) / 2
    dy = (y2 - y1)
    cy = y12 + (rad) * dy
    #Prepare line
    tau = np.linspace(0,1,t)
    xsupport = np.linspace(x1,x2,t)
    ysupport = [(1-i)**2 * y1 + 2*(1-i)*i*cy + (i**2)*y2 for i in tau]
    #Create arrow data    
    arset = list(np.linspace(0,1,arrows+2))
    c = zip([xsupport[int(t*a*push)] for a in arset[1:-1]],
                      [ysupport[int(t*a*push)] for a in arset[1:-1]])
    dt = zip([xsupport[int(t*a*push)+1]-xsupport[int(t*a*push)] for a in arset[1:-1]],
                      [ysupport[int(t*a*push)+1]-ysupport[int(t*a*push)] for a in arset[1:-1]])
    arrowpath = zip(c,dt)
    return xsupport, ysupport, arrowpath

def plotcurv(start,end,rad,t=100,arrows=1,arwidth=.25):
    x, y, c = curvline(start,end,rad,t,arrows)
    plt.plot(x,y,'k-')
    for d,dt in c:
        plt.arrow(d[0],d[1],dt[0],dt[1], shape='full', lw=0, 
                  length_includes_head=False, head_width=arwidth)
    return c

#Create figure
figure = plt.figure()
ax = plt.subplot(111)
for n1,n2 in rpoints:    
    #First line
    plotcurv(n1,n2,rad_one,200,narrow,0.5)
    #Second line
    plotcurv(n2,n1,rad_one,200,narrow,0.5)
ax.set_xlim(0,20)
ax.set_ylim(0,20)
plt.show

I have tested it with three random couple of points, plotting back and forth lines. Which gives the figure below:

The function allows for the user to set a number of desired arrow-heads, and it places them evenly on the plotted Bezier, making sure the appropriate direction is represented. However, because the Bezier curve is not exactly an 'arc', I heuristically push the start of the arrow-heads to make them look more centered. Any improvement to this solution will be greatly appreciated.



来源:https://stackoverflow.com/questions/35177675/draw-rounded-fancyarrowpatch-with-midpoint-arrow-in-matplotlib

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!