| /* |
| * Dracula Graph Layout and Drawing Framework 0.0.3alpha |
| * (c) 2010 Philipp Strathausen <strathausen@gmail.com>, http://strathausen.eu |
| * Contributions by Jake Stothard <stothardj@gmail.com>. |
| * |
| * based on the 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 <paul@jibble.org>. |
| * |
| * This code is freely distributable under the MIT license. Commercial use is |
| * hereby granted without any cost or restriction. |
| * |
| * Links: |
| * |
| * Graph Dracula JavaScript Framework: |
| * http://graphdracula.net |
| * |
| /*--------------------------------------------------------------------------*/ |
| |
| /* |
| * Edge Factory |
| */ |
| var AbstractEdge = function() { |
| } |
| AbstractEdge.prototype = { |
| hide: function() { |
| this.connection.fg.hide(); |
| this.connection.bg && this.bg.connection.hide(); |
| } |
| }; |
| var EdgeFactory = function() { |
| this.template = new AbstractEdge(); |
| this.template.style = new Object(); |
| this.template.style.directed = false; |
| this.template.weight = 1; |
| }; |
| EdgeFactory.prototype = { |
| build: function(source, target) { |
| var e = jQuery.extend(true, {}, this.template); |
| e.source = source; |
| e.target = target; |
| return e; |
| } |
| }; |
| |
| /* |
| * Graph |
| */ |
| var Graph = function() { |
| this.nodes = {}; |
| this.edges = []; |
| this.snapshots = []; // previous graph states TODO to be implemented |
| this.edgeFactory = new EdgeFactory(); |
| }; |
| Graph.prototype = { |
| /* |
| * add a node |
| * @id the node's ID (string or number) |
| * @content (optional, dictionary) can contain any information that is |
| * being interpreted by the layout algorithm or the graph |
| * representation |
| */ |
| addNode: function(id, content) { |
| /* testing if node is already existing in the graph */ |
| if(this.nodes[id] == undefined) { |
| this.nodes[id] = new Graph.Node(id, content); |
| } |
| return this.nodes[id]; |
| }, |
| |
| addEdge: function(source, target, style) { |
| var s = this.addNode(source); |
| var t = this.addNode(target); |
| var edge = this.edgeFactory.build(s, t); |
| jQuery.extend(edge.style,style); |
| s.edges.push(edge); |
| this.edges.push(edge); |
| // NOTE: Even directed edges are added to both nodes. |
| t.edges.push(edge); |
| }, |
| |
| /* TODO to be implemented |
| * Preserve a copy of the graph state (nodes, positions, ...) |
| * @comment a comment describing the state |
| */ |
| snapShot: function(comment) { |
| /* FIXME |
| var graph = new Graph(); |
| graph.nodes = jQuery.extend(true, {}, this.nodes); |
| graph.edges = jQuery.extend(true, {}, this.edges); |
| this.snapshots.push({comment: comment, graph: graph}); |
| */ |
| }, |
| removeNode: function(id) { |
| delete this.nodes[id]; |
| for(var i = 0; i < this.edges.length; i++) { |
| if (this.edges[i].source.id == id || this.edges[i].target.id == id) { |
| this.edges.splice(i, 1); |
| i--; |
| } |
| } |
| } |
| }; |
| |
| /* |
| * Node |
| */ |
| Graph.Node = function(id, node){ |
| node = node || {}; |
| node.id = id; |
| node.edges = []; |
| node.hide = function() { |
| this.hidden = true; |
| this.shape && this.shape.hide(); /* FIXME this is representation specific code and should be elsewhere */ |
| for(i in this.edges) |
| (this.edges[i].source.id == id || this.edges[i].target == id) && this.edges[i].hide && this.edges[i].hide(); |
| }; |
| node.show = function() { |
| this.hidden = false; |
| this.shape && this.shape.show(); |
| for(i in this.edges) |
| (this.edges[i].source.id == id || this.edges[i].target == id) && this.edges[i].show && this.edges[i].show(); |
| }; |
| return node; |
| }; |
| Graph.Node.prototype = { |
| }; |
| |
| /* |
| * Renderer base class |
| */ |
| Graph.Renderer = {}; |
| |
| /* |
| * Renderer implementation using RaphaelJS |
| */ |
| 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; |
| |
| /* TODO default node rendering function */ |
| if(!this.graph.render) { |
| this.graph.render = function() { |
| return; |
| } |
| } |
| |
| /* |
| * Dragging |
| */ |
| this.isDrag = false; |
| this.dragger = function (e) { |
| this.dx = e.clientX; |
| this.dy = e.clientY; |
| selfRef.isDrag = this; |
| this.set && this.set.animate({"fill-opacity": .1}, 200) && this.set.toFront(); |
| e.preventDefault && e.preventDefault(); |
| }; |
| |
| var d = document.getElementById(element); |
| d.onmousemove = function (e) { |
| e = e || window.event; |
| if (selfRef.isDrag) { |
| var bBox = selfRef.isDrag.set.getBBox(); |
| // TODO round the coordinates here (eg. for proper image representation) |
| var newX = e.clientX - selfRef.isDrag.dx + (bBox.x + bBox.width / 2); |
| var newY = e.clientY - selfRef.isDrag.dy + (bBox.y + bBox.height / 2); |
| /* 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.set.translate(clientX - Math.round(selfRef.isDrag.dx), clientY - Math.round(selfRef.isDrag.dy)); |
| // console.log(clientX - Math.round(selfRef.isDrag.dx), clientY - Math.round(selfRef.isDrag.dy)); |
| for (var i in selfRef.graph.edges) { |
| selfRef.graph.edges[i].connection && selfRef.graph.edges[i].connection.draw(); |
| } |
| //selfRef.r.safari(); |
| selfRef.isDrag.dx = clientX; |
| selfRef.isDrag.dy = clientY; |
| } |
| }; |
| d.onmouseup = function () { |
| selfRef.isDrag && selfRef.isDrag.set.animate({"fill-opacity": .6}, 500); |
| selfRef.isDrag = false; |
| }; |
| this.draw(); |
| }; |
| 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 = (this.width - 2 * this.radius) / (this.graph.layoutMaxX - this.graph.layoutMinX); |
| this.factorY = (this.height - 2 * this.radius) / (this.graph.layoutMaxY - this.graph.layoutMinY); |
| for (i in this.graph.nodes) { |
| 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) { |
| var oBBox = node.shape.getBBox(); |
| var opoint = { x: oBBox.x + oBBox.width / 2, y: oBBox.y + oBBox.height / 2}; |
| node.shape.translate(Math.round(point[0] - opoint.x), Math.round(point[1] - opoint.y)); |
| this.r.safari(); |
| return node; |
| }/* else, draw new nodes */ |
| |
| var shape; |
| |
| /* if a node renderer function is provided by the user, then use it |
| or the default render function instead */ |
| if(!node.render) { |
| node.render = function(r, node) { |
| /* the default node drawing */ |
| var color = Raphael.getColor(); |
| var ellipse = r.ellipse(0, 0, 30, 20).attr({fill: color, stroke: color, "stroke-width": 2}); |
| /* set DOM node ID */ |
| ellipse.node.id = node.label || node.id; |
| shape = r.set(). |
| push(ellipse). |
| push(r.text(0, 30, node.label || node.id)); |
| return shape; |
| } |
| } |
| /* or check for an ajax representation of the nodes */ |
| if(node.shapes) { |
| // TODO ajax representation evaluation |
| } |
| |
| shape = node.render(this.r, node).hide(); |
| |
| shape.attr({"fill-opacity": .6}); |
| /* re-reference to the node an element belongs to, needed for dragging all elements of a node */ |
| shape.items.forEach(function(item){ item.set = shape; item.node.style.cursor = "move"; }); |
| shape.mousedown(this.dragger); |
| |
| var box = shape.getBBox(); |
| shape.translate(Math.round(point[0]-(box.x+box.width/2)),Math.round(point[1]-(box.y+box.height/2))) |
| //console.log(box,point); |
| node.hidden || shape.show(); |
| node.shape = shape; |
| }, |
| drawEdge: function(edge) { |
| /* if this edge already exists the other way around and is undirected */ |
| if(edge.backedge) |
| return; |
| if(edge.source.hidden || edge.target.hidden) { |
| edge.connection && edge.connection.fg.hide() | edge.connection.bg && edge.connection.bg.hide(); |
| return; |
| } |
| /* if edge already has been drawn, only refresh the edge */ |
| if(!edge.connection) { |
| edge.style && edge.style.callback && edge.style.callback(edge); // TODO move this somewhere else |
| edge.connection = this.r.connection(edge.source.shape, edge.target.shape, edge.style); |
| return; |
| } |
| //FIXME showing doesn't work well |
| edge.connection.fg.show(); |
| edge.connection.bg && edge.connection.bg.show(); |
| edge.connection.draw(); |
| } |
| }; |
| 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; |
| this.layout(); |
| }; |
| Graph.Layout.Spring.prototype = { |
| layout: function() { |
| this.layoutPrepare(); |
| for (var i = 0; i < this.iterations; i++) { |
| this.layoutIteration(); |
| } |
| this.layoutCalcBounds(); |
| }, |
| |
| layoutPrepare: function() { |
| for (i in this.graph.nodes) { |
| 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 (i in this.graph.nodes) { |
| 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 |
| |
| var prev = new Array(); |
| for(var c in this.graph.nodes) { |
| var node1 = this.graph.nodes[c]; |
| for (var d in prev) { |
| var node2 = this.graph.nodes[prev[d]]; |
| this.layoutRepulsive(node1, node2); |
| |
| } |
| prev.push(c); |
| } |
| |
| // 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 (i in this.graph.nodes) { |
| 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) { |
| if (typeof node1 == 'undefined' || typeof node2 == 'undefined') |
| return; |
| 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.attraction == undefined) edge.attraction = 1; |
| attractiveForce *= Math.log(edge.attraction) * 0.5 + 1; |
| |
| node2.layoutForceX -= attractiveForce * dx / d; |
| node2.layoutForceY -= attractiveForce * dy / d; |
| node1.layoutForceX += attractiveForce * dx / d; |
| node1.layoutForceY += attractiveForce * dy / d; |
| } |
| }; |
| |
| Graph.Layout.Ordered = function(graph, order) { |
| this.graph = graph; |
| this.order = order; |
| this.layout(); |
| }; |
| Graph.Layout.Ordered.prototype = { |
| layout: function() { |
| this.layoutPrepare(); |
| this.layoutCalcBounds(); |
| }, |
| |
| layoutPrepare: function(order) { |
| for (i in this.graph.nodes) { |
| var node = this.graph.nodes[i]; |
| node.layoutPosX = 0; |
| node.layoutPosY = 0; |
| } |
| var counter = 0; |
| for (i in this.order) { |
| var node = this.order[i]; |
| node.layoutPosX = counter; |
| node.layoutPosY = Math.random(); |
| counter++; |
| } |
| }, |
| |
| layoutCalcBounds: function() { |
| var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity; |
| |
| for (i in this.graph.nodes) { |
| 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; |
| } |
| }; |
| |
| /* |
| * usefull JavaScript extensions, |
| */ |
| |
| function log(a) {console.log&&console.log(a);} |
| |
| /* |
| * Raphael Tooltip Plugin |
| * - attaches an element as a tooltip to another element |
| * |
| * Usage example, adding a rectangle as a tooltip to a circle: |
| * |
| * paper.circle(100,100,10).tooltip(paper.rect(0,0,20,30)); |
| * |
| * If you want to use more shapes, you'll have to put them into a set. |
| * |
| */ |
| Raphael.el.tooltip = function (tp) { |
| this.tp = tp; |
| this.tp.o = {x: 0, y: 0}; |
| this.tp.hide(); |
| this.hover( |
| function(event){ |
| this.mousemove(function(event){ |
| this.tp.translate(event.clientX - |
| this.tp.o.x,event.clientY - this.tp.o.y); |
| this.tp.o = {x: event.clientX, y: event.clientY}; |
| }); |
| this.tp.show().toFront(); |
| }, |
| function(event){ |
| this.tp.hide(); |
| this.unmousemove(); |
| }); |
| return this; |
| }; |
| |
| /* For IE */ |
| if (!Array.prototype.forEach) |
| { |
| Array.prototype.forEach = function(fun /*, thisp*/) |
| { |
| var len = this.length; |
| if (typeof fun != "function") |
| throw new TypeError(); |
| |
| var thisp = arguments[1]; |
| for (var i = 0; i < len; i++) |
| { |
| if (i in this) |
| fun.call(thisp, this[i], i, this); |
| } |
| }; |
| } |