QuadTree.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. /* *
  2. *
  3. * Networkgraph series
  4. *
  5. * (c) 2010-2020 Paweł Fus
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. 'use strict';
  13. import H from '../../Core/Globals.js';
  14. import U from '../../Core/Utilities.js';
  15. var extend = U.extend;
  16. /* eslint-disable no-invalid-this, valid-jsdoc */
  17. /**
  18. * The QuadTree node class. Used in Networkgraph chart as a base for Barnes-Hut
  19. * approximation.
  20. *
  21. * @private
  22. * @class
  23. * @name Highcharts.QuadTreeNode
  24. *
  25. * @param {Highcharts.Dictionary<number>} box Available space for the node
  26. */
  27. var QuadTreeNode = H.QuadTreeNode = function (box) {
  28. /**
  29. * Read only. The available space for node.
  30. *
  31. * @name Highcharts.QuadTreeNode#box
  32. * @type {Highcharts.Dictionary<number>}
  33. */
  34. this.box = box;
  35. /**
  36. * Read only. The minium of width and height values.
  37. *
  38. * @name Highcharts.QuadTreeNode#boxSize
  39. * @type {number}
  40. */
  41. this.boxSize = Math.min(box.width, box.height);
  42. /**
  43. * Read only. Array of subnodes. Empty if QuadTreeNode has just one Point.
  44. * When added another Point to this QuadTreeNode, array is filled with four
  45. * subnodes.
  46. *
  47. * @name Highcharts.QuadTreeNode#nodes
  48. * @type {Array<Highcharts.QuadTreeNode>}
  49. */
  50. this.nodes = [];
  51. /**
  52. * Read only. Flag to determine if QuadTreeNode is internal (and has
  53. * subnodes with mass and central position) or external (bound to Point).
  54. *
  55. * @name Highcharts.QuadTreeNode#isInternal
  56. * @type {boolean}
  57. */
  58. this.isInternal = false;
  59. /**
  60. * Read only. If QuadTreeNode is an external node, Point is stored in
  61. * `this.body`.
  62. *
  63. * @name Highcharts.QuadTreeNode#body
  64. * @type {boolean|Highcharts.Point}
  65. */
  66. this.body = false;
  67. /**
  68. * Read only. Internal nodes when created are empty to reserve the space. If
  69. * Point is added to this QuadTreeNode, QuadTreeNode is no longer empty.
  70. *
  71. * @name Highcharts.QuadTreeNode#isEmpty
  72. * @type {boolean}
  73. */
  74. this.isEmpty = true;
  75. };
  76. extend(QuadTreeNode.prototype,
  77. /** @lends Highcharts.QuadTreeNode.prototype */
  78. {
  79. /**
  80. * Insert recursively point(node) into the QuadTree. If the given
  81. * quadrant is already occupied, divide it into smaller quadrants.
  82. *
  83. * @param {Highcharts.Point} point
  84. * Point/node to be inserted
  85. * @param {number} depth
  86. * Max depth of the QuadTree
  87. */
  88. insert: function (point, depth) {
  89. var newQuadTreeNode;
  90. if (this.isInternal) {
  91. // Internal node:
  92. this.nodes[this.getBoxPosition(point)].insert(point, depth - 1);
  93. }
  94. else {
  95. this.isEmpty = false;
  96. if (!this.body) {
  97. // First body in a quadrant:
  98. this.isInternal = false;
  99. this.body = point;
  100. }
  101. else {
  102. if (depth) {
  103. // Every other body in a quadrant:
  104. this.isInternal = true;
  105. this.divideBox();
  106. // Reinsert main body only once:
  107. if (this.body !== true) {
  108. this.nodes[this.getBoxPosition(this.body)]
  109. .insert(this.body, depth - 1);
  110. this.body = true;
  111. }
  112. // Add second body:
  113. this.nodes[this.getBoxPosition(point)]
  114. .insert(point, depth - 1);
  115. }
  116. else {
  117. // We are below max allowed depth. That means either:
  118. // - really huge number of points
  119. // - falling two points into exactly the same position
  120. // In this case, create another node in the QuadTree.
  121. //
  122. // Alternatively we could add some noise to the
  123. // position, but that could result in different
  124. // rendered chart in exporting.
  125. newQuadTreeNode = new QuadTreeNode({
  126. top: point.plotX,
  127. left: point.plotY,
  128. // Width/height below 1px
  129. width: 0.1,
  130. height: 0.1
  131. });
  132. newQuadTreeNode.body = point;
  133. newQuadTreeNode.isInternal = false;
  134. this.nodes.push(newQuadTreeNode);
  135. }
  136. }
  137. }
  138. },
  139. /**
  140. * Each quad node requires it's mass and center position. That mass and
  141. * position is used to imitate real node in the layout by approximation.
  142. */
  143. updateMassAndCenter: function () {
  144. var mass = 0, plotX = 0, plotY = 0;
  145. if (this.isInternal) {
  146. // Calcualte weightened mass of the quad node:
  147. this.nodes.forEach(function (pointMass) {
  148. if (!pointMass.isEmpty) {
  149. mass += pointMass.mass;
  150. plotX +=
  151. pointMass.plotX * pointMass.mass;
  152. plotY +=
  153. pointMass.plotY * pointMass.mass;
  154. }
  155. });
  156. plotX /= mass;
  157. plotY /= mass;
  158. }
  159. else if (this.body) {
  160. // Just one node, use coordinates directly:
  161. mass = this.body.mass;
  162. plotX = this.body.plotX;
  163. plotY = this.body.plotY;
  164. }
  165. // Store details:
  166. this.mass = mass;
  167. this.plotX = plotX;
  168. this.plotY = plotY;
  169. },
  170. /**
  171. * When inserting another node into the box, that already hove one node,
  172. * divide the available space into another four quadrants.
  173. *
  174. * Indexes of quadrants are:
  175. * ```
  176. * ------------- -------------
  177. * | | | | |
  178. * | | | 0 | 1 |
  179. * | | divide() | | |
  180. * | 1 | -----------> -------------
  181. * | | | | |
  182. * | | | 3 | 2 |
  183. * | | | | |
  184. * ------------- -------------
  185. * ```
  186. */
  187. divideBox: function () {
  188. var halfWidth = this.box.width / 2, halfHeight = this.box.height / 2;
  189. // Top left
  190. this.nodes[0] = new QuadTreeNode({
  191. left: this.box.left,
  192. top: this.box.top,
  193. width: halfWidth,
  194. height: halfHeight
  195. });
  196. // Top right
  197. this.nodes[1] = new QuadTreeNode({
  198. left: this.box.left + halfWidth,
  199. top: this.box.top,
  200. width: halfWidth,
  201. height: halfHeight
  202. });
  203. // Bottom right
  204. this.nodes[2] = new QuadTreeNode({
  205. left: this.box.left + halfWidth,
  206. top: this.box.top + halfHeight,
  207. width: halfWidth,
  208. height: halfHeight
  209. });
  210. // Bottom left
  211. this.nodes[3] = new QuadTreeNode({
  212. left: this.box.left,
  213. top: this.box.top + halfHeight,
  214. width: halfWidth,
  215. height: halfHeight
  216. });
  217. },
  218. /**
  219. * Determine which of the quadrants should be used when placing node in
  220. * the QuadTree. Returned index is always in range `< 0 , 3 >`.
  221. *
  222. * @param {Highcharts.Point} point
  223. * @return {number}
  224. */
  225. getBoxPosition: function (point) {
  226. var left = point.plotX < this.box.left + this.box.width / 2, top = point.plotY < this.box.top + this.box.height / 2, index;
  227. if (left) {
  228. if (top) {
  229. // Top left
  230. index = 0;
  231. }
  232. else {
  233. // Bottom left
  234. index = 3;
  235. }
  236. }
  237. else {
  238. if (top) {
  239. // Top right
  240. index = 1;
  241. }
  242. else {
  243. // Bottom right
  244. index = 2;
  245. }
  246. }
  247. return index;
  248. }
  249. });
  250. /**
  251. * The QuadTree class. Used in Networkgraph chart as a base for Barnes-Hut
  252. * approximation.
  253. *
  254. * @private
  255. * @class
  256. * @name Highcharts.QuadTree
  257. *
  258. * @param {number} x left position of the plotting area
  259. * @param {number} y top position of the plotting area
  260. * @param {number} width width of the plotting area
  261. * @param {number} height height of the plotting area
  262. */
  263. var QuadTree = H.QuadTree = function (x, y, width, height) {
  264. // Boundary rectangle:
  265. this.box = {
  266. left: x,
  267. top: y,
  268. width: width,
  269. height: height
  270. };
  271. this.maxDepth = 25;
  272. this.root = new QuadTreeNode(this.box, '0');
  273. this.root.isInternal = true;
  274. this.root.isRoot = true;
  275. this.root.divideBox();
  276. };
  277. extend(QuadTree.prototype,
  278. /** @lends Highcharts.QuadTree.prototype */
  279. {
  280. /**
  281. * Insert nodes into the QuadTree
  282. *
  283. * @param {Array<Highcharts.Point>} points
  284. */
  285. insertNodes: function (points) {
  286. points.forEach(function (point) {
  287. this.root.insert(point, this.maxDepth);
  288. }, this);
  289. },
  290. /**
  291. * Depfth first treversal (DFS). Using `before` and `after` callbacks,
  292. * we can get two results: preorder and postorder traversals, reminder:
  293. *
  294. * ```
  295. * (a)
  296. * / \
  297. * (b) (c)
  298. * / \
  299. * (d) (e)
  300. * ```
  301. *
  302. * DFS (preorder): `a -> b -> d -> e -> c`
  303. *
  304. * DFS (postorder): `d -> e -> b -> c -> a`
  305. *
  306. * @param {Highcharts.QuadTreeNode|null} node
  307. * @param {Function} [beforeCallback] function to be called before
  308. * visiting children nodes
  309. * @param {Function} [afterCallback] function to be called after
  310. * visiting children nodes
  311. */
  312. visitNodeRecursive: function (node, beforeCallback, afterCallback) {
  313. var goFurther;
  314. if (!node) {
  315. node = this.root;
  316. }
  317. if (node === this.root && beforeCallback) {
  318. goFurther = beforeCallback(node);
  319. }
  320. if (goFurther === false) {
  321. return;
  322. }
  323. node.nodes.forEach(function (qtNode) {
  324. if (qtNode.isInternal) {
  325. if (beforeCallback) {
  326. goFurther = beforeCallback(qtNode);
  327. }
  328. if (goFurther === false) {
  329. return;
  330. }
  331. this.visitNodeRecursive(qtNode, beforeCallback, afterCallback);
  332. }
  333. else if (qtNode.body) {
  334. if (beforeCallback) {
  335. beforeCallback(qtNode.body);
  336. }
  337. }
  338. if (afterCallback) {
  339. afterCallback(qtNode);
  340. }
  341. }, this);
  342. if (node === this.root && afterCallback) {
  343. afterCallback(node);
  344. }
  345. },
  346. /**
  347. * Calculate mass of the each QuadNode in the tree.
  348. */
  349. calculateMassAndCenter: function () {
  350. this.visitNodeRecursive(null, null, function (node) {
  351. node.updateMassAndCenter();
  352. });
  353. }
  354. });