[英]matplotlib arrowheads and aspect ratio
如果我运行这个脚本:
import matplotlib.pyplot as plt
import pylab as plab
plt.figure()
plt.plot([0,2], [2,0], color='c', lw=0.5)
plt.plot([1,2], [2,1], color='k', lw=0.5)
plt.arrow(1,1,0.5,0.5, head_width=0.1, width=0.01, head_length=0.1, color='r')
plt.arrow(1.25,0.75,0.5,0.5, head_width=0.1, width=0.01, head_length=0.1, color='g')
plab.axes().set_aspect(0.5)
plt.show()
我明白了: 请注意,箭头的背面与从 (1,2) 延伸到 (2,1) 的黑线齐平。 这是有道理的,但我希望箭头的背面在视觉上垂直于尾部而不改变纵横比。 我该怎么做呢?
我几乎一直对这个问题感到恼火,并且最终使用annotate()
来绘制箭头。 例如(缺少很多调整以得到与你的情节相同的结果......):
import matplotlib.pyplot as plt
import pylab as plab
plt.figure()
ax=plt.subplot(211)
plt.plot([0,2], [2,0], color='c', lw=0.5)
plt.plot([1,2], [2,1], color='k', lw=0.5)
plt.arrow(1,1,0.5,0.5, head_width=0.1, width=0.01, head_length=0.1, color='r')
ax.set_aspect(0.5)
ax=plt.subplot(212)
plt.plot([0,2], [2,0], color='c', lw=0.5)
plt.plot([1,2], [2,1], color='k', lw=0.5)
ax.annotate("",
xy=(1.5, 1.5), xycoords='data',
xytext=(1, 1), textcoords='data',
arrowprops=dict(arrowstyle="-|>",
connectionstyle="arc3"),
)
ax.set_aspect(0.5)
plt.show()
我花了一些时间,但我创建了一个名为 WarpArrow 的非常基本的类来获取 matplotlib 中的箭头,其行为类似于 plt.arrow() 并考虑了纵横比。 请注意,纵横比应该是固定的。 我没有对其进行大量测试,因此请小心使用它,并随意添加更多关键字参数。
基本上它的作用是获取位于 x 轴上的一些虚拟箭头的角点,并按 dx 和 dy 给出的量旋转它们。 旋转考虑了纵横比,你就知道了。 剩下的只是根据输入参数对角点进行微调,填充由角点组成的多边形并绘制一条线到箭头('stem')。
您可以告诉它使用“x”或“y”坐标作为头部长度和头部宽度的测量单位。
提示:在 x 或 y 方向上绘制()箭头,以找到所需的箭头长度和宽度。
用法:
# creating arrow object (default: head_length='x' head_width_unit='y')
# set matplotlib axes and use all other standard arrow kwargs
my_arrow = WarpArrow(axes, x, y, dx, dy, head_length='x' head_width_unit='y')
my_arrow.draw() # draw arrow onto axes
这是要复制的完整代码,请注意包名
import math
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
class WarpArrow:
def __init__(self, axes, x, y, dx, dy, head_width_unit='y', head_length_unit='x', width_unit='y', **kwargs):
# handle inputs
self.axes = axes
self.aspect = self.axes.get_aspect()
if self.aspect == 'auto':
self.aspect = 1
self.x = x
self.y = y
self.dx = dx
self.dy = dy
self.len = math.sqrt(self.dx ** 2 + (self.dy * self.aspect) ** 2)
self.kwargs = kwargs
if self.dx == 0: # computing arctan for rotation angle
self.angle_rad = ((self.dy < 0) + .5) * math.pi
else:
_arctan = np.arctan(self.dy * self.aspect / self.dx)
self.angle_rad = ((self.dx > 0) * (self.dy > 0) + (self.dx > 0) * (self.dy < 0)) * _arctan + ((self.dx < 0) * (self.dy > 0) + (self.dx < 0)*(self.dy <= 0)) * (math.pi + _arctan)
self.angle_deg = 180 * self.angle_rad/math.pi
# units for width and length of arrowhead
self.head_width_unit = head_width_unit
self.head_length_unit = head_length_unit
self.width_unit = width_unit
_err1, _err2 = '', '' # Error managment for units
if self.head_width_unit not in ['x', 'y']:
_err1 = 'head_width_unit'
if self.head_length_unit not in ['x', 'y']:
_err2 = 'head_length_unit'
if _err1 + _err2 != '':
raise ValueError(
'choose \'x\' or \'y\' for \'' + _err1 + (_err1 != '') * (_err2 != '') * '\' and \'' + _err2 + '\'')
if self.width_unit not in ['x', 'y']:
raise ValueError('choose \'x\' or \'y\' for \'width_unit\'')
# setting arrow kwargs arguments (and defaults)
if 'width' in kwargs:
self.width = kwargs['width']
else:
self.width = 0.001
if 'head_width' in kwargs:
self.head_width = kwargs['head_width']
else:
self.head_width = 3*self.width
if 'head_length' in kwargs:
self.head_length= kwargs['head_length']
else:
self.head_length = 3*self.head_width
# convert length and width for computation
if self.width_unit == 'x':
self.norm_head_width = self.head_width * self.aspect
if self.width_unit == 'y':
self.norm_head_width = self.head_width
if self.head_length_unit == 'x':
self.norm_head_length = self.head_length
if self.head_width_unit == 'y':
self.norm_head_width = self.head_width
if self.head_width_unit == 'x':
self.norm_head_width = self.head_width / self.aspect
if self.head_length_unit == 'y':
self.norm_head_length = self.head_length * self.aspect
if 'length_includes_head' in kwargs:
self.length_includes_head = kwargs['length_includes_head']
else:
self.length_includes_head = False
# further arrow kwargs and line plot kwargs
if 'shape' in kwargs: # {'full', 'left', 'right'}
self.shape = kwargs['shape']
else:
self.shape = 'full'
if 'overhang' in kwargs:
self.overhang = kwargs['overhang']
else:
self.overhang = 0
if 'head_starts_at_zero' in kwargs:
self.head_starts_at_zero = kwargs['head_starts_at_zero']
else:
self.head_starts_at_zero = False
if 'c' in kwargs:
self.color = kwargs['c']
else:
self.color = 'black'
if 'color' in kwargs:
self.color = kwargs['color']
if 'linewidth' in kwargs:
self.linewidth = kwargs['linewidth']
if 'linestyle' in kwargs:
self.linestyle = kwargs['linestyle']
else:
self.linestyle = '-'
if 'zorder' in kwargs:
self.zorder = kwargs['zorder']
else:
self.zorder = 2
if 'lw' in kwargs:
self.linewidth = kwargs['lw']
else:
self.linewidth = plt.rcParams['lines.linewidth']
if 'linewidth' in kwargs:
self.linewidth = kwargs['linewidth']
# dummy arrow on x-axis
self.arrow_head_origin = {}
self.arrow_head_origin['tip'] = [self.len + self.norm_head_length * (self.length_includes_head is False), 0]
self.arrow_head_origin['left'] = [self.arrow_head_origin['tip'][0] - self.norm_head_length, .5 * self.norm_head_width * (self.shape != 'right')]
self.arrow_head_origin['left_stem'] = [self.arrow_head_origin['tip'][0] - (1 - self.overhang) * self.norm_head_length, .5 * self.width]
self.arrow_head_origin['stem'] = [self.arrow_head_origin['tip'][0] - (1 - self.overhang) * self.norm_head_length, 0]
self.arrow_head_origin['right_stem'] = [self.arrow_head_origin['tip'][0] - (1 - self.overhang) * self.norm_head_length, - .5 * self.width]
self.arrow_head_origin['right'] = [self.arrow_head_origin['tip'][0] - self.norm_head_length, - .5 * self.norm_head_width * (self.shape != 'left')]
# compute arrow head
self.arrow_head = {}
self.x_out, self.y_out = [], []
for key in self.arrow_head_origin:
new_x, new_y = self.rotate_2d(self.arrow_head_origin[key])
self.arrow_head[key] = [new_x, new_y]
self.x_out.append(new_x)
self.y_out.append(new_y)
# create polygon
self.polygon = Polygon(np.array([[i, j] for i, j in zip(self.x_out, self.y_out)]), True, facecolor=self.color, zorder=self.zorder)
# create plot coordinates for line plot for tail
self.linepoints = [np.array([self.x, self.arrow_head['stem'][0]]), np.array([self.y, self.arrow_head['stem'][1]])]
def draw(self):
self.axes.plot(self.linepoints[0], self.linepoints[1], lw=self.linewidth, c=self.color, linestyle=self.linestyle, zorder=self.zorder)
self.axes.add_patch(self.polygon)
def rotate_2d(self,coordinates):
'''
:param coordinates: [x,y]
:type coordinates: list
:param angle_deg: angle in degrees
:type angle_deg: int, float, double
'''
x = coordinates[0]
y = coordinates[1]
newx = self.x + (x * np.cos(self.angle_deg * math.pi / 180) - y * self.aspect * np.sin(
self.angle_deg * math.pi / 180))
newy = self.y + (y * self.aspect * np.cos(self.angle_deg * math.pi / 180) + x * np.sin(
self.angle_deg * math.pi / 180)) / self.aspect
return newx, newy
例子:
fig, ax = plt.subplots()
ax.set_xlim(0, 18)
ax.set_ylim(0.4, 1.6)
ax.set_aspect(6)
ax.grid()
arrow1 = WarpArrow(ax, 0, 1, 3, 0, head_width=0.04, head_length=1)
arrow1.draw()
arrow2 = WarpArrow(ax, 0, 1.2, 3, 0, head_width=0.04, head_length=1, length_includes_head=True)
arrow2.draw()
arrow3 = WarpArrow(ax, 0, 1.4, 3, 0, head_width=0.04, head_length=1, head_length_unit='x', head_width_unit='y', length_includes_head=True, )
arrow3.draw()
arrow4 = WarpArrow(ax, 4, 1, 3, .4, head_width=.5, head_length=0.2, head_length_unit='y', head_width_unit='x' ,length_includes_head=True, overhang=.5, color='lightskyblue')
arrow4.draw()
arrow5 = WarpArrow(ax, 5, 1, 3, .4, head_width=.5, head_length=0.2, head_length_unit='y', head_width_unit='x', length_includes_head=True, overhang=.5, color='rosybrown')
arrow5.draw()
arrow6 = WarpArrow(ax, 11, 1, -3, .4, head_width=.5/ax.get_aspect(), head_length=0.2*ax.get_aspect(), head_length_unit='x', head_width_unit='y', length_includes_head=True, overhang=.5, color='red', linestyle='--')
arrow6.draw()
plt.show()
输出:箭头示例
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.