From 8a3c45eab9fbb298b5a73cd5303c4dadf556abd6 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 20 Feb 2014 16:56:02 +0100 Subject: [PATCH] added hierarchical layout --- Jakefile.js | 2 + docs/graph.html | 84 +++++- examples/graph/17_network_info.html | 10 +- .../graph/19_scale_free_graph_clustering.html | 2 +- examples/graph/23_hierarchical_layout.html | 113 ++++++++ .../24_hierarchical_layout_userdefined.html | 139 +++++++++ examples/graph/index.html | 2 + img/gallery/graph/23_hierarchical_layout.png | Bin 0 -> 19986 bytes .../24_hierarchical_layout_predefined.png | Bin 0 -> 18435 bytes index.html | 34 ++- src/graph/Graph.js | 83 ++++-- src/graph/Node.js | 6 +- src/graph/graphMixins/ClusterMixin.js | 4 +- .../graphMixins/HierarchicalLayoutMixin.js | 263 ++++++++++++++++++ src/graph/graphMixins/ManipulationMixin.js | 2 +- src/graph/graphMixins/MixinLoader.js | 27 +- src/graph/graphMixins/physics/BarnesHut.js | 2 +- .../physics/HierarchialRepulsion.js | 64 +++++ src/graph/graphMixins/physics/PhysicsMixin.js | 6 +- 19 files changed, 791 insertions(+), 52 deletions(-) create mode 100644 examples/graph/23_hierarchical_layout.html create mode 100644 examples/graph/24_hierarchical_layout_userdefined.html create mode 100644 img/gallery/graph/23_hierarchical_layout.png create mode 100644 img/gallery/graph/24_hierarchical_layout_predefined.png create mode 100644 src/graph/graphMixins/HierarchicalLayoutMixin.js create mode 100644 src/graph/graphMixins/physics/HierarchialRepulsion.js diff --git a/Jakefile.js b/Jakefile.js index a36a934c..ad17bc12 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -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', diff --git a/docs/graph.html b/docs/graph.html index 461a2841..bac13b3f 100644 --- a/docs/graph.html +++ b/docs/graph.html @@ -58,6 +58,7 @@
  • Clustering
  • Navigation controls
  • Keyboard navigation
  • +
  • Hierarchical layout
  • Methods
  • @@ -291,10 +292,10 @@ var nodes = [ - fixed + allowedToMove Boolean - false - If fixed is true, then the node will not move from its supplied position. + true + 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. @@ -334,8 +335,15 @@ var nodes = [ string no Url of an image. Only applicable when the shape of the node is - image. - + image. + + + + level + number + -1 + This level is used in the hierarchical layout. If this is not selected, the level does not do anything. + radius @@ -836,10 +844,10 @@ var options = { Default border color of the node when selected. - fixed + allowedToMove Boolean false - If fixed is true, then the node will not move from its supplied position. + 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. @@ -875,6 +883,12 @@ var options = { Default image url for the nodes. only applicable to shape image. + + level + number + -1 + This level is used in the hierarchical layout. If this is not selected, the level does not do anything. + widthMin Number @@ -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: { + +

    Hierarchical layout

    +

    + 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 example 23 and the user-defined method is shown in example 24. +

    + +
    +// simple use of the hierarchical layout
    +var options: {
    +    hierarchicalLayout: true
    +}
    +
    +// advanced configuration for keyboard controls
    +var options: {
    +    hierarchicalLayout: {
    +      enabled:false,
    +      levelSeparation: 150,
    +      nodeSpacing: 100
    +    }
    +}
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDefaultDescription
    enabledBooleanfalseEnable or disable the hierarchical layout. +
    levelSeparationNumber150This defines the space between levels (in the Y-direction).
    nodeSpacingNumber100This defines the space between nodes in the same level (in the X-direction).

    Methods

    Graph supports the following methods. diff --git a/examples/graph/17_network_info.html b/examples/graph/17_network_info.html index ca97d75a..0b7e033f 100644 --- a/examples/graph/17_network_info.html +++ b/examples/graph/17_network_info.html @@ -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'); diff --git a/examples/graph/19_scale_free_graph_clustering.html b/examples/graph/19_scale_free_graph_clustering.html index 640e2207..ef3de8cd 100644 --- a/examples/graph/19_scale_free_graph_clustering.html +++ b/examples/graph/19_scale_free_graph_clustering.html @@ -107,7 +107,7 @@

    Clustering - Scale-Free-Graph

    - This example shows therandomly generated scale-free-graph set of nodes and connected edges from example 2. + This example shows the randomly generated scale-free-graph 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).
    diff --git a/examples/graph/23_hierarchical_layout.html b/examples/graph/23_hierarchical_layout.html new file mode 100644 index 00000000..5923e21e --- /dev/null +++ b/examples/graph/23_hierarchical_layout.html @@ -0,0 +1,113 @@ + + + + Graph | Random nodes + + + + + + + + + +

    Hierarchical Layout - Scale-Free-Graph

    +
    + This example shows the randomly generated scale-free-graph set of nodes and connected edges from example 2. + In this example, hierarchical layout has been enabled and the vertical levels are determine automatically. +
    +
    +
    + + + +
    +
    + +
    + +

    + + diff --git a/examples/graph/24_hierarchical_layout_userdefined.html b/examples/graph/24_hierarchical_layout_userdefined.html new file mode 100644 index 00000000..0605485a --- /dev/null +++ b/examples/graph/24_hierarchical_layout_userdefined.html @@ -0,0 +1,139 @@ + + + + Graph | Random nodes + + + + + + + + + +

    Hierarchical Layout - User-defined

    +
    + 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. +
    +
    + +
    + +

    + + diff --git a/examples/graph/index.html b/examples/graph/index.html index 4b38888d..db69b9ca 100644 --- a/examples/graph/index.html +++ b/examples/graph/index.html @@ -34,6 +34,8 @@

    20_navigation.html

    21_data_manipulation.html

    22_les_miserables.html

    +

    23_hierarchical_layout.html

    +

    24_hierarchical_layout_predefined.html

    graphviz_gallery.html

    diff --git a/img/gallery/graph/23_hierarchical_layout.png b/img/gallery/graph/23_hierarchical_layout.png new file mode 100644 index 0000000000000000000000000000000000000000..7d5035ed2e586b0e23cd99fcdbe4c95a50cb81b9 GIT binary patch literal 19986 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 literal 0 HcmV?d00001 diff --git a/img/gallery/graph/24_hierarchical_layout_predefined.png b/img/gallery/graph/24_hierarchical_layout_predefined.png new file mode 100644 index 0000000000000000000000000000000000000000..3499e9898d689ee9fd70a2d59c669b966f3d1700 GIT binary patch literal 18435 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 FzX5Y#_D28! literal 0 HcmV?d00001 diff --git a/index.html b/index.html index c58075da..8e7be2a1 100644 --- a/index.html +++ b/index.html @@ -271,26 +271,46 @@ The source code of the examples can be found in the + + + + +
    diff --git a/src/graph/Graph.js b/src/graph/Graph.js index cf2234fb..e517e99f 100644 --- a/src/graph/Graph.js +++ b/src/graph/Graph.js @@ -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() { diff --git a/src/graph/Node.js b/src/graph/Node.js index c6700d1a..f955b2e2 100644 --- a/src/graph/Node.js +++ b/src/graph/Node.js @@ -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') { diff --git a/src/graph/graphMixins/ClusterMixin.js b/src/graph/graphMixins/ClusterMixin.js index e0de527a..e20fa85c 100644 --- a/src/graph/graphMixins/ClusterMixin.js +++ b/src/graph/graphMixins/ClusterMixin.js @@ -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); // } // } diff --git a/src/graph/graphMixins/HierarchicalLayoutMixin.js b/src/graph/graphMixins/HierarchicalLayoutMixin.js new file mode 100644 index 00000000..5c1497c4 --- /dev/null +++ b/src/graph/graphMixins/HierarchicalLayoutMixin.js @@ -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); + } + } + } + } +}; \ No newline at end of file diff --git a/src/graph/graphMixins/ManipulationMixin.js b/src/graph/graphMixins/ManipulationMixin.js index d1dfc0c1..cc3ca3f8 100644 --- a/src/graph/graphMixins/ManipulationMixin.js +++ b/src/graph/graphMixins/ManipulationMixin.js @@ -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; diff --git a/src/graph/graphMixins/MixinLoader.js b/src/graph/graphMixins/MixinLoader.js index f025b08a..e31dd846 100644 --- a/src/graph/graphMixins/MixinLoader.js +++ b/src/graph/graphMixins/MixinLoader.js @@ -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); + } } diff --git a/src/graph/graphMixins/physics/BarnesHut.js b/src/graph/graphMixins/physics/BarnesHut.js index 9e658c8c..5a06e26d 100644 --- a/src/graph/graphMixins/physics/BarnesHut.js +++ b/src/graph/graphMixins/physics/BarnesHut.js @@ -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); diff --git a/src/graph/graphMixins/physics/HierarchialRepulsion.js b/src/graph/graphMixins/physics/HierarchialRepulsion.js new file mode 100644 index 00000000..bc6b55c8 --- /dev/null +++ b/src/graph/graphMixins/physics/HierarchialRepulsion.js @@ -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; + } + } + } + } +} \ No newline at end of file diff --git a/src/graph/graphMixins/physics/PhysicsMixin.js b/src/graph/graphMixins/physics/PhysicsMixin.js index c5513761..ad24599c 100644 --- a/src/graph/graphMixins/physics/PhysicsMixin.js +++ b/src/graph/graphMixins/physics/PhysicsMixin.js @@ -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;