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.

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