Browse Source

mixin refactoring, added cluster normalization, tweaked forces, separated forces for repulsion and barnes hut

css_transitions
Alex de Mulder 10 years ago
parent
commit
4a08269699
13 changed files with 618 additions and 446 deletions
  1. +9
    -6
      Jakefile.js
  2. +7
    -6
      dist/vis.min.js
  3. +74
    -170
      src/graph/Graph.js
  4. +42
    -34
      src/graph/Node.js
  5. +97
    -25
      src/graph/graphMixins/ClusterMixin.js
  6. +0
    -0
      src/graph/graphMixins/ManipulationMixin.js
  7. +185
    -0
      src/graph/graphMixins/MixinLoader.js
  8. +0
    -0
      src/graph/graphMixins/NavigationMixin.js
  9. +0
    -0
      src/graph/graphMixins/SectorsMixin.js
  10. +0
    -0
      src/graph/graphMixins/SelectionMixin.js
  11. +114
    -0
      src/graph/graphMixins/physics/PhysicsMixin.js
  12. +29
    -205
      src/graph/graphMixins/physics/barnesHut.js
  13. +61
    -0
      src/graph/graphMixins/physics/repulsion.js

+ 9
- 6
Jakefile.js View File

@ -82,12 +82,15 @@ task('build', {async: true}, function () {
'./src/graph/Popup.js',
'./src/graph/Groups.js',
'./src/graph/Images.js',
'./src/graph/PhysicsMixin.js',
'./src/graph/ManipulationMixin.js',
'./src/graph/SectorsMixin.js',
'./src/graph/ClusterMixin.js',
'./src/graph/SelectionMixin.js',
'./src/graph/NavigationMixin.js',
'./src/graph/graphMixins/physics/PhysicsMixin.js',
'./src/graph/graphMixins/physics/barnesHut.js',
'./src/graph/graphMixins/physics/repulsion.js',
'./src/graph/graphMixins/ManipulationMixin.js',
'./src/graph/graphMixins/SectorsMixin.js',
'./src/graph/graphMixins/ClusterMixin.js',
'./src/graph/graphMixins/SelectionMixin.js',
'./src/graph/graphMixins/NavigationMixin.js',
'./src/graph/graphMixins/MixinLoader.js',
'./src/graph/Graph.js',
'./src/module/exports.js'

+ 7
- 6
dist/vis.min.js
File diff suppressed because it is too large
View File


+ 74
- 170
src/graph/Graph.js View File

@ -10,6 +10,9 @@
* @param {Object} options Options
*/
function Graph (container, data, options) {
this._initializeMixinLoaders();
// create variables and set default values
this.containerElement = container;
this.width = '100%';
@ -66,12 +69,22 @@ function Graph (container, data, options) {
}
},
physics: {
enableBarnesHut: false,
barnesHutTheta: 1 / 0.4, // inverted to save time during calculation
barnesHutGravitationalConstant: -10000,
centralGravity: 0.08,
springLength: 50,
springConstant: 0.02
barnesHut: {
enabled: false,
theta: 1 / 0.3, // inverted to save time during calculation
gravitationalConstant: -10000,
centralGravity: 0.08,
springLength: 100,
springConstant: 0.02
},
repulsion: {
centralGravity: 0.01,
springLength: 100,
springConstant: 0.05
},
centralGravity: null,
springLength: null,
springConstant: null
},
clustering: { // Per Node in Cluster = PNiC
enabled: false, // (Boolean) | global on/off switch for clustering.
@ -80,17 +93,19 @@ function Graph (container, data, options) {
reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this
chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
sectorThreshold: 50, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node.
fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
forceAmplification: 0.6, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
distanceAmplification: 0.2, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
edgeGrowth: 11, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
nodeScaling: {width: 10, // (px PNiC) | growth of the width per node in cluster.
height: 10, // (px PNiC) | growth of the height per node in cluster.
radius: 10}, // (px PNiC) | growth of the radius per node in cluster.
activeAreaBoxSize: 100, // (px) | box area around the curser where clusters are popped open.
massTransferCoefficient: 1 // (multiplier) | parent.mass += massTransferCoefficient * child.mass
forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
maxFontSize: 1000,
distanceAmplification: 0.03, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
edgeGrowth: 1, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
nodeScaling: {width: 5, // (px PNiC) | growth of the width per node in cluster.
height: 5, // (px PNiC) | growth of the height per node in cluster.
radius: 5}, // (px PNiC) | growth of the radius per node in cluster.
maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
clusterLevelDifference: 2
},
navigation: {
enabled: false,
@ -103,6 +118,7 @@ function Graph (container, data, options) {
dataManipulationToolbar: {
enabled: false
},
maxVelocity: 35,
minVelocity: 0.1, // px/s
maxIterations: 1000 // maximum number of iteration to stabilize
};
@ -194,11 +210,12 @@ function Graph (container, data, options) {
this.moving = false; // True if any of the nodes have an undefined position
this.timer = undefined;
// load data (the disable start variable will be the same as the enabled clustering)
this.setData(data,this.constants.clustering.enabled);
// zoom so all data will fit on the screen
this.zoomToFit(true);
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) {
@ -281,7 +298,7 @@ Graph.prototype._centerGraph = function(range) {
*
* @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
*/
Graph.prototype.zoomToFit = function(initialZoom) {
Graph.prototype.zoomToFit = function(initialZoom, doNotStart) {
if (initialZoom === undefined) {
initialZoom = false;
}
@ -315,7 +332,9 @@ Graph.prototype.zoomToFit = function(initialZoom) {
this.pinch.mousewheelScale = zoomLevel;
this._setScale(zoomLevel);
this._centerGraph(range);
this.start();
if (doNotStart == false || doNotStart === undefined) {
this.start();
}
};
@ -394,6 +413,27 @@ Graph.prototype.setOptions = function (options) {
if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
if (options.selectable !== undefined) {this.selectable = options.selectable;}
/*
if (options.physics) {
if (options.physics.barnesHut) {
this.constants.physics.barnesHut.enabled = true;
for (var prop in options.physics.barnesHut) {
if (options.physics.barnesHut.hasOwnProperty(prop)) {
this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
}
}
}
if (options.physics.repulsion) {
this.constants.physics.barnesHut.enabled = false;
for (var prop in options.physics.repulsion) {
if (options.physics.repulsion.hasOwnProperty(prop)) {
this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
}
}
}
}
*/
if (options.clustering) {
this.constants.clustering.enabled = true;
for (var prop in options.clustering) {
@ -502,6 +542,9 @@ Graph.prototype.setOptions = function (options) {
}
}
// load the force calculation functions, grouped under the physics system.
this._loadPhysicsSystem();
// load the navigation system.
this._loadNavigationControls();
@ -1693,7 +1736,7 @@ Graph.prototype._doStabilize = function() {
* @private
*/
Graph.prototype._isMoving = function(vmin) {
var vminCorrected = vmin / this.scale;
var vminCorrected = vmin / Math.max(this.scale,0.05);
var nodes = this.nodes;
for (var id in nodes) {
if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vminCorrected)) {
@ -1715,8 +1758,6 @@ Graph.prototype._discreteStepNodes = function() {
var interval = 1;
var nodes = this.nodes;
this.constants.maxVelocity = 30;
if (this.constants.maxVelocity > 0) {
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
@ -1785,14 +1826,14 @@ Graph.prototype.start = function() {
//this.time = this.end - this.startTime;
//console.log('refresh time: ' + this.time);
//this.startTime = window.performance.now();
var DOMelement = document.getElementById("calctimereporter");
if (DOMelement !== undefined) {
DOMelement.innerHTML = calctime;
}
DOMelement = document.getElementById("rendertimereporter");
if (DOMelement !== undefined) {
DOMelement.innerHTML = rendertime;
}
// var DOMelement = document.getElementById("calctimereporter");
// if (DOMelement !== undefined) {
// DOMelement.innerHTML = calctime;
// }
// DOMelement = document.getElementById("rendertimereporter");
// if (DOMelement !== undefined) {
// DOMelement.innerHTML = rendertime;
// }
}, this.renderTimestep);
}
}
@ -1832,151 +1873,14 @@ Graph.prototype.toggleFreeze = function() {
};
/**
* Mixin the physics system and initialize the parameters required.
*
* @private
*/
Graph.prototype._loadPhysicsSystem = function() {
this.forceCalculationTime = 0;
for (var mixinFunction in physicsMixin) {
if (physicsMixin.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = physicsMixin[mixinFunction];
}
}
};
/**
* Mixin the cluster system and initialize the parameters required.
*
* @private
*/
Graph.prototype._loadClusterSystem = function() {
this.clusterSession = 0;
this.hubThreshold = 5;
for (var mixinFunction in ClusterMixin) {
if (ClusterMixin.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = ClusterMixin[mixinFunction];
}
}
}
/**
* Mixin the sector system and initialize the parameters required
*
* @private
*/
Graph.prototype._loadSectorSystem = function() {
this.sectors = {};
this.activeSector = ["default"];
this.sectors["active"] = {};
this.sectors["active"]["default"] = {"nodes":{},
"edges":{},
"nodeIndices":[],
"formationScale": 1.0,
"drawingNode": undefined};
this.sectors["frozen"] = {};
this.sectors["navigation"] = {"nodes":{},
"edges":{},
"nodeIndices":[],
"formationScale": 1.0,
"drawingNode": undefined};
this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
for (var mixinFunction in SectorMixin) {
if (SectorMixin.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = SectorMixin[mixinFunction];
}
}
};
/**
* Mixin the selection system and initialize the parameters required
*
* @private
*/
Graph.prototype._loadSelectionSystem = function() {
this.selectionObj = {};
for (var mixinFunction in SelectionMixin) {
if (SelectionMixin.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = SelectionMixin[mixinFunction];
}
}
}
/**
* Mixin the navigationUI (User Interface) system and initialize the parameters required
*
* @private
*/
Graph.prototype._loadManipulationSystem = function() {
// reset global variables -- these are used by the selection of nodes and edges.
this.blockConnectingEdgeSelection = false;
this.forceAppendSelection = false
if (this.constants.dataManipulationToolbar.enabled == true) {
// load the manipulator HTML elements. All styling done in css.
if (this.manipulationDiv === undefined) {
this.manipulationDiv = document.createElement('div');
this.manipulationDiv.className = 'graph-manipulationDiv';
this.containerElement.insertBefore(this.manipulationDiv, this.frame);
}
// load the manipulation functions
for (var mixinFunction in manipulationMixin) {
if (manipulationMixin.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = manipulationMixin[mixinFunction];
}
Graph.prototype._initializeMixinLoaders = function () {
for (var mixinFunction in graphMixinLoaders) {
if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
}
// create the manipulator toolbar
this._createManipulatorBar();
}
}
/**
* Mixin the navigation (User Interface) system and initialize the parameters required
*
* @private
*/
Graph.prototype._loadNavigationControls = function() {
for (var mixinFunction in NavigationMixin) {
if (NavigationMixin.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = NavigationMixin[mixinFunction];
}
}
if (this.constants.navigation.enabled == true) {
this._loadNavigationElements();
}
}
/**
* this function exists to avoid errors when not loading the navigation system
*/
Graph.prototype._relocateNavigation = function() {
// empty, is overloaded by navigation system
}
/**
* * this function exists to avoid errors when not loading the navigation system
*/
Graph.prototype._unHighlightAll = function() {
// empty, is overloaded by the navigation system
}

+ 42
- 34
src/graph/Node.js View File

@ -52,6 +52,7 @@ function Node(properties, imagelist, grouplist, constants) {
this.radiusFixed = false;
this.radiusMin = constants.nodes.radiusMin;
this.radiusMax = constants.nodes.radiusMax;
this.internalMultiplier = 1;
this.imagelist = imagelist;
@ -66,6 +67,8 @@ function Node(properties, imagelist, grouplist, constants) {
this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
this.growthIndicator = 0;
// mass, force, velocity
this.mass = 1; // kg
@ -105,7 +108,6 @@ Node.prototype.attachEdge = function(edge) {
this.dynamicEdges.push(edge);
}
this.dynamicEdgesLength = this.dynamicEdges.length;
// this._updateMass();
};
/**
@ -119,17 +121,8 @@ Node.prototype.detachEdge = function(edge) {
this.dynamicEdges.splice(index, 1);
}
this.dynamicEdgesLength = this.dynamicEdges.length;
// this._updateMass();
};
/**
* Update the nodes mass, which is determined by the number of edges connecting
* to it (more edges -> heavier node).
* @private
*/
//Node.prototype._updateMass = function() {
// this.mass = 1;// + 0.6 * this.edges.length; // kg
//};
/**
* Set or overwrite properties for the node
@ -150,6 +143,10 @@ Node.prototype.setProperties = function(properties, constants) {
if (properties.y !== undefined) {this.y = properties.y;}
if (properties.value !== undefined) {this.value = properties.value;}
// physics
if (properties.internalMultiplier !== undefined) {this.internalMultiplier = properties.value;}
// navigation controls properties
if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
@ -542,10 +539,12 @@ Node.prototype._resizeImage = function (ctx) {
this.width = width;
this.height = height;
this.growthIndicator = 0;
if (this.width > 0 && this.height > 0) {
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - width;
}
}
@ -590,9 +589,11 @@ Node.prototype._resizeBox = function (ctx) {
this.width = textSize.width + 2 * margin;
this.height = textSize.height + 2 * margin;
this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor;
// this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor;
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
this.growthIndicator = this.width - textSize.width + 2 * margin;
// this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
}
};
@ -639,9 +640,10 @@ Node.prototype._resizeDatabase = function (ctx) {
this.height = size;
// scaling used for clustering
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - size;
}
};
@ -688,9 +690,10 @@ Node.prototype._resizeCircle = function (ctx) {
this.height = diameter;
// scaling used for clustering
// this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor;
// this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor;
// this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
// this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
this.growthIndicator = this.radius - diameter;
}
};
@ -734,11 +737,13 @@ Node.prototype._resizeEllipse = function (ctx) {
if (this.width < this.height) {
this.width = this.height;
}
var defaultSize = this.width;
// scaling used for clustering
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
// scaling used for clustering
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - defaultSize;
}
};
@ -801,9 +806,10 @@ Node.prototype._resizeShape = function (ctx) {
this.height = size;
// scaling used for clustering
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor;
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - size;
}
};
@ -860,9 +866,10 @@ Node.prototype._resizeText = function (ctx) {
this.height = textSize.height + 2 * margin;
// scaling used for clustering
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - textSize.width + 2 * margin;
}
};
@ -968,12 +975,13 @@ Node.prototype.setScale = function(scale) {
};
/**
* This function updates the damping parameter for clusters, based ont he
* This function updates the damping parameter of the node based on its mass,
* heavier nodes have more damping.
*
* @param {Number} numberOfNodes
*/
Node.prototype.updateDamping = function(numberOfNodes) {
this.damping = (0.9 + 0.1*this.clusterSize * (1 + Math.pow(numberOfNodes,-2)));
this.damping = Math.min(1.5,0.9 + 0.01*this.growthIndicator);
};

src/graph/ClusterMixin.js → src/graph/graphMixins/ClusterMixin.js View File

@ -41,17 +41,19 @@ var ClusterMixin = {
// we first cluster the hubs, then we pull in the outliers, repeat
while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
if (level % 3 == 0) {
this.forceAggregateHubs();
this.forceAggregateHubs(true);
this.normalizeClusterLevels();
}
else {
this.increaseClusterLevel();
this.increaseClusterLevel(); // this also includes a cluster normalization
}
numberOfNodes = this.nodeIndices.length;
level += 1;
}
// after the clustering we reposition the nodes to reduce the initial chaos
if (level > 1 && reposition == true) {
if (level > 0 && reposition == true) {
this.repositionNodes();
}
},
@ -88,6 +90,7 @@ var ClusterMixin = {
}
},
/**
* This calls the updateClustes with default arguments
*/
@ -97,6 +100,7 @@ var ClusterMixin = {
}
},
/**
* This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
* be clustered with their connected node. This can be repeated as many times as needed.
@ -107,7 +111,6 @@ var ClusterMixin = {
},
/**
* This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
* be unpacked if they are a cluster. This can be repeated as many times as needed.
@ -129,7 +132,7 @@ var ClusterMixin = {
* @param {Boolean} force | enabled or disable forcing
*
*/
updateClusters : function(zoomDirection,recursive,force) {
updateClusters : function(zoomDirection,recursive,force,doNotStart) {
var isMovingBeforeClustering = this.moving;
var amountOfNodes = this.nodeIndices.length;
@ -178,11 +181,15 @@ var ClusterMixin = {
// if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
this.clusterSession += 1;
// if clusters have been made, we normalize the cluster level
this.normalizeClusterLevels();
}
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
if (doNotStart == false || doNotStart === undefined) {
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
}
},
@ -214,7 +221,7 @@ var ClusterMixin = {
* This function is fired by keypress. It forces hubs to form.
*
*/
forceAggregateHubs : function() {
forceAggregateHubs : function(doNotStart) {
var isMovingBeforeClustering = this.moving;
var amountOfNodes = this.nodeIndices.length;
@ -230,9 +237,11 @@ var ClusterMixin = {
this.clusterSession += 1;
}
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
if (doNotStart == false || doNotStart === undefined) {
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
}
},
@ -353,14 +362,14 @@ var ClusterMixin = {
this._validateEdges(parentNode);
// undo the changes from the clustering operation on the parent node
parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
parentNode.mass -= childNode.mass;
parentNode.clusterSize -= childNode.clusterSize;
parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
// place the child node near the parent, not at the exact same location to avoid chaos in the system
childNode.x = parentNode.x + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
childNode.y = parentNode.y + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random()) * childNode.clusterSize;
childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random()) * childNode.clusterSize;
// remove node from the list
delete parentNode.containedNodes[containedNodeId];
@ -493,6 +502,32 @@ var ClusterMixin = {
},
_clusterToSmallestNeighbour : function(node) {
var smallestNeighbour = -1;
var smallestNeighbourNode = null;
for (var i = 0; i < node.dynamicEdges.length; i++) {
if (node.dynamicEdges[i] !== undefined) {
var neighbour = null;
if (node.dynamicEdges[i].fromId != node.id) {
neighbour = node.dynamicEdges[i].from;
}
else if (node.dynamicEdges[i].toId != node.id) {
neighbour = node.dynamicEdges[i].to;
}
if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
smallestNeighbour = neighbour.clusterSessions.length;
smallestNeighbourNode = neighbour;
}
}
}
if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
this._addToCluster(neighbour, node, true);
}
},
/**
* This function forms clusters from hubs, it loops over all nodes
@ -618,9 +653,9 @@ var ClusterMixin = {
// update the properties of the child and parent
var massBefore = parentNode.mass;
childNode.clusterSession = this.clusterSession;
parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.mass += childNode.mass;
parentNode.clusterSize += childNode.clusterSize;
parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
// keep track of the clustersessions so we can open the cluster up as it has been formed.
if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
@ -895,16 +930,52 @@ var ClusterMixin = {
}
/* Debug Override */
// for (nodeId in this.nodes) {
// if (this.nodes.hasOwnProperty(nodeId)) {
// node = this.nodes[nodeId];
// node.label = String(Math.round(node.width)).concat(":",Math.round(node.width*this.scale));
// }
// }
// for (nodeId in this.nodes) {
// if (this.nodes.hasOwnProperty(nodeId)) {
// node = this.nodes[nodeId];
// node.label = String(node.fx).concat(",",node.fy);
// }
// }
},
normalizeClusterLevels : function() {
var maxLevel = 0;
var minLevel = 1e9;
var clusterLevel = 0;
// we loop over all nodes in the list
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
clusterLevel = this.nodes[nodeId].clusterSessions.length;
if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
if (minLevel > clusterLevel) {minLevel = clusterLevel;}
}
}
if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
var amountOfNodes = this.nodeIndices.length;
var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
// we loop over all nodes in the list
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
this._clusterToSmallestNeighbour(this.nodes[nodeId]);
}
}
}
this._updateNodeIndexList();
this._updateDynamicEdges();
// if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length != amountOfNodes) {
this.clusterSession += 1;
}
}
},
/**
* This function determines if the cluster we want to decluster is in the active area
* this means around the zoom center
@ -931,7 +1002,7 @@ var ClusterMixin = {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (!node.isFixed()) {
var radius = this.constants.edges.length * (1 + 0.6*node.clusterSize);
var radius = this.constants.physics.springLength * (1 + 0.1*node.mass);
var angle = 2 * Math.PI * Math.random();
node.x = radius * Math.cos(angle);
node.y = radius * Math.sin(angle);
@ -956,6 +1027,7 @@ var ClusterMixin = {
var largestHub = 0;
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (node.dynamicEdgesLength > largestHub) {
largestHub = node.dynamicEdgesLength;

src/graph/manipulationMixin.js → src/graph/graphMixins/ManipulationMixin.js View File


+ 185
- 0
src/graph/graphMixins/MixinLoader.js View File

@ -0,0 +1,185 @@
/**
* Created by Alex on 2/10/14.
*/
var graphMixinLoaders = {
/**
* Load a mixin into the graph object
*
* @param {Object} sourceVariable | this object has to contain functions.
* @private
*/
_loadMixin : function(sourceVariable) {
for (var mixinFunction in sourceVariable) {
if (sourceVariable.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = sourceVariable[mixinFunction];
}
}
},
/**
* removes a mixin from the graph object.
*
* @param {Object} sourceVariable | this object has to contain functions.
* @private
*/
_clearMixin : function(sourceVariable) {
for (var mixinFunction in sourceVariable) {
if (sourceVariable.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = undefined;
}
}
},
/**
* Mixin the physics system and initialize the parameters required.
*
* @private
*/
_loadPhysicsSystem : function() {
this._loadMixin(physicsMixin);
this._loadSelectedForceSolver();
},
/**
* This loads the node force solver based on the barnes hut or repulsion algorithm
*
* @private
*/
_loadSelectedForceSolver : function() {
// this overloads the this._calculateNodeForces
if (this.constants.physics.barnesHut.enabled == true) {
this._clearMixin(repulsionMixin);
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.springGrowthPerMass = this.constants.physics.barnesHut.springGrowthPerMass;
this._loadMixin(barnesHutMixin);
}
else {
this._clearMixin(barnesHutMixin);
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.springGrowthPerMass = this.constants.physics.repulsion.springGrowthPerMass;
this._loadMixin(repulsionMixin);
}
},
/**
* Mixin the cluster system and initialize the parameters required.
*
* @private
*/
_loadClusterSystem : function() {
this.clusterSession = 0;
this.hubThreshold = 5;
this._loadMixin(ClusterMixin);
},
/**
* Mixin the sector system and initialize the parameters required
*
* @private
*/
_loadSectorSystem : function() {
this.sectors = { },
this.activeSector = ["default"];
this.sectors["active"] = { },
this.sectors["active"]["default"] = {"nodes":{},
"edges":{},
"nodeIndices":[],
"formationScale": 1.0,
"drawingNode": undefined },
this.sectors["frozen"] = { },
this.sectors["navigation"] = {"nodes":{},
"edges":{},
"nodeIndices":[],
"formationScale": 1.0,
"drawingNode": undefined },
this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
this._loadMixin(SectorMixin);
},
/**
* Mixin the selection system and initialize the parameters required
*
* @private
*/
_loadSelectionSystem : function() {
this.selectionObj = { };
this._loadMixin(SelectionMixin);
},
/**
* Mixin the navigationUI (User Interface) system and initialize the parameters required
*
* @private
*/
_loadManipulationSystem : function() {
// reset global variables -- these are used by the selection of nodes and edges.
this.blockConnectingEdgeSelection = false;
this.forceAppendSelection = false
if (this.constants.dataManipulationToolbar.enabled == true) {
// load the manipulator HTML elements. All styling done in css.
if (this.manipulationDiv === undefined) {
this.manipulationDiv = document.createElement('div');
this.manipulationDiv.className = 'graph-manipulationDiv';
this.containerElement.insertBefore(this.manipulationDiv, this.frame);
}
// load the manipulation functions
this._loadMixin(manipulationMixin);
// create the manipulator toolbar
this._createManipulatorBar();
}
},
/**
* Mixin the navigation (User Interface) system and initialize the parameters required
*
* @private
*/
_loadNavigationControls : function() {
this._loadMixin(NavigationMixin);
if (this.constants.navigation.enabled == true) {
this._loadNavigationElements();
}
},
/**
* this function exists to avoid errors when not loading the navigation system
*/
_relocateNavigation : function() {
// empty, is overloaded by navigation system
},
/**
* this function exists to avoid errors when not loading the navigation system
*/
_unHighlightAll : function() {
// empty, is overloaded by the navigation system
}
}

src/graph/NavigationMixin.js → src/graph/graphMixins/NavigationMixin.js View File


src/graph/SectorsMixin.js → src/graph/graphMixins/SectorsMixin.js View File


src/graph/SelectionMixin.js → src/graph/graphMixins/SelectionMixin.js View File


+ 114
- 0
src/graph/graphMixins/physics/PhysicsMixin.js View File

@ -0,0 +1,114 @@
/**
* Created by Alex on 2/6/14.
*/
var physicsMixin = {
_toggleBarnesHut : function() {
this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
this._loadSelectedForceSolver();
this.moving = true;
this.start();
},
/**
* Before calculating the forces, we check if we need to cluster to keep up performance and we check
* if there is more than one node. If it is just one node, we dont calculate anything.
*
* @private
*/
_initializeForceCalculation : function() {
// stop calculation if there is only one node
if (this.nodeIndices.length == 1) {
this.nodes[this.nodeIndices[0]]._setForce(0,0);
}
else {
// if there are too many nodes on screen, we cluster without repositioning
if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
this.clusterToFit(this.constants.clustering.reduceToNodes, false);
}
// we now start the force calculation
this._calculateForces();
}
},
/**
* Calculate the external forces acting on the nodes
* Forces are caused by: edges, repulsing forces between nodes, gravity
* @private
*/
_calculateForces : function() {
this.barnesHutTree = undefined;
// Gravity is required to keep separated groups from floating off
// the forces are reset to zero in this loop by using _setForce instead
// of _addForce
this._calculateGravitationalForces();
this._calculateNodeForces();
this._calculateSpringForces();
},
_calculateGravitationalForces : function() {
var dx, dy, angle, fx, fy, node, i;
var nodes = this.nodes;
var gravity = this.constants.physics.centralGravity;
for (i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[i]];
// gravity does not apply when we are in a pocket sector
if (this._sector() == "default") {
dx = -node.x;// + screenCenterPos.x;
dy = -node.y;// + screenCenterPos.y;
angle = Math.atan2(dy, dx);
fx = Math.cos(angle) * gravity;
fy = Math.sin(angle) * gravity;
}
else {
fx = 0;
fy = 0;
}
node._setForce(fx, fy);
node.updateDamping(this.nodeIndices.length);
}
},
_calculateSpringForces : function() {
var dx, dy, angle, fx, fy, springForce, length, edgeLength, edge, edgeId;
var edges = this.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
edgeLength = edge.length;
// this implies that the edges between big clusters are longer
edgeLength += (edge.to.growthIndicator + edge.from.growthIndicator) * this.constants.clustering.edgeGrowth;
length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx);
springForce = this.constants.physics.springConstant * (edgeLength - length);
fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce;
edge.from._addForce(-fx, -fy);
edge.to._addForce(fx, fy);
}
}
}
}
}
}

