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.

471 lines
14 KiB

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