SVG chart generation in JavaScript - javascript

I'm looking for a way to generate pie charts using SVG.
The numbers I have are simple enough - just percentages, an array of numbers that obviously add up to 100.
I have a basic understanding of SVG, but I can't think how to translate these numbers into meaningful coordinates to use in the path tag
Can anyone point me to a useful utility or library, or give any hints as to how I could use percentages to draw a pie chart - in JavaScript?

Credits to https://stackoverflow.com/a/3642265/309483 and http://jbkflex.wordpress.com/2011/07/28/creating-a-svg-pie-chart-html5/ (note that the last one has a bug, fixed here)
function makeSVG(tag, attrs) {
var el= document.createElementNS('http://www.w3.org/2000/svg', tag);
for (var k in attrs)
if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]);
return el;
}
function drawArcs(paper, pieData){
var total = pieData.reduce(function (accu, that) { return that + accu; }, 0);
var sectorAngleArr = pieData.map(function (v) { return 360 * v / total; });
var startAngle = 0;
var endAngle = 0;
for (var i=0; i<sectorAngleArr.length; i++){
startAngle = endAngle;
endAngle = startAngle + sectorAngleArr[i];
var x1,x2,y1,y2 ;
x1 = parseInt(Math.round(200 + 195*Math.cos(Math.PI*startAngle/180)));
y1 = parseInt(Math.round(200 + 195*Math.sin(Math.PI*startAngle/180)));
x2 = parseInt(Math.round(200 + 195*Math.cos(Math.PI*endAngle/180)));
y2 = parseInt(Math.round(200 + 195*Math.sin(Math.PI*endAngle/180)));
var d = "M200,200 L" + x1 + "," + y1 + " A195,195 0 " +
((endAngle-startAngle > 180) ? 1 : 0) + ",1 " + x2 + "," + y2 + " z";
//alert(d); // enable to see coords as they are displayed
var c = parseInt(i / sectorAngleArr.length * 360);
var arc = makeSVG("path", {d: d, fill: "hsl(" + c + ", 66%, 50%)"});
paper.appendChild(arc);
arc.onclick = (function (originalData) {
return function(event) {
alert("Associated pie piece data: " + originalData);
}
})(pieData[i]);
}
}
var svgdoc = document.getElementById("s");
drawArcs(svgdoc, [52,15,20,80]); // here is the pie chart data
// You can attach additional content (from e.g. AJAX) like this:
var parser = new DOMParser();
var docToEmbed = parser.parseFromString(
"<svg xmlns='http://www.w3.org/2000/svg'><text x='50' y='50' fill='black'>hi</text></svg>",
"image/svg+xml");
Array.prototype.slice.call(docToEmbed.documentElement.childNodes).forEach(function(elem) {
svgdoc.appendChild(document.importNode(elem, true));
});
#con {
resize:both;
overflow:hidden;
display:inline-block;
width:20em;
height:20em;
padding:0.5em;
}
<div id="con">
<!-- the div container is of course optional. It is used with
{width,height}="100%" below to make the chart resizable. -->
<svg width="100%" height="100%" id="s"
xmlns="http://www.w3.org/2000/svg" viewbox="0 0 400 400">
<style type="text/css">
path:hover {
opacity: 0.8;
}
</style>
</svg>
</div>

Here are a few more:
Elycharts (based on jQuery and Raphaël, MIT license)
ZingCharts (commercial, has SVG/VML/HTML5/Flash backends)
Grafico (based on Prototype and Raphaël, MIT license)
d3.js (very nice library for interactive and dynamic graphs, MIT-like license)
I try to collect links to all svg graphing libraries here.

Raphael is a very good SVG drawing library -- in particular, it beats the others because in older versions of IE, it automatically falls back to using VML, and therefore it works in IE from version 6 and up, as well as in all other mainstream browsers.
It has a separate graphing library, called gRaphael. This does all the usual graph types (pies, lines, bars, etc), and can animate them too.
If those aren't enough, it's easy enough to use the main Raphael library to roll your own - it's very easy to use.

The best (IMO): Highcharts
Others I have heard about:
PlotKit
Raphaël

Related

How to get Consistent Stroke Widths Regardless of Location?

