Drawing arrows on an HTML page to visualize semantic links between textual spans

31,782

Solution 1

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.

Try WireIt. Have a look at this WireIt Demo (which has been deprecated). 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: the demo has been deprecated.

Edit: Ignore this answer, @Phil H nailed it

Solution 2

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

screenshot

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) && (atts.getNamedItem('ids'))) { 
      ids_str = atts.getNamedItem('ids').value; 
    } 
    if(ids_str) { 
      retarr[id] = ids_str.split(" ");
    }
  } 
  return retarr; 
} 
    
// degrees to radians, because most people think in degrees
function degToRad(angle_degrees) {
   return angle_degrees/180*Math.PI;
}
// 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)
{
  var alpha = degToRad(alpha_degrees);
  var startangle = (upside ? ((3.0/2.0)*Math.PI + alpha) : ((1.0/2.0)*Math.PI - alpha));
  var endangle = (upside ? ((3.0/2.0)*Math.PI - alpha) : ((1.0/2.0)*Math.PI + alpha));

  var ax=Math.min(inax,inbx);
  var bx=Math.max(inax,inbx);

  // 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 + (upside ? 1 : -1) * circleyoffset;
  var radius = Math.sqrt(Math.pow(circlex-ax,2) + Math.pow(circley-y,2));

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


// 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
   (barb_length==undefined) && (barb_length=13);
   (barb_angle_degrees==undefined) && (barb_angle_degrees = 20);
   (filled==undefined) && (filled=true);
   var alpha_degrees = (upside ? -1 : 1) * angle_from_horizontal_degrees; 
  
   //first point is end of one barb
   var plus = degToRad(alpha_degrees - barb_angle_degrees);
   a = x + (barb_length * Math.cos(plus));
   b = y + (barb_length * Math.sin(plus));
   
   //final point is end of the second barb
   var minus = degToRad(alpha_degrees + barb_angle_degrees);
   c = x + (barb_length * Math.cos(minus));
   d = y + (barb_length * Math.sin(minus));

   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
   (alpha_degrees==undefined) && (alpha_degrees=45);
   (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
  (above==undefined) && (above = true);
  (angle==undefined) && (angle = 45); //degrees 
  midfrom = fromelem.offsetLeft + (fromelem.offsetWidth / 2) - left - tofromseparation/2; 
  midto   =   toelem.offsetLeft + (  toelem.offsetWidth / 2) - left + tofromseparation/2;
  //var y = above ? (fromelem.offsetTop - top) : (fromelem.offsetTop + fromelem.offsetHeight - top);
  var y = fromelem.offsetTop + (above ? 0 : fromelem.offsetHeight) - canvasTop;
  drawHorizArcArrow(ctx, midfrom, midto, y, angle, above);
}

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

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

  left = canvasdiv.offsetLeft - spanboxdiv.offsetLeft;
  canvasTop = canvasdiv.offsetTop - spanboxdiv.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.

[Edit Dec 2011: Fixed, thanks @Palo]

Hope that's as useful as it was fun.

Solution 3

A great library for arrows is JointJS that is based on Raphael as shown above. With JointJS you can easily draw arrows with curves or vertices without any complicated stuff ;-)

var j34 = s3.joint(s4, uml.arrow).setVertices(["170 130", "250 120"]);

This defines an arrow 'j34' that connects two js items s3 with s4. Everything else can be read in the documentation of JointJS.

Solution 4

You could try this JavaScript Vector Graphics Library - it's very clever stuff, hope it helps.

EDIT: As this link is dead, here is another link from Archive.org.

Solution 5

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"></script> 
<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.

Share:
31,782
Joshua Ong
Author by

Joshua Ong

Interested in: nlp, prolog, owl, android, ... Follow me on Google+

Updated on July 09, 2022

Comments

  • Joshua Ong
    Joshua Ong almost 2 years

    I have an HTML page with some textual spans marked up something like this:

    ...
    <span id="T2" class="Protein">p50</span>
    ...
    <span id="T3" class="Protein">p65</span>
    ...
    <span id="T34" ids="T2 T3" class="Positive_regulation">recruitment</span>
    ...
    

    I.e. each span has an ID and refers to zero or more spans via their IDs.

    I would like to visualize these references as arrows.

    Two questions:

    • How can I map an ID of a span to the screen coordinates of the rendering of the span?
    • How do I draw arrows going from one rendering to another?

    The solution should work in Firefox, working in other browsers is a plus but not really necessary. The solution could use jQuery, or some other lightweight JavaScript library.

  • SarahK
    SarahK about 15 years
    WireIt was my first thought, but you beat me to it.
  • Simon Groenewolt
    Simon Groenewolt about 15 years
    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.
  • user1700801
    user1700801 about 15 years
    +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!).
  • Joshua Ong
    Joshua Ong about 15 years
    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?
  • Josh Bush
    Josh Bush about 15 years
    You can use jQuery offset or position and width/height functions to determine that. (docs.jquery.com/CSS)
  • Josh Bush
    Josh Bush about 15 years
    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.
  • Joshua Ong
    Joshua Ong about 15 years
    Thanks, this looks quite impressive. And seems to be the answer that I was after. Too bad the bounty competition is over already.
  • Phil H
    Phil H about 15 years
    Unfortunately a full time life didn't give me time to finish it before the bounty ended! Ah well.
  • Phil H
    Phil H about 15 years
    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!
  • mistertodd
    mistertodd over 13 years
    What browser would i use to see this in action? Chrome and ie9 beta don't draw any arrows - while both supporting HtmlCanvas.
  • Phil H
    Phil H over 13 years
    Firefox 3 and 4 work at present. If you set the height of the canvas to something in pixels I think it will work in the others too.
  • Palo
    Palo over 12 years
    To make this work in all browsers you need to rename the top variable. Top is a reserved property of window. See w3schools.com/jsref/prop_win_top.asp. This needs to be renamed in functions draw and drawArrow. Plus you might need to specify height of canvas in pixels for some browsers as Phil already noted.
  • Phil H
    Phil H over 11 years
    Just realised this wasn't working (heights in 'em' not a good idea in html tags). Fixed again.
  • Quamis
    Quamis over 11 years
    -1 this link is no longer available, you should had detailed more on whats on that page
  • Kieron
    Kieron over 11 years
    This answer is nearly three years old, as I don't own the internet I can't guarantee the longevity of the links. However, a two second hunt around Archive.org found a version from the 20th Feb 2009...!
  • Alok Ranjan
    Alok Ranjan over 9 years
    Great answer. Has anyone tried to using D3.js to draw the arrows?
  • Software Engineer
    Software Engineer over 3 years
    Adding a notice from 2021: Flash has now been deprecated for some time and is no longer supported by most browsers.