Matplotlib轴箭头尖端

26

我正在尝试在Matplotlib中的坐标轴末尾设置一个箭头。 我不想删除脊柱并用纯箭头替换它们,因为我需要它们的功能...

我的实现是对joferkington实现的轻微修改

import matplotlib.pyplot as plt
import numpy as np

def arrowed_spines(ax=None, arrowLength=30, labels=('X', 'Y'), arrowStyle='<|-'):
    xlabel, ylabel = labels

    for i, spine in enumerate(['left', 'bottom']):
        # Set up the annotation parameters
        t = ax.spines[spine].get_transform()
        xy, xycoords = [1, 0], ('axes fraction', t)
        xytext, textcoords = [arrowLength, 0], ('offset points', t)
        
        # create arrowprops
        arrowprops = dict( arrowstyle=arrowStyle,
                           facecolor=ax.spines[spine].get_facecolor(), 
                           linewidth=ax.spines[spine].get_linewidth(),
                           alpha = ax.spines[spine].get_alpha(),
                           zorder=ax.spines[spine].get_zorder(),
                           linestyle = ax.spines[spine].get_linestyle() )
    
        if spine is 'bottom':
            ha, va = 'left', 'center'
            xarrow = ax.annotate(xlabel, xy, xycoords=xycoords, xytext=xytext, 
                        textcoords=textcoords, ha=ha, va='center',
                        arrowprops=arrowprops)
        else:
            ha, va = 'center', 'bottom'
            yarrow = ax.annotate(ylabel, xy[::-1], xycoords=xycoords[::-1], 
                        xytext=xytext[::-1], textcoords=textcoords[::-1], 
                        ha='center', va=va, arrowprops=arrowprops)
    return xarrow, yarrow

# plot
x = np.arange(-2., 10.0, 0.01)
plt.plot(x, x**2)
plt.gcf().set_facecolor('white')    
ax = plt.gca()
ax.set_xticks([]) 
ax.set_yticks([]) 

ax.spines['left'].set_position('center')
ax.spines['right'].set_color('none')
ax.spines['bottom'].set_position('center')
ax.spines['top'].set_color('none')

arrowed_spines(ax)

plt.show()

图形结果显示箭头存在偏移,如下enter image description here

似乎起始位置和箭头与脊柱的对齐存在一到两个点的偏移。我不知道如何修复这个问题。


1
可能应该包含对源Gist的引用:https://gist.github.com/joferkington/3845684 - tacaswell
2
我建议使用 ax.spines['left'].set_position('zero') 而不是 ax.spines['left'].set_position('center'),这样坐标轴将在原点相交。 - charmoniumQ
这是一个快速的hack解决方法:ax.spines['bottom'].set_position(('data', 0.001)) - bytesinflight
6个回答

14

在matplotlib文档中有一个示例,演示了如何使用mpl_toolkits.axisartist工具包获取箭头作为轴修饰符:

from mpl_toolkits.axisartist.axislines import SubplotZero
import matplotlib.pyplot as plt
import numpy as np


fig = plt.figure()
ax = SubplotZero(fig, 111)
fig.add_subplot(ax)

for direction in ["xzero", "yzero"]:
    # adds arrows at the ends of each axis
    ax.axis[direction].set_axisline_style("-|>")

    # adds X and Y-axis from the origin
    ax.axis[direction].set_visible(True)

for direction in ["left", "right", "bottom", "top"]:
    # hides borders
    ax.axis[direction].set_visible(False)

x = np.linspace(-0.5, 1., 100)
ax.plot(x, np.sin(x*np.pi))

plt.show()

输入图像描述


对于许多情况,不希望使用 mpl_toolkits.axisartist.axislines 模块。在这种情况下,可以通过在脊柱顶部使用三角形标记来轻松获取箭头头:

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-np.pi, np.pi, 100)
y = 2 * np.sin(x)

rc = {"xtick.direction" : "inout", "ytick.direction" : "inout",
      "xtick.major.size" : 5, "ytick.major.size" : 5,}
with plt.rc_context(rc):
    fig, ax = plt.subplots()
    ax.plot(x, y)

    ax.spines['left'].set_position('zero')
    ax.spines['right'].set_visible(False)
    ax.spines['bottom'].set_position('zero')
    ax.spines['top'].set_visible(False)
    ax.xaxis.set_ticks_position('bottom')
    ax.yaxis.set_ticks_position('left')

    # make arrows
    ax.plot((1), (0), ls="", marker=">", ms=10, color="k",
            transform=ax.get_yaxis_transform(), clip_on=False)
    ax.plot((0), (1), ls="", marker="^", ms=10, color="k",
            transform=ax.get_xaxis_transform(), clip_on=False)

    plt.show()

在此输入图片描述


除非你想用错误的方法并且在重新缩放图表时遇到问题,否则这是正确的方法。 - xuva

9

