How to make 'fuller' axis arrows with matplotlib
It seems to be the case that a call to matplotlib.pyplot.arrow (with a fair amount of calibration) can get the required arrows:
plt.arrow(5, -0.003, 0.1, 0, width=0.015, color="k", clip_on=False, head_width=0.12, head_length=0.12)
plt.arrow(0.003, 5, 0, 0.1, width=0.015, color="k", clip_on=False, head_width=0.12, head_length=0.12)
Note the "0.003" offsets for the coordinates, this is because for some reason plt.arrow does not draw the arrow in alignment with the axis. Really? What a pain.
Also of note is clip_on which allows the arrow to extend past the boundaries set for the graph (like plt.xlim(-5, 5)).
This:
from mpl_toolkits.axes_grid.axislines import SubplotZero
from matplotlib.transforms import BlendedGenericTransform
from matplotlib import patches
import matplotlib.pyplot as plt
import numpy
if 1:
fig = plt.figure(1)
ax = SubplotZero(fig, 111)
fig.add_subplot(ax)
ax.axhline(linewidth=1.7, color="k")
ax.axvline(linewidth=1.7, color="k")
plt.xticks([])
plt.yticks([])
ax.text(0, 1.05, r'$y$', transform=BlendedGenericTransform(ax.transData, ax.transAxes), ha='center')
ax.text(1.03, 0, r'$x$', transform=BlendedGenericTransform(ax.transAxes, ax.transData), va='center')
for direction in ["xzero", "yzero"]:
ax.axis[direction].set_visible(True)
for direction in ["left", "right", "bottom", "top"]:
ax.axis[direction].set_visible(False)
x = numpy.linspace(-1.499999999, 5, 10000)
yy = numpy.log(2*x + 3)/2 + 3
ax.plot(x, yy, linewidth=1.2, color="black")
plt.ylim(-2, 5)
plt.xlim(-5, 5)
plt.arrow(5, -0.003, 0.1, 0, width=0.015, color="k", clip_on=False, head_width=0.12, head_length=0.12)
plt.arrow(0.003, 5, 0, 0.1, width=0.015, color="k", clip_on=False, head_width=0.12, head_length=0.12)
plt.text((numpy.e**(-6) - 3)/2, 0, r'$(\frac{1}{2} (e^{-6} - 3), 0)$', position=((numpy.e**(-6) - 3)/2 + 0.1, 0.1))
plt.plot((numpy.e**(-6) - 3)/2, 0, 'ko')
plt.text(0, numpy.log(3)/2 + 3, r'$(0, \frac{1}{2} \log_e{\left (3 \right )} + 3)$', position=(0.1, numpy.log(3)/2 + 3 + 0.1))
plt.plot(0, numpy.log(3)/2 + 3, 'ko')
plt.savefig('AnswersSA1a.png')
produces a graph like so: (ignore the poor axis-intercept labels)
I only put this as an answer because it's the only way I see how to do it. Surely there has to be a better way than manually working out that I need to be offsetting arrows by 0.003. That doesn't feel right.
My solution is essentially the same as nebffa's. I created a minimal example that calculates arrowhead width and length for the y-axis to match the one specified for the x-axis. I hope that this might be helpful to somebody else.
import pylab as pl
fig = pl.figure()
ax = fig.add_subplot(111)
x = pl.arange(-5,5,0.1)
ax.plot(x, x**2-8.8)
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
pl.xticks([]) # labels
pl.yticks([])
ax.xaxis.set_ticks_position('none') # tick markers
ax.yaxis.set_ticks_position('none')
# wider figure for demonstration
fig.set_size_inches(4,2.2)
# 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)
# clip_on = False if only positive x or y values.
pl.savefig('arrow_axis.png', dpi = 300)
Produces:
How do I size them up a little so that they look normal with respect to the width of the axes[?]
You should add "size=2" to set_axisline_style:
ax.axis[direction].set_axisline_style("-|>", size=2)
Also - it is difficult to see here, but the interior of the arrows is blue - how do I change that to black?
This is more difficult. In fact, you have to overwrite mpl_toolkits.axisartist.axisline_style._FancyAxislineStyle.SimpleArrow.__init__ before running your code:
# overwriting
# mpl_toolkits.axisartist.axisline_style._FancyAxislineStyle.SimpleArrow.__init__
# to have a black color in
# mpl_toolkits.axisartist.axisline_style.AxislineStyle.FilledArrow
from matplotlib.transforms import IdentityTransform
from matplotlib.patches import FancyArrowPatch
def new_init(self, axis_artist, line_path, transform,
line_mutation_scale):
self._axis_artist = axis_artist
self._line_transform = transform
self._line_path = line_path
self._line_mutation_scale = line_mutation_scale
FancyArrowPatch.__init__(self,
path=self._line_path,
arrowstyle=self._ARROW_STYLE,
arrow_transmuter=None,
patchA=None,
patchB=None,
shrinkA=0.,
shrinkB=0.,
mutation_scale=line_mutation_scale,
mutation_aspect=None,
transform=IdentityTransform(),
color='black' # this line is new
)
import mpl_toolkits
mpl_toolkits.axisartist.axisline_style._FancyAxislineStyle.SimpleArrow.__init__ = new_init
To save the picture (plt.show() or dpi < 200 will not work properly), use:
plt.savefig('test.png', dpi=200)