我想在Matplotlib中以一种指示特定路径的方式显示一组xy数据。理想情况下,linestyle会被修改为使用类似箭头的补丁。我创建了一个模型,如下所示(使用Omnigraphsketcher)。看起来我应该能够覆盖其中一个常见的linestyle
声明('-'
,'--'
,':'
等)。
请注意,我不想简单地用一个箭头连接每个数据点---实际数据点的间距不均匀,我需要一致的箭头间距。
答案 0 :(得分:7)
这是一个起点:
按固定步骤(在我下面的示例中为aspace
)沿着您的线行走。
一个。这涉及沿着由两组点(x1
,y1
)和(x2
,y2
)创建的线段采取步骤。
B中。如果您的步长比线段长,请转到下一组点。
此时确定线的角度。
绘制一个倾斜度与该角度相对应的箭头。
我写了一个小脚本来证明这一点:
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()
此示例导致此图:
对于初学者来说,这里还有很大的改进空间:
在研究这个时,我发现了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()
结果图可以在这里看到:
答案 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()
答案 4 :(得分:0)