src/graph/physicsMixin.js → src/graph/graphMixins/physics/barnesHut.js View File

@ -1,194 +1,11 @@
/**
* Created by Alex on 2/6/14.
* Created by Alex on 2/10/14.
*/
var barnesHutMixin = {
var physicsMixin = {
_toggleBarnesHut : function() {
this.constants.physics.enableBarnesHut = !this.constants.physics.enableBarnesHut;
this.moving = true;
this.start();
},
/**
* Before calculating the forces, we check if we need to cluster to keep up performance and we check
* if there is more than one node. If it is just one node, we dont calculate anything.
*
* @private
*/
_initializeForceCalculation : function() {
// stop calculation if there is only one node
if (this.nodeIndices.length == 1) {
this.nodes[this.nodeIndices[0]]._setForce(0,0);
}
else {
// if there are too many nodes on screen, we cluster without repositioning
if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
this.clusterToFit(this.constants.clustering.reduceToNodes, false);
}
// we now start the force calculation
if (this.constants.physics.enableBarnesHut == true) {
this._calculateForcesBarnesHut();
}
else {
this.barnesHutTree = undefined;
this._calculateForcesRepulsion();
}
}
},
/**
* Calculate the external forces acting on the nodes
* Forces are caused by: edges, repulsing forces between nodes, gravity
* @private
*/
_calculateForcesRepulsion : function() {
// Gravity is required to keep separated groups from floating off
// the forces are reset to zero in this loop by using _setForce instead
// of _addForce
this._calculateGravitationalForces();
this._calculateRepulsionForces();
this._calculateSpringForces();
},
/**
* Calculate the external forces acting on the nodes
* Forces are caused by: edges, repulsing forces between nodes, gravity
* @private
*/
_calculateForcesBarnesHut : function() {
this._calculateGravitationalForces();
this._calculateBarnesHutForces();
this._calculateSpringForces();
},
_clearForces : function() {
var node;
var nodes = this.nodes;
for (var i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[i]];
node._setForce(0, 0);
node.updateDamping(this.nodeIndices.length);
}
},
_calculateGravitationalForces : function() {
var dx, dy, angle, fx, fy, node, i;
var nodes = this.nodes;
var gravity = this.constants.physics.centralGravity;
for (i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[i]];
// gravity does not apply when we are in a pocket sector
if (this._sector() == "default") {
dx = -node.x;// + screenCenterPos.x;
dy = -node.y;// + screenCenterPos.y;
angle = Math.atan2(dy, dx);
fx = Math.cos(angle) * gravity;
fy = Math.sin(angle) * gravity;
}
else {
fx = 0;
fy = 0;
}
node._setForce(fx, fy);
node.updateDamping(this.nodeIndices.length);
}
},
_calculateRepulsionForces : function() {
var dx, dy, angle, distance, fx, fy, clusterSize,
repulsingForce, node1, node2, i, j;
var nodes = this.nodes;
// approximation constants
var a_base = -2/3;
var b = 4/3;
// repulsing forces between nodes
var minimumDistance = this.constants.nodes.distance;
// 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 < this.nodeIndices.length-1; i++) {
node1 = nodes[this.nodeIndices[i]];
for (j = i+1; j < this.nodeIndices.length; j++) {
node2 = nodes[this.nodeIndices[j]];
clusterSize = (node1.clusterSize + node2.clusterSize - 2);
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification));
var a = a_base / minimumDistance;
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
angle = Math.atan2(dy, dx);
if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
repulsingForce = 1.0;
}
else {
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
}
// amplify the repulsion for clusters.
repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification;
fx = Math.cos(angle) * repulsingForce;
fy = Math.sin(angle) * repulsingForce ;
node1._addForce(-fx, -fy);
node2._addForce(fx, fy);
}
}
}
},
_calculateSpringForces : function() {
var dx, dy, angle, fx, fy, springForce, length, edgeLength, edge, edgeId, clusterSize;
var edges = this.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
edgeLength = edge.length;
// this implies that the edges between big clusters are longer
edgeLength += clusterSize * this.constants.clustering.edgeGrowth;
length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx);
springForce = this.constants.physics.springConstant * (edgeLength - length);
fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce;
edge.from._addForce(-fx, -fy);
edge.to._addForce(fx, fy);
}
}
}
}
},
_calculateBarnesHutForces : function() {
_calculateNodeForces : function() {
this._formBarnesHutTree();
var nodes = this.nodes;
@ -221,7 +38,7 @@ var physicsMixin = {
if (distance > 0) { // distance is 0 if it looks to apply a force on itself.
// BarnesHut condition
if (distance * parentBranch.calcSize < this.constants.physics.barnesHutTheta) {
if (distance * parentBranch.calcSize < this.constants.physics.barnesHut.theta) {
// Did not pass the condition, go into children if available
if (parentBranch.childrenCount == 4) {
this._getForceContribution(parentBranch.children.NW,node);
@ -243,8 +60,9 @@ var physicsMixin = {
},
_getForceOnNode : function(parentBranch, node, dx ,dy, distance) {
//console.log(Math.max(Math.max(node.height,node.radius),node.width),parentBranch.maxWidth,distance);
// even if the parentBranch only has one node, its Center of Mass is at the right place (the node in this case).
var gravityForce = this.constants.physics.barnesHutGravitationalConstant * parentBranch.mass * node.mass / (distance * distance);
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance);
var angle = Math.atan2(dy, dx);
var fx = Math.cos(angle) * gravityForce;
var fy = Math.sin(angle) * gravityForce;
@ -259,9 +77,9 @@ var physicsMixin = {
var nodeCount = nodeIndices.length;
var minX = Number.MAX_VALUE,
minY = Number.MAX_VALUE,
maxX =-Number.MAX_VALUE,
maxY =-Number.MAX_VALUE;
minY = Number.MAX_VALUE,
maxX =-Number.MAX_VALUE,
maxY =-Number.MAX_VALUE;
// get the range of the nodes
for (var i = 0; i < nodeCount; i++) {
@ -280,15 +98,16 @@ var physicsMixin = {
// construct the barnesHutTree
var barnesHutTree = {root:{
CenterOfMass:{x:0,y:0}, // Center of Mass
mass:0,
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
size: Math.abs(maxX - minX),
calcSize: 1 / Math.abs(maxX - minX),
children: {data:null},
level: 0,
childrenCount: 4
}};
CenterOfMass:{x:0,y:0}, // Center of Mass
mass:0,
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
size: Math.abs(maxX - minX),
calcSize: 1 / Math.abs(maxX - minX),
children: {data:null},
maxWidth: 0,
level: 0,
childrenCount: 4
}};
this._splitBranch(barnesHutTree.root);
// place the nodes one by one recursively
@ -313,6 +132,9 @@ var physicsMixin = {
parentBranch.CenterOfMass.y *= totalMassInv;
parentBranch.mass = totalMass;
var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
},
@ -424,6 +246,7 @@ var physicsMixin = {
size: 0.5 * parentBranch.size,
calcSize: 2 * parentBranch.calcSize,
children: {data:null},
maxWidth: 0,
level: parentBranch.level +1,
childrenCount: 0
};
@ -471,10 +294,11 @@ var physicsMixin = {
ctx.stroke();
/*
if (branch.mass > 0) {
ctx.circle(branch.CenterOfMass.x, branch.CenterOfMass.y, 3*branch.mass);
ctx.stroke();
}
*/
if (branch.mass > 0) {
ctx.circle(branch.CenterOfMass.x, branch.CenterOfMass.y, 3*branch.mass);
ctx.stroke();
}
*/
}
};

+ 61
- 0
src/graph/graphMixins/physics/repulsion.js View File

@ -0,0 +1,61 @@
/**
* Created by Alex on 2/10/14.
*/
var repulsionMixin = {
/**
* Calculate the forces the nodes apply on eachother based on a repulsion field.
* This field is linearly approximated.
*
* @private
*/
_calculateNodeForces : function() {
var dx, dy, angle, distance, fx, fy, combinedClusterSize,
repulsingForce, node1, node2, i, j;
var nodes = this.nodes;
// approximation constants
var a_base = -2/3;
var b = 4/3;
// repulsing forces between nodes
var minimumDistance = this.constants.nodes.distance;
// 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 < this.nodeIndices.length-1; i++) {
node1 = nodes[this.nodeIndices[i]];
for (j = i+1; j < this.nodeIndices.length; j++) {
node2 = nodes[this.nodeIndices[j]];
combinedClusterSize = (node1.growthIndicator + node2.growthIndicator);
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
minimumDistance = (combinedClusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
var a = a_base / minimumDistance;
if (distance < 2*minimumDistance) {
angle = Math.atan2(dy, dx);
if (distance < 0.5*minimumDistance) {
repulsingForce = 1.0;
}
else {
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
}
// amplify the repulsion for clusters.
repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
repulsingForce *= node1.internalMultiplier * node2.internalMultiplier;
fx = Math.cos(angle) * repulsingForce;
fy = Math.sin(angle) * repulsingForce;
node1._addForce(-fx, -fy);
node2._addForce(fx, fy);
}
}
}
}
}

Loading…
Cancel
Save