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.

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