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.

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