+15  A: 

You have a couple options: svg or canvas.

From the looks of it you don't need these arrows to have any particular mathematical form, you just need them to go between elements.

Have a look at this WireIt Demo. It uses a canvas tag for each individual wire between the floating dialog divs, then sizes and positions each canvas element to give the appearance of a connecting line at just the right spot. You may have to implement an additional rotating arrowhead, unless you don't mind the arrows coming in to each element at the same angle.

Edit: Ignore this answer, @Phil H nailed it

Crescent Fresh
WireIt was my first thought, but you beat me to it.
Toby Hede
+3  A: 

You could try this library - it's very clever stuff, hope it helps.

Kieron
+1 for Walter Zorn's drawing library ... it's perfect for this type of application (but don't try to use it for a web-based CAD system!).
Steve Moyer
A: 

I'd suggest using SilverLight, flash, or Java for this. Or, making a static image with the arrows

Click Upvote
+3  A: 

I try to go with open web technologies wherever possible but the truth is that html & JavaScript (or jQuery) aren't the tools for this particular job (sad but true), especially as the diagrams you're drawing increase in complexity.

On the other hand, flash was made for this. Significantly less Actionscript 3.0 code would be required to parse that XML, layout your text (with more control over fonts & super/subscripts) and render the curves (see the flash.display.Graphics class methods like curveTo). Overall you'll be looking at less code, better maintainability, fewer hacks, wider compatibility and more stable drawing libraries.

Good luck with the project

Jaysen Marais
And you don't even need to buy a full flash license to create the flash app - the free flex sdk is sufficient since all the drawing will be procedural anyway.
Simon Groenewolt
+1  A: 

As others have mentioned, javascript and html are not good tools for this sort of thing.

John Resig wrote an implementation of processing.org in javascript. It uses the canvas element, so it will work in modern versions of Firefox, but it will not work in all browsers. If you only care about Firefox, this would probably be the way to go.

You might be able to use svg, but again, this is not supported in all browsers.

Buddy
+1  A: 

If you don't need curved arrows, you could use absolutely positioned divs above or below the list. You could then use css to style those divs plus a couple of images that make up the arrow head. Below is an example using the icon set from the jquery ui project (sorry about the long url).

Here's the CSS to get things started:

<style>
 .below{
     border-bottom:1px solid #000;
     border-left:1px solid #000;
     border-right:1px solid #000;
 }
 .below span{
    background-position:0px -16px;
    top:-8px;
 }
 .above{
     border-top:1px solid #000;
     border-left:1px solid #000;
     border-right:1px solid #000;
 }
 .above span{
    background-position:-64px -16px;
    bottom:-8px;
 }

 .arrow{
    position:absolute;
    display:block;
    background-image:url(http://jquery-ui.googlecode.com/svn/trunk/themes/base/images/ui-icons_454545_256x240.png);
    width:16px;
    height:16px;
    margin:0;
    padding:0;
 }

.left{left:-8px;}

.right{right:-9px;}

</style>

Now we can start to assemble arrow divs. For instance, to style the arrow from "requires" to "promoter" in your example above, you could do left,bottom, and right borders on the div with and upward facing arrow graphic in the top left of the div.

<div class='below' style="position:absolute;top:30px;left:30px;width:100px;height:16px">
   <span class='arrow left'></span>
</div>

The inline styles would be need to be applied by script after you figured out the locations of the things you would need to connect. Let's say that your list looks like this:

<span id="promoter">Promoter</span><span>Something Else</span><span id="requires">Requires</span>

Then the following script will position your arrow:

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"&gt;&lt;/script&gt; 
<script>
$(function(){
 var promoterPos=$("#promoter").offset();
 var requiresPos=$("#requires").offset();
 $("<div class='below'><span class='arrow left'></span></div>")
 .css({position:"absolute",left:promoterPos.left,right:promoterPos.top+$("#promoter").height()})
 .width(requiresPos.left-promoterPos.left)
 .height(16)
 .appendTo("body");
});
</script>

Go ahead and paste the examples above into a blank html page. It's kind of neat.

Josh Bush
Well, my main question is: how do I figure out the locations of the things that need to be connected. The HTML file would only have ID attributes for these things. How to I map an ID to the screen location with Javascript?
Kaarel
You can use jQuery offset or position and width/height functions to determine that. (http://docs.jquery.com/CSS)
Josh Bush
Edited my answer to include an example using jQuery to find out the positions of the items to connect and drawing a line between them.
Josh Bush
A: 

you could get the curved arrow ends using a handful of position:absolute divs with background-image set to transparent GIFs .. a set for beginning (top and bottom) .. a bacground:repeat div for expandible middle, and another pair for the ends (top and bottom)

Scott Evernden
+9  A: 

This captured my interest for long enough to produce a little test. The code is below, and you can see it in action

It lists all the spans on the page (might want to restrict that to just those with ids starting with T if that is suitable), and uses the 'ids' attribute to build the list of links. Using a canvas element behind the spans, it draws arc arrows alternately above and below the spans for each source span.

<script type="application/x-javascript"> 

function generateNodeSet() 
{
  var spans = document.getElementsByTagName("span");
  var retarr = [];
  for(var i=0;i<spans.length; i++) 
  { 
     retarr[retarr.length] = spans[i].id; 
  } 
  return retarr; 
} 

function generateLinks(nodeIds) 
{ 
  var retarr = []; 
  for(var i=0; i<nodeIds.length; i++) 
  { 
    var id=nodeIds[i]; 
    var span = document.getElementById(id); 
    var atts = span.attributes; 
    var ids_str = false; 

    if(atts.getNamedItem) 
    { 
      if(atts.getNamedItem('ids')) 
      { 
        ids_str = atts.getNamedItem('ids').value; 
      } 
    } 

    if(ids_str)
    { 
      target_ids = ids_str.split(" "); 
      retarr[id] = target_ids;
    }
  } 
  return retarr; 
} 

// draw a horizontal arc
//   ctx: canvas context;
//   inax: first x point
//   inbx: second x point
//   y: y value of start and end
//   alpha_degrees: (tangential) angle of start and end
//   upside: true for arc above y, false for arc below y.
function drawHorizArc(ctx, inax, inbx, y, alpha_degrees, upside)
{
  alpha = alpha_degrees/360.0 * 2.0 * Math.PI;
  if(upside)
  {
    var ax=Math.min(inax,inbx);
    var bx=Math.max(inax,inbx);
    var startangle = (3.0/2.0)*Math.PI + alpha; //North plus alpha west
    var endangle = (3.0/2.0)*Math.PI - alpha; //North minus alpha east
  }
  else
  {
    var ax=Math.max(inax,inbx);
    var bx=Math.min(inax,inbx);
    var startangle = (1.0/2.0)*Math.PI - alpha; //South minus alpha west
    var endangle = (1.0/2.0)*Math.PI + alpha; //North plus alpha east
  }
  // tan(alpha) = o/a = ((bx-ax)/2) / o
  // o = ((bx-ax)/2/tan(alpha))
  // centre of circle is (bx+ax)/2, y-o
  var circleyoffset = ((bx-ax)/2)/Math.tan(alpha);
  var circlex = (ax+bx)/2.0;
  var circley = y + circleyoffset;
  var radius = Math.sqrt(Math.pow(circlex-ax,2) + Math.pow(circley-y,2));

  ctx.beginPath();
  if(upside)
  {
    ctx.arc(circlex,circley,radius,startangle,endangle,1);
  }
  else
  {
    ctx.arc(circlex,circley,radius,startangle,endangle,0);
  }
  ctx.stroke();
}

// degrees to radians, because most people think in degrees
function degToRad(angle_degrees)
{
   return angle_degrees/180*Math.PI;
}

// draw the head of an arrow (not the main line)
//  ctx: canvas context
//  x,y: coords of arrow point
//  angle_from_north_clockwise: angle of the line of the arrow from horizontal
//  upside: true=above the horizontal, false=below
//  barb_angle: angle between barb and line of the arrow
//  filled: fill the triangle? (true or false)
function drawArrowHead(ctx, x, y, angle_from_horizontal_degrees, upside, //mandatory
                       barb_length, barb_angle_degrees, filled)          //optional
{
   if(barb_length==undefined) { barb_length=13; }
   if(barb_angle_degrees==undefined) { barb_angle_degrees = 20; }
   if(filled==undefined) { filled=true; }
   if(upside) { alpha = -angle_from_horizontal_degrees; }
   else { alpha = angle_from_horizontal_degrees; }

   //first point is end of one barb
   a = x + (barb_length * Math.cos(degToRad(alpha - barb_angle_degrees)));
   b = y + (barb_length * Math.sin(degToRad(alpha - barb_angle_degrees)));

   //final point is end of the second barb
   c = x + (barb_length * Math.cos(degToRad(alpha + barb_angle_degrees)));
   d = y + (barb_length * Math.sin(degToRad(alpha + barb_angle_degrees)));

   ctx.beginPath();
   ctx.moveTo(a,b);
   ctx.lineTo(x,y);
   ctx.lineTo(c,d);
   if(filled) { ctx.fill(); }
   else { ctx.stroke(); }
   return true;
}

// draw a horizontal arcing arrow
//  ctx: canvas context
//  inax: start x value
//  inbx: end x value
//  y: y value
//  alpha_degrees: angle of ends to horizontal (30=shallow, >90=silly)
function drawHorizArcArrow(ctx, inax, inbx, y,                 //mandatory
                           alpha_degrees, upside, barb_length) //optional
{
   if(alpha_degrees==undefined) { alpha_degrees=45; }
   if(upside==undefined) { upside=true; }
   drawHorizArc(ctx, inax, inbx, y, alpha_degrees, upside);
   if(inax>inbx) { drawArrowHead(ctx, inbx, y, alpha_degrees*0.9, upside, barb_length); }
   else { drawArrowHead(ctx, inbx, y, (180-alpha_degrees*0.9), upside, barb_length); }
   return true;
}


function drawArrow(ctx,fromelem,toelem,    //mandatory
                     above, angle)         //optional
{
  if(above==undefined) {above = true; }
  if(angle==undefined) {angle = 45; } //degrees 
  midfrom = fromelem.offsetLeft + (fromelem.offsetWidth / 2) - left - tofromseparation/2; 
  midto   =   toelem.offsetLeft + (  toelem.offsetWidth / 2) - left + tofromseparation/2;

  if(above)
  {
    var y = fromelem.offsetTop - top ;
    drawHorizArcArrow(ctx, midfrom, midto, y, angle, true);
  }
  else        //below
  {
    var y = fromelem.offsetTop + fromelem.offsetHeight - top;
    drawHorizArcArrow(ctx, midfrom, midto, y, angle, false);
  }
}



function draw() 
{ 
  var canvas = document.getElementById("canvas");
  var spanbox = document.getElementById("spanbox");
  var ctx = canvas.getContext("2d");

  nodeset = generateNodeSet(); 
  linkset = generateLinks(nodeset);

  tofromseparation = 20;

  left = canvas.offsetLeft - spanbox.offsetLeft;
  top = canvas.offsetTop - spanbox.offsetTop; 

  for(var key in linkset) 
  {  
    for (var i=0; i<linkset[key].length; i++) 
    {  
      fromid = key; 
      toid = linkset[key][i]; 

      var above = (i%2==1);
      drawArrow(ctx,document.getElementById(fromid),document.getElementById(toid),above);
    } 
  } 
} 

</script>

And you just need a call somewhere to the draw() function:

<body onload="draw();">

Then a canvas behind the set of spans.

<canvas style='border:1px solid red' id="canvas" width="800" height="7em"></canvas><br /> 
<div id="spanbox" style='float:left; position:absolute; top:75px; left:50px'>
<span id="T2">p50</span>
...
<span id="T3">p65</span> 
...
<span id="T34" ids="T2 T3">recruitment</span>
</div>

Future modifications, as far as I can see:

  • Flattening the top of longer arrows
  • Refactoring to be able to draw non-horizontal arrows: add a new canvas for each?
  • Use a better routine to get the total offsets of the canvas and span elements.

Hope that's as useful as it was fun.

Phil H
Thanks, this looks quite impressive. And seems to be the answer that I was after. Too bad the bounty competition is over already.
Kaarel
Unfortunately a full time life didn't give me time to finish it before the bounty ended! Ah well.
Phil H
Holy Hell, fantastic answer!
Crescent Fresh
Thanks, I wish these kinds of little challenges came up more often. Nice mixture of geometry, learning APIs, and HTML frustration (canvas has no text rendering yet). And it makes something pretty!
Phil H
A: 

@Phil H: The code doesn't work anymore? :S

a110y
This should be a comment.
Kaarel
True, sorry new to stack overflow. :)EDIT: In fact, I don't know how to add a comment. :S
a110y
You just made one!
Bart S.
Yes, I can only add comment on *my* own posts. :SI meant I don't know how to add comments on others posts since I don't see the "add comment" link.
a110y