graph.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. /* Graph JavaScript framework, version 0.0.1
  2. * (c) 2006 Aslak Hellesoy <aslak.hellesoy@gmail.com>
  3. * (c) 2006 Dave Hoover <dave.hoover@gmail.com>
  4. *
  5. * Ported from Graph::Layouter::Spring in
  6. * http://search.cpan.org/~pasky/Graph-Layderer-0.02/
  7. * The algorithm is based on a spring-style layouter of a Java-based social
  8. * network tracker PieSpy written by Paul Mutton E<lt>paul@jibble.orgE<gt>.
  9. *
  10. * Adopted by Philipp Strathausen <strathausen@gmail.com> to support Raphael JS
  11. * for rendering, dragging and much more. See http://blog.ameisenbar.de
  12. *
  13. * Graph is freely distributable under the terms of an MIT-style license.
  14. * For details, see the Graph web site: http://dev.buildpatternd.com/trac
  15. *
  16. * Links:
  17. *
  18. * Demo of the original applet:
  19. * http://redsquirrel.com/dave/work/webdep/
  20. *
  21. * Mirrored original source code at snipplr:
  22. * http://snipplr.com/view/1950/graph-javascript-framework-version-001/
  23. *
  24. * Original usage example:
  25. * http://ajaxian.com/archives/new-javascriptcanvas-graph-library
  26. *
  27. /*--------------------------------------------------------------------------*/
  28. /*
  29. * Graph
  30. */
  31. var Graph = function() {
  32. this.nodes = [];
  33. this.edges = [];
  34. };
  35. Graph.prototype = {
  36. addNode: function(id, content) {
  37. /* testing if node is already existing in the graph */
  38. var new_node = this.nodes[id];
  39. if(new_node == undefined) {
  40. new_node = new Graph.Node(id, content||{"id":id});
  41. this.nodes[id] = new_node;
  42. this.nodes.push(new_node); // TODO get rid of the array
  43. }
  44. return new_node;
  45. },
  46. addEdge: function(source, target, style) {
  47. var s = this.addNode(source);
  48. var t = this.addNode(target);
  49. var color;
  50. var colorbg;
  51. var directed;
  52. if(style) { color = style.color; colorbg = style.colorbg; directed = style.directed }
  53. var edge = { source: s, target: t, color: color, colorbg: colorbg, directed: directed };
  54. this.edges.push(edge);
  55. }
  56. };
  57. /*
  58. * Node
  59. */
  60. Graph.Node = function(id, value){
  61. this.id = id;
  62. this.content = value;
  63. };
  64. Graph.Node.prototype = {
  65. };
  66. Graph.Renderer = {};
  67. Graph.Renderer.Raphael = function(element, graph, width, height) {
  68. this.width = width||400;
  69. this.height = height||400;
  70. var selfRef = this;
  71. this.r = Raphael(element, this.width, this.height);
  72. this.radius = 40; /* max dimension of a node */
  73. this.graph = graph;
  74. this.mouse_in = false;
  75. /*
  76. * Dragging
  77. */
  78. this.isDrag = false;
  79. this.dragger = function (e) {
  80. this.dx = e.clientX;
  81. this.dy = e.clientY;
  82. selfRef.isDrag = this;
  83. this.animate({"fill-opacity": .2}, 500);
  84. e.preventDefault && e.preventDefault();
  85. };
  86. document.onmousemove = function (e) {
  87. e = e || window.event;
  88. if (selfRef.isDrag) {
  89. 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);
  90. 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);
  91. /* prevent shapes from being dragged out of the canvas */
  92. var clientX = e.clientX - (newX < 20 ? newX - 20 : newX > selfRef.width - 20 ? newX - selfRef.width + 20 : 0);
  93. var clientY = e.clientY - (newY < 20 ? newY - 20 : newY > selfRef.height - 20 ? newY - selfRef.height + 20 : 0);
  94. selfRef.isDrag.translate(clientX - selfRef.isDrag.dx, clientY - selfRef.isDrag.dy);
  95. selfRef.isDrag.label.translate(clientX - selfRef.isDrag.dx, clientY - selfRef.isDrag.dy);
  96. for (var i in selfRef.graph.edges) {
  97. selfRef.graph.edges[i].connection.draw();
  98. }
  99. //selfRef.r.safari();
  100. selfRef.isDrag.dx = clientX;
  101. selfRef.isDrag.dy = clientY;
  102. }
  103. };
  104. document.onmouseup = function () {
  105. selfRef.isDrag && selfRef.isDrag.animate({"fill-opacity": 0}, 500);
  106. selfRef.isDrag = false;
  107. };
  108. };
  109. /*
  110. * Renderer using RaphaelJS
  111. */
  112. Graph.Renderer.Raphael.prototype = {
  113. translate: function(point) {
  114. return [
  115. (point[0] - this.graph.layoutMinX) * this.factorX + this.radius,
  116. (point[1] - this.graph.layoutMinY) * this.factorY + this.radius
  117. ];
  118. },
  119. rotate: function(point, length, angle) {
  120. var dx = length * Math.cos(angle);
  121. var dy = length * Math.sin(angle);
  122. return [point[0]+dx, point[1]+dy];
  123. },
  124. draw: function() {
  125. this.factorX = (width - 2 * this.radius) / (this.graph.layoutMaxX - this.graph.layoutMinX);
  126. this.factorY = (height - 2 * this.radius) / (this.graph.layoutMaxY - this.graph.layoutMinY);
  127. for (var i = 0; i < this.graph.nodes.length; i++) {
  128. this.drawNode(this.graph.nodes[i]);
  129. }
  130. for (var i = 0; i < this.graph.edges.length; i++) {
  131. this.drawEdge(this.graph.edges[i]);
  132. }
  133. },
  134. drawNode: function(node) {
  135. var point = this.translate([node.layoutPosX, node.layoutPosY]);
  136. node.point = point;
  137. /* if node has already been drawn, move the nodes */
  138. if(node.shape) {
  139. // console.log(node.shape.attrs );
  140. 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 ];
  141. node.shape.translate(point[0]-opoint[0], point[1]-opoint[1]);
  142. node.shape.label.translate(point[0]-opoint[0], point[1]-opoint[1]);
  143. this.r.safari();
  144. return;
  145. }
  146. var shape;
  147. if(node.content.getShape) {
  148. shape = node.content.getShape(this.r, point[0], point[1]);
  149. shape.attr({"fill-opacity": 0});
  150. } else {
  151. shape = this.r.ellipse(point[0], point[1], 30, 20);
  152. var color = Raphael.getColor();
  153. shape.attr({fill: color, stroke: color, "fill-opacity": 0, "stroke-width": 2})
  154. }
  155. shape.mousedown(this.dragger);
  156. shape.node.style.cursor = "move";
  157. shape.label = this.r.text(point[0], point[1] + 30, node.content.label || node.id); // Beware: operator || also considers values like -1, 0, ...
  158. node.shape = shape;
  159. },
  160. drawEdge: function(edge) {
  161. /* if edge already has been drawn, only refresh the edge */
  162. edge.connection && edge.connection.draw();
  163. if(!edge.connection)
  164. edge.connection = this.r.connection(edge.source.shape, edge.target.shape, { fg: edge.color, bg: edge.colorbg, directed: edge.directed });
  165. }
  166. };
  167. Graph.Layout = {};
  168. Graph.Layout.Spring = function(graph) {
  169. this.graph = graph;
  170. this.iterations = 500;
  171. this.maxRepulsiveForceDistance = 6;
  172. this.k = 2;
  173. this.c = 0.01;
  174. this.maxVertexMovement = 0.5;
  175. };
  176. Graph.Layout.Spring.prototype = {
  177. layout: function() {
  178. this.layoutPrepare();
  179. for (var i = 0; i < this.iterations; i++) {
  180. this.layoutIteration();
  181. }
  182. this.layoutCalcBounds();
  183. },
  184. layoutPrepare: function() {
  185. for (var i = 0; i < this.graph.nodes.length; i++) {
  186. var node = this.graph.nodes[i];
  187. node.layoutPosX = 0;
  188. node.layoutPosY = 0;
  189. node.layoutForceX = 0;
  190. node.layoutForceY = 0;
  191. }
  192. },
  193. layoutCalcBounds: function() {
  194. var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity;
  195. for (var i = 0; i < this.graph.nodes.length; i++) {
  196. var x = this.graph.nodes[i].layoutPosX;
  197. var y = this.graph.nodes[i].layoutPosY;
  198. if(x > maxx) maxx = x;
  199. if(x < minx) minx = x;
  200. if(y > maxy) maxy = y;
  201. if(y < miny) miny = y;
  202. }
  203. this.graph.layoutMinX = minx;
  204. this.graph.layoutMaxX = maxx;
  205. this.graph.layoutMinY = miny;
  206. this.graph.layoutMaxY = maxy;
  207. },
  208. layoutIteration: function() {
  209. // Forces on nodes due to node-node repulsions
  210. for (var i = 0; i < this.graph.nodes.length; i++) {
  211. var node1 = this.graph.nodes[i];
  212. for (var j = i + 1; j < this.graph.nodes.length; j++) {
  213. var node2 = this.graph.nodes[j];
  214. this.layoutRepulsive(node1, node2);
  215. }
  216. }
  217. // Forces on nodes due to edge attractions
  218. for (var i = 0; i < this.graph.edges.length; i++) {
  219. var edge = this.graph.edges[i];
  220. this.layoutAttractive(edge);
  221. }
  222. // Move by the given force
  223. for (var i = 0; i < this.graph.nodes.length; i++) {
  224. var node = this.graph.nodes[i];
  225. var xmove = this.c * node.layoutForceX;
  226. var ymove = this.c * node.layoutForceY;
  227. var max = this.maxVertexMovement;
  228. if(xmove > max) xmove = max;
  229. if(xmove < -max) xmove = -max;
  230. if(ymove > max) ymove = max;
  231. if(ymove < -max) ymove = -max;
  232. node.layoutPosX += xmove;
  233. node.layoutPosY += ymove;
  234. node.layoutForceX = 0;
  235. node.layoutForceY = 0;
  236. }
  237. },
  238. layoutRepulsive: function(node1, node2) {
  239. var dx = node2.layoutPosX - node1.layoutPosX;
  240. var dy = node2.layoutPosY - node1.layoutPosY;
  241. var d2 = dx * dx + dy * dy;
  242. if(d2 < 0.01) {
  243. dx = 0.1 * Math.random() + 0.1;
  244. dy = 0.1 * Math.random() + 0.1;
  245. var d2 = dx * dx + dy * dy;
  246. }
  247. var d = Math.sqrt(d2);
  248. if(d < this.maxRepulsiveForceDistance) {
  249. var repulsiveForce = this.k * this.k / d;
  250. node2.layoutForceX += repulsiveForce * dx / d;
  251. node2.layoutForceY += repulsiveForce * dy / d;
  252. node1.layoutForceX -= repulsiveForce * dx / d;
  253. node1.layoutForceY -= repulsiveForce * dy / d;
  254. }
  255. },
  256. layoutAttractive: function(edge) {
  257. var node1 = edge.source;
  258. var node2 = edge.target;
  259. var dx = node2.layoutPosX - node1.layoutPosX;
  260. var dy = node2.layoutPosY - node1.layoutPosY;
  261. var d2 = dx * dx + dy * dy;
  262. if(d2 < 0.01) {
  263. dx = 0.1 * Math.random() + 0.1;
  264. dy = 0.1 * Math.random() + 0.1;
  265. var d2 = dx * dx + dy * dy;
  266. }
  267. var d = Math.sqrt(d2);
  268. if(d > this.maxRepulsiveForceDistance) {
  269. d = this.maxRepulsiveForceDistance;
  270. d2 = d * d;
  271. }
  272. var attractiveForce = (d2 - this.k * this.k) / this.k;
  273. if(edge.weight == undefined || edge.weight < 1) edge.weight = 1;
  274. attractiveForce *= Math.log(edge.weight) * 0.5 + 1;
  275. node2.layoutForceX -= attractiveForce * dx / d;
  276. node2.layoutForceY -= attractiveForce * dy / d;
  277. node1.layoutForceX += attractiveForce * dx / d;
  278. node1.layoutForceY += attractiveForce * dy / d;
  279. }
  280. };