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.

294 lines
8.8 KiB

  1. // distance finding algorithm
  2. import FloydWarshall from "./components/algorithms/FloydWarshall.js"
  3. /**
  4. * KamadaKawai positions the nodes initially based on
  5. *
  6. * "AN ALGORITHM FOR DRAWING GENERAL UNDIRECTED GRAPHS"
  7. * -- Tomihisa KAMADA and Satoru KAWAI in 1989
  8. *
  9. * Possible optimizations in the distance calculation can be implemented.
  10. */
  11. class KamadaKawai {
  12. /**
  13. * @param {Object} body
  14. * @param {number} edgeLength
  15. * @param {number} edgeStrength
  16. */
  17. constructor(body, edgeLength, edgeStrength) {
  18. this.body = body;
  19. this.springLength = edgeLength;
  20. this.springConstant = edgeStrength;
  21. this.distanceSolver = new FloydWarshall();
  22. }
  23. /**
  24. * Not sure if needed but can be used to update the spring length and spring constant
  25. * @param {Object} options
  26. */
  27. setOptions(options) {
  28. if (options) {
  29. if (options.springLength) {
  30. this.springLength = options.springLength;
  31. }
  32. if (options.springConstant) {
  33. this.springConstant = options.springConstant;
  34. }
  35. }
  36. }
  37. /**
  38. * Position the system
  39. * @param {Array.<Node>} nodesArray
  40. * @param {Array.<vis.Edge>} edgesArray
  41. * @param {boolean} [ignoreClusters=false]
  42. */
  43. solve(nodesArray, edgesArray, ignoreClusters = false) {
  44. // get distance matrix
  45. let D_matrix = this.distanceSolver.getDistances(this.body, nodesArray, edgesArray); // distance matrix
  46. // get the L Matrix
  47. this._createL_matrix(D_matrix);
  48. // get the K Matrix
  49. this._createK_matrix(D_matrix);
  50. // initial E Matrix
  51. this._createE_matrix();
  52. // calculate positions
  53. let threshold = 0.01;
  54. let innerThreshold = 1;
  55. let iterations = 0;
  56. let maxIterations = Math.max(1000, Math.min(10 * this.body.nodeIndices.length, 6000));
  57. let maxInnerIterations = 5;
  58. let maxEnergy = 1e9;
  59. let highE_nodeId = 0, dE_dx = 0, dE_dy = 0, delta_m = 0, subIterations = 0;
  60. while (maxEnergy > threshold && iterations < maxIterations) {
  61. iterations += 1;
  62. [highE_nodeId, maxEnergy, dE_dx, dE_dy] = this._getHighestEnergyNode(ignoreClusters);
  63. delta_m = maxEnergy;
  64. subIterations = 0;
  65. while (delta_m > innerThreshold && subIterations < maxInnerIterations) {
  66. subIterations += 1;
  67. this._moveNode(highE_nodeId, dE_dx, dE_dy);
  68. [delta_m, dE_dx, dE_dy] = this._getEnergy(highE_nodeId);
  69. }
  70. }
  71. }
  72. /**
  73. * get the node with the highest energy
  74. * @param {boolean} ignoreClusters
  75. * @returns {number[]}
  76. * @private
  77. */
  78. _getHighestEnergyNode(ignoreClusters) {
  79. let nodesArray = this.body.nodeIndices;
  80. let nodes = this.body.nodes;
  81. let maxEnergy = 0;
  82. let maxEnergyNodeId = nodesArray[0];
  83. let dE_dx_max = 0, dE_dy_max = 0;
  84. for (let nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) {
  85. let m = nodesArray[nodeIdx];
  86. // by not evaluating nodes with predefined positions we should only move nodes that have no positions.
  87. if ((nodes[m].predefinedPosition === false || nodes[m].isCluster === true && ignoreClusters === true) || nodes[m].options.fixed.x === true || nodes[m].options.fixed.y === true) {
  88. let [delta_m,dE_dx,dE_dy] = this._getEnergy(m);
  89. if (maxEnergy < delta_m) {
  90. maxEnergy = delta_m;
  91. maxEnergyNodeId = m;
  92. dE_dx_max = dE_dx;
  93. dE_dy_max = dE_dy;
  94. }
  95. }
  96. }
  97. return [maxEnergyNodeId, maxEnergy, dE_dx_max, dE_dy_max];
  98. }
  99. /**
  100. * calculate the energy of a single node
  101. * @param {Node.id} m
  102. * @returns {number[]}
  103. * @private
  104. */
  105. _getEnergy(m) {
  106. let [dE_dx,dE_dy] = this.E_sums[m];
  107. let delta_m = Math.sqrt(Math.pow(dE_dx, 2) + Math.pow(dE_dy, 2));
  108. return [delta_m, dE_dx, dE_dy];
  109. }
  110. /**
  111. * move the node based on it's energy
  112. * the dx and dy are calculated from the linear system proposed by Kamada and Kawai
  113. * @param {number} m
  114. * @param {number} dE_dx
  115. * @param {number} dE_dy
  116. * @private
  117. */
  118. _moveNode(m, dE_dx, dE_dy) {
  119. let nodesArray = this.body.nodeIndices;
  120. let nodes = this.body.nodes;
  121. let d2E_dx2 = 0;
  122. let d2E_dxdy = 0;
  123. let d2E_dy2 = 0;
  124. let x_m = nodes[m].x;
  125. let y_m = nodes[m].y;
  126. let km = this.K_matrix[m];
  127. let lm = this.L_matrix[m];
  128. for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
  129. let i = nodesArray[iIdx];
  130. if (i !== m) {
  131. let x_i = nodes[i].x;
  132. let y_i = nodes[i].y;
  133. let kmat = km[i];
  134. let lmat = lm[i];
  135. let denominator = 1.0 / Math.pow(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2), 1.5);
  136. d2E_dx2 += kmat * (1 - lmat * Math.pow(y_m - y_i, 2) * denominator);
  137. d2E_dxdy += kmat * (lmat * (x_m - x_i) * (y_m - y_i) * denominator);
  138. d2E_dy2 += kmat * (1 - lmat * Math.pow(x_m - x_i, 2) * denominator);
  139. }
  140. }
  141. // make the variable names easier to make the solving of the linear system easier to read
  142. let A = d2E_dx2, B = d2E_dxdy, C = dE_dx, D = d2E_dy2, E = dE_dy;
  143. // solve the linear system for dx and dy
  144. let dy = (C / A + E / B) / (B / A - D / B);
  145. let dx = -(B * dy + C) / A;
  146. // move the node
  147. nodes[m].x += dx;
  148. nodes[m].y += dy;
  149. // Recalculate E_matrix (should be incremental)
  150. this._updateE_matrix(m);
  151. }
  152. /**
  153. * Create the L matrix: edge length times shortest path
  154. * @param {Object} D_matrix
  155. * @private
  156. */
  157. _createL_matrix(D_matrix) {
  158. let nodesArray = this.body.nodeIndices;
  159. let edgeLength = this.springLength;
  160. this.L_matrix = [];
  161. for (let i = 0; i < nodesArray.length; i++) {
  162. this.L_matrix[nodesArray[i]] = {};
  163. for (let j = 0; j < nodesArray.length; j++) {
  164. this.L_matrix[nodesArray[i]][nodesArray[j]] = edgeLength * D_matrix[nodesArray[i]][nodesArray[j]];
  165. }
  166. }
  167. }
  168. /**
  169. * Create the K matrix: spring constants times shortest path
  170. * @param {Object} D_matrix
  171. * @private
  172. */
  173. _createK_matrix(D_matrix) {
  174. let nodesArray = this.body.nodeIndices;
  175. let edgeStrength = this.springConstant;
  176. this.K_matrix = [];
  177. for (let i = 0; i < nodesArray.length; i++) {
  178. this.K_matrix[nodesArray[i]] = {};
  179. for (let j = 0; j < nodesArray.length; j++) {
  180. this.K_matrix[nodesArray[i]][nodesArray[j]] = edgeStrength * Math.pow(D_matrix[nodesArray[i]][nodesArray[j]], -2);
  181. }
  182. }
  183. }
  184. /**
  185. * Create matrix with all energies between nodes
  186. * @private
  187. */
  188. _createE_matrix() {
  189. let nodesArray = this.body.nodeIndices;
  190. let nodes = this.body.nodes;
  191. this.E_matrix = {};
  192. this.E_sums = {};
  193. for (let mIdx = 0; mIdx < nodesArray.length; mIdx++) {
  194. this.E_matrix[nodesArray[mIdx]] = [];
  195. }
  196. for (let mIdx = 0; mIdx < nodesArray.length; mIdx++) {
  197. let m = nodesArray[mIdx];
  198. let x_m = nodes[m].x;
  199. let y_m = nodes[m].y;
  200. let dE_dx = 0;
  201. let dE_dy = 0;
  202. for (let iIdx = mIdx; iIdx < nodesArray.length; iIdx++) {
  203. let i = nodesArray[iIdx];
  204. if (i !== m) {
  205. let x_i = nodes[i].x;
  206. let y_i = nodes[i].y;
  207. let denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2));
  208. this.E_matrix[m][iIdx] = [
  209. this.K_matrix[m][i] * ((x_m - x_i) - this.L_matrix[m][i] * (x_m - x_i) * denominator),
  210. this.K_matrix[m][i] * ((y_m - y_i) - this.L_matrix[m][i] * (y_m - y_i) * denominator)
  211. ];
  212. this.E_matrix[i][mIdx] = this.E_matrix[m][iIdx];
  213. dE_dx += this.E_matrix[m][iIdx][0];
  214. dE_dy += this.E_matrix[m][iIdx][1];
  215. }
  216. }
  217. //Store sum
  218. this.E_sums[m] = [dE_dx, dE_dy];
  219. }
  220. }
  221. /**
  222. * Update method, just doing single column (rows are auto-updated) (update all sums)
  223. *
  224. * @param {number} m
  225. * @private
  226. */
  227. _updateE_matrix(m) {
  228. let nodesArray = this.body.nodeIndices;
  229. let nodes = this.body.nodes;
  230. let colm = this.E_matrix[m];
  231. let kcolm = this.K_matrix[m];
  232. let lcolm = this.L_matrix[m];
  233. let x_m = nodes[m].x;
  234. let y_m = nodes[m].y;
  235. let dE_dx = 0;
  236. let dE_dy = 0;
  237. for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
  238. let i = nodesArray[iIdx];
  239. if (i !== m) {
  240. //Keep old energy value for sum modification below
  241. let cell = colm[iIdx];
  242. let oldDx = cell[0];
  243. let oldDy = cell[1];
  244. //Calc new energy:
  245. let x_i = nodes[i].x;
  246. let y_i = nodes[i].y;
  247. let denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2));
  248. let dx = kcolm[i] * ((x_m - x_i) - lcolm[i] * (x_m - x_i) * denominator);
  249. let dy = kcolm[i] * ((y_m - y_i) - lcolm[i] * (y_m - y_i) * denominator);
  250. colm[iIdx] = [dx, dy];
  251. dE_dx += dx;
  252. dE_dy += dy;
  253. //add new energy to sum of each column
  254. let sum = this.E_sums[i];
  255. sum[0] += (dx-oldDx);
  256. sum[1] += (dy-oldDy);
  257. }
  258. }
  259. //Store sum at -1 index
  260. this.E_sums[m] = [dE_dx, dE_dy];
  261. }
  262. }
  263. export default KamadaKawai;