Browse Source

Added Barnes-Hut force model. Main slowdown is in the drawing now..

css_transitions
Alex de Mulder 10 years ago
parent
commit
984f2efafb
13 changed files with 1622 additions and 521 deletions
  1. +2
    -1
      Jakefile.js
  2. +619
    -256
      dist/vis.js
  3. +8
    -8
      dist/vis.min.js
  4. +1
    -0
      examples/graph/02_random_nodes.html
  5. +1
    -1
      examples/graph/21_data_manipulation.html
  6. +373
    -0
      examples/graph/22_les_miserables.html
  7. +1
    -1
      src/graph/ClusterMixin.js
  8. +4
    -4
      src/graph/Edge.js
  9. +41
    -235
      src/graph/Graph.js
  10. +27
    -5
      src/graph/Node.js
  11. +3
    -9
      src/graph/SelectionMixin.js
  12. +3
    -1
      src/graph/manipulationMixin.js
  13. +539
    -0
      src/graph/physicsMixin.js

+ 2
- 1
Jakefile.js View File

@ -82,7 +82,8 @@ task('build', {async: true}, function () {
'./src/graph/Popup.js', './src/graph/Popup.js',
'./src/graph/Groups.js', './src/graph/Groups.js',
'./src/graph/Images.js', './src/graph/Images.js',
'./src/graph/manipulationMixin.js',
'./src/graph/PhysicsMixin.js',
'./src/graph/ManipulationMixin.js',
'./src/graph/SectorsMixin.js', './src/graph/SectorsMixin.js',
'./src/graph/ClusterMixin.js', './src/graph/ClusterMixin.js',
'./src/graph/SelectionMixin.js', './src/graph/SelectionMixin.js',

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


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


+ 1
- 0
examples/graph/02_random_nodes.html View File

@ -57,6 +57,7 @@
j++; j++;
} }
var from = i; var from = i;
var to = j; var to = j;
edges.push({ edges.push({

+ 1
- 1
examples/graph/21_data_manipulation.html View File

@ -220,7 +220,7 @@
length: 50 length: 50
}, },
stabilize: false, stabilize: false,
clustering:true,
clustering:false,
navigation: true, navigation: true,
keyboard: true, keyboard: true,
dataManipulationToolbar: true dataManipulationToolbar: true

+ 373
- 0
examples/graph/22_les_miserables.html View File

@ -0,0 +1,373 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Graph | Multiline text</title>
<style type="text/css">
#mygraph {
width: 900px;
height: 900px;
border: 1px solid lightgray;
}
</style>
<script type="text/javascript" src="../../dist/vis.js"></script>
<script type="text/javascript">
function draw() {
// create some nodes
var nodes = [
{id:0,"labelIGNORE":"Myriel","group":1},
{id:1,"labelIGNORE":"Napoleon","group":1},
{id:2,"labelIGNORE":"Mlle.Baptistine","group":1},
{id:3,"labelIGNORE":"Mme.Magloire","group":1},
{id:4,"labelIGNORE":"CountessdeLo","group":1},
{id:5,"labelIGNORE":"Geborand","group":1},
{id:6,"labelIGNORE":"Champtercier","group":1},
{id:7,"labelIGNORE":"Cravatte","group":1},
{id:8,"labelIGNORE":"Count","group":1},
{id:9,"labelIGNORE":"OldMan","group":1},
{id:10,"labelIGNORE":"Labarre","group":2},
{id:11,"labelIGNORE":"Valjean","group":2},
{id:12,"labelIGNORE":"Marguerite","group":3},
{id:13,"labelIGNORE":"Mme.deR","group":2},
{id:14,"labelIGNORE":"Isabeau","group":2},
{id:15,"labelIGNORE":"Gervais","group":2},
{id:16,"labelIGNORE":"Tholomyes","group":3},
{id:17,"labelIGNORE":"Listolier","group":3},
{id:18,"labelIGNORE":"Fameuil","group":3},
{id:19,"labelIGNORE":"Blacheville","group":3},
{id:20,"labelIGNORE":"Favourite","group":3},
{id:21,"labelIGNORE":"Dahlia","group":3},
{id:22,"labelIGNORE":"Zephine","group":3},
{id:23,"labelIGNORE":"Fantine","group":3},
{id:24,"labelIGNORE":"Mme.Thenardier","group":4},
{id:25,"labelIGNORE":"Thenardier","group":4},
{id:26,"labelIGNORE":"Cosette","group":5},
{id:27,"labelIGNORE":"Javert","group":4},
{id:28,"labelIGNORE":"Fauchelevent","group":0},
{id:29,"labelIGNORE":"Bamatabois","group":2},
{id:30,"labelIGNORE":"Perpetue","group":3},
{id:31,"labelIGNORE":"Simplice","group":2},
{id:32,"labelIGNORE":"Scaufflaire","group":2},
{id:33,"labelIGNORE":"Woman1","group":2},
{id:34,"labelIGNORE":"Judge","group":2},
{id:35,"labelIGNORE":"Champmathieu","group":2},
{id:36,"labelIGNORE":"Brevet","group":2},
{id:37,"labelIGNORE":"Chenildieu","group":2},
{id:38,"labelIGNORE":"Cochepaille","group":2},
{id:39,"labelIGNORE":"Pontmercy","group":4},
{id:40,"labelIGNORE":"Boulatruelle","group":6},
{id:41,"labelIGNORE":"Eponine","group":4},
{id:42,"labelIGNORE":"Anzelma","group":4},
{id:43,"labelIGNORE":"Woman2","group":5},
{id:44,"labelIGNORE":"MotherInnocent","group":0},
{id:45,"labelIGNORE":"Gribier","group":0},
{id:46,"labelIGNORE":"Jondrette","group":7},
{id:47,"labelIGNORE":"Mme.Burgon","group":7},
{id:48,"labelIGNORE":"Gavroche","group":8},
{id:49,"labelIGNORE":"Gillenormand","group":5},
{id:50,"labelIGNORE":"Magnon","group":5},
{id:51,"labelIGNORE":"Mlle.Gillenormand","group":5},
{id:52,"labelIGNORE":"Mme.Pontmercy","group":5},
{id:53,"labelIGNORE":"Mlle.Vaubois","group":5},
{id:54,"labelIGNORE":"Lt.Gillenormand","group":5},
{id:55,"labelIGNORE":"Marius","group":8},
{id:56,"labelIGNORE":"BaronessT","group":5},
{id:57,"labelIGNORE":"Mabeuf","group":8},
{id:58,"labelIGNORE":"Enjolras","group":8},
{id:59,"labelIGNORE":"Combeferre","group":8},
{id:60,"labelIGNORE":"Prouvaire","group":8},
{id:61,"labelIGNORE":"Feuilly","group":8},
{id:62,"labelIGNORE":"Courfeyrac","group":8},
{id:63,"labelIGNORE":"Bahorel","group":8},
{id:64,"labelIGNORE":"Bossuet","group":8},
{id:65,"labelIGNORE":"Joly","group":8},
{id:66,"labelIGNORE":"Grantaire","group":8},
{id:67,"labelIGNORE":"MotherPlutarch","group":9},
{id:68,"labelIGNORE":"Gueulemer","group":4},
{id:69,"labelIGNORE":"Babet","group":4},
{id:70,"labelIGNORE":"Claquesous","group":4},
{id:71,"labelIGNORE":"Montparnasse","group":4},
{id:72,"labelIGNORE":"Toussaint","group":5},
{id:73,"labelIGNORE":"Child1","group":10},
{id:74,"labelIGNORE":"Child2","group":10},
{id:75,"labelIGNORE":"Brujon","group":4},
{id:76,"labelIGNORE":"Mme.Hucheloup","group":8}
];
// create some edges
var edges = [
{"from":1,"to":0,"valueIGNORED":1},
{"from":2,"to":0,"valueIGNORED":8},
{"from":3,"to":0,"valueIGNORED":10},
{"from":3,"to":2,"valueIGNORED":6},
{"from":4,"to":0,"valueIGNORED":1},
{"from":5,"to":0,"valueIGNORED":1},
{"from":6,"to":0,"valueIGNORED":1},
{"from":7,"to":0,"valueIGNORED":1},
{"from":8,"to":0,"valueIGNORED":2},
{"from":9,"to":0,"valueIGNORED":1},
{"from":11,"to":10,"valueIGNORED":1},
{"from":11,"to":3,"valueIGNORED":3},
{"from":11,"to":2,"valueIGNORED":3},
{"from":11,"to":0,"valueIGNORED":5},
{"from":12,"to":11,"valueIGNORED":1},
{"from":13,"to":11,"valueIGNORED":1},
{"from":14,"to":11,"valueIGNORED":1},
{"from":15,"to":11,"valueIGNORED":1},
{"from":17,"to":16,"valueIGNORED":4},
{"from":18,"to":16,"valueIGNORED":4},
{"from":18,"to":17,"valueIGNORED":4},
{"from":19,"to":16,"valueIGNORED":4},
{"from":19,"to":17,"valueIGNORED":4},
{"from":19,"to":18,"valueIGNORED":4},
{"from":20,"to":16,"valueIGNORED":3},
{"from":20,"to":17,"valueIGNORED":3},
{"from":20,"to":18,"valueIGNORED":3},
{"from":20,"to":19,"valueIGNORED":4},
{"from":21,"to":16,"valueIGNORED":3},
{"from":21,"to":17,"valueIGNORED":3},
{"from":21,"to":18,"valueIGNORED":3},
{"from":21,"to":19,"valueIGNORED":3},
{"from":21,"to":20,"valueIGNORED":5},
{"from":22,"to":16,"valueIGNORED":3},
{"from":22,"to":17,"valueIGNORED":3},
{"from":22,"to":18,"valueIGNORED":3},
{"from":22,"to":19,"valueIGNORED":3},
{"from":22,"to":20,"valueIGNORED":4},
{"from":22,"to":21,"valueIGNORED":4},
{"from":23,"to":16,"valueIGNORED":3},
{"from":23,"to":17,"valueIGNORED":3},
{"from":23,"to":18,"valueIGNORED":3},
{"from":23,"to":19,"valueIGNORED":3},
{"from":23,"to":20,"valueIGNORED":4},
{"from":23,"to":21,"valueIGNORED":4},
{"from":23,"to":22,"valueIGNORED":4},
{"from":23,"to":12,"valueIGNORED":2},
{"from":23,"to":11,"valueIGNORED":9},
{"from":24,"to":23,"valueIGNORED":2},
{"from":24,"to":11,"valueIGNORED":7},
{"from":25,"to":24,"valueIGNORED":13},
{"from":25,"to":23,"valueIGNORED":1},
{"from":25,"to":11,"valueIGNORED":12},
{"from":26,"to":24,"valueIGNORED":4},
{"from":26,"to":11,"valueIGNORED":31},
{"from":26,"to":16,"valueIGNORED":1},
{"from":26,"to":25,"valueIGNORED":1},
{"from":27,"to":11,"valueIGNORED":17},
{"from":27,"to":23,"valueIGNORED":5},
{"from":27,"to":25,"valueIGNORED":5},
{"from":27,"to":24,"valueIGNORED":1},
{"from":27,"to":26,"valueIGNORED":1},
{"from":28,"to":11,"valueIGNORED":8},
{"from":28,"to":27,"valueIGNORED":1},
{"from":29,"to":23,"valueIGNORED":1},
{"from":29,"to":27,"valueIGNORED":1},
{"from":29,"to":11,"valueIGNORED":2},
{"from":30,"to":23,"valueIGNORED":1},
{"from":31,"to":30,"valueIGNORED":2},
{"from":31,"to":11,"valueIGNORED":3},
{"from":31,"to":23,"valueIGNORED":2},
{"from":31,"to":27,"valueIGNORED":1},
{"from":32,"to":11,"valueIGNORED":1},
{"from":33,"to":11,"valueIGNORED":2},
{"from":33,"to":27,"valueIGNORED":1},
{"from":34,"to":11,"valueIGNORED":3},
{"from":34,"to":29,"valueIGNORED":2},
{"from":35,"to":11,"valueIGNORED":3},
{"from":35,"to":34,"valueIGNORED":3},
{"from":35,"to":29,"valueIGNORED":2},
{"from":36,"to":34,"valueIGNORED":2},
{"from":36,"to":35,"valueIGNORED":2},
{"from":36,"to":11,"valueIGNORED":2},
{"from":36,"to":29,"valueIGNORED":1},
{"from":37,"to":34,"valueIGNORED":2},
{"from":37,"to":35,"valueIGNORED":2},
{"from":37,"to":36,"valueIGNORED":2},
{"from":37,"to":11,"valueIGNORED":2},
{"from":37,"to":29,"valueIGNORED":1},
{"from":38,"to":34,"valueIGNORED":2},
{"from":38,"to":35,"valueIGNORED":2},
{"from":38,"to":36,"valueIGNORED":2},
{"from":38,"to":37,"valueIGNORED":2},
{"from":38,"to":11,"valueIGNORED":2},
{"from":38,"to":29,"valueIGNORED":1},
{"from":39,"to":25,"valueIGNORED":1},
{"from":40,"to":25,"valueIGNORED":1},
{"from":41,"to":24,"valueIGNORED":2},
{"from":41,"to":25,"valueIGNORED":3},
{"from":42,"to":41,"valueIGNORED":2},
{"from":42,"to":25,"valueIGNORED":2},
{"from":42,"to":24,"valueIGNORED":1},
{"from":43,"to":11,"valueIGNORED":3},
{"from":43,"to":26,"valueIGNORED":1},
{"from":43,"to":27,"valueIGNORED":1},
{"from":44,"to":28,"valueIGNORED":3},
{"from":44,"to":11,"valueIGNORED":1},
{"from":45,"to":28,"valueIGNORED":2},
{"from":47,"to":46,"valueIGNORED":1},
{"from":48,"to":47,"valueIGNORED":2},
{"from":48,"to":25,"valueIGNORED":1},
{"from":48,"to":27,"valueIGNORED":1},
{"from":48,"to":11,"valueIGNORED":1},
{"from":49,"to":26,"valueIGNORED":3},
{"from":49,"to":11,"valueIGNORED":2},
{"from":50,"to":49,"valueIGNORED":1},
{"from":50,"to":24,"valueIGNORED":1},
{"from":51,"to":49,"valueIGNORED":9},
{"from":51,"to":26,"valueIGNORED":2},
{"from":51,"to":11,"valueIGNORED":2},
{"from":52,"to":51,"valueIGNORED":1},
{"from":52,"to":39,"valueIGNORED":1},
{"from":53,"to":51,"valueIGNORED":1},
{"from":54,"to":51,"valueIGNORED":2},
{"from":54,"to":49,"valueIGNORED":1},
{"from":54,"to":26,"valueIGNORED":1},
{"from":55,"to":51,"valueIGNORED":6},
{"from":55,"to":49,"valueIGNORED":12},
{"from":55,"to":39,"valueIGNORED":1},
{"from":55,"to":54,"valueIGNORED":1},
{"from":55,"to":26,"valueIGNORED":21},
{"from":55,"to":11,"valueIGNORED":19},
{"from":55,"to":16,"valueIGNORED":1},
{"from":55,"to":25,"valueIGNORED":2},
{"from":55,"to":41,"valueIGNORED":5},
{"from":55,"to":48,"valueIGNORED":4},
{"from":56,"to":49,"valueIGNORED":1},
{"from":56,"to":55,"valueIGNORED":1},
{"from":57,"to":55,"valueIGNORED":1},
{"from":57,"to":41,"valueIGNORED":1},
{"from":57,"to":48,"valueIGNORED":1},
{"from":58,"to":55,"valueIGNORED":7},
{"from":58,"to":48,"valueIGNORED":7},
{"from":58,"to":27,"valueIGNORED":6},
{"from":58,"to":57,"valueIGNORED":1},
{"from":58,"to":11,"valueIGNORED":4},
{"from":59,"to":58,"valueIGNORED":15},
{"from":59,"to":55,"valueIGNORED":5},
{"from":59,"to":48,"valueIGNORED":6},
{"from":59,"to":57,"valueIGNORED":2},
{"from":60,"to":48,"valueIGNORED":1},
{"from":60,"to":58,"valueIGNORED":4},
{"from":60,"to":59,"valueIGNORED":2},
{"from":61,"to":48,"valueIGNORED":2},
{"from":61,"to":58,"valueIGNORED":6},
{"from":61,"to":60,"valueIGNORED":2},
{"from":61,"to":59,"valueIGNORED":5},
{"from":61,"to":57,"valueIGNORED":1},
{"from":61,"to":55,"valueIGNORED":1},
{"from":62,"to":55,"valueIGNORED":9},
{"from":62,"to":58,"valueIGNORED":17},
{"from":62,"to":59,"valueIGNORED":13},
{"from":62,"to":48,"valueIGNORED":7},
{"from":62,"to":57,"valueIGNORED":2},
{"from":62,"to":41,"valueIGNORED":1},
{"from":62,"to":61,"valueIGNORED":6},
{"from":62,"to":60,"valueIGNORED":3},
{"from":63,"to":59,"valueIGNORED":5},
{"from":63,"to":48,"valueIGNORED":5},
{"from":63,"to":62,"valueIGNORED":6},
{"from":63,"to":57,"valueIGNORED":2},
{"from":63,"to":58,"valueIGNORED":4},
{"from":63,"to":61,"valueIGNORED":3},
{"from":63,"to":60,"valueIGNORED":2},
{"from":63,"to":55,"valueIGNORED":1},
{"from":64,"to":55,"valueIGNORED":5},
{"from":64,"to":62,"valueIGNORED":12},
{"from":64,"to":48,"valueIGNORED":5},
{"from":64,"to":63,"valueIGNORED":4},
{"from":64,"to":58,"valueIGNORED":10},
{"from":64,"to":61,"valueIGNORED":6},
{"from":64,"to":60,"valueIGNORED":2},
{"from":64,"to":59,"valueIGNORED":9},
{"from":64,"to":57,"valueIGNORED":1},
{"from":64,"to":11,"valueIGNORED":1},
{"from":65,"to":63,"valueIGNORED":5},
{"from":65,"to":64,"valueIGNORED":7},
{"from":65,"to":48,"valueIGNORED":3},
{"from":65,"to":62,"valueIGNORED":5},
{"from":65,"to":58,"valueIGNORED":5},
{"from":65,"to":61,"valueIGNORED":5},
{"from":65,"to":60,"valueIGNORED":2},
{"from":65,"to":59,"valueIGNORED":5},
{"from":65,"to":57,"valueIGNORED":1},
{"from":65,"to":55,"valueIGNORED":2},
{"from":66,"to":64,"valueIGNORED":3},
{"from":66,"to":58,"valueIGNORED":3},
{"from":66,"to":59,"valueIGNORED":1},
{"from":66,"to":62,"valueIGNORED":2},
{"from":66,"to":65,"valueIGNORED":2},
{"from":66,"to":48,"valueIGNORED":1},
{"from":66,"to":63,"valueIGNORED":1},
{"from":66,"to":61,"valueIGNORED":1},
{"from":66,"to":60,"valueIGNORED":1},
{"from":67,"to":57,"valueIGNORED":3},
{"from":68,"to":25,"valueIGNORED":5},
{"from":68,"to":11,"valueIGNORED":1},
{"from":68,"to":24,"valueIGNORED":1},
{"from":68,"to":27,"valueIGNORED":1},
{"from":68,"to":48,"valueIGNORED":1},
{"from":68,"to":41,"valueIGNORED":1},
{"from":69,"to":25,"valueIGNORED":6},
{"from":69,"to":68,"valueIGNORED":6},
{"from":69,"to":11,"valueIGNORED":1},
{"from":69,"to":24,"valueIGNORED":1},
{"from":69,"to":27,"valueIGNORED":2},
{"from":69,"to":48,"valueIGNORED":1},
{"from":69,"to":41,"valueIGNORED":1},
{"from":70,"to":25,"valueIGNORED":4},
{"from":70,"to":69,"valueIGNORED":4},
{"from":70,"to":68,"valueIGNORED":4},
{"from":70,"to":11,"valueIGNORED":1},
{"from":70,"to":24,"valueIGNORED":1},
{"from":70,"to":27,"valueIGNORED":1},
{"from":70,"to":41,"valueIGNORED":1},
{"from":70,"to":58,"valueIGNORED":1},
{"from":71,"to":27,"valueIGNORED":1},
{"from":71,"to":69,"valueIGNORED":2},
{"from":71,"to":68,"valueIGNORED":2},
{"from":71,"to":70,"valueIGNORED":2},
{"from":71,"to":11,"valueIGNORED":1},
{"from":71,"to":48,"valueIGNORED":1},
{"from":71,"to":41,"valueIGNORED":1},
{"from":71,"to":25,"valueIGNORED":1},
{"from":72,"to":26,"valueIGNORED":2},
{"from":72,"to":27,"valueIGNORED":1},
{"from":72,"to":11,"valueIGNORED":1},
{"from":73,"to":48,"valueIGNORED":2},
{"from":74,"to":48,"valueIGNORED":2},
{"from":74,"to":73,"valueIGNORED":3},
{"from":75,"to":69,"valueIGNORED":3},
{"from":75,"to":68,"valueIGNORED":3},
{"from":75,"to":25,"valueIGNORED":3},
{"from":75,"to":48,"valueIGNORED":1},
{"from":75,"to":41,"valueIGNORED":1},
{"from":75,"to":70,"valueIGNORED":1},
{"from":75,"to":71,"valueIGNORED":1},
{"from":76,"to":64,"valueIGNORED":1},
{"from":76,"to":65,"valueIGNORED":1},
{"from":76,"to":66,"valueIGNORED":1},
{"from":76,"to":63,"valueIGNORED":1},
{"from":76,"to":62,"valueIGNORED":1},
{"from":76,"to":48,"valueIGNORED":1},
{"from":76,"to":58,"valueIGNORED":1}
];
// create a graph
var container = document.getElementById('mygraph');
var data = {
nodes: nodes,
edges: edges
};
var options = {stabilize: false};
var graph = new vis.Graph(container, data, options);
}
</script>
</head>
<body onload="draw()">
<div id="mygraph"></div>
</body>
</html>

+ 1
- 1
src/graph/ClusterMixin.js View File

@ -654,7 +654,7 @@ var ClusterMixin = {
/** /**
* This function will apply the changes made to the remainingEdges during the formation of the clusters. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
* This is a seperate function to allow for level-wise collapsing of the node tree.
* This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
* It has to be called if a level is collapsed. It is called by _formClusters(). * It has to be called if a level is collapsed. It is called by _formClusters().
* @private * @private
*/ */

+ 4
- 4
src/graph/Edge.js View File

@ -82,10 +82,10 @@ Edge.prototype.setProperties = function(properties, constants) {
if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;} if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;} if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
} }
if (properties.title !== undefined) {this.title = properties.title;}
if (properties.width !== undefined) {this.width = properties.width;}
if (properties.value !== undefined) {this.value = properties.value;}
if (properties.length !== undefined) {this.length = properties.length;}
if (properties.title !== undefined) {this.title = properties.title;}
if (properties.width !== undefined) {this.width = properties.width;}
if (properties.value !== undefined) {this.value = properties.value;}
if (properties.length !== undefined) {this.length = properties.length;}
// Added to support dashed lines // Added to support dashed lines
// David Jordan // David Jordan

+ 41
- 235
src/graph/Graph.js View File

@ -20,15 +20,13 @@ function Graph (container, data, options) {
this.stabilize = true; // stabilize before displaying the graph this.stabilize = true; // stabilize before displaying the graph
this.selectable = true; this.selectable = true;
this.forceFactor = 50000;
// set constant values // set constant values
this.constants = { this.constants = {
nodes: { nodes: {
radiusMin: 5, radiusMin: 5,
radiusMax: 20, radiusMax: 20,
radius: 5, radius: 5,
distance: 100, // px
distance: 100, // px
shape: 'ellipse', shape: 'ellipse',
image: undefined, image: undefined,
widthMin: 16, // px widthMin: 16, // px
@ -97,7 +95,7 @@ function Graph (container, data, options) {
dataManipulationToolbar: { dataManipulationToolbar: {
enabled: false enabled: false
}, },
minVelocity: 2, // px/s
minVelocity: 0.2, // px/s
maxIterations: 1000 // maximum number of iteration to stabilize maxIterations: 1000 // maximum number of iteration to stabilize
}; };
@ -113,6 +111,9 @@ function Graph (container, data, options) {
this.yIncrement = 0; this.yIncrement = 0;
this.zoomIncrement = 0; this.zoomIncrement = 0;
// load the force calculation functions, grouped under the physics system.
this._loadPhysicsSystem();
// create a frame and canvas // create a frame and canvas
this._create(); this._create();
@ -635,6 +636,7 @@ Graph.prototype._createKeyBinds = function() {
this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown"); this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup"); this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
} }
this.mousetrap.bind("b",this._formBarnesHutTree.bind(me));
if (this.constants.dataManipulationToolbar.enabled == true) { if (this.constants.dataManipulationToolbar.enabled == true) {
this.mousetrap.bind("escape",this._createManipulatorBar.bind(me)); this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
@ -1209,7 +1211,7 @@ Graph.prototype._addNodes = function(ids) {
if (!node.isFixed() && this.createNodeOnClick != true) { if (!node.isFixed() && this.createNodeOnClick != true) {
// TODO: position new nodes in a smarter way! // TODO: position new nodes in a smarter way!
var radius = this.constants.edges.length * 2;
var radius = this.constants.edges.length;
var count = ids.length; var count = ids.length;
var angle = 2 * Math.PI * (i / count); var angle = 2 * Math.PI * (i / count);
node.x = radius * Math.cos(angle); node.x = radius * Math.cos(angle);
@ -1494,6 +1496,8 @@ Graph.prototype._redraw = function() {
this._doInAllSectors("_drawEdges",ctx); this._doInAllSectors("_drawEdges",ctx);
this._doInAllSectors("_drawNodes",ctx,true); this._doInAllSectors("_drawNodes",ctx,true);
//this._drawTree(ctx,"#F00F0F");
// restore original scaling and translation // restore original scaling and translation
ctx.restore(); ctx.restore();
@ -1669,233 +1673,9 @@ Graph.prototype._doStabilize = function() {
count++; count++;
} }
this.zoomToFit(); this.zoomToFit();
// var end = new Date();
// console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
};
/**
* 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
*/
Graph.prototype._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
*/
Graph.prototype._calculateForces = function() {
// var screenCenterPos = {"x":(0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x)),
// "y":(0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y))}
// create a local edge to the nodes and edges, that is faster
var dx, dy, angle, distance, fx, fy,
repulsingForce, springForce, length, edgeLength,
node, node1, node2, edge, edgeId, i, j, nodeId, xCenter, yCenter;
var clusterSize;
var nodes = this.nodes;
var edges = this.edges;
// 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
var gravity = 0.08 * this.forceFactor;
for (i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[i]];
// gravity does not apply when we are in a pocket sector
if (this._sector() == "default") {
dx = -node.x;// + screenCenterPos.x;
dy = -node.y;// + screenCenterPos.y;
angle = Math.atan2(dy, dx);
fx = Math.cos(angle) * gravity;
fy = Math.sin(angle) * gravity;
}
else {
fx = 0;
fy = 0;
}
node._setForce(fx, fy);
node.updateDamping(this.nodeIndices.length);
}
// repulsing forces between nodes
var minimumDistance = this.constants.nodes.distance,
steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
// we loop from i over all but the last entree in the array
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
var a_base = (-2/3); var b = 4/3;
for (i = 0; i < this.nodeIndices.length-1; i++) {
node1 = nodes[this.nodeIndices[i]];
for (j = i+1; j < this.nodeIndices.length; j++) {
node2 = nodes[this.nodeIndices[j]];
clusterSize = (node1.clusterSize + node2.clusterSize - 2);
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
// clusters have a larger region of influence
minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification));
var a = a_base / minimumDistance;
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
angle = Math.atan2(dy, dx);
if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
repulsingForce = 1.0;
}
else {
// TODO: correct factor for repulsing force
//repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
repulsingForce = a * distance + b; // TODO: test the approximation of the function above
}
// amplify the repulsion for clusters.
repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification;
repulsingForce *= this.forceFactor;
fx = Math.cos(angle) * repulsingForce;
fy = Math.sin(angle) * repulsingForce ;
node1._addForce(-fx, -fy);
node2._addForce(fx, fy);
}
}
}
/*
// repulsion of the edges on the nodes and
for (var nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
node = nodes[nodeId];
for(var edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
// get the center of the edge
xCenter = edge.from.x+(edge.to.x - edge.from.x)/2;
yCenter = edge.from.y+(edge.to.y - edge.from.y)/2;
// calculate normally distributed force
dx = node.x - xCenter;
dy = node.y - yCenter;
distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
angle = Math.atan2(dy, dx);
if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
repulsingForce = 1.0;
}
else {
// TODO: correct factor for repulsing force
//var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
repulsingForce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)); // TODO: customize the repulsing force
}
fx = Math.cos(angle) * repulsingForce;
fy = Math.sin(angle) * repulsingForce;
node._addForce(fx, fy);
edge.from._addForce(-fx/2,-fy/2);
edge.to._addForce(-fx/2,-fy/2);
}
}
}
}
}
*/
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
//edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
//edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
//edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
edgeLength = edge.length;
// this implies that the edges between big clusters are longer
edgeLength += clusterSize * this.constants.clustering.edgeGrowth;
length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx);
springForce = edge.stiffness * (edgeLength - length) * this.forceFactor;
fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce;
edge.from._addForce(-fx, -fy);
edge.to._addForce(fx, fy);
}
}
}
}
/*
// TODO: re-implement repulsion of edges
// repulsing forces between edges
var minimumDistance = this.constants.edges.distance,
steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
for (var l = 0; l < edges.length; l++) {
//Keep distance from other edge centers
for (var l2 = l + 1; l2 < this.edges.length; l2++) {
//var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
//var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
//dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
// calculate normally distributed force
dx = l2x - lx,
dy = l2y - ly,
distance = Math.sqrt(dx * dx + dy * dy),
angle = Math.atan2(dy, dx),
// TODO: correct factor for repulsing force
//var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
fx = Math.cos(angle) * repulsingforce,
fy = Math.sin(angle) * repulsingforce;
edges[l].from._addForce(-fx, -fy);
edges[l].to._addForce(-fx, -fy);
edges[l2].from._addForce(fx, fy);
edges[l2].to._addForce(fx, fy);
}
}
*/
};
/** /**
@ -1924,14 +1704,25 @@ Graph.prototype._isMoving = function(vmin) {
* @private * @private
*/ */
Graph.prototype._discreteStepNodes = function() { Graph.prototype._discreteStepNodes = function() {
var interval = 0.01;
var interval = 1.0;
var nodes = this.nodes; var nodes = this.nodes;
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
nodes[id].discreteStep(interval);
this.constants.maxVelocity = 30;
if (this.constants.maxVelocity > 0) {
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
nodes[id].discreteStepLimited(interval, this.constants.maxVelocity);
}
}
}
else {
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
nodes[id].discreteStep(interval);
}
} }
} }
var vmin = this.constants.minVelocity; var vmin = this.constants.minVelocity;
this.moving = this._isMoving(vmin); this.moving = this._isMoving(vmin);
}; };
@ -1979,7 +1770,6 @@ Graph.prototype.start = function() {
//this.time = this.end - this.startTime; //this.time = this.end - this.startTime;
//console.log('refresh time: ' + this.time); //console.log('refresh time: ' + this.time);
//this.startTime = window.performance.now(); //this.startTime = window.performance.now();
}, this.renderTimestep); }, this.renderTimestep);
} }
} }
@ -2018,6 +1808,22 @@ Graph.prototype.toggleFreeze = function() {
} }
}; };
/**
* Mixin the physics system and initialize the parameters required.
*
* @private
*/
Graph.prototype._loadPhysicsSystem = function() {
for (var mixinFunction in physicsMixin) {
if (physicsMixin.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = physicsMixin[mixinFunction];
}
}
};
/** /**
* Mixin the cluster system and initialize the parameters required. * Mixin the cluster system and initialize the parameters required.
* *

+ 27
- 5
src/graph/Node.js View File

@ -21,6 +21,7 @@
* retrieving group properties * retrieving group properties
* @param {Object} constants An object with default values for * @param {Object} constants An object with default values for
* example for the color * example for the color
*
*/ */
function Node(properties, imagelist, grouplist, constants) { function Node(properties, imagelist, grouplist, constants) {
this.selected = false; this.selected = false;
@ -73,8 +74,7 @@ function Node(properties, imagelist, grouplist, constants) {
this.vx = 0.0; // velocity x this.vx = 0.0; // velocity x
this.vy = 0.0; // velocity y this.vy = 0.0; // velocity y
this.minForce = constants.minForce; this.minForce = constants.minForce;
this.damping = 0.9;
this.dampingFactor = 75;
this.damping = 0.9; // this is manipulated in the updateDaming function
this.graphScaleInv = 1; this.graphScaleInv = 1;
this.canvasTopLeft = {"x": -300, "y": -300}; this.canvasTopLeft = {"x": -300, "y": -300};
@ -128,7 +128,7 @@ Node.prototype.detachEdge = function(edge) {
* @private * @private
*/ */
Node.prototype._updateMass = function() { Node.prototype._updateMass = function() {
this.mass = 1 + 0.6 * this.edges.length; // kg
this.mass = 1;// + 0.6 * this.edges.length; // kg
}; };
/** /**
@ -390,6 +390,29 @@ Node.prototype.discreteStep = function(interval) {
}; };
/**
* Perform one discrete step for the node
* @param {number} interval Time interval in seconds
*/
Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
if (!this.xFixed) {
var dx = -this.damping * this.vx; // damping force
var ax = (this.fx + dx) / this.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
}
if (!this.yFixed) {
var dy = -this.damping * this.vy; // damping force
var ay = (this.fy + dy) / this.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
}
};
/** /**
* Check if this node has a fixed x and y position * Check if this node has a fixed x and y position
* @return {boolean} true if fixed, false if not * @return {boolean} true if fixed, false if not
@ -950,8 +973,7 @@ Node.prototype.setScale = function(scale) {
* @param {Number} numberOfNodes * @param {Number} numberOfNodes
*/ */
Node.prototype.updateDamping = function(numberOfNodes) { Node.prototype.updateDamping = function(numberOfNodes) {
this.damping = (0.8 + 0.1*this.clusterSize * (1 + Math.pow(numberOfNodes,-2)));
this.damping *= this.dampingFactor;
this.damping = (1 + 0.1*this.clusterSize * (1 + Math.pow(numberOfNodes,-2)));
}; };

+ 3
- 9
src/graph/SelectionMixin.js View File

@ -213,9 +213,7 @@ var SelectionMixin = {
this.selectionObj = {}; this.selectionObj = {};
if (doNotTrigger == false) { if (doNotTrigger == false) {
this._trigger('select', {
nodes: this.getSelection()
});
this._trigger('select', this.getSelection());
} }
}, },
@ -242,9 +240,7 @@ var SelectionMixin = {
} }
if (doNotTrigger == false) { if (doNotTrigger == false) {
this._trigger('select', {
nodes: this.getSelection()
});
this._trigger('select', this.getSelection());
} }
}, },
@ -399,9 +395,7 @@ var SelectionMixin = {
this._removeFromSelection(object); this._removeFromSelection(object);
} }
if (doNotTrigger == false) { if (doNotTrigger == false) {
this._trigger('select', {
nodes: this.getSelection()
});
this._trigger('select', this.getSelection());
} }
}, },

+ 3
- 1
src/graph/manipulationMixin.js View File

@ -442,6 +442,8 @@ var manipulationMixin = {
/** /**
* delete everything in the selection * delete everything in the selection
* TODO : place the alert in the gui.
*
* *
* @private * @private
*/ */
@ -460,4 +462,4 @@ var manipulationMixin = {
} }
}
};

+ 539
- 0
src/graph/physicsMixin.js View File

@ -0,0 +1,539 @@
/**
* Created by Alex on 2/6/14.
*/
var physicsMixin = {
/**
* Before calculating the forces, we check if we need to cluster to keep up performance and we check
* if there is more than one node. If it is just one node, we dont calculate anything.
*
* @private
*/
_initializeForceCalculation : function() {
// stop calculation if there is only one node
if (this.nodeIndices.length == 1) {
this.nodes[this.nodeIndices[0]]._setForce(0,0);
}
else {
// if there are too many nodes on screen, we cluster without repositioning
if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
this.clusterToFit(this.constants.clustering.reduceToNodes, false);
}
// we now start the force calculation
this._calculateForcesBarnesHut();
// this._calculateForcesOriginal();
}
},
/**
* Calculate the external forces acting on the nodes
* Forces are caused by: edges, repulsing forces between nodes, gravity
* @private
*/
_calculateForcesOriginal : 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
// var startTimeAll = Date.now();
this._calculateGravitationalForces(1);
// var startTimeRepulsion = Date.now();
// All nodes repel eachother.
this._calculateRepulsionForces();
// var endTimeRepulsion = Date.now();
// the edges are strings
this._calculateSpringForces(1);
// var endTimeAll = Date.now();
// echo("Time repulsion part:", endTimeRepulsion - startTimeRepulsion);
// echo("Time total force calc:", endTimeAll - startTimeAll);
},
/**
* Calculate the external forces acting on the nodes
* Forces are caused by: edges, repulsing forces between nodes, gravity
* @private
*/
_calculateForcesBarnesHut : 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
// var startTimeAll = Date.now();
this._clearForces();
// var startTimeRepulsion = Date.now();
// All nodes repel eachother.
this._calculateBarnesHutForces();
// var endTimeRepulsion = Date.now();
// the edges are strings
this._calculateSpringForces(1);
// var endTimeAll = Date.now();
// echo("Time repulsion part:", endTimeRepulsion - startTimeRepulsion);
// echo("Time total force calc:", endTimeAll - startTimeAll);
},
_clearForces : function() {
var node;
var nodes = this.nodes;
for (var i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[i]];
node._setForce(0, 0);
node.updateDamping(this.nodeIndices.length);
}
},
_calculateGravitationalForces : function(boost) {
var dx, dy, angle, fx, fy, node, i;
var nodes = this.nodes;
var gravity = 0.08 * boost;
for (i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[i]];
// gravity does not apply when we are in a pocket sector
if (this._sector() == "default") {
dx = -node.x;// + screenCenterPos.x;
dy = -node.y;// + screenCenterPos.y;
angle = Math.atan2(dy, dx);
fx = Math.cos(angle) * gravity;
fy = Math.sin(angle) * gravity;
}
else {
fx = 0;
fy = 0;
}
node._setForce(fx, fy);
node.updateDamping(this.nodeIndices.length);
}
},
_calculateRepulsionForces : function() {
var dx, dy, angle, distance, fx, fy, clusterSize,
repulsingForce, node1, node2, i, j;
var nodes = this.nodes;
// approximation constants
var a_base = (-2/3);
var b = 4/3;
// repulsing forces between nodes
var minimumDistance = this.constants.nodes.distance;
//var steepness = 10;
// we loop from i over all but the last entree in the array
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
for (i = 0; i < this.nodeIndices.length-1; i++) {
node1 = nodes[this.nodeIndices[i]];
for (j = i+1; j < this.nodeIndices.length; j++) {
node2 = nodes[this.nodeIndices[j]];
clusterSize = (node1.clusterSize + node2.clusterSize - 2);
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
// clusters have a larger region of influence
minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification));
var a = a_base / minimumDistance;
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
angle = Math.atan2(dy, dx);
if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
repulsingForce = 1.0;
}
else {
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
}
// amplify the repulsion for clusters.
repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification;
fx = Math.cos(angle) * repulsingForce;
fy = Math.sin(angle) * repulsingForce ;
node1._addForce(-fx, -fy);
node2._addForce(fx, fy);
}
}
}
},
_calculateSpringForces : function(boost) {
var dx, dy, angle, fx, fy, springForce, length, edgeLength, edge, edgeId, clusterSize;
var edges = this.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
edgeLength = edge.length;
// this implies that the edges between big clusters are longer
edgeLength += clusterSize * this.constants.clustering.edgeGrowth;
length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx);
springForce = 0.02 * (edgeLength - length) * boost;
fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce;
edge.from._addForce(-fx, -fy);
edge.to._addForce(fx, fy);
}
}
}
}
},
_calculateBarnesHutForces : function() {
this._formBarnesHutTree();
var nodes = this.nodes;
var nodeIndices = this.nodeIndices;
var node;
var nodeCount = nodeIndices.length;
var barnesHutTree = this.barnesHutTree;
this.theta = 0.2;
this.graviationalConstant = -10000;
// place the nodes one by one recursively
for (var i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
// 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);
}
},
_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);
if (distance > 0) { // distance is 0 if it looks to apply a force on itself.
// we invert it here because we need the inverted distance for the force calculation too.
var distanceInv = 1/distance;
// BarnesHut condition
if (parentBranch.size * distanceInv > this.theta) {
// 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
this._getForceOnNode(parentBranch, node, dx ,dy, distanceInv);
}
}
else {
this._getForceOnNode(parentBranch, node, dx ,dy, distanceInv);
}
}
}
},
_getForceOnNode : function(parentBranch, node, dx ,dy, distanceInv) {
// even if the parentBranch only has one node, its Center of Mass is at the right place (the node in this case).
var gravityForce = this.graviationalConstant * parentBranch.mass * node.mass * distanceInv * distanceInv;
var angle = Math.atan2(dy, dx);
var fx = Math.cos(angle) * gravityForce;
var fy = Math.sin(angle) * gravityForce;
node._addForce(fx, fy);
},
_formBarnesHutTree : function() {
var nodes = this.nodes;
var nodeIndices = this.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 (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; maxY -= 0.5 * sizeDiff;} // xSize < ySize
// construct the barnesHutTree
var barnesHutTree = {root:{
CenterOfMass:{x:0,y:0}, // Center of Mass
mass:0,
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
size: Math.abs(maxX - minX),
children: {data:null},
childrenCount: 4
}};
this._splitBranch(barnesHutTree.root);
// place the nodes one by one recursively
for (i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
this._placeInTree(barnesHutTree.root,node);
}
// make global
this.barnesHutTree = barnesHutTree
},
_updateBranchMass : function(parentBranch, node) {
var totalMass = parentBranch.mass + node.mass;
var totalMassInv = 1/totalMass;
parentBranch.CenterOfMass.x = parentBranch.CenterOfMass.x * parentBranch.mass + node.x * node.mass;
parentBranch.CenterOfMass.x *= totalMassInv;
parentBranch.CenterOfMass.y = parentBranch.CenterOfMass.y * parentBranch.mass + node.y * node.mass;
parentBranch.CenterOfMass.y *= totalMassInv;
parentBranch.mass = totalMass;
},
_placeInTree : function(parentBranch,node) {
// 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.NE.range.maxY > node.y) { // in NE
this._placeInRegion(parentBranch,node,"NE");
}
else { // in SE
this._placeInRegion(parentBranch,node,"SE");
}
}
},
_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
this._splitBranch(parentBranch.children[region]);
this._placeInTree(parentBranch.children[region],node);
break;
case 4: // place in branch
this._placeInTree(parentBranch.children[region],node);
break;
}
},
_splitBranch : function(parentBranch) {
// if the branch is filled 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 : function(parentBranch, region) {
var minX,maxX,minY,maxY;
switch (region) {
case "NW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + parentBranch.size;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + parentBranch.size;
break;
case "NE":
minX = parentBranch.range.minX + parentBranch.size;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + parentBranch.size;
break;
case "SW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + parentBranch.size;
minY = parentBranch.range.minY + parentBranch.size;
maxY = parentBranch.range.maxY;
break;
case "SE":
minX = parentBranch.range.minX + parentBranch.size;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY + parentBranch.size;
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,
children: {data:null},
childrenCount: 0
};
},
_drawTree : function(ctx,color) {
if (this.barnesHutTree !== undefined) {
ctx.lineWidth = 2;
this._drawBranch(this.barnesHutTree.root,ctx,color);
}
},
_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();
}
*/
}
};
function echo() {
switch (arguments.length) {
case 1:
echoN1(arguments[0]); break;
case 2:
echoN2(arguments[0],arguments[1]); break;
case 3:
echoN3(arguments[0],arguments[1],arguments[2]); break;
}
}
function echoN1(message) {
console.log(message);
}
function echoN2(message1,message2) {
console.log(message1,message2);
}
function echoN3(message1,message2,message3) {
console.log(message1,message2,message3);
}

Loading…
Cancel
Save