gameplan 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -1
- data/lib/gameplan/frontend/public/js/Curry-1.0.1.js +29 -0
- data/lib/gameplan/frontend/public/js/app.js +112 -0
- data/lib/gameplan/frontend/public/js/dracula_algorithms.js +616 -0
- data/lib/gameplan/frontend/public/js/dracula_graffle.js +127 -0
- data/lib/gameplan/frontend/public/js/dracula_graph.js +667 -0
- data/lib/gameplan/frontend/public/js/jquery.js +4 -0
- data/lib/gameplan/frontend/public/js/raphael-min.js +10 -0
- data/lib/gameplan/frontend/public/js/raphael.js +10 -0
- data/lib/gameplan/frontend/public/js/seedrandom.js +266 -0
- data/lib/gameplan/frontend/public/js/underscore.js +999 -0
- data/lib/gameplan/frontend/views/app.erb +20 -0
- data/lib/gameplan/frontend/views/layout/application.erb +8 -4
- data/lib/gameplan/frontend/views/state.erb +1 -1
- metadata +14 -4
@@ -0,0 +1,127 @@
|
|
1
|
+
/**
|
2
|
+
* Originally grabbed from the official RaphaelJS Documentation
|
3
|
+
* http://raphaeljs.com/graffle.html
|
4
|
+
* Adopted (arrows) and commented by Philipp Strathausen http://blog.ameisenbar.de
|
5
|
+
* Licenced under the MIT licence.
|
6
|
+
*/
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Usage:
|
10
|
+
* connect two shapes
|
11
|
+
* parameters:
|
12
|
+
* source shape [or connection for redrawing],
|
13
|
+
* target shape,
|
14
|
+
* style with { fg : linecolor, bg : background color, directed: boolean }
|
15
|
+
* returns:
|
16
|
+
* connection { draw = function() }
|
17
|
+
*/
|
18
|
+
Raphael.fn.connection = function Connection(obj1, obj2, style) {
|
19
|
+
var selfRef = this;
|
20
|
+
/* create and return new connection */
|
21
|
+
var edge = {/*
|
22
|
+
from : obj1,
|
23
|
+
to : obj2,
|
24
|
+
style : style,*/
|
25
|
+
draw : function() {
|
26
|
+
/* get bounding boxes of target and source */
|
27
|
+
var bb1 = obj1.getBBox();
|
28
|
+
var bb2 = obj2.getBBox();
|
29
|
+
var off1 = 0;
|
30
|
+
var off2 = 0;
|
31
|
+
/* coordinates for potential connection coordinates from/to the objects */
|
32
|
+
var p = [
|
33
|
+
/* NORTH 1 */
|
34
|
+
{ x: bb1.x + bb1.width / 2, y: bb1.y - off1 },
|
35
|
+
/* SOUTH 1 */
|
36
|
+
{ x: bb1.x + bb1.width / 2, y: bb1.y + bb1.height + off1 },
|
37
|
+
/* WEST */
|
38
|
+
{ x: bb1.x - off1, y: bb1.y + bb1.height / 2 },
|
39
|
+
/* EAST 1 */
|
40
|
+
{ x: bb1.x + bb1.width + off1, y: bb1.y + bb1.height / 2 },
|
41
|
+
/* NORTH 2 */
|
42
|
+
{ x: bb2.x + bb2.width / 2, y: bb2.y - off2 },
|
43
|
+
/* SOUTH 2 */
|
44
|
+
{ x: bb2.x + bb2.width / 2, y: bb2.y + bb2.height + off2 },
|
45
|
+
/* WEST 2 */
|
46
|
+
{ x: bb2.x - off2, y: bb2.y + bb2.height / 2 },
|
47
|
+
/* EAST 2 */
|
48
|
+
{ x: bb2.x + bb2.width + off2, y: bb2.y + bb2.height / 2 }
|
49
|
+
];
|
50
|
+
|
51
|
+
/* distances between objects and according coordinates connection */
|
52
|
+
var d = {}, dis = [];
|
53
|
+
|
54
|
+
/*
|
55
|
+
* find out the best connection coordinates by trying all possible ways
|
56
|
+
*/
|
57
|
+
/* loop the first object's connection coordinates */
|
58
|
+
for (var i = 0; i < 4; i++) {
|
59
|
+
/* loop the seond object's connection coordinates */
|
60
|
+
for (var j = 4; j < 8; j++) {
|
61
|
+
var dx = Math.abs(p[i].x - p[j].x);
|
62
|
+
var dy = Math.abs(p[i].y - p[j].y);
|
63
|
+
if ((i == j - 4) || (((i != 3 && j != 6) || p[i].x < p[j].x)
|
64
|
+
&& ((i != 2 && j != 7) || p[i].x > p[j].x)
|
65
|
+
&& ((i != 0 && j != 5) || p[i].y > p[j].y)
|
66
|
+
&& ((i != 1 && j != 4) || p[i].y < p[j].y)))
|
67
|
+
{
|
68
|
+
dis.push(dx + dy);
|
69
|
+
d[dis[dis.length - 1].toFixed(3)] = [i, j];
|
70
|
+
}
|
71
|
+
}
|
72
|
+
}
|
73
|
+
var res = dis.length == 0 ? [0, 4] : d[Math.min.apply(Math, dis).toFixed(3)];
|
74
|
+
/* bezier path */
|
75
|
+
var x1 = p[res[0]].x,
|
76
|
+
y1 = p[res[0]].y,
|
77
|
+
x4 = p[res[1]].x,
|
78
|
+
y4 = p[res[1]].y,
|
79
|
+
dx = Math.max(Math.abs(x1 - x4) / 2, 10),
|
80
|
+
dy = Math.max(Math.abs(y1 - y4) / 2, 10),
|
81
|
+
x2 = [ x1, x1, x1 - dx, x1 + dx ][res[0]].toFixed(3),
|
82
|
+
y2 = [ y1 - dy, y1 + dy, y1, y1 ][res[0]].toFixed(3),
|
83
|
+
x3 = [ 0, 0, 0, 0, x4, x4, x4 - dx, x4 + dx ][res[1]].toFixed(3),
|
84
|
+
y3 = [ 0, 0, 0, 0, y1 + dy, y1 - dy, y4, y4 ][res[1]].toFixed(3);
|
85
|
+
/* assemble path and arrow */
|
86
|
+
var path = [ "M" + x1.toFixed(3), y1.toFixed(3),
|
87
|
+
"C" + x2, y2, x3, y3, x4.toFixed(3), y4.toFixed(3) ].join(",");
|
88
|
+
/* arrow */
|
89
|
+
if(style && style.directed) {
|
90
|
+
// magnitude, length of the last path vector
|
91
|
+
var mag = Math.sqrt((y4 - y3) * (y4 - y3) + (x4 - x3) * (x4 - x3));
|
92
|
+
// vector normalisation to specified length
|
93
|
+
var norm = function(x,l){return (-x*(l||5)/mag);};
|
94
|
+
// calculate array coordinates (two lines orthogonal to the path vector)
|
95
|
+
var arr = [
|
96
|
+
{ x:(norm(x4-x3)+norm(y4-y3)+x4).toFixed(3),
|
97
|
+
y:(norm(y4-y3)+norm(x4-x3)+y4).toFixed(3) },
|
98
|
+
{ x:(norm(x4-x3)-norm(y4-y3)+x4).toFixed(3),
|
99
|
+
y:(norm(y4-y3)-norm(x4-x3)+y4).toFixed(3) }
|
100
|
+
];
|
101
|
+
path = path + ",M"+arr[0].x+","+arr[0].y+",L"+x4+","+y4+",L"+arr[1].x+","+arr[1].y;
|
102
|
+
}
|
103
|
+
/* function to be used for moving existent path(s), e.g. animate() or attr() */
|
104
|
+
var move = "attr";
|
105
|
+
/* applying path(s) */
|
106
|
+
edge.fg && edge.fg[move]({path:path})
|
107
|
+
|| (edge.fg = selfRef.path(path)
|
108
|
+
.attr({ stroke: style && style.stroke || "#000", fill: "none" })
|
109
|
+
.toBack());
|
110
|
+
edge.bg && edge.bg[move]({path:path})
|
111
|
+
|| style && style.fill && (edge.bg = style.fill.split
|
112
|
+
&& selfRef.path(path)
|
113
|
+
.attr({ stroke: style.fill.split("|")[0], fill: "none",
|
114
|
+
"stroke-width": style.fill.split("|")[1] || 3 }).toBack());
|
115
|
+
/* setting label */
|
116
|
+
style && style.label
|
117
|
+
&& (edge.label && edge.label.attr({x:(x1+x4)/2, y:(y1+y4)/2})
|
118
|
+
|| (edge.label = selfRef.text((x1+x4)/2, (y1+y4)/2, style.label)
|
119
|
+
.attr({fill: "#000", "font-size": style["font-size"] || "12px"})));
|
120
|
+
style && style.label && style["label-style"] && edge.label
|
121
|
+
&& edge.label.attr(style["label-style"]);
|
122
|
+
style && style.callback && style.callback(edge);
|
123
|
+
}
|
124
|
+
}
|
125
|
+
edge.draw();
|
126
|
+
return edge;
|
127
|
+
};
|
@@ -0,0 +1,667 @@
|
|
1
|
+
/*
|
2
|
+
* Dracula Graph Layout and Drawing Framework 0.0.3alpha
|
3
|
+
* (c) 2010 Philipp Strathausen <strathausen@gmail.com>, http://strathausen.eu
|
4
|
+
* Contributions by Jake Stothard <stothardj@gmail.com>.
|
5
|
+
*
|
6
|
+
* based on the Graph JavaScript framework, version 0.0.1
|
7
|
+
* (c) 2006 Aslak Hellesoy <aslak.hellesoy@gmail.com>
|
8
|
+
* (c) 2006 Dave Hoover <dave.hoover@gmail.com>
|
9
|
+
*
|
10
|
+
* Ported from Graph::Layouter::Spring in
|
11
|
+
* http://search.cpan.org/~pasky/Graph-Layderer-0.02/
|
12
|
+
* The algorithm is based on a spring-style layouter of a Java-based social
|
13
|
+
* network tracker PieSpy written by Paul Mutton <paul@jibble.org>.
|
14
|
+
*
|
15
|
+
* This code is freely distributable under the MIT license. Commercial use is
|
16
|
+
* hereby granted without any cost or restriction.
|
17
|
+
*
|
18
|
+
* Links:
|
19
|
+
*
|
20
|
+
* Graph Dracula JavaScript Framework:
|
21
|
+
* http://graphdracula.net
|
22
|
+
*
|
23
|
+
/*--------------------------------------------------------------------------*/
|
24
|
+
|
25
|
+
/*
|
26
|
+
* Edge Factory
|
27
|
+
*/
|
28
|
+
var AbstractEdge = function() {
|
29
|
+
}
|
30
|
+
AbstractEdge.prototype = {
|
31
|
+
hide: function() {
|
32
|
+
this.connection.fg.hide();
|
33
|
+
this.connection.bg && this.bg.connection.hide();
|
34
|
+
}
|
35
|
+
};
|
36
|
+
var EdgeFactory = function() {
|
37
|
+
this.template = new AbstractEdge();
|
38
|
+
this.template.style = new Object();
|
39
|
+
this.template.style.directed = false;
|
40
|
+
this.template.weight = 1;
|
41
|
+
};
|
42
|
+
EdgeFactory.prototype = {
|
43
|
+
build: function(source, target) {
|
44
|
+
var e = jQuery.extend(true, {}, this.template);
|
45
|
+
e.source = source;
|
46
|
+
e.target = target;
|
47
|
+
return e;
|
48
|
+
}
|
49
|
+
};
|
50
|
+
|
51
|
+
/*
|
52
|
+
* Graph
|
53
|
+
*/
|
54
|
+
var Graph = function() {
|
55
|
+
this.nodes = {};
|
56
|
+
this.edges = [];
|
57
|
+
this.snapshots = []; // previous graph states TODO to be implemented
|
58
|
+
this.edgeFactory = new EdgeFactory();
|
59
|
+
};
|
60
|
+
Graph.prototype = {
|
61
|
+
/*
|
62
|
+
* add a node
|
63
|
+
* @id the node's ID (string or number)
|
64
|
+
* @content (optional, dictionary) can contain any information that is
|
65
|
+
* being interpreted by the layout algorithm or the graph
|
66
|
+
* representation
|
67
|
+
*/
|
68
|
+
addNode: function(id, content) {
|
69
|
+
/* testing if node is already existing in the graph */
|
70
|
+
if(this.nodes[id] == undefined) {
|
71
|
+
this.nodes[id] = new Graph.Node(id, content);
|
72
|
+
}
|
73
|
+
return this.nodes[id];
|
74
|
+
},
|
75
|
+
|
76
|
+
addEdge: function(source, target, style) {
|
77
|
+
var s = this.addNode(source);
|
78
|
+
var t = this.addNode(target);
|
79
|
+
var edge = this.edgeFactory.build(s, t);
|
80
|
+
jQuery.extend(true, edge.style, style);
|
81
|
+
s.edges.push(edge);
|
82
|
+
this.edges.push(edge);
|
83
|
+
// NOTE: Even directed edges are added to both nodes.
|
84
|
+
t.edges.push(edge);
|
85
|
+
},
|
86
|
+
|
87
|
+
/* TODO to be implemented
|
88
|
+
* Preserve a copy of the graph state (nodes, positions, ...)
|
89
|
+
* @comment a comment describing the state
|
90
|
+
*/
|
91
|
+
snapShot: function(comment) {
|
92
|
+
/* FIXME
|
93
|
+
var graph = new Graph();
|
94
|
+
graph.nodes = jQuery.extend(true, {}, this.nodes);
|
95
|
+
graph.edges = jQuery.extend(true, {}, this.edges);
|
96
|
+
this.snapshots.push({comment: comment, graph: graph});
|
97
|
+
*/
|
98
|
+
},
|
99
|
+
removeNode: function(id) {
|
100
|
+
delete this.nodes[id];
|
101
|
+
for(var i = 0; i < this.edges.length; i++) {
|
102
|
+
if (this.edges[i].source.id == id || this.edges[i].target.id == id) {
|
103
|
+
this.edges.splice(i, 1);
|
104
|
+
i--;
|
105
|
+
}
|
106
|
+
}
|
107
|
+
}
|
108
|
+
};
|
109
|
+
|
110
|
+
/*
|
111
|
+
* Node
|
112
|
+
*/
|
113
|
+
Graph.Node = function(id, node){
|
114
|
+
node = node || {};
|
115
|
+
node.id = id;
|
116
|
+
node.edges = [];
|
117
|
+
node.hide = function() {
|
118
|
+
this.hidden = true;
|
119
|
+
this.shape && this.shape.hide(); /* FIXME this is representation specific code and should be elsewhere */
|
120
|
+
for(i in this.edges)
|
121
|
+
(this.edges[i].source.id == id || this.edges[i].target == id) && this.edges[i].hide && this.edges[i].hide();
|
122
|
+
};
|
123
|
+
node.show = function() {
|
124
|
+
this.hidden = false;
|
125
|
+
this.shape && this.shape.show();
|
126
|
+
for(i in this.edges)
|
127
|
+
(this.edges[i].source.id == id || this.edges[i].target == id) && this.edges[i].show && this.edges[i].show();
|
128
|
+
};
|
129
|
+
return node;
|
130
|
+
};
|
131
|
+
Graph.Node.prototype = {
|
132
|
+
};
|
133
|
+
|
134
|
+
/*
|
135
|
+
* Renderer base class
|
136
|
+
*/
|
137
|
+
Graph.Renderer = {};
|
138
|
+
|
139
|
+
/*
|
140
|
+
* Renderer implementation using RaphaelJS
|
141
|
+
*/
|
142
|
+
Graph.Renderer.Raphael = function(element, graph, width, height) {
|
143
|
+
this.width = width || 400;
|
144
|
+
this.height = height || 400;
|
145
|
+
var selfRef = this;
|
146
|
+
this.r = Raphael(element, this.width, this.height);
|
147
|
+
this.radius = 40; /* max dimension of a node */
|
148
|
+
this.graph = graph;
|
149
|
+
this.mouse_in = false;
|
150
|
+
|
151
|
+
/* TODO default node rendering function */
|
152
|
+
if(!this.graph.render) {
|
153
|
+
this.graph.render = function() {
|
154
|
+
return;
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
/*
|
159
|
+
* Dragging
|
160
|
+
*/
|
161
|
+
this.isDrag = false;
|
162
|
+
this.dragger = function (e) {
|
163
|
+
this.dx = e.clientX;
|
164
|
+
this.dy = e.clientY;
|
165
|
+
selfRef.isDrag = this;
|
166
|
+
this.set && this.set.animate({"fill-opacity": .1}, 200);
|
167
|
+
e.preventDefault && e.preventDefault();
|
168
|
+
};
|
169
|
+
|
170
|
+
var d = document.getElementById(element);
|
171
|
+
d.onmousemove = function (e) {
|
172
|
+
e = e || window.event;
|
173
|
+
if (selfRef.isDrag) {
|
174
|
+
var bBox = selfRef.isDrag.set.getBBox();
|
175
|
+
// TODO round the coordinates here (eg. for proper image representation)
|
176
|
+
var newX = e.clientX - selfRef.isDrag.dx + (bBox.x + bBox.width / 2);
|
177
|
+
var newY = e.clientY - selfRef.isDrag.dy + (bBox.y + bBox.height / 2);
|
178
|
+
/* prevent shapes from being dragged out of the canvas */
|
179
|
+
var clientX = e.clientX - (newX < 20 ? newX - 20 : newX > selfRef.width - 20 ? newX - selfRef.width + 20 : 0);
|
180
|
+
var clientY = e.clientY - (newY < 20 ? newY - 20 : newY > selfRef.height - 20 ? newY - selfRef.height + 20 : 0);
|
181
|
+
selfRef.isDrag.set.translate(clientX - Math.round(selfRef.isDrag.dx), clientY - Math.round(selfRef.isDrag.dy));
|
182
|
+
// console.log(clientX - Math.round(selfRef.isDrag.dx), clientY - Math.round(selfRef.isDrag.dy));
|
183
|
+
for (var i in selfRef.graph.edges) {
|
184
|
+
selfRef.graph.edges[i] &&
|
185
|
+
selfRef.graph.edges[i].connection && selfRef.graph.edges[i].connection.draw();
|
186
|
+
}
|
187
|
+
//selfRef.r.safari();
|
188
|
+
selfRef.isDrag.dx = clientX;
|
189
|
+
selfRef.isDrag.dy = clientY;
|
190
|
+
}
|
191
|
+
};
|
192
|
+
d.onmouseup = function () {
|
193
|
+
selfRef.isDrag && selfRef.isDrag.set.animate({"fill-opacity": .6}, 500);
|
194
|
+
selfRef.isDrag = false;
|
195
|
+
};
|
196
|
+
this.draw();
|
197
|
+
};
|
198
|
+
|
199
|
+
|
200
|
+
/* Moved this default node renderer function out of the main prototype code
|
201
|
+
* so it can be override by default */
|
202
|
+
Graph.Renderer.defaultRenderFunc = function(r, node) {
|
203
|
+
/* the default node drawing */
|
204
|
+
var color = Raphael.getColor();
|
205
|
+
var ellipse = r.ellipse(0, 0, 30, 20).attr({fill: color, stroke: color, "stroke-width": 2});
|
206
|
+
/* set DOM node ID */
|
207
|
+
ellipse.node.id = node.label || node.id;
|
208
|
+
shape = r.set().
|
209
|
+
push(ellipse).
|
210
|
+
push(r.text(0, 30, node.label || node.id));
|
211
|
+
return shape;
|
212
|
+
}
|
213
|
+
|
214
|
+
|
215
|
+
Graph.Renderer.Raphael.prototype = {
|
216
|
+
translate: function(point) {
|
217
|
+
return [
|
218
|
+
(point[0] - this.graph.layoutMinX) * this.factorX + this.radius,
|
219
|
+
(point[1] - this.graph.layoutMinY) * this.factorY + this.radius
|
220
|
+
];
|
221
|
+
},
|
222
|
+
|
223
|
+
rotate: function(point, length, angle) {
|
224
|
+
var dx = length * Math.cos(angle);
|
225
|
+
var dy = length * Math.sin(angle);
|
226
|
+
return [point[0]+dx, point[1]+dy];
|
227
|
+
},
|
228
|
+
|
229
|
+
draw: function() {
|
230
|
+
this.factorX = (this.width - 2 * this.radius) / (this.graph.layoutMaxX - this.graph.layoutMinX);
|
231
|
+
this.factorY = (this.height - 2 * this.radius) / (this.graph.layoutMaxY - this.graph.layoutMinY);
|
232
|
+
for (i in this.graph.nodes) {
|
233
|
+
this.drawNode(this.graph.nodes[i]);
|
234
|
+
}
|
235
|
+
for (var i = 0; i < this.graph.edges.length; i++) {
|
236
|
+
this.drawEdge(this.graph.edges[i]);
|
237
|
+
}
|
238
|
+
},
|
239
|
+
|
240
|
+
drawNode: function(node) {
|
241
|
+
var point = this.translate([node.layoutPosX, node.layoutPosY]);
|
242
|
+
node.point = point;
|
243
|
+
|
244
|
+
/* if node has already been drawn, move the nodes */
|
245
|
+
if(node.shape) {
|
246
|
+
var oBBox = node.shape.getBBox();
|
247
|
+
var opoint = { x: oBBox.x + oBBox.width / 2, y: oBBox.y + oBBox.height / 2};
|
248
|
+
node.shape.translate(Math.round(point[0] - opoint.x), Math.round(point[1] - opoint.y));
|
249
|
+
this.r.safari();
|
250
|
+
return node;
|
251
|
+
}/* else, draw new nodes */
|
252
|
+
|
253
|
+
var shape;
|
254
|
+
|
255
|
+
/* if a node renderer function is provided by the user, then use it
|
256
|
+
or the default render function instead */
|
257
|
+
if(!node.render) {
|
258
|
+
node.render = Graph.Renderer.defaultRenderFunc;
|
259
|
+
}
|
260
|
+
/* or check for an ajax representation of the nodes */
|
261
|
+
if(node.shapes) {
|
262
|
+
// TODO ajax representation evaluation
|
263
|
+
}
|
264
|
+
|
265
|
+
shape = node.render(this.r, node).hide();
|
266
|
+
|
267
|
+
shape.attr({"fill-opacity": .6});
|
268
|
+
/* re-reference to the node an element belongs to, needed for dragging all elements of a node */
|
269
|
+
shape.items.forEach(function(item){ item.set = shape; item.node.style.cursor = "move"; });
|
270
|
+
shape.mousedown(this.dragger);
|
271
|
+
|
272
|
+
var box = shape.getBBox();
|
273
|
+
shape.translate(Math.round(point[0]-(box.x+box.width/2)),Math.round(point[1]-(box.y+box.height/2)))
|
274
|
+
//console.log(box,point);
|
275
|
+
node.hidden || shape.show();
|
276
|
+
node.shape = shape;
|
277
|
+
},
|
278
|
+
drawEdge: function(edge) {
|
279
|
+
/* if this edge already exists the other way around and is undirected */
|
280
|
+
if(edge.backedge)
|
281
|
+
return;
|
282
|
+
if(edge.source.hidden || edge.target.hidden) {
|
283
|
+
edge.connection && edge.connection.fg.hide();
|
284
|
+
edge.connection.bg && edge.connection.bg.hide();
|
285
|
+
return;
|
286
|
+
}
|
287
|
+
/* if edge already has been drawn, only refresh the edge */
|
288
|
+
if(!edge.connection) {
|
289
|
+
edge.style && edge.style.callback && edge.style.callback(edge); // TODO move this somewhere else
|
290
|
+
edge.connection = this.r.connection(edge.source.shape, edge.target.shape, edge.style);
|
291
|
+
return;
|
292
|
+
}
|
293
|
+
//FIXME showing doesn't work well
|
294
|
+
edge.connection.fg.show();
|
295
|
+
edge.connection.bg && edge.connection.bg.show();
|
296
|
+
edge.connection.draw();
|
297
|
+
}
|
298
|
+
};
|
299
|
+
Graph.Layout = {};
|
300
|
+
Graph.Layout.Spring = function(graph) {
|
301
|
+
this.graph = graph;
|
302
|
+
this.iterations = 500;
|
303
|
+
this.maxRepulsiveForceDistance = 6;
|
304
|
+
this.k = 2;
|
305
|
+
this.c = 0.01;
|
306
|
+
this.maxVertexMovement = 0.5;
|
307
|
+
this.layout();
|
308
|
+
};
|
309
|
+
Graph.Layout.Spring.prototype = {
|
310
|
+
layout: function() {
|
311
|
+
this.layoutPrepare();
|
312
|
+
for (var i = 0; i < this.iterations; i++) {
|
313
|
+
this.layoutIteration();
|
314
|
+
}
|
315
|
+
this.layoutCalcBounds();
|
316
|
+
},
|
317
|
+
|
318
|
+
layoutPrepare: function() {
|
319
|
+
for (i in this.graph.nodes) {
|
320
|
+
var node = this.graph.nodes[i];
|
321
|
+
node.layoutPosX = 0;
|
322
|
+
node.layoutPosY = 0;
|
323
|
+
node.layoutForceX = 0;
|
324
|
+
node.layoutForceY = 0;
|
325
|
+
}
|
326
|
+
|
327
|
+
},
|
328
|
+
|
329
|
+
layoutCalcBounds: function() {
|
330
|
+
var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity;
|
331
|
+
|
332
|
+
for (i in this.graph.nodes) {
|
333
|
+
var x = this.graph.nodes[i].layoutPosX;
|
334
|
+
var y = this.graph.nodes[i].layoutPosY;
|
335
|
+
|
336
|
+
if(x > maxx) maxx = x;
|
337
|
+
if(x < minx) minx = x;
|
338
|
+
if(y > maxy) maxy = y;
|
339
|
+
if(y < miny) miny = y;
|
340
|
+
}
|
341
|
+
|
342
|
+
this.graph.layoutMinX = minx;
|
343
|
+
this.graph.layoutMaxX = maxx;
|
344
|
+
this.graph.layoutMinY = miny;
|
345
|
+
this.graph.layoutMaxY = maxy;
|
346
|
+
},
|
347
|
+
|
348
|
+
layoutIteration: function() {
|
349
|
+
// Forces on nodes due to node-node repulsions
|
350
|
+
|
351
|
+
var prev = new Array();
|
352
|
+
for(var c in this.graph.nodes) {
|
353
|
+
var node1 = this.graph.nodes[c];
|
354
|
+
for (var d in prev) {
|
355
|
+
var node2 = this.graph.nodes[prev[d]];
|
356
|
+
this.layoutRepulsive(node1, node2);
|
357
|
+
|
358
|
+
}
|
359
|
+
prev.push(c);
|
360
|
+
}
|
361
|
+
|
362
|
+
// Forces on nodes due to edge attractions
|
363
|
+
for (var i = 0; i < this.graph.edges.length; i++) {
|
364
|
+
var edge = this.graph.edges[i];
|
365
|
+
this.layoutAttractive(edge);
|
366
|
+
}
|
367
|
+
|
368
|
+
// Move by the given force
|
369
|
+
for (i in this.graph.nodes) {
|
370
|
+
var node = this.graph.nodes[i];
|
371
|
+
var xmove = this.c * node.layoutForceX;
|
372
|
+
var ymove = this.c * node.layoutForceY;
|
373
|
+
|
374
|
+
var max = this.maxVertexMovement;
|
375
|
+
if(xmove > max) xmove = max;
|
376
|
+
if(xmove < -max) xmove = -max;
|
377
|
+
if(ymove > max) ymove = max;
|
378
|
+
if(ymove < -max) ymove = -max;
|
379
|
+
|
380
|
+
node.layoutPosX += xmove;
|
381
|
+
node.layoutPosY += ymove;
|
382
|
+
node.layoutForceX = 0;
|
383
|
+
node.layoutForceY = 0;
|
384
|
+
}
|
385
|
+
},
|
386
|
+
|
387
|
+
layoutRepulsive: function(node1, node2) {
|
388
|
+
if (typeof node1 == 'undefined' || typeof node2 == 'undefined')
|
389
|
+
return;
|
390
|
+
var dx = node2.layoutPosX - node1.layoutPosX;
|
391
|
+
var dy = node2.layoutPosY - node1.layoutPosY;
|
392
|
+
var d2 = dx * dx + dy * dy;
|
393
|
+
if(d2 < 0.01) {
|
394
|
+
dx = 0.1 * Math.random() + 0.1;
|
395
|
+
dy = 0.1 * Math.random() + 0.1;
|
396
|
+
var d2 = dx * dx + dy * dy;
|
397
|
+
}
|
398
|
+
var d = Math.sqrt(d2);
|
399
|
+
if(d < this.maxRepulsiveForceDistance) {
|
400
|
+
var repulsiveForce = this.k * this.k / d;
|
401
|
+
node2.layoutForceX += repulsiveForce * dx / d;
|
402
|
+
node2.layoutForceY += repulsiveForce * dy / d;
|
403
|
+
node1.layoutForceX -= repulsiveForce * dx / d;
|
404
|
+
node1.layoutForceY -= repulsiveForce * dy / d;
|
405
|
+
}
|
406
|
+
},
|
407
|
+
|
408
|
+
layoutAttractive: function(edge) {
|
409
|
+
var node1 = edge.source;
|
410
|
+
var node2 = edge.target;
|
411
|
+
|
412
|
+
var dx = node2.layoutPosX - node1.layoutPosX;
|
413
|
+
var dy = node2.layoutPosY - node1.layoutPosY;
|
414
|
+
var d2 = dx * dx + dy * dy;
|
415
|
+
if(d2 < 0.01) {
|
416
|
+
dx = 0.1 * Math.random() + 0.1;
|
417
|
+
dy = 0.1 * Math.random() + 0.1;
|
418
|
+
var d2 = dx * dx + dy * dy;
|
419
|
+
}
|
420
|
+
var d = Math.sqrt(d2);
|
421
|
+
if(d > this.maxRepulsiveForceDistance) {
|
422
|
+
d = this.maxRepulsiveForceDistance;
|
423
|
+
d2 = d * d;
|
424
|
+
}
|
425
|
+
var attractiveForce = (d2 - this.k * this.k) / this.k;
|
426
|
+
if(edge.attraction == undefined) edge.attraction = 1;
|
427
|
+
attractiveForce *= Math.log(edge.attraction) * 0.5 + 1;
|
428
|
+
|
429
|
+
node2.layoutForceX -= attractiveForce * dx / d;
|
430
|
+
node2.layoutForceY -= attractiveForce * dy / d;
|
431
|
+
node1.layoutForceX += attractiveForce * dx / d;
|
432
|
+
node1.layoutForceY += attractiveForce * dy / d;
|
433
|
+
}
|
434
|
+
};
|
435
|
+
|
436
|
+
Graph.Layout.Ordered = function(graph, order) {
|
437
|
+
this.graph = graph;
|
438
|
+
this.order = order;
|
439
|
+
this.layout();
|
440
|
+
};
|
441
|
+
Graph.Layout.Ordered.prototype = {
|
442
|
+
layout: function() {
|
443
|
+
this.layoutPrepare();
|
444
|
+
this.layoutCalcBounds();
|
445
|
+
},
|
446
|
+
|
447
|
+
layoutPrepare: function(order) {
|
448
|
+
for (i in this.graph.nodes) {
|
449
|
+
var node = this.graph.nodes[i];
|
450
|
+
node.layoutPosX = 0;
|
451
|
+
node.layoutPosY = 0;
|
452
|
+
}
|
453
|
+
var counter = 0;
|
454
|
+
for (i in this.order) {
|
455
|
+
var node = this.order[i];
|
456
|
+
node.layoutPosX = counter;
|
457
|
+
node.layoutPosY = Math.random();
|
458
|
+
counter++;
|
459
|
+
}
|
460
|
+
},
|
461
|
+
|
462
|
+
layoutCalcBounds: function() {
|
463
|
+
var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity;
|
464
|
+
|
465
|
+
for (i in this.graph.nodes) {
|
466
|
+
var x = this.graph.nodes[i].layoutPosX;
|
467
|
+
var y = this.graph.nodes[i].layoutPosY;
|
468
|
+
|
469
|
+
if(x > maxx) maxx = x;
|
470
|
+
if(x < minx) minx = x;
|
471
|
+
if(y > maxy) maxy = y;
|
472
|
+
if(y < miny) miny = y;
|
473
|
+
}
|
474
|
+
|
475
|
+
this.graph.layoutMinX = minx;
|
476
|
+
this.graph.layoutMaxX = maxx;
|
477
|
+
|
478
|
+
this.graph.layoutMinY = miny;
|
479
|
+
this.graph.layoutMaxY = maxy;
|
480
|
+
}
|
481
|
+
};
|
482
|
+
|
483
|
+
|
484
|
+
Graph.Layout.OrderedTree = function(graph, order) {
|
485
|
+
this.graph = graph;
|
486
|
+
this.order = order;
|
487
|
+
this.layout();
|
488
|
+
};
|
489
|
+
|
490
|
+
/*
|
491
|
+
* OrderedTree is like Ordered but assumes there is one root
|
492
|
+
* This way we can give non random positions to nodes on the Y-axis
|
493
|
+
* it assumes the ordered nodes are of a perfect binary tree
|
494
|
+
*/
|
495
|
+
Graph.Layout.OrderedTree.prototype = {
|
496
|
+
layout: function() {
|
497
|
+
this.layoutPrepare();
|
498
|
+
this.layoutCalcBounds();
|
499
|
+
},
|
500
|
+
|
501
|
+
layoutPrepare: function(order) {
|
502
|
+
for (i in this.graph.nodes) {
|
503
|
+
var node = this.graph.nodes[i];
|
504
|
+
node.layoutPosX = 0;
|
505
|
+
node.layoutPosY = 0;
|
506
|
+
}
|
507
|
+
//to reverse the order of rendering, we need to find out the
|
508
|
+
//absolute number of levels we have. simple log math applies.
|
509
|
+
var numNodes = this.order.length;
|
510
|
+
var totalLevels = Math.floor(Math.log(numNodes) / Math.log(2));
|
511
|
+
|
512
|
+
var counter = 1;
|
513
|
+
for (i in this.order) {
|
514
|
+
var node = this.order[i];
|
515
|
+
//rank aka x coordinate
|
516
|
+
var rank = Math.floor(Math.log(counter) / Math.log(2));
|
517
|
+
//file relative to top
|
518
|
+
var file = counter - Math.pow(rank, 2);
|
519
|
+
|
520
|
+
log('Node ' + node.id + ' #' + counter + ' is at rank ' + rank + ' file ' + file);
|
521
|
+
node.layoutPosX = totalLevels - rank;
|
522
|
+
node.layoutPosY = file;
|
523
|
+
counter++;
|
524
|
+
}
|
525
|
+
},
|
526
|
+
|
527
|
+
layoutCalcBounds: function() {
|
528
|
+
var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity;
|
529
|
+
|
530
|
+
for (i in this.graph.nodes) {
|
531
|
+
var x = this.graph.nodes[i].layoutPosX;
|
532
|
+
var y = this.graph.nodes[i].layoutPosY;
|
533
|
+
|
534
|
+
if(x > maxx) maxx = x;
|
535
|
+
if(x < minx) minx = x;
|
536
|
+
if(y > maxy) maxy = y;
|
537
|
+
if(y < miny) miny = y;
|
538
|
+
}
|
539
|
+
|
540
|
+
this.graph.layoutMinX = minx;
|
541
|
+
this.graph.layoutMaxX = maxx;
|
542
|
+
|
543
|
+
this.graph.layoutMinY = miny;
|
544
|
+
this.graph.layoutMaxY = maxy;
|
545
|
+
}
|
546
|
+
};
|
547
|
+
|
548
|
+
|
549
|
+
Graph.Layout.TournamentTree = function(graph, order) {
|
550
|
+
this.graph = graph;
|
551
|
+
this.order = order;
|
552
|
+
this.layout();
|
553
|
+
};
|
554
|
+
|
555
|
+
/*
|
556
|
+
* TournamentTree looks more like a binary tree
|
557
|
+
*/
|
558
|
+
Graph.Layout.TournamentTree.prototype = {
|
559
|
+
layout: function() {
|
560
|
+
this.layoutPrepare();
|
561
|
+
this.layoutCalcBounds();
|
562
|
+
},
|
563
|
+
|
564
|
+
layoutPrepare: function(order) {
|
565
|
+
for (i in this.graph.nodes) {
|
566
|
+
var node = this.graph.nodes[i];
|
567
|
+
node.layoutPosX = 0;
|
568
|
+
node.layoutPosY = 0;
|
569
|
+
}
|
570
|
+
//to reverse the order of rendering, we need to find out the
|
571
|
+
//absolute number of levels we have. simple log math applies.
|
572
|
+
var numNodes = this.order.length;
|
573
|
+
var totalLevels = Math.floor(Math.log(numNodes) / Math.log(2));
|
574
|
+
|
575
|
+
var counter = 1;
|
576
|
+
for (i in this.order) {
|
577
|
+
var node = this.order[i];
|
578
|
+
var depth = Math.floor(Math.log(counter) / Math.log(2));
|
579
|
+
var xpos = counter - Math.pow(depth, 2);
|
580
|
+
var offset = Math.pow(2, totalLevels - depth);
|
581
|
+
var final_x = offset + (counter - Math.pow(2,depth)) * Math.pow(2,(totalLevels - depth)+1);
|
582
|
+
|
583
|
+
log('Node ' + node.id + ' #' + counter + ' is at depth ' + depth + ' offset ' + offset + ' final_x ' + final_x);
|
584
|
+
node.layoutPosX = final_x;
|
585
|
+
node.layoutPosY = depth;
|
586
|
+
counter++;
|
587
|
+
}
|
588
|
+
},
|
589
|
+
|
590
|
+
layoutCalcBounds: function() {
|
591
|
+
var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity;
|
592
|
+
|
593
|
+
for (i in this.graph.nodes) {
|
594
|
+
var x = this.graph.nodes[i].layoutPosX;
|
595
|
+
var y = this.graph.nodes[i].layoutPosY;
|
596
|
+
|
597
|
+
if(x > maxx) maxx = x;
|
598
|
+
if(x < minx) minx = x;
|
599
|
+
if(y > maxy) maxy = y;
|
600
|
+
if(y < miny) miny = y;
|
601
|
+
}
|
602
|
+
|
603
|
+
this.graph.layoutMinX = minx;
|
604
|
+
this.graph.layoutMaxX = maxx;
|
605
|
+
|
606
|
+
this.graph.layoutMinY = miny;
|
607
|
+
this.graph.layoutMaxY = maxy;
|
608
|
+
}
|
609
|
+
};
|
610
|
+
|
611
|
+
|
612
|
+
|
613
|
+
|
614
|
+
/*
|
615
|
+
* usefull JavaScript extensions,
|
616
|
+
*/
|
617
|
+
|
618
|
+
function log(a) {console.log&&console.log(a);}
|
619
|
+
|
620
|
+
/*
|
621
|
+
* Raphael Tooltip Plugin
|
622
|
+
* - attaches an element as a tooltip to another element
|
623
|
+
*
|
624
|
+
* Usage example, adding a rectangle as a tooltip to a circle:
|
625
|
+
*
|
626
|
+
* paper.circle(100,100,10).tooltip(paper.rect(0,0,20,30));
|
627
|
+
*
|
628
|
+
* If you want to use more shapes, you'll have to put them into a set.
|
629
|
+
*
|
630
|
+
*/
|
631
|
+
Raphael.el.tooltip = function (tp) {
|
632
|
+
this.tp = tp;
|
633
|
+
this.tp.o = {x: 0, y: 0};
|
634
|
+
this.tp.hide();
|
635
|
+
this.hover(
|
636
|
+
function(event){
|
637
|
+
this.mousemove(function(event){
|
638
|
+
this.tp.translate(event.clientX -
|
639
|
+
this.tp.o.x,event.clientY - this.tp.o.y);
|
640
|
+
this.tp.o = {x: event.clientX, y: event.clientY};
|
641
|
+
});
|
642
|
+
this.tp.show().toFront();
|
643
|
+
},
|
644
|
+
function(event){
|
645
|
+
this.tp.hide();
|
646
|
+
this.unmousemove();
|
647
|
+
});
|
648
|
+
return this;
|
649
|
+
};
|
650
|
+
|
651
|
+
/* For IE */
|
652
|
+
if (!Array.prototype.forEach)
|
653
|
+
{
|
654
|
+
Array.prototype.forEach = function(fun /*, thisp*/)
|
655
|
+
{
|
656
|
+
var len = this.length;
|
657
|
+
if (typeof fun != "function")
|
658
|
+
throw new TypeError();
|
659
|
+
|
660
|
+
var thisp = arguments[1];
|
661
|
+
for (var i = 0; i < len; i++)
|
662
|
+
{
|
663
|
+
if (i in this)
|
664
|
+
fun.call(thisp, this[i], i, this);
|
665
|
+
}
|
666
|
+
};
|
667
|
+
}
|