The essence of the problem is that when I move an SVG element around, the widths of the lines varies.
I'm working in a browser environment and I want crisp lines (no anti-aliasing), so shape-rendering is set to crispEdges. For example, you might have
"d M 0 0 L 10 0 L 10 10 L 0 10 z"
to define a square, with style set to
'fill: none; stroke: black; stroke-width: 1px; shape-rendering: crispEdges'
Then use translate() to drag it around with the mouse. It works, but as the square moves, the thickness of the lines jumps around by a pixel, and I want the thickness to remain constant.
I think I know why it's happening, but I can't find any way to prevent it. It happens (I think) due to the way the coordinates relative to the viewBox are mapped to coordinates relative to the physical monitor's pixels. When a one pixel wide (in SVG terms) line is mapped to the monitor, it may or may not straddle several pixels, so it may end up a (screen) pixel thicker or thinner.
I've tried messing with the ratio of the viewBox size to the size of the drawing area, rounding the translation amount to be a whole number or to take the form integer + 0.50, but no solution like this seems likely to work because it comes down to the level of zoom in the browser. vector-effect: non-scaling-stroke doesn't fix it either.
Is there a way to tell SVG to treat all paths as 1d geometric shapes, and to use a particular pen for all of them? Put another way, I want the pen used to draw everything to "scale with the zoom", rather than having the lines scale after they've been drawn.
Please, vanilla JS only.
You can test what I mean by running the code below. Click on the square and drag it around (no touch devices). Note that at some levels of browser zoom, it may behave nicely and not do what I've described.
There's something odd going on. The svg element (the square) starts out looking uniform, with all edges of the same width. It only moves in response to a mousemove, and that event (presumably) happens when the mouse moves by a whole (never fractional) screen pixel. Therefore, one would expect the square to move by a whole screen pixel and remain aligned to the screen pixels if it started off that way.
Follow-up...The problem is not with the screen CTM. I tested that the inverse really is an inverse; that is, CTM x CTM^{-1} is the identity for every value I tried. And the values that matrixTransform(getScreenCTM().inverse()) provides are linear in the inputs (or as linear as floating-point numbers allow).
<html>
<body>
<svg xmlns="http://www.w3.org/2000/svg" id="svgtest"
style = "border: 1px solid; display: block; margin-left: auto; margin-right: auto; shape-rendering: crispEdges;"
width = "400" height = "400" viewBox = "0 0 100 100" >
</svg>
<script>
var theSquare = {
x : 10,
y : 10
};
var initialSquare = {
x : 10,
y : 10
};
var SideLength = 10;
var initialX = 0;
var initialY = 0;
var draggingSquare = 0;
function pointInRect(p, r) {
return p.x > r.xLeft && p.x < r.xRight && p.y > r.yTop && p.y < r.yBottom;
}
function mouseToLocal(theEvent) {
var screenPt = svgArea.createSVGPoint();
screenPt.x = theEvent.clientX;
screenPt.y = theEvent.clientY;
var svgPt = screenPt.matrixTransform(svgArea.getScreenCTM().inverse());
return {
x : svgPt.x,
y : svgPt.y
};
}
function doMouseDown(event) {
var localCoord = mouseToLocal(event);
initialX = localCoord.x;
initialY = localCoord.y;
var pt = {x: initialX, y: initialY};
var rect = {xLeft: theSquare.x, xRight: theSquare.x + SideLength,
yTop: theSquare.y, yBottom: theSquare.y + SideLength};
if (pointInRect(pt,rect))
{
draggingSquare = 1;
initialSquare.x = theSquare.x;
initialSquare.y = theSquare.y;
}
else
draggingSquare = 0;
}
function doMouseUp(event) {
draggingSquare = 0;
}
function doMouseMove(event) {
if (draggingSquare == 0)
return;
var localCoord = mouseToLocal(event);
zeroTranslate.setTranslate(initialSquare.x + localCoord.x - initialX,
+initialSquare.y + localCoord.y - initialY);
theSquare.x = initialSquare.x + localCoord.x - initialX;
theSquare.y = initialSquare.y + localCoord.y - initialY;
}
var svgArea = document.getElementById('svgtest');
var theSVGSquare = document.createElementNS("http://www.w3.org/2000/svg", 'path' );
theSVGSquare.setAttributeNS(null, "d", "M 0 0" +
" L " + SideLength + " 0" +
" L " + SideLength + " " + SideLength +
" L 0 " +SideLength +
" z");
theSVGSquare.setAttributeNS(null, 'style', 'fill: none; stroke: black; stroke-width: 0.5px; shape-rendering: crispEdges' );
var tforms = theSVGSquare.transform;
var zeroTranslate = svgArea.createSVGTransform();
zeroTranslate.setTranslate(initialSquare.x,initialSquare.y);
theSVGSquare.transform.baseVal.insertItemBefore(zeroTranslate,0);
svgArea.appendChild(theSVGSquare);
svgArea.addEventListener("mousedown",doMouseDown,false);
svgArea.addEventListener("mouseup",doMouseUp,false);
svgArea.addEventListener("mousemove",doMouseMove,false);
</script>
</body>
</html>
I'm posting this "answer" as a way to officially close the question, and to thank Robert Longson for his attention.
In short, what I want to do can't be done in a browser, which is surprising since all I want to do is draw a line. Using the HTML canvas tag produces something blurry due to anti-aliasing, and SVG produces jittery lines. It comes down to the fact that the browser doesn't provide the information needed to work at a device-pixel level of accuracy.
I posted a discussion of this problem, with examples, on my personal website:
Canvas drawing is blurry and SVG is jittery

