I'm trying to get some "pretty" arrows for graphs and other uses in Sage. One of the problems we've been having with the FancyArrow and YAArrow is that the arrow is skewed when the aspect ratio is not 1:1 and it is scaled along with the plot. I've written the attached ArrowLine class which basically modifies the marker drawing code to draw an arrowhead at the end of a Line2D. It doesn't suffer either of these problems; it works beautifully.
However, in drawing (vertex and line) graphs, we have another problem. The vertices of the graph are drawn using scatterplot, and I know the corresponding vertex size (in whatever units scatterplot uses). I'd like to draw an arrow between the boundaries of the vertices. Is there a way to shorten a line that originally goes between the centers of two circles so that the line instead goes between the two boundaries of the circles? Note that clipping the line isn't an option since I want to keep the arrowhead on the line instead of clipping it off. I presume this shortening will have to be done in the drawing routine since it needs to be independent of zooming since the circles are drawn the same independent of zooming.
Another related issue is that width of the path used to draw the arrowhead makes the arrow tip go beyond the endpoint; is there a way to shorten a line by a certain number of points so that we can account for that? Also, in drawing the arrowhead, the line pokes through the arrowhead; I'd like to shorten the shaft to the beginning of the arrowhead.
I think all three of these shortening questions are similar; I'd like to shorten an arrow in a scale-independent way (i.e., by a certain number of points or something).
The code I have for the ArrowLine class is below. If people are interested, I could (eventually, as I have time) incorporate this functionality into the Line2D class (i.e., putting arrowheads on the ends of lines).
r"""
A matplotlib subclass to draw an arrowhead on a line.
AUTHORS:
-- Jason Grout (2008-08-19): initial version
"""
···
############################################################################
# Copyright (C) 2008 Jason Grout <jason-sage@...2130...>
# Released under the terms of the modified BSD License
############################################################################
import matplotlib
from matplotlib.path import Path
from matplotlib.lines import Line2D
import math
import matplotlib.cbook
class ArrowLine(Line2D):
"""
A matplotlib subclass to draw an arrowhead on a line.
EXAMPLE:
sage: import pylab
sage: fig = pylab.figure()
sage: ax = fig.add_subplot(111, autoscale_on=False)
sage: t = [-1,2]
sage: s = [0,-1]
sage: line = ArrowLine(t, s, color='b', ls='-', lw=2, arrow='>', arrowsize=20)
sage: ax.add_line(line)
sage: ax.set_xlim(-3,3)
(-3, 3)
sage: ax.set_ylim(-3,3)
(-3, 3)
sage: pylab.show()
"""
arrows = {'>' : '_draw_triangle_arrow'}
def __init__(self, *args, **kwargs):
"""Initialize the line and arrow."""
self._arrow = kwargs.pop('arrow', None)
self._arrowsize = kwargs.pop('arrowsize', 2*4)
self._arrowedgecolor = kwargs.pop('arrowedgecolor', 'b')
self._arrowfacecolor = kwargs.pop('arrowfacecolor', 'b')
self._arrowedgewidth = kwargs.pop('arrowedgewidth', 4)
self._arrowheadwidth = kwargs.pop('arrowheadwidth', self._arrowsize)
self._arrowheadlength = kwargs.pop('arrowheadlength', self._arrowsize)
Line2D.__init__(self, *args, **kwargs)
def draw(self, renderer):
"""Draw the line and arrowhead using the passed renderer."""
if self._invalid:
self.recache()
renderer.open_group('arrowline2d')
if not self._visible: return
Line2D.draw(self, renderer)
if self._arrow is not None:
gc = renderer.new_gc()
self._set_gc_clip(gc)
gc.set_foreground(self._arrowedgecolor)
gc.set_linewidth(self._arrowedgewidth)
gc.set_alpha(self._alpha)
funcname = self.arrows.get(self._arrow, '_draw_nothing')
if funcname != '_draw_nothing':
tpath, affine = self._transformed_path.get_transformed_points_and_affine()
arrowFunc = getattr(self, funcname)
arrowFunc(renderer, gc, tpath, affine.frozen())
renderer.close_group('arrowline2d')
_arrow_path = Path([[0.0, 0.0], [-1.0, 1.0], [-1.0, -1.0], [0.0, 0.0]], codes=[Path.MOVETO, Path.LINETO,Path.LINETO, Path.CLOSEPOLY])
def _draw_triangle_arrow(self, renderer, gc, path, path_trans):
"""Draw a triangular arrow."""
segment = [i[0] for i in path.iter_segments()][-2:]
startx,starty = path_trans.transform_point(segment[0])
endx,endy = path_trans.transform_point(segment[1])
angle = math.atan2(endy-starty, endx-startx)
halfwidth = 0.5*renderer.points_to_pixels(self._arrowheadwidth)
length = renderer.points_to_pixels(self._arrowheadlength)
transform = matplotlib.transforms.Affine2D().scale(length,halfwidth).rotate(angle).translate(endx,endy)
rgbFace = self._get_rgb_arrowface()
renderer.draw_path(gc, self._arrow_path, transform, rgbFace)
def _get_rgb_arrowface(self):
facecolor = self._arrowfacecolor
if matplotlib.cbook.is_string_like(facecolor) and facecolor.lower()=='none':
rgbFace = None
else:
rgbFace = matplotlib.colors.colorConverter.to_rgb(facecolor)
return rgbFace