matplotlib axis arrow tip

后端 未结 5 2077
执念已碎
执念已碎 2020-12-14 18:23

I am trying to set an arrow at the end of a an axis in matplotlib. I don\'t want to remove the spines and replace them with pure arrows because I need their functionalities

相关标签:
5条回答
  • 2020-12-14 18:47

    There is an example showing how to get arrows as axis decorators in the matplotlib documentation using the mpl_toolkits.axisartist toolkit:

    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()
    


    For many cases, the use of the mpl_toolkits.axisartist.axislines module is not desired. In that case one can also easily get arrow heads by using triangles as markers on the top of the spines:

    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()
    

    0 讨论(0)
  • 2020-12-14 18:50

    In order to obtain what you want, Julien's answer is enough after deleting the following section from the arrowed_spines function:

    # 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')
    

    Spines can still be modified after the inclusion of arrows, as you can see here:

    0 讨论(0)
  • 2020-12-14 18:57

    I found the most straightforward solution in matplotlib documentation. Following is an example:

    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()
    
    0 讨论(0)
  • 2020-12-14 18:59

    Here I have combined existing answers from Julien and s3b4s, and made the function more general so that you can specify the axes you wish to modify and the direction of the arrows.

    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://stackoverflow.com/a/33738359/1474448
        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()
    

    Result:

    Outstanding issue: I have attempted to match the linewidth of the existing spines, but for some reason the arrows appear to have a thicker line. Experimenting with this reveals that a spine linewidth of 0.8 matches an arrow linewidth of around 0.3. Not sure why this is - currently you have to set lw=<value> as a manual fix.

    0 讨论(0)
  • 2020-12-14 19:00

    You could remove all spines and expand the arrows to cover the data range (found this code here):

    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()
    

    0 讨论(0)
提交回复
热议问题