/* * Dracula Graph Layout and Drawing Framework 0.0.3alpha * (c) 2010 Philipp Strathausen , http://strathausen.eu * Contributions by Jake Stothard . * * based on the Graph JavaScript framework, version 0.0.1 * (c) 2006 Aslak Hellesoy * (c) 2006 Dave Hoover * * 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 . * * 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); } }; }