Reputation: 4548
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 ...
my implementation is as slight modification of joferkington implementation
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()
The plot result shows a shift in the arrow as the following
It seems that a shift of a point or two is consistent in the starting position and the alignment of the arrow with the spine. I don't know how to fix this problem.
Upvotes: 27
Views: 51750
Reputation: 1
I like Gabriels solution. However the problem remains that the spines do not start at zero. Especially for educational purposes I find it helpful to get the spines to zero. Here ist my slightly improvement for his version incorporating the "zero" and some minor changes.
from matplotlib import pyplot as plt
import numpy as np
def arrowed_spines(
ax,
x_width_fraction=0.02,
x_height_fraction=0.02,
lw=0,
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})
arrow_kwargs.update({"head_starts_at_zero": 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 {"bottom", "top", "left", "right", "zero"}, "Unsupported side"
assert direction in {"up", "down", "left", "right"}, "Unsupported direction"
if direction in {"left", "right"}:
if side in {"left", "right"}:
raise ValueError("Only up/down arrows supported on the left and right")
dy = 0
head_width = hw
head_length = hl
if side == "zero":
y = 0
else:
y = ymin if side == "bottom" else ymax
if direction == "right":
x = xmin
dx = xmax - xmin
else:
x = xmax
dx = xmin - xmax
else:
if side in {"bottom", "up"}:
raise ValueError(
"Only left/right arrows supported on the bottom and up"
)
dx = 0
head_width = yhw
head_length = yhl
if side == "zero":
x = 0
else:
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
)
plt.ylim(ymin, ymax)
plt.xlim(xmin, xmax)
return annots
fig = plt.figure()
ax = fig.add_subplot(111)
x = np.arange(-2.0, 10.0, 0.01)
ax.plot(x, x**2)
fig.set_facecolor("white")
ax.spines["left"].set_position("zero")
ax.spines["right"].set_color("none")
ax.spines["bottom"].set_position("zero")
ax.spines["top"].set_color("none")
ax.set_xticks([])
ax.set_yticks([])
ax.set_xlabel("X")
ax.xaxis.set_label_coords(1.03, 0.06)
ax.set_ylabel("Y", rotation=0)
ax.yaxis.set_label_coords(0.197, 1.03)
annots = arrowed_spines(ax, locations=("zero right", "zero up"))
plt.show()
[Result][1] [1]: https://i.sstatic.net/f5D1jp6t.png
Hopefully it helps someone.
Upvotes: 0
Reputation: 206
There is a solution in the doc here: https://matplotlib.org/3.3.4/gallery/recipes/centered_spines_with_arrows.html, as specified in Sadegh Bakhtiarzadeh's answer.
However, this solution is not satsfying when dispaying the grid, as it moves the edges of the grid accross the axes and the ticks labels.
You can avoid it by replacing:
# 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))
By:
# Set the x and y starting tick to 0.
ax.set_ylim(bottom=0)
ax.set_xlim(0)
Full example:
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()
Upvotes: 0
Reputation: 121
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()
Upvotes: 12
Reputation: 339330
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()
Upvotes: 17
Reputation: 1910
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.
Upvotes: 6
Reputation: 93
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:
Upvotes: 3
Reputation: 15433
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()
Upvotes: 9