Browse Source

added hierarchical layout

css_transitions
Alex de Mulder 10 years ago
parent
commit
8a3c45eab9
19 changed files with 791 additions and 52 deletions
  1. +2
    -0
      Jakefile.js
  2. +74
    -10
      docs/graph.html
  3. +5
    -5
      examples/graph/17_network_info.html
  4. +1
    -1
      examples/graph/19_scale_free_graph_clustering.html
  5. +113
    -0
      examples/graph/23_hierarchical_layout.html
  6. +139
    -0
      examples/graph/24_hierarchical_layout_userdefined.html
  7. +2
    -0
      examples/graph/index.html
  8. BIN
      img/gallery/graph/23_hierarchical_layout.png
  9. BIN
      img/gallery/graph/24_hierarchical_layout_predefined.png
  10. +27
    -7
      index.html
  11. +64
    -19
      src/graph/Graph.js
  12. +4
    -2
      src/graph/Node.js
  13. +2
    -2
      src/graph/graphMixins/ClusterMixin.js
  14. +263
    -0
      src/graph/graphMixins/HierarchicalLayoutMixin.js
  15. +1
    -1
      src/graph/graphMixins/ManipulationMixin.js
  16. +24
    -3
      src/graph/graphMixins/MixinLoader.js
  17. +1
    -1
      src/graph/graphMixins/physics/BarnesHut.js
  18. +64
    -0
      src/graph/graphMixins/physics/HierarchialRepulsion.js
  19. +5
    -1
      src/graph/graphMixins/physics/PhysicsMixin.js

+ 2
- 0
Jakefile.js View File

