You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1679 lines
59 KiB
JavaScript

/*### PATTERN | GRAPH.JS ###########################################################################*/
// Copyright (c) 2010 University of Antwerp, Belgium
// Authors: Tom De Smedt <tom@organisms.be>, Daniel Friesen (daniel@nadir-seen-fire.com)
// License: BSD (see LICENSE.txt for details).
// Version: 1.2
// http://www.clips.ua.ac.be/pages/pattern
/*##################################################################################################*/
function attachEvent(element, name, f) {
/* Cross-browser attachEvent().
* Ensures that "this" inside the function f refers to the given element .
*/
f = Function.closure(element, f);
if (element.addEventListener) {
element.addEventListener(name, f, false);
} else if (element.attachEvent) {
element.attachEvent("on"+name, f);
} else {
element["on"+name] = f;
}
element[name] = f;
}
Function.closure = function(parent, f) {
/* Returns the function f, where "this" inside the function refers to the given parent.
*/
return function() { return f.apply(parent, arguments); };
};
/*--- OBJECT ---------------------------------------------------------------------------------------*/
Object.keys = function(object) {
var a = [];
for (var k in object) a.push(k);
return a;
};
Object.values = function(object) {
var a = [];
for (var k in object) a.push(object[k]);
return a;
};
/*--- ARRAY ----------------------------------------------------------------------------------------*/
Array.sum = function(array) {
for (var i=0, sum=0; i < array.length; sum+=array[i++]){}; return sum;
};
Array.index = function(array, v) {
for (var i=0; i < array.length; i++) { if (array[i] === v) return i; }
return -1;
};
Array.choice = function(array) {
return array[Math.round(Math.random() * (array.length-1))];
};
Array.unique = function(array) {
/* Returns a new array with unique items.
*/
var a = array.slice();
for (var i=a.length-1; i > 0; --i) {
var v = a[i];
for (var j=i-1; j >= 0; --j) {
if (a[j] === v) a.splice(j, 1); i--;
}
}
return a;
};
var choice = Array.choice;
/*--- MATH -----------------------------------------------------------------------------------------*/
Math.degrees = function(radians) {
return radians * 180 / Math.PI;
};
Math.radians = function(degrees) {
return degrees / 180 * Math.PI;
};
Math.coordinates = function(x, y, distance, angle) {
return [x + distance * Math.cos(Math.radians(angle)),
y + distance * Math.sin(Math.radians(angle))];
};
/*--- MOUSE ----------------------------------------------------------------------------------------*/
var __MOUSE__ = {
x: 0,
y: 0,
_x0: 0,
_y0: 0,
dx: 0, // Drag distance from x0.
dy: 0, // Drag distance from y0.
pressed: false,
dragged: false,
relative: function(element) {
/* Returns position from the top-left corner of the given element.
*/
var dx = 0;
var dy = 0;
if (element.offsetParent) {
do {
dx += element.offsetLeft;
dy += element.offsetTop;
} while (element = element.offsetParent);
}
return {
x: __MOUSE__.x - dx,
y: __MOUSE__.y - dy
}
}
};
attachEvent(document, "mousemove", function(e) {
__MOUSE__.x = e.pageX || (e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft));
__MOUSE__.y = e.pageY || (e.clientY + (document.documentElement.scrollTop || document.body.scrollTop));
if (__MOUSE__.pressed) {
__MOUSE__.dragged = true;
__MOUSE__.dx = __MOUSE__.x - __MOUSE__._x0;
__MOUSE__.dy = __MOUSE__.y - __MOUSE__._y0;
}
});
attachEvent(document, "mousedown", function(e) {
__MOUSE__.pressed = true;
__MOUSE__._x0 = __MOUSE__.x;
__MOUSE__._y0 = __MOUSE__.y;
});
attachEvent(document, "mouseup", function(e) {
__MOUSE__.pressed = false;
__MOUSE__.dragged = false;
__MOUSE__.dx = 0;
__MOUSE__.dy = 0;
});
function _unselectable(element) {
/* Disables text selection on the given element (interferes with node dragging).
*/
if (element) {
element.onselectstart = function() { return false; };
element.unselectable = "on";
element.style.MozUserSelect = "none";
element.style.cursor = "default";
}
}
/*--- IE HACKS -------------------------------------------------------------------------------------*/
// setInterval() on IE does not take function arguments so we patch it:
/*@cc_on
(function(f) {
window.setTimeout = f(window.setTimeout);
window.setInterval = f(window.setInterval);
})(function(f) {
return function(c, t) {
var a = [].slice.call(arguments, 2);
return f(function() {
c.apply(this, a);
}, t);
};
});
@*/
/*--- BASE CLASS -----------------------------------------------------------------------------------*/
// JavaScript class inheritance, John Resig (http://ejohn.org/blog/simple-javascript-inheritance).
//
// var Person = Class.extend({
// init: function(name) {
// this.name = name;
// }
// });
//
// var Employee = Person.extend({
// init: function(name, salary) {
// this._super(name); // Call Person.init().
// this.salary = salary;
// }
// });
//
// var e = new Employee("tom", 10);
(function() {
var initialized = false;
var has_super = /xyz/.test(function() { xyz; }) ? /\b_super\b/ : /.*/;
this.Class = function(){};
Class.extend = function(properties) {
// Instantiate base class (create the instance, don't run the init constructor).
var _super = this.prototype;
initialized = true; var p = new this();
initialized = false;
// Copy the properties onto the new prototype.
for (var k in properties) {
p[k] = typeof properties[k] == "function"
&& typeof _super[k] == "function"
&& has_super.test(properties[k]) ? (function(k, f) { return function() {
// If properties[k] is actually a method,
// add a _super() method (= same method but on the superclass).
var s, r;
s = this._super;
this._super = _super[k]; r = f.apply(this, arguments);
this._super = s;
return r;
};
})(k, properties[k]) : properties[k];
}
function Class() {
if (!initialized && this.init) {
this.init.apply(this, arguments);
}
}
Class.constructor = Class;
Class.prototype = p;
// Make the class extendable.
Class.extend = arguments.callee;
return Class;
};
})();
/*--- COLOR ----------------------------------------------------------------------------------------*/
// Edge.stroke, Node.stroke, Node.fill can be an rgba() string, an [R,G,B,A]-array, a canvas.js Color
// or null (transparent). If the color is undefined, either the canvas.js Canvas state color is used,
// or black for edges and transparent for nodes if graph.js is used without canvas.js.
function _parseRGBA(clr) {
if (clr && clr.rgba && clr._get) { // canvas.js Color
return clr._get();
}
if (clr instanceof Array) {
var r = Math.round(clr[0] * 255);
var g = Math.round(clr[1] * 255);
var b = Math.round(clr[2] * 255);
return "rgba("+r+", "+g+", "+b+", "+clr[3]+")";
}
if (clr === null) {
return "rgba(0,0,0,0)";
}
return clr;
}
var _GRAPH_FILL1 = null;
var _GRAPH_FILL2 = null;
function _ctx_graph_fillStyle(clr, ctx) {
clr = _parseRGBA(clr);
if (clr === undefined) {
if (_ctx && _ctx.state && _ctx.state.fill !== undefined) {
clr = _parseRGBA(_ctx.state.fill);
} else {
clr = "rgba(0,0,0,0)";
}
}
if (_GRAPH_FILL1 != clr || _GRAPH_FILL2 != ctx.fillStyle) {
_GRAPH_FILL1 = ctx.fillStyle = clr;
_GRAPH_FILL2 = ctx.fillStyle;
}
}
var _GRAPH_STROKE1 = null;
var _GRAPH_STROKE2 = null;
function _ctx_graph_strokeStyle(clr, ctx) {
clr = _parseRGBA(clr);
if (clr === undefined) {
if (_ctx && _ctx.state && _ctx.state.stroke !== undefined) {
clr = _parseRGBA(_ctx.state.stroke);
} else {
clr = "rgba(0,0,0,1)";
}
}
if (_GRAPH_STROKE1 != clr || _GRAPH_STROKE2 != ctx.strokeStyle) {
_GRAPH_STROKE1 = ctx.strokeStyle = clr;
_GRAPH_STROKE2 = ctx.strokeStyle;
}
}
function _ctx_graph_lineWidth(w1, w2, ctx) {
if (w1 === undefined) {
w1 = _ctx && _ctx.state && _ctx.state.strokewidth || 1.0;
}
ctx.lineWidth = w1 + w2 || 0;
}
/*--- GRAPH NODE -----------------------------------------------------------------------------------*/
var Node = Class.extend({
// init: function(id, {radius:5, weight:0, centrality:0,
// fill:"rgba(0,0,0,0)", stroke:"rgba(0,0,0,1)", strokewidth:1,
// text:true, font:null, fontsize:null, fontweight:null, href:null, css:null})
init: function(id, a) {
/* A node with an id in the graph.
* Node.text is a <div> element displaying id.
*/
if (a === undefined) a = {};
if (a.x === undefined) a.x = 0;
if (a.y === undefined) a.y = 0;
if (a.radius === undefined) a.radius = 5;
if (a.fixed === undefined) a.fixed = false;
//if (a.fill === undefined) a.fill = "rgba(0,0,0,0)";
//if (a.stroke === undefined) a.stroke = "rgba(0,0,0,1)";
//if (a.strokewidth === undefined) a.strokewidth = 1;
this.graph = null;
this.links = new NodeLinks();
this.id = id;
this.x = 0; // Calculated by Graph.layout.update().
this.y = 0; // Calculated by Graph.layout.update().
this._x = a.x;
this._y = a.y;
this._vx = 0;
this._vy = 0;
this.radius = a.radius;
this.fixed = a.fixed;
this.fill = a.fill;
this.stroke = a.stroke;
this.strokewidth = a.strokewidth;
this.weight = a.weight || 0;
this.centrality = a.centrality || 0;
this.degree = a.degree || 0;
this.text = null;
if (a.text != false) {
var div = document.createElement('div');
var txt = ((a.label || id)+"").replace("\\\"", "\"");
txt = txt.replace("<","&lt;");
txt = txt.replace(">","&gt;");
div.innerHTML = (a.href)? '<a href="'+a.href+'">'+txt+"</a>" : txt;
div.className = (a.css)? ("node-label " + a.css) : "node-label";
div.style.fontFamily = (a.font)? a.font : "";
div.style.fontSize = (a.fontsize)? a.fontsize+"px" : "";
div.style.fontWeight = (a.fontweight)? a.fontweight : ""; // XXX doesn't work for "italic" (=fontStyle).
div.style.color = (typeof(a.text) == "string")? a.text : "";
this.text = div;
_unselectable(div);
}
},
edges: function() {
/* Yields a list of edges from/to the node.
*/
var a = [];
for (var i=0; i < this.graph.edges.length; i++) {
var e = this.graph.edges[i];
if (e.node1 == this || e.node2 == this) {
a.push(e);
}
}
return a;
},
flatten: function(depth, traversable, _visited) {
/* Recursively lists the node and nodes linked to it.
* Depth 0 returns a list with the node.
* Depth 1 returns a list with the node and all the directly linked nodes.
* Depth 2 includes the linked nodes' links, and so on.
*/
if (depth === undefined) depth = 1;
if (traversable === undefined) traversable = function(node, edge) { return true; };
_visited = _visited || {};
_visited[this.id] = [this, depth];
if (depth >= 1) {
for (var i=0; i < this.links.length; i++) {
var n = this.links[i];
if (!_visited[n.id] || _visited[n.id][1] < depth-1) {
if (traversable(this, this.links.edges[n.id])) {
n.flatten(depth-1, traversable, _visited);
}
}
}
}
var a = Object.values(_visited); // Fast, but not order-preserving.
for (var i=0; i < a.length; i++) {
a[i] = a[i][0];
}
return a;
},
draw: function(weighted) {
/* Draws the node as a circle with the given radius, fill, stroke and strokewidth.
* Draws the node centrality as a shadow effect when weighted=True.
* Draws the node text label.
* Override this method in a subclass for custom drawing.
*/
var ctx = this.graph._ctx;
// Draw the node weight as a shadow (based on node betweenness centrality).
if (weighted && weighted != false && this.centrality > ((weighted==true)?-1:weighted)) {
var w = this.centrality * 35;
_ctx_graph_fillStyle("rgba(0,0,0,0.075)", ctx);
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius+w, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}
// Draw the node.
_ctx_graph_lineWidth(this.strokewidth, 0, ctx);
_ctx_graph_strokeStyle(this.stroke, ctx);
_ctx_graph_fillStyle(this.fill, ctx);
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Draw the node text label.
if (this.text) {
this.text.style.display = "inline";
this.text.style.position = "absolute";
this.text.style.left = Math.round(this.x + this.radius/2 + this.graph.canvas.width/2) + "px";
this.text.style.top = Math.round(this.y + this.radius/2 + this.graph.canvas.height/2) + "px";
}
},
contains: function(x, y) {
/* Returns True if the given coordinates (x, y) are inside the node radius.
*/
return Math.abs(this.x - x) < this.radius*2 &&
Math.abs(this.y - y) < this.radius*2
}
});
var NodeLinks = Class.extend({
init: function() {
/* A list in which each node has an associated edge.
* The edge() method returns the edge for a given node id.
*/
this.edges = {};
this.length = 0;
},
append: function(node, edge) {
if (!this.edges[node.id]) {
this[this.length] = node;
this.length += 1;
}
this.edges[node.id] = edge;
},
remove: function(node) {
var i = Array.index(this, node);
if (i >= 0) {
for (var j=i; j < this.length; j++) this[j] = this[j+1];
this.length -= 1;
delete this.edges[node.id];
}
},
edge: function(node) {
return this.edges[(node instanceof Node)? node.id : node] || null;
}
});
/*--- GRAPH EDGE -----------------------------------------------------------------------------------*/
var Edge = Class.extend({
// init: function(node1, node2, {weight:0, length:1, type:null, stroke:"rgba(0,0,0,1)", strokewidth:1})
init: function(node1, node2, a) {
/* A connection between two nodes.
* Its weight indicates the importance (not the cost) of the connection.
* Its type is useful in a semantic network (e.g. "is-a", "is-part-of", ...)
*/
if (a === undefined) a = {};
if (a.weight === undefined) a.weight = 0.0;
if (a.length === undefined) a.length = 1.0;
if (a.type === undefined) a.type = null;
//if (a.stroke === undefined) a.stroke = "rgba(0,0,0,1)";
//if (a.strokewidth === undefined) a.strokewidth = 1;
this.node1 = node1;
this.node2 = node2;
this.weight = a.weight;
this.length = a.length;
this.type = a.type;
this.stroke = a.stroke;
this.strokewidth = a.strokewidth;
},
draw: function(weighted, directed) {
/* Draws the edge as a line with the given stroke and strokewidth (increased with Edge.weight).
* Override this method in a subclass for custom drawing.
*/
var w = weighted && this.weight || 0;
var ctx = this.node1.graph._ctx;
_ctx_graph_lineWidth(this.strokewidth, w, ctx);
_ctx_graph_strokeStyle(this.stroke, ctx);
ctx.beginPath();
ctx.moveTo(this.node1.x, this.node1.y);
ctx.lineTo(this.node2.x, this.node2.y);
ctx.stroke();
if (directed) {
this.drawArrow(this.strokewidth);
}
},
drawArrow: function(strokewidth) {
/* Draws the direction of the edge as an arrow on the rim of the receiving node.
*/
var s = (strokewidth !== undefined)? strokewidth : _ctx && _ctx.state && _ctx.state.strokewidth || 1;
var x0 = this.node1.x;
var y0 = this.node1.y;
var x1 = this.node2.x;
var y1 = this.node2.y;
// Find the edge's angle based on node1 and node2 position.
var a = Math.degrees(Math.atan2(y1-y0, x1-x0));
// The arrow points to node2's rim instead of it's center.
// Find the two other arrow corners under the given angle.
var d = Math.sqrt(Math.pow(x1-x0, 2) + Math.pow(y1-y0, 2));
var r = Math.max(s * 4, 8);
var p1 = Math.coordinates(x0, y0, d-this.node2.radius-1, a);
var p2 = Math.coordinates(p1[0], p1[1], -r, a-20);
var p3 = Math.coordinates(p1[0], p1[1], -r, a+20);
var ctx = this.node1.graph._ctx;
_ctx_graph_fillStyle(ctx.strokeStyle, ctx);
ctx.beginPath();
ctx.moveTo(p1[0], p1[1]);
ctx.lineTo(p2[0], p2[1]);
ctx.lineTo(p3[0], p3[1]);
ctx.fill();
}
});
/*--- GRAPH ----------------------------------------------------------------------------------------*/
// Dropshadow opacity:
var SHADOW = 0.0;
// Graph layouts:
var SPRING = "spring";
// Graph node centrality:
var EIGENVECTOR = "eigenvector";
var BETWEENNESS = "betweenness";
// Graph node sort order:
var WEIGHT = "weight"
var CENTRALITY = "centrality"
var Graph = Class.extend({
init: function(canvas, distance, layout) {
/* A network of nodes connected by edges that can be drawn with a given layout.
* A HTML5 <canvas> element must be given.
*/
if (distance === undefined) distance = 10;
if (layout === undefined) layout = SPRING;
this.canvas = canvas; _unselectable(canvas);
this._ctx = this.canvas? this.canvas.getContext("2d") : null;
this.nodeset = {};
this.nodes = [];
this.edges = [];
this.root = null;
this.mouse = __MOUSE__;
this.distance = distance;
this.layout = (layout==SPRING)? new GraphSpringLayout(this) :
new GraphLayout(this);
},
$: function(id) {
return this.nodeset[id];
},
append: function(base, a) {
/* Appends a Node or Edge to the graph.
*/
if (base == Node)
return this.add_node(a.id, a);
if (base == Edge)
return this.add_edge(a.id1, a.id2, a);
},
addNode: function(id, a) {
/* Appends a new Node to the graph.
*/
var n = a && a._super || Node;
n = (id instanceof Node)? id : (this.nodeset[id])? this.nodeset[id] : new n(id, a);
if (a && a.root) this.root = n;
if (!this.nodeset[n.id]) {
this.nodes.push(n);
this.nodeset[n.id] = n; n.graph = this;
if (n.text && this.canvas && this.canvas.parentNode) {
this.canvas.parentNode.appendChild(n.text);
}
}
return n;
},
addEdge: function(id1, id2, a) {
/* Appends a new Edge to the graph.
* An optional base parameter can be used to pass a subclass of Edge:
* Graph.addEdge("cold", "winter", {base:IsPropertyOf});
*/
// Create nodes that are not yet part of the graph.
var n1 = this.addNode(id1);
var n2 = this.addNode(id2);
// Create an Edge instance.
// If an edge (in the same direction) already exists, yields that edge instead.
var e1 = n1.links.edge(n2)
if (e1 && e1.node1 == n1 && e1.node2 == n2) {
return e1; // Shortcut to existing edge.
}
e2 = a && a._super || Edge;
e2 = new e2(n1, n2, a);
this.edges.push(e2);
// Synchronizes Node.links:
// A.links.edge(B) yields edge A->B
// B.links.edge(A) yields edge B->A
n1.links.append(n2, edge=e2)
n2.links.append(n1, edge=e1||e2)
return e2;
},
remove: function(x) {
/* Removes the given Node (and all its edges) or Edge from the graph.
* Note: removing Edge a->b does not remove Edge b->a.
*/
if (x instanceof Node && this.nodeset[x.id]) {
delete this.nodeset[x.id];
this.nodes.splice(Array.index(this.nodes, x), 1); x.graph = null;
// Remove all edges involving the given node.
var a = [];
for (var i=0; i < this.edges.length; i++) {
var e = this.edges[i];
if (x == e.node1 || x == e.node2) {
if (x == e.node2) e.node1.links.remove(x);
if (x == e.node1) e.node2.links.remove(x);
} else {
a.push(e);
}
}
this.edges = a;
// Remove label <div>.
if (x.text) x.text.parentNode.removeChild(x.text);
}
if (x instanceof Edge) {
this.edges.splice(Array.index(this.edges, x), 1);
}
},
node: function(id) {
/* Returns the node in the graph with the given id.
*/
if (id instanceof Node && id.graph == this) return id;
return this.nodeset[id] || null;
},
edge: function(id1, id2) {
/* Returns the edge between the nodes with given id1 and id2.
*/
if (id1 instanceof Node && id1.graph == this) id1 = id1.id;
if (id2 instanceof Node && id2.graph == this) id2 = id2.id;
return (this.nodeset[id1] && this.nodeset[id2])? this.nodeset[id1].links.edge(id2) : null;
},
// paths: function(node1, node2)
paths: function(node1, node2, length, path) {
/* Returns a list of paths (shorter than or equal to given length) connecting the two nodes.
*/
if (length === undefined) length = 4;
if (path === undefined) path = [];
if (!(node1 instanceof Node)) node1 = this.nodeset(node1);
if (!(node2 instanceof Node)) node2 = this.nodeset(node2);
var p = [];
var P = Graph.paths(this, node1.id, node2.id, length, path, true);
for (var i=0; i < P.length; i++) {
var n = [];
for (var j=0; j < P[i].length; j++) {
n.push(this.nodeset[P[i][j]]);
}
p.push(n);
}
return p;
},
// shortestPath: function(node1, node2, {heuristic:function(id1,id2){return 0;}, directed:false})
shortestPath: function(node1, node2, a) {
/* Returns a list of nodes connecting the two nodes.
*/
if (!(node1 instanceof Node)) node1 = this.nodeset[node1];
if (!(node2 instanceof Node)) node2 = this.nodeset[node2];
try {
var p = Graph.dijkstraShortestPath(this, node1.id, node2.id, a);
var n = [];
for (var i=0; i < p.length; i++) {
n.push(this.nodeset[p[i]]);
}
return n;
} catch(e) {
return null;
}
},
// shortestPaths: function(node, {heuristic:function(id1,id2){return 0;}, directed:false})
shortestPaths: function(node, a) {
/* Returns a dictionary of nodes, each linked to a list of nodes (shortest path).
*/
if (!(node instanceof Node)) node = this.nodeset[node];
var p = {};
var P = Graph.dijkstraShortestPaths(this, node.id, a);
for (var id in P) {
if (P[id]) {
var n = [];
for (var i=0; i < P[id].length; i++) {
n.push(this.nodeset[P[id][i]]);
}
p[this.nodeset[id]] = n;
} else {
p[this.nodeset[id]] = null;
}
}
},
// eigenvectorCentrality: function(graph, {normalized:true, reversed:true, rating:{}, iterations:100, tolerance:0.0001})
eigenvectorCentrality: function(graph, a) {
/* Calculates eigenvector centrality and returns a node => weight dictionary.
* Node.weight is updated in the process.
* Node.weight is higher for nodes with a lot of (indirect) incoming traffic.
*/
var ec = Graph.eigenvectorCentrality(this, a);
var r = {};
for (var id in ec) {
var n = this.nodeset[id];
n.weight = ec[id];
r[n] = n.weight;
}
return r;
},
// betweennessCentrality: function(graph, {normalized:true, directed:false})
betweennessCentrality: function(graph, a) {
/* Calculates betweenneess centrality and returns a node => weight dictionary.
* Node.centrality is updated in the process.
* Node.centrality is higher for nodes with a lot of passing traffic.
*/
var bc = Graph.brandesBetweennessCentrality(this, a);
var r = {};
for (var id in bc) {
var n = this.nodeset[id];
n.centrality = bc[id];
r[n] = n.centrality;
}
return r;
},
degreeCentrality: function(graph) {
/* Calculates degree centrality and returns a node => weight dictionary.
* Node.degree is updated in the process.
* Node.degree is higher for nodes with a lot of local traffic.
*/
var r = {};
for (var i=0; i < this.nodes.length; i++) {
var n = this.nodes[i];
n.degree = n.links.length / this.nodes.length;
r[n] = n.degree;
}
return r;
},
sorted: function(order, threshold) {
/* Returns a list of nodes sorted by WEIGHT or CENTRALITY.
* Nodes with a lot of traffic will be at the start of the list.
*/
if (order === undefined) order = WEIGHT;
if (threshold === undefined) threshold = 0.0;
var a = [];
for (var i=0; i < this.nodes.length; i++) {
if (this.nodes[i][order] >= threshold) {
a.push([this.nodes[i][order], this.nodes[i]]);
}
}
a = a.sort();
a = a.reverse();
for (var i=0; i < a.length; i++) {
a[i] = a[i][1];
}
return a;
},
prune: function(depth) {
/* Removes all nodes with less or equal links than depth.
*/
if (depth === undefined) depth = 0;
var m = {};
for(i=0; i<this.nodes.length; i++) {
m[this.nodes[i].id] = 0;
}
for(i=0; i<this.edges.length; i++) {
m[this.edges[i].node1.id] += 1;
m[this.edges[i].node2.id] += 1;
}
for(id in m) {
if (m[id] <= depth) {
this.remove(this.nodeset[id]);
}
}
},
fringe: function(depth) {
/* For depth=0, returns the list of leaf nodes (nodes with only one connection).
* For depth=1, returns the list of leaf nodes and their connected nodes, and so on.
*/
if (depth === undefined) depth = 0;
var u = [];
for (var i=0; i < this.nodes.length; i++) {
if (this.nodes[i].links.length == 1) {
u.push.apply(u, this.nodes[i].flatten(depth));
}
}
return Array.unique(u);
},
density: function() {
/* Yields the number of edges vs. the maximum number of possible edges.
* For example, <0.35 => sparse, >0.65 => dense, 1.0 => complete.
*/
return 2.0*this.edges.length / (this.nodes.length * (this.nodes.length-1));
},
split: function() {
/* Returns the list of unconnected subgraphs.
*/
return Graph.partition(this);
},
update: function(iterations, weight, limit) {
/* Graph.layout.update() is called the given number of iterations.
*/
if (iterations === undefined) iterations = 2;
for (var i=0; i < iterations; i++) {
this.layout.update(weight, limit);
}
},
draw: function(weighted, directed) {
/* Draws all nodes and edges.
*/
this._ctx.save();
this._ctx.translate(this.canvas.width/2, this.canvas.height/2);
for (var i=0; i < this.edges.length; i++) {
this.edges[i].draw(weighted, directed);
}
for (var i=0; i < this.nodes.length; i++) {
this.nodes[i].draw(weighted);
}
this._ctx.restore();
},
drag: function(mouse) {
/* Node drag behavior (resets the frame counter).
* The given mouse is either the graph.js built-in __MOUSE__ or canvas.js Mouse.
*/
var pt = null;
if (mouse.pressed) {
pt = mouse.relative && mouse.relative(this.canvas) || {x: mouse.x, y: mouse.y};
pt.x -= this.canvas.width/2; // Undo center translate.
pt.y -= this.canvas.height/2;
}
if (mouse.pressed && !mouse.dragged) {
this.dragged = this.nodeAt(pt.x, pt.y);
}
if (!mouse.pressed) {
this.dragged = null;
}
if (this.dragged) {
this.dragged._x = pt.x / this.distance;
this.dragged._y = pt.y / this.distance;
this.layout.iterations = 0;
this._i = 0;
}
},
// loop: function({frames:500, weighted:false, directed:false, fps:30, ipf:2})
loop: function(a) {
/* Calls Graph.update() and Graph.draw() in an animation loop.
*/
if (a === undefined) a = {};
if (a.frames === undefined) a.frames = 500;
if (a.fps === undefined) a.fps = 30;
if (a.ipf === undefined) a.ipf = 2;
this._i = 0;
this._frames = a.frames;
this._animation = setInterval(function(g) {
// The animation loops fps frames per second,
// until the given number of frames has elapsed.
// Nodes can be dragged around (this resets the frame counter).
// Transparent background, shadows enabled.
if (g._i < g._frames) {
g._ctx.clearRect(0, 0, g.canvas.width, g.canvas.height);
if (SHADOW) {
g._ctx.shadowColor = "rgba(0,0,0,"+SHADOW+")";
g._ctx.shadowBlur = 8;
g._ctx.shadowOffsetX = 6;
g._ctx.shadowOffsetY = 6;
}
g.update(a.ipf);
g.draw(a.weighted, a.directed);
g._i += 1;
}
g.drag(g.mouse);
}, 1000/a.fps, this);
},
stop: function() {
clearInterval(this._animation);
this._animation = null;
},
nodeAt: function(x, y) {
/* Returns the node at (x,y) or null.
*/
for (var i=0; i < this.nodes.length; i++) {
var n = this.nodes[i];
if (n.contains(x, y)) {
return n;
}
}
},
_addNodeCopy: function(n, a) {
var args = {
radius: n.radius,
fill: n.fill,
stroke: n.stroke,
strokewidth: n.strokewidth,
text: n.text? n.text.style.color || true : false,
font: n.text? n.text.style.fontFamily || null : null,
fontsize: n.text? parseInt(n.text.style.fontSize) || null : null,
fontweight: n.text? n.text.style.fontWeight || null : null,
css: n.text? n.text.className || null : null,
root: a && a.root || false
};
var a = n.text? n.text.getElementsByTagName("a") : null;
if (a && a.length > 0) args.href = a[0].href || null;
this.addNode(n.id, args);
},
_addEdgeCopy: function(e, a) {
if (!((a && a["node1"] || e.node1).id in this.nodeset) ||
!((a && a["node2"] || e.node2).id in this.nodeset)) {
return;
}
this.addEdge(
a && a["node1"] || this.nodeset[e.node1.id],
a && a["node2"] || this.nodeset[e.node2.id], {
weight: e.weight,
length: e.length,
type: e.type,
stroke: e.stroke,
strokewidth: e.strokewidth
}
);
},
copy: function(canvas, nodes) {
/* Returns a copy of the graph with the given list of nodes (and connecting edges).
* The layout will be reset.
*/
if (nodes === undefined) nodes = this.nodes;
var g = new Graph(canvas, this.distance, null);
g.layout = this.layout.copy(g);
for (var i=0; i < nodes.length; i++) {
var n = nodes[i];
if (!(n instanceof Node)) n = this.nodeset[n];
g._addNodeCopy(n, {root:this.root==n});
}
for (var i=0; i < this.edges.length; i++) {
var e = this.edges[i];
g._addEdgeCopy(e);
}
return g;
},
clear: function() {
// Removes the graph from the canvas.
// Removes the <div> node labels from the DOM.
// Removes nodes and edges from memory.
if (this.canvas && this._ctx) {
this._ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
for (var i=0; i < this.nodes.length; i++) {
var n = this.nodes[i];
if (n.text && n.text.parentNode) {
n.text.parentNode.removeChild(n.text);
n.text = null;
}
}
this.nodeset = {};
this.nodes = [];
this.edges = [];
this.root = null;
this.layout = null;
this.canvas = null;
}
});
/*--- GRAPH LAYOUT ---------------------------------------------------------------------------------*/
// Based on: Graph JavaScript framework (2006), Aslak Hellesoy & Dave Hoover.
GraphLayout = Class.extend({
init: function(graph) {
/* Calculates node positions iteratively when GraphLayout.update() is called.
*/
this.graph = graph;
this.iterations = 0;
},
update: function() {
this.iterations += 1;
},
reset: function() {
this.iterations = 0;
for (var i=0; i < this.graph.nodes.length; i++) {
var n = this.graph.nodes[i];
n._x = 0;
n._y = 0;
n._vx = 0;
n._vy = 0;
}
},
bounds: function() {
/* Returns a (x, y, width, height)-tuple of the approximate layout dimensions.
*/
var min = {'x': +Infinity, 'y': +Infinity}
var max = {'x': -Infinity, 'y': -Infinity}
for (var i=0; i < this.graph.nodes.length; i++) {
var n = this.graph.nodes[i];
if (n._x < min.x) min.x = n._x;
if (n._y < min.y) min.y = n._y;
if (n._x > max.x) max.x = n._x;
if (n._y > max.y) max.y = n._y;
}
return [min.x, min.y, max.x-min.x, max.y-min.y];
},
copy: function(graph) {
return new GraphLayout(graph);
}
});
/*--- GRAPH LAYOUT: FORCE-BASED --------------------------------------------------------------------*/
GraphSpringLayout = GraphLayout.extend({
init: function(graph) {
/* A force-based layout in which edges are regarded as springs.
* The forces are applied to the nodes, pulling them closer or pushing them apart.
*/
this._super(graph);
this.k = 4.0; // Force constant.
this.force = 0.01; // Force multiplier.
this.repulsion = 50; // Maximum repulsive force radius.
},
_distance: function(node1, node2) {
/* Yields a tuple with distances (dx, dy, d, d**2).
* Ensures that the distance is never zero (which deadlocks the animation).
*/
var dx = node2._x - node1._x;
var dy = node2._y - node1._y;
var d2 = dx*dx + dy*dy;
if (d2 < 0.01) {
dx = Math.random() * 0.1 + 0.1;
dy = Math.random() * 0.1 + 0.1;
d2 = dx*dx + dy*dy;
}
return [dx, dy, Math.sqrt(d2), d2];
},
_repulse: function(node1, node2) {
/* Updates Node force vector with the repulsive force.
*/
var a = this._distance(node1, node2); dx=a[0]; dy=a[1]; d=a[2]; d2=a[3];
if (d < this.repulsion) {
var f = Math.pow(this.k,2) / d2;
node2._vx += f * dx;
node2._vy += f * dy;
node1._vx -= f * dx;
node1._vy -= f * dy;
}
},
_attract: function(node1, node2, weight, length) {
/* Updates Node force vector with the attractive edge force.
*/
var a = this._distance(node1, node2); dx=a[0]; dy=a[1]; d=a[2]; d2=a[3];
var d = Math.min(d, this.repulsion);
var f = (d2 - Math.pow(this.k,2)) / this.k * length;
f *= weight * 0.5 + 1;
f /= d;
node2._vx -= f * dx;
node2._vy -= f * dy;
node1._vx += f * dx;
node1._vy += f * dy;
},
update: function(weight, limit) {
/* Updates the position of nodes in the graph.
* The weight parameter determines the impact of edge weight.
* The limit parameter determines the maximum movement each update().
*/
if (weight === undefined) weight = 10.0;
if (limit === undefined) limit = 0.5;
// Call GraphLayout.update().
this._super();
// Forces on all nodes due to node-node repulsions.
for (var i=0; i < this.graph.nodes.length; i++) {
var n1 = this.graph.nodes[i];
for (var j=i+1; j < this.graph.nodes.length; j++) {
var n2 = this.graph.nodes[j];
this._repulse(n1, n2);
}
}
// Forces on nodes due to edge attractions.
for (var i=0; i < this.graph.edges.length; i++) {
var e = this.graph.edges[i];
this._attract(e.node1, e.node2, weight*e.weight, 1.0/(e.length||0.01));
}
// Move by the given force.
for (var i=0; i < this.graph.nodes.length; i++) {
var n = this.graph.nodes[i];
if (!n.fixed) {
n._x += Math.max(-limit, Math.min(this.force * n._vx, limit));
n._y += Math.max(-limit, Math.min(this.force * n._vy, limit));
n.x = n._x * n.graph.distance;
n.y = n._y * n.graph.distance;
}
n._vx = 0;
n._vy = 0;
}
},
copy: function(graph) {
var g = new GraphSpringLayout(graph);
g.k=this.k; g.force=this.force; g.repulsion=this.repulsion;
return g;
}
});
/*--- GRAPH SEARCH ---------------------------------------------------------------------------------*/
// depthFirstSearch(node, {visit:function(node){return false;}, traversable:function(node,edge){return true;}, _visited:null}
Graph.depthFirstSearch = function(node, a) {
/* Visits all the nodes connected to the given root node, depth-first.
* The visit function is called on each node.
* Recursion will stop if it returns true, and subsequently dfs() will return true.
* The traversable function takes the current node and edge,
* and returns true if we are allowed to follow this connection to the next node.
* For example, the traversable for directed edges is follows:
* function(node, edge) { return node == edge.node1; }
*/
if (a === undefined) a = {};
if (a.visit === undefined) a.visit = function(node) { return false; };
if (a.traversable === undefined) a.traversable = function(node, edge) { return true; };
var stop = visit(node);
a._visited = a._visited || {};
a._visited[node.id] = true;
for (var i=0; i < node.links.length; i++) {
var n = node.links[i];
if (stop) return true;
if (a.traversable(node, node.links.edge(n) == false)) continue;
if (!a._visited[n.id]) {
stop = Graph.depthFirstSearch(n, a);
}
}
return stop;
};
dfs = Graph.depthFirstSearch;
Graph.breadthFirstSearch = function(node, a) {
/* Visits all the nodes connected to the given root node, breadth-first.
*/
if (a === undefined) a = {};
if (a.visit === undefined) a.visit = function(node) { return false; };
if (a.traversable === undefined) a.traversable = function(node, edge) { return true; };
var q = [node];
var _visited = {};
while (q.length > 0) {
node = q.splice(0,1)[0];
if (!_visited[node.id]) {
if (a.visit(node))
return true;
for (var i=0; i < node.links.length; i++) {
var n = node.links[i];
if (a.traversable(node, node.links.edge(n)) != false) q.push(n);
}
_visited[node.id] = true;
}
}
return false;
};
bfs = Graph.breadthFirstSearch;
Graph.paths = function(graph, id1, id2, length, path, _root) {
/* Returns a list of paths from node with id1 to node with id2.
* Only paths shorter than the given length are included.
* Uses a brute-force DFS approach (performance drops exponentially for longer paths).
*/
if (path.length >= length) {
return [];
}
if (!(id1 in graph.nodeset)) {
return [];
}
if (id1 == id2) {
path = path.slice(); path.push(id1);
return [path];
}
path = path.slice(); path.push(id1);
var p = [];
var n = graph.nodeset[id1].links;
for (var i=0; i < n.length; i++) {
if (Array.index(path, n[i].id) < 0) {
p = p.push.apply(Graph.paths(graph, n[i].id, id2, length, path, false));
}
}
if (_root != false) p.sort(function(a, b) { return a.length-b.length; });
return p;
};
function edges(path) {
/* Returns a list of Edge objects for the given list of nodes.
* It contains null where two successive nodes are not connected.
*/
// For example, the distance (i.e., edge weight sum) of a path:
// var w = 0;
// var e = Graph.edges(path);
// for (var i=0; i < e.length; i++) w += e[i].weight;
if (path && path.length > 1) {
var e = [];
for (var i=0; i < path.length-1; i++) {
e.push(path[i].links.edge(path[i+1]));
}
return e;
}
return [];
};
/*--- GRAPH ADJACENCY ------------------------------------------------------------------------------*/
var Heap = Class.extend({
init: function() {
/* Items in the heap are ordered by weight (i.e. priority).
* Heap.pop() returns the item with the lowest weight.
*/
this.k = [];
this.w = [];
this.length = 0;
},
push: function (key, weight) {
var i = 0; while (i <= this.w.length && weight < (this.w[i]||Infinity)) i++;
this.k.splice(i, 0, key);
this.w.splice(i, 0, weight);
this.length += 1;
return true;
},
pop: function () {
this.length -= 1;
this.w.pop(); return this.k.pop();
}
});
// adjacency(graph, {directed:false, reversed:false, stochastic:false, heuristic:function(id1,id2){return 0;}})
Graph.adjacency = function(graph, a) {
/* Returns a dictionary indexed by node id1's,
* in which each value is a dictionary of connected node id2's linking to the edge weight.
* If directed=true, edges go from id1 to id2, but not the other way.
* If stochastic=true, all the weights for the neighbors of a given node sum to 1.
* A heuristic function can be given that takes two node id's and returns
* an additional cost for movement between the two nodes.
*/
if (a === undefined) a = {};
if (a.directed === undefined) a.directed = false;
if (a.reversed === undefined) a.reversed = false;
if (a.stochastic === undefined) a.stochastic = false;
var map = {};
for (var i=0; i < graph.nodes.length; i++) {
map[graph.nodes[i].id] = {};
}
for (var i=0; i < graph.edges.length; i++) {
var e = graph.edges[i];
var id1 = e[(a.reversed)?"node2":"node1"].id;
var id2 = e[(a.reversed)?"node1":"node2"].id;
map[id1][id2] = 1.0 - e.weight*0.5;
if (a.heuristic) {
map[id1][id2] += a.heuristic(id1, id2);
}
if (!a.directed) {
map[id2][id1] = map[id1][id2];
}
}
if (a.stochastic) {
for (var id1 in map) {
var n = Array.sum(Object.values(map[id1]));
for (var id2 in map[id1]) {
map[id1][id2] /= n;
}
}
}
return map;
};
// dijkstraShortestPath(graph, id1, id2, {heuristic:function(id1,id2){return 0;}, directed:false})
Graph.dijkstraShortestPath = function(graph, id1, id2, a) {
/* Dijkstra algorithm for finding shortest paths.
* Raises an IndexError between nodes on unconnected graphs.
*/
// Based on: Connelly Barnes, http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/119466
function flatten(array) {
// Flattens a linked list of the form [0,[1,[2,[]]]]
var a = [];
for (var i=0; i < array.length; i++) {
(array[i] instanceof Array)? a=a.concat(flatten(array[i])) : a.push(array[i]);
}
return a;
}
var G = Graph.adjacency(graph, a);
var q = new Heap(); q.push([0, id1, []], 0);
var visited = {};
for(;;) {
var x = q.pop(); cost=x[0]; n1=x[1]; path=x[2];
visited[n1] = true;
if (n1 == id2) {
var r = flatten(path);
r.reverse();
r.push(n1);
return r;
}
var path = [n1, path];
for (var n2 in G[n1] ) {
if (!visited[n2]) {
q.push([cost+G[n1][n2], n2, path], cost+G[n1][n2]);
}
}
}
};
// dijkstraShortestPaths(graph, id, {heuristic:function(id1,id2){return 0;}, directed:false})
Graph.dijkstraShortestPaths = function(graph, id, a) {
/* Dijkstra algorithm for finding the shortest paths from the given node to all other nodes.
* Returns a dictionary of node id's, each linking to a list of node id's (i.e., the path).
*/
// Based on: Dijkstra's algorithm for shortest paths modified from Eppstein.
// Based on: NetworkX 1.4.1: Aric Hagberg, Dan Schult and Pieter Swart.
var W = Graph.adjacency(graph, a);
var Q = new Heap(); // Use Q as a heap with [distance, node id] lists.
var D = {}; // Dictionary of final distances.
var P = {}; // Dictionary of paths.
P[id] = [id];
var seen = {id: 0};
Q.push([0, id], 0);
while(Q.length) {
var q = Q.pop(); dist=q[0]; v=q[1];
if (v in D) continue;
D[v] = dist;
for (var w in W[v]) {
var vw_dist = D[v] + W[v][w];
if (!(w in D) && (!(w in seen) || vw_dist < seen[w])) {
seen[w] = vw_dist;
Q.push([vw_dist, w], vw_dist);
P[w] = P[v].slice();
P[w].push(w);
}
}
}
for (var n in graph.nodeset) {
if (!(n in P)) P[n]=null;
}
return P;
};
/*--- GRAPH CENTRALITY -----------------------------------------------------------------------------*/
// brandesBetweennessCentrality(graph {normalized:true, directed:false})
Graph.brandesBetweennessCentrality = function(graph, a) {
/* Betweenness centrality for nodes in the graph.
* Betweenness centrality is a measure of the number of shortests paths that pass through a node.
* Nodes in high-density areas will get a good score.
*/
// Ulrik Brandes, A Faster Algorithm for Betweenness Centrality,
// Journal of Mathematical Sociology 25(2):163-177, 2001,
// http://www.inf.uni-konstanz.de/algo/publications/b-fabc-01.pdf
// Based on: Dijkstra's algorithm for shortest paths modified from Eppstein.
// Based on: NetworkX 1.0.1: Aric Hagberg, Dan Schult and Pieter Swart.
// http://python-networkx.sourcearchive.com/documentation/1.0.1/centrality_8py-source.html
if (a === undefined) a = {};
if (a.normalized === undefined) a.normalized = true;
if (a.directed === undefined) a.directed = false;
var W = Graph.adjacency(graph, a);
var b = {}; for (var n in graph.nodeset) b[n]=0.0;
for (var id in graph.nodeset) {
var Q = new Heap(); // Use Q as a heap with [distance, node id] lists.
var D = {}; // Dictionary of final distances.
var P = {}; // # Dictionary of paths.
for (var n in graph.nodeset) P[n]=[];
var seen = {id: 0};
Q.push([0, id, id], 0);
var S = [];
var E = {}; for (var n in graph.nodeset) E[n]=0; // sigma
E[id] = 1.0;
while(Q.length) {
var q = Q.pop(); dist=q[0]; pred=q[1]; v=q[2];
if (v in D) continue;
D[v] = dist;
S.push(v);
E[v] += E[pred];
for (var w in W[v]) {
var vw_dist = D[v] + W[v][w];
if (!(w in D) && (!(w in seen) || vw_dist < seen[w])) {
seen[w] = vw_dist;
Q.push([vw_dist, v, w], vw_dist);
P[w] = [v];
E[w] = 0.0;
} else if (vw_dist == seen[w]) { // Handle equal paths.
P[w].push(v);
E[w] = E[w] + E[v];
}
}
}
var d = {}; for (var v in graph.nodeset) d[v]=0;
while (S.length) {
var w = S.pop();
for (var i=0; i < P[w].length; i++) {
v = P[w][i];
d[v] += (1 + d[w]) * E[v] / E[w];
}
if (w != id) {
b[w] += d[w];
}
}
}
// Normalize between 0 and 1.
var m = a.normalized? Math.max.apply(Math, Object.values(b)) || 1 : 1;
for (var id in b) b[id] = b[id]/m;
return b;
};
// eigenvectorCentrality(graph {normalized:true, reversed:true, rating:{}, iterations:100, tolerance:0.0001})
Graph.eigenvectorCentrality = function(graph, a) {
/* Eigenvector centrality for nodes in the graph (cfr. Google's PageRank).
* Eigenvector centrality is a measure of the importance of a node in a directed network.
* It rewards nodes with a high potential of (indirectly) connecting to high-scoring nodes.
* Nodes with no incoming connections have a score of zero.
* If you want to measure outgoing connections, reversed should be False.
*/
// Based on: NetworkX, Aric Hagberg (hagberg@lanl.gov)
// http://python-networkx.sourcearchive.com/documentation/1.0.1/centrality_8py-source.html
// Note: much faster than betweenness centrality (which grows exponentially).
if (a === undefined) a = {};
if (a.normalized === undefined) a.normalized = true;
if (a.reversed === undefined) a.reversed = true;
if (a.rating === undefined) a.rating = {};
if (a.iterations === undefined) a.iterations = 100;
if (a.tolerance === undefined) a.tolerance = 0.0001;
function normalize(vector) {
var w = 1.0 / (Array.sum(Object.values(vector)) || 1);
for (var node in vector) {
vector[node] *= w;
}
}
var G = Graph.adjacency(graph, a);
var v = {}; for(var n in graph.nodeset) v[n] = Math.random(); normalize(v);
// Eigenvector calculation using the power iteration method: y = Ax.
// It has no guarantee of convergence.
for (var i=0; i < a.iterations; i++) {
var v0 = v
var v={}; for (var k in v0) v[k]=0;
for (var n1 in v) {
for (var n2 in G[n1]) {
v[n1] += 0.01 + v0[n2] * G[n1][n2] * (a.rating[n]? a.rating[n] : 1);
}
}
normalize(v);
var e=0; for (var n in v) e += Math.abs(v[n]-v0[n]); // Check for convergence.
if (e < graph.nodes.length * a.tolerance) {
// Normalize between 0 and 1.
var m = a.normalized? Math.max.apply(Math, Object.values(v)) || 1 : 1;
for (var id in v) v[id] /= m;
return v;
}
}
if (window.console) {
console.warn("node weight is 0 because Graph.eigenvectorCentrality() did not converge.");
}
var x={}; for (var n in graph.nodeset) x[n]=0;
return x;
};
/*--- GRAPH PARTITIONING ---------------------------------------------------------------------------*/
// a | b => all elements from a and all the elements from b.
// a & b => elements that appear in a as well as in b.
// a - b => elements that appear in a but not in b.
Array.union = function(a, b) {
return Array.unique(a.concat(b));
};
Array.intersection = function(a, b) {
var r=[], m={}, i;
for (i=0; i < b.length; i++) m[b[i]] = true;
for (i=0; i < a.length; i++) {
if (a[i] in m) r.push(a[i]);
}
return r;
};
Array.difference = function(a, b) {
var r=[], m={}, i;
for (i=0; i < b.length; i++) m[b[i]] = true;
for (i=0; i < a.length; i++) {
if (!(a[i] in m)) r.push(a[i]);
}
return r;
};
Graph.partition = function(graph) {
/* Returns a list of unconnected subgraphs.
*/
// Creates clusters of nodes and directly connected nodes.
// Iteratively merges two clusters if they overlap.
var g = [];
for (var i=0; i < graph.nodes.length; i++) {
var n = graph.nodes[i]; n=n.flatten();
var d = {}; for(var j=0; j < n.length; j++) d[n[j].id] = true;
g.push(Object.keys(d));
}
for (var i=g.length-1; i >= 0; i--) {
for(var j=g.length-1; j >= i+1; j--) {
if (g[i].length > 0 && g[j].length > 0 && Array.intersection(g[i], g[j]).length > 0) {
g[i] = Array.union(g[i], g[j]);
g[j] = [];
}
}
}
for (var i=g.length-1; i >= 0; i--) {
if (g[i].length == 0) g.splice(i,1);
}
for (var i=0; i < g.length; i++) {
var n = []; for(var j=0; j < g[i].length; j++) n[j] = graph.nodeset[g[i][j]];
g[i] = graph.copy(graph.canvas, nodes=n);
}
g.sort(function(a, b) { return b.length-a.length; });
return g;
};
Graph.isClique = function(graph) {
/* A clique is a set of nodes in which each node is connected to all other nodes.
*/
return (graph.density() == 1.0);
};
Graph.clique = function(graph, id) {
/* Returns the largest possible clique for the node with given id.
*/
if (id instanceof Node) {
id = id.id;
}
var a = [id];
for (var i=0; i < graph.nodes.length; i++) {
var n = graph.nodes[i];
var b = true;
for (var j=0; j < a.length; j++) {
if (n.id != a[i].id && graph.edge(n.id, a[i].id) == null) {
b = false;
break;
}
}
if (b && n.id != a[i].id) {
a.push(n.id);
}
return a;
}
};
Graph.cliques = function(graph, threshold) {
/* Returns all cliques in the graph with at least the given number of nodes.
*/
if (threshold === undefined) threshold = 3;
var a = [];
for (var i=0; i < graph.nodes.length; i++) {
var n = graph.nodes[i];
var c = Graph.clique(graph, n.id);
if (c.length >= threshold) {
c.sort();
if (Array.index(a, c) < 0) {
a.push(c);
}
}
}
};
/*--- GRAPH UTLITY FUNCTIONS -----------------------------------------------------------------------*/
Graph.unlink = function(graph, node1, node2) {
/* Removes the edges between node1 and node2.
* If only node1 is given, removes all edges to and from it.
* This does not remove node1 from the graph.
*/
if (!(node1 instanceof Node)) node1 = graph.nodeset[node1];
if (!(node2 instanceof Node)) node2 = graph.nodeset[node2];
var e = graph.edges.slice();
for (var i=0; i < e.length; i++) {
if ((node1 == e[i].node1 || node1 == e[i].node2) &&
(node2 == e[i].node1 || node2 == e[i].node2 || node2 === undefined)) {
graph.edges.splice(Array.index(graph.edges, e[i]), 1);
}
try {
node1.links.remove(Array.index(node1.links, node2), 1);
node2.links.remove(Array.index(node2.links, node1), 1);
} catch(x) { // node2 === undefined
}
}
};
Graph.redirect = function(graph, node1, node2) {
/* Connects all of node1's edges to node2 and unlinks node1.
*/
if (!(node1 instanceof Node)) node1 = graph.nodeset[node1];
if (!(node2 instanceof Node)) node2 = graph.nodeset[node2];
for (var i=0; i < graph.edges.length; i++) {
var e = graph.edges[i];
if (node1 == e.node1 || node1 == e.node2) {
if (e.node1 == node1 && e.node2 != node2) {
graph._addEdgeCopy(e, node2, e.node2);
}
if (e.node2 == node1 && e.node1 != node2) {
graph._addEdgeCopy(e, e.node1, node2);
}
}
}
Graph.unlink(graph, node1);
};
Graph.cut = function(graph, node) {
/* Unlinks the given node, but keeps edges intact by connecting the surrounding nodes.
* If A, B, C, D are nodes and A->B, B->C, B->D, if we then cut B: A->C, A->D.
*/
if (!(node instanceof Node)) node = graph.nodeset[node];
for (var i=0; i < graph.edges.length; i++) {
var e = graph.edges[i];
if (node == e.node1 || node == e.node2) {
for (var j=0; j < node.links.length; j++) {
var n = node.links[j];
if (e.node1 == node && e.node2 != n) {
graph._addEdgeCopy(e, n, e.node2);
}
if (e.node2 == node && e.node1 != n) {
graph._addEdgeCopy(e, e.node1, n);
}
}
}
}
Graph.unlink(graph, node);
};
Graph.insert = function(graph, node, a, b) {
/* Inserts the given node between node a and node b.
* If A, B, C are nodes and A->B, if we then insert C: A->C, C->B.
*/
if (!(node instanceof Node)) node = graph.nodeset[node];
if (!(a instanceof Node)) a = graph.nodeset[a];
if (!(b instanceof Node)) b = graph.nodeset[b];
for (var i=0; i < graph.edges.length; i++) {
var e = graph.edges[i];
if (e.node1 == a && e.node2 == b) {
graph._addEdgeCopy(e, a, node);
graph._addEdgeCopy(e, node, b);
}
if (e.node1 == b && e.node2 == a) {
grapg._addEdgeCopy(e, b, node);
graph._addEdgeCopy(e, node, a);
}
}
Graph.unlink(graph, a, b);
};