Arrows using Line2D and shortening lines

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

Jason Grout wrote:

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?

For this problem, what you want is to fill the arrowhead
without stroking it.

Cheers,
Alan Isaac

Alan G Isaac wrote:

Jason Grout wrote:

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?

For this problem, what you want is to fill the arrowhead
without stroking it.

Brilliant! Thanks for the suggestion.

The other problem is a more serious problem for me: how do I shorten the line so that it goes between the boundaries of the circle instead of the centers, especially when the circles are constructed in a scatter plot.

If I knew how big the circles were in plot coordinates, it wouldn't be a problem. But the circle size isn't specified in plot coordinates, but in scale-independent coordinates, I believe.

Is there a way I could somehow compute the intersections of the paths? Maybe while I draw the line, I could also construct a circle of the right size at the endpoint, ask for the intersection, and shorten my line to go there?

Thanks again!

Jason

Jason Grout wrote:

The other problem is a more serious problem for me: how do I shorten the line so that it goes between the boundaries of the circle instead of the centers, especially when the circles are constructed in a scatter plot.

Some years back I briefly tried to think about arrows and
I found it trickier than expected. Note that some famous
software clearly does arrows wrong. (E.g., gnuplot, at
least last I checked.)

Example: you have decided that you want to draw to the edge
of a point, but a) is that right and b) can it be reasonably
implemented?

a) One might well argue in many applications that the arrow
tip should go to the center of the circle.

b) I'm not sure.

But surely somebody out there will offer some great clues.
Perhaps along the line of graphviz:

Really this is not an answer to your questions ...

Cheers,
Alan Isaac

Alan G Isaac wrote:

Jason Grout wrote:

The other problem is a more serious problem for me: how do I shorten the line so that it goes between the boundaries of the circle instead of the centers, especially when the circles are constructed in a scatter plot.

Some years back I briefly tried to think about arrows and
I found it trickier than expected. Note that some famous
software clearly does arrows wrong. (E.g., gnuplot, at
least last I checked.)

Example: you have decided that you want to draw to the edge
of a point, but a) is that right and b) can it be reasonably
implemented?

a) One might well argue in many applications that the arrow
tip should go to the center of the circle.

I'm sure there are lots of applications where this is true. My specific problem domain is drawing graphs, exactly as graphviz. Often there is a label inside the circle. Drawing to the edge of the circle is the standard way of doing it, so a) is yes, it is the right thing to do.

Things like flowcharts are another example of wanting to do this.

b) I'm not sure.

Thanks; at least this is validating the amount of time I already spent thinking about this and trying to get it to work.

But surely somebody out there will offer some great clues.
Perhaps along the line of graphviz:
Finite Automaton | Graphviz

Really this is not an answer to your questions ...

Thanks for your comments. I think I'll post up another post asking some more specific questions, along the lines of what I think might work if I can figure out what matplotlib is thinking.

Jason

Hi Jason,

I did made a similar class sometime ago and I'm attaching it just in
case. I guess it is very similar to yours but I rely on
matplolib.patches.FancyArrow class to draw the arrow head.

The circle drawn by scatter() command should be a circle with size s
(the third argument of the scatter command) in points . It seems that
it is implemented as a unit circle centered at (0,0) with a transform
corresponding to the size s (and offset). So you may try something
like below to calculate the size of the circle in data coord.

  ax = gca()
  p = scatter([0],[0], 500.)
  tr = p.get_transforms()[0] + ax.transData.inverted()
  x1, y1 = tr.transform_point([0,0])
  x2, y2 = tr.transform_point([1,0])
  r = abs(x2 - x1)

p is a collection object and p.get_transforms() is a list of transforms.
Note that a circle in the canvas coordinate(?) can be an ellipse in
data coordinates. So, I guess you'd better do things in the canvas
coordinates.

For shortening your path, if you're only concerned with a straight
line, it should be straight forward. But I guess it would a bit tricky
to do this for general bezier curves (as in the example that Alan
linked). I think (but I may be wrong) there is no universal algorithm
to find the "all" intersecting points of two bezier curves. There may
be one for between a bezier curve and a circle. And in this case where
one point is inside the circle and the other is outside, one simple
way I can think of is to recursively bisect the bezier curve (similar
to the bisect root finding).

Regards,

-JJ

arrow.py (2.85 KB)

···

On Fri, Aug 22, 2008 at 12:15 PM, Alan G Isaac <aisaac@...310...> wrote:

Jason Grout wrote:

The other problem is a more serious problem for me: how do
I shorten the line so that it goes between the boundaries
of the circle instead of the centers, especially when the
circles are constructed in a scatter plot.

Some years back I briefly tried to think about arrows and
I found it trickier than expected. Note that some famous
software clearly does arrows wrong. (E.g., gnuplot, at
least last I checked.)

Example: you have decided that you want to draw to the edge
of a point, but a) is that right and b) can it be reasonably
implemented?

a) One might well argue in many applications that the arrow
tip should go to the center of the circle.

b) I'm not sure.

But surely somebody out there will offer some great clues.
Perhaps along the line of graphviz:
Finite Automaton | Graphviz