您可以删除所有的刻度线,然后扩展箭头以覆盖数据范围(在这里找到了这段代码:链接):

import matplotlib.pyplot as plt
import numpy as np

def arrowed_spines(fig, ax):

    xmin, xmax = ax.get_xlim() 
    ymin, ymax = ax.get_ylim()

    # removing the default axis on all sides:
    for side in ['bottom','right','top','left']:
        ax.spines[side].set_visible(False)

    # removing the axis ticks
    plt.xticks([]) # labels 
    plt.yticks([])
    ax.xaxis.set_ticks_position('none') # tick markers
    ax.yaxis.set_ticks_position('none')

    # get width and height of axes object to compute 
    # matching arrowhead length and width
    dps = fig.dpi_scale_trans.inverted()
    bbox = ax.get_window_extent().transformed(dps)
    width, height = bbox.width, bbox.height

    # manual arrowhead width and length
    hw = 1./20.*(ymax-ymin) 
    hl = 1./20.*(xmax-xmin)
    lw = 1. # axis line width
    ohg = 0.3 # arrow overhang

    # compute matching arrowhead length and width
    yhw = hw/(ymax-ymin)*(xmax-xmin)* height/width 
    yhl = hl/(xmax-xmin)*(ymax-ymin)* width/height

    # draw x and y axis
    ax.arrow(xmin, 0, xmax-xmin, 0., fc='k', ec='k', lw = lw, 
             head_width=hw, head_length=hl, overhang = ohg, 
             length_includes_head= True, clip_on = False) 

    ax.arrow(0, ymin, 0., ymax-ymin, fc='k', ec='k', lw = lw, 
             head_width=yhw, head_length=yhl, overhang = ohg, 
             length_includes_head= True, clip_on = False)


# plot
x = np.arange(-2., 10.0, 0.01)
plt.plot(x, x**2)
fig = plt.gcf()
fig.set_facecolor('white') 
ax = plt.gca()

arrowed_spines(fig, ax)

plt.show()

And this code gives ...


我不想删除脊柱,因为我仍然需要网格、刻度和脊柱的所有其他功能。我的代码是我需要完成的事情的简化示例。 - Cobry
好的。然后,我建议您在问题中告诉我们您实际需要做什么。 - Julien Spronck
3
我需要在保留脊柱及其所有功能的同时,能够添加箭头。 - Cobry

9

我在Matplotlib文档中找到了最简单的解决方案。以下是一个示例:

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
# Move the left and bottom spines to x = 0 and y = 0, respectively.
ax.spines["left"].set_position(("data", 0))
ax.spines["bottom"].set_position(("data", 0))
# Hide the top and right spines.
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)

# Draw arrows (as black triangles: ">k"/"^k") at the end of the axes.  In each
# case, one of the coordinates (0) is a data coordinate (i.e., y = 0 or x = 0,
# respectively) and the other one (1) is an axes coordinate (i.e., at the very
# right/top of the axes).  Also, disable clipping (clip_on=False) as the marker
# actually spills out of the axes.
ax.plot(1, 0, ">k", transform=ax.get_yaxis_transform(), clip_on=False)
ax.plot(0, 1, "^k", transform=ax.get_xaxis_transform(), clip_on=False)

# Some sample data.
x = np.linspace(-0.5, 1., 100)
ax.plot(x, np.sin(x*np.pi))

plt.show()

6

在这里,我结合了Julien和s3b4s的现有答案,并使函数更加通用,以便您可以指定要修改的轴和箭头的方向。

from matplotlib import pyplot as plt
import numpy as np

