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.

214 lines
6.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. // calculate positions
  45. let threshold = 0.01;
  46. let innerThreshold = 1;
  47. let iterations = 0;
  48. let maxIterations = Math.max(1000,Math.min(10*this.body.nodeIndices.length,6000));
  49. let maxInnerIterations = 5;
  50. let maxEnergy = 1e9;
  51. let highE_nodeId = 0, dE_dx = 0, dE_dy = 0, delta_m = 0, subIterations = 0;
  52. while (maxEnergy > threshold && iterations < maxIterations) {
  53. iterations += 1;
  54. [highE_nodeId, maxEnergy, dE_dx, dE_dy] = this._getHighestEnergyNode(ignoreClusters);
  55. delta_m = maxEnergy;
  56. subIterations = 0;
  57. while(delta_m > innerThreshold && subIterations < maxInnerIterations) {
  58. subIterations += 1;
  59. this._moveNode(highE_nodeId, dE_dx, dE_dy);
  60. [delta_m,dE_dx,dE_dy] = this._getEnergy(highE_nodeId);
  61. }
  62. }
  63. }
  64. /**
  65. * get the node with the highest energy
  66. * @returns {*[]}
  67. * @private
  68. */
  69. _getHighestEnergyNode(ignoreClusters) {
  70. let nodesArray = this.body.nodeIndices;
  71. let nodes = this.body.nodes;
  72. let maxEnergy = 0;
  73. let maxEnergyNodeId = nodesArray[0];
  74. let dE_dx_max = 0, dE_dy_max = 0;
  75. for (let nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) {
  76. let m = nodesArray[nodeIdx];
  77. // by not evaluating nodes with predefined positions we should only move nodes that have no positions.
  78. if ((nodes[m].predefinedPosition === false || nodes[m].isCluster === true && ignoreClusters === true) || nodes[m].options.fixed.x === true || nodes[m].options.fixed.y === true) {
  79. let [delta_m,dE_dx,dE_dy] = this._getEnergy(m);
  80. if (maxEnergy < delta_m) {
  81. maxEnergy = delta_m;
  82. maxEnergyNodeId = m;
  83. dE_dx_max = dE_dx;
  84. dE_dy_max = dE_dy;
  85. }
  86. }
  87. }
  88. return [maxEnergyNodeId, maxEnergy, dE_dx_max, dE_dy_max];
  89. }
  90. /**
  91. * calculate the energy of a single node
  92. * @param m
  93. * @returns {*[]}
  94. * @private
  95. */
  96. _getEnergy(m) {
  97. let nodesArray = this.body.nodeIndices;
  98. let nodes = this.body.nodes;
  99. let x_m = nodes[m].x;
  100. let y_m = nodes[m].y;
  101. let dE_dx = 0;
  102. let dE_dy = 0;
  103. for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
  104. let i = nodesArray[iIdx];
  105. if (i !== m) {
  106. let x_i = nodes[i].x;
  107. let y_i = nodes[i].y;
  108. let denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2));
  109. dE_dx += this.K_matrix[m][i] * ((x_m - x_i) - this.L_matrix[m][i] * (x_m - x_i) * denominator);
  110. dE_dy += this.K_matrix[m][i] * ((y_m - y_i) - this.L_matrix[m][i] * (y_m - y_i) * denominator);
  111. }
  112. }
  113. let delta_m = Math.sqrt(Math.pow(dE_dx, 2) + Math.pow(dE_dy, 2));
  114. return [delta_m, dE_dx, dE_dy];
  115. }
  116. /**
  117. * move the node based on it's energy
  118. * the dx and dy are calculated from the linear system proposed by Kamada and Kawai
  119. * @param m
  120. * @param dE_dx
  121. * @param dE_dy
  122. * @private
  123. */
  124. _moveNode(m, dE_dx, dE_dy) {
  125. let nodesArray = this.body.nodeIndices;
  126. let nodes = this.body.nodes;
  127. let d2E_dx2 = 0;
  128. let d2E_dxdy = 0;
  129. let d2E_dy2 = 0;
  130. let x_m = nodes[m].x;
  131. let y_m = nodes[m].y;
  132. for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
  133. let i = nodesArray[iIdx];
  134. if (i !== m) {
  135. let x_i = nodes[i].x;
  136. let y_i = nodes[i].y;
  137. let denominator = 1.0 / Math.pow(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2), 1.5);
  138. d2E_dx2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(y_m - y_i, 2) * denominator);
  139. d2E_dxdy += this.K_matrix[m][i] * (this.L_matrix[m][i] * (x_m - x_i) * (y_m - y_i) * denominator);
  140. d2E_dy2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(x_m - x_i, 2) * denominator);
  141. }
  142. }
  143. // make the variable names easier to make the solving of the linear system easier to read
  144. let A = d2E_dx2, B = d2E_dxdy, C = dE_dx, D = d2E_dy2, E = dE_dy;
  145. // solve the linear system for dx and dy
  146. let dy = (C / A + E / B) / (B / A - D / B);
  147. let dx = -(B * dy + C) / A;
  148. // move the node
  149. nodes[m].x += dx;
  150. nodes[m].y += dy;
  151. }
  152. /**
  153. * Create the L matrix: edge length times shortest path
  154. * @param 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 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. export default KamadaKawai;