Really this is not an answer to your questions ...

Cheers,
Alan Isaac

-------------------------------------------------------------------------
This SF.Net email is sponsored by the Moblin Your Move Developer's challenge
Build the coolest Linux based applications with Moblin SDK & win great prizes
Grand prize is a trip for two to an Open Source event anywhere in the world
http://moblin-contest.org/redirect.php?banner_id=100&url=/
_______________________________________________
Matplotlib-users mailing list
Matplotlib-users@lists.sourceforge.net
matplotlib-users List Signup and Options

Jae-Joon Lee wrote:

Hi Jason,

I did made a similar class sometime ago and I'm attaching it just in
case. I guess it is very similar to yours but I rely on
matplolib.patches.FancyArrow class to draw the arrow head.

The circle drawn by scatter() command should be a circle with size s
(the third argument of the scatter command) in points . It seems that
it is implemented as a unit circle centered at (0,0) with a transform
corresponding to the size s (and offset). So you may try something
like below to calculate the size of the circle in data coord.

  ax = gca()
  p = scatter([0],[0], 500.)
  tr = p.get_transforms()[0] + ax.transData.inverted()
  x1, y1 = tr.transform_point([0,0])
  x2, y2 = tr.transform_point([1,0])
  r = abs(x2 - x1)

p is a collection object and p.get_transforms() is a list of transforms.
Note that a circle in the canvas coordinate(?) can be an ellipse in
data coordinates. So, I guess you'd better do things in the canvas
coordinates.

For shortening your path, if you're only concerned with a straight
line, it should be straight forward. But I guess it would a bit tricky
to do this for general bezier curves (as in the example that Alan
linked). I think (but I may be wrong) there is no universal algorithm
to find the "all" intersecting points of two bezier curves. There may
be one for between a bezier curve and a circle. And in this case where
one point is inside the circle and the other is outside, one simple
way I can think of is to recursively bisect the bezier curve (similar
to the bisect root finding).

Jae-Joon,

Thank you very much. I am just finishing implementing a working version of what I wanted in my ArrowLine class; it now shortens itself by a certain number of points (assuming a line, just using a scale transformation). However, I use paths for drawing the arrowhead where you use patches. I think I like the flexibility your approach offers. Do you mind if I include your code in the GPL-licensed Sage, and extend it to do this shortening thing that I need? I still haven't decided which is ultimately better for what I need (my class or your class), but if you're willing to license your class in a compatible way, that provides a choice.

Thanks,

Jason

Sure. you may include it if you want.

-JJ

···

On Sat, Aug 23, 2008 at 12:10 AM, Jason Grout <jason-sage@...2130...> wrote:

Jae-Joon Lee wrote:

Hi Jason,

I did made a similar class sometime ago and I'm attaching it just in
case. I guess it is very similar to yours but I rely on
matplolib.patches.FancyArrow class to draw the arrow head.

The circle drawn by scatter() command should be a circle with size s
(the third argument of the scatter command) in points . It seems that
it is implemented as a unit circle centered at (0,0) with a transform
corresponding to the size s (and offset). So you may try something
like below to calculate the size of the circle in data coord.

  ax = gca()
  p = scatter([0],[0], 500.)
  tr = p.get_transforms()[0] + ax.transData.inverted()
  x1, y1 = tr.transform_point([0,0])
  x2, y2 = tr.transform_point([1,0])
  r = abs(x2 - x1)

p is a collection object and p.get_transforms() is a list of transforms.
Note that a circle in the canvas coordinate(?) can be an ellipse in
data coordinates. So, I guess you'd better do things in the canvas
coordinates.

For shortening your path, if you're only concerned with a straight
line, it should be straight forward. But I guess it would a bit tricky
to do this for general bezier curves (as in the example that Alan
linked). I think (but I may be wrong) there is no universal algorithm
to find the "all" intersecting points of two bezier curves. There may
be one for between a bezier curve and a circle. And in this case where
one point is inside the circle and the other is outside, one simple
way I can think of is to recursively bisect the bezier curve (similar
to the bisect root finding).

Jae-Joon,

Thank you very much. I am just finishing implementing a working version
of what I wanted in my ArrowLine class; it now shortens itself by a
certain number of points (assuming a line, just using a scale
transformation). However, I use paths for drawing the arrowhead where
you use patches. I think I like the flexibility your approach offers.
Do you mind if I include your code in the GPL-licensed Sage, and extend
it to do this shortening thing that I need? I still haven't decided
which is ultimately better for what I need (my class or your class), but
if you're willing to license your class in a compatible way, that
provides a choice.

Thanks,

Jason

-------------------------------------------------------------------------
This SF.Net email is sponsored by the Moblin Your Move Developer's challenge
Build the coolest Linux based applications with Moblin SDK & win great prizes
Grand prize is a trip for two to an Open Source event anywhere in the world
http://moblin-contest.org/redirect.php?banner_id=100&url=/
_______________________________________________
Matplotlib-users mailing list
Matplotlib-users@lists.sourceforge.net
matplotlib-users List Signup and Options