I want to create a matplotlib plot containing arrows, whose head's shape is independent from the data coordinates. This is similar to FancyArrowPatch
, but when the arrow length is smaller than the head length is shrank to fit the length of the arrow.
Currently, I solve this by setting the length of the arrow head by transforming the width to display coordinates, calculating the head length in display coordinates and transform it back into data coordinates.
This approach works well as long the axes' dimensions do not change, which can happen due to set_xlim()
, set_ylim()
or tight_layout()
for example.
I want to cover these cases, by redrawing the arrow whenever the plot's dimensions do change. At the moment I handle this by registering a function on_draw(event)
via
axes.get_figure().canvas.mpl_connect("resize_event", on_draw)
but this does only work for interactive backends. I also need a solution for cases, where I save the plot as image file. Is there any other place, where I can register my callback function?
EDIT: Here is the code, I am currently using:
def draw_adaptive_arrow(axes, x, y, dx, dy,
tail_width, head_width, head_ratio, draw_head=True,
shape="full", **kwargs):
from matplotlib.patches import FancyArrow
from matplotlib.transforms import Bbox
arrow = None
def on_draw(event=None):
"""
Callback function that is called, every time the figure is resized
Removes the current arrow and replaces it with an arrow with
recalcualted head
"""
nonlocal tail_width
nonlocal head_width
nonlocal arrow
if arrow is not None:
arrow.remove()
# Create a head that looks equal, independent of the aspect
# ratio
# Hence, a transformation into display coordinates has to be
# performed to fix the head width to length ratio
# In this transformation only the height and width are
# interesting, absolute coordinates are not needed
# -> box origin at (0,0)
arrow_box = Bbox([(0,0),(0,head_width)])
arrow_box_display = axes.transData.transform_bbox(arrow_box)
head_length_display = np.abs(arrow_box_display.height * head_ratio)
arrow_box_display.x1 = arrow_box_display.x0 + head_length_display
# Transfrom back to data coordinates for plotting
arrow_box = axes.transData.inverted().transform_bbox(arrow_box_display)
head_length = arrow_box.width
if head_length > np.abs(dx):
# If the head would be longer than the entire arrow,
# only draw the arrow head with reduced length
head_length = np.abs(dx)
if not draw_head:
head_length = 0
head_width = tail_width
arrow = FancyArrow(
x, y, dx, dy,
width=tail_width, head_width=head_width, head_length=head_length,
length_includes_head=True, **kwargs)
axes.add_patch(arrow)
axes.get_figure().canvas.mpl_connect("resize_event", on_draw)
# Some place in the user code...
fig = plt.figure(figsize=(8.0, 3.0))
ax = fig.add_subplot(1,1,1)
# 90 degree tip
draw_adaptive_arrow(
ax, 0, 0, 4, 0, tail_width=0.4, head_width=0.8, head_ratio=0.5
)
# Still 90 degree tip
draw_adaptive_arrow(
ax, 5, 0, 2, 0, tail_width=0.4, head_width=0.8, head_ratio=0.5
)
# Smaller head, since otherwise head would be longer than entire arrow
draw_adaptive_arrow(
ax, 8, 0, 0.5, 0, tail_width=0.4, head_width=0.8, head_ratio=0.5
)
ax.set_xlim(0,10)
ax.set_ylim(-1,1)
# Does not work in non-interactive backend
plt.savefig("test.pdf")
# But works in interactive backend
plt.show()