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.