matplotlib 子图之间的箭头

Arrows between matplotlib subplots

我决定尝试一下 this 示例代码。我能够弄清楚如何在两个子图之间画一条直线,即使这条线在其中一个子图的边界之外。

import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np

fig = plt.figure(figsize=(10, 5))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
axs = [ax1, ax2]

# Fixing random state for reproducibility
np.random.seed(19680801)

# generate some random test data
all_data = [np.random.normal(0, std, 100) for std in range(6, 10)]

# plot violin plot
axs[0].violinplot(all_data,
                  showmeans=False,
                  showmedians=True)
axs[0].set_title('Violin plot')

# plot box plot
axs[1].boxplot(all_data)
axs[1].set_title('Box plot')

# adding horizontal grid lines
for ax in axs:
    ax.yaxis.grid(True)
    ax.set_xticks([y + 1 for y in range(len(all_data))])
    ax.set_xlabel('Four separate samples')
    ax.set_ylabel('Observed values')

for tick in ax.xaxis.get_major_ticks():
    tick.label.set_fontsize(20)
plt.setp(axs[0], xticklabels=['x1', 'x2', 'x3', 'x4'])

transFigure = fig.transFigure.inverted()
coord1 = transFigure.transform(ax1.transData.transform([5,10]))
coord2 = transFigure.transform(ax2.transData.transform([2,-10]))
line = mpl.lines.Line2D((coord1[0],coord2[0]),(coord1[1],coord2[1]),
                        c='k', lw=5, transform=fig.transFigure)
fig.lines.append(line)

是的,添加的那行很难看,但我只是想让它发挥作用。

但是,我真正想做的是在子图之间制作一个箭头,如果没有 jury-rigging 我自己的箭头尾巴,我无法弄清楚。有没有办法使用 matplotlib.pyplot.arrow class?

我也想在两个子图之间画一个箭头,但我什至不知道从哪里开始!然而,原始问题中子图示例之间的界线给了我足够的线索开始......

首先,我将原始问题中的代码简化为一个最小的工作示例:

from matplotlib import lines, pyplot as plt

fig = plt.figure()

# First subplot
ax1 = fig.add_subplot(121)
plt.plot([0, 1], [0, 1])

# Second subplot
ax2 = fig.add_subplot(122)
plt.plot([0, 1], [0, 1])

# Add line from one subplot to the other
xyA = [0.5, 1.0]
ax1.plot(*xyA, "o")
xyB = [0.75, 0.25]
ax2.plot(*xyB, "o")
transFigure = fig.transFigure.inverted()
coord1 = transFigure.transform(ax1.transData.transform(xyA))
coord2 = transFigure.transform(ax2.transData.transform(xyB))
line = lines.Line2D(
    (coord1[0], coord2[0]),  # xdata
    (coord1[1], coord2[1]),  # ydata
    transform=fig.transFigure,
    color="black",
)
fig.lines.append(line)

# Show figure
plt.show()

这会产生以下输出:

然后,使用 this blog post, I thought the answer was to create a matplotlib.patches.FancyArrowPatch and append it to fig.patches (instead of creating a matplotlib.lines.Line2D and appending it to fig.lines). After consulting the matplotlib.patches.FancyArrowPatch documentation,加上一些试验和错误,我想出了一些在 matplotlib 3.1.2 中有效的东西:

from matplotlib import patches, pyplot as plt

fig = plt.figure()

# First subplot
ax1 = fig.add_subplot(121)
plt.plot([0, 1], [0, 1])

# Second subplot
ax2 = fig.add_subplot(122)
plt.plot([0, 1], [0, 1])

# Add line from one subplot to the other
xyA = [0.5, 1.0]
ax1.plot(*xyA, "o")
xyB = [0.75, 0.25]
ax2.plot(*xyB, "o")
transFigure = fig.transFigure.inverted()
coord1 = transFigure.transform(ax1.transData.transform(xyA))
coord2 = transFigure.transform(ax2.transData.transform(xyB))
arrow = patches.FancyArrowPatch(
    coord1,  # posA
    coord2,  # posB
    shrinkA=0,  # so tail is exactly on posA (default shrink is 2)
    shrinkB=0,  # so head is exactly on posB (default shrink is 2)
    transform=fig.transFigure,
    color="black",
    arrowstyle="-|>",  # "normal" arrow
    mutation_scale=30,  # controls arrow head size
    linewidth=3,
)
fig.patches.append(arrow)

# Show figure
plt.show()

但是,根据下面的评论,这在 matplotlib 3.4.2 中不起作用,你会得到这个:

请注意,箭头的末端没有与目标点(橙色圆圈)对齐,而它们应该对齐。

matplotlib 版本更改也导致原始行示例以同样的方式失败。

不过,还有更好的补丁!使用 ConnectionPatch (docs), which is a subclass of FancyArrowPatch, instead of using FancyArrowPatch directly as the ConnectionPatch is designed specifically for this use case and deals with the transform more correctly, as shown in this matplotlib documentation example:

fig = plt.figure()

# First subplot
ax1 = fig.add_subplot(121)
plt.plot([0, 1], [0, 1]) 

# Second subplot
ax2 = fig.add_subplot(122)
plt.plot([0, 1], [0, 1]) 

# Add line from one subplot to the other
xyA = [0.5, 1.0]
ax1.plot(*xyA, "o")
xyB = [0.75, 0.25]
ax2.plot(*xyB, "o")
# ConnectionPatch handles the transform internally so no need to get fig.transFigure
arrow = patches.ConnectionPatch(
    xyA,
    xyB,
    coordsA=ax1.transData,
    coordsB=ax2.transData,
    # Default shrink parameter is 0 so can be omitted
    color="black",
    arrowstyle="-|>",  # "normal" arrow
    mutation_scale=30,  # controls arrow head size
    linewidth=3,
)
fig.patches.append(arrow)

# Show figure
plt.show()

这会在 matplotlib 3.1.2matplotlib 3.4.2 中产生正确的输出,如下所示:

要在 matplotlib 3.4.2 中绘制一条连接两个子图的正确位置的线,请像上面那样使用 ConnectionPatch,但要使用 arrowstyle="-"(即没有箭头,所以只有一条线)。

注意:您不能使用:

  • plt.arrow 因为它会自动添加到当前轴,所以只出现在一个子图中

  • matplotlib.patches.Arrow 因为坐标轴图形变换使箭头倾斜

  • matplotlib.patches.FancyArrow 因为这也会导致箭头倾斜