Arrow class, take 2

Aha. I just managed to have the stem drawn. My silly

    > mistake; i thought that to instantiate a Line2D i
    > needed to pass it (x0, y0) and (x1, y1), but it rather
    > expects (x0, x1) and (y0, y1). The arrow looks cool
    > now.

Rather than a line and a polygon, it might be more flexible and
attractive to design the arrow simply as a polygon (you could then
have control of the linewidth, facecolor, and edgewidth, something
like
         
                p0
              / \
             / \
            / \
         p6--p5 p2--p1
             > >
             > >
             > >
             > >
             > >
             p4--p3

    > My remaining problem is the coordinates. It seems that
    > matplotlib is positioning the arrow using pixels as
    > coordinates, from the bottom left corner of the figure
    > window.

    > Is my problem a 'transformation' issue?

Yes. If you derive your class from Artist and add it to the axes with
ax.add_artist (or Patch if you use the polygon approach above and add
it with ax.add_artist), the axes will set the default data
transformation for you, iff and only if you haven't already set the
transform. There are three default transforms you can choose from

  fig.transFigure # 0,0 is lower left of fig and 1,1 is upper right
  ax.transAxes # 0,0 is lower left of axes and 1,1 is upper right
  ax.transData # same coordinates as the data in the axes

You have a additional choices with custom transforms. One approach
would be to set the coordinates of the polygon in points such that the
arrow tip is 0,0 and the width and height are both 1. You could then
use a scaling and rotation affine where sx, sy are the x and y scales,
and theta is the angle. If you apply this affine to the arrow, the
width of the arrow would be sx points, the height sy points, and the
angle would be theta and the sucker would still be pointing at 0,0.
One nice feature of transformations is that the let you combine two
coordinate systems by applying a an offset transformation. In this
case you'd want to apply and offset in data coords and then the arrow
would be pointing at some data location x,y but would still have a
width and height specified in points.

This is basically how the ticks work. An x tick is located at an x
location in data coords, a y location in axes coords (eg 0 for bottom
ticks and 1 for top ticks) and a length in points.

Here's an example. I'm not sure this is the best design. It might be
more useful to specify a point for the base and a point for the arrowhead,
and draw the arrow between them. But I am not sure what the best way
to specify the arrow width if you use that design. In any case, this
will serve as an example you can study to get an idea of how the
transforms work, and you can go from there. It would also be nice to
have some intelligent labeling built it, eg at the arrow base

from pylab import *
from matplotlib.patches import Polygon
from matplotlib.transforms import Affine, Value, zero
import math

class Arrow(Polygon):
    zorder = 4 # these should generally above the things they mark
    def __init__(self, x, y, xytrans, width, height, theta,
                 tipx=2, tipy=0.2):
        """
        Create an arrow pointing at x,y with a base width and total
        height in points

        theta is the arrow rotation - 0 degrees is point up, 90 is
          pointing to the right, 180 is pointing down, 270 is pointing
          left.

        tipx is the tip width and is expressed as fraction of the base width.

        tipy is the tip height expressed as a fraction of the total
          height

        xytrans is the transformation of the x,y coordinate, eg
          ax.transData for data coords and ax.transAxes for axes coords

        """
         
        # p0
        # / \
        # / \
        # / \
        # p6--p5 p2--p1
        # | |
        # | |
        # | |
        # | |
        # | |
        # p4--p3

        p0 = 0,0
        p1 = tipx*0.5, -tipy
        p2 = 0.5, -tipy
        p3 = 0.5, -1
        p4 = -0.5, -1
        p5 = -0.5, -tipy
        p6 = -tipx*0.5, -tipy
        
        verts = p0, p1, p2, p3, p4, p5, p6
        Polygon.__init__(self, verts)

        theta = math.pi*theta/180.
        a = width*math.cos(theta)
        b = -width*math.sin(theta)
        c = height*math.sin(theta)
        d = height*math.cos(theta)
        a,b,c,d = [Value(val) for val in (a,b,c,d)]
        trans = Affine(a, b, c, d, zero(), zero())
        trans.set_offset((x,y), xytrans)
        self.set_transform(trans)
        
plot([0,1,2], [1,2,3], 'bo', ms=15)
axis([0,3, 0, 4])

ax = gca()
arrow = Arrow(1,2, ax.transData, 10, 100, 135)
set(arrow, fc='g', ec='r', lw=1)
ax.add_patch(arrow)

show()

John,

Many thanks for the lengthy and thorough explanations on transformations in matplotlib. I am working my way through them and the source files. I am still trying to get my line+triangle example to work; i think it might be flexible in the end, if we want to, e.g., position the head anywhere along the stem, or use different polygons for the head. Your polygon example is certainly flexible for shaping the arrow, but for instance, if i want to draw an 'oriented path' in 2d space, it will become more complicated.

I can now position the stem correctly and the head 'almost' correctly using offsets. Here is what i have in my Arrow class:

  orig, dest = zip( xdata, ydata )
  self._x = tuple( xdata )
  self._y = tuple( ydata )
  self._center = dest # Temporary
  radius = 4

  # Stem
  self._stem = Line2D( self._x, self._y, **kwargs )
  self._stem.set_transform( ax.transData )

  # Head
  self._head = RegularPolygon( tuple(self._center), 3,

      radius = radius, orientation = angle, **kwargs )
  trans = identity_affine()
  trans.set_offset( tuple( dest ), ax.transData )
  self._head.set_transform( trans )

and the draw() method just says:

  def draw( self, renderer ):
         # Draw stem and head
         self._stem.draw( renderer )
         self._head.draw( renderer )

You instantiate it, for example, with:

  ax = axes( [0.1, 0.1, 0.8, 0.8], polar = polar )
  ax.set_xlim( [0,10] )
  ax.set_ylim( [0,10] )
  arr = ax.arrow( [1, 4], [1, 5] )

to draw an arrow from (1,1) to (4,5).

There are two things:
1) I know the center of the arrow head isn't right; i'll shift it later
2) The arrow is drawn correctly (even on polar axes) but there is a slight gap between the tip of the stem and the bottom of the head; although the center should coincide with the tip (called 'dest' in the code excerpt).

Why isn't the triangle centered where the tip of the stem is?

Dominique