Browse Source

Merge branch 'v4' into hammerjs2

Conflicts:
	dist/vis.js
	lib/network/mixins/ManipulationMixin.js
flowchartTest
jos 9 years ago
parent
commit
15d0ae84c0
47 changed files with 16439 additions and 17346 deletions
  1. +31
    -0
      HISTORY.md
  2. +3
    -3
      README.md
  3. +1
    -1
      bower.json
  4. +3
    -5
      dist/vis.css
  5. +11968
    -12525
      dist/vis.js
  6. +1
    -1
      dist/vis.min.css
  7. +2
    -2
      docs/timeline.html
  8. +1
    -0
      examples/graph2d/19_labels.html
  9. +103
    -0
      examples/network/39_newClustering.html
  10. +2
    -2
      examples/timeline/18_range_overflow.html
  11. +5
    -2
      gulpfile.js
  12. +1
    -3
      lib/DataSet.js
  13. +18
    -20
      lib/network/Edge.js
  14. +355
    -1822
      lib/network/Network.js
  15. +15
    -290
      lib/network/Node.js
  16. +0
    -1129
      lib/network/mixins/ClusterMixin.js
  17. +28
    -31
      lib/network/mixins/HierarchicalLayoutMixin.js
  18. +28
    -31
      lib/network/mixins/ManipulationMixin.js
  19. +0
    -40
      lib/network/mixins/MixinLoader.js
  20. +52
    -52
      lib/network/mixins/SectorsMixin.js
  21. +16
    -17
      lib/network/mixins/SelectionMixin.js
  22. +0
    -399
      lib/network/mixins/physics/BarnesHutMixin.js
  23. +0
    -154
      lib/network/mixins/physics/HierarchialRepulsionMixin.js
  24. +0
    -724
      lib/network/mixins/physics/PhysicsMixin.js
  25. +0
    -64
      lib/network/mixins/physics/RepulsionMixin.js
  26. +229
    -0
      lib/network/modules/Canvas.js
  27. +246
    -0
      lib/network/modules/CanvasRenderer.js
  28. +620
    -0
      lib/network/modules/Clustering.js
  29. +419
    -0
      lib/network/modules/PhysicsEngine.js
  30. +460
    -0
      lib/network/modules/TouchEventHandler.js
  31. +342
    -0
      lib/network/modules/View.js
  32. +640
    -0
      lib/network/modules/components/SelectionHandler.js
  33. +446
    -0
      lib/network/modules/components/physics/BarnesHutSolver.js
  34. +40
    -0
      lib/network/modules/components/physics/CentralGravitySolver.js
  35. +73
    -0
      lib/network/modules/components/physics/HierarchicalRepulsionSolver.js
  36. +104
    -0
      lib/network/modules/components/physics/HierarchicalSpringSolver.js
  37. +75
    -0
      lib/network/modules/components/physics/RepulsionSolver.js
  38. +80
    -0
      lib/network/modules/components/physics/SpringSolver.js
  39. +3
    -9
      lib/timeline/Core.js
  40. +2
    -2
      lib/timeline/TimeStep.js
  41. +1
    -1
      lib/timeline/component/CurrentTime.js
  42. +3
    -5
      lib/timeline/component/css/item.css
  43. +4
    -2
      lib/timeline/component/graph2d_types/bar.js
  44. +5
    -3
      lib/util.js
  45. +4
    -1
      package.json
  46. +5
    -3
      test/DataSet.test.js
  47. +5
    -3
      test/DataView.test.js

+ 31
- 0
HISTORY.md View File

