@ -0,0 +1,113 @@ | |||
<!doctype html> | |||
<html> | |||
<head> | |||
<title>Graph | Random nodes</title> | |||
<style type="text/css"> | |||
body { | |||
font: 10pt sans; | |||
} | |||
#mygraph { | |||
width: 600px; | |||
height: 600px; | |||
border: 1px solid lightgray; | |||
} | |||
</style> | |||
<script type="text/javascript" src="../../dist/vis.js"></script> | |||
<script type="text/javascript"> | |||
var nodes = null; | |||
var edges = null; | |||
var graph = null; | |||
function draw() { | |||
nodes = []; | |||
edges = []; | |||
var connectionCount = []; | |||
// randomly create some nodes and edges | |||
var nodeCount = document.getElementById('nodeCount').value; | |||
for (var i = 0; i < nodeCount; i++) { | |||
nodes.push({ | |||
id: i, | |||
label: String(i) | |||
}); | |||
connectionCount[i] = 0; | |||
// create edges in a scale-free-graph way | |||
if (i == 1) { | |||
var from = i; | |||
var to = 0; | |||
edges.push({ | |||
from: from, | |||
to: to | |||
}); | |||
connectionCount[from]++; | |||
connectionCount[to]++; | |||
} | |||
else if (i > 1) { | |||
var conn = edges.length * 2; | |||
var rand = Math.floor(Math.random() * conn); | |||
var cum = 0; | |||
var j = 0; | |||
while (j < connectionCount.length && cum < rand) { | |||
cum += connectionCount[j]; | |||
j++; | |||
} | |||
var from = i; | |||
var to = j; | |||
edges.push({ | |||
from: from, | |||
to: to | |||
}); | |||
connectionCount[from]++; | |||
connectionCount[to]++; | |||
} | |||
} | |||
// create a graph | |||
var container = document.getElementById('mygraph'); | |||
var data = { | |||
nodes: nodes, | |||
edges: edges | |||
}; | |||
var options = { | |||
edges: { | |||
}, | |||
stabilize: false, | |||
hierarchicalLayout:true | |||
}; | |||
graph = new vis.Graph(container, data, options); | |||
// add event listeners | |||
graph.on('select', function(params) { | |||
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes; | |||
}); | |||
} | |||
</script> | |||
</head> | |||
<body onload="draw();"> | |||
<h2>Hierarchical Layout - Scale-Free-Graph</h2> | |||
<div style="width:700px; font-size:14px;"> | |||
This example shows the randomly generated <b>scale-free-graph</b> set of nodes and connected edges from example 2. | |||
In this example, hierarchical layout has been enabled and the vertical levels are determine automatically. | |||
</div> | |||
<br /> | |||
<form onsubmit="draw(); return false;"> | |||
<label for="nodeCount">Number of nodes:</label> | |||
<input id="nodeCount" type="text" value="25" style="width: 50px;"> | |||
<input type="submit" value="Go"> | |||
</form> | |||
<br> | |||
<div id="mygraph"></div> | |||
<p id="selection"></p> | |||
</body> | |||
</html> |
@ -0,0 +1,139 @@ | |||
<!doctype html> | |||
<html> | |||
<head> | |||
<title>Graph | Random nodes</title> | |||
<style type="text/css"> | |||
body { | |||
font: 10pt sans; | |||
} | |||
#mygraph { | |||
width: 600px; | |||
height: 600px; | |||
border: 1px solid lightgray; | |||
} | |||
</style> | |||
<script type="text/javascript" src="../../dist/vis.js"></script> | |||
<script type="text/javascript"> | |||
var nodes = null; | |||
var edges = null; | |||
var graph = null; | |||
function draw() { | |||
nodes = []; | |||
edges = []; | |||
var connectionCount = []; | |||
// randomly create some nodes and edges | |||
for (var i = 0; i < 15; i++) { | |||
nodes.push({ | |||
id: i, | |||
label: String(i) | |||
}); | |||
} | |||
edges.push({ | |||
from: 0, | |||
to: 1 | |||
}); | |||
edges.push({ | |||
from: 0, | |||
to: 6 | |||
}); | |||
edges.push({ | |||
from: 0, | |||
to: 13 | |||
});edges.push({ | |||
from: 0, | |||
to: 11 | |||
}); | |||
edges.push({ | |||
from: 1, | |||
to: 2 | |||
}); | |||
edges.push({ | |||
from: 2, | |||
to: 3 | |||
}); | |||
edges.push({ | |||
from: 2, | |||
to: 4 | |||
}); | |||
edges.push({ | |||
from: 3, | |||
to: 5 | |||
}); | |||
edges.push({ | |||
from: 1, | |||
to: 10 | |||
}); | |||
edges.push({ | |||
from: 1, | |||
to: 7 | |||
}); | |||
edges.push({ | |||
from: 2, | |||
to: 8 | |||
}); | |||
edges.push({ | |||
from: 2, | |||
to: 9 | |||
}); | |||
edges.push({ | |||
from: 3, | |||
to: 14 | |||
}); | |||
edges.push({ | |||
from: 1, | |||
to: 12 | |||
}); | |||
nodes[0]["level"] = 0; | |||
nodes[1]["level"] = 1; | |||
nodes[2]["level"] = 3; | |||
nodes[3]["level"] = 4; | |||
nodes[4]["level"] = 4; | |||
nodes[5]["level"] = 5; | |||
nodes[6]["level"] = 1; | |||
nodes[7]["level"] = 2; | |||
nodes[8]["level"] = 4; | |||
nodes[9]["level"] = 4; | |||
nodes[10]["level"] = 2; | |||
nodes[11]["level"] = 1; | |||
nodes[12]["level"] = 2; | |||
nodes[13]["level"] = 1; | |||
nodes[14]["level"] = 5; | |||
// create a graph | |||
var container = document.getElementById('mygraph'); | |||
var data = { | |||
nodes: nodes, | |||
edges: edges | |||
}; | |||
var options = { | |||
hierarchicalLayout:true | |||
}; | |||
graph = new vis.Graph(container, data, options); | |||
// add event listeners | |||
graph.on('select', function(params) { | |||
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes; | |||
}); | |||
} | |||
</script> | |||
</head> | |||
<body onload="draw();"> | |||
<h2>Hierarchical Layout - User-defined</h2> | |||
<div style="width:700px; font-size:14px;"> | |||
This example shows a user-defined hierarchical layout. If the user defines levels for nodes but does not do so for all nodes, an alert will show up and hierarchical layout will be disabled. Either all or none can be defined. | |||
</div> | |||
<br /> | |||
<div id="mygraph"></div> | |||
<p id="selection"></p> | |||
</body> | |||
</html> |
@ -0,0 +1,263 @@ | |||
var HierarchicalLayoutMixin = { | |||
/** | |||
* This is the main function to layout the nodes in a hierarchical way. | |||
* It checks if the node details are supplied correctly | |||
* | |||
* @private | |||
*/ | |||
_setupHierarchicalLayout : function() { | |||
if (this.constants.hierarchicalLayout.enabled == true) { | |||
// get the size of the largest hubs and check if the user has defined a level for a node. | |||
var hubsize = 0; | |||
var node, nodeId; | |||
var definedLevel = false; | |||
var undefinedLevel = false; | |||
for (nodeId in this.nodes) { | |||
if (this.nodes.hasOwnProperty(nodeId)) { | |||
node = this.nodes[nodeId]; | |||
if (node.level != -1) { | |||
definedLevel = true; | |||
} | |||
else { | |||
undefinedLevel = true; | |||
} | |||
if (hubsize < node.edges.length) { | |||
hubsize = node.edges.length; | |||
} | |||
} | |||
} | |||
// if the user defined some levels but not all, alert and run without hierarchical layout | |||
if (undefinedLevel == true && definedLevel == true) { | |||
alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.") | |||
this.zoomToFit(true,this.constants.clustering.enabled); | |||
if (!this.constants.clustering.enabled) { | |||
this.start(); | |||
} | |||
} | |||
else { | |||
// setup the system to use hierarchical method. | |||
this._changeConstants(); | |||
// define levels if undefined by the users. Based on hubsize | |||
if (undefinedLevel == true) { | |||
this._determineLevels(hubsize); | |||
} | |||
// check the distribution of the nodes per level. | |||
var distribution = this._getDistribution(); | |||
// place the nodes on the canvas. This also stablilizes the system. | |||
this._placeNodesByHierarchy(distribution); | |||
// start the simulation. | |||
this.start(); | |||
} | |||
} | |||
}, | |||
/** | |||
* This function places the nodes on the canvas based on the hierarchial distribution. | |||
* | |||
* @param {Object} distribution | obtained by the function this._getDistribution() | |||
* @private | |||
*/ | |||
_placeNodesByHierarchy : function(distribution) { | |||
var nodeId, node; | |||
// start placing all the level 0 nodes first. Then recursively position their branches. | |||
for (nodeId in distribution[0].nodes) { | |||
if (distribution[0].nodes.hasOwnProperty(nodeId)) { | |||
node = distribution[0].nodes[nodeId]; | |||
if (node.xFixed) { | |||
node.x = distribution[0].minPos; | |||
distribution[0].minPos += distribution[0].nodeSpacing; | |||
node.xFixed = false; | |||
} | |||
this._placeBranchNodes(node.edges,node.id,distribution,node.level); | |||
} | |||
} | |||
// give the nodes a defined width so the zoomToFit can be used. This size is arbitrary. | |||
for (nodeId in this.nodes) { | |||
if (this.nodes.hasOwnProperty(nodeId)) { | |||
node = this.nodes[nodeId]; | |||
node.width = 100; | |||
node.height = 100; | |||
} | |||
} | |||
// stabilize the system after positioning. This function calls zoomToFit. | |||
this._doStabilize(); | |||
// reset the arbitrary width and height we gave the nodes. | |||
for (nodeId in this.nodes) { | |||
if (this.nodes.hasOwnProperty(nodeId)) { | |||
this.nodes[nodeId]._reset(); | |||
} | |||
} | |||
}, | |||
/** | |||
* This function get the distribution of levels based on hubsize | |||
* | |||
* @returns {Object} | |||
* @private | |||
*/ | |||
_getDistribution : function() { | |||
var distribution = {}; | |||
var nodeId, node; | |||
// we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. | |||
// the fix of X is removed after the x value has been set. | |||
for (nodeId in this.nodes) { | |||
if (this.nodes.hasOwnProperty(nodeId)) { | |||
node = this.nodes[nodeId]; | |||
node.xFixed = true; | |||
node.yFixed = true; | |||
node.y = this.constants.hierarchicalLayout.levelSeparation*node.level; | |||
if (!distribution.hasOwnProperty(node.level)) { | |||
distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0}; | |||
} | |||
distribution[node.level].amount += 1; | |||
distribution[node.level].nodes[node.id] = node; | |||
} | |||
} | |||
// determine the largest amount of nodes of all levels | |||
var maxCount = 0; | |||
for (var level in distribution) { | |||
if (distribution.hasOwnProperty(level)) { | |||
if (maxCount < distribution[level].amount) { | |||
maxCount = distribution[level].amount; | |||
} | |||
} | |||
} | |||
// set the initial position and spacing of each nodes accordingly | |||
for (var level in distribution) { | |||
if (distribution.hasOwnProperty(level)) { | |||
distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; | |||
distribution[level].nodeSpacing /= (distribution[level].amount + 1); | |||
distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing); | |||
} | |||
} | |||
return distribution; | |||
}, | |||
/** | |||
* this function allocates nodes in levels based on the recursive branching from the largest hubs. | |||
* | |||
* @param hubsize | |||
* @private | |||
*/ | |||
_determineLevels : function(hubsize) { | |||
var nodeId, node; | |||
// determine hubs | |||
for (nodeId in this.nodes) { | |||
if (this.nodes.hasOwnProperty(nodeId)) { | |||
node = this.nodes[nodeId]; | |||
if (node.edges.length == hubsize) { | |||
node.level = 0; | |||
} | |||
} | |||
} | |||
// branch from hubs | |||
for (nodeId in this.nodes) { | |||
if (this.nodes.hasOwnProperty(nodeId)) { | |||
node = this.nodes[nodeId]; | |||
if (node.level == 0) { | |||
this._setLevel(1,node.edges,node.id); | |||
} | |||
} | |||
} | |||
}, | |||
/** | |||
* Since hierarchical layout does not support: | |||
* - smooth curves (based on the physics), | |||
* - clustering (based on dynamic node counts) | |||
* | |||
* We disable both features so there will be no problems. | |||
* | |||
* @private | |||
*/ | |||
_changeConstants : function() { | |||
this.constants.clustering.enabled = false; | |||
this.constants.physics.barnesHut.enabled = false; | |||
this.constants.physics.hierarchicalRepulsion.enabled = true; | |||
this._loadSelectedForceSolver(); | |||
this.constants.smoothCurves = false; | |||
this._configureSmoothCurves(); | |||
}, | |||
/** | |||
* This is a recursively called function to enumerate the branches from the largest hubs and place the nodes | |||
* on a X position that ensures there will be no overlap. | |||
* | |||
* @param edges | |||
* @param parentId | |||
* @param distribution | |||
* @param parentLevel | |||
* @private | |||
*/ | |||
_placeBranchNodes : function(edges, parentId, distribution, parentLevel) { | |||
for (var i = 0; i < edges.length; i++) { | |||
var childNode = null; | |||
if (edges[i].toId == parentId) { | |||
childNode = edges[i].from; | |||
} | |||
else { | |||
childNode = edges[i].to; | |||
} | |||
// if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. | |||
if (childNode.xFixed && childNode.level > parentLevel) { | |||
childNode.xFixed = false; | |||
childNode.x = distribution[childNode.level].minPos; | |||
distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; | |||
if (childNode.edges.length > 1) { | |||
this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level); | |||
} | |||
} | |||
} | |||
}, | |||
/** | |||
* this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. | |||
* | |||
* @param level | |||
* @param edges | |||
* @param parentId | |||
* @private | |||
*/ | |||
_setLevel : function(level, edges, parentId) { | |||
for (var i = 0; i < edges.length; i++) { | |||
var childNode = null; | |||
if (edges[i].toId == parentId) { | |||
childNode = edges[i].from; | |||
} | |||
else { | |||
childNode = edges[i].to; | |||
} | |||
if (childNode.level == -1 || childNode.level > level) { | |||
childNode.level = level; | |||
if (edges.length > 1) { | |||
this._setLevel(level+1, childNode.edges, childNode.id); | |||
} | |||
} | |||
} | |||
} | |||
}; |
@ -0,0 +1,64 @@ | |||
/** | |||
* Created by Alex on 2/10/14. | |||
*/ | |||
var hierarchalRepulsionMixin = { | |||
/** | |||
* Calculate the forces the nodes apply on eachother based on a repulsion field. | |||
* This field is linearly approximated. | |||
* | |||
* @private | |||
*/ | |||
_calculateNodeForces : function() { | |||
var dx, dy, distance, fx, fy, combinedClusterSize, | |||
repulsingForce, node1, node2, i, j; | |||
var nodes = this.calculationNodes; | |||
var nodeIndices = this.calculationNodeIndices; | |||
// approximation constants | |||
var b = 5; | |||
var a_base = 0.5*-b; | |||
// repulsing forces between nodes | |||
var nodeDistance = this.constants.physics.repulsion.nodeDistance; | |||
var minimumDistance = nodeDistance; | |||
// we loop from i over all but the last entree in the array | |||
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||
for (i = 0; i < nodeIndices.length-1; i++) { | |||
node1 = nodes[nodeIndices[i]]; | |||
for (j = i+1; j < nodeIndices.length; j++) { | |||
node2 = nodes[nodeIndices[j]]; | |||
dx = node2.x - node1.x; | |||
dy = node2.y - node1.y; | |||
distance = Math.sqrt(dx * dx + dy * dy); | |||
var a = a_base / minimumDistance; | |||
if (distance < 2*minimumDistance) { | |||
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) | |||
// normalize force with | |||
if (distance == 0) { | |||
distance = 0.01; | |||
} | |||
else { | |||
repulsingForce = repulsingForce/distance; | |||
} | |||
fx = dx * repulsingForce; | |||
fy = dy * repulsingForce; | |||
node1.fx -= fx; | |||
node1.fy -= fy; | |||
node2.fx += fx; | |||
node2.fy += fy; | |||
} | |||
} | |||
} | |||
} | |||
} |