diff --git a/Jakefile.js b/Jakefile.js index 332dd688..80a725f8 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -81,8 +81,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 e5e73bd5..e8c402ac 100644 --- a/docs/graph.html +++ b/docs/graph.html @@ -59,6 +59,7 @@
  • Clustering
  • Navigation controls
  • Keyboard navigation
  • +
  • Hierarchical layout
  • Methods
  • @@ -292,10 +293,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. @@ -335,8 +336,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 @@ -837,10 +845,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. @@ -876,6 +884,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 @@ -1330,13 +1344,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. @@ -1659,6 +1671,59 @@ 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. + This layout method does not support smooth curves or clustering. It automatically turns these features off. +

    + +
    +// 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/18_fully_random_nodes_clustering.png b/img/gallery/graph/18_fully_random_nodes_clustering.png new file mode 100644 index 00000000..6356994e Binary files /dev/null and b/img/gallery/graph/18_fully_random_nodes_clustering.png differ diff --git a/img/gallery/graph/19_scale_free_graph_clustering.png b/img/gallery/graph/19_scale_free_graph_clustering.png new file mode 100644 index 00000000..70505e2b Binary files /dev/null and b/img/gallery/graph/19_scale_free_graph_clustering.png differ diff --git a/img/gallery/graph/20_navigation.png b/img/gallery/graph/20_navigation.png new file mode 100644 index 00000000..3cc4dc06 Binary files /dev/null and b/img/gallery/graph/20_navigation.png differ diff --git a/img/gallery/graph/21_data_manipulation.png b/img/gallery/graph/21_data_manipulation.png new file mode 100644 index 00000000..26e9f1d3 Binary files /dev/null and b/img/gallery/graph/21_data_manipulation.png differ diff --git a/img/gallery/graph/22_les_miserables.png b/img/gallery/graph/22_les_miserables.png new file mode 100644 index 00000000..e09225c4 Binary files /dev/null and b/img/gallery/graph/22_les_miserables.png differ diff --git a/img/gallery/graph/23_hierarchical_layout.png b/img/gallery/graph/23_hierarchical_layout.png new file mode 100644 index 00000000..7d5035ed Binary files /dev/null and b/img/gallery/graph/23_hierarchical_layout.png differ 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 00000000..3499e989 Binary files /dev/null and b/img/gallery/graph/24_hierarchical_layout_predefined.png differ diff --git a/index.html b/index.html index a0c18d7f..8e7be2a1 100644 --- a/index.html +++ b/index.html @@ -268,6 +268,49 @@ The source code of the examples can be found in the
    network info
    +
    + + +
    random clustering
    +
    +
    +
    + + +
    scale-free clustering
    +
    +
    +
    + + +
    navigation
    +
    +
    + +
    + + +
    data manipulation
    +
    +
    +
    + + +
    les miserables
    +
    +
    +
    + + +
    autmatic hierarchy
    +
    +
    +
    + + +
    user-defined hierarchy
    +
    +
    diff --git a/src/graph/Graph.js b/src/graph/Graph.js index a170f0fd..32c5377d 100644 --- a/src/graph/Graph.js +++ b/src/graph/Graph.js @@ -40,11 +40,11 @@ function Graph (container, data, options) { image: undefined, widthMin: 16, // px widthMax: 64, // px - fixed:false, + fixed: false, fontColor: 'black', fontSize: 14, // px fontFace: 'verdana', -// fontFace: 'arial', + level: -1, color: { border: '#2B7CE9', background: '#97C2FC', @@ -78,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, @@ -128,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 @@ -143,7 +156,6 @@ function Graph (container, data, options) { graph._redraw(); }); - // keyboard navigation variables this.xIncrement = 0; this.yIncrement = 0; @@ -160,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); @@ -221,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) { @@ -265,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}; }; @@ -314,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) { @@ -356,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(); } }; @@ -473,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) { @@ -685,7 +722,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)); @@ -1692,7 +1728,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; @@ -1743,7 +1779,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(); @@ -1821,7 +1856,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(); } @@ -1836,8 +1875,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 1d34ed70..fff3e7d3 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;