@ -2,6 +2,32 @@
http://visjs.org
## not yet released, version 4.0.0-SNAPSHOT
### General
- Changed the build scripts to include a transpilation of ES6 to ES5
(using http://6to5.org), so we can use ES6 features in the vis.js code.
When creating a custom bundle using browserify, one now needs to add a
transform step using `6to5ify`, this is described in README.md.
### Timeline
- Fixed range items not being displayed smaller than 10 pixels (twice the
padding). In order to have overflowing text, one should now apply css style
`.vis.timeline .item.range { overflow: visible; }` instead of
`.vis.timeline .item.range .content { overflow: visible; }`.
See example 18_range_overflow.html.
- Fixed invalid css names for time axis grid, renamed hours class names from
`4-8h` to `h4-h8`.
### Network
- Rebuilt the cluster system
## not yet released, version 3.10.1-SNAPSHOT
### Network
@ -65,6 +91,11 @@ http://visjs.org
- Fixed a bug in the `DataSet` returning an empty object instead of `null` when
no item was found when using both a filter and specifying fields.
### Timeline
- Implemented option `timeAxis: {scale: string, step: number}` to set a
fixed scale.
## 2015-01-16, version 3.9.1

+ 3
- 3
README.md View File

@ -163,7 +163,7 @@ The source code of vis.js consists of commonjs modules, which makes it possible
Before you can do a build:
- Install node.js, npm, and browserify on your system.
- Install node.js, npm, browserify, and uglify-js on your system.
- Download or clone the vis.js project.
- Install the dependencies of vis.js by running `npm install` in the root of the project.
@ -179,7 +179,7 @@ exports.Timeline = require('./lib/timeline/Timeline');
Install browserify globally via `[sudo] npm install -g browserify`, then create a custom bundle like:
browserify custom.js -o vis-custom.js -s vis
browserify custom.js -t 6to5ify -o vis-custom.js -s vis
This will generate a custom bundle *vis-custom.js*, which exposes the namespace `vis` containing only `DataSet` and `Timeline`. The generated bundle can be minified with uglifyjs (installed globally with `[sudo] npm install -g uglify-js`):
@ -204,7 +204,7 @@ The custom bundle can now be loaded like:
The default bundle `vis.js` is standalone and includes external dependencies such as hammer.js and moment.js. When these libraries are already loaded by the application, vis.js does not need to include these dependencies itself too. To build a custom bundle of vis.js excluding moment.js and hammer.js, run browserify in the root of the project:
browserify index.js -o vis-custom.js -s vis -x moment -x hammerjs
browserify index.js -t 6to5ify -o vis-custom.js -s vis -x moment -x hammerjs
This will generate a custom bundle *vis-custom.js*, which exposes the namespace `vis`, and has moment and hammerjs excluded. The generated bundle can be minified with uglifyjs:

+ 1
- 1
bower.json View File

@ -1,6 +1,6 @@
{
"name": "vis",
"version": "3.10.1-SNAPSHOT",
"version": "4.0.0-SNAPSHOT",
"main": ["dist/vis.min.js", "dist/vis.min.css"],
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",

+ 3
- 5
dist/vis.css View File

@ -173,7 +173,7 @@
border-width: 1px;
background-color: #D5DDF6;
display: inline-block;
padding: 5px;
overflow: hidden;
}
.vis.timeline .item.selected {
@ -217,7 +217,6 @@
}
.vis.timeline .item.background {
overflow: hidden;
border: none;
background-color: rgba(213, 221, 246, 0.4);
box-sizing: border-box;
@ -229,13 +228,11 @@
position: relative;
display: inline-block;
max-width: 100%;
overflow: hidden;
}
.vis.timeline .item.background .content {
position: absolute;
display: inline-block;
overflow: hidden;
max-width: 100%;
margin: 5px;
}
@ -250,7 +247,8 @@
.vis.timeline .item .content {
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
padding: 5px;
}
.vis.timeline .item .delete {

+ 11968
- 12525
dist/vis.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/vis.min.css
File diff suppressed because it is too large
View File


+ 2
- 2
docs/timeline.html View File

@ -1401,10 +1401,10 @@ To load a locale into the Timeline not supported by default, one can add a new l
<td>Current date</td><td><code>today</code>, <code>tomorrow</code>, <code>yesterday</code>, <code>current-week</code>, <code>current-month</code>, <code>current-year</code></td>
</tr>
<tr>
<td>Hours</td><td><code>0h</code>, <code>1h</code>, ..., <code>23h</code></td>
<td>Hours</td><td><code>h0</code>, <code>h1</code>, ..., <code>h23</code></td>
</tr>
<tr>
<td>Grouped hours</td><td><code>0-4h</code> to <code>20-24h</code></td>
<td>Grouped hours</td><td><code>h0-h4</code> to <code>h20-h24</code></td>
</tr>
<tr>
<td>Weekday</td><td><code>monday</code>, <code>tuesday</code>, <code>wednesday</code>, <code>thursday</code>, <code>friday</code>, <code>saturday</code>, <code>sunday</code></td>

+ 1
- 0
examples/graph2d/19_labels.html View File

@ -57,6 +57,7 @@
var options = {
start: '2014-06-10',
end: '2014-06-18',
style:'bar'
};
var graph2d = new vis.Graph2d(container, dataset, options);
</script>

+ 103
- 0
examples/network/39_newClustering.html View File

@ -0,0 +1,103 @@
<!doctype html>
<html>
<head>
<title>Network | Basic usage</title>
<script type="text/javascript" src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
<style type="text/css">
#mynetwork {
width: 400px;
height: 400px;
border: 1px solid lightgray;
}
</style>
</head>
<body>
<div id="mynetwork"></div>
<script type="text/javascript">
// create an array with nodes
var nodes = [
{id: 1, label: 'Node 1'},
{id: 2, label: 'Node 2'},
{id: 3, label: 'Node 3'},
{id: 4, label: 'Node 4'},
{id: 5, label: 'Node 5'},
{id: 6, label: 'Node 6', cid:1},
{id: 7, label: 'Node 7', cid:1},
{id: 8, label: 'Node 8', cid:1},
{id: 9, label: 'Node 9', cid:1},
{id: 10, label: 'Node 10', cid:1}
];
// create an array with edges
var edges = [
{from: 1, to: 2},
{from: 1, to: 3},
{from: 10, to: 4},
{from: 2, to: 5},
{from: 6, to: 2},
{from: 7, to: 5},
{from: 8, to: 6},
{from: 9, to: 7},
{from: 10, to: 9}
];
// create a network
var container = document.getElementById('mynetwork');
var data = {
nodes: nodes,
edges: edges
};
var options = {};
var network = new vis.Network(container, data, options);
var clusterOptions = {
joinCondition:function(parentOptions,childOptions) {
return true;
},
processClusterProperties: function (properties, childNodes, childEdges) {
return properties;
},
clusterNodeProperties: {id:'bla', borderWidth:8},
}
var clusterOptionsByData = {
joinCondition:function(childOptions) {
return childOptions.cid == 1;
},
processClusterProperties: function (properties, childNodes, childEdges) {
return properties;
},
clusterNodeProperties: {id:'bla', borderWidth:8}
}
// network.clusterByNodeData(clusterOptionsByData)
network.clustering.clusterOutliers({clusterNodeProperties: {borderWidth:8}})
// network.clusterByConnection(2, clusterOptions);
// network.clusterByConnection(9, {
// joinCondition:function(parentOptions,childOptions) {return true;},
// processProperties:function (properties, childNodes, childEdges) {
// return properties;
// },
// clusterNodeProperties: {id:'bla2', label:"bla2", borderWidth:8}
// });
network.on("select", function(params) {
if (params.nodes.length == 1) {
if (network.clustering.isCluster(params.nodes[0]) == true) {
network.clustering.openCluster(params.nodes[0])
}
}
})
// network.openCluster('bla');
// network.openCluster('bla2');
</script>
</body>
</html>

+ 2
- 2
examples/timeline/18_range_overflow.html View File

@ -11,7 +11,7 @@
font-family: sans-serif;
}
.vis.timeline .item.range .content {
.vis.timeline .item.range {
overflow: visible;
}
</style>
@ -22,7 +22,7 @@
In case of ranges being spread over a wide range of time, it can be interesting to have the text contents of the ranges overflow the box. This can be achieved by changing the overflow property of the contents to visible with css:
</p>
<pre>
.vis.timeline .item.range .content {
.vis.timeline .item.range {
overflow: visible;
}
</pre>

+ 5
- 2
gulpfile.js View File

@ -34,7 +34,6 @@ var bannerPlugin = new webpack.BannerPlugin(createBanner(), {
raw: true
});
// TODO: the moment.js language files should be excluded by default (they are quite big)
var webpackConfig = {
entry: ENTRY,
output: {
@ -44,8 +43,12 @@ var webpackConfig = {
filename: VIS_JS,
sourcePrefix: ' '
},
// exclude requires of moment.js language files
module: {
loaders: [
{ test: /\.js$/, exclude: /node_modules/, loader: '6to5-loader'}
],
// exclude requires of moment.js language files
wrappedContextRegExp: /$^/
},
plugins: [ bannerPlugin ],

+ 1
- 3
lib/DataSet.js View File

@ -153,9 +153,7 @@ DataSet.prototype.subscribe = DataSet.prototype.on;
DataSet.prototype.off = function(event, callback) {
var subscribers = this._subscribers[event];
if (subscribers) {
this._subscribers[event] = subscribers.filter(function (listener) {
return (listener.callback != callback);
});
this._subscribers[event] = subscribers.filter(listener => listener.callback != callback);
}
};

+ 18
- 20
lib/network/Edge.js View File

@ -16,18 +16,16 @@ var Node = require('./Node');
* @param {Object} constants An object with default values for
* example for the color
*/
function Edge (properties, network, networkConstants) {
if (!network) {
throw "No network provided";
function Edge (properties, body, networkConstants) {
if (body === undefined) {
throw "No body provided";
}
var fields = ['edges','physics'];
var fields = ['edges'];
var constants = util.selectiveBridgeObject(fields,networkConstants);
this.options = constants.edges;
this.physics = constants.physics;
this.options['smoothCurves'] = networkConstants['smoothCurves'];
this.network = network;
this.options['smoothCurves'] = networkConstants['smoothCurves'];
this.body = body;
// initialize variables
this.id = undefined;
@ -46,13 +44,13 @@ function Edge (properties, network, networkConstants) {
this.to = null; // a node
this.via = null; // a temp node
this.fromBackup = null; // used to clean up after reconnect
this.toBackup = null;; // used to clean up after reconnect
this.fromBackup = null; // used to clean up after reconnect (used for manipulation)
this.toBackup = null; // used to clean up after reconnect (used for manipulation)
// we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
// by storing the original information we can revert to the original connection when the cluser is opened.
this.originalFromId = [];
this.originalToId = [];
this.fromArray = [];
this.toArray = [];
this.connected = false;
@ -76,10 +74,11 @@ Edge.prototype.setProperties = function(properties) {
if (!properties) {
return;
}
this.properties = properties;
var fields = ['style','fontSize','fontFace','fontColor','fontFill','fontStrokeWidth','fontStrokeColor','width',
'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor','labelAlignment', 'opacity',
'customScalingFunction','useGradients'
'customScalingFunction','useGradients','value'
];
util.selectiveDeepExtend(fields, this.options, properties);
@ -106,9 +105,7 @@ Edge.prototype.setProperties = function(properties) {
}
}
// A node is connected when it has a from and to node.
// A node is connected when it has a from and to node that both exist in the network.body.nodes.
this.connect();
this.widthFixed = this.widthFixed || (properties.width !== undefined);
@ -133,11 +130,11 @@ Edge.prototype.setProperties = function(properties) {
Edge.prototype.connect = function () {
this.disconnect();
this.from = this.network.nodes[this.fromId] || null;
this.to = this.network.nodes[this.toId] || null;
this.connected = (this.from && this.to);
this.from = this.body.nodes[this.fromId] || null;
this.to = this.body.nodes[this.toId] || null;
this.connected = (this.from !== null && this.to !== null);
if (this.connected) {
if (this.connected === true) {
this.from.attachEdge(this);
this.to.attachEdge(this);
}
@ -259,6 +256,7 @@ Edge.prototype._getColor = function(ctx) {
}
if (this.colorDirty === true) {
if (this.options.inheritColor == "to") {
colorObj = {
highlight: this.to.options.color.highlight.border,

+ 355
- 1822
lib/network/Network.js
File diff suppressed because it is too large
View File


+ 15
- 290
lib/network/Node.js View File

@ -33,8 +33,6 @@ function Node(properties, imagelist, grouplist, networkConstants) {
this.hover = false;
this.edges = []; // all edges connected to this node
this.dynamicEdges = [];
this.reroutedEdges = {};
// set defaults for the properties
this.id = undefined;
@ -56,10 +54,6 @@ function Node(properties, imagelist, grouplist, networkConstants) {
this.grouplist = grouplist;
// physics properties
this.fx = 0.0; // external force x
this.fy = 0.0; // external force y
this.vx = 0.0; // velocity x
this.vy = 0.0; // velocity y
this.x = null;
this.y = null;
this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate
@ -67,52 +61,19 @@ function Node(properties, imagelist, grouplist, networkConstants) {
// used for reverting to previous position on stabilization
this.previousState = {vx:0,vy:0,x:0,y:0};
this.damping = networkConstants.physics.damping; // written every time gravity is calculated
this.fixedData = {x:null,y:null};
this.setProperties(properties, constants);
// creating the variables for clustering
this.resetCluster();
this.clusterSession = 0;
this.clusterSizeWidthFactor = networkConstants.clustering.nodeScaling.width;
this.clusterSizeHeightFactor = networkConstants.clustering.nodeScaling.height;
this.clusterSizeRadiusFactor = networkConstants.clustering.nodeScaling.radius;
this.maxNodeSizeIncrements = networkConstants.clustering.maxNodeSizeIncrements;
this.growthIndicator = 0;
// variables to tell the node about the network.
this.networkScaleInv = 1;
this.networkScale = 1;
this.canvasTopLeft = {"x": -300, "y": -300};
this.canvasBottomRight = {"x": 300, "y": 300};
this.canvasTopLeft = {x: -300, y: -300};
this.canvasBottomRight = {x: 300, y: 300};
this.parentEdgeId = null;
}
/**
* Revert the position and velocity of the previous step.
*/
Node.prototype.revertPosition = function() {
this.x = this.previousState.x;
this.y = this.previousState.y;
this.vx = this.previousState.vx;
this.vy = this.previousState.vy;
}
/**
* (re)setting the clustering variables and objects
*/
Node.prototype.resetCluster = function() {
// clustering variables
this.formationScale = undefined; // this is used to determine when to open the cluster
this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
this.containedNodes = {};
this.containedEdges = {};
this.clusterSessions = [];
};
/**
* Attach a edge to the node
* @param {Edge} edge
@ -121,9 +82,6 @@ Node.prototype.attachEdge = function(edge) {
if (this.edges.indexOf(edge) == -1) {
this.edges.push(edge);
}
if (this.dynamicEdges.indexOf(edge) == -1) {
this.dynamicEdges.push(edge);
}
};
/**
@ -135,10 +93,6 @@ Node.prototype.detachEdge = function(edge) {
if (index != -1) {
this.edges.splice(index, 1);
}
index = this.dynamicEdges.indexOf(edge);
if (index != -1) {
this.dynamicEdges.splice(index, 1);
}
};
@ -151,10 +105,12 @@ Node.prototype.setProperties = function(properties, constants) {
if (!properties) {
return;
}
this.properties = properties;
var fields = ['borderWidth','borderWidthSelected','shape','image','brokenImage','radius','fontColor',
'fontSize','fontFace','fontFill','fontStrokeWidth','fontStrokeColor','group','mass','fontDrawThreshold',
'scaleFontWithValue','fontSizeMaxVisible','customScalingFunction','iconFontFace', 'icon', 'iconColor', 'iconSize'
var fields = ['borderWidth', 'borderWidthSelected', 'shape', 'image', 'brokenImage', 'radius', 'fontColor',
'fontSize', 'fontFace', 'fontFill', 'fontStrokeWidth', 'fontStrokeColor', 'group', 'mass', 'fontDrawThreshold',
'scaleFontWithValue', 'fontSizeMaxVisible', 'customScalingFunction', 'iconFontFace', 'icon', 'iconColor', 'iconSize',
'value'
];
util.selectiveDeepExtend(fields, this.options, properties);
@ -332,99 +288,6 @@ Node.prototype.distanceToBorder = function (ctx, angle) {
// TODO: implement calculation of distance to border for all shapes
};
/**
* Set forces acting on the node
* @param {number} fx Force in horizontal direction
* @param {number} fy Force in vertical direction
*/
Node.prototype._setForce = function(fx, fy) {
this.fx = fx;
this.fy = fy;
};
/**
* Add forces acting on the node
* @param {number} fx Force in horizontal direction
* @param {number} fy Force in vertical direction
* @private
*/
Node.prototype._addForce = function(fx, fy) {
this.fx += fx;
this.fy += fy;
};
/**
* Store the state before the next step
*/
Node.prototype.storeState = function() {
this.previousState.x = this.x;
this.previousState.y = this.y;
this.previousState.vx = this.vx;
this.previousState.vy = this.vy;
}
/**
* Perform one discrete step for the node
* @param {number} interval Time interval in seconds
*/
Node.prototype.discreteStep = function(interval) {
this.storeState();
if (!this.xFixed) {
var dx = this.damping * this.vx; // damping force
var ax = (this.fx - dx) / this.options.mass; // acceleration
this.vx += ax * interval; // velocity
this.x += this.vx * interval; // position
}
else {
this.fx = 0;
this.vx = 0;
}
if (!this.yFixed) {
var dy = this.damping * this.vy; // damping force
var ay = (this.fy - dy) / this.options.mass; // acceleration
this.vy += ay * interval; // velocity
this.y += this.vy * interval; // position
}
else {
this.fy = 0;
this.vy = 0;
}
};
/**
* Perform one discrete step for the node
* @param {number} interval Time interval in seconds
* @param {number} maxVelocity The speed limit imposed on the velocity
*/
Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
this.storeState();
if (!this.xFixed) {
var dx = this.damping * this.vx; // damping force
var ax = (this.fx - dx) / this.options.mass; // acceleration
this.vx += ax * interval; // velocity
this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
this.x += this.vx * interval; // position
}
else {
this.fx = 0;
this.vx = 0;
}
if (!this.yFixed) {
var dy = this.damping * this.vy; // damping force
var ay = (this.fy - dy) / this.options.mass; // acceleration
this.vy += ay * interval; // velocity
this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
this.y += this.vy * interval; // position
}
else {
this.fy = 0;
this.vy = 0;
}
};
/**
* Check if this node has a fixed x and y position
@ -434,17 +297,6 @@ Node.prototype.isFixed = function() {
return (this.xFixed && this.yFixed);
};
/**
* Check if this node is moving
* @param {number} vmin the minimum velocity considered as "moving"
* @return {boolean} true if moving, false if it has no velocity
*/
Node.prototype.isMoving = function(vmin) {
var velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2));
// this.velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2))
return (velocity > vmin);
};
/**
* check if this node is selecte
* @return {boolean} selected True if node is selected, else false
@ -547,29 +399,11 @@ Node.prototype._resizeImage = function (ctx) {
}
this.width = width;
this.height = height;
this.growthIndicator = 0;
if (this.width > 0 && this.height > 0) {
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - width;
}
}
};
Node.prototype._drawImageAtPosition = function (ctx) {
if (this.imageObj.width != 0 ) {
// draw the shade
if (this.clusterSize > 1) {
var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
lineWidth *= this.networkScaleInv;
lineWidth = Math.min(0.2 * this.width,lineWidth);
ctx.globalAlpha = 0.5;
ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
}
// draw the image
ctx.globalAlpha = 1.0;
ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
@ -619,12 +453,6 @@ Node.prototype._resizeCircularImage = function (ctx) {
var diameter = this.options.radius * 2;
this.width = diameter;
this.height = diameter;
// scaling used for clustering
//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.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
this.growthIndicator = this.options.radius- 0.5*diameter;
this._swapToImageResizeWhenImageLoaded = true;
}
}
@ -678,12 +506,6 @@ Node.prototype._resizeBox = function (ctx) {
var textSize = this.getTextSize(ctx);
this.width = textSize.width + 2 * margin;
this.height = textSize.height + 2 * margin;
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.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
}
};
@ -693,22 +515,11 @@ Node.prototype._drawBox = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
var clusterLineWidth = 2.5;
var borderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.options.radius);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
@ -734,12 +545,6 @@ Node.prototype._resizeDatabase = function (ctx) {
var size = textSize.width + 2 * margin;
this.width = size;
this.height = size;
// 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.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - size;
}
};
@ -748,22 +553,11 @@ Node.prototype._drawDatabase = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
var clusterLineWidth = 2.5;
var borderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
@ -790,32 +584,16 @@ Node.prototype._resizeCircle = function (ctx) {
this.width = diameter;
this.height = diameter;
// scaling used for clustering
// 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.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
this.growthIndicator = this.options.radius- 0.5*diameter;
}
};
Node.prototype._drawRawCircle = function (ctx, x, y, radius) {
var clusterLineWidth = 2.5;
var borderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
ctx.circle(x, y, radius+2*ctx.lineWidth);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
@ -850,12 +628,6 @@ Node.prototype._resizeEllipse = function (ctx) {
this.width = this.height;
}
var defaultSize = this.width;
// 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.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - defaultSize;
}
};
@ -864,22 +636,12 @@ Node.prototype._drawEllipse = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
var clusterLineWidth = 2.5;
var borderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
@ -923,12 +685,6 @@ Node.prototype._resizeShape = function (ctx) {
var size = 2 * this.options.radius;
this.width = size;
this.height = size;
// 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.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - size;
}
};
@ -938,7 +694,6 @@ Node.prototype._drawShape = function (ctx, shape) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
var clusterLineWidth = 2.5;
var borderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var radiusMultiplier = 2;
@ -953,16 +708,7 @@ Node.prototype._drawShape = function (ctx, shape) {
}
ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
ctx[shape](this.x, this.y, this.options.radius+ radiusMultiplier * ctx.lineWidth);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
@ -990,12 +736,6 @@ Node.prototype._resizeText = function (ctx) {
var textSize = this.getTextSize(ctx);
this.width = textSize.width + 2 * margin;
this.height = textSize.height + 2 * margin;
// 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.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - (textSize.width + 2 * margin);
}
};
@ -1022,12 +762,6 @@ Node.prototype._resizeIcon = function (ctx) {
};
this.width = iconSize.width + 2 * margin;
this.height = iconSize.height + 2 * margin;
// 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.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - (iconSize.width + 2 * margin);
}
};
@ -1160,13 +894,14 @@ Node.prototype.getTextSize = function(ctx) {
width = Math.max(width, ctx.measureText(lines[i]).width);
}
return {"width": width, "height": height, lineCount: lines.length};
return {width: width, height: height, lineCount: lines.length};
}
else {
return {"width": 0, "height": 0, lineCount: 0};
return {width: 0, height: 0, lineCount: 0};
}
};
/**
* this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
* there is a safety margin of 0.3 * width;
@ -1185,16 +920,6 @@ Node.prototype.inArea = function() {
}
};
/**
* checks if the core of the node is in the display area, this is used for opening clusters around zoom
* @returns {boolean}
*/
Node.prototype.inView = function() {
return (this.x >= this.canvasTopLeft.x &&
this.x < this.canvasBottomRight.x &&
this.y >= this.canvasTopLeft.y &&
this.y < this.canvasBottomRight.y);
};
/**
* This allows the zoom level of the network to influence the rendering

+ 0
- 1129
lib/network/mixins/ClusterMixin.js
File diff suppressed because it is too large
View File


+ 28
- 31
lib/network/mixins/HierarchicalLayoutMixin.js View File

@ -1,7 +1,7 @@
exports._resetLevels = function() {
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
var node = this.nodes[nodeId];
for (var nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
var node = this.body.nodes[nodeId];
if (node.preassignedLevel == false) {
node.level = -1;
node.hierarchyEnumerated = false;
@ -24,9 +24,9 @@ exports._setupHierarchicalLayout = function() {
var definedLevel = false;
var undefinedLevel = false;
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (node.level != -1) {
definedLevel = true;
}
@ -64,11 +64,8 @@ exports._setupHierarchicalLayout = function() {
// check the distribution of the nodes per level.
var distribution = this._getDistribution();
// place the nodes on the canvas. This also stablilizes the system.
// place the nodes on the canvas. This also stablilizes the system. Redraw in started automatically after stabilize.
this._placeNodesByHierarchy(distribution);
// start the simulation.
this.start();
}
}
};
@ -129,9 +126,9 @@ exports._getDistribution = function() {
// 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];
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
node.xFixed = true;
node.yFixed = true;
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
@ -181,9 +178,9 @@ exports._determineLevels = function(hubsize) {
var nodeId, node;
// determine hubs
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (node.edges.length == hubsize) {
node.level = 0;
}
@ -191,9 +188,9 @@ exports._determineLevels = function(hubsize) {
}
// branch from hubs
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (node.level == 0) {
this._setLevel(1,node.edges,node.id);
}
@ -214,22 +211,22 @@ exports._determineLevelsDirected = function() {
var minLevel = 10000;
// set first node to source
firstNode = this.nodes[this.nodeIndices[0]];
firstNode = this.body.nodes[this.nodeIndices[0]];
firstNode.level = minLevel;
this._setLevelDirected(minLevel,firstNode.edges,firstNode.id);
// get the minimum level
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
minLevel = node.level < minLevel ? node.level : minLevel;
}
}
// subtract the minimum from the set so we have a range starting from 0
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
node.level -= minLevel;
}
}
@ -357,7 +354,7 @@ exports._setLevel = function(level, edges, parentId) {
* @private
*/
exports._setLevelDirected = function(level, edges, parentId) {
this.nodes[parentId].hierarchyEnumerated = true;
this.body.nodes[parentId].hierarchyEnumerated = true;
var childNode, direction;
for (var i = 0; i < edges.length; i++) {
direction = 1;
@ -390,10 +387,10 @@ exports._setLevelDirected = function(level, edges, parentId) {
* @private
*/
exports._restoreNodes = function() {
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
this.nodes[nodeId].xFixed = false;
this.nodes[nodeId].yFixed = false;
for (var nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
this.body.nodes[nodeId].xFixed = false;
this.body.nodes[nodeId].yFixed = false;
}
}
};

+ 28
- 31
lib/network/mixins/ManipulationMixin.js View File

@ -15,10 +15,10 @@ exports._clearManipulatorBar = function() {
this._cleanManipulatorHammers();
this._manipulationReleaseOverload = function () {};
delete this.sectors['support']['nodes']['targetNode'];
delete this.sectors['support']['nodes']['targetViaNode'];
delete this.body.sectors['support']['nodes']['targetNode'];
delete this.body.sectors['support']['nodes']['targetViaNode'];
this.controlNodesActive = false;
this.freezeSimulation(false);
this.freezeSimulationEnabled = false;
};
@ -276,7 +276,7 @@ exports._createAddEdgeToolbar = function() {
// clear the toolbar
this._clearManipulatorBar();
this._unselectAll(true);
this.freezeSimulation(true);
this.freezeSimulationEnabled = true;
if (this.boundFunction) {
this.off('select', this.boundFunction);
@ -284,7 +284,6 @@ exports._createAddEdgeToolbar = function() {
var locale = this.constants.locales[this.constants.locale];
this._unselectAll();
this.forceAppendSelection = false;
this.blockConnectingEdgeSelection = true;
@ -466,22 +465,22 @@ exports._handleConnect = function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null) {
if (node.clusterSize > 1) {
if (this.isCluster(node.id) == true) {
alert(this.constants.locales[this.constants.locale]['createEdgeError'])
}
else {
this._selectObject(node,false);
var supportNodes = this.sectors['support']['nodes'];
var supportNodes = this.body.sectors['support']['nodes'];
// create a node the temporary line can look at
supportNodes['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
supportNodes['targetNode'] = this.body.functions.createNode({id:'targetNode'});
var targetNode = supportNodes['targetNode'];
targetNode.x = node.x;
targetNode.y = node.y;
// create a temporary edge
this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:targetNode.id}, this, this.constants);
var connectionEdge = this.edges['connectionEdge'];
this.body.edges['connectionEdge'] = this.body.functions.createEdge({id:"connectionEdge",from:node.id,to:targetNode.id});
var connectionEdge = this.body.edges['connectionEdge'];
connectionEdge.from = node;
connectionEdge.connected = true;
connectionEdge.options.smoothCurves = {enabled: true,
@ -495,15 +494,12 @@ exports._handleConnect = function(pointer) {
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
var me = this;
this._handleOnDrag = function(event) {
var pointer = this._getPointer(event.gesture.center);
var connectionEdge = me.edges['connectionEdge'];
var pointer = me._getPointer(event.gesture.center);
var connectionEdge = me.body.edges['connectionEdge'];
connectionEdge.to.x = me._XconvertDOMtoCanvas(pointer.x);
connectionEdge.to.y = me._YconvertDOMtoCanvas(pointer.y);
me._redraw();
};
this.moving = true;
this.start();
}
}
}
@ -517,16 +513,16 @@ exports._finishConnect = function(event) {
delete this.cachedFunctions["_handleOnDrag"];
// remember the edge id
var connectFromId = this.edges['connectionEdge'].fromId;
var connectFromId = this.body.edges['connectionEdge'].fromId;
// remove the temporary nodes and edge
delete this.edges['connectionEdge'];
delete this.sectors['support']['nodes']['targetNode'];
delete this.sectors['support']['nodes']['targetViaNode'];
delete this.body.edges['connectionEdge'];
delete this.body.sectors['support']['nodes']['targetNode'];
delete this.body.sectors['support']['nodes']['targetViaNode'];
var node = this._getNodeAt(pointer);
if (node != null) {
if (node.clusterSize > 1) {
if (this.isCluster(node.id) === true) {
alert(this.constants.locales[this.constants.locale]["createEdgeError"])
}
else {
@ -550,7 +546,7 @@ exports._addNode = function() {
if (this.triggerFunctions.add.length == 2) {
var me = this;
this.triggerFunctions.add(defaultData, function(finalizedData) {
me.nodesData.add(finalizedData);
me.body.data.nodes.add(finalizedData);
me._createManipulatorBar();
me.moving = true;
me.start();
@ -564,7 +560,7 @@ exports._addNode = function() {
}
}
else {
this.nodesData.add(defaultData);
this.body.data.nodes.add(defaultData);
this._createManipulatorBar();
this.moving = true;
this.start();
@ -585,7 +581,7 @@ exports._createEdge = function(sourceNodeId,targetNodeId) {
if (this.triggerFunctions.connect.length == 2) {
var me = this;
this.triggerFunctions.connect(defaultData, function(finalizedData) {
me.edgesData.add(finalizedData);
me.body.data.edges.add(finalizedData);
me.moving = true;
me.start();
});
@ -597,7 +593,7 @@ exports._createEdge = function(sourceNodeId,targetNodeId) {
}
}
else {
this.edgesData.add(defaultData);
this.body.data.edges.add(defaultData);
this.moving = true;
this.start();
}
@ -612,11 +608,12 @@ exports._createEdge = function(sourceNodeId,targetNodeId) {
exports._editEdge = function(sourceNodeId,targetNodeId) {
if (this.editMode == true) {
var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
console.log(defaultData);
if (this.triggerFunctions.editEdge) {
if (this.triggerFunctions.editEdge.length == 2) {
var me = this;
this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
me.edgesData.update(finalizedData);
me.body.data.edges.update(finalizedData);
me.moving = true;
me.start();
});
@ -628,7 +625,7 @@ exports._editEdge = function(sourceNodeId,targetNodeId) {
}
}
else {
this.edgesData.update(defaultData);
this.body.data.edges.update(defaultData);
this.moving = true;
this.start();
}
@ -658,7 +655,7 @@ exports._editNode = function() {
if (this.triggerFunctions.edit.length == 2) {
var me = this;
this.triggerFunctions.edit(data, function (finalizedData) {
me.nodesData.update(finalizedData);
me.body.data.nodes.update(finalizedData);
me._createManipulatorBar();
me.moving = true;
me.start();
@ -691,8 +688,8 @@ exports._deleteSelected = function() {
var data = {nodes: selectedNodes, edges: selectedEdges};
if (this.triggerFunctions.del.length == 2) {
this.triggerFunctions.del(data, function (finalizedData) {
me.edgesData.remove(finalizedData.edges);
me.nodesData.remove(finalizedData.nodes);
me.body.data.edges.remove(finalizedData.edges);
me.body.data.nodes.remove(finalizedData.nodes);
me._unselectAll();
me.moving = true;
me.start();
@ -703,8 +700,8 @@ exports._deleteSelected = function() {
}
}
else {
this.edgesData.remove(selectedEdges);
this.nodesData.remove(selectedNodes);
this.body.data.edges.remove(selectedEdges);
this.body.data.nodes.remove(selectedNodes);
this._unselectAll();
this.moving = true;
this.start();

+ 0
- 40
lib/network/mixins/MixinLoader.js View File

@ -1,6 +1,3 @@
var PhysicsMixin = require('./physics/PhysicsMixin');
var ClusterMixin = require('./ClusterMixin');
var SectorsMixin = require('./SectorsMixin');
var SelectionMixin = require('./SelectionMixin');
var ManipulationMixin = require('./ManipulationMixin');
var NavigationMixin = require('./NavigationMixin');
@ -53,43 +50,6 @@ exports._loadPhysicsSystem = function () {
};
/**
* Mixin the cluster system and initialize the parameters required.
*
* @private
*/
exports._loadClusterSystem = function () {
this.clusterSession = 0;
this.hubThreshold = 5;
this._loadMixin(ClusterMixin);
};
/**
* Mixin the sector system and initialize the parameters required
*
* @private
*/
exports._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["support"] = {"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(SectorsMixin);
};
/**

+ 52
- 52
lib/network/mixins/SectorsMixin.js View File

@ -16,9 +16,9 @@ var Node = require('../Node');
* @private
*/
exports._putDataInSector = function() {
this.sectors["active"][this._sector()].nodes = this.nodes;
this.sectors["active"][this._sector()].edges = this.edges;
this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
this.body.sectors["active"][this._sector()].nodes = this.body.nodes;
this.body.sectors["active"][this._sector()].edges = this.body.edges;
this.body.sectors["active"][this._sector()].nodeIndices = this.body.nodeIndices;
};
@ -49,9 +49,9 @@ exports._switchToSector = function(sectorId, sectorType) {
* @private
*/
exports._switchToActiveSector = function(sectorId) {
this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
this.nodes = this.sectors["active"][sectorId]["nodes"];
this.edges = this.sectors["active"][sectorId]["edges"];
this.body.nodeIndices = this.body.sectors["active"][sectorId]["nodeIndices"];
this.body.nodes = this.body.sectors["active"][sectorId]["nodes"];
this.body.edges = this.body.sectors["active"][sectorId]["edges"];
};
@ -62,9 +62,9 @@ exports._switchToActiveSector = function(sectorId) {
* @private
*/
exports._switchToSupportSector = function() {
this.nodeIndices = this.sectors["support"]["nodeIndices"];
this.nodes = this.sectors["support"]["nodes"];
this.edges = this.sectors["support"]["edges"];
this.body.nodeIndices = this.body.sectors["support"]["nodeIndices"];
this.body.nodes = this.body.sectors["support"]["nodes"];
this.body.edges = this.body.sectors["support"]["edges"];
};
@ -76,9 +76,9 @@ exports._switchToSupportSector = function() {
* @private
*/
exports._switchToFrozenSector = function(sectorId) {
this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
this.nodes = this.sectors["frozen"][sectorId]["nodes"];
this.edges = this.sectors["frozen"][sectorId]["edges"];
this.body.nodeIndices = this.body.sectors["frozen"][sectorId]["nodeIndices"];
this.body.nodes = this.body.sectors["frozen"][sectorId]["nodes"];
this.body.edges = this.body.sectors["frozen"][sectorId]["edges"];
};
@ -153,21 +153,21 @@ exports._forgetLastSector = function() {
*/
exports._createNewSector = function(newId) {
// create the new sector
this.sectors["active"][newId] = {"nodes":{},
this.body.sectors["active"][newId] = {"nodes":{},
"edges":{},
"nodeIndices":[],
"formationScale": this.scale,
"drawingNode": undefined};
// create the new sector render node. This gives visual feedback that you are in a new sector.
this.sectors["active"][newId]['drawingNode'] = new Node(
this.body.sectors["active"][newId]['drawingNode'] = new Node(
{id:newId,
color: {
background: "#eaefef",
border: "495c5e"
}
},{},{},this.constants);
this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
this.body.sectors["active"][newId]['drawingNode'].clusterSize = 2;
};
@ -179,7 +179,7 @@ exports._createNewSector = function(newId) {
* @private
*/
exports._deleteActiveSector = function(sectorId) {
delete this.sectors["active"][sectorId];
delete this.body.sectors["active"][sectorId];
};
@ -191,7 +191,7 @@ exports._deleteActiveSector = function(sectorId) {
* @private
*/
exports._deleteFrozenSector = function(sectorId) {
delete this.sectors["frozen"][sectorId];
delete this.body.sectors["frozen"][sectorId];
};
@ -204,7 +204,7 @@ exports._deleteFrozenSector = function(sectorId) {
*/
exports._freezeSector = function(sectorId) {
// we move the set references from the active to the frozen stack.
this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
this.body.sectors["frozen"][sectorId] = this.body.sectors["active"][sectorId];
// we have moved the sector data into the frozen set, we now remove it from the active set
this._deleteActiveSector(sectorId);
@ -220,7 +220,7 @@ exports._freezeSector = function(sectorId) {
*/
exports._activateSector = function(sectorId) {
// we move the set references from the frozen to the active stack.
this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
this.body.sectors["active"][sectorId] = this.body.sectors["frozen"][sectorId];
// we have moved the sector data into the active set, we now remove it from the frozen stack
this._deleteFrozenSector(sectorId);
@ -238,22 +238,22 @@ exports._activateSector = function(sectorId) {
*/
exports._mergeThisWithFrozen = function(sectorId) {
// copy all nodes
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
for (var nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
this.body.sectors["frozen"][sectorId]["nodes"][nodeId] = this.body.nodes[nodeId];
}
}
// copy all edges (if not fully clustered, else there are no edges)
for (var edgeId in this.edges) {
if (this.edges.hasOwnProperty(edgeId)) {
this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
for (var edgeId in this.body.edges) {
if (this.body.edges.hasOwnProperty(edgeId)) {
this.body.sectors["frozen"][sectorId]["edges"][edgeId] = this.body.edges[edgeId];
}
}
// merge the nodeIndices
for (var i = 0; i < this.nodeIndices.length; i++) {
this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
for (var i = 0; i < this.body.nodeIndices.length; i++) {
this.body.sectors["frozen"][sectorId]["nodeIndices"].push(this.body.nodeIndices[i]);
}
};
@ -280,7 +280,7 @@ exports._addSector = function(node) {
var sector = this._sector();
// // this should allow me to select nodes from a frozen set.
// if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
// if (this.body.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
// console.log("the node is part of the active sector");
// }
// else {
@ -288,7 +288,7 @@ exports._addSector = function(node) {
// }
// when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
delete this.nodes[node.id];
delete this.body.nodes[node.id];
var unqiueIdentifier = util.randomUUID();
@ -305,7 +305,7 @@ exports._addSector = function(node) {
this._switchToSector(this._sector());
// finally we add the node we removed from our previous active sector to the new active sector
this.nodes[node.id] = node;
this.body.nodes[node.id] = node;
};
@ -321,9 +321,9 @@ exports._collapseSector = function() {
// we cannot collapse the default sector
if (sector != "default") {
if ((this.nodeIndices.length == 1) ||
(this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
(this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
if ((this.body.nodeIndices.length == 1) ||
(this.body.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
(this.body.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
var previousSector = this._previousSector();
// we collapse the sector back to a single cluster
@ -368,8 +368,8 @@ exports._collapseSector = function() {
exports._doInAllActiveSectors = function(runFunction,argument) {
var returnValues = [];
if (argument === undefined) {
for (var sector in this.sectors["active"]) {
if (this.sectors["active"].hasOwnProperty(sector)) {
for (var sector in this.body.sectors["active"]) {
if (this.body.sectors["active"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToActiveSector(sector);
returnValues.push( this[runFunction]() );
@ -377,8 +377,8 @@ exports._doInAllActiveSectors = function(runFunction,argument) {
}
}
else {
for (var sector in this.sectors["active"]) {
if (this.sectors["active"].hasOwnProperty(sector)) {
for (var sector in this.body.sectors["active"]) {
if (this.body.sectors["active"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToActiveSector(sector);
var args = Array.prototype.splice.call(arguments, 1);
@ -439,8 +439,8 @@ exports._doInSupportSector = function(runFunction,argument) {
*/
exports._doInAllFrozenSectors = function(runFunction,argument) {
if (argument === undefined) {
for (var sector in this.sectors["frozen"]) {
if (this.sectors["frozen"].hasOwnProperty(sector)) {
for (var sector in this.body.sectors["frozen"]) {
if (this.body.sectors["frozen"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToFrozenSector(sector);
this[runFunction]();
@ -448,8 +448,8 @@ exports._doInAllFrozenSectors = function(runFunction,argument) {
}
}
else {
for (var sector in this.sectors["frozen"]) {
if (this.sectors["frozen"].hasOwnProperty(sector)) {
for (var sector in this.body.sectors["frozen"]) {
if (this.body.sectors["frozen"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToFrozenSector(sector);
var args = Array.prototype.splice.call(arguments, 1);
@ -495,15 +495,15 @@ exports._doInAllSectors = function(runFunction,argument) {
/**
* This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
* active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
* This clears the nodeIndices list. We cannot use this.body.nodeIndices = [] because we would break the link with the
* active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.body.nodeIndices to it.
*
* @private
*/
exports._clearNodeIndexList = function() {
var sector = this._sector();
this.sectors["active"][sector]["nodeIndices"] = [];
this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
this.body.sectors["active"][sector]["nodeIndices"] = [];
this.body.nodeIndices = this.body.sectors["active"][sector]["nodeIndices"];
};
@ -516,16 +516,16 @@ exports._clearNodeIndexList = function() {
*/
exports._drawSectorNodes = function(ctx,sectorType) {
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
for (var sector in this.sectors[sectorType]) {
if (this.sectors[sectorType].hasOwnProperty(sector)) {
if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
for (var sector in this.body.sectors[sectorType]) {
if (this.body.sectors[sectorType].hasOwnProperty(sector)) {
if (this.body.sectors[sectorType][sector]["drawingNode"] !== undefined) {
this._switchToSector(sector,sectorType);
minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
for (var nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
node.resize(ctx);
if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
@ -533,7 +533,7 @@ exports._drawSectorNodes = function(ctx,sectorType) {
if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
}
}
node = this.sectors[sectorType][sector]["drawingNode"];
node = this.body.sectors[sectorType][sector]["drawingNode"];
node.x = 0.5 * (maxX + minX);
node.y = 0.5 * (maxY + minY);
node.width = 2 * (node.x - minX);

+ 16
- 17
lib/network/mixins/SelectionMixin.js View File

@ -1,14 +1,13 @@
var Node = require('../Node');
/**
* This function can be called from the _doInAllSectors function
*
* @param object
* @param overlappingNodes
* @private
*/
exports._getNodesOverlappingWith = function(object, overlappingNodes) {
var nodes = this.nodes;
var nodes = this.body.nodes;
for (var nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
if (nodes[nodeId].isOverlappingWith(object)) {
@ -26,7 +25,7 @@ exports._getNodesOverlappingWith = function(object, overlappingNodes) {
*/
exports._getAllNodesOverlappingWith = function (object) {
var overlappingNodes = [];
this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
this._getNodesOverlappingWith(object,overlappingNodes);
return overlappingNodes;
};
@ -66,7 +65,7 @@ exports._getNodeAt = function (pointer) {
// if there are overlapping nodes, select the last one, this is the
// one which is drawn on top of the others
if (overlappingNodes.length > 0) {
return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]];
}
else {
return null;
@ -81,7 +80,7 @@ exports._getNodeAt = function (pointer) {
* @private
*/
exports._getEdgesOverlappingWith = function (object, overlappingEdges) {
var edges = this.edges;
var edges = this.body.edges;
for (var edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
if (edges[edgeId].isOverlappingWith(object)) {
@ -100,7 +99,7 @@ exports._getEdgesOverlappingWith = function (object, overlappingEdges) {
*/
exports._getAllEdgesOverlappingWith = function (object) {
var overlappingEdges = [];
this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
this._getEdgesOverlappingWith(object,overlappingEdges);
return overlappingEdges;
};
@ -117,7 +116,7 @@ exports._getEdgeAt = function(pointer) {
var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
if (overlappingEdges.length > 0) {
return this.edges[overlappingEdges[overlappingEdges.length - 1]];
return this.body.edges[overlappingEdges[overlappingEdges.length - 1]];
}
else {
return null;
@ -355,8 +354,8 @@ exports._clusterInSelection = function() {
* @private
*/
exports._selectConnectedEdges = function(node) {
for (var i = 0; i < node.dynamicEdges.length; i++) {
var edge = node.dynamicEdges[i];
for (var i = 0; i < node.edges.length; i++) {
var edge = node.edges[i];
edge.select();
this._addToSelection(edge);
}
@ -369,8 +368,8 @@ exports._selectConnectedEdges = function(node) {
* @private
*/
exports._hoverConnectedEdges = function(node) {
for (var i = 0; i < node.dynamicEdges.length; i++) {
var edge = node.dynamicEdges[i];
for (var i = 0; i < node.edges.length; i++) {
var edge = node.edges[i];
edge.hover = true;
this._addToHover(edge);
}
@ -384,8 +383,8 @@ exports._hoverConnectedEdges = function(node) {
* @private
*/
exports._unselectConnectedEdges = function(node) {
for (var i = 0; i < node.dynamicEdges.length; i++) {
var edge = node.dynamicEdges[i];
for (var i = 0; i < node.edges.length; i++) {
var edge = node.edges[i];
edge.unselect();
this._removeFromSelection(edge);
}
@ -650,7 +649,7 @@ exports.selectNodes = function(selection, highlightEdges) {
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
var node = this.nodes[id];
var node = this.body.nodes[id];
if (!node) {
throw new RangeError('Node with id "' + id + '" not found');
}
@ -677,7 +676,7 @@ exports.selectEdges = function(selection) {
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
var edge = this.edges[id];
var edge = this.body.edges[id];
if (!edge) {
throw new RangeError('Edge with id "' + id + '" not found');
}
@ -693,14 +692,14 @@ exports.selectEdges = function(selection) {
exports._updateSelection = function () {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (!this.nodes.hasOwnProperty(nodeId)) {
if (!this.body.nodes.hasOwnProperty(nodeId)) {
delete this.selectionObj.nodes[nodeId];
}
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
if (!this.edges.hasOwnProperty(edgeId)) {
if (!this.body.edges.hasOwnProperty(edgeId)) {
delete this.selectionObj.edges[edgeId];
}
}

+ 0
- 399
lib/network/mixins/physics/BarnesHutMixin.js View File

@ -1,399 +0,0 @@
/**
* This function calculates the forces the nodes apply on eachother based on a gravitational model.
* The Barnes Hut method is used to speed up this N-body simulation.
*
* @private
*/
exports._calculateNodeForces = function() {
if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
var node;
var nodes = this.calculationNodes;
var nodeIndices = this.calculationNodeIndices;
var nodeCount = nodeIndices.length;
this._formBarnesHutTree(nodes,nodeIndices);
var barnesHutTree = this.barnesHutTree;
// place the nodes one by one recursively
for (var i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
if (node.options.mass > 0) {
// starting with root is irrelevant, it never passes the BarnesHut condition
this._getForceContribution(barnesHutTree.root.children.NW,node);
this._getForceContribution(barnesHutTree.root.children.NE,node);
this._getForceContribution(barnesHutTree.root.children.SW,node);
this._getForceContribution(barnesHutTree.root.children.SE,node);
}
}
}
};
/**
* This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
* If a region contains a single node, we check if it is not itself, then we apply the force.
*
* @param parentBranch
* @param node
* @private
*/
exports._getForceContribution = function(parentBranch,node) {
// we get no force contribution from an empty region
if (parentBranch.childrenCount > 0) {
var dx,dy,distance;
// get the distance from the center of mass to the node.
dx = parentBranch.centerOfMass.x - node.x;
dy = parentBranch.centerOfMass.y - node.y;
distance = Math.sqrt(dx * dx + dy * dy);
// BarnesHut condition
// original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed
// calcSize = 1/s --> d * 1/s > 1/theta = passed
if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.thetaInverted) {
// duplicate code to reduce function calls to speed up program
if (distance == 0) {
distance = 0.1*Math.random();
dx = distance;
}
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
var fx = dx * gravityForce;
var fy = dy * gravityForce;
node.fx += fx;
node.fy += fy;
}
else {
// Did not pass the condition, go into children if available
if (parentBranch.childrenCount == 4) {
this._getForceContribution(parentBranch.children.NW,node);
this._getForceContribution(parentBranch.children.NE,node);
this._getForceContribution(parentBranch.children.SW,node);
this._getForceContribution(parentBranch.children.SE,node);
}
else { // parentBranch must have only one node, if it was empty we wouldnt be here
if (parentBranch.children.data.id != node.id) { // if it is not self
// duplicate code to reduce function calls to speed up program
if (distance == 0) {
distance = 0.5*Math.random();
dx = distance;
}
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
var fx = dx * gravityForce;
var fy = dy * gravityForce;
node.fx += fx;
node.fy += fy;
}
}
}
}
};
/**
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
*
* @param nodes
* @param nodeIndices
* @private
*/
exports._formBarnesHutTree = function(nodes,nodeIndices) {
var node;
var nodeCount = nodeIndices.length;
var minX = 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++) {
var x = nodes[nodeIndices[i]].x;
var y = nodes[nodeIndices[i]].y;
if (nodes[nodeIndices[i]].options.mass > 0) {
if (x < minX) { minX = x; }
if (x > maxX) { maxX = x; }
if (y < minY) { minY = y; }
if (y > maxY) { maxY = y; }
}
}
// make the range a square
var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
var minimumTreeSize = 1e-5;
var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
var halfRootSize = 0.5 * rootSize;
var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
// construct the barnesHutTree
var barnesHutTree = {
root:{
centerOfMass: {x:0, y:0},
mass:0,
range: {
minX: centerX-halfRootSize,maxX:centerX+halfRootSize,
minY: centerY-halfRootSize,maxY:centerY+halfRootSize
},
size: rootSize,
calcSize: 1 / rootSize,
children: { data:null},
maxWidth: 0,
level: 0,
childrenCount: 4
}
};
this._splitBranch(barnesHutTree.root);
// place the nodes one by one recursively
for (i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
if (node.options.mass > 0) {
this._placeInTree(barnesHutTree.root,node);
}
}
// make global
this.barnesHutTree = barnesHutTree
};
/**
* this updates the mass of a branch. this is increased by adding a node.
*
* @param parentBranch
* @param node
* @private
*/
exports._updateBranchMass = function(parentBranch, node) {
var totalMass = parentBranch.mass + node.options.mass;
var totalMassInv = 1/totalMass;
parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
parentBranch.centerOfMass.x *= totalMassInv;
parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
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;
};
/**
* determine in which branch the node will be placed.
*
* @param parentBranch
* @param node
* @param skipMassUpdate
* @private
*/
exports._placeInTree = function(parentBranch,node,skipMassUpdate) {
if (skipMassUpdate != true || skipMassUpdate === undefined) {
// update the mass of the branch.
this._updateBranchMass(parentBranch,node);
}
if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
if (parentBranch.children.NW.range.maxY > node.y) { // in NW
this._placeInRegion(parentBranch,node,"NW");
}
else { // in SW
this._placeInRegion(parentBranch,node,"SW");
}
}
else { // in NE or SE
if (parentBranch.children.NW.range.maxY > node.y) { // in NE
this._placeInRegion(parentBranch,node,"NE");
}
else { // in SE
this._placeInRegion(parentBranch,node,"SE");
}
}
};
/**
* actually place the node in a region (or branch)
*
* @param parentBranch
* @param node
* @param region
* @private
*/
exports._placeInRegion = function(parentBranch,node,region) {
switch (parentBranch.children[region].childrenCount) {
case 0: // place node here
parentBranch.children[region].children.data = node;
parentBranch.children[region].childrenCount = 1;
this._updateBranchMass(parentBranch.children[region],node);
break;
case 1: // convert into children
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
// we move one node a pixel and we do not put it in the tree.
if (parentBranch.children[region].children.data.x == node.x &&
parentBranch.children[region].children.data.y == node.y) {
node.x += Math.random();
node.y += Math.random();
}
else {
this._splitBranch(parentBranch.children[region]);
this._placeInTree(parentBranch.children[region],node);
}
break;
case 4: // place in branch
this._placeInTree(parentBranch.children[region],node);
break;
}
};
/**
* this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
* after the split is complete.
*
* @param parentBranch
* @private
*/
exports._splitBranch = function(parentBranch) {
// if the branch is shaded with a node, replace the node in the new subset.
var containedNode = null;
if (parentBranch.childrenCount == 1) {
containedNode = parentBranch.children.data;
parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
}
parentBranch.childrenCount = 4;
parentBranch.children.data = null;
this._insertRegion(parentBranch,"NW");
this._insertRegion(parentBranch,"NE");
this._insertRegion(parentBranch,"SW");
this._insertRegion(parentBranch,"SE");
if (containedNode != null) {
this._placeInTree(parentBranch,containedNode);
}
};
/**
* This function subdivides the region into four new segments.
* Specifically, this inserts a single new segment.
* It fills the children section of the parentBranch
*
* @param parentBranch
* @param region
* @param parentRange
* @private
*/
exports._insertRegion = function(parentBranch, region) {
var minX,maxX,minY,maxY;
var childSize = 0.5 * parentBranch.size;
switch (region) {
case "NW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + childSize;
break;
case "NE":
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + childSize;
break;
case "SW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
case "SE":
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
}
parentBranch.children[region] = {
centerOfMass:{x:0,y:0},
mass:0,
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
size: 0.5 * parentBranch.size,
calcSize: 2 * parentBranch.calcSize,
children: {data:null},
maxWidth: 0,
level: parentBranch.level+1,
childrenCount: 0
};
};
/**
* This function is for debugging purposed, it draws the tree.
*
* @param ctx
* @param color
* @private
*/
exports._drawTree = function(ctx,color) {
if (this.barnesHutTree !== undefined) {
ctx.lineWidth = 1;
this._drawBranch(this.barnesHutTree.root,ctx,color);
}
};
/**
* This function is for debugging purposes. It draws the branches recursively.
*
* @param branch
* @param ctx
* @param color
* @private
*/
exports._drawBranch = function(branch,ctx,color) {
if (color === undefined) {
color = "#FF0000";
}
if (branch.childrenCount == 4) {
this._drawBranch(branch.children.NW,ctx);
this._drawBranch(branch.children.NE,ctx);
this._drawBranch(branch.children.SE,ctx);
this._drawBranch(branch.children.SW,ctx);
}
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(branch.range.minX,branch.range.minY);
ctx.lineTo(branch.range.maxX,branch.range.minY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.maxX,branch.range.minY);
ctx.lineTo(branch.range.maxX,branch.range.maxY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.maxX,branch.range.maxY);
ctx.lineTo(branch.range.minX,branch.range.maxY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.minX,branch.range.maxY);
ctx.lineTo(branch.range.minX,branch.range.minY);
ctx.stroke();
/*
if (branch.mass > 0) {
ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
ctx.stroke();
}
*/
};

+ 0
- 154
lib/network/mixins/physics/HierarchialRepulsionMixin.js View File

@ -1,154 +0,0 @@
/**
* Calculate the forces the nodes apply on eachother based on a repulsion field.
* This field is linearly approximated.
*
* @private
*/
exports._calculateNodeForces = function () {
var dx, dy, distance, fx, fy,
repulsingForce, node1, node2, i, j;
var nodes = this.calculationNodes;
var nodeIndices = this.calculationNodeIndices;
// repulsing forces between nodes
var nodeDistance = this.constants.physics.hierarchicalRepulsion.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]];
// nodes only affect nodes on their level
if (node1.level == node2.level) {
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
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;
}
else {
repulsingForce = repulsingForce / distance;
}
fx = dx * repulsingForce;
fy = dy * repulsingForce;
node1.fx -= fx;
node1.fy -= fy;
node2.fx += fx;
node2.fy += fy;
}
}
}
};
/**
* this function calculates the effects of the springs in the case of unsmooth curves.
*
* @private
*/
exports._calculateHierarchicalSpringForces = function () {
var edgeLength, edge, edgeId;
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)) {
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)) {
edgeLength = edge.physics.springLength;
// this implies that the edges between big clusters are longer
edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
dx = (edge.from.x - edge.to.x);
dy = (edge.from.y - edge.to.y);
distance = Math.sqrt(dx * dx + dy * dy);
if (distance == 0) {
distance = 0.01;
}
// 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;
if (edge.to.level != edge.from.level) {
edge.to.springFx -= fx;
edge.to.springFy -= fy;
edge.from.springFx += fx;
edge.from.springFy += fy;
}
else {
var factor = 0.5;
edge.to.fx -= factor*fx;
edge.to.fy -= factor*fy;
edge.from.fx += factor*fx;
edge.from.fy += factor*fy;
}
}
}
}
}
// 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;
}
};

+ 0
- 724
lib/network/mixins/physics/PhysicsMixin.js View File

@ -1,724 +0,0 @@
var util = require('../../../util');
var RepulsionMixin = require('./RepulsionMixin');
var HierarchialRepulsionMixin = require('./HierarchialRepulsionMixin');
var BarnesHutMixin = require('./BarnesHutMixin');
/**
* Toggling barnes Hut calculation on and off.
*
* @private
*/
exports._toggleBarnesHut = function () {
this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
this._loadSelectedForceSolver();
this.moving = true;
this.start();
};
/**
* This loads the node force solver based on the barnes hut or repulsion algorithm
*
* @private
*/
exports._loadSelectedForceSolver = function () {
// this overloads the this._calculateNodeForces
if (this.constants.physics.barnesHut.enabled == true) {
this._clearMixin(RepulsionMixin);
this._clearMixin(HierarchialRepulsionMixin);
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._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(HierarchialRepulsionMixin);
}
else {
this._clearMixin(BarnesHutMixin);
this._clearMixin(HierarchialRepulsionMixin);
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._loadMixin(RepulsionMixin);
}
};
/**
* 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
*/
exports._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
*/
exports._calculateForces = 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._calculateNodeForces();
if (this.constants.physics.springConstant > 0) {
if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
this._calculateSpringForcesWithSupport();
}
else {
if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
this._calculateHierarchicalSpringForces();
}
else {
this._calculateSpringForces();
}
}
}
};
/**
* Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
* handled in the calculateForces function. We then use a quadratic curve with the center node as control.
* This function joins the datanodes and invisible (called support) nodes into one object.
* We do this so we do not contaminate this.nodes with the support nodes.
*
* @private
*/
exports._updateCalculationNodes = function () {
if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
this.calculationNodes = {};
this.calculationNodeIndices = [];
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
this.calculationNodes[nodeId] = this.nodes[nodeId];
}
}
var supportNodes = this.sectors['support']['nodes'];
for (var supportNodeId in supportNodes) {
if (supportNodes.hasOwnProperty(supportNodeId)) {
if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
}
else {
supportNodes[supportNodeId]._setForce(0, 0);
}
}
}
for (var idx in this.calculationNodes) {
if (this.calculationNodes.hasOwnProperty(idx)) {
this.calculationNodeIndices.push(idx);
}
}
}
else {
this.calculationNodes = this.nodes;
this.calculationNodeIndices = this.nodeIndices;
}
};
/**
* this function applies the central gravity effect to keep groups from floating off
*
* @private
*/
exports._calculateGravitationalForces = function () {
var dx, dy, distance, node, i;
var nodes = this.calculationNodes;
var gravity = this.constants.physics.centralGravity;
var gravityForce = 0;
for (i = 0; i < this.calculationNodeIndices.length; i++) {
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" && gravity != 0) {
dx = -node.x;
dy = -node.y;
distance = Math.sqrt(dx * dx + dy * dy);
gravityForce = (distance == 0) ? 0 : (gravity / distance);
node.fx = dx * gravityForce;
node.fy = dy * gravityForce;
}
else {
node.fx = 0;
node.fy = 0;
}
}
};
/**
* this function calculates the effects of the springs in the case of unsmooth curves.
*
* @private
*/
exports._calculateSpringForces = function () {
var edgeLength, edge, edgeId;
var dx, dy, fx, fy, springForce, distance;
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)) {
edgeLength = edge.physics.springLength;
// this implies that the edges between big clusters are longer
edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
dx = (edge.from.x - edge.to.x);
dy = (edge.from.y - edge.to.y);
distance = Math.sqrt(dx * dx + dy * dy);
if (distance == 0) {
distance = 0.01;
}
// 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.from.fx += fx;
edge.from.fy += fy;
edge.to.fx -= fx;
edge.to.fy -= fy;
}
}
}
}
};
/**
* This function calculates the springforces on the nodes, accounting for the support nodes.
*
* @private
*/
exports._calculateSpringForcesWithSupport = function () {
var edgeLength, edge, edgeId, combinedClusterSize;
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)) {
if (edge.via != null) {
var node1 = edge.to;
var node2 = edge.via;
var node3 = edge.from;
edgeLength = edge.physics.springLength;
combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
// this implies that the edges between big clusters are longer
edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
}
}
}
}
}
};
/**
* This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
*
* @param node1
* @param node2
* @param edgeLength
* @private
*/
exports._calculateSpringForce = function (node1, node2, edgeLength) {
var dx, dy, fx, fy, springForce, distance;
dx = (node1.x - node2.x);
dy = (node1.y - node2.y);
distance = Math.sqrt(dx * dx + dy * dy);
if (distance == 0) {
distance = 0.01;
}
// 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;
node1.fx += fx;
node1.fy += fy;
node2.fx -= fx;
node2.fy -= fy;
};
exports._cleanupPhysicsConfiguration = function() {
if (this.physicsConfiguration !== undefined) {
while (this.physicsConfiguration.hasChildNodes()) {
this.physicsConfiguration.removeChild(this.physicsConfiguration.firstChild);
}
this.physicsConfiguration.parentNode.removeChild(this.physicsConfiguration);
this.physicsConfiguration = undefined;
}
}
/**
* Load the HTML for the physics config and bind it
* @private
*/
exports._loadPhysicsConfiguration = function () {
if (this.physicsConfiguration === undefined) {
this.backupConstants = {};
util.deepExtend(this.backupConstants,this.constants);
var maxGravitational = Math.max(20000, (-1 * this.constants.physics.barnesHut.gravitationalConstant) * 10);
var maxSpring = Math.min(0.05, this.constants.physics.barnesHut.springConstant * 10)
var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
this.physicsConfiguration = document.createElement('div');
this.physicsConfiguration.className = "PhysicsConfiguration";
this.physicsConfiguration.innerHTML = '' +
'<table><tr><td><b>Simulation Mode:</b></td></tr>' +
'<tr>' +
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' +
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' +
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' +
'</tr>' +
'</table>' +
'<table id="graph_BH_table" style="display:none">' +
'<tr><td><b>Barnes Hut</b></td></tr>' +
'<tr>' +
'<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="0" max="'+maxGravitational+'" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-'+maxGravitational+'</td><td><input value="' + (this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="6" value="' + this.constants.physics.barnesHut.centralGravity + '" step="0.05" style="width:300px" id="graph_BH_cg"></td><td>3</td><td><input value="' + this.constants.physics.barnesHut.centralGravity + '" id="graph_BH_cg_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.barnesHut.springLength + '" step="1" style="width:300px" id="graph_BH_sl"></td><td>500</td><td><input value="' + this.constants.physics.barnesHut.springLength + '" id="graph_BH_sl_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="'+maxSpring+'" value="' + this.constants.physics.barnesHut.springConstant + '" step="0.0001" style="width:300px" id="graph_BH_sc"></td><td>'+maxSpring+'</td><td><input value="' + this.constants.physics.barnesHut.springConstant + '" id="graph_BH_sc_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.barnesHut.damping + '" step="0.005" style="width:300px" id="graph_BH_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.barnesHut.damping + '" id="graph_BH_damp_value" style="width:60px"></td>' +
'</tr>' +
'</table>' +
'<table id="graph_R_table" style="display:none">' +
'<tr><td><b>Repulsion</b></td></tr>' +
'<tr>' +
'<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.repulsion.nodeDistance + '" step="1" style="width:300px" id="graph_R_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.repulsion.nodeDistance + '" id="graph_R_nd_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.repulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_R_cg"></td><td>3</td><td><input value="' + this.constants.physics.repulsion.centralGravity + '" id="graph_R_cg_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.repulsion.springLength + '" step="1" style="width:300px" id="graph_R_sl"></td><td>500</td><td><input value="' + this.constants.physics.repulsion.springLength + '" id="graph_R_sl_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.repulsion.springConstant + '" step="0.001" style="width:300px" id="graph_R_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.repulsion.springConstant + '" id="graph_R_sc_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.repulsion.damping + '" step="0.005" style="width:300px" id="graph_R_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.repulsion.damping + '" id="graph_R_damp_value" style="width:60px"></td>' +
'</tr>' +
'</table>' +
'<table id="graph_H_table" style="display:none">' +
'<tr><td width="150"><b>Hierarchical</b></td></tr>' +
'<tr>' +
'<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" step="1" style="width:300px" id="graph_H_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" id="graph_H_nd_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_H_cg"></td><td>3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" id="graph_H_cg_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" step="1" style="width:300px" id="graph_H_sl"></td><td>500</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" id="graph_H_sl_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" step="0.001" style="width:300px" id="graph_H_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" id="graph_H_sc_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.hierarchicalRepulsion.damping + '" step="0.005" style="width:300px" id="graph_H_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.damping + '" id="graph_H_damp_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">direction</td><td>1</td><td><input type="range" min="0" max="3" value="' + hierarchicalLayoutDirections.indexOf(this.constants.hierarchicalLayout.direction) + '" step="1" style="width:300px" id="graph_H_direction"></td><td>4</td><td><input value="' + this.constants.hierarchicalLayout.direction + '" id="graph_H_direction_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">levelSeparation</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.levelSeparation + '" step="1" style="width:300px" id="graph_H_levsep"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.levelSeparation + '" id="graph_H_levsep_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">nodeSpacing</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.nodeSpacing + '" step="1" style="width:300px" id="graph_H_nspac"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.nodeSpacing + '" id="graph_H_nspac_value" style="width:60px"></td>' +
'</tr>' +
'</table>' +
'<table><tr><td><b>Options:</b></td></tr>' +
'<tr>' +
'<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' +
'<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' +
'<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' +
'</tr>' +
'</table>'
this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
this.optionsDiv = document.createElement("div");
this.optionsDiv.style.fontSize = "14px";
this.optionsDiv.style.fontFamily = "verdana";
this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
var rangeElement;
rangeElement = document.getElementById('graph_BH_gc');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
rangeElement = document.getElementById('graph_BH_cg');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
rangeElement = document.getElementById('graph_BH_sc');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
rangeElement = document.getElementById('graph_BH_sl');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
rangeElement = document.getElementById('graph_BH_damp');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
rangeElement = document.getElementById('graph_R_nd');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
rangeElement = document.getElementById('graph_R_cg');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
rangeElement = document.getElementById('graph_R_sc');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
rangeElement = document.getElementById('graph_R_sl');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
rangeElement = document.getElementById('graph_R_damp');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
rangeElement = document.getElementById('graph_H_nd');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
rangeElement = document.getElementById('graph_H_cg');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
rangeElement = document.getElementById('graph_H_sc');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
rangeElement = document.getElementById('graph_H_sl');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
rangeElement = document.getElementById('graph_H_damp');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
rangeElement = document.getElementById('graph_H_direction');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
rangeElement = document.getElementById('graph_H_levsep');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
rangeElement = document.getElementById('graph_H_nspac');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
var radioButton1 = document.getElementById("graph_physicsMethod1");
var radioButton2 = document.getElementById("graph_physicsMethod2");
var radioButton3 = document.getElementById("graph_physicsMethod3");
radioButton2.checked = true;
if (this.constants.physics.barnesHut.enabled) {
radioButton1.checked = true;
}
if (this.constants.hierarchicalLayout.enabled) {
radioButton3.checked = true;
}
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
var graph_repositionNodes = document.getElementById("graph_repositionNodes");
var graph_generateOptions = document.getElementById("graph_generateOptions");
graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
graph_generateOptions.onclick = graphGenerateOptions.bind(this);
if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) {
graph_toggleSmooth.style.background = "#A4FF56";
}
else {
graph_toggleSmooth.style.background = "#FF8532";
}
switchConfigurations.apply(this);
radioButton1.onchange = switchConfigurations.bind(this);
radioButton2.onchange = switchConfigurations.bind(this);
radioButton3.onchange = switchConfigurations.bind(this);
}
};
/**
* This overwrites the this.constants.
*
* @param constantsVariableName
* @param value
* @private
*/
exports._overWriteGraphConstants = function (constantsVariableName, value) {
var nameArray = constantsVariableName.split("_");
if (nameArray.length == 1) {
this.constants[nameArray[0]] = value;
}
else if (nameArray.length == 2) {
this.constants[nameArray[0]][nameArray[1]] = value;
}
else if (nameArray.length == 3) {
this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
}
};
/**
* this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
*/
function graphToggleSmoothCurves () {
this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled;
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";}
else {graph_toggleSmooth.style.background = "#FF8532";}
this._configureSmoothCurves(false);
}
/**
* this function is used to scramble the nodes
*
*/
function graphRepositionNodes () {
for (var nodeId in this.calculationNodes) {
if (this.calculationNodes.hasOwnProperty(nodeId)) {
this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
}
}
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();
}
this.moving = true;
this.start();
}
/**
* this is used to generate an options file from the playing with physics system.
*/
function graphGenerateOptions () {
var options = "No options are required, default values used.";
var optionsSpecific = [];
var radioButton1 = document.getElementById("graph_physicsMethod1");
var radioButton2 = document.getElementById("graph_physicsMethod2");
if (radioButton1.checked == true) {
if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
if (optionsSpecific.length != 0) {
options = "var options = {";
options += "physics: {barnesHut: {";
for (var i = 0; i < optionsSpecific.length; i++) {
options += optionsSpecific[i];
if (i < optionsSpecific.length - 1) {
options += ", "
}
}
options += '}}'
}
if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) {
if (optionsSpecific.length == 0) {options = "var options = {";}
else {options += ", "}
options += "smoothCurves: " + this.constants.smoothCurves.enabled;
}
if (options != "No options are required, default values used.") {
options += '};'
}
}
else if (radioButton2.checked == true) {
options = "var options = {";
options += "physics: {barnesHut: {enabled: false}";
if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
if (optionsSpecific.length != 0) {
options += ", repulsion: {";
for (var i = 0; i < optionsSpecific.length; i++) {
options += optionsSpecific[i];
if (i < optionsSpecific.length - 1) {
options += ", "
}
}
options += '}}'
}
if (optionsSpecific.length == 0) {options += "}"}
if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
options += ", smoothCurves: " + this.constants.smoothCurves;
}
options += '};'
}
else {
options = "var options = {";
if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
if (optionsSpecific.length != 0) {
options += "physics: {hierarchicalRepulsion: {";
for (var i = 0; i < optionsSpecific.length; i++) {
options += optionsSpecific[i];
if (i < optionsSpecific.length - 1) {
options += ", ";
}
}
options += '}},';
}
options += 'hierarchicalLayout: {';
optionsSpecific = [];
if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
if (optionsSpecific.length != 0) {
for (var i = 0; i < optionsSpecific.length; i++) {
options += optionsSpecific[i];
if (i < optionsSpecific.length - 1) {
options += ", "
}
}
options += '}'
}
else {
options += "enabled:true}";
}
options += '};'
}
this.optionsDiv.innerHTML = options;
}
/**
* this is used to switch between barnesHut, repulsion and hierarchical.
*
*/
function switchConfigurations () {
var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
var tableId = "graph_" + radioButton + "_table";
var table = document.getElementById(tableId);
table.style.display = "block";
for (var i = 0; i < ids.length; i++) {
if (ids[i] != tableId) {
table = document.getElementById(ids[i]);
table.style.display = "none";
}
}
this._restoreNodes();
if (radioButton == "R") {
this.constants.hierarchicalLayout.enabled = false;
this.constants.physics.hierarchicalRepulsion.enabled = false;
this.constants.physics.barnesHut.enabled = false;
}
else if (radioButton == "H") {
if (this.constants.hierarchicalLayout.enabled == false) {
this.constants.hierarchicalLayout.enabled = true;
this.constants.physics.hierarchicalRepulsion.enabled = true;
this.constants.physics.barnesHut.enabled = false;
this.constants.smoothCurves.enabled = false;
this._setupHierarchicalLayout();
}
}
else {
this.constants.hierarchicalLayout.enabled = false;
this.constants.physics.hierarchicalRepulsion.enabled = false;
this.constants.physics.barnesHut.enabled = true;
}
this._loadSelectedForceSolver();
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";}
else {graph_toggleSmooth.style.background = "#FF8532";}
this.moving = true;
this.start();
}
/**
* this generates the ranges depending on the iniital values.
*
* @param id
* @param map
* @param constantsVariableName
*/
function showValueOfRange (id,map,constantsVariableName) {
var valueId = id + "_value";
var rangeValue = document.getElementById(id).value;
if (Array.isArray(map)) {
document.getElementById(valueId).value = map[parseInt(rangeValue)];
this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
}
else {
document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
}
if (constantsVariableName == "hierarchicalLayout_direction" ||
constantsVariableName == "hierarchicalLayout_levelSeparation" ||
constantsVariableName == "hierarchicalLayout_nodeSpacing") {
this._setupHierarchicalLayout();
}
this.moving = true;
this.start();
}

+ 0
- 64
lib/network/mixins/physics/RepulsionMixin.js View File

@ -1,64 +0,0 @@
/**
* Calculate the forces the nodes apply on each other based on a repulsion field.
* This field is linearly approximated.
*
* @private
*/
exports._calculateNodeForces = function () {
var dx, dy, angle, distance, fx, fy, combinedClusterSize,
repulsingForce, node1, node2, i, j;
var nodes = this.calculationNodes;
var nodeIndices = this.calculationNodeIndices;
// approximation constants
var a_base = -2 / 3;
var b = 4 / 3;
// 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]];
combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
// same condition as BarnesHut, making sure nodes are never 100% overlapping.
if (distance == 0) {
distance = 0.1*Math.random();
dx = distance;
}
minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
var a = a_base / minimumDistance;
if (distance < 2 * minimumDistance) {
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 = repulsingForce / Math.max(distance,0.01*minimumDistance);
fx = dx * repulsingForce;
fy = dy * repulsingForce;
node1.fx -= fx;
node1.fy -= fy;
node2.fx += fx;
node2.fy += fy;
}
}
}
};

+ 229
- 0
lib/network/modules/Canvas.js View File

@ -0,0 +1,229 @@
/**
* Created by Alex on 26-Feb-15.
*/
var Hammer = require('../../module/hammer');
class Canvas {
/**
* Create the main frame for the Network.
* This function is executed once when a Network object is created. The frame
* contains a canvas, and this canvas contains all objects like the axis and
* nodes.
* @private
*/
constructor(body, options) {
this.body = body;
this.setOptions(options);
this.translation = {x: 0, y: 0};
this.scale = 1.0;
this.body.emitter.on("_setScale", (scale) => {this.scale = scale});
this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;});
this.body.emitter.once("resize", (obj) => {this.translation.x = obj.width * 0.5; this.translation.y = obj.height * 0.5; this.body.emitter.emit("_setTranslation", this.translation)});
this.pixelRatio = 1;
// remove all elements from the container element.
while (this.body.container.hasChildNodes()) {
this.body.container.removeChild(this.body.container.firstChild);
}
this.frame = document.createElement('div');
this.frame.className = 'vis network-frame';
this.frame.style.position = 'relative';
this.frame.style.overflow = 'hidden';
this.frame.tabIndex = 900;
//////////////////////////////////////////////////////////////////
this.frame.canvas = document.createElement("canvas");
this.frame.canvas.style.position = 'relative';
this.frame.appendChild(this.frame.canvas);
if (!this.frame.canvas.getContext) {
var noCanvas = document.createElement( 'DIV' );
noCanvas.style.color = 'red';
noCanvas.style.fontWeight = 'bold' ;
noCanvas.style.padding = '10px';
noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
this.frame.canvas.appendChild(noCanvas);
}
else {
var ctx = this.frame.canvas.getContext("2d");
this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1);
//this.pixelRatio = Math.max(1,this.pixelRatio); // this is to account for browser zooming out. The pixel ratio is ment to switch between 1 and 2 for HD screens.
this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
}
// add the frame to the container element
this.body.container.appendChild(this.frame);
this.body.emitter.emit("_setScale", 1);;
this.body.emitter.emit("_setTranslation", {x: 0.5 * this.frame.canvas.clientWidth,y: 0.5 * this.frame.canvas.clientHeight});;
this._bindHammer();
}
/**
* This function binds hammer, it can be repeated over and over due to the uniqueness check.
* @private
*/
_bindHammer() {
var me = this;
if (this.hammer !== undefined) {
this.hammer.dispose();
}
this.drag = {};
this.pinch = {};
this.hammer = Hammer(this.frame.canvas, {
prevent_default: true
});
this.hammer.on('tap', me.body.eventListeners.onTap );
this.hammer.on('doubletap', me.body.eventListeners.onDoubleTap );
this.hammer.on('hold', me.body.eventListeners.onHold );
this.hammer.on('touch', me.body.eventListeners.onTouch );
this.hammer.on('dragstart', me.body.eventListeners.onDragStart );
this.hammer.on('drag', me.body.eventListeners.onDrag );
this.hammer.on('dragend', me.body.eventListeners.onDragEnd );
if (this.options.zoomable == true) {
this.hammer.on('mousewheel', me.body.eventListeners.onMouseWheel.bind(me));
this.hammer.on('DOMMouseScroll', me.body.eventListeners.onMouseWheel.bind(me)); // for FF
this.hammer.on('pinch', me.body.eventListeners.onPinch.bind(me) );
}
this.hammer.on('mousemove', me.body.eventListeners.onMouseMove.bind(me) );
this.hammerFrame = Hammer(this.frame, {
prevent_default: true
});
this.hammerFrame.on('release', me.body.eventListeners.onRelease.bind(me) );
}
setOptions(options = {}) {
this.options = options;
}
/**
* Set a new size for the network
* @param {string} width Width in pixels or percentage (for example '800px'
* or '50%')
* @param {string} height Height in pixels or percentage (for example '400px'
* or '30%')
*/
setSize(width, height) {
var emitEvent = false;
var oldWidth = this.frame.canvas.width;
var oldHeight = this.frame.canvas.height;
if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) {
this.frame.style.width = width;
this.frame.style.height = height;
this.frame.canvas.style.width = '100%';
this.frame.canvas.style.height = '100%';
this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio;
this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio;
this.options.width = width;
this.options.height = height;
emitEvent = true;
}
else {
// this would adapt the width of the canvas to the width from 100% if and only if
// there is a change.
if (this.frame.canvas.width != this.frame.canvas.clientWidth * this.pixelRatio) {
this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio;
emitEvent = true;
}
if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) {
this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio;
emitEvent = true;
}
}
if (emitEvent === true) {
this.body.emitter.emit('resize', {width:this.frame.canvas.width * this.pixelRatio,height:this.frame.canvas.height * this.pixelRatio, oldWidth: oldWidth * this.pixelRatio, oldHeight: oldHeight * this.pixelRatio});
}
};
/**
* Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
* the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
* @param {number} x
* @returns {number}
* @private
*/
_XconvertDOMtoCanvas(x) {
return (x - this.translation.x) / this.scale;
}
/**
* Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
* the X coordinate in DOM-space (coordinate point in browser relative to the container div)
* @param {number} x
* @returns {number}
* @private
*/
_XconvertCanvasToDOM(x) {
return x * this.scale + this.translation.x;
}
/**
* Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to
* the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
* @param {number} y
* @returns {number}
* @private
*/
_YconvertDOMtoCanvas(y) {
return (y - this.translation.y) / this.scale;
}
/**
* Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
* the Y coordinate in DOM-space (coordinate point in browser relative to the container div)
* @param {number} y
* @returns {number}
* @private
*/
_YconvertCanvasToDOM(y) {
return y * this.scale + this.translation.y ;
}
/**
*
* @param {object} pos = {x: number, y: number}
* @returns {{x: number, y: number}}
* @constructor
*/
canvasToDOM (pos) {
return {x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y)};
}
/**
*
* @param {object} pos = {x: number, y: number}
* @returns {{x: number, y: number}}
* @constructor
*/
DOMtoCanvas (pos) {
return {x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y)};
}
}
export {Canvas};

+ 246
- 0
lib/network/modules/CanvasRenderer.js View File

@ -0,0 +1,246 @@
/**
* Created by Alex on 26-Feb-15.
*/
if (typeof window !== 'undefined') {
window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
}
class CanvasRenderer {
constructor(body) {
this.body = body;
this.redrawRequested = false;
this.renderTimer = false;
this.requiresTimeout = true;
this.continueRendering = true;
this.renderRequests = 0;
this.translation = {x: 0, y: 0};
this.scale = 1.0;
this.canvasTopLeft = {x: 0, y: 0};
this.canvasBottomRight = {x: 0, y: 0};
this.body.emitter.on("_setScale", (scale) => this.scale = scale);
this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;});
this.body.emitter.on("_redraw", this._redraw.bind(this));
this.body.emitter.on("_redrawHidden", this._redraw.bind(this, true));
this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this));
this.body.emitter.on("_startRendering", () => {this.renderRequests += 1; this.continueRendering = true; this.startRendering();});
this.body.emitter.on("_stopRendering", () => {this.renderRequests -= 1; this.continueRendering = this.renderRequests > 0;});
this._determineBrowserMethod();
}
startRendering() {
if (this.continueRendering === true) {
if (!this.renderTimer) {
if (this.requiresTimeout == true) {
this.renderTimer = window.setTimeout(this.renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function
}
else {
this.renderTimer = window.requestAnimationFrame(this.renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function
}
}
}
else {
}
}
renderStep() {
// reset the renderTimer so a new scheduled animation step can be set
this.renderTimer = undefined;
if (this.requiresTimeout == true) {
// this schedules a new simulation step
this.startRendering();
}
this._redraw();
if (this.requiresTimeout == false) {
// this schedules a new simulation step
this.startRendering();
}
}
setCanvas(canvas) {
this.canvas = canvas;
}
/**
* Redraw the network with the current data
* chart will be resized too.
*/
redraw() {
this.setSize(this.constants.width, this.constants.height);
this._redraw();
}
/**
* Redraw the network with the current data
* @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over.
* @private
*/
_requestRedraw(hidden) {
if (this.redrawRequested !== true) {
this.redrawRequested = true;
if (this.requiresTimeout === true) {
window.setTimeout(this._redraw.bind(this, hidden),0);
}
else {
window.requestAnimationFrame(this._redraw.bind(this, hidden, true));
}
}
}
_redraw(hidden = false) {
this.body.emitter.emit("_beforeRender");
this.redrawRequested = false;
var ctx = this.canvas.frame.canvas.getContext('2d');
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
// clear the canvas
var w = this.canvas.frame.canvas.clientWidth;
var h = this.canvas.frame.canvas.clientHeight;
ctx.clearRect(0, 0, w, h);
// set scaling and translation
ctx.save();
ctx.translate(this.translation.x, this.translation.y);
ctx.scale(this.scale, this.scale);
this.canvasTopLeft = this.canvas.DOMtoCanvas({x:0,y:0});
this.canvasBottomRight = this.canvas.DOMtoCanvas({x:this.canvas.frame.canvas.clientWidth,y:this.canvas.frame.canvas.clientHeight});
if (hidden === false) {
// todo: solve this
//if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) {
this._drawEdges(ctx);
//}
}
// todo: solve this
//if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) {
this._drawNodes(ctx, this.body.nodes, hidden);
//}
if (hidden === false) {
if (this.controlNodesActive == true) {
this._drawControlNodes(ctx);
}
}
//this._drawNodes(ctx,this.body.supportNodes,true);
// this.physics.nodesSolver._debug(ctx,"#F00F0F");
// restore original scaling and translation
ctx.restore();
if (hidden === true) {
ctx.clearRect(0, 0, w, h);
}
}
/**
* Redraw all nodes
* The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
* @param {CanvasRenderingContext2D} ctx
* @param {Boolean} [alwaysShow]
* @private
*/
_drawNodes(ctx,nodes,alwaysShow = false) {
// first draw the unselected nodes
var selected = [];
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
if (nodes[id].isSelected()) {
selected.push(id);
}
else {
if (alwaysShow === true) {
nodes[id].draw(ctx);
}
else if (nodes[id].inArea() === true) {
nodes[id].draw(ctx);
}
}
}
}
// draw the selected nodes on top
for (var s = 0, sMax = selected.length; s < sMax; s++) {
if (nodes[selected[s]].inArea() || alwaysShow) {
nodes[selected[s]].draw(ctx);
}
}
}
/**
* Redraw all edges
* The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_drawEdges(ctx) {
var edges = this.body.edges;
for (var id in edges) {
if (edges.hasOwnProperty(id)) {
var edge = edges[id];
edge.setScale(this.scale);
if (edge.connected === true) {
edges[id].draw(ctx);
}
}
}
}
/**
* Redraw all edges
* The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_drawControlNodes(ctx) {
var edges = this.body.edges;
for (var id in edges) {
if (edges.hasOwnProperty(id)) {
edges[id]._drawControlNodes(ctx);
}
}
}
/**
* Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because
* some implementations (safari and IE9) did not support requestAnimationFrame
* @private
*/
_determineBrowserMethod() {
if (typeof window !== 'undefined') {
var browserType = navigator.userAgent.toLowerCase();
this.requiresTimeout = false;
if (browserType.indexOf('msie 9.0') != -1) { // IE 9
this.requiresTimeout = true;
}
else if (browserType.indexOf('safari') != -1) { // safari
if (browserType.indexOf('chrome') <= -1) {
this.requiresTimeout = true;
}
}
}
else {
this.requiresTimeout = true;
}
}
}
export {CanvasRenderer};

+ 620
- 0
lib/network/modules/Clustering.js View File

@ -0,0 +1,620 @@
/**
* Created by Alex on 24-Feb-15.
*/
var util = require("../../util");
class ClusterEngine {
constructor(body) {
this.body = body;
this.clusteredNodes = {};
}
/**
*
* @param hubsize
* @param options
*/
clusterByConnectionCount(hubsize, options) {
if (hubsize === undefined) {
hubsize = this._getHubSize();
}
else if (tyepof(hubsize) == "object") {
options = this._checkOptions(hubsize);
hubsize = this._getHubSize();
}
var nodesToCluster = [];
for (var i = 0; i < this.body.nodeIndices.length; i++) {
var node = this.body.nodes[this.body.nodeIndices[i]];
if (node.edges.length >= hubsize) {
nodesToCluster.push(node.id);
}
}
for (var i = 0; i < nodesToCluster.length; i++) {
var node = this.body.nodes[nodesToCluster[i]];
this.clusterByConnection(node,options,{},{},true);
}
this.body.emitter.emit('_dataChanged');
}
/**
* loop over all nodes, check if they adhere to the condition and cluster if needed.
* @param options
* @param doNotUpdateCalculationNodes
*/
clusterByNodeData(options = {}, doNotUpdateCalculationNodes = false) {
if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");}
// check if the options object is fine, append if needed
options = this._checkOptions(options);
var childNodesObj = {};
var childEdgesObj = {}
// collect the nodes that will be in the cluster
for (var i = 0; i < this.body.nodeIndices.length; i++) {
var nodeId = this.body.nodeIndices[i];
var clonedOptions = this._cloneOptions(nodeId);
if (options.joinCondition(clonedOptions) == true) {
childNodesObj[nodeId] = this.body.nodes[nodeId];
}
}
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
}
/**
* Cluster all nodes in the network that have only 1 edge
* @param options
* @param doNotUpdateCalculationNodes
*/
clusterOutliers(options, doNotUpdateCalculationNodes) {
options = this._checkOptions(options);
var clusters = [];
// collect the nodes that will be in the cluster
for (var i = 0; i < this.body.nodeIndices.length; i++) {
var childNodesObj = {};
var childEdgesObj = {};
var nodeId = this.body.nodeIndices[i];
if (this.body.nodes[nodeId].edges.length == 1) {
var edge = this.body.nodes[nodeId].edges[0];
var childNodeId = this._getConnectedId(edge, nodeId);
if (childNodeId != nodeId) {
if (options.joinCondition === undefined) {
childNodesObj[nodeId] = this.body.nodes[nodeId];
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
}
else {
var clonedOptions = this._cloneOptions(nodeId);
if (options.joinCondition(clonedOptions) == true) {
childNodesObj[nodeId] = this.body.nodes[nodeId];
}
clonedOptions = this._cloneOptions(childNodeId);
if (options.joinCondition(clonedOptions) == true) {
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
}
}
clusters.push({nodes:childNodesObj, edges:childEdgesObj})
}
}
}
for (var i = 0; i < clusters.length; i++) {
this._cluster(clusters[i].nodes, clusters[i].edges, options, true)
}
if (doNotUpdateCalculationNodes !== true) {
this.body.emitter.emit('_dataChanged');
}
}
/**
*
* @param nodeId
* @param options
* @param doNotUpdateCalculationNodes
*/
clusterByConnection(nodeId, options, doNotUpdateCalculationNodes) {
// kill conditions
if (nodeId === undefined) {throw new Error("No nodeId supplied to clusterByConnection!");}
if (this.body.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");}
var node = this.body.nodes[nodeId];
options = this._checkOptions(options, node);
if (options.clusterNodeProperties.x === undefined) {options.clusterNodeProperties.x = node.x; options.clusterNodeProperties.allowedToMoveX = !node.xFixed;}
if (options.clusterNodeProperties.y === undefined) {options.clusterNodeProperties.y = node.y; options.clusterNodeProperties.allowedToMoveY = !node.yFixed;}
var childNodesObj = {};
var childEdgesObj = {}
var parentNodeId = node.id;
var parentClonedOptions = this._cloneOptions(parentNodeId);
childNodesObj[parentNodeId] = node;
// collect the nodes that will be in the cluster
for (var i = 0; i < node.edges.length; i++) {
var edge = node.edges[i];
var childNodeId = this._getConnectedId(edge, parentNodeId);
if (childNodeId !== parentNodeId) {
if (options.joinCondition === undefined) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
}
else {
// clone the options and insert some additional parameters that could be interesting.
var childClonedOptions = this._cloneOptions(childNodeId);
if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
}
}
}
else {
childEdgesObj[edge.id] = edge;
}
}
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
}
/**
* This returns a clone of the options or properties of the edge or node to be used for construction of new edges or check functions for new nodes.
* @param objId
* @param type
* @returns {{}}
* @private
*/
_cloneOptions(objId, type) {
var clonedOptions = {};
if (type === undefined || type == 'node') {
util.deepExtend(clonedOptions, this.body.nodes[objId].options, true);
util.deepExtend(clonedOptions, this.body.nodes[objId].properties, true);
clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length;
}
else {
util.deepExtend(clonedOptions, this.body.edges[objId].properties, true);
}
return clonedOptions;
}
/**
* This function creates the edges that will be attached to the cluster.
*
* @param childNodesObj
* @param childEdgesObj
* @param newEdges
* @param options
* @private
*/
_createClusterEdges (childNodesObj, childEdgesObj, newEdges, options) {
var edge, childNodeId, childNode;
var childKeys = Object.keys(childNodesObj);
for (var i = 0; i < childKeys.length; i++) {
childNodeId = childKeys[i];
childNode = childNodesObj[childNodeId];
// mark all edges for removal from global and construct new edges from the cluster to others
for (var j = 0; j < childNode.edges.length; j++) {
edge = childNode.edges[j];
childEdgesObj[edge.id] = edge;
var otherNodeId = edge.toId;
var otherOnTo = true;
if (edge.toId != childNodeId) {
otherNodeId = edge.toId;
otherOnTo = true;
}
else if (edge.fromId != childNodeId) {
otherNodeId = edge.fromId;
otherOnTo = false;
}
if (childNodesObj[otherNodeId] === undefined) {
var clonedOptions = this._cloneOptions(edge.id, 'edge');
util.deepExtend(clonedOptions, options.clusterEdgeProperties);
if (otherOnTo === true) {
clonedOptions.from = options.clusterNodeProperties.id;
clonedOptions.to = otherNodeId;
}
else {
clonedOptions.from = otherNodeId;
clonedOptions.to = options.clusterNodeProperties.id;
}
clonedOptions.id = 'clusterEdge:' + util.randomUUID();
newEdges.push(this.body.functions.createEdge(clonedOptions))
}
}
}
}
/**
* This function checks the options that can be supplied to the different cluster functions
* for certain fields and inserts defaults if needed
* @param options
* @returns {*}
* @private
*/
_checkOptions(options = {}) {
if (options.clusterEdgeProperties === undefined) {options.clusterEdgeProperties = {};}
if (options.clusterNodeProperties === undefined) {options.clusterNodeProperties = {};}
return options;
}
/**
*
* @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node
* @param {Object} childEdgesObj | object with edge objects, id as keys
* @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties}
* @param {Boolean} doNotUpdateCalculationNodes | when true, do not wrap up
* @private
*/
_cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes = false) {
// kill condition: no children so cant cluster
if (Object.keys(childNodesObj).length == 0) {return;}
// check if we have an unique id;
if (options.clusterNodeProperties.id === undefined) {options.clusterNodeProperties.id = 'cluster:' + util.randomUUID();}
var clusterId = options.clusterNodeProperties.id;
// create the new edges that will connect to the cluster
var newEdges = [];
this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options);
// construct the clusterNodeProperties
var clusterNodeProperties = options.clusterNodeProperties;
if (options.processProperties !== undefined) {
// get the childNode options
var childNodesOptions = [];
for (var nodeId in childNodesObj) {
var clonedOptions = this._cloneOptions(nodeId);
childNodesOptions.push(clonedOptions);
}
// get clusterproperties based on childNodes
var childEdgesOptions = [];
for (var edgeId in childEdgesObj) {
var clonedOptions = this._cloneOptions(edgeId, 'edge');
childEdgesOptions.push(clonedOptions);
}
clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions);
if (!clusterNodeProperties) {
throw new Error("The processClusterProperties function does not return properties!");
}
}
if (clusterNodeProperties.label === undefined) {
clusterNodeProperties.label = 'cluster';
}
// give the clusterNode a postion if it does not have one.
var pos = undefined;
if (clusterNodeProperties.x === undefined) {
pos = this._getClusterPosition(childNodesObj);
clusterNodeProperties.x = pos.x;
clusterNodeProperties.allowedToMoveX = true;
}
if (clusterNodeProperties.x === undefined) {
if (pos === undefined) {
pos = this._getClusterPosition(childNodesObj);
}
clusterNodeProperties.y = pos.y;
clusterNodeProperties.allowedToMoveY = true;
}
// force the ID to remain the same
clusterNodeProperties.id = clusterId;
// create the clusterNode
var clusterNode = this.body.functions.createNode(clusterNodeProperties);
clusterNode.isCluster = true;
clusterNode.containedNodes = childNodesObj;
clusterNode.containedEdges = childEdgesObj;
// delete contained edges from global
for (var edgeId in childEdgesObj) {
if (childEdgesObj.hasOwnProperty(edgeId)) {
if (this.body.edges[edgeId] !== undefined) {
if (this.body.edges[edgeId].via !== null) {
var viaId = this.body.edges[edgeId].via.id;
if (viaId) {
this.body.edges[edgeId].via = null
delete this.body.supportNodes[viaId];
}
}
this.body.edges[edgeId].disconnect();
delete this.body.edges[edgeId];
}
}
}
// remove contained nodes from global
for (var nodeId in childNodesObj) {
if (childNodesObj.hasOwnProperty(nodeId)) {
this.clusteredNodes[nodeId] = {clusterId:clusterNodeProperties.id, node: this.body.nodes[nodeId]};
delete this.body.nodes[nodeId];
}
}
// finally put the cluster node into global
this.body.nodes[clusterNodeProperties.id] = clusterNode;
// push new edges to global
for (var i = 0; i < newEdges.length; i++) {
this.body.edges[newEdges[i].id] = newEdges[i];
this.body.edges[newEdges[i].id].connect();
}
// create bezier nodes for smooth curves if needed
this.body.emitter.emit("_newEdgesCreated");
// set ID to undefined so no duplicates arise
clusterNodeProperties.id = undefined;
// wrap up
if (doNotUpdateCalculationNodes !== true) {
this.body.emitter.emit('_dataChanged');
}
}
/**
* Check if a node is a cluster.
* @param nodeId
* @returns {*}
*/
isCluster(nodeId) {
if (this.body.nodes[nodeId] !== undefined) {
return this.body.nodes[nodeId].isCluster;
}
else {
console.log("Node does not exist.")
return false;
}
}
/**
* get the position of the cluster node based on what's inside
* @param {object} childNodesObj | object with node objects, id as keys
* @returns {{x: number, y: number}}
* @private
*/
_getClusterPosition(childNodesObj) {
var childKeys = Object.keys(childNodesObj);
var minX = childNodesObj[childKeys[0]].x;
var maxX = childNodesObj[childKeys[0]].x;
var minY = childNodesObj[childKeys[0]].y;
var maxY = childNodesObj[childKeys[0]].y;
var node;
for (var i = 0; i < childKeys.lenght; i++) {
node = childNodesObj[childKeys[0]];
minX = node.x < minX ? node.x : minX;
maxX = node.x > maxX ? node.x : maxX;
minY = node.y < minY ? node.y : minY;
maxY = node.y > maxY ? node.y : maxY;
}
return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)};
}
/**
* Open a cluster by calling this function.
* @param {String} clusterNodeId | the ID of the cluster node
* @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true
*/
openCluster(clusterNodeId, doNotUpdateCalculationNodes) {
// kill conditions
if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");}
if (this.body.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");}
if (this.body.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return};
var node = this.body.nodes[clusterNodeId];
var containedNodes = node.containedNodes;
var containedEdges = node.containedEdges;
// release nodes
for (var nodeId in containedNodes) {
if (containedNodes.hasOwnProperty(nodeId)) {
this.body.nodes[nodeId] = containedNodes[nodeId];
// inherit position
this.body.nodes[nodeId].x = node.x;
this.body.nodes[nodeId].y = node.y;
// inherit speed
this.body.nodes[nodeId].vx = node.vx;
this.body.nodes[nodeId].vy = node.vy;
delete this.clusteredNodes[nodeId];
}
}
// release edges
for (var edgeId in containedEdges) {
if (containedEdges.hasOwnProperty(edgeId)) {
this.body.edges[edgeId] = containedEdges[edgeId];
this.body.edges[edgeId].connect();
var edge = this.body.edges[edgeId];
if (edge.connected === false) {
if (this.clusteredNodes[edge.fromId] !== undefined) {
this._connectEdge(edge, edge.fromId, true);
}
if (this.clusteredNodes[edge.toId] !== undefined) {
this._connectEdge(edge, edge.toId, false);
}
}
}
}
this.body.emitter.emit("_newEdgesCreated",containedEdges);
var edgeIds = [];
for (var i = 0; i < node.edges.length; i++) {
edgeIds.push(node.edges[i].id);
}
// remove edges in clusterNode
for (var i = 0; i < edgeIds.length; i++) {
var edge = this.body.edges[edgeIds[i]];
// if the edge should have been connected to a contained node
if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) {
// the node in the from array was contained in the cluster
if (this.body.nodes[edge.fromArray[0].id] !== undefined) {
this._connectEdge(edge, edge.fromArray[0].id, true);
}
}
else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) {
// the node in the to array was contained in the cluster
if (this.body.nodes[edge.toArray[0].id] !== undefined) {
this._connectEdge(edge, edge.toArray[0].id, false);
}
}
else {
var edgeId = edgeIds[i];
var viaId = this.body.edges[edgeId].via.id;
if (viaId) {
this.body.edges[edgeId].via = null
delete this.body.supportNodes[viaId];
}
// this removes the edge from node.edges, which is why edgeIds is formed
this.body.edges[edgeId].disconnect();
delete this.body.edges[edgeId];
}
}
// remove clusterNode
delete this.body.nodes[clusterNodeId];
if (doNotUpdateCalculationNodes !== true) {
this.body.emitter.emit('_dataChanged');
}
}
/**
* Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to
* is currently residing in cluster B
* @param edge
* @param nodeId
* @param from
* @private
*/
_connectEdge(edge, nodeId, from) {
var clusterStack = this._getClusterStack(nodeId);
if (from == true) {
edge.from = clusterStack[clusterStack.length - 1];
edge.fromId = clusterStack[clusterStack.length - 1].id;
clusterStack.pop()
edge.fromArray = clusterStack;
}
else {
edge.to = clusterStack[clusterStack.length - 1];
edge.toId = clusterStack[clusterStack.length - 1].id;
clusterStack.pop();
edge.toArray = clusterStack;
}
edge.connect();
}
/**
* Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
* @param nodeId
* @returns {Array}
* @private
*/
_getClusterStack(nodeId) {
var stack = [];
var max = 100;
var counter = 0;
while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
stack.push(this.clusteredNodes[nodeId].node);
nodeId = this.clusteredNodes[nodeId].clusterId;
counter++;
}
stack.push(this.body.nodes[nodeId]);
return stack;
}
/**
* Get the Id the node is connected to
* @param edge
* @param nodeId
* @returns {*}
* @private
*/
_getConnectedId(edge, nodeId) {
if (edge.toId != nodeId) {
return edge.toId;
}
else if (edge.fromId != nodeId) {
return edge.fromId;
}
else {
return edge.fromId;
}
}
/**
* We determine how many connections denote an important hub.
* We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
*
* @private
*/
_getHubSize() {
var average = 0;
var averageSquared = 0;
var hubCounter = 0;
var largestHub = 0;
for (var i = 0; i < this.body.nodeIndices.length; i++) {
var node = this.body.nodes[this.body.nodeIndices[i]];
if (node.edges.length > largestHub) {
largestHub = node.edges.length;
}
average += node.edges.length;
averageSquared += Math.pow(node.edges.length,2);
hubCounter += 1;
}
average = average / hubCounter;
averageSquared = averageSquared / hubCounter;
var variance = averageSquared - Math.pow(average,2);
var standardDeviation = Math.sqrt(variance);
var hubThreshold = Math.floor(average + 2*standardDeviation);
// always have at least one to cluster
if (hubThreshold > largestHub) {
hubThreshold = largestHub;
}
return hubThreshold;
};
}
export { ClusterEngine };

+ 419
- 0
lib/network/modules/PhysicsEngine.js View File

@ -0,0 +1,419 @@
/**
* Created by Alex on 2/23/2015.
*/
import {BarnesHutSolver} from "./components/physics/BarnesHutSolver";
import {Repulsion} from "./components/physics/RepulsionSolver";
import {HierarchicalRepulsion} from "./components/physics/HierarchicalRepulsionSolver";
import {SpringSolver} from "./components/physics/SpringSolver";
import {HierarchicalSpringSolver} from "./components/physics/HierarchicalSpringSolver";
import {CentralGravitySolver} from "./components/physics/CentralGravitySolver";
var util = require('../../util');
class PhysicsEngine {
constructor(body, options) {
this.body = body;
this.physicsBody = {calculationNodes: {}, calculationNodeIndices:[], forces: {}, velocities: {}};
this.scale = 1;
this.viewFunction = undefined;
this.body.emitter.on("_setScale", (scale) => this.scale = scale);
this.simulationInterval = 1000 / 60;
this.requiresTimeout = true;
this.previousStates = {};
this.renderTimer == undefined;
this.stabilized = false;
this.stabilizationIterations = 0;
// default options
this.options = {
barnesHut: {
thetaInverted: 1 / 0.5, // inverted to save time during calculation
gravitationalConstant: -2000,
centralGravity: 0.3,
springLength: 95,
springConstant: 0.04,
damping: 0.09
},
repulsion: {
centralGravity: 0.0,
springLength: 200,
springConstant: 0.05,
nodeDistance: 100,
damping: 0.09
},
hierarchicalRepulsion: {
centralGravity: 0.0,
springLength: 100,
springConstant: 0.01,
nodeDistance: 150,
damping: 0.09
},
model: 'BarnesHut',
timestep: 0.5,
maxVelocity: 50,
minVelocity: 0.1, // px/s
stabilization: {
enabled: true,
iterations: 1000, // maximum number of iteration to stabilize
updateInterval: 100,
onlyDynamicEdges: false,
zoomExtent: true
}
}
this.setOptions(options);
}
setOptions(options) {
if (options !== undefined) {
if (typeof options.stabilization == 'boolean') {
options.stabilization = {
enabled: options.stabilization
}
}
util.deepExtend(this.options, options);
}
this.init();
}
init() {
var options;
if (this.options.model == "repulsion") {
options = this.options.repulsion;
this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
}
else if (this.options.model == "hierarchicalRepulsion") {
options = this.options.hierarchicalRepulsion;
this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options);
}
else { // barnesHut
options = this.options.barnesHut;
this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
}
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
this.modelOptions = options;
}
startSimulation() {
this.stabilized = false;
if (this.options.stabilization.enabled === true) {
this.stabilize();
}
else {
this.runSimulation();
}
}
runSimulation() {
if (this.viewFunction === undefined) {
this.viewFunction = this.simulationStep.bind(this);
this.body.emitter.on("_beforeRender", this.viewFunction);
this.body.emitter.emit("_startRendering");
}
}
simulationStep() {
// check if the physics have settled
var startTime = Date.now();
this.physicsTick();
var physicsTime = Date.now() - startTime;
// run double speed if it is a little graph
if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) {
this.physicsTick();
// this makes sure there is no jitter. The decision is taken once to run it at double speed.
this.runDoubleSpeed = true;
}
if (this.stabilized === true) {
if (this.stabilizationIterations > 1) {
// trigger the "stabilized" event.
// The event is triggered on the next tick, to prevent the case that
// it is fired while initializing the Network, in which case you would not
// be able to catch it
var me = this;
var params = {
iterations: this.stabilizationIterations
};
this.stabilizationIterations = 0;
this.startedStabilization = false;
setTimeout(function () {
me.body.emitter.emit("stabilized", params);
}, 0);
}
else {
this.stabilizationIterations = 0;
}
this.body.emitter.emit("_stopRendering");
}
}
/**
* A single simulation step (or "tick") in the physics simulation
*
* @private
*/
physicsTick() {
if (this.stabilized === false) {
this.calculateForces();
this.stabilized = this.moveNodes();
// determine if the network has stabilzied
if (this.stabilized === true) {
this.revert();
}
else {
// this is here to ensure that there is no start event when the network is already stable.
if (this.startedStabilization == false) {
this.body.emitter.emit("startStabilizing");
this.startedStabilization = true;
}
}
this.stabilizationIterations++;
}
}
/**
* Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
* handled in the calculateForces function. We then use a quadratic curve with the center node as control.
* This function joins the datanodes and invisible (called support) nodes into one object.
* We do this so we do not contaminate this.body.nodes with the support nodes.
*
* @private
*/
_updateCalculationNodes() {
this.physicsBody.calculationNodes = {};
this.physicsBody.forces = {};
this.physicsBody.calculationNodeIndices = [];
for (let i = 0; i < this.body.nodeIndices.length; i++) {
let nodeId = this.body.nodeIndices[i];
this.physicsBody.calculationNodes[nodeId] = this.body.nodes[nodeId];
}
// if support nodes are used, we have them here
var supportNodes = this.body.supportNodes;
for (let i = 0; i < this.body.supportNodeIndices.length; i++) {
let supportNodeId = this.body.supportNodeIndices[i];
if (this.body.edges[supportNodes[supportNodeId].parentEdgeId] !== undefined) {
this.physicsBody.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
}
else {
console.error("Support node detected that does not have an edge!")
}
}
this.physicsBody.calculationNodeIndices = Object.keys(this.physicsBody.calculationNodes);
for (let i = 0; i < this.physicsBody.calculationNodeIndices.length; i++) {
let nodeId = this.physicsBody.calculationNodeIndices[i];
this.physicsBody.forces[nodeId] = {x:0,y:0};
// forces can be reset because they are recalculated. Velocities have to persist.
if (this.physicsBody.velocities[nodeId] === undefined) {
this.physicsBody.velocities[nodeId] = {x:0,y:0};
}
}
// clean deleted nodes from the velocity vector
for (let nodeId in this.physicsBody.velocities) {
if (this.physicsBody.calculationNodes[nodeId] === undefined) {
delete this.physicsBody.velocities[nodeId];
}
}
}
revert() {
var nodeIds = Object.keys(this.previousStates);
var nodes = this.physicsBody.calculationNodes;
var velocities = this.physicsBody.velocities;
for (let i = 0; i < nodeIds.length; i++) {
let nodeId = nodeIds[i];
if (nodes[nodeId] !== undefined) {
velocities[nodeId].x = this.previousStates[nodeId].vx;
velocities[nodeId].y = this.previousStates[nodeId].vy;
nodes[nodeId].x = this.previousStates[nodeId].x;
nodes[nodeId].y = this.previousStates[nodeId].y;
}
else {
delete this.previousStates[nodeId];
}
}
}
moveNodes() {
var nodesPresent = false;
var nodeIndices = this.physicsBody.calculationNodeIndices;
var maxVelocity = this.options.maxVelocity === 0 ? 1e9 : this.options.maxVelocity;
var stabilized = true;
var vminCorrected = this.options.minVelocity / Math.max(this.scale,0.05);
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
let nodeVelocity = this._performStep(nodeId, maxVelocity);
// stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
stabilized = nodeVelocity < vminCorrected && stabilized === true;
nodesPresent = true;
}
if (nodesPresent == true) {
if (vminCorrected > 0.5*this.options.maxVelocity) {
return false;
}
else {
return stabilized;
}
}
return true;
}
_performStep(nodeId,maxVelocity) {
var node = this.physicsBody.calculationNodes[nodeId];
var timestep = this.options.timestep;
var forces = this.physicsBody.forces;
var velocities = this.physicsBody.velocities;
// store the state so we can revert
this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].y};
if (!node.xFixed) {
let dx = this.modelOptions.damping * velocities[nodeId].x; // damping force
let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration
velocities[nodeId].x += ax * timestep; // velocity
velocities[nodeId].x = (Math.abs(velocities[nodeId].x) > maxVelocity) ? ((velocities[nodeId].x > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].x;
node.x += velocities[nodeId].x * timestep; // position
}
else {
forces[nodeId].x = 0;
velocities[nodeId].x = 0;
}
if (!node.yFixed) {
let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force
let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration
velocities[nodeId].y += ay * timestep; // velocity
velocities[nodeId].y = (Math.abs(velocities[nodeId].y) > maxVelocity) ? ((velocities[nodeId].y > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].y;
node.y += velocities[nodeId].y * timestep; // position
}
else {
forces[nodeId].y = 0;
velocities[nodeId].y = 0;
}
var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2));
return totalVelocity;
}
calculateForces() {
this.gravitySolver.solve();
this.nodesSolver.solve();
this.edgesSolver.solve();
}
/**
* When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
* because only the supportnodes for the smoothCurves have to settle.
*
* @private
*/
_freezeNodes() {
var nodes = this.body.nodes;
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
if (nodes[id].x != null && nodes[id].y != null) {
nodes[id].fixedData.x = nodes[id].xFixed;
nodes[id].fixedData.y = nodes[id].yFixed;
nodes[id].xFixed = true;
nodes[id].yFixed = true;
}
}
}
}
/**
* Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
*
* @private
*/
_restoreFrozenNodes() {
var nodes = this.body.nodes;
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
if (nodes[id].fixedData.x != null) {
nodes[id].xFixed = nodes[id].fixedData.x;
nodes[id].yFixed = nodes[id].fixedData.y;
}
}
}
}
/**
* Find a stable position for all nodes
* @private
*/
stabilize() {
if (this.options.stabilization.onlyDynamicEdges == true) {
this._freezeNodes();
}
this.stabilizationSteps = 0;
setTimeout(this._stabilizationBatch.bind(this),0);
}
_stabilizationBatch() {
var count = 0;
while (this.stabilized == false && count < this.options.stabilization.updateInterval && this.stabilizationSteps < this.options.stabilization.iterations) {
this.physicsTick();
this.stabilizationSteps++;
count++;
}
if (this.stabilized == false && this.stabilizationSteps < this.options.stabilization.iterations) {
this.body.emitter.emit("stabilizationProgress", {steps: this.stabilizationSteps, total: this.options.stabilization.iterations});
setTimeout(this._stabilizationBatch.bind(this),0);
}
else {
this._finalizeStabilization();
}
}
_finalizeStabilization() {
if (this.options.stabilization.zoomExtent == true) {
this.body.emitter.emit("zoomExtent", {duration:0});
}
if (this.options.stabilization.onlyDynamicEdges == true) {
this._restoreFrozenNodes();
}
this.body.emitter.emit("stabilizationIterationsDone");
this.body.emitter.emit("_requestRedraw");
}
}
export {PhysicsEngine};

+ 460
- 0
lib/network/modules/TouchEventHandler.js View File

@ -0,0 +1,460 @@
/**
* Created by Alex on 2/27/2015.
*
*/
import {SelectionHandler} from "./components/SelectionHandler"
var util = require('../../util');
class TouchEventHandler {
constructor(body) {
this.body = body;
this.body.eventListeners.onTap = this.onTap.bind(this);
this.body.eventListeners.onTouch = this.onTouch.bind(this);
this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this);
this.body.eventListeners.onHold = this.onHold.bind(this);
this.body.eventListeners.onDragStart = this.onDragStart.bind(this);
this.body.eventListeners.onDrag = this.onDrag.bind(this);
this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this);
this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this);
this.body.eventListeners.onPinch = this.onPinch.bind(this);
this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this);
this.body.eventListeners.onRelease = this.onRelease.bind(this);
this.touchTime = 0;
this.drag = {};
this.pinch = {};
this.pointerPosition = {x:0,y:0};
this.scale = 1.0;
this.body.emitter.on("_setScale", (scale) => this.scale = scale);
this.selectionHandler = new SelectionHandler(body);
}
setCanvas(canvas) {
this.canvas = canvas;
this.selectionHandler.setCanvas(canvas);
}
/**
* Get the pointer location from a touch location
* @param {{pageX: Number, pageY: Number}} touch
* @return {{x: Number, y: Number}} pointer
* @private
*/
getPointer(touch) {
return {
x: touch.pageX - util.getAbsoluteLeft(this.canvas.frame.canvas),
y: touch.pageY - util.getAbsoluteTop(this.canvas.frame.canvas)
};
}
/**
* On start of a touch gesture, store the pointer
* @param event
* @private
*/
onTouch(event) {
if (new Date().valueOf() - this.touchTime > 100) {
this.drag.pointer = this.getPointer(event.gesture.center);
this.drag.pinched = false;
this.pinch.scale = this.scale;
// to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame)
this.touchTime = new Date().valueOf();
}
}
/**
* handle tap/click event: select/unselect a node
* @private
*/
onTap(event) {
console.log("tap",event)
var pointer = this.getPointer(event.gesture.center);
this.pointerPosition = pointer;
this.selectionHandler.selectOnPoint(pointer);
}
/**
* handle drag start event
* @private
*/
/**
* This function is called by onDragStart.
* It is separated out because we can then overload it for the datamanipulation system.
*
* @private
*/
onDragStart(event) {
// in case the touch event was triggered on an external div, do the initial touch now.
//if (this.drag.pointer === undefined) {
// this.onTouch(event);
//}
//
//var node = this._getNodeAt(this.drag.pointer);
//// note: drag.pointer is set in onTouch to get the initial touch location
//
//this.drag.dragging = true;
//this.drag.selection = [];
//this.drag.translation = this._getTranslation();
//this.drag.nodeId = null;
//this.draggingNodes = false;
//
//if (node != null && this.constants.dragNodes == true) {
// this.draggingNodes = true;
// this.drag.nodeId = node.id;
// // select the clicked node if not yet selected
// if (!node.isSelected()) {
// this._selectObject(node, false);
// }
//
// this.emit("dragStart", {nodeIds: this.getSelection().nodes});
//
// // create an array with the selected nodes and their original location and status
// for (var objectId in this.selectionObj.nodes) {
// if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
// var object = this.selectionObj.nodes[objectId];
// var s = {
// id: object.id,
// node: object,
//
// // store original x, y, xFixed and yFixed, make the node temporarily Fixed
// x: object.x,
// y: object.y,
// xFixed: object.xFixed,
// yFixed: object.yFixed
// };
//
// object.xFixed = true;
// object.yFixed = true;
//
// this.drag.selection.push(s);
// }
// }
//}
}
/**
* handle drag event
* @private
*/
onDrag(event) {
//if (this.drag.pinched) {
// return;
//}
//
//// remove the focus on node if it is focussed on by the focusOnNode
//this.releaseNode();
//
//var pointer = this.getPointer(event.gesture.center);
//var me = this;
//var drag = this.drag;
//var selection = drag.selection;
//if (selection && selection.length && this.constants.dragNodes == true) {
// // calculate delta's and new location
// var deltaX = pointer.x - drag.pointer.x;
// var deltaY = pointer.y - drag.pointer.y;
//
// // update position of all selected nodes
// selection.forEach(function (s) {
// var node = s.node;
//
// if (!s.xFixed) {
// node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX);
// }
//
// if (!s.yFixed) {
// node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY);
// }
// });
//
//
// // start _animationStep if not yet running
// if (!this.moving) {
// this.moving = true;
// this.start();
// }
//}
//else {
// // move the network
// if (this.constants.dragNetwork == true) {
// // if the drag was not started properly because the click started outside the network div, start it now.
// if (this.drag.pointer === undefined) {
// this._handleDragStart(event);
// return;
// }
// var diffX = pointer.x - this.drag.pointer.x;
// var diffY = pointer.y - this.drag.pointer.y;
//
// this._setTranslation(
// this.drag.translation.x + diffX,
// this.drag.translation.y + diffY
// );
// this._redraw();
// }
//}
}
/**
* handle drag start event
* @private
*/
onDragEnd(event) {
//this.drag.dragging = false;
//var selection = this.drag.selection;
//if (selection && selection.length) {
// selection.forEach(function (s) {
// // restore original xFixed and yFixed
// s.node.xFixed = s.xFixed;
// s.node.yFixed = s.yFixed;
// });
// this.moving = true;
// this.start();
//}
//else {
// this._redraw();
//}
//if (this.draggingNodes == false) {
// this.emit("dragEnd", {nodeIds: []});
//}
//else {
// this.emit("dragEnd", {nodeIds: this.getSelection().nodes});
//}
}
/**
* handle doubletap event
* @private
*/
onDoubleTap(event) {
//var pointer = this.getPointer(event.gesture.center);
//this._handleDoubleTap(pointer);
}
/**
* handle long tap event: multi select nodes
* @private
*/
onHold(event) {
//var pointer = this.getPointer(event.gesture.center);
//this.pointerPosition = pointer;
//this._handleOnHold(pointer);
}
/**
* handle the release of the screen
*
* @private
*/
onRelease(event) {
//var pointer = this.getPointer(event.gesture.center);
//this._handleOnRelease(pointer);
}
/**
* Handle pinch event
* @param event
* @private
*/
onPinch(event) {
//var pointer = this.getPointer(event.gesture.center);
//
//this.drag.pinched = true;
//if (!('scale' in this.pinch)) {
// this.pinch.scale = 1;
//}
//
//// TODO: enabled moving while pinching?
//var scale = this.pinch.scale * event.gesture.scale;
//this._zoom(scale, pointer)
}
/**
* Zoom the network in or out
* @param {Number} scale a number around 1, and between 0.01 and 10
* @param {{x: Number, y: Number}} pointer Position on screen
* @return {Number} appliedScale scale is limited within the boundaries
* @private
*/
_zoom(scale, pointer) {
//if (this.constants.zoomable == true) {
// var scaleOld = this._getScale();
// if (scale < 0.00001) {
// scale = 0.00001;
// }
// if (scale > 10) {
// scale = 10;
// }
//
// var preScaleDragPointer = null;
// if (this.drag !== undefined) {
// if (this.drag.dragging == true) {
// preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer);
// }
// }
// // + this.canvas.frame.canvas.clientHeight / 2
// var translation = this._getTranslation();
//
// var scaleFrac = scale / scaleOld;
// var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
// var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
//
// this._setScale(scale);
// this._setTranslation(tx, ty);
//
// if (preScaleDragPointer != null) {
// var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer);
// this.drag.pointer.x = postScaleDragPointer.x;
// this.drag.pointer.y = postScaleDragPointer.y;
// }
//
// this._redraw();
//
// if (scaleOld < scale) {
// this.emit("zoom", {direction: "+"});
// }
// else {
// this.emit("zoom", {direction: "-"});
// }
//
// return scale;
//}
}
/**
* Event handler for mouse wheel event, used to zoom the timeline
* See http://adomas.org/javascript-mouse-wheel/
* https://github.com/EightMedia/hammer.js/issues/256
* @param {MouseEvent} event
* @private
*/
onMouseWheel(event) {
//// retrieve delta
//var delta = 0;
//if (event.wheelDelta) { /* IE/Opera. */
// delta = event.wheelDelta / 120;
//} else if (event.detail) { /* Mozilla case. */
// // In Mozilla, sign of delta is different than in IE.
// // Also, delta is multiple of 3.
// delta = -event.detail / 3;
//}
//
//// If delta is nonzero, handle it.
//// Basically, delta is now positive if wheel was scrolled up,
//// and negative, if wheel was scrolled down.
//if (delta) {
//
// // calculate the new scale
// var scale = this._getScale();
// var zoom = delta / 10;
// if (delta < 0) {
// zoom = zoom / (1 - zoom);
// }
// scale *= (1 + zoom);
//
// // calculate the pointer location
// var gesture = hammerUtil.fakeGesture(this, event);
// var pointer = this.getPointer(gesture.center);
//
// // apply the new scale
// this._zoom(scale, pointer);
//}
//
//// Prevent default actions caused by mouse wheel.
//event.preventDefault();
}
/**
* Mouse move handler for checking whether the title moves over a node with a title.
* @param {Event} event
* @private
*/
onMouseMove(event) {
//var gesture = hammerUtil.fakeGesture(this, event);
//var pointer = this.getPointer(gesture.center);
//var popupVisible = false;
//
//// check if the previously selected node is still selected
//if (this.popup !== undefined) {
// if (this.popup.hidden === false) {
// this._checkHidePopup(pointer);
// }
//
// // if the popup was not hidden above
// if (this.popup.hidden === false) {
// popupVisible = true;
// this.popup.setPosition(pointer.x + 3, pointer.y - 5)
// this.popup.show();
// }
//}
//
//// if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over
//if (this.constants.keyboard.bindToWindow == false && this.constants.keyboard.enabled == true) {
// this.canvas.frame.focus();
//}
//
//// start a timeout that will check if the mouse is positioned above an element
//if (popupVisible === false) {
// var me = this;
// var checkShow = function() {
// me._checkShowPopup(pointer);
// };
//
// if (this.popupTimer) {
// clearInterval(this.popupTimer); // stop any running calculationTimer
// }
// if (!this.drag.dragging) {
// this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
// }
//}
//
///**
// * Adding hover highlights
// */
//if (this.constants.hover == true) {
// // removing all hover highlights
// for (var edgeId in this.hoverObj.edges) {
// if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
// this.hoverObj.edges[edgeId].hover = false;
// delete this.hoverObj.edges[edgeId];
// }
// }
//
// // adding hover highlights
// var obj = this._getNodeAt(pointer);
// if (obj == null) {
// obj = this._getEdgeAt(pointer);
// }
// if (obj != null) {
// this._hoverObject(obj);
// }
//
// // removing all node hover highlights except for the selected one.
// for (var nodeId in this.hoverObj.nodes) {
// if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
// if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) {
// this._blurObject(this.hoverObj.nodes[nodeId]);
// delete this.hoverObj.nodes[nodeId];
// }
// }
// }
// this.redraw();
//}
}
}
export {TouchEventHandler};

+ 342
- 0
lib/network/modules/View.js View File

@ -0,0 +1,342 @@
/**
* Created by Alex on 26-Feb-15.
*/
var util = require('../../util');
class View {
constructor(body, options) {
this.body = body;
this.setOptions(options);
this.animationSpeed = 1/this.renderRefreshRate;
this.animationEasingFunction = "easeInOutQuint";
this.easingTime = 0;
this.sourceScale = 0;
this.targetScale = 0;
this.sourceTranslation = 0;
this.targetTranslation = 0;
this.lockedOnNodeId = null;
this.lockedOnNodeOffset = null;
this.touchTime = 0;
this.translation = {x: 0, y: 0};
this.scale = 1.0;
this.viewFunction = undefined;
this.body.emitter.on("zoomExtent", this.zoomExtent.bind(this));
this.body.emitter.on("_setScale", (scale) => this.scale = scale);
this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;});
this.body.emitter.on("animationFinished", () => {this.body.emitter.emit("_stopRendering");});
this.body.emitter.on("unlockNode", this.releaseNode.bind(this));
}
setOptions(options = {}) {
this.options = options;
}
setCanvas(canvas) {
this.canvas = canvas;
}
// zoomExtent
/**
* Find the center position of the network
* @private
*/
_getRange(specificNodes = []) {
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
if (specificNodes.length > 0) {
for (var i = 0; i < specificNodes.length; i++) {
node = this.body.nodes[specificNodes[i]];
if (minX > (node.boundingBox.left)) {
minX = node.boundingBox.left;
}
if (maxX < (node.boundingBox.right)) {
maxX = node.boundingBox.right;
}
if (minY > (node.boundingBox.bottom)) {
minY = node.boundingBox.top;
} // top is negative, bottom is positive
if (maxY < (node.boundingBox.top)) {
maxY = node.boundingBox.bottom;
} // top is negative, bottom is positive
}
}
else {
for (var nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (minX > (node.boundingBox.left)) {
minX = node.boundingBox.left;
}
if (maxX < (node.boundingBox.right)) {
maxX = node.boundingBox.right;
}
if (minY > (node.boundingBox.bottom)) {
minY = node.boundingBox.top;
} // top is negative, bottom is positive
if (maxY < (node.boundingBox.top)) {
maxY = node.boundingBox.bottom;
} // top is negative, bottom is positive
}
}
}
if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
minY = 0, maxY = 0, minX = 0, maxX = 0;
}
return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
}
/**
* @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
* @returns {{x: number, y: number}}
* @private
*/
_findCenter(range) {
return {x: (0.5 * (range.maxX + range.minX)),
y: (0.5 * (range.maxY + range.minY))};
}
/**
* This function zooms out to fit all data on screen based on amount of nodes
* @param {Object}
* @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
* @param {Boolean} [disableStart] | If true, start is not called.
*/
zoomExtent(options = {nodes:[]}, initialZoom = false) {
var range;
var zoomLevel;
if (initialZoom == true) {
// check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation.
var positionDefined = 0;
for (var nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
var node = this.body.nodes[nodeId];
if (node.predefinedPosition == true) {
positionDefined += 1;
}
}
}
if (positionDefined > 0.5 * this.body.nodeIndices.length) {
this.zoomExtent(options,false);
return;
}
range = this._getRange(options.nodes);
var numberOfNodes = this.body.nodeIndices.length;
if (this.options.smoothCurves == true) {
zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
}
else {
zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
}
// correct for larger canvasses.
var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600);
zoomLevel *= factor;
}
else {
this.body.emitter.emit("_redrawHidden");
range = this._getRange(options.nodes);
var xDistance = Math.abs(range.maxX - range.minX) * 1.1;
var yDistance = Math.abs(range.maxY - range.minY) * 1.1;
var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance;
var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance;
zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
}
if (zoomLevel > 1.0) {
zoomLevel = 1.0;
}
var center = this._findCenter(range);
var animationOptions = {position: center, scale: zoomLevel, animation: options};
this.moveTo(animationOptions);
}
// animation
/**
* Center a node in view.
*
* @param {Number} nodeId
* @param {Number} [options]
*/
focusOnNode(nodeId, options = {}) {
if (this.body.nodes[nodeId] !== undefined) {
var nodePosition = {x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y};
options.position = nodePosition;
options.lockedOnNode = nodeId;
this.moveTo(options)
}
else {
console.log("Node: " + nodeId + " cannot be found.");
}
}
/**
*
* @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels
* | options.scale = Number // scale to move to
* | options.position = {x:Number, y:Number} // position to move to
* | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to
*/
moveTo(options) {
if (options === undefined) {
options = {};
return;
}
if (options.offset === undefined) {options.offset = {x: 0, y: 0}; }
if (options.offset.x === undefined) {options.offset.x = 0; }
if (options.offset.y === undefined) {options.offset.y = 0; }
if (options.scale === undefined) {options.scale = this.scale; }
if (options.position === undefined) {options.position = this.translation;}
if (options.animation === undefined) {options.animation = {duration:0}; }
if (options.animation === false ) {options.animation = {duration:0}; }
if (options.animation === true ) {options.animation = {}; }
if (options.animation.duration === undefined) {options.animation.duration = 1000; } // default duration
if (options.animation.easingFunction === undefined) {options.animation.easingFunction = "easeInOutQuad"; } // default easing function
this.animateView(options);
}
/**
*
* @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels
* | options.time = Number // animation time in milliseconds
* | options.scale = Number // scale to animate to
* | options.position = {x:Number, y:Number} // position to animate to
* | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad,
* // easeInCubic, easeOutCubic, easeInOutCubic,
* // easeInQuart, easeOutQuart, easeInOutQuart,
* // easeInQuint, easeOutQuint, easeInOutQuint
*/
animateView(options) {
if (options === undefined) {
return;
}
this.animationEasingFunction = options.animation.easingFunction;
// release if something focussed on the node
this.releaseNode();
if (options.locked == true) {
this.lockedOnNodeId = options.lockedOnNode;
this.lockedOnNodeOffset = options.offset;
}
// forcefully complete the old animation if it was still running
if (this.easingTime != 0) {
this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation.
}
this.sourceScale = this.scale;
this.sourceTranslation = this.translation;
this.targetScale = options.scale;
// set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw
// but at least then we'll have the target transition
this.body.emitter.emit("_setScale",this.targetScale);
var viewCenter = this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight});
var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
x: viewCenter.x - options.position.x,
y: viewCenter.y - options.position.y
};
this.targetTranslation = {
x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x,
y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y
};
// if the time is set to 0, don't do an animation
if (options.animation.duration == 0) {
if (this.lockedOnNodeId != null) {
this.viewFunction = this._lockedRedraw.bind(this);
this.body.emitter.on("_beforeRender", this.viewFunction);
}
else {
this.body.emitter.emit("_setScale", this.targetScale);;
this.body.emitter.emit("_setTranslation", this.targetTranslation);
this.body.emitter.emit("_requestRedraw");
}
}
else {
this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's
this.animationEasingFunction = options.animation.easingFunction;
this.viewFunction = this._transitionRedraw.bind(this);
this.body.emitter.on("_beforeRender", this.viewFunction);
this.body.emitter.emit("_startRendering");
}
}
/**
* used to animate smoothly by hijacking the redraw function.
* @private
*/
_lockedRedraw() {
var nodePosition = {x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y};
var viewCenter = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight});
var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
x: viewCenter.x - nodePosition.x,
y: viewCenter.y - nodePosition.y
};
var sourceTranslation = this.translation;
var targetTranslation = {
x: sourceTranslation.x + distanceFromCenter.x * this.scale + this.lockedOnNodeOffset.x,
y: sourceTranslation.y + distanceFromCenter.y * this.scale + this.lockedOnNodeOffset.y
};
this.body.emitter.emit("_setTranslation", targetTranslation);
}
releaseNode() {
if (this.lockedOnNodeId !== undefined) {
this.body.emitter.off("_beforeRender", this.viewFunction);
this.lockedOnNodeId = undefined;
this.lockedOnNodeOffset = undefined;
}
}
/**
*
* @param easingTime
* @private
*/
_transitionRedraw(finished = false) {
this.easingTime += this.animationSpeed;
this.easingTime = finished === true ? 1.0 : this.easingTime;
var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime);
this.body.emitter.emit("_setScale", this.sourceScale + (this.targetScale - this.sourceScale) * progress);
this.body.emitter.emit("_setTranslation", {
x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress,
y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress
});
// cleanup
if (this.easingTime >= 1.0) {
this.body.emitter.off("_beforeRender", this.viewFunction);
this.easingTime = 0;
if (this.lockedOnNodeId != null) {
this.viewFunction = this._lockedRedraw.bind(this);
this.body.emitter.on("_beforeRender", this.viewFunction);
}
this.body.emitter.emit("animationFinished");
}
};
}
export {View};

+ 640
- 0
lib/network/modules/components/SelectionHandler.js View File

@ -0,0 +1,640 @@
/**
* Created by Alex on 2/27/2015.
*/
var Node = require("../../Node");
class SelectionHandler {
constructor(body) {
this.body = body;
this.selectionObj = {nodes:[], edges:[]};
this.options = {
select: true,
selectConnectedEdges: true
}
}
setCanvas(canvas) {
this.canvas = canvas;
}
/**
* handles the selection part of the tap;
*
* @param {Object} pointer
* @private
*/
selectOnPoint(pointer) {
if (this.options.select === true) {
if (this._getSelectedObjectCount() > 0) {this._unselectAll();}
this.selectObject(pointer);
this._generateClickEvent(pointer);
this.body.emitter.emit("_requestRedraw");
}
}
_generateClickEvent(pointer) {
var properties = this.getSelection();
properties['pointer'] = {
DOM: {x: pointer.x, y: pointer.y},
canvas: this.canvas.DOMtoCanvas(pointer)
}
this.body.emitter.emit("click", properties);
}
selectObject(pointer) {
var obj = this._getNodeAt(pointer);
if (obj != null) {
if (this.options.selectConnectedEdges === true) {
this._selectConnectedEdges(obj);
}
}
else {
obj = this._getEdgeAt(pointer);
}
if (obj !== null) {
obj.select();
this._addToSelection(obj);
this.body.emitter.emit('selected', this.getSelection())
}
return obj;
}
/**
*
* @param object
* @param overlappingNodes
* @private
*/
_getNodesOverlappingWith(object, overlappingNodes) {
var nodes = this.body.nodes;
for (var nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
if (nodes[nodeId].isOverlappingWith(object)) {
overlappingNodes.push(nodeId);
}
}
}
}
/**
* retrieve all nodes overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
_getAllNodesOverlappingWith(object) {
var overlappingNodes = [];
this._getNodesOverlappingWith(object,overlappingNodes);
return overlappingNodes;
}
/**
* Return a position object in canvasspace from a single point in screenspace
*
* @param pointer
* @returns {{left: number, top: number, right: number, bottom: number}}
* @private
*/
_pointerToPositionObject(pointer) {
var canvasPos = this.canvas.DOMtoCanvas(pointer);
return {
left: canvasPos.x,
top: canvasPos.y,
right: canvasPos.x,
bottom: canvasPos.y
};
}
/**
* Get the top node at the a specific point (like a click)
*
* @param {{x: Number, y: Number}} pointer
* @return {Node | null} node
* @private
*/
_getNodeAt(pointer) {
// we first check if this is an navigation controls element
var positionObject = this._pointerToPositionObject(pointer);
var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
// if there are overlapping nodes, select the last one, this is the
// one which is drawn on top of the others
if (overlappingNodes.length > 0) {
return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]];
}
else {
return null;
}
}
/**
* retrieve all edges overlapping with given object, selector is around center
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
_getEdgesOverlappingWith(object, overlappingEdges) {
var edges = this.body.edges;
for (var edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
if (edges[edgeId].isOverlappingWith(object)) {
overlappingEdges.push(edgeId);
}
}
}
}
/**
* retrieve all nodes overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
_getAllEdgesOverlappingWith(object) {
var overlappingEdges = [];
this._getEdgesOverlappingWith(object,overlappingEdges);
return overlappingEdges;
}
/**
* Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
* _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
*
* @param pointer
* @returns {null}
* @private
*/
_getEdgeAt(pointer) {
var positionObject = this._pointerToPositionObject(pointer);
var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
if (overlappingEdges.length > 0) {
return this.body.edges[overlappingEdges[overlappingEdges.length - 1]];
}
else {
return null;
}
}
/**
* Add object to the selection array.
*
* @param obj
* @private
*/
_addToSelection(obj) {
if (obj instanceof Node) {
this.selectionObj.nodes[obj.id] = obj;
}
else {
this.selectionObj.edges[obj.id] = obj;
}
}
/**
* Add object to the selection array.
*
* @param obj
* @private
*/
_addToHover(obj) {
if (obj instanceof Node) {
this.hoverObj.nodes[obj.id] = obj;
}
else {
this.hoverObj.edges[obj.id] = obj;
}
}
/**
* Remove a single option from selection.
*
* @param {Object} obj
* @private
*/
_removeFromSelection(obj) {
if (obj instanceof Node) {
delete this.selectionObj.nodes[obj.id];
}
else {
delete this.selectionObj.edges[obj.id];
}
}
/**
* Unselect all. The selectionObj is useful for this.
*
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
_unselectAll(doNotTrigger = false) {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
this.selectionObj.nodes[nodeId].unselect();
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
this.selectionObj.edges[edgeId].unselect();
}
}
this.selectionObj = {nodes:{},edges:{}};
if (doNotTrigger == false) {
this.body.emitter.emit('select', this.getSelection());
}
}
/**
* return the number of selected nodes
*
* @returns {number}
* @private
*/
_getSelectedNodeCount() {
var count = 0;
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
count += 1;
}
}
return count;
}
/**
* return the selected node
*
* @returns {number}
* @private
*/
_getSelectedNode() {
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
return this.selectionObj.nodes[nodeId];
}
}
return null;
}
/**
* return the selected edge
*
* @returns {number}
* @private
*/
_getSelectedEdge() {
for (var edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
return this.selectionObj.edges[edgeId];
}
}
return null;
}
/**
* return the number of selected edges
*
* @returns {number}
* @private
*/
_getSelectedEdgeCount() {
var count = 0;
for (var edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
count += 1;
}
}
return count;
}
/**
* return the number of selected objects.
*
* @returns {number}
* @private
*/
_getSelectedObjectCount() {
var count = 0;
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
count += 1;
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
count += 1;
}
}
return count;
}
/**
* Check if anything is selected
*
* @returns {boolean}
* @private
*/
_selectionIsEmpty() {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
return false;
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
return false;
}
}
return true;
}
/**
* check if one of the selected nodes is a cluster.
*
* @returns {boolean}
* @private
*/
_clusterInSelection() {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
return true;
}
}
}
return false;
}
/**
* select the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
_selectConnectedEdges(node) {
for (var i = 0; i < node.edges.length; i++) {
var edge = node.edges[i];
edge.select();
this._addToSelection(edge);
}
}
/**
* select the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
_hoverConnectedEdges(node) {
for (var i = 0; i < node.edges.length; i++) {
var edge = node.edges[i];
edge.hover = true;
this._addToHover(edge);
}
}
/**
* unselect the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
_unselectConnectedEdges(node) {
for (var i = 0; i < node.edges.length; i++) {
var edge = node.edges[i];
edge.unselect();
this._removeFromSelection(edge);
}
}
/**
* This is called when someone clicks on a node. either select or deselect it.
* If there is an existing selection and we don't want to append to it, clear the existing selection
*
* @param {Node || Edge} object
* @private
*/
_blurObject(object) {
if (object.hover == true) {
object.hover = false;
this.body.emitter.emit("blurNode",{node:object.id});
}
}
/**
* This is called when someone clicks on a node. either select or deselect it.
* If there is an existing selection and we don't want to append to it, clear the existing selection
*
* @param {Node || Edge} object
* @private
*/
_hoverObject(object) {
if (object.hover == false) {
object.hover = true;
this._addToHover(object);
if (object instanceof Node) {
this.body.emitter.emit("hoverNode",{node:object.id});
}
}
if (object instanceof Node) {
this._hoverConnectedEdges(object);
}
}
/**
* handles the selection part of the double tap and opens a cluster if needed
*
* @param {Object} pointer
* @private
*/
_handleDoubleTap(pointer) {
var node = this._getNodeAt(pointer);
if (node != null && node !== undefined) {
// we reset the areaCenter here so the opening of the node will occur
this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
"y" : this._YconvertDOMtoCanvas(pointer.y)};
this.openCluster(node);
}
var properties = this.getSelection();
properties['pointer'] = {
DOM: {x: pointer.x, y: pointer.y},
canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)}
}
this.body.emitter.emit("doubleClick", properties);
}
/**
* Handle the onHold selection part
*
* @param pointer
* @private
*/
_handleOnHold(pointer) {
var node = this._getNodeAt(pointer);
if (node != null) {
this._selectObject(node,true);
}
else {
var edge = this._getEdgeAt(pointer);
if (edge != null) {
this._selectObject(edge,true);
}
}
this._requestRedraw();
}
/**
*
* retrieve the currently selected objects
* @return {{nodes: Array.<String>, edges: Array.<String>}} selection
*/
getSelection() {
var nodeIds = this.getSelectedNodes();
var edgeIds = this.getSelectedEdges();
return {nodes:nodeIds, edges:edgeIds};
}
/**
*
* retrieve the currently selected nodes
* @return {String[]} selection An array with the ids of the
* selected nodes.
*/
getSelectedNodes() {
var idArray = [];
if (this.options.select == true) {
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
idArray.push(nodeId);
}
}
}
return idArray
}
/**
*
* retrieve the currently selected edges
* @return {Array} selection An array with the ids of the
* selected nodes.
*/
getSelectedEdges() {
var idArray = [];
if (this.options.select == true) {
for (var edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
idArray.push(edgeId);
}
}
}
return idArray;
}
/**
* select zero or more nodes with the option to highlight edges
* @param {Number[] | String[]} selection An array with the ids of the
* selected nodes.
* @param {boolean} [highlightEdges]
*/
selectNodes(selection, highlightEdges) {
var i, iMax, id;
if (!selection || (selection.length == undefined))
throw 'Selection must be an array with ids';
// first unselect any selected node
this._unselectAll(true);
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
var node = this.body.nodes[id];
if (!node) {
throw new RangeError('Node with id "' + id + '" not found');
}
this._selectObject(node,true,true,highlightEdges,true);
}
this.redraw();
}
/**
* select zero or more edges
* @param {Number[] | String[]} selection An array with the ids of the
* selected nodes.
*/
selectEdges(selection) {
var i, iMax, id;
if (!selection || (selection.length == undefined))
throw 'Selection must be an array with ids';
// first unselect any selected node
this._unselectAll(true);
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
var edge = this.body.edges[id];
if (!edge) {
throw new RangeError('Edge with id "' + id + '" not found');
}
this._selectObject(edge,true,true,false,true);
}
this.redraw();
}
/**
* Validate the selection: remove ids of nodes which no longer exist
* @private
*/
_updateSelection() {
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (!this.body.nodes.hasOwnProperty(nodeId)) {
delete this.selectionObj.nodes[nodeId];
}
}
}
for (var edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
if (!this.body.edges.hasOwnProperty(edgeId)) {
delete this.selectionObj.edges[edgeId];
}
}
}
}
}
export {SelectionHandler};

+ 446
- 0
lib/network/modules/components/physics/BarnesHutSolver.js View File

@ -0,0 +1,446 @@
/**
* Created by Alex on 2/23/2015.
*/
class BarnesHutSolver {
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.barnesHutTree;
this.setOptions(options);
}
setOptions(options) {
this.options = options;
}
/**
* This function calculates the forces the nodes apply on eachother based on a gravitational model.
* The Barnes Hut method is used to speed up this N-body simulation.
*
* @private
*/
solve() {
if (this.options.gravitationalConstant != 0) {
var node;
var nodes = this.physicsBody.calculationNodes;
var nodeIndices = this.physicsBody.calculationNodeIndices;
var nodeCount = nodeIndices.length;
// create the tree
var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices);
// for debugging
this.barnesHutTree = barnesHutTree;
// place the nodes one by one recursively
for (var i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
if (node.options.mass > 0) {
// starting with root is irrelevant, it never passes the BarnesHutSolver condition
this._getForceContribution(barnesHutTree.root.children.NW, node);
this._getForceContribution(barnesHutTree.root.children.NE, node);
this._getForceContribution(barnesHutTree.root.children.SW, node);
this._getForceContribution(barnesHutTree.root.children.SE, node);
}
}
}
}
/**
* This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
* If a region contains a single node, we check if it is not itself, then we apply the force.
*
* @param parentBranch
* @param node
* @private
*/
_getForceContribution(parentBranch, node) {
// we get no force contribution from an empty region
if (parentBranch.childrenCount > 0) {
var dx, dy, distance;
// get the distance from the center of mass to the node.
dx = parentBranch.centerOfMass.x - node.x;
dy = parentBranch.centerOfMass.y - node.y;
distance = Math.sqrt(dx * dx + dy * dy);
// BarnesHutSolver condition
// original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed
// calcSize = 1/s --> d * 1/s > 1/theta = passed
if (distance * parentBranch.calcSize > this.options.thetaInverted) {
// duplicate code to reduce function calls to speed up program
if (distance == 0) {
distance = 0.1 * Math.random();
dx = distance;
}
var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
var fx = dx * gravityForce;
var fy = dy * gravityForce;
this.physicsBody.forces[node.id].x += fx;
this.physicsBody.forces[node.id].y += fy;
}
else {
// Did not pass the condition, go into children if available
if (parentBranch.childrenCount == 4) {
this._getForceContribution(parentBranch.children.NW, node);
this._getForceContribution(parentBranch.children.NE, node);
this._getForceContribution(parentBranch.children.SW, node);
this._getForceContribution(parentBranch.children.SE, node);
}
else { // parentBranch must have only one node, if it was empty we wouldnt be here
if (parentBranch.children.data.id != node.id) { // if it is not self
// duplicate code to reduce function calls to speed up program
if (distance == 0) {
distance = 0.5 * Math.random();
dx = distance;
}
var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
var fx = dx * gravityForce;
var fy = dy * gravityForce;
this.physicsBody.forces[node.id].x += fx;
this.physicsBody.forces[node.id].y += fy;
}
}
}
}
}
/**
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
*
* @param nodes
* @param nodeIndices
* @private
*/
_formBarnesHutTree(nodes, nodeIndices) {
var node;
var nodeCount = nodeIndices.length;
var minX = 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++) {
var x = nodes[nodeIndices[i]].x;
var y = nodes[nodeIndices[i]].y;
if (nodes[nodeIndices[i]].options.mass > 0) {
if (x < minX) {
minX = x;
}
if (x > maxX) {
maxX = x;
}
if (y < minY) {
minY = y;
}
if (y > maxY) {
maxY = y;
}
}
}
// make the range a square
var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
if (sizeDiff > 0) {
minY -= 0.5 * sizeDiff;
maxY += 0.5 * sizeDiff;
} // xSize > ySize
else {
minX += 0.5 * sizeDiff;
maxX -= 0.5 * sizeDiff;
} // xSize < ySize
var minimumTreeSize = 1e-5;
var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX));
var halfRootSize = 0.5 * rootSize;
var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
// construct the barnesHutTree
var barnesHutTree = {
root: {
centerOfMass: {x: 0, y: 0},
mass: 0,
range: {
minX: centerX - halfRootSize, maxX: centerX + halfRootSize,
minY: centerY - halfRootSize, maxY: centerY + halfRootSize
},
size: rootSize,
calcSize: 1 / rootSize,
children: {data: null},
maxWidth: 0,
level: 0,
childrenCount: 4
}
};
this._splitBranch(barnesHutTree.root);
// place the nodes one by one recursively
for (i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
if (node.options.mass > 0) {
this._placeInTree(barnesHutTree.root, node);
}
}
// make global
return barnesHutTree
}
/**
* this updates the mass of a branch. this is increased by adding a node.
*
* @param parentBranch
* @param node
* @private
*/
_updateBranchMass(parentBranch, node) {
var totalMass = parentBranch.mass + node.options.mass;
var totalMassInv = 1 / totalMass;
parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
parentBranch.centerOfMass.x *= totalMassInv;
parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
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;
}
/**
* determine in which branch the node will be placed.
*
* @param parentBranch
* @param node
* @param skipMassUpdate
* @private
*/
_placeInTree(parentBranch, node, skipMassUpdate) {
if (skipMassUpdate != true || skipMassUpdate === undefined) {
// update the mass of the branch.
this._updateBranchMass(parentBranch, node);
}
if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
if (parentBranch.children.NW.range.maxY > node.y) { // in NW
this._placeInRegion(parentBranch, node, "NW");
}
else { // in SW
this._placeInRegion(parentBranch, node, "SW");
}
}
else { // in NE or SE
if (parentBranch.children.NW.range.maxY > node.y) { // in NE
this._placeInRegion(parentBranch, node, "NE");
}
else { // in SE
this._placeInRegion(parentBranch, node, "SE");
}
}
}
/**
* actually place the node in a region (or branch)
*
* @param parentBranch
* @param node
* @param region
* @private
*/
_placeInRegion(parentBranch, node, region) {
switch (parentBranch.children[region].childrenCount) {
case 0: // place node here
parentBranch.children[region].children.data = node;
parentBranch.children[region].childrenCount = 1;
this._updateBranchMass(parentBranch.children[region], node);
break;
case 1: // convert into children
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
// we move one node a pixel and we do not put it in the tree.
if (parentBranch.children[region].children.data.x == node.x &&
parentBranch.children[region].children.data.y == node.y) {
node.x += Math.random();
node.y += Math.random();
}
else {
this._splitBranch(parentBranch.children[region]);
this._placeInTree(parentBranch.children[region], node);
}
break;
case 4: // place in branch
this._placeInTree(parentBranch.children[region], node);
break;
}
}
/**
* this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
* after the split is complete.
*
* @param parentBranch
* @private
*/
_splitBranch(parentBranch) {
// if the branch is shaded with a node, replace the node in the new subset.
var containedNode = null;
if (parentBranch.childrenCount == 1) {
containedNode = parentBranch.children.data;
parentBranch.mass = 0;
parentBranch.centerOfMass.x = 0;
parentBranch.centerOfMass.y = 0;
}
parentBranch.childrenCount = 4;
parentBranch.children.data = null;
this._insertRegion(parentBranch, "NW");
this._insertRegion(parentBranch, "NE");
this._insertRegion(parentBranch, "SW");
this._insertRegion(parentBranch, "SE");
if (containedNode != null) {
this._placeInTree(parentBranch, containedNode);
}
}
/**
* This function subdivides the region into four new segments.
* Specifically, this inserts a single new segment.
* It fills the children section of the parentBranch
*
* @param parentBranch
* @param region
* @param parentRange
* @private
*/
_insertRegion(parentBranch, region) {
var minX, maxX, minY, maxY;
var childSize = 0.5 * parentBranch.size;
switch (region) {
case "NW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + childSize;
break;
case "NE":
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + childSize;
break;
case "SW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
case "SE":
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
}
parentBranch.children[region] = {
centerOfMass: {x: 0, y: 0},
mass: 0,
range: {minX: minX, maxX: maxX, minY: minY, maxY: maxY},
size: 0.5 * parentBranch.size,
calcSize: 2 * parentBranch.calcSize,
children: {data: null},
maxWidth: 0,
level: parentBranch.level + 1,
childrenCount: 0
};
}
//--------------------------- DEBUGGING BELOW ---------------------------//
/**
* This function is for debugging purposed, it draws the tree.
*
* @param ctx
* @param color
* @private
*/
_debug(ctx, color) {
if (this.barnesHutTree !== undefined) {
ctx.lineWidth = 1;
this._drawBranch(this.barnesHutTree.root, ctx, color);
}
}
/**
* This function is for debugging purposes. It draws the branches recursively.
*
* @param branch
* @param ctx
* @param color
* @private
*/
_drawBranch(branch, ctx, color) {
if (color === undefined) {
color = "#FF0000";
}
if (branch.childrenCount == 4) {
this._drawBranch(branch.children.NW, ctx);
this._drawBranch(branch.children.NE, ctx);
this._drawBranch(branch.children.SE, ctx);
this._drawBranch(branch.children.SW, ctx);
}
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(branch.range.minX, branch.range.minY);
ctx.lineTo(branch.range.maxX, branch.range.minY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.maxX, branch.range.minY);
ctx.lineTo(branch.range.maxX, branch.range.maxY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.maxX, branch.range.maxY);
ctx.lineTo(branch.range.minX, branch.range.maxY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.minX, branch.range.maxY);
ctx.lineTo(branch.range.minX, branch.range.minY);
ctx.stroke();
/*
if (branch.mass > 0) {
ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
ctx.stroke();
}
*/
}
}
export {BarnesHutSolver};

+ 40
- 0
lib/network/modules/components/physics/CentralGravitySolver.js View File

@ -0,0 +1,40 @@
/**
* Created by Alex on 2/23/2015.
*/
class CentralGravitySolver {
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.setOptions(options);
}
setOptions(options) {
this.options = options;
}
solve() {
var dx, dy, distance, node, i;
var nodes = this.physicsBody.calculationNodes;
var nodeIndices = this.physicsBody.calculationNodeIndices;
var forces = this.physicsBody.forces;
var gravity = this.options.centralGravity;
var gravityForce = 0;
for (i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
node = nodes[nodeId];
dx = -node.x;
dy = -node.y;
distance = Math.sqrt(dx * dx + dy * dy);
gravityForce = (distance == 0) ? 0 : (gravity / distance);
forces[nodeId].x = dx * gravityForce;
forces[nodeId].y = dy * gravityForce;
}
}
}
export {CentralGravitySolver};

+ 73
- 0
lib/network/modules/components/physics/HierarchicalRepulsionSolver.js View File

@ -0,0 +1,73 @@
/**
* Created by Alex on 2/23/2015.
*/
class HierarchicalRepulsionSolver {
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.setOptions(options);
}
setOptions(options) {
this.options = options;
}
/**
* Calculate the forces the nodes apply on each other based on a repulsion field.
* This field is linearly approximated.
*
* @private
*/
solve() {
var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j;
var nodes = this.physicsBody.calculationNodes;
var nodeIndices = this.physicsBody.calculationNodeIndices;
var forces = this.physicsBody.forces;
// repulsing forces between nodes
var nodeDistance = this.options.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]];
// nodes only affect nodes on their level
if (node1.level == node2.level) {
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
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;
}
else {
repulsingForce = repulsingForce / distance;
}
fx = dx * repulsingForce;
fy = dy * repulsingForce;
forces[node1.id].x -= fx;
forces[node1.id].y -= fy;
forces[node2.id].x += fx;
forces[node2.id].y += fy;
}
}
}
}
}
export {HierarchicalRepulsionSolver};

