diff --git a/HISTORY.md b/HISTORY.md index beebdb8d..ca04c584 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -30,6 +30,7 @@ http://visjs.org - Fixed drift in dragging nodes while zooming. - Fixed recursively constructing of hierarchical layouts. - Added borderWidth option for nodes. +- Implemented new Hierarchical view solver. diff --git a/dist/vis.js b/dist/vis.js index 6e68c5c0..668efd23 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 3.0.1-SNAPSHOT - * @date 2014-07-15 + * @date 2014-07-16 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -19256,7 +19256,7 @@ return /******/ (function(modules) { // webpackBootstrap damping: 0.09 }, repulsion: { - centralGravity: 0.1, + centralGravity: 0.0, springLength: 200, springConstant: 0.05, nodeDistance: 100, @@ -19264,10 +19264,10 @@ return /******/ (function(modules) { // webpackBootstrap }, hierarchicalRepulsion: { enabled: false, - centralGravity: 0.5, - springLength: 150, + centralGravity: 0.0, + springLength: 100, springConstant: 0.01, - nodeDistance: 60, + nodeDistance: 150, damping: 0.09 }, damping: null, @@ -26348,6 +26348,11 @@ return /******/ (function(modules) { // webpackBootstrap } if (this.constants.hierarchicalLayout.enabled == true) { this._setupHierarchicalLayout(); + showValueOfRange.call(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); + showValueOfRange.call(this, 'graph_H_cg', 1, "physics_centralGravity"); + showValueOfRange.call(this, 'graph_H_sc', 1, "physics_springConstant"); + showValueOfRange.call(this, 'graph_H_sl', 1, "physics_springLength"); + showValueOfRange.call(this, 'graph_H_damp', 1, "physics_damping"); } else { this.repositionNodes(); @@ -26605,29 +26610,23 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ exports._calculateNodeForces = function () { - var dx, dy, distance, fx, fy, combinedClusterSize, + var dx, dy, distance, fx, fy, 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.hierarchicalRepulsion.nodeDistance; - var minimumDistance = nodeDistance; - var a = a_base / minimumDistance; // 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]]; + + // nodes only affect nodes on their level if (node1.level == node2.level) { dx = node2.x - node1.x; @@ -26635,12 +26634,13 @@ return /******/ (function(modules) { // webpackBootstrap distance = Math.sqrt(dx * dx + dy * dy); - if (distance < 2 * minimumDistance) { - repulsingForce = a * distance + b; - var c = 0.05; - var d = 2 * minimumDistance * 2 * c; - repulsingForce = c * Math.pow(distance,2) - d * distance + d*d/(4*c); - + var steepness = 0.05; + if (distance < nodeDistance) { + repulsingForce = -Math.pow(steepness*distance,2) + Math.pow(steepness*nodeDistance,2); + } + else { + repulsingForce = 0; + } // normalize force with if (distance == 0) { distance = 0.01; @@ -26655,7 +26655,6 @@ return /******/ (function(modules) { // webpackBootstrap node1.fy -= fy; node2.fx += fx; node2.fy += fy; - } } } } @@ -26672,6 +26671,17 @@ return /******/ (function(modules) { // webpackBootstrap var dx, dy, fx, fy, springForce, distance; var edges = this.edges; + var nodes = this.calculationNodes; + var nodeIndices = this.calculationNodeIndices; + + + for (var i = 0; i < nodeIndices.length; i++) { + var node1 = nodes[nodeIndices[i]]; + node1.springFx = 0; + node1.springFy = 0; + } + + // forces caused by the edges, modelled as springs for (edgeId in edges) { if (edges.hasOwnProperty(edgeId)) { @@ -26691,30 +26701,24 @@ return /******/ (function(modules) { // webpackBootstrap distance = 0.01; } - distance = Math.max(0.8*edgeLength,Math.min(5*edgeLength, distance)); - // the 1/distance is so the fx and fy can be calculated without sine or cosine. springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; fx = dx * springForce; fy = dy * springForce; - edge.to.fx -= fx; - edge.to.fy -= fy; - edge.from.fx += fx; - edge.from.fy += fy; - var factor = 5; - if (distance > edgeLength) { - factor = 25; + if (edge.to.level != edge.from.level) { + edge.to.springFx -= fx; + edge.to.springFy -= fy; + edge.from.springFx += fx; + edge.from.springFy += fy; } - - if (edge.from.level > edge.to.level) { + else { + var factor = 0.5; edge.to.fx -= factor*fx; edge.to.fy -= factor*fy; - } - else if (edge.from.level < edge.to.level) { edge.from.fx += factor*fx; edge.from.fy += factor*fy; } @@ -26722,6 +26726,36 @@ return /******/ (function(modules) { // webpackBootstrap } } } + + // normalize spring forces + var springForce = 1; + var springFx, springFy; + for (i = 0; i < nodeIndices.length; i++) { + var node = nodes[nodeIndices[i]]; + springFx = Math.min(springForce,Math.max(-springForce,node.springFx)); + springFy = Math.min(springForce,Math.max(-springForce,node.springFy)); + + node.fx += springFx; + node.fy += springFy; + } + + // retain energy balance + var totalFx = 0; + var totalFy = 0; + for (i = 0; i < nodeIndices.length; i++) { + var node = nodes[nodeIndices[i]]; + totalFx += node.fx; + totalFy += node.fy; + } + var correctionFx = totalFx / nodeIndices.length; + var correctionFy = totalFy / nodeIndices.length; + + for (i = 0; i < nodeIndices.length; i++) { + var node = nodes[nodeIndices[i]]; + node.fx -= correctionFx; + node.fy -= correctionFy; + } + }; /***/ }, diff --git a/lib/network/Network.js b/lib/network/Network.js index 291c7cdc..764933a1 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -122,7 +122,7 @@ function Network (container, data, options) { damping: 0.09 }, repulsion: { - centralGravity: 0.1, + centralGravity: 0.0, springLength: 200, springConstant: 0.05, nodeDistance: 100, @@ -130,10 +130,10 @@ function Network (container, data, options) { }, hierarchicalRepulsion: { enabled: false, - centralGravity: 0.5, - springLength: 150, + centralGravity: 0.0, + springLength: 100, springConstant: 0.01, - nodeDistance: 60, + nodeDistance: 150, damping: 0.09 }, damping: null, diff --git a/lib/network/mixins/physics/HierarchialRepulsionMixin.js b/lib/network/mixins/physics/HierarchialRepulsionMixin.js index 9ece24c6..2b57e024 100644 --- a/lib/network/mixins/physics/HierarchialRepulsionMixin.js +++ b/lib/network/mixins/physics/HierarchialRepulsionMixin.js @@ -5,29 +5,23 @@ * @private */ exports._calculateNodeForces = function () { - var dx, dy, distance, fx, fy, combinedClusterSize, + var dx, dy, distance, fx, fy, 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.hierarchicalRepulsion.nodeDistance; - var minimumDistance = nodeDistance; - var a = a_base / minimumDistance; // 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]]; + + // nodes only affect nodes on their level if (node1.level == node2.level) { dx = node2.x - node1.x; @@ -35,12 +29,13 @@ exports._calculateNodeForces = function () { distance = Math.sqrt(dx * dx + dy * dy); - if (distance < 2 * minimumDistance) { - repulsingForce = a * distance + b; - var c = 0.05; - var d = 2 * minimumDistance * 2 * c; - repulsingForce = c * Math.pow(distance,2) - d * distance + d*d/(4*c); - + var steepness = 0.05; + if (distance < nodeDistance) { + repulsingForce = -Math.pow(steepness*distance,2) + Math.pow(steepness*nodeDistance,2); + } + else { + repulsingForce = 0; + } // normalize force with if (distance == 0) { distance = 0.01; @@ -55,7 +50,6 @@ exports._calculateNodeForces = function () { node1.fy -= fy; node2.fx += fx; node2.fy += fy; - } } } } @@ -72,6 +66,17 @@ exports._calculateHierarchicalSpringForces = function () { var dx, dy, fx, fy, springForce, distance; var edges = this.edges; + var nodes = this.calculationNodes; + var nodeIndices = this.calculationNodeIndices; + + + for (var i = 0; i < nodeIndices.length; i++) { + var node1 = nodes[nodeIndices[i]]; + node1.springFx = 0; + node1.springFy = 0; + } + + // forces caused by the edges, modelled as springs for (edgeId in edges) { if (edges.hasOwnProperty(edgeId)) { @@ -91,30 +96,24 @@ exports._calculateHierarchicalSpringForces = function () { distance = 0.01; } - distance = Math.max(0.8*edgeLength,Math.min(5*edgeLength, distance)); - // the 1/distance is so the fx and fy can be calculated without sine or cosine. springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; fx = dx * springForce; fy = dy * springForce; - edge.to.fx -= fx; - edge.to.fy -= fy; - edge.from.fx += fx; - edge.from.fy += fy; - var factor = 5; - if (distance > edgeLength) { - factor = 25; + if (edge.to.level != edge.from.level) { + edge.to.springFx -= fx; + edge.to.springFy -= fy; + edge.from.springFx += fx; + edge.from.springFy += fy; } - - if (edge.from.level > edge.to.level) { + else { + var factor = 0.5; edge.to.fx -= factor*fx; edge.to.fy -= factor*fy; - } - else if (edge.from.level < edge.to.level) { edge.from.fx += factor*fx; edge.from.fy += factor*fy; } @@ -122,4 +121,34 @@ exports._calculateHierarchicalSpringForces = function () { } } } + + // normalize spring forces + var springForce = 1; + var springFx, springFy; + for (i = 0; i < nodeIndices.length; i++) { + var node = nodes[nodeIndices[i]]; + springFx = Math.min(springForce,Math.max(-springForce,node.springFx)); + springFy = Math.min(springForce,Math.max(-springForce,node.springFy)); + + node.fx += springFx; + node.fy += springFy; + } + + // retain energy balance + var totalFx = 0; + var totalFy = 0; + for (i = 0; i < nodeIndices.length; i++) { + var node = nodes[nodeIndices[i]]; + totalFx += node.fx; + totalFy += node.fy; + } + var correctionFx = totalFx / nodeIndices.length; + var correctionFy = totalFy / nodeIndices.length; + + for (i = 0; i < nodeIndices.length; i++) { + var node = nodes[nodeIndices[i]]; + node.fx -= correctionFx; + node.fy -= correctionFy; + } + }; \ No newline at end of file diff --git a/lib/network/mixins/physics/PhysicsMixin.js b/lib/network/mixins/physics/PhysicsMixin.js index 404dd150..520f1bcf 100644 --- a/lib/network/mixins/physics/PhysicsMixin.js +++ b/lib/network/mixins/physics/PhysicsMixin.js @@ -521,6 +521,11 @@ function graphRepositionNodes () { } if (this.constants.hierarchicalLayout.enabled == true) { this._setupHierarchicalLayout(); + showValueOfRange.call(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); + showValueOfRange.call(this, 'graph_H_cg', 1, "physics_centralGravity"); + showValueOfRange.call(this, 'graph_H_sc', 1, "physics_springConstant"); + showValueOfRange.call(this, 'graph_H_sl', 1, "physics_springLength"); + showValueOfRange.call(this, 'graph_H_damp', 1, "physics_damping"); } else { this.repositionNodes();