vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

408 lines
12 KiB

  1. /**
  2. * Created by Alex on 2/23/2015.
  3. */
  4. function BarnesHutSolver(body, options) {
  5. this.body = body;
  6. this.options = options;
  7. }
  8. /**
  9. * This function calculates the forces the nodes apply on eachother based on a gravitational model.
  10. * The Barnes Hut method is used to speed up this N-body simulation.
  11. *
  12. * @private
  13. */
  14. BarnesHutSolver.prototype.solve = function() {
  15. if (this.options.gravitationalConstant != 0) {
  16. var node;
  17. var nodes = this.body.calculationNodes;
  18. var nodeIndices = this.body.calculationNodeIndices;
  19. var nodeCount = nodeIndices.length;
  20. var barnesHutTree = this._formBarnesHutTree(nodes,nodeIndices);
  21. // place the nodes one by one recursively
  22. for (var i = 0; i < nodeCount; i++) {
  23. node = nodes[nodeIndices[i]];
  24. if (node.options.mass > 0) {
  25. // starting with root is irrelevant, it never passes the BarnesHutSolver condition
  26. this._getForceContribution(barnesHutTree.root.children.NW,node);
  27. this._getForceContribution(barnesHutTree.root.children.NE,node);
  28. this._getForceContribution(barnesHutTree.root.children.SW,node);
  29. this._getForceContribution(barnesHutTree.root.children.SE,node);
  30. }
  31. }
  32. }
  33. };
  34. /**
  35. * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
  36. * If a region contains a single node, we check if it is not itself, then we apply the force.
  37. *
  38. * @param parentBranch
  39. * @param node
  40. * @private
  41. */
  42. BarnesHutSolver.prototype._getForceContribution = function(parentBranch,node) {
  43. // we get no force contribution from an empty region
  44. if (parentBranch.childrenCount > 0) {
  45. var dx,dy,distance;
  46. // get the distance from the center of mass to the node.
  47. dx = parentBranch.centerOfMass.x - node.x;
  48. dy = parentBranch.centerOfMass.y - node.y;
  49. distance = Math.sqrt(dx * dx + dy * dy);
  50. // BarnesHutSolver condition
  51. // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed
  52. // calcSize = 1/s --> d * 1/s > 1/theta = passed
  53. if (distance * parentBranch.calcSize > this.options.thetaInverted) {
  54. // duplicate code to reduce function calls to speed up program
  55. if (distance == 0) {
  56. distance = 0.1*Math.random();
  57. dx = distance;
  58. }
  59. var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
  60. var fx = dx * gravityForce;
  61. var fy = dy * gravityForce;
  62. node.fx += fx;
  63. node.fy += fy;
  64. }
  65. else {
  66. // Did not pass the condition, go into children if available
  67. if (parentBranch.childrenCount == 4) {
  68. this._getForceContribution(parentBranch.children.NW,node);
  69. this._getForceContribution(parentBranch.children.NE,node);
  70. this._getForceContribution(parentBranch.children.SW,node);
  71. this._getForceContribution(parentBranch.children.SE,node);
  72. }
  73. else { // parentBranch must have only one node, if it was empty we wouldnt be here
  74. if (parentBranch.children.data.id != node.id) { // if it is not self
  75. // duplicate code to reduce function calls to speed up program
  76. if (distance == 0) {
  77. distance = 0.5*Math.random();
  78. dx = distance;
  79. }
  80. var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
  81. var fx = dx * gravityForce;
  82. var fy = dy * gravityForce;
  83. node.fx += fx;
  84. node.fy += fy;
  85. }
  86. }
  87. }
  88. }
  89. };
  90. /**
  91. * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
  92. *
  93. * @param nodes
  94. * @param nodeIndices
  95. * @private
  96. */
  97. BarnesHutSolver.prototype._formBarnesHutTree = function(nodes,nodeIndices) {
  98. var node;
  99. var nodeCount = nodeIndices.length;
  100. var minX = Number.MAX_VALUE,
  101. minY = Number.MAX_VALUE,
  102. maxX =-Number.MAX_VALUE,
  103. maxY =-Number.MAX_VALUE;
  104. // get the range of the nodes
  105. for (var i = 0; i < nodeCount; i++) {
  106. var x = nodes[nodeIndices[i]].x;
  107. var y = nodes[nodeIndices[i]].y;
  108. if (nodes[nodeIndices[i]].options.mass > 0) {
  109. if (x < minX) { minX = x; }
  110. if (x > maxX) { maxX = x; }
  111. if (y < minY) { minY = y; }
  112. if (y > maxY) { maxY = y; }
  113. }
  114. }
  115. // make the range a square
  116. var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
  117. if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
  118. else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
  119. var minimumTreeSize = 1e-5;
  120. var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
  121. var halfRootSize = 0.5 * rootSize;
  122. var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
  123. // construct the barnesHutTree
  124. var barnesHutTree = {
  125. root:{
  126. centerOfMass: {x:0, y:0},
  127. mass:0,
  128. range: {
  129. minX: centerX-halfRootSize,maxX:centerX+halfRootSize,
  130. minY: centerY-halfRootSize,maxY:centerY+halfRootSize
  131. },
  132. size: rootSize,
  133. calcSize: 1 / rootSize,
  134. children: { data:null},
  135. maxWidth: 0,
  136. level: 0,
  137. childrenCount: 4
  138. }
  139. };
  140. this._splitBranch(barnesHutTree.root);
  141. // place the nodes one by one recursively
  142. for (i = 0; i < nodeCount; i++) {
  143. node = nodes[nodeIndices[i]];
  144. if (node.options.mass > 0) {
  145. this._placeInTree(barnesHutTree.root,node);
  146. }
  147. }
  148. // make global
  149. return barnesHutTree
  150. };
  151. /**
  152. * this updates the mass of a branch. this is increased by adding a node.
  153. *
  154. * @param parentBranch
  155. * @param node
  156. * @private
  157. */
  158. BarnesHutSolver.prototype._updateBranchMass = function(parentBranch, node) {
  159. var totalMass = parentBranch.mass + node.options.mass;
  160. var totalMassInv = 1/totalMass;
  161. parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
  162. parentBranch.centerOfMass.x *= totalMassInv;
  163. parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
  164. parentBranch.centerOfMass.y *= totalMassInv;
  165. parentBranch.mass = totalMass;
  166. var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
  167. parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
  168. };
  169. /**
  170. * determine in which branch the node will be placed.
  171. *
  172. * @param parentBranch
  173. * @param node
  174. * @param skipMassUpdate
  175. * @private
  176. */
  177. BarnesHutSolver.prototype._placeInTree = function(parentBranch,node,skipMassUpdate) {
  178. if (skipMassUpdate != true || skipMassUpdate === undefined) {
  179. // update the mass of the branch.
  180. this._updateBranchMass(parentBranch,node);
  181. }
  182. if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
  183. if (parentBranch.children.NW.range.maxY > node.y) { // in NW
  184. this._placeInRegion(parentBranch,node,"NW");
  185. }
  186. else { // in SW
  187. this._placeInRegion(parentBranch,node,"SW");
  188. }
  189. }
  190. else { // in NE or SE
  191. if (parentBranch.children.NW.range.maxY > node.y) { // in NE
  192. this._placeInRegion(parentBranch,node,"NE");
  193. }
  194. else { // in SE
  195. this._placeInRegion(parentBranch,node,"SE");
  196. }
  197. }
  198. };
  199. /**
  200. * actually place the node in a region (or branch)
  201. *
  202. * @param parentBranch
  203. * @param node
  204. * @param region
  205. * @private
  206. */
  207. BarnesHutSolver.prototype._placeInRegion = function(parentBranch,node,region) {
  208. switch (parentBranch.children[region].childrenCount) {
  209. case 0: // place node here
  210. parentBranch.children[region].children.data = node;
  211. parentBranch.children[region].childrenCount = 1;
  212. this._updateBranchMass(parentBranch.children[region],node);
  213. break;
  214. case 1: // convert into children
  215. // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
  216. // we move one node a pixel and we do not put it in the tree.
  217. if (parentBranch.children[region].children.data.x == node.x &&
  218. parentBranch.children[region].children.data.y == node.y) {
  219. node.x += Math.random();
  220. node.y += Math.random();
  221. }
  222. else {
  223. this._splitBranch(parentBranch.children[region]);
  224. this._placeInTree(parentBranch.children[region],node);
  225. }
  226. break;
  227. case 4: // place in branch
  228. this._placeInTree(parentBranch.children[region],node);
  229. break;
  230. }
  231. };
  232. /**
  233. * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
  234. * after the split is complete.
  235. *
  236. * @param parentBranch
  237. * @private
  238. */
  239. BarnesHutSolver.prototype._splitBranch = function(parentBranch) {
  240. // if the branch is shaded with a node, replace the node in the new subset.
  241. var containedNode = null;
  242. if (parentBranch.childrenCount == 1) {
  243. containedNode = parentBranch.children.data;
  244. parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
  245. }
  246. parentBranch.childrenCount = 4;
  247. parentBranch.children.data = null;
  248. this._insertRegion(parentBranch,"NW");
  249. this._insertRegion(parentBranch,"NE");
  250. this._insertRegion(parentBranch,"SW");
  251. this._insertRegion(parentBranch,"SE");
  252. if (containedNode != null) {
  253. this._placeInTree(parentBranch,containedNode);
  254. }
  255. };
  256. /**
  257. * This function subdivides the region into four new segments.
  258. * Specifically, this inserts a single new segment.
  259. * It fills the children section of the parentBranch
  260. *
  261. * @param parentBranch
  262. * @param region
  263. * @param parentRange
  264. * @private
  265. */
  266. BarnesHutSolver.prototype._insertRegion = function(parentBranch, region) {
  267. var minX,maxX,minY,maxY;
  268. var childSize = 0.5 * parentBranch.size;
  269. switch (region) {
  270. case "NW":
  271. minX = parentBranch.range.minX;
  272. maxX = parentBranch.range.minX + childSize;
  273. minY = parentBranch.range.minY;
  274. maxY = parentBranch.range.minY + childSize;
  275. break;
  276. case "NE":
  277. minX = parentBranch.range.minX + childSize;
  278. maxX = parentBranch.range.maxX;
  279. minY = parentBranch.range.minY;
  280. maxY = parentBranch.range.minY + childSize;
  281. break;
  282. case "SW":
  283. minX = parentBranch.range.minX;
  284. maxX = parentBranch.range.minX + childSize;
  285. minY = parentBranch.range.minY + childSize;
  286. maxY = parentBranch.range.maxY;
  287. break;
  288. case "SE":
  289. minX = parentBranch.range.minX + childSize;
  290. maxX = parentBranch.range.maxX;
  291. minY = parentBranch.range.minY + childSize;
  292. maxY = parentBranch.range.maxY;
  293. break;
  294. }
  295. parentBranch.children[region] = {
  296. centerOfMass:{x:0,y:0},
  297. mass:0,
  298. range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
  299. size: 0.5 * parentBranch.size,
  300. calcSize: 2 * parentBranch.calcSize,
  301. children: {data:null},
  302. maxWidth: 0,
  303. level: parentBranch.level+1,
  304. childrenCount: 0
  305. };
  306. };
  307. /**
  308. * This function is for debugging purposed, it draws the tree.
  309. *
  310. * @param ctx
  311. * @param color
  312. * @private
  313. */
  314. BarnesHutSolver.prototype._drawTree = function(ctx,color) {
  315. if (this.barnesHutTree !== undefined) {
  316. ctx.lineWidth = 1;
  317. this._drawBranch(this.barnesHutTree.root,ctx,color);
  318. }
  319. };
  320. /**
  321. * This function is for debugging purposes. It draws the branches recursively.
  322. *
  323. * @param branch
  324. * @param ctx
  325. * @param color
  326. * @private
  327. */
  328. BarnesHutSolver.prototype._drawBranch = function(branch,ctx,color) {
  329. if (color === undefined) {
  330. color = "#FF0000";
  331. }
  332. if (branch.childrenCount == 4) {
  333. this._drawBranch(branch.children.NW,ctx);
  334. this._drawBranch(branch.children.NE,ctx);
  335. this._drawBranch(branch.children.SE,ctx);
  336. this._drawBranch(branch.children.SW,ctx);
  337. }
  338. ctx.strokeStyle = color;
  339. ctx.beginPath();
  340. ctx.moveTo(branch.range.minX,branch.range.minY);
  341. ctx.lineTo(branch.range.maxX,branch.range.minY);
  342. ctx.stroke();
  343. ctx.beginPath();
  344. ctx.moveTo(branch.range.maxX,branch.range.minY);
  345. ctx.lineTo(branch.range.maxX,branch.range.maxY);
  346. ctx.stroke();
  347. ctx.beginPath();
  348. ctx.moveTo(branch.range.maxX,branch.range.maxY);
  349. ctx.lineTo(branch.range.minX,branch.range.maxY);
  350. ctx.stroke();
  351. ctx.beginPath();
  352. ctx.moveTo(branch.range.minX,branch.range.maxY);
  353. ctx.lineTo(branch.range.minX,branch.range.minY);
  354. ctx.stroke();
  355. /*
  356. if (branch.mass > 0) {
  357. ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
  358. ctx.stroke();
  359. }
  360. */
  361. };
  362. module.exports = BarnesHutSolver;