def arrowed_spines(
        ax,
        x_width_fraction=0.05,
        x_height_fraction=0.05,
        lw=None,
        ohg=0.3,
        locations=('bottom right', 'left up'),
        **arrow_kwargs
):
    """
    Add arrows to the requested spines
    Code originally sourced here: https://3diagramsperpage.wordpress.com/2014/05/25/arrowheads-for-axis-in-matplotlib/
    And interpreted here by @Julien Spronck: https://dev59.com/FFsX5IYBdhLWcg3wf_r2#33738359
    Then corrected and adapted by me for more general applications.
    :param ax: The axis being modified
    :param x_{height,width}_fraction: The fraction of the **x** axis range used for the arrow height and width
    :param lw: Linewidth. If not supplied, default behaviour is to use the value on the current left spine.
    :param ohg: Overhang fraction for the arrow.
    :param locations: Iterable of strings, each of which has the format "<spine> <direction>". These must be orthogonal
    (e.g. "left left" will result in an error). Can specify as many valid strings as required.
    :param arrow_kwargs: Passed to ax.arrow()
    :return: Dictionary of FancyArrow objects, keyed by the location strings.
    """
    # set/override some default plotting parameters if required
    arrow_kwargs.setdefault('overhang', ohg)
    arrow_kwargs.setdefault('clip_on', False)
    arrow_kwargs.update({'length_includes_head': True})

    # axis line width
    if lw is None:
        # FIXME: does this still work if the left spine has been deleted?
        lw = ax.spines['left'].get_linewidth()

    annots = {}

    xmin, xmax = ax.get_xlim()
    ymin, ymax = ax.get_ylim()

    # get width and height of axes object to compute
    # matching arrowhead length and width
    fig = ax.get_figure()
    dps = fig.dpi_scale_trans.inverted()
    bbox = ax.get_window_extent().transformed(dps)
    width, height = bbox.width, bbox.height

    # manual arrowhead width and length
    hw = x_width_fraction * (ymax-ymin)
    hl = x_height_fraction * (xmax-xmin)

    # compute matching arrowhead length and width
    yhw = hw/(ymax-ymin)*(xmax-xmin)* height/width
    yhl = hl/(xmax-xmin)*(ymax-ymin)* width/height

    # draw x and y axis
    for loc_str in locations:
        side, direction = loc_str.split(' ')
        assert side in {'top', 'bottom', 'left', 'right'}, "Unsupported side"
        assert direction in {'up', 'down', 'left', 'right'}, "Unsupported direction"

        if side in {'bottom', 'top'}:
            if direction in {'up', 'down'}:
                raise ValueError("Only left/right arrows supported on the bottom and top")

            dy = 0
            head_width = hw
            head_length = hl

            y = ymin if side == 'bottom' else ymax

            if direction == 'right':
                x = xmin
                dx = xmax - xmin
            else:
                x = xmax
                dx = xmin - xmax

        else:
            if direction in {'left', 'right'}:
                raise ValueError("Only up/downarrows supported on the left and right")
            dx = 0
            head_width = yhw
            head_length = yhl

            x = xmin if side == 'left' else xmax

            if direction == 'up':
                y = ymin
                dy = ymax - ymin
            else:
                y = ymax
                dy = ymin - ymax


        annots[loc_str] = ax.arrow(x, y, dx, dy, fc='k', ec='k', lw = lw,
                 head_width=head_width, head_length=head_length, **arrow_kwargs)

    return annots


fig = plt.figure()
ax = fig.add_subplot(111)
x = np.arange(-2., 10.0, 0.01)
ax.plot(x, x**2)
fig.set_facecolor('white')

annots = arrowed_spines(ax, locations=('bottom right', 'bottom left', 'left up', 'right down'))

plt.show()

结果:

为坐标轴添加箭头

未解决的问题:我尝试匹配现有脊柱的线宽,但是由于某种原因,箭头似乎具有更粗的线条。进行实验表明,0.8的脊柱线宽大约匹配0.3的箭头线宽。目前不清楚原因-您需要手动设置lw=<value>来进行修复。


3
为了获得您想要的内容,可以从arrowed_spines函数中删除以下部分,并使用Julien的答案即可:
# removing the default axis on all sides:
for side in ['bottom','right','top','left']:
    ax.spines[side].set_visible(False)

# removing the axis ticks
plt.xticks([]) # labels 
plt.yticks([])
ax.xaxis.set_ticks_position('none') # tick markers
ax.yaxis.set_ticks_position('none')

在包含箭头之后,脊柱仍然可以被修改,如下图所示:

enter image description here


你可能想要将顶部和右侧的脊柱去掉。 - Mad Physicist

0

这里文档中有一个解决方案:https://matplotlib.org/3.3.4/gallery/recipes/centered_spines_with_arrows.html,正如Sadegh Bakhtiarzadeh的回答所指出的。

然而,当显示网格时,这个解决方案并不令人满意,因为它会将网格的边缘移动到坐标轴和刻度标签上。

你可以通过替换以下内容来避免这种情况:

# Move the left and bottom spines to x = 0 and y = 0, respectively.
ax.spines["left"].set_position(("data", 0))
ax.spines["bottom"].set_position(("data", 0))

通过:

# Set the x and y starting tick to 0.
ax.set_ylim(bottom=0)
ax.set_xlim(0)

完整示例:

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
# Set the x and y starting tick to 0.
ax.set_ylim(bottom=0)
ax.set_xlim(0)
# Hide the top and right spines.
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)

# Draw arrows (as black triangles: ">k"/"^k") at the end of the axes.  In each
# case, one of the coordinates (0) is a data coordinate (i.e., y = 0 or x = 0,
# respectively) and the other one (1) is an axes coordinate (i.e., at the very
# right/top of the axes).  Also, disable clipping (clip_on=False) as the marker
# actually spills out of the axes.
ax.plot(1, 0, ">k", transform=ax.get_yaxis_transform(), clip_on=False)
ax.plot(0, 1, "^k", transform=ax.get_xaxis_transform(), clip_on=False)

# Some sample data.
x = np.linspace(-0.5, 1., 100)
ax.plot(x, np.sin(x*np.pi))

plt.show()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接