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.

218 lines
6.4 KiB

  1. /**
  2. * Created by Alex on 8/7/2015.
  3. */
  4. // distance finding algorithm
  5. import FloydWarshall from "./components/algorithms/FloydWarshall.js"
  6. /**
  7. * KamadaKawai positions the nodes initially based on
  8. *
  9. * "AN ALGORITHM FOR DRAWING GENERAL UNDIRECTED GRAPHS"
  10. * -- Tomihisa KAMADA and Satoru KAWAI in 1989
  11. *
  12. * Possible optimizations in the distance calculation can be implemented.
  13. */
  14. class KamadaKawai {
  15. constructor(body, edgeLength, edgeStrength) {
  16. this.body = body;
  17. this.springLength = edgeLength;
  18. this.springConstant = edgeStrength;
  19. this.distanceSolver = new FloydWarshall();
  20. }
  21. /**
  22. * Not sure if needed but can be used to update the spring length and spring constant
  23. * @param options
  24. */
  25. setOptions(options) {
  26. if (options) {
  27. if (options.springLength) {
  28. this.springLength = options.springLength;
  29. }
  30. if (options.springConstant) {
  31. this.springConstant = options.springConstant;
  32. }
  33. }
  34. }
  35. /**
  36. * Position the system
  37. * @param nodesArray
  38. * @param edgesArray
  39. */
  40. solve(nodesArray, edgesArray, ignoreClusters = false) {
  41. // get distance matrix
  42. let D_matrix = this.distanceSolver.getDistances(this.body, nodesArray, edgesArray); // distance matrix
  43. // get the L Matrix
  44. this._createL_matrix(D_matrix);
  45. // get the K Matrix
  46. this._createK_matrix(D_matrix);
  47. // calculate positions
  48. let threshold = 0.01;
  49. let innerThreshold = 1;
  50. let iterations = 0;
  51. let maxIterations = Math.max(1000,Math.min(10*this.body.nodeIndices.length,6000));
  52. let maxInnerIterations = 5;
  53. let maxEnergy = 1e9;
  54. let highE_nodeId = 0, dE_dx = 0, dE_dy = 0, delta_m = 0, subIterations = 0;
  55. while (maxEnergy > threshold && iterations < maxIterations) {
  56. iterations += 1;
  57. [highE_nodeId, maxEnergy, dE_dx, dE_dy] = this._getHighestEnergyNode(ignoreClusters);
  58. delta_m = maxEnergy;
  59. subIterations = 0;
  60. while(delta_m > innerThreshold && subIterations < maxInnerIterations) {
  61. subIterations += 1;
  62. this._moveNode(highE_nodeId, dE_dx, dE_dy);
  63. [delta_m,dE_dx,dE_dy] = this._getEnergy(highE_nodeId);
  64. }
  65. }
  66. }
  67. /**
  68. * get the node with the highest energy
  69. * @returns {*[]}
  70. * @private
  71. */
  72. _getHighestEnergyNode(ignoreClusters) {
  73. let nodesArray = this.body.nodeIndices;
  74. let nodes = this.body.nodes;
  75. let maxEnergy = 0;
  76. let maxEnergyNodeId = nodesArray[0];
  77. let dE_dx_max = 0, dE_dy_max = 0;
  78. for (let nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) {
  79. let m = nodesArray[nodeIdx];
  80. // by not evaluating nodes with predefined positions we should only move nodes that have no positions.
  81. if ((nodes[m].predefinedPosition === false || nodes[m].isCluster === true && ignoreClusters === true) || nodes[m].options.fixed.x === true || nodes[m].options.fixed.y === true) {
  82. let [delta_m,dE_dx,dE_dy] = this._getEnergy(m);
  83. if (maxEnergy < delta_m) {
  84. maxEnergy = delta_m;
  85. maxEnergyNodeId = m;
  86. dE_dx_max = dE_dx;
  87. dE_dy_max = dE_dy;
  88. }
  89. }
  90. }
  91. return [maxEnergyNodeId, maxEnergy, dE_dx_max, dE_dy_max];
  92. }
  93. /**
  94. * calculate the energy of a single node
  95. * @param m
  96. * @returns {*[]}
  97. * @private
  98. */
  99. _getEnergy(m) {
  100. let nodesArray = this.body.nodeIndices;
  101. let nodes = this.body.nodes;
  102. let x_m = nodes[m].x;
  103. let y_m = nodes[m].y;
  104. let dE_dx = 0;
  105. let dE_dy = 0;
  106. for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
  107. let i = nodesArray[iIdx];
  108. if (i !== m) {
  109. let x_i = nodes[i].x;
  110. let y_i = nodes[i].y;
  111. let denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2));
  112. dE_dx += this.K_matrix[m][i] * ((x_m - x_i) - this.L_matrix[m][i] * (x_m - x_i) * denominator);
  113. dE_dy += this.K_matrix[m][i] * ((y_m - y_i) - this.L_matrix[m][i] * (y_m - y_i) * denominator);
  114. }
  115. }
  116. let delta_m = Math.sqrt(Math.pow(dE_dx, 2) + Math.pow(dE_dy, 2));
  117. return [delta_m, dE_dx, dE_dy];
  118. }
  119. /**
  120. * move the node based on it's energy
  121. * the dx and dy are calculated from the linear system proposed by Kamada and Kawai
  122. * @param m
  123. * @param dE_dx
  124. * @param dE_dy
  125. * @private
  126. */
  127. _moveNode(m, dE_dx, dE_dy) {
  128. let nodesArray = this.body.nodeIndices;
  129. let nodes = this.body.nodes;
  130. let d2E_dx2 = 0;
  131. let d2E_dxdy = 0;
  132. let d2E_dy2 = 0;
  133. let x_m = nodes[m].x;
  134. let y_m = nodes[m].y;
  135. for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
  136. let i = nodesArray[iIdx];
  137. if (i !== m) {
  138. let x_i = nodes[i].x;
  139. let y_i = nodes[i].y;
  140. let denominator = 1.0 / Math.pow(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2), 1.5);
  141. d2E_dx2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(y_m - y_i, 2) * denominator);
  142. d2E_dxdy += this.K_matrix[m][i] * (this.L_matrix[m][i] * (x_m - x_i) * (y_m - y_i) * denominator);
  143. d2E_dy2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(x_m - x_i, 2) * denominator);
  144. }
  145. }
  146. // make the variable names easier to make the solving of the linear system easier to read
  147. let A = d2E_dx2, B = d2E_dxdy, C = dE_dx, D = d2E_dy2, E = dE_dy;
  148. // solve the linear system for dx and dy
  149. let dy = (C / A + E / B) / (B / A - D / B);
  150. let dx = -(B * dy + C) / A;
  151. // move the node
  152. nodes[m].x += dx;
  153. nodes[m].y += dy;
  154. }
  155. /**
  156. * Create the L matrix: edge length times shortest path
  157. * @param D_matrix
  158. * @private
  159. */
  160. _createL_matrix(D_matrix) {
  161. let nodesArray = this.body.nodeIndices;
  162. let edgeLength = this.springLength;
  163. this.L_matrix = [];
  164. for (let i = 0; i < nodesArray.length; i++) {
  165. this.L_matrix[nodesArray[i]] = {};
  166. for (let j = 0; j < nodesArray.length; j++) {
  167. this.L_matrix[nodesArray[i]][nodesArray[j]] = edgeLength * D_matrix[nodesArray[i]][nodesArray[j]];
  168. }
  169. }
  170. }
  171. /**
  172. * Create the K matrix: spring constants times shortest path
  173. * @param D_matrix
  174. * @private
  175. */
  176. _createK_matrix(D_matrix) {
  177. let nodesArray = this.body.nodeIndices;
  178. let edgeStrength = this.springConstant;
  179. this.K_matrix = [];
  180. for (let i = 0; i < nodesArray.length; i++) {
  181. this.K_matrix[nodesArray[i]] = {};
  182. for (let j = 0; j < nodesArray.length; j++) {
  183. this.K_matrix[nodesArray[i]][nodesArray[j]] = edgeStrength * Math.pow(D_matrix[nodesArray[i]][nodesArray[j]], -2);
  184. }
  185. }
  186. }
  187. }
  188. export default KamadaKawai;