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