+ 104
- 0
lib/network/modules/components/physics/HierarchicalSpringSolver.js View File

@ -0,0 +1,104 @@
/**
* Created by Alex on 2/25/2015.
*/
class HierarchicalSpringSolver {
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.setOptions(options);
}
setOptions(options) {
this.options = options;
}
/**
* This function calculates the springforces on the nodes, accounting for the support nodes.
*
* @private
*/
solve() {
var edgeLength, edge, edgeId;
var dx, dy, fx, fy, springForce, distance;
var edges = this.body.edges;
var nodeIndices = this.physicsBody.calculationNodeIndices;
var forces = this.physicsBody.forces;
// initialize the spring force counters
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
forces[nodeId].springFx = 0;
forces[nodeId].springFy = 0;
}
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected === true) {
edgeLength = edge.properties.length === undefined ? this.options.springLength : edge.properties.length;
dx = (edge.from.x - edge.to.x);
dy = (edge.from.y - edge.to.y);
distance = Math.sqrt(dx * dx + dy * dy);
distance = distance == 0 ? 0.01 : distance;
// the 1/distance is so the fx and fy can be calculated without sine or cosine.
springForce = this.options.springConstant * (edgeLength - distance) / distance;
fx = dx * springForce;
fy = dy * springForce;
if (edge.to.level != edge.from.level) {
forces[edge.toId].springFx -= fx;
forces[edge.toId].springFy -= fy;
forces[edge.fromId].springFx += fx;
forces[edge.fromId].springFy += fy;
}
else {
let factor = 0.5;
forces[edge.toId].x -= factor*fx;
forces[edge.toId].y -= factor*fy;
forces[edge.fromId].x += factor*fx;
forces[edge.fromId].y += factor*fy;
}
}
}
}
// normalize spring forces
var springForce = 1;
var springFx, springFy;
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
springFx = Math.min(springForce,Math.max(-springForce,forces[nodeId].springFx));
springFy = Math.min(springForce,Math.max(-springForce,forces[nodeId].springFy));
forces[nodeId].x += springFx;
forces[nodeId].y += springFy;
}
// retain energy balance
var totalFx = 0;
var totalFy = 0;
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
totalFx += forces[nodeId].x;
totalFy += forces[nodeId].y;
}
var correctionFx = totalFx / nodeIndices.length;
var correctionFy = totalFy / nodeIndices.length;
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
forces[nodeId].x -= correctionFx;
forces[nodeId].y -= correctionFy;
}
}
}
export {HierarchicalSpringSolver};

