79773241

Date: 2025-09-24 03:45:45
Score: 1.5
Natty:
Report link

Splitting the tail from the arrow and controlling the curve via points yields better results.

Matplotlib's FancyArrowPatch implementation is interface-oriented rather than coordinate-based (as seen in the source code, it selects rendering positions for quadratic Bézier curve drawing). By imitating its approach, quadratic Bézier curves can be drawn at the coordinate level.

For the arrowhead, I referenced the approach from https://github.com/matplotlib/matplotlib/issues/17284, which offers greater control. I selected the -|> arrow style over Simple because it provides better visual results when line widths are small.

My blog post discusses this topic in greater detail: https://omnisyr.github.io/post/%3B%3B%3BeNon-Solid%20Curves%20with%20Arrows%20in%20Matplotlib%3B%3B%3Be%3B%3B%3BcMatplotlib-zhong-dai-jian-tou-de-fei-shi-qu-xian-%3B%3B%3Bc.html

import math

import matplotlib.patches as patches
import matplotlib.pyplot as plt
import matplotlib.transforms as transforms
import numpy as np

def add_arrow(from_, to_, rad=0.3, control_=None, color='#515151',
              line='--', head_length=0.6, size=0.04, detail=False):
    """
    :param from_:Starting point
    :param to_:Target endpoint
    :param rad:Curve radius
    :param control_:Control points;
                    if None, calculated based on the curvature of the curve.
    :param color:Drawing colors
    :param line:Curve Style
    :param head_length:Arrow size
    :param size:Mask Size
    :param detail:Select whether to draw details,
                  and manually adjust the mask size by drawing details.
    """
    if control_ is None:
        x1, y1 = np.array(from_)
        x2, y2 = np.array(to_)
        dx, dy = x2 - x1, y2 - y1
        x12, y12 = (x1 + x2) / 2., (y1 + y2) / 2.
        cx, cy = x12 + rad * dy, y12 - rad * dx
        control_ = (cx, cy)
    vertices = [from_, control_, to_]
    codes = [patches.Path.MOVETO, patches.Path.CURVE3, patches.Path.CURVE3]
    path = patches.Path(vertices, codes)
    patch = patches.PathPatch(path, facecolor='none', edgecolor=color,
                              linestyle=line, linewidth=1, zorder=-2)
    ax.add_patch(patch)
    direction = -1 if control_[0] < to_[0] else 1
    mask_c = 'red' if detail else 'white'
    mask = patches.FancyBboxPatch(
        (to_[0], to_[1] - size / 2), direction * size, size, boxstyle="square, pad=0",
        ec=mask_c, fc=mask_c, zorder=-1, linewidth=1)
    de = math.degrees(math.atan((control_[1] - to_[1]) / (control_[0] - to_[0])))
    tf = transforms.Affine2D().rotate_deg_around(to_[0], to_[1], de) + ax.transData
    mask.set_transform(tf)
    ax.add_patch(mask)
    ax.annotate("", to_, xytext=control_, arrowprops=dict(
        linewidth=0,
        arrowstyle="-|>, head_width=%f, head_length=%f" % (head_length / 2, head_length),
        shrinkA=0, shrinkB=0, facecolor=color, linestyle="solid", mutation_scale=10
    ))
    if not detail:
        return
    ax.scatter(control_[0], control_[1], c=color, marker='x', s=12, linewidths=0.8)
    ax.scatter(from_[0], from_[1], c=color, marker='x', s=12, linewidths=0.8)
    ax.scatter(to_[0], to_[1], c=color, marker='x', s=12, linewidths=0.8)
    ax.plot((control_[0], from_[0]), (control_[1], from_[1]),
            color=color, linewidth=.5, linestyle=':')
    ax.plot((control_[0], to_[0]), (control_[1], to_[1]),
            color=color, linewidth=.5, linestyle=':')

def draw_eg():
    debug = False
    add_arrow((0.1, 0.1), (1.5, 1.6), rad=0.1, color=colors[1], detail=debug)
    add_arrow((0.4, 1.7), (1.4, 0.7), rad=0.8, color=colors[2], detail=debug)
    add_arrow((1.9, 1.9), (1.2, 0.1), rad=-0.2, color=colors[3], detail=debug)
    add_arrow((1.7, 0.7), (0.3, 1.8), rad=0.5, color=colors[4], detail=debug)
    plt.savefig('arrow_example%s.png' % ('_detail' if debug else ''), dpi=600)

fig, ax = plt.subplots(1, 1, figsize=(4, 4))
ax.set_xlim(0, 2)
ax.set_ylim(0, 2)
colors = ['#515151', '#CC9900', '#B177DE', '#37AD6B', '#1A6FDF']
draw_eg()

enter image description here

Reasons:
  • Blacklisted phrase (1): enter image description here
  • Contains signature (1):
  • Long answer (-1):
  • Has code block (-0.5):
  • Low reputation (1):
Posted by: OmnisyR