Color-Scales depending on how often a point gets drawn (JS Canvas)

Lately I created a project involving drawing a lot of points on a canvas to plot a strange attractor. The details of this project aren't really relevant, but if you want to see it in action, go here: How can I check if an attractor is strange?
The problem I was encountering is the following: How can I draw a point on a canvas, whose color depends on the color, that was already there? In other words: How do I implement a color scale that depends on that number of times, a specific point has been colored?
I actually found a way, but I'm not convinced if it's the best. Here is how it works:
ctx.globalCompositeOperation = "lighter";
ctx.fillStyle = "rgb(50,5,1)";
ctx.fillRect(x,y,size,size);
It simply adds to the color that is already there. This can already look pretty good:
But there are also a lot of restrictions when using this method:
I can't get a colorchange from green to red for example
Using this method on a white background is impossible
I can't create a colorscale that includes more than to "fixed points", like for example red->green->blue
Maybe you know methods that work better than mine...
I think you need to track hits per pixel to implement a function that would allow you to change picture color, rather than just luminosity or redness. As suggested above, you should use a multi-dimensional array to track hits per pixel.
var canvasPixels = [];
for (var y = 0; y < 1000; y++) {
canvasPixels[y] = [];
for (var x = 0; x < 1000; x++) {
canvasPixels[y][x] = 0;
}
}
There are any number of things you can do if you apply the color math yourself. Here I'm using a color sine wave.
function getColor(hits) {
var frequency = 0.3;
var r = Math.round(Math.sin(frequency*hits + 0) * 127 + 128);
var g = Math.round(Math.sin(frequency*hits + 2) * 127 + 128);
var b = Math.round(Math.sin(frequency*hits + 4) * 127 + 128);
return "rgb(" + r + "," + g + "," + b + ")";
}
Then, you just use this function when drawing to cycle through the rainbow.
canvasPixels[y][x]++;
ctx.fillStyle = getColor(canvasPixels[y][x]);
ctx.fillRect(x,y,size,size);

Is there any way to draw a table using Raphael js library

I am using Raphael JS to creating a ERD like tool, but don't know to to create a table like instance by using Raphael.
paper = new Raphael(0,0,500,500);
var x = 100;
var y = 50;
var height = 50
var width = 100;
WriteTableRow(x,y,width*2,height,paper,"TOP Title");
y= y+height;
WriteTableRow(x,y,width,height,paper,"Score,Player");
y= y+height;
for (i=1;i<=4;i++)
{
var k;
k = Math.floor(Math.random() * (10 + 1 - 5) + 5);
WriteTableRow(x,y,width,height,paper,i+","+ k + "");
y= y+height;
}
function WriteTableRow(x,y,width,height,paper,TDdata)
{
var TD = TDdata.split(",");
for (j=0;j<TD.length;j++)
{
var rect = paper.rect(x,y,width,height).attr({"fill":"white","stroke":"red"});
paper.text(x+width/2, y+height/2, TD[j])
x = x + width;
}
}
//http://jsfiddle.net/LG5zn/116/
Raphael can draw anything, but if you're doing a lot of manual creation of tables and so forth than you're probably fighting the framework. My favorite aspect of the library is that is works so seamlessly with traditional HTML, CSS and Javascript. I would strong suggest taking what you want from HCJ and using Raphael for whatever that can't easily do.

Fastest way to implement z-buffering for a software renderer?