+ 75
- 0
lib/network/modules/components/physics/RepulsionSolver.js View File

@ -0,0 +1,75 @@
/**
* Created by Alex on 2/23/2015.
*/
class RepulsionSolver {
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.setOptions(options);
}
setOptions(options) {
this.options = options;
}
/**
* Calculate the forces the nodes apply on each other based on a repulsion field.
* This field is linearly approximated.
*
* @private
*/
solve() {
var dx, dy, distance, fx, fy, repulsingForce, node1, node2;
var nodes = this.physicsBody.calculationNodes;
var nodeIndices = this.physicsBody.calculationNodeIndices;
var forces = this.physicsBody.forces;
// repulsing forces between nodes
var nodeDistance = this.options.nodeDistance;
// approximation constants
var a = (-2 / 3) / nodeDistance;
var b = 4 / 3;
// 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 (let i = 0; i < nodeIndices.length - 1; i++) {
node1 = nodes[nodeIndices[i]];
for (let 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);
// same condition as BarnesHutSolver, making sure nodes are never 100% overlapping.
if (distance == 0) {
distance = 0.1*Math.random();
dx = distance;
}
if (distance < 2 * nodeDistance) {
if (distance < 0.5 * nodeDistance) {
repulsingForce = 1.0;
}
else {
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness))
}
repulsingForce = repulsingForce / distance;
fx = dx * repulsingForce;
fy = dy * repulsingForce;
forces[node1.id].x -= fx;
forces[node1.id].y -= fy;
forces[node2.id].x += fx;
forces[node2.id].y += fy;
}
}
}
}
}
export {RepulsionSolver};

+ 80
- 0
lib/network/modules/components/physics/SpringSolver.js View File

@ -0,0 +1,80 @@
/**
* Created by Alex on 2/23/2015.
*/
class SpringSolver {
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.setOptions(options);
}
setOptions(options) {
this.options = options;
}
/**
* This function calculates the springforces on the nodes, accounting for the support nodes.
*
* @private
*/
solve() {
var edgeLength, edge, edgeId;
var edges = this.body.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected === true) {
// only calculate forces if nodes are in the same sector
if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) {
edgeLength = edge.properties.length === undefined ? this.options.springLength : edge.properties.length;
if (edge.via != null) {
var node1 = edge.to;
var node2 = edge.via;
var node3 = edge.from;
this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
}
else {
this._calculateSpringForce(edge.from, edge.to, edgeLength);
}
}
}
}
}
}
/**
* This is the code actually performing the calculation for the function above.
*
* @param node1
* @param node2
* @param edgeLength
* @private
*/
_calculateSpringForce(node1, node2, edgeLength) {
var dx, dy, fx, fy, springForce, distance;
dx = (node1.x - node2.x);
dy = (node1.y - node2.y);
distance = Math.sqrt(dx * dx + dy * dy);
distance = distance == 0 ? 0.01 : distance;
// the 1/distance is so the fx and fy can be calculated without sine or cosine.
springForce = this.options.springConstant * (edgeLength - distance) / distance;
fx = dx * springForce;
fy = dy * springForce;
this.physicsBody.forces[node1.id].x += fx;
this.physicsBody.forces[node1.id].y += fy;
this.physicsBody.forces[node2.id].x -= fx;
this.physicsBody.forces[node2.id].y -= fy;
}
}
export {SpringSolver};

