How do I specify an arrow-like linestyle in Matplotlib?
Here's a starting off point:
Walk along your line at fixed steps (
aspace
in my example below) .A. This involves taking steps along the line segments created by two sets of points (
x1
,y1
) and (x2
,y2
).B. If your step is longer than the line segment, shift to the next set of points.
At that point determine the angle of the line.
Draw an arrow with an inclination corresponding to the angle.
I wrote a little script to demonstrate this:
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()
This example results in this figure:
There's plenty of room for improvement here, for starters:
- One can use FancyArrowPatch to customize the look of the arrows.
- One can add a further test when creating the arrows to make sure they don't extend beyond the line. This will be relevant to arrows created at or near a vertex where the line changes direction sharply. This is the case for the right most point above.
- One can make a method from this script that will work across a broader range of cases, ie make it more portable.
While looking into this, I discovered the quiver plotting method. It might be able to replace the above work, but it wasn't immediately obvious that this was guaranteed.
Vectorized version of Yann's answer:
import numpy as np
import matplotlib.pyplot as plt
def distance(data):
return np.sum((data[1:] - data[:-1]) ** 2, axis=1) ** .5
def draw_path(path):
HEAD_WIDTH = 2
HEAD_LEN = 3
fig = plt.figure()
axes = fig.add_subplot(111)
x = path[:,0]
y = path[:,1]
axes.plot(x, y)
theta = np.arctan2(y[1:] - y[:-1], x[1:] - x[:-1])
dist = distance(path) - HEAD_LEN
x = x[:-1]
y = y[:-1]
ax = x + dist * np.sin(theta)
ay = y + dist * np.cos(theta)
for x1, y1, x2, y2 in zip(x,y,ax-x,ay-y):
axes.arrow(x1, y1, x2, y2, head_width=HEAD_WIDTH, head_length=HEAD_LEN)
plt.show()
Very nice answer by Yann, but by using arrow the resulting arrows can be affected by the axes aspect ratio and limits. I have made a version that uses axes.annotate() instead of axes.arrow(). I include it here for others to use.
In short this is used to plot arrows along your lines in matplotlib. The code is shown below. It can still be improved by adding the possibility of having different arrowheads. Here I only included control for the width and length of the arrowhead.
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()
The resulting figure can be seen here: