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.

442 lines
13 KiB

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