@ -83,8 +83,10 @@ task('build', {async: true}, function () {
'./src/graph/Groups.js',
'./src/graph/Images.js',
'./src/graph/graphMixins/physics/PhysicsMixin.js',
'./src/graph/graphMixins/physics/HierarchialRepulsion.js',
'./src/graph/graphMixins/physics/BarnesHut.js',
'./src/graph/graphMixins/physics/Repulsion.js',
'./src/graph/graphMixins/HierarchicalLayoutMixin.js',
'./src/graph/graphMixins/ManipulationMixin.js',
'./src/graph/graphMixins/SectorsMixin.js',
'./src/graph/graphMixins/ClusterMixin.js',

+ 74
- 10
docs/graph.html View File

@ -58,6 +58,7 @@
<li><a href="#Clustering">Clustering</a></li>
<li><a href="#Navigation_controls">Navigation controls</a></li>
<li><a href="#Keyboard_navigation">Keyboard navigation</a></li>
<li><a href="#Hierarchical_layout">Hierarchical layout</a></li>
</ul>
</li>
<li><a href="#Methods">Methods</a></li>
@ -291,10 +292,10 @@ var nodes = [
</tr>
<tr>
<td>fixed</td>
<td>allowedToMove</td>
<td>Boolean</td>
<td>false</td>
<td>If fixed is true, then the node will not move from its supplied position.
<td>true</td>
<td>If allowedToMove is false, then the node will not move from its supplied position.
If only an x position has been supplied, it is only fixed in the x-direction.
The same holds for y. If both x and y have been defined, the node will not move.</td>
</tr>
@ -334,8 +335,15 @@ var nodes = [
<td>string</td>
<td>no</td>
<td>Url of an image. Only applicable when the shape of the node is
<code>image</code>.</td>
</tr>
<code>image</code>.</td>
</tr>
<tr>
<td>level</td>
<td>number</td>
<td>-1</td>
<td>This level is used in the hierarchical layout. If this is not selected, the level does not do anything.</td>
</tr>
<tr>
<td>radius</td>
@ -836,10 +844,10 @@ var options = {
<td>Default border color of the node when selected.</td>
</tr>
<tr>
<td>fixed</td>
<td>allowedToMove</td>
<td>Boolean</td>
<td>false</td>
<td>If fixed is true, then the node will not move from its supplied position.
<td>If allowedToMove is false, then the node will not move from its supplied position.
If only an x position has been supplied, it is only fixed in the x-direction.
The same holds for y. If both x and y have been defined, the node will not move.</td>
</tr>
@ -875,6 +883,12 @@ var options = {
<td>Default image url for the nodes. only applicable to shape <code>image</code>.</td>
</tr>
<tr>
<td>level</td>
<td>number</td>
<td>-1</td>
<td>This level is used in the hierarchical layout. If this is not selected, the level does not do anything.</td>
</tr>
<tr>
<td>widthMin</td>
<td>Number</td>
@ -1329,13 +1343,11 @@ var options: {
var options: {
dataManipulation: true,
onAdd: function(data,callback) {
// fixed must be false because we define a set x and y position.
// If fixed is not false, the node cannot move.
/** data = {id: random unique id,
* label: new,
* x: x position of click (canvas space),
* y: y position of click (canvas space),
* fixed: false
* allowedToMove: true
* };
*/
var newData = {..}; // alter the data as you want.
@ -1658,6 +1670,58 @@ var options: {
</tr>
</table>
<h3 id="Hierarchical_layout">Hierarchical layout</h3>
<p>
The graph can be used to display nodes in a hierarchical way. This can be determined automatically, based on the amount of edges connected to each node, or defined by the user.
If the user wants to manually determine the hierarchy, each node has to be supplied with a level (from 0 being heighest to n). The automatic method
is shown in <a href="../examples/graph/23_hierarchical_layout.html">example 23</a> and the user-defined method is shown in <a href="../examples/graph/24_hierarchical_layout_userdefined.html">example 24</a>.
</p>
<pre class="prettyprint">
// simple use of the hierarchical layout
var options: {
hierarchicalLayout: true
}
// advanced configuration for keyboard controls
var options: {
hierarchicalLayout: {
enabled:false,
levelSeparation: 150,
nodeSpacing: 100
}
}
</pre>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td>enabled</td>
<td>Boolean</td>
<td>false</td>
<td>Enable or disable the hierarchical layout.
</td>
</tr>
<tr>
<td>levelSeparation</td>
<td>Number</td>
<td>150</td>
<td>This defines the space between levels (in the Y-direction).</td>
</tr>
<tr>
<td>nodeSpacing</td>
<td>Number</td>
<td>100</td>
<td>This defines the space between nodes in the same level (in the X-direction).</td>
</tr>
</table>
<h2 id="Methods">Methods</h2>
<p>
Graph supports the following methods.

+ 5
- 5
examples/graph/17_network_info.html View File

@ -96,11 +96,11 @@
var x = - mygraph.clientWidth / 2 + 50;
var y = - mygraph.clientHeight / 2 + 50;
var step = 70;
nodes.push({id: 1000, x: x, y: y, label: 'Internet', group: 'internet', value: 1, fixed:true});
nodes.push({id: 1001, x: x, y: y + step, label: 'Switch', group: 'switch', value: 1, fixed:true});
nodes.push({id: 1002, x: x, y: y + 2 * step, label: 'Server', group: 'server', value: 1, fixed:true});
nodes.push({id: 1003, x: x, y: y + 3 * step, label: 'Computer', group: 'desktop', value: 1, fixed:true});
nodes.push({id: 1004, x: x, y: y + 4 * step, label: 'Smartphone', group: 'mobile', value: 1, fixed:true});
nodes.push({id: 1000, x: x, y: y, label: 'Internet', group: 'internet', value: 1});
nodes.push({id: 1001, x: x, y: y + step, label: 'Switch', group: 'switch', value: 1});
nodes.push({id: 1002, x: x, y: y + 2 * step, label: 'Server', group: 'server', value: 1});
nodes.push({id: 1003, x: x, y: y + 3 * step, label: 'Computer', group: 'desktop', value: 1});
nodes.push({id: 1004, x: x, y: y + 4 * step, label: 'Smartphone', group: 'mobile', value: 1});
// create a graph
var container = document.getElementById('mygraph');

+ 1
- 1
examples/graph/19_scale_free_graph_clustering.html View File

@ -107,7 +107,7 @@
<body onload="draw();">
<h2>Clustering - Scale-Free-Graph</h2>
<div style="width:700px; font-size:14px;">
This example shows therandomly generated <b>scale-free-graph</b> set of nodes and connected edges from example 2.
This example shows the randomly generated <b>scale-free-graph</b> set of nodes and connected edges from example 2.
By clicking the checkbox you can turn clustering on and off. If you increase the number of nodes to
a value higher than 100, automatic clustering is used before the initial draw (assuming the checkbox is checked).
<br />

+ 113
- 0
examples/graph/23_hierarchical_layout.html View File

@ -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>

+ 139
- 0
examples/graph/24_hierarchical_layout_userdefined.html View File

@ -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>

+ 2
- 0
examples/graph/index.html View File

@ -34,6 +34,8 @@
<p><a href="20_navigation.html">20_navigation.html</a></p>
<p><a href="21_data_manipulation.html">21_data_manipulation.html</a></p>
<p><a href="22_les_miserables.html">22_les_miserables.html</a></p>
<p><a href="23_hierarchical_layout.html">23_hierarchical_layout.html</a></p>
<p><a href="24_hierarchical_layout_userdefined.html">24_hierarchical_layout_predefined.html</a></p>
<p><a href="graphviz/graphviz_gallery.html">graphviz_gallery.html</a></p>
</div>

BIN
img/gallery/graph/23_hierarchical_layout.png View File

Before After
Width: 92  |  Height: 92  |  Size: 20 KiB

BIN
img/gallery/graph/24_hierarchical_layout_predefined.png View File

Before After
Width: 92  |  Height: 92  |  Size: 18 KiB

+ 27
- 7
index.html View File

@ -271,26 +271,46 @@ The source code of the examples can be found in the
<div class="thumb">
<a href="examples/graph/18_fully_random_nodes_clustering.html">
<img src="img/gallery/graph/18_fully_random_nodes_clustering.png">
<div>network info</div>
<div>random clustering</div>
</a>
</div>
<div class="thumb">
<a href="examples/graph/19_scale_free_graph_clustering.html">
<img src="img/gallery/graph/19_scale_free_graph_clustering.png">
<div>network info</div>
<div>scale-free clustering</div>
</a>
</div>
<div class="thumb">
<a href="examples/graph/20_navigation.html">
<img src="img/gallery/graph/20_navigation.png">
<div>network info</div>
<div>navigation</div>
</a>
</div><div class="thumb">
<a href="examples/graph/22_les_miserables.html">
<img src="img/gallery/graph/22_les_miserables.png">
<div>network info</div>
</div>
<div class="thumb">
<a href="examples/graph/21_data_manipulation.html">
<img src="img/gallery/graph/21_data_manipulation.png">
<div>data manipulation</div>
</a>
</div>
<div class="thumb">
<a href="examples/graph/22_les_miserables.html">
<img src="img/gallery/graph/22_les_miserables.png">
<div>les miserables</div>
</a>
</div>
<div class="thumb">
<a href="examples/graph/23_hierarchical_layout.html">
<img src="img/gallery/graph/23_hierarchical_layout.png">
<div>autmatic hierarchy</div>
</a>
</div>
<div class="thumb">
<a href="examples/graph/24_hierarchical_layout_userdefined.html">
<img src="img/gallery/graph/24_hierarchical_layout_predefined.png">
<div>user-defined hierarchy</div>
</a>
</div>
<div class="thumb">
<a href="examples/graph/graphviz/graphviz_gallery.html">
<img src="img/gallery/graph/graphviz_gallery.png">

+ 64
- 19
src/graph/Graph.js View File

@ -44,6 +44,7 @@ function Graph (container, data, options) {
fontColor: 'black',
fontSize: 14, // px
fontFace: 'verdana',
level: -1,
color: {
border: '#2B7CE9',
background: '#97C2FC',
@ -77,18 +78,26 @@ function Graph (container, data, options) {
enabled: true,
theta: 1 / 0.6, // inverted to save time during calculation
gravitationalConstant: -2000,
centralGravity: 0.1,
centralGravity: 0.3,
springLength: 100,
springConstant: 0.05,
damping: 0.09
},
repulsion: {
centralGravity: 0.1,
springLength: 50,
springLength: 200,
springConstant: 0.05,
nodeDistance: 100,
damping: 0.09
},
hierarchicalRepulsion: {
enabled: false,
centralGravity: 0.0,
springLength: 100,
springConstant: 0.01,
nodeDistance: 60,
damping: 0.09
},
damping: null,
centralGravity: null,
springLength: null,
@ -127,6 +136,11 @@ function Graph (container, data, options) {
enabled: false,
initiallyVisible: false
},
hierarchicalLayout: {
enabled:false,
levelSeparation: 150,
nodeSpacing: 100
},
smoothCurves: true,
maxVelocity: 10,
minVelocity: 0.1, // px/s
@ -142,7 +156,6 @@ function Graph (container, data, options) {
graph._redraw();
});
// keyboard navigation variables
this.xIncrement = 0;
this.yIncrement = 0;
@ -159,6 +172,9 @@ function Graph (container, data, options) {
this._loadClusterSystem();
// load the selection system. (mandatory, required by Graph)
this._loadSelectionSystem();
// load the selection system. (mandatory, required by Graph)
this._loadHierarchySystem();
// apply options
this.setOptions(options);
@ -220,10 +236,17 @@ function Graph (container, data, options) {
this.timer = undefined; // Scheduling function. Is definded in this.start();
// load data (the disable start variable will be the same as the enabled clustering)
this.setData(data,this.constants.clustering.enabled);
this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
// hierarchical layout
if (this.constants.hierarchicalLayout.enabled == true) {
this._setupHierarchicalLayout();
}
else {
// zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
this.zoomToFit(true,this.constants.clustering.enabled);
}
// zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
this.zoomToFit(true,this.constants.clustering.enabled);
// if clustering is disabled, the simulation will have started in the setData function
if (this.constants.clustering.enabled) {
@ -231,6 +254,9 @@ function Graph (container, data, options) {
}
}
/**
* Get the script path where the vis.js library is located
*
@ -261,12 +287,14 @@ Graph.prototype._getScriptPath = function() {
*/
Graph.prototype._getRange = function() {
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
for (var i = 0; i < this.nodeIndices.length; i++) {
node = this.nodes[this.nodeIndices[i]];
if (minX > (node.x - node.width)) {minX = node.x - node.width;}
if (maxX < (node.x + node.width)) {maxX = node.x + node.width;}
if (minY > (node.y - node.height)) {minY = node.y - node.height;}
if (maxY < (node.y + node.height)) {maxY = node.y + node.height;}
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
if (minX > (node.x - node.width)) {minX = node.x - node.width;}
if (maxX < (node.x + node.width)) {maxX = node.x + node.width;}
if (minY > (node.y - node.height)) {minY = node.y - node.height;}
if (maxY < (node.y + node.height)) {maxY = node.y + node.height;}
}
}
return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
};
@ -310,11 +338,11 @@ Graph.prototype.zoomToFit = function(initialZoom, disableStart) {
initialZoom = false;
}
var numberOfNodes = this.nodeIndices.length;
var range = this._getRange();
var zoomLevel;
if (initialZoom == true) {
var numberOfNodes = this.nodeIndices.length;
if (this.constants.smoothCurves == true) {
if (this.constants.clustering.enabled == true &&
numberOfNodes >= this.constants.clustering.initialMaxNodes) {
@ -352,6 +380,7 @@ Graph.prototype.zoomToFit = function(initialZoom, disableStart) {
this._setScale(zoomLevel);
this._centerGraph(range);
if (disableStart == false || disableStart === undefined) {
this.moving = true;
this.start();
}
};
@ -469,6 +498,18 @@ Graph.prototype.setOptions = function (options) {
}
}
if (options.hierarchicalLayout) {
this.constants.hierarchicalLayout.enabled = true;
for (prop in options.hierarchicalLayout) {
if (options.hierarchicalLayout.hasOwnProperty(prop)) {
this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
}
}
}
else if (options.hierarchicalLayout !== undefined) {
this.constants.hierarchicalLayout.enabled = false;
}
if (options.clustering) {
this.constants.clustering.enabled = true;
for (prop in options.clustering) {
@ -719,7 +760,6 @@ Graph.prototype._createKeyBinds = function() {
this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
}
// this.mousetrap.bind("b",this._toggleBarnesHut.bind(me));
if (this.constants.dataManipulation.enabled == true) {
this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
@ -1725,7 +1765,7 @@ Graph.prototype._isMoving = function(vmin) {
* @private
*/
Graph.prototype._discreteStepNodes = function() {
var interval = 0.5;
var interval = 0.65;
var nodes = this.nodes;
var nodeId;
@ -1776,7 +1816,6 @@ Graph.prototype._physicsTick = function() {
Graph.prototype._animationStep = function() {
// reset the timer so a new scheduled animation step can be set
this.timer = undefined;
// handle the keyboad movement
this._handleNavigation();
@ -1854,7 +1893,11 @@ Graph.prototype.toggleFreeze = function() {
Graph.prototype._configureSmoothCurves = function() {
Graph.prototype._configureSmoothCurves = function(disableStart) {
if (disableStart === undefined) {
disableStart = true;
}
if (this.constants.smoothCurves == true) {
this._createBezierNodes();
}
@ -1869,8 +1912,10 @@ Graph.prototype._configureSmoothCurves = function() {
}
}
this._updateCalculationNodes();
this.moving = true;
this.start();
if (!disableStart) {
this.moving = true;
this.start();
}
};
Graph.prototype._createBezierNodes = function() {

+ 4
- 2
src/graph/Node.js View File

@ -53,6 +53,7 @@ function Node(properties, imagelist, grouplist, constants) {
this.radiusFixed = false;
this.radiusMin = constants.nodes.radiusMin;
this.radiusMax = constants.nodes.radiusMax;
this.level = -1;
this.imagelist = imagelist;
this.grouplist = grouplist;
@ -144,6 +145,7 @@ Node.prototype.setProperties = function(properties, constants) {
if (properties.x !== undefined) {this.x = properties.x;}
if (properties.y !== undefined) {this.y = properties.y;}
if (properties.value !== undefined) {this.value = properties.value;}
if (properties.level !== undefined) {this.level = properties.level;}
// physics
@ -189,8 +191,8 @@ Node.prototype.setProperties = function(properties, constants) {
}
}
this.xFixed = this.xFixed || (properties.x !== undefined && properties.fixed);
this.yFixed = this.yFixed || (properties.y !== undefined && properties.fixed);
this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMove);
this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMove);
this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
if (this.shape == 'image') {

+ 2
- 2
src/graph/graphMixins/ClusterMixin.js View File

@ -966,11 +966,11 @@ var ClusterMixin = {
}
}
/* Debug Override */
// /* Debug Override */
// for (nodeId in this.nodes) {
// if (this.nodes.hasOwnProperty(nodeId)) {
// node = this.nodes[nodeId];
// node.label = String(node.fx).concat(",",node.fy);
// node.label = String(node.level);
// }
// }

+ 263
- 0
src/graph/graphMixins/HierarchicalLayoutMixin.js View File

@ -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);
}
}
}
}
};

+ 1
- 1
src/graph/graphMixins/ManipulationMixin.js View File

@ -288,7 +288,7 @@ var manipulationMixin = {
_addNode : function() {
if (this._selectionIsEmpty() && this.editMode == true) {
var positionObject = this._pointerToPositionObject(this.pointerPosition);
var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",fixed:false};
var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMove:true};
if (this.triggerFunctions.add) {
if (this.triggerFunctions.add.length == 2) {
var me = this;

+ 24
- 3
src/graph/graphMixins/MixinLoader.js View File

@ -55,24 +55,35 @@ var graphMixinLoaders = {
// this overloads the this._calculateNodeForces
if (this.constants.physics.barnesHut.enabled == true) {
this._clearMixin(repulsionMixin);
this._clearMixin(hierarchalRepulsionMixin);
this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
this.constants.physics.damping = this.constants.physics.barnesHut.damping;
this.constants.physics.springGrowthPerMass = this.constants.physics.barnesHut.springGrowthPerMass;
this._loadMixin(barnesHutMixin);
}
else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
this._clearMixin(barnesHutMixin);
this._clearMixin(repulsionMixin);
this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
this._loadMixin(hierarchalRepulsionMixin);
}
else {
this._clearMixin(barnesHutMixin);
this._clearMixin(hierarchalRepulsionMixin);
this.barnesHutTree = undefined;
this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
this.constants.physics.damping = this.constants.physics.repulsion.damping;
this.constants.physics.springGrowthPerMass = this.constants.physics.repulsion.springGrowthPerMass;
this._loadMixin(repulsionMixin);
}
@ -214,6 +225,16 @@ var graphMixinLoaders = {
if (this.constants.navigation.enabled == true) {
this._loadNavigationElements();
}
}
},
/**
* Mixin the hierarchical layout system.
*
* @private
*/
_loadHierarchySystem : function() {
this._loadMixin(HierarchicalLayoutMixin);
}
}

+ 1
- 1
src/graph/graphMixins/physics/BarnesHut.js View File

@ -56,7 +56,7 @@ var barnesHutMixin = {
if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
// duplicate code to reduce function calls to speed up program
if (distance == 0) {
distance = 0.5*Math.random();
distance = 0.1*Math.random();
dx = distance;
}
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);

+ 64
- 0
src/graph/graphMixins/physics/HierarchialRepulsion.js View File

@ -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;
}
}
}
}
}

+ 5
- 1
src/graph/graphMixins/physics/PhysicsMixin.js View File

@ -122,7 +122,7 @@ var physicsMixin = {
node = nodes[this.calculationNodeIndices[i]];
node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
// gravity does not apply when we are in a pocket sector
if (this._sector() == "default") {
if (this._sector() == "default" && gravity != 0) {
dx = -node.x;
dy = -node.y;
distance = Math.sqrt(dx*dx + dy*dy);
@ -164,6 +164,10 @@ var physicsMixin = {
dy = (edge.from.y - edge.to.y);
length = Math.sqrt(dx * dx + dy * dy);
if (length == 0) {
length = 0.01;
}
springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
fx = dx * springForce;

Loading…
Cancel
Save