Я хочу создать matplotlib график, содержащий стрелки, форма головы которых не зависит от координат данных.Это похоже на FancyArrowPatch
, но когда длина стрелы меньше длины головки, сокращается, чтобы соответствовать длине стрелки.
В настоящее время я решаю эту проблему, устанавливая длину головки стрелки путем преобразованияширина для отображения координат, вычисление длины головки в координатах отображения и преобразование ее обратно в координаты данных.
Этот подход работает хорошо, если размеры осей не меняются, что может произойти из-за set_xlim()
,set_ylim()
или tight_layout()
например.Я хочу охватить эти случаи, перерисовывая стрелку всякий раз, когда размеры графика меняются.В данный момент я справляюсь с этим, регистрируя функцию on_draw(event)
через
axes.get_figure().canvas.mpl_connect("resize_event", on_draw)
, но это работает только для интерактивных бэкэндов.Мне также нужно решение для случаев, когда я сохраняю график в виде файла изображения.Есть ли другое место, где я могу зарегистрировать свою функцию обратного вызова?
РЕДАКТИРОВАТЬ: Вот код, который я сейчас использую:
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()