Question

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.

Was it helpful?

Solution

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

OTHER TIPS

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.

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.

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

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

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.

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.

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.

I needed a similar solution, and I was looking into RaphaelJS JavaScript Library. For example you can draw a straight arrow from (x1,y1) to (x2,y2) with:

Raphael.fn.arrow = function (x1, y1, x2, y2, size) {
  var angle = Math.atan2(x1-x2,y2-y1);
  angle = (angle / (2 * Math.PI)) * 360;
  var arrowPath = this.path(“M” + x2 + ” ” + y2 + ” L” + (x2 - size) + ” ” + (y2 - size) + ” L” + (x2 - size) + ” ” + (y2 + size) + ” L” + x2 + ” ” + y2 ).attr(“fill”,”black”).rotate((90+angle),x2,y2);
  var linePath = this.path(“M” + x1 + ” ” + y1 + ” L” + x2 + ” ” + y2);
  return [linePath,arrowPath];
}

I haven't figure out how to draw a curved arrow, but I'm sure it's possible.

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).

You can use this library: just annotate your SVG lines with the ids of the source & target element. It uses MutationObserver to observe changes in the connected elements.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top