+ 3
- 9
lib/timeline/Core.js View File

@ -229,9 +229,7 @@ Core.prototype.setOptions = function (options) {
}
// propagate options to all components
this.components.forEach(function (component) {
component.setOptions(options);
});
this.components.forEach(component => component.setOptions(options));
// TODO: remove deprecation error one day (deprecated since version 0.8.0)
if (options && options.order) {
@ -285,9 +283,7 @@ Core.prototype.destroy = function () {
this.hammer = null;
// give all components the opportunity to cleanup
this.components.forEach(function (component) {
component.destroy();
});
this.components.forEach(component => component.destroy());
this.body = null;
};
@ -449,9 +445,7 @@ Core.prototype.clear = function(what) {
// clear options of timeline and of each of the components
if (!what || what.options) {
this.components.forEach(function (component) {
component.setOptions(component.defaultOptions);
});
this.components.forEach(component => component.setOptions(component.defaultOptions));
this.setOptions(this.defaultOptions); // this will also do a redraw
}

+ 2
- 2
lib/timeline/TimeStep.js View File

@ -576,9 +576,9 @@ TimeStep.prototype.getClassName = function() {
case 'hour':
var hours = date.hours();
if (this.step == 4) {
hours = hours + '-' + (hours + 4);
hours = hours + '-h' + (hours + 4);
}
return hours + 'h' + today(date) + even(date.hours());
return 'h' + hours + today(date) + even(date.hours());
case 'weekday':
return date.format('dddd').toLowerCase() +

+ 1
- 1
lib/timeline/component/CurrentTime.js View File

@ -122,7 +122,7 @@ CurrentTime.prototype.start = function() {
me.redraw();
// start a timer to adjust for the new time
// start a renderTimer to adjust for the new time
me.currentTimeTimer = setTimeout(update, interval);
}

+ 3
- 5
lib/timeline/component/css/item.css View File

@ -6,7 +6,7 @@
border-width: 1px;
background-color: #D5DDF6;
display: inline-block;
padding: 5px;
overflow: hidden;
}
.vis.timeline .item.selected {
@ -50,7 +50,6 @@
}
.vis.timeline .item.background {
overflow: hidden;
border: none;
background-color: rgba(213, 221, 246, 0.4);
box-sizing: border-box;
@ -62,13 +61,11 @@
position: relative;
display: inline-block;
max-width: 100%;
overflow: hidden;
}
.vis.timeline .item.background .content {
position: absolute;
display: inline-block;
overflow: hidden;
max-width: 100%;
margin: 5px;
}
@ -83,7 +80,8 @@
.vis.timeline .item .content {
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
padding: 5px;
}
.vis.timeline .item .delete {

+ 4
- 2
lib/timeline/component/graph2d_types/bar.js View File

@ -58,7 +58,8 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) {
combinedData.push({
x: processedGroupData[groupIds[i]][j].x,
y: processedGroupData[groupIds[i]][j].y,
groupId: groupIds[i]
groupId: groupIds[i],
label: processedGroupData[groupIds[i]][j].label
});
barPoints += 1;
}
@ -114,7 +115,8 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) {
DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', framework.svgElements, framework.svg);
// draw points
if (group.options.drawPoints.enabled == true) {
DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg);
Points.draw([combinedData[i]], group, framework, drawData.offset);
//DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg);
}
}
};

