const KdTree = (function () {
/**
* k-d Tree JavaScript - V 1.0
*
* https://github.com/ubilabs/kd-tree-javascript
*
* @author Mircea Pricop <pricop@ubilabs.net>, 2012
* @author Martin Kleppe <kleppe@ubilabs.net>, 2012
* @author Ubilabs http://ubilabs.net, 2012
* @license MIT License <http://www.opensource.org/licenses/mit-license.php>
*/
function Node(obj, dimension, parent) {
this.obj = obj;
this.left = null;
this.right = null;
this.parent = parent;
this.dimension = dimension;
}
function kdTree(points, metric, dimensions) {
var self = this;
function buildTree(points, depth, parent) {
var dim = depth % dimensions.length,
median,
node;
if (points.length === 0) {
return null;
}
if (points.length === 1) {
return new Node(points[0], dim, parent);
}
points.sort(function (a, b) {
return a[dimensions[dim]] - b[dimensions[dim]];
});
median = Math.floor(points.length / 2);
node = new Node(points[median], dim, parent);
node.left = buildTree(points.slice(0, median), depth + 1, node);
node.right = buildTree(points.slice(median + 1), depth + 1, node);
return node;
}
// Reloads a serialied tree
function loadTree(data) {
// Just need to restore the `parent` parameter
self.root = data;
function restoreParent(root) {
if (root.left) {
root.left.parent = root;
restoreParent(root.left);
}
if (root.right) {
root.right.parent = root;
restoreParent(root.right);
}
}
restoreParent(self.root);
}
// If points is not an array, assume we're loading a pre-built tree
if (!Array.isArray(points)) loadTree(points, metric, dimensions);
else this.root = buildTree(points, 0, null);
// Convert to a JSON serializable structure; this just requires removing
// the `parent` property
this.toJSON = function (src) {
if (!src) src = this.root;
var dest = new Node(src.obj, src.dimension, null);
if (src.left) dest.left = self.toJSON(src.left);
if (src.right) dest.right = self.toJSON(src.right);
return dest;
};
this.insert = function (point) {
function innerSearch(node, parent) {
if (node === null) {
return parent;
}
var dimension = dimensions[node.dimension];
if (point[dimension] < node.obj[dimension]) {
return innerSearch(node.left, node);
} else {
return innerSearch(node.right, node);
}
}
var insertPosition = innerSearch(this.root, null),
newNode,
dimension;
if (insertPosition === null) {
this.root = new Node(point, 0, null);
return;
}
newNode = new Node(point, (insertPosition.dimension + 1) % dimensions.length, insertPosition);
dimension = dimensions[insertPosition.dimension];
if (point[dimension] < insertPosition.obj[dimension]) {
insertPosition.left = newNode;
} else {
insertPosition.right = newNode;
}
};
this.remove = function (point) {
var node;
function nodeSearch(node) {
if (node === null) {
return null;
}
if (node.obj === point) {
return node;
}
var dimension = dimensions[node.dimension];
if (point[dimension] < node.obj[dimension]) {
return nodeSearch(node.left, node);
} else {
return nodeSearch(node.right, node);
}
}
function removeNode(node) {
var nextNode, nextObj, pDimension;
function findMax(node, dim) {
var dimension, own, left, right, max;
if (node === null) {
return null;
}
dimension = dimensions[dim];
if (node.dimension === dim) {
if (node.right !== null) {
return findMax(node.right, dim);
}
return node;
}
own = node.obj[dimension];
left = findMax(node.left, dim);
right = findMax(node.right, dim);
max = node;
if (left !== null && left.obj[dimension] > own) {
max = left;
}
if (right !== null && right.obj[dimension] > max.obj[dimension]) {
max = right;
}
return max;
}
function findMin(node, dim) {
var dimension, own, left, right, min;
if (node === null) {
return null;
}
dimension = dimensions[dim];
if (node.dimension === dim) {
if (node.left !== null) {
return findMin(node.left, dim);
}
return node;
}
own = node.obj[dimension];
left = findMin(node.left, dim);
right = findMin(node.right, dim);
min = node;
if (left !== null && left.obj[dimension] < own) {
min = left;
}
if (right !== null && right.obj[dimension] < min.obj[dimension]) {
min = right;
}
return min;
}
if (node.left === null && node.right === null) {
if (node.parent === null) {
self.root = null;
return;
}
pDimension = dimensions[node.parent.dimension];
if (node.obj[pDimension] < node.parent.obj[pDimension]) {
node.parent.left = null;
} else {
node.parent.right = null;
}
return;
}
if (node.left !== null) {
nextNode = findMax(node.left, node.dimension);
} else {
nextNode = findMin(node.right, node.dimension);
}
nextObj = nextNode.obj;
removeNode(nextNode);
node.obj = nextObj;
}
node = nodeSearch(self.root);
if (node === null) {
return;
}
removeNode(node);
};
this.nearest = function (point, maxNodes, maxDistance) {
var i, result, bestNodes;
bestNodes = new BinaryHeap(function (e) {
return -e[1];
});
function nearestSearch(node) {
var bestChild,
dimension = dimensions[node.dimension],
ownDistance = metric(point, node.obj),
linearPoint = {},
linearDistance,
otherChild,
i;
function saveNode(node, distance) {
bestNodes.push([node, distance]);
if (bestNodes.size() > maxNodes) {
bestNodes.pop();
}
}
for (i = 0; i < dimensions.length; i += 1) {
if (i === node.dimension) {
linearPoint[dimensions[i]] = point[dimensions[i]];
} else {
linearPoint[dimensions[i]] = node.obj[dimensions[i]];
}
}
linearDistance = metric(linearPoint, node.obj);
if (node.right === null && node.left === null) {
if (bestNodes.size() < maxNodes || ownDistance < bestNodes.peek()[1]) {
saveNode(node, ownDistance);
}
return;
}
if (node.right === null) {
bestChild = node.left;
} else if (node.left === null) {
bestChild = node.right;
} else {
if (point[dimension] < node.obj[dimension]) {
bestChild = node.left;
} else {
bestChild = node.right;
}
}
nearestSearch(bestChild);
if (bestNodes.size() < maxNodes || ownDistance < bestNodes.peek()[1]) {
saveNode(node, ownDistance);
}
if (bestNodes.size() < maxNodes || Math.abs(linearDistance) < bestNodes.peek()[1]) {
if (bestChild === node.left) {
otherChild = node.right;
} else {
otherChild = node.left;
}
if (otherChild !== null) {
nearestSearch(otherChild);
}
}
}
if (maxDistance) {
for (i = 0; i < maxNodes; i += 1) {
bestNodes.push([null, maxDistance]);
}
}
nearestSearch(self.root);
result = [];
for (i = 0; i < maxNodes; i += 1) {
if (bestNodes.content[i]) {
result.push([bestNodes.content[i][0].obj, bestNodes.content[i][1]]);
}
}
return result;
};
this.balanceFactor = function () {
function height(node) {
if (node === null) {
return 0;
}
return Math.max(height(node.left), height(node.right)) + 1;
}
function count(node) {
if (node === null) {
return 0;
}
return count(node.left) + count(node.right) + 1;
}
return height(self.root) / (Math.log(count(self.root)) / Math.log(2));
};
}
// Binary heap implementation from:
// http://eloquentjavascript.net/appendix2.html
function BinaryHeap(scoreFunction) {
this.content = [];
this.scoreFunction = scoreFunction;
}
BinaryHeap.prototype = {
push: function (element) {
// Add the new element to the end of the array.
this.content.push(element);
// Allow it to bubble up.
this.bubbleUp(this.content.length - 1);
},
pop: function () {
// Store the first element so we can return it later.
var result = this.content[0];
// Get the element at the end of the array.
var end = this.content.pop();
// If there are any elements left, put the end element at the
// start, and let it sink down.
if (this.content.length > 0) {
this.content[0] = end;
this.sinkDown(0);
}
return result;
},
peek: function () {
return this.content[0];
},
remove: function (node) {
var len = this.content.length;
// To remove a value, we must search through the array to find
// it.
for (var i = 0; i < len; i++) {
if (this.content[i] == node) {
// When it is found, the process seen in 'pop' is repeated
// to fill up the hole.
var end = this.content.pop();
if (i != len - 1) {
this.content[i] = end;
if (this.scoreFunction(end) < this.scoreFunction(node)) this.bubbleUp(i);
else this.sinkDown(i);
}
return;
}
}
throw new Error("Node not found.");
},
size: function () {
return this.content.length;
},
bubbleUp: function (n) {
// Fetch the element that has to be moved.
var element = this.content[n];
// When at 0, an element can not go up any further.
while (n > 0) {
// Compute the parent element's index, and fetch it.
var parentN = Math.floor((n + 1) / 2) - 1,
parent = this.content[parentN];
// Swap the elements if the parent is greater.
if (this.scoreFunction(element) < this.scoreFunction(parent)) {
this.content[parentN] = element;
this.content[n] = parent;
// Update 'n' to continue at the new position.
n = parentN;
}
// Found a parent that is less, no need to move it further.
else {
break;
}
}
},
sinkDown: function (n) {
// Look up the target element and its score.
var length = this.content.length,
element = this.content[n],
elemScore = this.scoreFunction(element);
while (true) {
// Compute the indices of the child elements.
var child2N = (n + 1) * 2,
child1N = child2N - 1;
// This is used to store the new position of the element,
// if any.
var swap = null;
// If the first child exists (is inside the array)...
if (child1N < length) {
// Look it up and compute its score.
var child1 = this.content[child1N],
child1Score = this.scoreFunction(child1);
// If the score is less than our element's, we need to swap.
if (child1Score < elemScore) swap = child1N;
}
// Do the same checks for the other child.
if (child2N < length) {
var child2 = this.content[child2N],
child2Score = this.scoreFunction(child2);
if (child2Score < (swap == null ? elemScore : child1Score)) {
swap = child2N;
}
}
// If the element needs to be moved, swap it, and continue.
if (swap != null) {
this.content[n] = this.content[swap];
this.content[swap] = element;
n = swap;
}
// Otherwise, we are done.
else {
break;
}
}
},
};
return kdTree;
})();
const Graph = (function () {
function descriptor(object) {
var desc = {},
p;
for (p in object) {
desc[p] = {
value: object[p],
writable: true,
enumerable: true,
configurable: true,
};
}
return desc;
}
function merge(proto, object) {
return Object.create(proto, descriptor(object));
}
function extend(proto, object) {
var p;
for (p in object) {
proto[p] = object[p];
}
}
/*
* File: Graph.js
*
*/
/*
Class: Graph
A Graph Class that provides useful manipulation functions. You can find more manipulation methods in the <Graph.Util> object.
An instance of this class can be accessed by using the *graph* parameter of any tree or graph visualization.
Example:
(start code js)
//create new visualization
var viz = new $jit.Viz(options);
//load JSON data
viz.loadJSON(json);
//access model
viz.graph; //<Graph> instance
(end code)
Implements:
The following <Graph.Util> methods are implemented in <Graph>
- <Graph.Util.getNode>
- <Graph.Util.eachNode>
- <Graph.Util.computeLevels>
- <Graph.Util.eachBFS>
- <Graph.Util.clean>
- <Graph.Util.getClosestNodeToPos>
- <Graph.Util.getClosestNodeToOrigin>
*/
var Graph = function (opt) {
this.opt = merge(
{
node: {},
},
opt || {}
);
this.nodes = {};
this.edges = {};
};
Graph.fromJSON = function (json) {
var nodes = json.nodes,
edges = json.edges,
Node = Graph.Node,
Edge = Graph.Edge,
graph = new Graph(),
k;
for (k in nodes) {
nodes[k] = Node.fromJSON(nodes[k]);
}
graph.nodes = nodes;
for (k in edges) {
edges[k] = Edge.fromJSON(graph, edges[k]);
}
graph.edges = edges;
return graph;
};
Graph.prototype = {
clear: function () {
this.nodes = {};
this.edges = {};
},
//serialize
toJSON: function () {
var nodes = [],
edges = [],
gNodes = this.nodes,
gEdges = this.edges,
k,
from,
to;
for (k in gNodes) {
nodes.push(gNodes[k].toJSON());
}
for (from in gEdges) {
for (to in gEdges[from]) {
edges.push(gEdges[from][to].toJSON());
}
}
return { nodes: nodes, edges: edges };
},
/*
Method: getNode
Returns a <Graph.Node> by *id*.
Parameters:
id - (string) A <Graph.Node> id.
Example:
(start code js)
var node = graph.getNode('nodeId');
(end code)
*/
getNode: function (id) {
if (this.hasNode(id)) return this.nodes[id];
return false;
},
/*
Method: get
An alias for <Graph.Util.getNode>. Returns a node by *id*.
Parameters:
id - (string) A <Graph.Node> id.
Example:
(start code js)
var node = graph.get('nodeId');
(end code)
*/
get: function (id) {
return this.getNode(id);
},
/*
Method: getByName
Returns a <Graph.Node> by *name*.
Parameters:
name - (string) A <Graph.Node> name.
Example:
(start code js)
var node = graph.getByName('someName');
(end code)
*/
getByName: function (name) {
for (var id in this.nodes) {
var n = this.nodes[id];
if (n.name == name) return n;
}
return false;
},
/*
Method: getEdge
Returns a <Graph.Edge> object connecting nodes with ids *id* and *id2*.
Parameters:
id - (string) A <Graph.Node> id.
id2 - (string) A <Graph.Node> id.
*/
getEdge: function (id, id2) {
if (id in this.edges) {
return this.edges[id][id2];
}
return false;
},
/*
Method: addNode
Adds a node.
Parameters:
obj - An object with the properties described below
id - (string) A node id
name - (string) A node's name
data - (object) A node's data hash
See also:
<Graph.Node>
*/
addNode: function (obj) {
if (!this.nodes[obj.id]) {
var edges = (this.edges[obj.id] = {});
this.nodes[obj.id] = new Graph.Node(
merge(
{
id: obj.id,
name: obj.name,
data: merge(obj.data || {}, {}),
adjacencies: edges,
},
this.opt.node
)
);
}
return this.nodes[obj.id];
},
/*
Method: addEdge
Connects nodes specified by *obj* and *obj2*. If not found, nodes are created.
Parameters:
obj - (object) A <Graph.Node> object.
obj2 - (object) Another <Graph.Node> object.
data - (object) A data object. Used to store some extra information in the <Graph.Edge> object created.
See also:
<Graph.Node>, <Graph.Edge>
*/
addEdge: function (obj, obj2, data) {
if (!this.hasNode(obj.id)) {
this.addNode(obj);
}
if (!this.hasNode(obj2.id)) {
this.addNode(obj2);
}
obj = this.nodes[obj.id];
obj2 = this.nodes[obj2.id];
if (!obj.adjacentTo(obj2)) {
var adjsObj = (this.edges[obj.id] = this.edges[obj.id] || {});
var adjsObj2 = (this.edges[obj2.id] = this.edges[obj2.id] || {});
adjsObj[obj2.id] = adjsObj2[obj.id] = new Graph.Edge(obj, obj2, data, this.Edge, this.Label);
return adjsObj[obj2.id];
}
return this.edges[obj.id][obj2.id];
},
/*
Method: removeNode
Removes a <Graph.Node> matching the specified *id*.
Parameters:
id - (string) A node's id.
*/
removeNode: function (id) {
if (this.hasNode(id)) {
delete this.nodes[id];
var adjs = this.edges[id];
for (var to in adjs) {
delete this.edges[to][id];
}
delete this.edges[id];
}
},
/*
Method: removeEdge
Removes a <Graph.Edge> matching *id1* and *id2*.
Parameters:
id1 - (string) A <Graph.Node> id.
id2 - (string) A <Graph.Node> id.
*/
removeEdge: function (id1, id2) {
delete this.edges[id1][id2];
delete this.edges[id2][id1];
},
/*
Method: hasNode
Returns a boolean indicating if the node belongs to the <Graph> or not.
Parameters:
id - (string) Node id.
*/
hasNode: function (id) {
return id in this.nodes;
},
/*
Method: empty
Empties the Graph
*/
empty: function () {
this.nodes = {};
this.edges = {};
},
};
/*
Class: Graph.Node
A <Graph> node.
Implements:
<Accessors> methods.
The following <Graph.Util> methods are implemented by <Graph.Node>
- <Graph.Util.eachEdge>
- <Graph.Util.eachLevel>
- <Graph.Util.eachSubgraph>
- <Graph.Util.eachSubnode>
- <Graph.Util.anySubnode>
- <Graph.Util.getSubnodes>
- <Graph.Util.getParents>
- <Graph.Util.isDescendantOf>
*/
Graph.Node = function (opt) {
var innerOptions = {
id: "",
name: "",
data: {},
adjacencies: {},
};
extend(this, merge(innerOptions, opt));
};
Graph.Node.fromJSON = function (json) {
return new Graph.Node(json);
};
Graph.Node.prototype = {
toJSON: function () {
return {
id: this.id,
name: this.name,
data: this.serializeData(this.data),
};
},
serializeData: function (data) {
var serializedData = {},
parents = data.parents,
parentsCopy,
i,
l;
if (parents) {
parentsCopy = Array(parents.length);
for (i = 0, l = parents.length; i < l; ++i) {
parentsCopy[i] = parents[i].toJSON();
}
}
for (i in data) {
serializedData[i] = data[i];
}
delete serializedData.parents;
delete serializedData.bundle;
serializedData = JSON.parse(JSON.stringify(serializedData));
if (parentsCopy) {
serializedData.parents = parentsCopy;
}
return serializedData;
},
/*
Method: adjacentTo
Indicates if the node is adjacent to the node specified by id
Parameters:
id - (string) A node id.
Example:
(start code js)
node.adjacentTo('nodeId') == true;
(end code)
*/
adjacentTo: function (node) {
return node.id in this.adjacencies;
},
/*
Method: getAdjacency
Returns a <Graph.Edge> object connecting the current <Graph.Node> and the node having *id* as id.
Parameters:
id - (string) A node id.
*/
getEdge: function (id) {
return this.adjacencies[id];
},
/*
Method: toString
Returns a String with information on the Node.
*/
toString: function () {
return "Node(" + JSON.stringify([this.id, this.name, this.data, this.adjacencies]) + ")";
},
};
/*
Class: Graph.Edge
A <Graph> adjacence (or edge) connecting two <Graph.Nodes>.
Implements:
<Accessors> methods.
See also:
<Graph>, <Graph.Node>
Properties:
nodeFrom - A <Graph.Node> connected by this edge.
nodeTo - Another <Graph.Node> connected by this edge.
data - Node data property containing a hash (i.e {}) with custom options.
*/
Graph.Edge = function (nodeFrom, nodeTo, data) {
this.nodeFrom = nodeFrom;
this.nodeTo = nodeTo;
this.data = data || {};
};
Graph.Edge.fromJSON = function (graph, edgeJSON) {
return new Graph.Edge(graph.get(edgeJSON.nodeFrom), graph.get(edgeJSON.nodeTo), edgeJSON.data);
};
Graph.Edge.prototype.toJSON = function () {
return {
nodeFrom: this.nodeFrom.id,
nodeTo: this.nodeTo.id,
data: this.data,
};
};
/*
Object: Graph.Util
<Graph> traversal and processing utility object.
Note:
For your convenience some of these methods have also been appended to <Graph> and <Graph.Node> classes.
*/
Graph.Util = {
/*
filter
For internal use only. Provides a filtering function based on flags.
*/
filter: function (param) {
if (!param || !(typeof param == "string"))
return function () {
return true;
};
var props = param.split(" ");
return function (elem) {
for (var i = 0; i < props.length; i++) {
if (elem[props[i]]) {
return false;
}
}
return true;
};
},
/*
Method: getNode
Returns a <Graph.Node> by *id*.
Also implemented by:
<Graph>
Parameters:
graph - (object) A <Graph> instance.
id - (string) A <Graph.Node> id.
Example:
(start code js)
$jit.Graph.Util.getNode(graph, 'nodeid');
//or...
graph.getNode('nodeid');
(end code)
*/
getNode: function (graph, id) {
return graph.nodes[id];
},
/*
Method: eachNode
Iterates over <Graph> nodes performing an *action*.
Also implemented by:
<Graph>.
Parameters:
graph - (object) A <Graph> instance.
action - (function) A callback function having a <Graph.Node> as first formal parameter.
Example:
(start code js)
$jit.Graph.Util.eachNode(graph, function(node) {
alert(node.name);
});
//or...
graph.eachNode(function(node) {
alert(node.name);
});
(end code)
*/
eachNode: function (graph, action, flags) {
var filter = this.filter(flags);
for (var i in graph.nodes) {
if (filter(graph.nodes[i])) action(graph.nodes[i]);
}
},
/*
Method: each
Iterates over <Graph> nodes performing an *action*. It's an alias for <Graph.Util.eachNode>.
Also implemented by:
<Graph>.
Parameters:
graph - (object) A <Graph> instance.
action - (function) A callback function having a <Graph.Node> as first formal parameter.
Example:
(start code js)
$jit.Graph.Util.each(graph, function(node) {
alert(node.name);
});
//or...
graph.each(function(node) {
alert(node.name);
});
(end code)
*/
each: function (graph, action, flags) {
this.eachNode(graph, action, flags);
},
/*
Method: eachEdge
Iterates over <Graph.Node> adjacencies applying the *action* function.
Also implemented by:
<Graph.Node>.
Parameters:
node - (object) A <Graph.Node>.
action - (function) A callback function having <Graph.Edge> as first formal parameter.
Example:
(start code js)
$jit.Graph.Util.eachEdge(node, function(adj) {
alert(adj.nodeTo.name);
});
//or...
node.eachEdge(function(adj) {
alert(adj.nodeTo.name);
});
(end code)
*/
eachEdge: function (node, action, flags) {
var adj = node.adjacencies,
filter = this.filter(flags);
for (var id in adj) {
var a = adj[id];
if (filter(a)) {
if (a.nodeFrom != node) {
var tmp = a.nodeFrom;
a.nodeFrom = a.nodeTo;
a.nodeTo = tmp;
}
action(a, id);
}
}
},
/*
Method: computeLevels
Performs a BFS traversal setting the correct depth for each node.
Also implemented by:
<Graph>.
Note:
The depth of each node can then be accessed by
>node.depth
Parameters:
graph - (object) A <Graph>.
id - (string) A starting node id for the BFS traversal.
startDepth - (optional|number) A minimum depth value. Default's 0.
*/
computeLevels: function (graph, id, startDepth, flags) {
startDepth = startDepth || 0;
var filter = this.filter(flags);
this.eachNode(
graph,
function (elem) {
elem._flag = false;
elem.depth = -1;
},
flags
);
var root = graph.getNode(id);
root.depth = startDepth;
var queue = [root];
while (queue.length != 0) {
var node = queue.pop();
node._flag = true;
this.eachEdge(
node,
function (adj) {
var n = adj.nodeTo;
if (n._flag == false && filter(n) && !adj._hiding) {
if (n.depth < 0) n.depth = node.depth + 1 + startDepth;
queue.unshift(n);
}
},
flags
);
}
},
/*
Method: eachBFS
Performs a BFS traversal applying *action* to each <Graph.Node>.
Also implemented by:
<Graph>.
Parameters:
graph - (object) A <Graph>.
id - (string) A starting node id for the BFS traversal.
action - (function) A callback function having a <Graph.Node> as first formal parameter.
Example:
(start code js)
$jit.Graph.Util.eachBFS(graph, 'mynodeid', function(node) {
alert(node.name);
});
//or...
graph.eachBFS('mynodeid', function(node) {
alert(node.name);
});
(end code)
*/
eachBFS: function (graph, id, action, flags) {
var filter = this.filter(flags);
this.clean(graph);
var queue = [graph.getNode(id)];
while (queue.length != 0) {
var node = queue.pop();
if (!node) return;
node._flag = true;
action(node, node.depth);
this.eachEdge(
node,
function (adj) {
var n = adj.nodeTo;
if (n._flag == false && filter(n) && !adj._hiding) {
n._flag = true;
queue.unshift(n);
}
},
flags
);
}
},
/*
Method: eachLevel
Iterates over a node's subgraph applying *action* to the nodes of relative depth between *levelBegin* and *levelEnd*.
In case you need to break the iteration, *action* should return false.
Also implemented by:
<Graph.Node>.
Parameters:
node - (object) A <Graph.Node>.
levelBegin - (number) A relative level value.
levelEnd - (number) A relative level value.
action - (function) A callback function having a <Graph.Node> as first formal parameter.
*/
eachLevel: function (node, levelBegin, levelEnd, action, flags) {
var d = node.depth,
filter = this.filter(flags),
that = this,
shouldContinue = true;
levelEnd = levelEnd === false ? Number.MAX_VALUE - d : levelEnd;
(function loopLevel(node, levelBegin, levelEnd) {
if (!shouldContinue) return;
var d = node.depth,
ret;
if (d >= levelBegin && d <= levelEnd && filter(node)) ret = action(node, d);
if (typeof ret !== "undefined") shouldContinue = ret;
if (shouldContinue && d < levelEnd) {
that.eachEdge(node, function (adj) {
var n = adj.nodeTo;
if (n.depth > d) loopLevel(n, levelBegin, levelEnd);
});
}
})(node, levelBegin + d, levelEnd + d);
},
/*
Method: eachSubgraph
Iterates over a node's children recursively.
Also implemented by:
<Graph.Node>.
Parameters:
node - (object) A <Graph.Node>.
action - (function) A callback function having a <Graph.Node> as first formal parameter.
Example:
(start code js)
$jit.Graph.Util.eachSubgraph(node, function(node) {
alert(node.name);
});
//or...
node.eachSubgraph(function(node) {
alert(node.name);
});
(end code)
*/
eachSubgraph: function (node, action, flags) {
this.eachLevel(node, 0, false, action, flags);
},
/*
Method: eachSubnode
Iterates over a node's children (without deeper recursion).
Also implemented by:
<Graph.Node>.
Parameters:
node - (object) A <Graph.Node>.
action - (function) A callback function having a <Graph.Node> as first formal parameter.
Example:
(start code js)
$jit.Graph.Util.eachSubnode(node, function(node) {
alert(node.name);
});
//or...
node.eachSubnode(function(node) {
alert(node.name);
});
(end code)
*/
eachSubnode: function (node, action, flags) {
this.eachLevel(node, 1, 1, action, flags);
},
/*
Method: anySubnode
Returns *true* if any subnode matches the given condition.
Also implemented by:
<Graph.Node>.
Parameters:
node - (object) A <Graph.Node>.
cond - (function) A callback function returning a Boolean instance. This function has as first formal parameter a <Graph.Node>.
Example:
(start code js)
$jit.Graph.Util.anySubnode(node, function(node) { return node.name == "mynodename"; });
//or...
node.anySubnode(function(node) { return node.name == 'mynodename'; });
(end code)
*/
anySubnode: function (node, cond, flags) {
var flag = false;
cond =
cond ||
function () {
return true;
};
var c =
typeof cond == "string"
? function (n) {
return n[cond];
}
: cond;
this.eachSubnode(
node,
function (elem) {
if (c(elem)) flag = true;
},
flags
);
return flag;
},
/*
Method: getSubnodes
Collects all subnodes for a specified node.
The *level* parameter filters nodes having relative depth of *level* from the root node.
Also implemented by:
<Graph.Node>.
Parameters:
node - (object) A <Graph.Node>.
level - (optional|number) Default's *0*. A starting relative depth for collecting nodes.
Returns:
An array of nodes.
*/
getSubnodes: function (node, level, flags) {
var ans = [],
that = this;
level = level || 0;
var levelStart, levelEnd;
if (Array.isArray(level) == "array") {
levelStart = level[0];
levelEnd = level[1];
} else {
levelStart = level;
levelEnd = Number.MAX_VALUE - node.depth;
}
this.eachLevel(
node,
levelStart,
levelEnd,
function (n) {
ans.push(n);
},
flags
);
return ans;
},
/*
Method: getParents
Returns an Array of <Graph.Nodes> which are parents of the given node.
Also implemented by:
<Graph.Node>.
Parameters:
node - (object) A <Graph.Node>.
Returns:
An Array of <Graph.Nodes>.
Example:
(start code js)
var pars = $jit.Graph.Util.getParents(node);
//or...
var pars = node.getParents();
if(pars.length > 0) {
//do stuff with parents
}
(end code)
*/
getParents: function (node) {
var ans = [];
this.eachEdge(node, function (adj) {
var n = adj.nodeTo;
if (n.depth < node.depth) ans.push(n);
});
return ans;
},
/*
Method: isDescendantOf
Returns a boolean indicating if some node is descendant of the node with the given id.
Also implemented by:
<Graph.Node>.
Parameters:
node - (object) A <Graph.Node>.
id - (string) A <Graph.Node> id.
Example:
(start code js)
$jit.Graph.Util.isDescendantOf(node, "nodeid"); //true|false
//or...
node.isDescendantOf('nodeid');//true|false
(end code)
*/
isDescendantOf: function (node, id) {
if (node.id == id) return true;
var pars = this.getParents(node),
ans = false;
for (var i = 0; !ans && i < pars.length; i++) {
ans = ans || this.isDescendantOf(pars[i], id);
}
return ans;
},
/*
Method: clean
Cleans flags from nodes.
Also implemented by:
<Graph>.
Parameters:
graph - A <Graph> instance.
*/
clean: function (graph) {
this.eachNode(graph, function (elem) {
elem._flag = false;
});
},
};
//Append graph methods to <Graph>
["get", "getNode", "each", "eachNode", "computeLevels", "eachBFS", "clean"].forEach(function (m) {
Graph.prototype[m] = function () {
return Graph.Util[m].apply(Graph.Util, [this].concat(Array.prototype.slice.call(arguments)));
};
});
//Append node methods to <Graph.Node>
[
"eachEdge",
"eachLevel",
"eachSubgraph",
"eachSubnode",
"anySubnode",
"getSubnodes",
"getParents",
"isDescendantOf",
].forEach(function (m) {
Graph.Node.prototype[m] = function () {
return Graph.Util[m].apply(Graph.Util, [this].concat(Array.prototype.slice.call(arguments)));
};
});
return Graph;
})();
module.exports = (function () {
//General convenience functions and constants
Math.PHI = (1 + Math.sqrt(5)) / 2;
function $dist(a, b) {
var diffX = a[0] - b[0],
diffY = a[1] - b[1];
return Math.sqrt(diffX * diffX + diffY * diffY);
}
function $norm(a) {
return Math.sqrt(a[0] * a[0] + a[1] * a[1]);
}
function $normalize(a) {
var n = $norm(a);
return $mult(1 / n, a);
}
function $lerp(a, b, delta) {
return [a[0] * (1 - delta) + b[0] * delta, a[1] * (1 - delta) + b[1] * delta];
}
function $add(a, b) {
return [a[0] + b[0], a[1] + b[1]];
}
function $sub(a, b) {
return [a[0] - b[0], a[1] - b[1]];
}
function $dot(a, b) {
return a[0] * b[0] + a[1] * b[1];
}
function $mult(k, a) {
return [a[0] * k, a[1] * k];
}
function $lerpPoint(from, to, delta) {
return [$lerp(from[0], to[0], delta), $lerp(from[1], to[1], delta)];
}
function cloneJSON(json) {
return JSON.parse(JSON.stringify(json));
}
function cloneEdge(json) {
var i,
l = json.length,
ans = Array(json.length);
for (i = 0; i < l; ++i) {
ans[i] = {
node: json[i].node,
pos: json[i].pos,
normal: json[i].normal && json[i].normal.slice(),
};
}
return ans;
}
//Extend generic Graph class with bundle methods and rendering options
function expandEdgesHelper(node, array, collect) {
var coords = node.data.coords,
i,
l,
p,
ps;
if (!array.length) {
array.push([(coords[0] + coords[2]) / 2, (coords[1] + coords[3]) / 2]);
}
array.unshift([coords[0], coords[1]]);
array.push([coords[2], coords[3]]);
ps = node.data.parents;
if (ps) {
for (i = 0, l = ps.length; i < l; ++i) {
expandEdgesHelper(ps[i], array.slice(), collect);
}
} else {
collect.push(array);
}
}
function setNormalVector(nodeFrom, nodeTo) {
var node = nodeFrom || nodeTo,
dir,
coords,
normal;
if (!nodeFrom || !nodeTo) {
coords = node.data.coords;
dir = [coords[2] - coords[0], coords[3] - coords[1]];
normal = [-dir[1], dir[0]];
normal = $mult(normal, 1 / $norm(normal));
}
return normal;
}
function createPosItem(node, pos, index, total) {
return {
node: node, //.toJSON(),
pos: pos,
normal: null,
};
}
//Extend generic Graph class with bundle methods and rendering options
function expandEdgesRichHelper(node, array, collect) {
var coords = node.data.coords,
i,
l,
p,
ps,
a,
posItem;
ps = node.data.parents;
if (ps) {
for (i = 0, l = ps.length; i < l; ++i) {
a = array.slice();
if (!a.length) {
p = [(coords[0] + coords[2]) / 2, (coords[1] + coords[3]) / 2];
posItem = createPosItem(node, p, i, l);
a.push(posItem);
}
posItem = createPosItem(node, [coords[0], coords[1]], i, l);
a.unshift(posItem);
posItem = createPosItem(node, [coords[2], coords[3]], i, l);
a.push(posItem);
expandEdgesRichHelper(ps[i], a, collect);
}
} else {
a = array.slice();
if (!a.length) {
p = [(coords[0] + coords[2]) / 2, (coords[1] + coords[3]) / 2];
posItem = createPosItem(node, p, 0, 1);
a.push(posItem);
}
posItem = createPosItem(node, [coords[0], coords[1]], 0, 1);
a.unshift(posItem);
posItem = createPosItem(node, [coords[2], coords[3]], 0, 1);
a.push(posItem);
collect.push(a);
}
}
Graph.Node.prototype.expandEdges = function () {
if (this.expandedEdges) {
return this.expandedEdges;
}
var ans = [];
expandEdgesRichHelper(this, [], ans);
this.expandedEdges = ans;
return ans;
};
Graph.Node.prototype.unbundleEdges = function (delta) {
var expandedEdges = this.expandEdges(),
ans = Array(expandedEdges.length),
min = Math.min,
i,
l,
j,
n,
edge,
edgeCopy,
normal,
x0,
xk,
xk_x0,
xi,
xi_x0,
xi_bar,
dot,
norm,
norm2,
c,
last;
delta = delta || 0;
this.unbundledEdges = this.unbundledEdges || {};
if ((delta === 0 || delta === 1) && this.unbundledEdges[delta]) {
return this.unbundledEdges[delta];
}
for (i = 0, l = expandedEdges.length; i < l; ++i) {
edge = expandedEdges[i];
last = edge.length - 1;
edgeCopy = cloneEdge(edge);
//edgeCopy = cloneJSON(edge);
x0 = edge[0].pos;
xk = edge[last].pos;
xk_x0 = $sub(xk, x0);
edgeCopy[0].unbundledPos = edgeCopy[0].pos.slice();
normal = $sub(edgeCopy[1].pos, edgeCopy[0].pos);
normal = $normalize([-normal[1], normal[0]]);
edgeCopy[0].normal = normal;
edgeCopy[last].unbundledPos = edgeCopy[edge.length - 1].pos.slice();
normal = $sub(edgeCopy[last].pos, edgeCopy[last - 1].pos);
normal = $normalize([-normal[1], normal[0]]);
edgeCopy[last].normal = normal;
for (j = 1, n = edge.length - 1; j < n; ++j) {
xi = edge[j].pos;
xi_x0 = $sub(xi, x0);
dot = $dot(xi_x0, xk_x0);
norm = $dist(xk, x0);
norm2 = norm * norm;
c = dot / norm2;
xi_bar = $add(x0, $mult(c, xk_x0));
edgeCopy[j].unbundledPos = $lerp(xi_bar, xi, delta);
normal = $sub(edgeCopy[j + 1].pos, edgeCopy[j - 1].pos);
normal = $normalize([-normal[1], normal[0]]);
edgeCopy[j].normal = normal;
}
ans[i] = edgeCopy;
}
if (delta === 0 || delta === 1) {
this.unbundledEdges[delta] = ans;
}
return ans;
};
Graph.Render = {
renderLine: function (ctx, edges, options) {
options = options || {};
var lineWidth = options.lineWidth || 1,
fillStyle = options.fillStyle || "gray",
i,
l,
j,
n,
e,
pos;
ctx.fillStyle = fillStyle;
ctx.lineWidth = lineWidth;
for (i = 0, l = edges.length; i < l; ++i) {
e = edges[i];
ctx.beginPath();
for (j = 0, n = e.length; j < n; ++j) {
pos = e[j].unbundledPos;
if (j == 0) {
ctx.moveTo(pos[0], pos[1]);
} else {
ctx.lineTo(pos[0], pos[1]);
}
}
ctx.stroke();
ctx.closePath();
}
},
renderQuadratic: function (ctx, edges, options) {
options = options || {};
var lineWidth = options.lineWidth || 1,
fillStyle = options.fillStyle || "gray",
margin = (options.margin || 0) * (options.delta || 0),
lengthBefore,
lengthAfter,
index,
i,
l,
j,
k,
n,
e,
node,
pos,
pos0,
pos1,
pos2,
pos3,
pos01,
pos02,
pos03,
pos04,
colorFrom,
colorTo,
grd,
midPos,
quadStart,
weightStart,
posStart,
nodeStart,
posItem,
posItemStart,
dist,
distMin,
nodeArray,
nodeLength;
ctx.fillStyle = fillStyle;
ctx.lineWidth = lineWidth;
for (i = 0, l = edges.length; i < l; ++i) {
e = edges[i];
quadStart = null;
posStart = null;
nodeStart = e[0].node;
ctx.lineWidth = (Math.max(1, nodeStart.data.weight) || 1) * (options.scale || 1);
if (nodeStart.data.color && Array.isArray(nodeStart.data.color)) {
colorFrom = nodeStart.data.color[0];
colorTo = nodeStart.data.color[1];
grd = ctx.createLinearGradient(
nodeStart.data.coords[0],
nodeStart.data.coords[1],
nodeStart.data.coords[2],
nodeStart.data.coords[3]
);
grd.addColorStop(0, colorFrom);
grd.addColorStop(0.4, colorFrom);
grd.addColorStop(0.6, colorTo);
grd.addColorStop(1, colorTo);
ctx.strokeStyle = grd;
} else {
ctx.strokeStyle = nodeStart.data.color || ctx.strokeStyle;
}
ctx.globalAlpha = nodeStart.data.alpha == undefined ? 1 : nodeStart.data.alpha;
ctx.beginPath();
for (j = 0, n = e.length; j < n; ++j) {
posItem = e[j];
pos = posItem.unbundledPos;
if (j !== 0) {
pos0 = posStart || e[j - 1].unbundledPos;
pos = this.adjustPosition(nodeStart.id, posItem, pos, margin, options.delta || 0);
midPos = $lerp(pos0, pos, 0.5);
pos1 = $lerp(pos0, midPos, j === 1 ? 0 : options.curviness || 0);
pos3 = pos;
pos2 = $lerp(midPos, pos3, j === n - 1 ? 1 : 1 - (options.curviness || 0));
//ctx.lineCap = 'butt';//'round';
//ctx.beginPath();
if (quadStart) {
//ctx.strokeStyle = 'black';
ctx.moveTo(quadStart[0], quadStart[1]);
ctx.quadraticCurveTo(pos0[0], pos0[1], pos1[0], pos1[1]);
//ctx.stroke();
//ctx.closePath();
}
//ctx.beginPath();
//ctx.strokeStyle = 'red';
ctx.moveTo(pos1[0], pos1[1]);
ctx.lineTo(pos2[0], pos2[1]);
//ctx.stroke();
//ctx.closePath();
quadStart = pos2;
posStart = pos;
}
}
ctx.stroke();
ctx.closePath();
}
},
adjustPosition: function (id, posItem, pos, margin, delta) {
var nodeArray = posItem.node.data.nodeArray,
epsilon = 1,
nodeLength,
index,
lengthBefore,
lengthAfter,
k,
node;
if (nodeArray) {
nodeLength = nodeArray.length;
index = Infinity;
lengthBefore = 0;
lengthAfter = 0;
for (k = 0; k < nodeLength; ++k) {
node = nodeArray[k];
if (node.id == id) {
index = k;
}
if (k < index) {
lengthBefore += (node.data.weight || 0) + margin;
} else if (k > index) {
lengthAfter += (node.data.weight || 0) + margin;
}
}
//remove -margin to get the line weight into account.
//pos = $add(pos, $mult((lengthBefore - (lengthBefore + lengthAfter) / 2) * -margin, posItem.normal));
pos = $add(
pos,
$mult(
(lengthBefore - (lengthBefore + lengthAfter) / 2) * Math.min(epsilon, delta),
posItem.normal
)
);
}
return pos;
},
renderBezier: function (ctx, edges, options) {
options = options || {};
var pct = options.curviness || 0,
i,
l,
j,
n,
e,
pos,
midpoint,
c1,
c2,
start,
end;
for (i = 0, l = edges.length; i < l; ++i) {
e = edges[i];
start = e[0].unbundledPos;
ctx.strokeStyle = e[0].node.data.color || ctx.strokeStyle;
ctx.lineWidth = e[0].node.data.weight || 1;
midpoint = e[(e.length - 1) / 2].unbundledPos;
if (e.length > 3) {
c1 = e[1].unbundledPos;
c2 = e[(e.length - 1) / 2 - 1].unbundledPos;
end = $lerp(midpoint, c2, 1 - pct);
ctx.beginPath();
ctx.moveTo(start[0], start[1]);
ctx.bezierCurveTo(c1[0], c1[1], c2[0], c2[1], end[0], end[1]);
c1 = e[(e.length - 1) / 2 + 1].unbundledPos;
c2 = e[e.length - 2].unbundledPos;
end = e[e.length - 1].unbundledPos;
if (1 - pct) {
//line to midpoint + pct of something
start = $lerp(midpoint, c1, 1 - pct);
ctx.lineTo(start[0], start[1]);
}
ctx.bezierCurveTo(c1[0], c1[1], c2[0], c2[1], end[0], end[1]);
ctx.stroke();
ctx.closePath();
} else {
ctx.beginPath();
ctx.moveTo(start[0], start[1]);
end = e[e.length - 1].unbundledPos;
ctx.lineTo(end[0], end[1]);
}
}
},
};
//Edge bundling algorithm class.
function Bundler(options) {
this.options = options || {};
this.graph = new Graph();
this.kdTree = null;
}
//copy static methods to render lines and other from Graph
Bundler.Graph = Graph.Render;
Bundler.prototype = {
setNodes: function (nodes) {
var i,
l,
graph = this.graph;
graph.clear();
for (i = 0, l = nodes.length; i < l; ++i) {
graph.addNode(nodes[i]);
}
},
buildKdTree: function () {
var nodeArray = [];
this.graph.each(function (n) {
var coords = n.data.coords;
n.x = coords[0];
n.y = coords[1];
n.z = coords[2];
n.w = coords[3];
nodeArray.push(n);
});
this.kdTree = new KdTree(
nodeArray,
function (a, b) {
var diff0 = a.x - b.x,
diff1 = a.y - b.y,
diff2 = a.z - b.z,
diff3 = a.w - b.w;
return Math.sqrt(diff0 * diff0 + diff1 * diff1 + diff2 * diff2 + diff3 * diff3);
},
["x", "y", "z", "w"]
);
},
buildNearestNeighborGraph: function (k) {
k = k || 10;
var graph = this.graph,
node,
dist,
kdTree;
this.buildKdTree();
kdTree = this.kdTree;
graph.each(function (n) {
var nodes = kdTree.nearest(n, k),
i,
l;
for (i = 0, l = nodes.length; i < l; ++i) {
node = nodes[i][0];
dist = nodes[i][1];
if (node.id != n.id) {
graph.addEdge(n, node);
}
}
});
},
computeIntermediateNodePositions: function (node) {
var m1, m2, centroids, a, b, c, tau, f, res;
if (!node.data.nodes) {
return;
}
centroids = this.getCentroids(node.data.nodes);
f = this.costFunction.bind(this, node, centroids);
a = 0;
b = 1;
c = 0.72; //because computers
tau = 0.1;
res = this.goldenSectionSearch(a, b, c, tau, f);
f(res); //set m1 and m2;
},
costFunction: function (node, centroids, x) {
var top, bottom, m1, m2, ink, alpha, p;
x /= 2;
top = centroids[0];
bottom = centroids[1];
m1 = $lerp(top, bottom, x);
m2 = $lerp(top, bottom, 1 - x);
node.data.m1 = m1;
node.data.m2 = m2;
delete node.data.ink;
ink = this.getInkValue(node);
alpha = this.getMaxTurningAngleValue(node, m1, m2);
p = this.options.angleStrength || 1.2;
return ink * (1 + Math.sin(alpha) / p);
},
goldenSectionSearch: function (a, b, c, tau, f) {
var phi = Math.PHI,
resphi = 2 - Math.PHI,
abs = Math.abs,
x;
if (c - b > b - a) {
x = b + resphi * (c - b);
} else {
x = b - resphi * (b - a);
}
if (abs(c - a) < tau * (abs(b) + abs(x))) {
return (c + a) / 2;
}
if (f(x) < f(b)) {
if (c - b > b - a) {
return this.goldenSectionSearch(b, x, c, tau, f);
}
return this.goldenSectionSearch(a, x, b, tau, f);
}
if (c - b > b - a) {
return this.goldenSectionSearch(a, b, x, tau, f);
}
return this.goldenSectionSearch(x, b, c, tau, f);
},
getCentroids: function (nodes) {
var topCentroid = [0, 0],
bottomCentroid = [0, 0],
coords,
i,
l;
for (i = 0, l = nodes.length; i < l; ++i) {
coords = nodes[i].data.coords;
topCentroid[0] += coords[0];
topCentroid[1] += coords[1];
bottomCentroid[0] += coords[2];
bottomCentroid[1] += coords[3];
}
topCentroid[0] /= l;
topCentroid[1] /= l;
bottomCentroid[0] /= l;
bottomCentroid[1] /= l;
return [topCentroid, bottomCentroid];
},
getInkValue: function (node, depth) {
var data = node.data,
sqrt = Math.sqrt,
coords,
diffX,
diffY,
m1,
m2,
acum,
i,
l,
nodes,
ni;
depth = depth || 0;
//bundled node
if (!depth && (data.bundle || data.nodes)) {
nodes = data.bundle ? data.bundle.data.nodes : data.nodes;
m1 = data.m1;
m2 = data.m2;
acum = 0;
for (i = 0, l = nodes.length; i < l; ++i) {
ni = nodes[i];
coords = ni.data.coords;
diffX = m1[0] - coords[0];
diffY = m1[1] - coords[1];
acum += $norm([diffX, diffY]);
diffX = m2[0] - coords[2];
diffY = m2[1] - coords[3];
acum += $norm([diffX, diffY]);
acum += this.getInkValue(ni, depth + 1);
}
if (!depth) {
acum += $dist(m1, m2);
}
return (node.data.ink = acum);
}
//coalesced node
if (data.parents) {
nodes = data.parents;
m1 = [data.coords[0], data.coords[1]];
m2 = [data.coords[2], data.coords[3]];
acum = 0;
for (i = 0, l = nodes.length; i < l; ++i) {
ni = nodes[i];
coords = ni.data.coords;
diffX = m1[0] - coords[0];
diffY = m1[1] - coords[1];
acum += $norm([diffX, diffY]);
diffX = m2[0] - coords[2];
diffY = m2[1] - coords[3];
acum += $norm([diffX, diffY]);
acum += this.getInkValue(ni, depth + 1);
}
//only add the distance if this is the first recursion
if (!depth) {
acum += $dist(m1, m2);
}
return (node.data.ink = acum);
}
//simple node
if (depth) {
return (node.data.ink = 0);
}
coords = node.data.coords;
diffX = coords[0] - coords[2];
diffY = coords[1] - coords[3];
return (node.data.ink = $norm([diffX, diffY]));
},
getMaxTurningAngleValue: function (node, m1, m2) {
var sqrt = Math.sqrt,
abs = Math.abs,
acos = Math.acos,
m2Tom1 = [m1[0] - m2[0], m1[1] - m2[1]],
m1Tom2 = [-m2Tom1[0], -m2Tom1[1]],
m1m2Norm = $norm(m2Tom1),
angle = 0,
nodes,
vec,
norm,
dot,
angleValue,
x,
y,
coords,
i,
l,
n;
if (node.data.bundle || node.data.nodes) {
nodes = node.data.bundle ? node.data.bundle.data.nodes : node.data.nodes;
for (i = 0, l = nodes.length; i < l; ++i) {
coords = nodes[i].data.coords;
vec = [coords[0] - m1[0], coords[1] - m1[1]];
norm = $norm(vec);
dot = vec[0] * m2Tom1[0] + vec[1] * m2Tom1[1];
angleValue = abs(acos(dot / norm / m1m2Norm));
angle = angle < angleValue ? angleValue : angle;
vec = [coords[2] - m2[0], coords[3] - m2[1]];
norm = $norm(vec);
dot = vec[0] * m1Tom2[0] + vec[1] * m1Tom2[1];
angleValue = abs(acos(dot / norm / m1m2Norm));
angle = angle < angleValue ? angleValue : angle;
}
return angle;
}
return -1;
},
getCombinedNode: function (node1, node2, data) {
node1 = node1.data.bundle || node1;
node2 = node2.data.bundle || node2;
var id = node1.id + "-" + node2.id,
name = node1.name + "-" + node2.name,
nodes1 = node1.data.nodes || [node1],
nodes2 = node2.data.nodes || [node2],
weight1 = node1.data.weight || 0,
weight2 = node2.data.weight || 0,
nodes = [],
ans;
if (node1.id == node2.id) {
return node1;
}
nodes.push.apply(nodes, nodes1);
nodes.push.apply(nodes, nodes2);
data = data || {};
data.nodes = nodes;
data.nodeArray = (node1.data.nodeArray || []).concat(node2.data.nodeArray || []);
data.weight = weight1 + weight2;
ans = {
id: id,
name: name,
data: data,
};
this.computeIntermediateNodePositions(ans);
return ans;
},
coalesceNodes: function (nodes) {
var node = nodes[0],
data = node.data,
m1 = data.m1,
m2 = data.m2,
weight = nodes.reduce(function (acum, n) {
return acum + (n.data.weight || 0);
}, 0),
coords = data.coords,
bundle = data.bundle,
nodeArray = [],
i,
l;
if (m1) {
coords = [m1[0], m1[1], m2[0], m2[1]];
//flattened nodes for cluster.
for (i = 0, l = nodes.length; i < l; ++i) {
nodeArray.push.apply(
nodeArray,
nodes[i].data.nodeArray || (nodes[i].data.parents ? [] : [nodes[i]])
);
}
if (this.options.sort) {
console.log(nodeArray);
nodeArray.sort(this.options.sort);
}
//if (!nodeArray.length || (typeof nodeArray[0].id == 'string')) {
//debugger;
//}
return {
id: bundle.id,
name: bundle.id,
data: {
nodeArray: nodeArray,
parents: nodes,
coords: coords,
weight: weight,
parentsInk: bundle.data.ink,
},
};
}
return nodes[0];
},
bundle: function (combinedNode, node1, node2) {
var graph = this.graph;
node1.data.bundle = combinedNode;
node2.data.bundle = combinedNode;
node1.data.ink = combinedNode.data.ink;
node1.data.m1 = combinedNode.data.m1;
node1.data.m2 = combinedNode.data.m2;
//node1.data.nodeArray = combinedNode.data.nodeArray;
node2.data.ink = combinedNode.data.ink;
node2.data.m1 = combinedNode.data.m1;
node2.data.m2 = combinedNode.data.m2;
//node2.data.nodeArray = combinedNode.data.nodeArray;
},
updateGraph: function (graph, groupedNode, nodes, ids) {
var i,
l,
n,
connections,
checkConnection = function (e) {
var nodeToId = e.nodeTo.id;
if (!ids[nodeToId]) {
connections.push(e.nodeTo);
}
};
for (i = 0, l = nodes.length; i < l; ++i) {
n = nodes[i];
connections = [];
n.eachEdge(checkConnection);
graph.removeNode(n.id);
}
graph.addNode(groupedNode);
for (i = 0, l = connections.length; i < l; ++i) {
graph.addEdge(groupedNode, connections[i]);
}
},
coalesceGraph: function () {
var graph = this.graph,
newGraph = new Graph(),
groupsIds = {},
maxGroup = -Infinity,
nodes,
i,
l,
ids,
groupedNode,
connections,
updateGraph = this.updateGraph,
coalesceNodes = this.coalesceNodes.bind(this);
graph.each(function (node) {
var group = node.data.group;
if (maxGroup < group) {
maxGroup = group;
}
if (!groupsIds[group]) {
groupsIds[group] = {};
}
groupsIds[group][node.id] = node;
});
maxGroup++;
while (maxGroup--) {
ids = groupsIds[maxGroup];
nodes = [];
for (i in ids) {
nodes.push(ids[i]);
}
if (nodes.length) {
groupedNode = coalesceNodes(nodes);
updateGraph(graph, groupedNode, nodes, ids);
}
}
},
getMaximumInkSavingNeighbor: function (n) {
var nodeFrom = n,
getInkValue = this.getInkValue.bind(this),
inkFrom = getInkValue(nodeFrom),
combineNodes = this.getCombinedNode.bind(this),
inkTotal = Infinity,
bundle = Array(2),
combinedBundle;
n.eachEdge(function (e) {
var nodeTo = e.nodeTo,
inkTo = getInkValue(nodeTo),
combined = combineNodes(nodeFrom, nodeTo),
inkUnion = getInkValue(combined),
inkValue = inkUnion - (inkFrom + inkTo);
if (inkTotal > inkValue) {
inkTotal = inkValue;
bundle[0] = nodeFrom;
bundle[1] = nodeTo;
combinedBundle = combined;
}
});
return {
bundle: bundle,
inkTotal: inkTotal,
combined: combinedBundle,
};
},
MINGLE: function () {
var edgeProximityGraph = this.graph,
that = this,
totalGain = 0,
ungrouped = -1,
gain = 0,
k = 0,
clean = function (n) {
n.data.group = ungrouped;
},
nodeMingle = function (node) {
if (node.data.group == ungrouped) {
var ans = that.getMaximumInkSavingNeighbor(node),
bundle = ans.bundle,
u = bundle[0],
v = bundle[1],
combined = ans.combined,
gainUV = -ans.inkTotal;
//graph has been collapsed and is now only one node
if (!u && !v) {
gain = -Infinity;
return;
}
if (gainUV > 0) {
that.bundle(combined, u, v);
gain += gainUV;
if (v.data.group != ungrouped) {
u.data.group = v.data.group;
} else {
u.data.group = v.data.group = k;
}
} else {
u.data.group = k;
}
k++;
}
};
do {
gain = 0;
k = 0;
edgeProximityGraph.each(clean);
edgeProximityGraph.each(nodeMingle);
this.coalesceGraph();
totalGain += gain;
} while (gain > 0);
},
};
// this.Bundler = Bundler;
return Bundler;
})();