I'm implementing a javascript software renderer (for academic purposes). It handles representing a 3d object as triangles, and handles Perspective Projection from 3d space to 2d space.
Until now, I used the lineTo and fillRect to represent the vertices and the lines on screen. I've even used lineTo to do Scan Line triangle filling. (you can check out the project here)
So far the FPS has been quite good. But the last part of the assignment is to implement z-Buffering :P. To my knowledge, the only way to do this is to stop filling my triangles using lineTo and fill them with either an array of 1px lines or an array of 1px squares. (because before I draw each "pixel", I have to check the depth buffer and see if I should actually draw it or not.)
The problem is, filling triangles with tiny rectangles or lines is SLOW. Gets everything down to 2FPS. So my question is, is there any method to draw one pixel instead of a tiny line (which may be faster)?
Alternatively, what else can I do to speed things up? My goal is to have it spin fast enough to demo the principle. (6-10fps would be enough)
Cheers.
[EDIT] While I wait for an answer, I will procede to modify my triangle filling functions to draw 4px sized "pixels" instead of 1px. But that will look jaggedy...
Check this out: http://jsfiddle.net/ZXjAM/2/
// points 0,1,2,3 front face
var fAvgZ = (cube.processPoints[0].colorZ +
cube.processPoints[1].colorZ +
cube.processPoints[2].colorZ +
cube.processPoints[3].colorZ) / 4 / 20;
// points 0,2,4,6 top
var tAvgZ = (cube.processPoints[0].colorZ +
cube.processPoints[2].colorZ +
cube.processPoints[4].colorZ +
cube.processPoints[6].colorZ) / 4 / 20;
// points 4,5,6,7 rear
var reAvgZ = (cube.processPoints[4].colorZ +
cube.processPoints[5].colorZ +
cube.processPoints[6].colorZ +
cube.processPoints[7].colorZ) / 4 / 20;
// points 1,3,5,7 bottom
var bAvgZ = (cube.processPoints[1].colorZ +
cube.processPoints[3].colorZ +
cube.processPoints[5].colorZ +
cube.processPoints[7].colorZ) / 4 / 20;
// points 2,3,6,7 right side
var rAvgZ = (cube.processPoints[2].colorZ +
cube.processPoints[3].colorZ +
cube.processPoints[6].colorZ +
cube.processPoints[7].colorZ) / 4 / 20;
// points 0,1,4,5 left side
var lAvgZ = (cube.processPoints[0].colorZ +
cube.processPoints[1].colorZ +
cube.processPoints[4].colorZ +
cube.processPoints[5].colorZ) / 4 / 20;
var layers = [{key:0, val:fAvgZ},
{key:1, val:fAvgZ},
{key:2, val:tAvgZ},
{key:3, val:tAvgZ},
{key:4, val:reAvgZ},
{key:5, val:reAvgZ},
{key:6, val:bAvgZ},
{key:7, val:bAvgZ},
{key:8, val:rAvgZ},
{key:9, val:rAvgZ},
{key:10, val:lAvgZ},
{key:11, val:lAvgZ}];
var outLay = layers.sort(function(a,b){
return (a.val - b.val);
});
for(var i = 0; i < outLay.length; i++)
{
var k = outLay[i].key;
...
}
This is, by no means, the most efficient way to average/sort the point values, and it can probably be done with fewer lines of code using the cube's pre-existing properties, but the basic concept remains the same.
I'm finding the average z-index and using that to assume layering order. Obviously, this won't work for everything ever, but for simple polyhedra, it should suffice.
This can be simplified to:
var layers = [];
for (var i = 0; i < cube.sides.length; i++){
var side = cube.sides[i];
var avg = (cube.processPoints[side.a].colorZ +
cube.processPoints[side.b].colorZ +
cube.processPoints[side.c].colorZ) / 3 / 20;
layers.push({key:i, val:avg});
}
var outLay = layers.sort(function(a,b){
return (a.val - b.val);
});
There do seem to be some fringe-cases where there is a quick ordering-problem.
This seems to be more accurate: http://jsfiddle.net/ZXjAM/4/
var layers = [];
for (var i = 0; i < 12; ++i){
var side1 = cube.sides[i];
var side2 = cube.sides[++i];
var avg = (cube.processPoints[side1.a].colorZ +
cube.processPoints[side1.b].colorZ +
cube.processPoints[side1.c].colorZ +
cube.processPoints[side2.a].colorZ +
cube.processPoints[side2.b].colorZ +
cube.processPoints[side2.c].colorZ) / 6;
layers.push({key:i-1, val:avg});
layers.push({key:i, val:avg});
}
var outLay = layers.sort(function(a,b){
return (a.val - b.val);
});

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

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

Categories