+ 5
- 3
lib/util.js View File

@ -225,22 +225,24 @@ exports.selectiveNotDeepExtend = function (props, a, b) {
* Deep extend an object a with the properties of object b
* @param {Object} a
* @param {Object} b
* @param {Boolean} protoExtend --> optional parameter. If true, the prototype values will also be extended.
* (ie. the options objects that inherit from others will also get the inherited options)
* @returns {Object}
*/
exports.deepExtend = function(a, b) {
exports.deepExtend = function(a, b, protoExtend) {
// TODO: add support for Arrays to deepExtend
if (Array.isArray(b)) {
throw new TypeError('Arrays are not supported by deepExtend');
}
for (var prop in b) {
if (b.hasOwnProperty(prop)) {
if (b.hasOwnProperty(prop) || protoExtend === true) {
if (b[prop] && b[prop].constructor === Object) {
if (a[prop] === undefined) {
a[prop] = {};
}
if (a[prop].constructor === Object) {
exports.deepExtend(a[prop], b[prop]);
exports.deepExtend(a[prop], b[prop], protoExtend);
}
else {
a[prop] = b[prop];

+ 4
- 1
package.json View File

@ -1,6 +1,6 @@
{
"name": "vis",
"version": "3.10.1-SNAPSHOT",
"version": "4.0.0-SNAPSHOT",
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",
"repository": {
@ -35,6 +35,9 @@
"propagating-hammerjs": "^1.2.0"
},
"devDependencies": {
"6to5": "^3.5.3",
"6to5-loader": "^3.0.0",
"6to5ify": "^4.1.0",
"clean-css": "latest",
"gulp": "^3.8.11",
"gulp-concat": "^2.4.3",

+ 5
- 3
test/DataSet.test.js View File

@ -1,7 +1,9 @@
var assert = require('assert');
var moment = require('moment');
var DataSet = require('../lib/DataSet');
var Queue = require('../lib/Queue');
var vis = require('../dist/vis');
var moment = vis.moment;
var DataSet = vis.DataSet;
var Queue = vis.Queue;
// TODO: test the source code immediately, but this is ES6
var now = new Date();

+ 5
- 3
test/DataView.test.js View File

@ -1,7 +1,9 @@
var assert = require('assert');
var moment = require('moment');
var DataSet = require('../lib/DataSet');
var DataView = require('../lib/DataView');
var vis = require('../dist/vis');
var moment = vis.moment;
var DataSet = vis.DataSet;
var DataView = vis.DataView;
// TODO: test the source code immediately, but this is ES6
// TODO: improve DataView tests, split up in one test per function
describe('DataView', function () {

Loading…
Cancel
Save