| /* Graph JavaScript framework, version 0.0.1 |
| * (c) 2006 Aslak Hellesoy <aslak.hellesoy@gmail.com> |
| * (c) 2006 Dave Hoover <dave.hoover@gmail.com> |
| * |
| * Ported from Graph::Layouter::Spring in |
| * http://search.cpan.org/~pasky/Graph-Layderer-0.02/ |
| * The algorithm is based on a spring-style layouter of a Java-based social |
| * network tracker PieSpy written by Paul Mutton E<lt>paul@jibble.orgE<gt>. |
| * |
| * Adopted by Philipp Strathausen <strathausen@gmail.com> to support Raphael JS |
| * for rendering, dragging and much more. See http://blog.ameisenbar.de |
| * |
| * Graph is freely distributable under the terms of an MIT-style license. |
| * For details, see the Graph web site: http://dev.buildpatternd.com/trac |
| * |
| * Links: |
| * |
| * Demo of the original applet: |
| * http://redsquirrel.com/dave/work/webdep/ |
| * |
| * Mirrored original source code at snipplr: |
| * http://snipplr.com/view/1950/graph-javascript-framework-version-001/ |
| * |
| * Original usage example: |
| * http://ajaxian.com/archives/new-javascriptcanvas-graph-library |
| * |
| /*--------------------------------------------------------------------------*/ |
| |
| /* |
| * Graph |
| */ |
| var Graph = function() { |
| this.nodes = []; |
| this.edges = []; |
| }; |
| Graph.prototype = { |
| addNode: function(id, content) { |
| /* testing if node is already existing in the graph */ |
| var new_node = this.nodes[id]; |
| if(new_node == undefined) { |
| new_node = new Graph.Node(id, content||{"id":id}); |
| this.nodes[id] = new_node; |
| this.nodes.push(new_node); // TODO get rid of the array |
| } |
| return new_node; |
| }, |
| |
| addEdge: function(source, target, style) { |
| var s = this.addNode(source); |
| var t = this.addNode(target); |
| var color; |
| var colorbg; |
| var directed; |
| if(style) { color = style.color; colorbg = style.colorbg; directed = style.directed } |
| var edge = { source: s, target: t, color: color, colorbg: colorbg, directed: directed }; |
| this.edges.push(edge); |
| } |
| }; |
| |
| /* |
| * Node |
| */ |
| Graph.Node = function(id, value){ |
| this.id = id; |
| this.content = value; |
| }; |
| Graph.Node.prototype = { |
| }; |
| Graph.Renderer = {}; |
| Graph.Renderer.Raphael = function(element, graph, width, height) { |
| this.width = width||400; |
| this.height = height||400; |
| var selfRef = this; |
| this.r = Raphael(element, this.width, this.height); |
| this.radius = 40; /* max dimension of a node */ |
| this.graph = graph; |
| this.mouse_in = false; |
| |
| /* |
| * Dragging |
| */ |
| this.isDrag = false; |
| this.dragger = function (e) { |
| this.dx = e.clientX; |
| this.dy = e.clientY; |
| selfRef.isDrag = this; |
| this.animate({"fill-opacity": .2}, 500); |
| e.preventDefault && e.preventDefault(); |
| }; |
| |
| document.onmousemove = function (e) { |
| e = e || window.event; |
| if (selfRef.isDrag) { |
| var newX = e.clientX - selfRef.isDrag.dx + (selfRef.isDrag.attrs.cx == null ? (selfRef.isDrag.attrs.x + selfRef.isDrag.attrs.width / 2) : selfRef.isDrag.attrs.cx); |
| var newY = e.clientY - selfRef.isDrag.dy + (selfRef.isDrag.attrs.cy == null ? (selfRef.isDrag.attrs.y + selfRef.isDrag.attrs.height / 2) : selfRef.isDrag.attrs.cy); |
| /* prevent shapes from being dragged out of the canvas */ |
| var clientX = e.clientX - (newX < 20 ? newX - 20 : newX > selfRef.width - 20 ? newX - selfRef.width + 20 : 0); |
| var clientY = e.clientY - (newY < 20 ? newY - 20 : newY > selfRef.height - 20 ? newY - selfRef.height + 20 : 0); |
| selfRef.isDrag.translate(clientX - selfRef.isDrag.dx, clientY - selfRef.isDrag.dy); |
| selfRef.isDrag.label.translate(clientX - selfRef.isDrag.dx, clientY - selfRef.isDrag.dy); |
| for (var i in selfRef.graph.edges) { |
| selfRef.graph.edges[i].connection.draw(); |
| } |
| //selfRef.r.safari(); |
| selfRef.isDrag.dx = clientX; |
| selfRef.isDrag.dy = clientY; |
| } |
| }; |
| document.onmouseup = function () { |
| selfRef.isDrag && selfRef.isDrag.animate({"fill-opacity": 0}, 500); |
| selfRef.isDrag = false; |
| }; |
| }; |
| |
| /* |
| * Renderer using RaphaelJS |
| */ |
| Graph.Renderer.Raphael.prototype = { |
| translate: function(point) { |
| return [ |
| (point[0] - this.graph.layoutMinX) * this.factorX + this.radius, |
| (point[1] - this.graph.layoutMinY) * this.factorY + this.radius |
| ]; |
| }, |
| |
| rotate: function(point, length, angle) { |
| var dx = length * Math.cos(angle); |
| var dy = length * Math.sin(angle); |
| return [point[0]+dx, point[1]+dy]; |
| }, |
| |
| draw: function() { |
| this.factorX = (width - 2 * this.radius) / (this.graph.layoutMaxX - this.graph.layoutMinX); |
| this.factorY = (height - 2 * this.radius) / (this.graph.layoutMaxY - this.graph.layoutMinY); |
| for (var i = 0; i < this.graph.nodes.length; i++) { |
| this.drawNode(this.graph.nodes[i]); |
| } |
| for (var i = 0; i < this.graph.edges.length; i++) { |
| this.drawEdge(this.graph.edges[i]); |
| } |
| }, |
| drawNode: function(node) { |
| var point = this.translate([node.layoutPosX, node.layoutPosY]); |
| node.point = point; |
| |
| /* if node has already been drawn, move the nodes */ |
| if(node.shape) { |
| // console.log(node.shape.attrs ); |
| var opoint = [ node.shape.attrs.cx || node.shape.attrs.x + node.shape.attrs.width / 2 , node.shape.attrs.cy || node.shape.attrs.y + node.shape.attrs.height / 2 + 15 ]; |
| node.shape.translate(point[0]-opoint[0], point[1]-opoint[1]); |
| node.shape.label.translate(point[0]-opoint[0], point[1]-opoint[1]); |
| this.r.safari(); |
| return; |
| } |
| var shape; |
| if(node.content.getShape) { |
| shape = node.content.getShape(this.r, point[0], point[1]); |
| shape.attr({"fill-opacity": 0}); |
| } else { |
| shape = this.r.ellipse(point[0], point[1], 30, 20); |
| var color = Raphael.getColor(); |
| shape.attr({fill: color, stroke: color, "fill-opacity": 0, "stroke-width": 2}) |
| } |
| shape.mousedown(this.dragger); |
| shape.node.style.cursor = "move"; |
| shape.label = this.r.text(point[0], point[1] + 30, node.content.label || node.id); // Beware: operator || also considers values like -1, 0, ... |
| node.shape = shape; |
| }, |
| drawEdge: function(edge) { |
| /* if edge already has been drawn, only refresh the edge */ |
| edge.connection && edge.connection.draw(); |
| if(!edge.connection) |
| edge.connection = this.r.connection(edge.source.shape, edge.target.shape, { fg: edge.color, bg: edge.colorbg, directed: edge.directed }); |
| } |
| }; |
| Graph.Layout = {}; |
| Graph.Layout.Spring = function(graph) { |
| this.graph = graph; |
| this.iterations = 500; |
| this.maxRepulsiveForceDistance = 6; |
| this.k = 2; |
| this.c = 0.01; |
| this.maxVertexMovement = 0.5; |
| }; |
| Graph.Layout.Spring.prototype = { |
| layout: function() { |
| this.layoutPrepare(); |
| for (var i = 0; i < this.iterations; i++) { |
| this.layoutIteration(); |
| } |
| this.layoutCalcBounds(); |
| }, |
| |
| layoutPrepare: function() { |
| for (var i = 0; i < this.graph.nodes.length; i++) { |
| var node = this.graph.nodes[i]; |
| node.layoutPosX = 0; |
| node.layoutPosY = 0; |
| node.layoutForceX = 0; |
| node.layoutForceY = 0; |
| } |
| |
| }, |
| |
| layoutCalcBounds: function() { |
| var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity; |
| |
| for (var i = 0; i < this.graph.nodes.length; i++) { |
| var x = this.graph.nodes[i].layoutPosX; |
| var y = this.graph.nodes[i].layoutPosY; |
| |
| if(x > maxx) maxx = x; |
| if(x < minx) minx = x; |
| if(y > maxy) maxy = y; |
| if(y < miny) miny = y; |
| } |
| |
| this.graph.layoutMinX = minx; |
| this.graph.layoutMaxX = maxx; |
| this.graph.layoutMinY = miny; |
| this.graph.layoutMaxY = maxy; |
| }, |
| |
| layoutIteration: function() { |
| // Forces on nodes due to node-node repulsions |
| for (var i = 0; i < this.graph.nodes.length; i++) { |
| var node1 = this.graph.nodes[i]; |
| for (var j = i + 1; j < this.graph.nodes.length; j++) { |
| var node2 = this.graph.nodes[j]; |
| this.layoutRepulsive(node1, node2); |
| } |
| } |
| // Forces on nodes due to edge attractions |
| for (var i = 0; i < this.graph.edges.length; i++) { |
| var edge = this.graph.edges[i]; |
| this.layoutAttractive(edge); |
| } |
| |
| // Move by the given force |
| for (var i = 0; i < this.graph.nodes.length; i++) { |
| var node = this.graph.nodes[i]; |
| var xmove = this.c * node.layoutForceX; |
| var ymove = this.c * node.layoutForceY; |
| |
| var max = this.maxVertexMovement; |
| if(xmove > max) xmove = max; |
| if(xmove < -max) xmove = -max; |
| if(ymove > max) ymove = max; |
| if(ymove < -max) ymove = -max; |
| |
| node.layoutPosX += xmove; |
| node.layoutPosY += ymove; |
| node.layoutForceX = 0; |
| node.layoutForceY = 0; |
| } |
| }, |
| |
| layoutRepulsive: function(node1, node2) { |
| var dx = node2.layoutPosX - node1.layoutPosX; |
| var dy = node2.layoutPosY - node1.layoutPosY; |
| var d2 = dx * dx + dy * dy; |
| if(d2 < 0.01) { |
| dx = 0.1 * Math.random() + 0.1; |
| dy = 0.1 * Math.random() + 0.1; |
| var d2 = dx * dx + dy * dy; |
| } |
| var d = Math.sqrt(d2); |
| if(d < this.maxRepulsiveForceDistance) { |
| var repulsiveForce = this.k * this.k / d; |
| node2.layoutForceX += repulsiveForce * dx / d; |
| node2.layoutForceY += repulsiveForce * dy / d; |
| node1.layoutForceX -= repulsiveForce * dx / d; |
| node1.layoutForceY -= repulsiveForce * dy / d; |
| } |
| }, |
| |
| layoutAttractive: function(edge) { |
| var node1 = edge.source; |
| var node2 = edge.target; |
| |
| var dx = node2.layoutPosX - node1.layoutPosX; |
| var dy = node2.layoutPosY - node1.layoutPosY; |
| var d2 = dx * dx + dy * dy; |
| if(d2 < 0.01) { |
| dx = 0.1 * Math.random() + 0.1; |
| dy = 0.1 * Math.random() + 0.1; |
| var d2 = dx * dx + dy * dy; |
| } |
| var d = Math.sqrt(d2); |
| if(d > this.maxRepulsiveForceDistance) { |
| d = this.maxRepulsiveForceDistance; |
| d2 = d * d; |
| } |
| var attractiveForce = (d2 - this.k * this.k) / this.k; |
| if(edge.weight == undefined || edge.weight < 1) edge.weight = 1; |
| attractiveForce *= Math.log(edge.weight) * 0.5 + 1; |
| |
| node2.layoutForceX -= attractiveForce * dx / d; |
| node2.layoutForceY -= attractiveForce * dy / d; |
| node1.layoutForceX += attractiveForce * dx / d; |
| node1.layoutForceY += attractiveForce * dy / d; |
| } |
| }; |