From 0161b22160738d70a5ad8bba4e9d8227e575c894 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 13 Aug 2015 10:55:32 +0200 Subject: [PATCH 01/10] - added kamadaKawai algorithm as prototype --- dist/vis.js | 329 ++++++++++++++++-- lib/network/modules/LayoutEngine.js | 5 + .../modules/components/FloydWarshall.js | 46 +++ lib/network/modules/components/KamadaKawai.js | 158 +++++++++ 4 files changed, 517 insertions(+), 21 deletions(-) create mode 100644 lib/network/modules/components/FloydWarshall.js create mode 100644 lib/network/modules/components/KamadaKawai.js diff --git a/dist/vis.js b/dist/vis.js index 2d1ea21a..e080d0f2 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 4.7.1-SNAPSHOT - * @date 2015-08-07 + * @date 2015-08-12 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -139,10 +139,10 @@ return /******/ (function(modules) { // webpackBootstrap // Network exports.Network = __webpack_require__(59); exports.network = { - Images: __webpack_require__(114), - dotparser: __webpack_require__(112), - gephiParser: __webpack_require__(113), - allOptions: __webpack_require__(110) + Images: __webpack_require__(116), + dotparser: __webpack_require__(114), + gephiParser: __webpack_require__(115), + allOptions: __webpack_require__(112) }; exports.network.convertDot = function (input) { return exports.network.dotparser.DOTToGraph(input); @@ -26823,7 +26823,7 @@ return /******/ (function(modules) { // webpackBootstrap var _modulesLayoutEngine2 = _interopRequireDefault(_modulesLayoutEngine); - var _modulesManipulationSystem = __webpack_require__(109); + var _modulesManipulationSystem = __webpack_require__(111); var _modulesManipulationSystem2 = _interopRequireDefault(_modulesManipulationSystem); @@ -26835,7 +26835,7 @@ return /******/ (function(modules) { // webpackBootstrap var _sharedValidator2 = _interopRequireDefault(_sharedValidator); - var _optionsJs = __webpack_require__(110); + var _optionsJs = __webpack_require__(112); /** * @constructor Network @@ -26848,18 +26848,18 @@ return /******/ (function(modules) { // webpackBootstrap * {Array} edges * @param {Object} options Options */ - __webpack_require__(111); + __webpack_require__(113); var Emitter = __webpack_require__(19); var Hammer = __webpack_require__(3); var util = __webpack_require__(7); var DataSet = __webpack_require__(14); var DataView = __webpack_require__(16); - var dotparser = __webpack_require__(112); - var gephiParser = __webpack_require__(113); - var Images = __webpack_require__(114); + var dotparser = __webpack_require__(114); + var gephiParser = __webpack_require__(115); + var Images = __webpack_require__(116); var Activator = __webpack_require__(40); - var locales = __webpack_require__(115); + var locales = __webpack_require__(117); function Network(container, data, options) { var _this = this; @@ -28216,7 +28216,6 @@ return /******/ (function(modules) { // webpackBootstrap if (!options) { return; } - // basic options if (options.id !== undefined) { this.id = options.id; @@ -28290,7 +28289,6 @@ return /******/ (function(modules) { // webpackBootstrap if (this.options.label === undefined || this.options.label === null) { this.options.label = ''; } - this.labelModule.setOptions(this.options, true); if (this.labelModule.baseSize !== undefined) { this.baseFontSize = this.labelModule.baseSize; @@ -38665,15 +38663,20 @@ return /******/ (function(modules) { // webpackBootstrap /***/ function(module, exports, __webpack_require__) { 'use strict'; - Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + var _componentsKamadaKawaiJs = __webpack_require__(109); + + var _componentsKamadaKawaiJs2 = _interopRequireDefault(_componentsKamadaKawaiJs); + var util = __webpack_require__(7); var LayoutEngine = (function () { @@ -38857,6 +38860,10 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: 'setupHierarchicalLayout', value: function setupHierarchicalLayout() { + var kk = new _componentsKamadaKawaiJs2['default'](this.body, 100, 0.05); + kk.solve(this.body.nodeIndices, this.body.edgeIndices); + return; + if (this.options.hierarchical.enabled === true && this.body.nodeIndices.length > 0) { // get the size of the largest hubs and check if the user has defined a level for a node. var node = undefined, @@ -39177,6 +39184,286 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, /* 109 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * Created by Alex on 8/7/2015. + */ + + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; })(); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var _FloydWarshallJs = __webpack_require__(110); + + var _FloydWarshallJs2 = _interopRequireDefault(_FloydWarshallJs); + + var KamadaKawai = (function () { + function KamadaKawai(body, edgeLength, edgeStrength) { + _classCallCheck(this, KamadaKawai); + + this.body = body; + this.springLength = edgeLength; + this.springConstant = edgeStrength; + this.distanceSolver = new _FloydWarshallJs2["default"](); + } + + _createClass(KamadaKawai, [{ + key: "setOptions", + value: function setOptions(options) { + if (options) { + if (options.springLength) { + this.springLength = options.springLength; + } + if (options.springConstant) { + this.springConstant = options.springConstant; + } + } + } + }, { + key: "solve", + value: function solve(nodesArray, edgesArray) { + console.time("FLOYD - getDistances"); + var D_matrix = this.distanceSolver.getDistances(this.body, nodesArray, edgesArray); // distance matrix + console.timeEnd("FLOYD - getDistances"); + + // get the L Matrix + this._createL_matrix(D_matrix); + + // get the K Matrix + this._createK_matrix(D_matrix); + + console.time("positioning"); + var threshold = 0.01; + var counter = 0; + var maxIterations = 1500; + var maxEnergy = 2 * threshold; + var highE_nodeId = 0, + dE_dx = 0, + dE_dy = 0; + + while (maxEnergy > threshold && counter < maxIterations) { + counter += 1; + + var _getHighestEnergyNode2 = this._getHighestEnergyNode(); + + var _getHighestEnergyNode22 = _slicedToArray(_getHighestEnergyNode2, 4); + + highE_nodeId = _getHighestEnergyNode22[0]; + maxEnergy = _getHighestEnergyNode22[1]; + dE_dx = _getHighestEnergyNode22[2]; + dE_dy = _getHighestEnergyNode22[3]; + + this._moveNode(highE_nodeId, dE_dx, dE_dy); + } + console.timeEnd("positioning"); + } + }, { + key: "_getHighestEnergyNode", + value: function _getHighestEnergyNode() { + var nodesArray = this.body.nodeIndices; + var maxEnergy = 0; + var maxEnergyNode = nodesArray[0]; + var energies = { dE_dx: 0, dE_dy: 0 }; + + for (var nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) { + var m = nodesArray[nodeIdx]; + + var _getEnergy2 = this._getEnergy(m); + + var _getEnergy22 = _slicedToArray(_getEnergy2, 3); + + var delta_m = _getEnergy22[0]; + var dE_dx = _getEnergy22[1]; + var dE_dy = _getEnergy22[2]; + + if (maxEnergy < delta_m) { + maxEnergy = delta_m; + maxEnergyNode = m; + energies.dE_dx = dE_dx; + energies.dE_dy = dE_dy; + } + } + + return [maxEnergyNode, maxEnergy, energies.dE_dx, energies.dE_dy]; + } + }, { + key: "_getEnergy", + value: function _getEnergy(m) { + var nodesArray = this.body.nodeIndices; + var nodes = this.body.nodes; + + var x_m = nodes[m].x; + var y_m = nodes[m].y; + var dE_dx = 0; + var dE_dy = 0; + for (var iIdx = 0; iIdx < nodesArray.length; iIdx++) { + var i = nodesArray[iIdx]; + if (i !== m) { + var x_i = nodes[i].x; + var y_i = nodes[i].y; + var denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2)); + dE_dx += this.K_matrix[m][i] * (x_m - x_i - this.L_matrix[m][i] * (x_m - x_i) * denominator); + dE_dy += this.K_matrix[m][i] * (y_m - y_i - this.L_matrix[m][i] * (y_m - y_i) * denominator); + } + } + + var delta_m = Math.sqrt(Math.pow(dE_dx, 2) + Math.pow(dE_dy, 2)); + return [delta_m, dE_dx, dE_dy]; + } + }, { + key: "_moveNode", + value: function _moveNode(m, dE_dx, dE_dy) { + var nodesArray = this.body.nodeIndices; + var nodes = this.body.nodes; + var d2E_dx2 = 0; + var d2E_dxdy = 0; + var d2E_dy2 = 0; + + var x_m = nodes[m].x; + var y_m = nodes[m].y; + for (var iIdx = 0; iIdx < nodesArray.length; iIdx++) { + var i = nodesArray[iIdx]; + if (i !== m) { + var x_i = nodes[i].x; + var y_i = nodes[i].y; + var denominator = 1.0 / Math.pow(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2), 1.5); + d2E_dx2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(y_m - y_i, 2) * denominator); + d2E_dxdy += this.K_matrix[m][i] * (this.L_matrix[m][i] * (x_m - x_i) * (y_m - y_i) * denominator); + d2E_dy2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(x_m - x_i, 2) * denominator); + } + } + // make the variable names easier to make the solving of the linear system easier to read + var A = d2E_dx2, + B = d2E_dxdy, + C = dE_dx, + D = d2E_dy2, + E = dE_dy; + + // solve the linear system for dx and dy + var dy = (C / A + E / B) / (B / A - D / B); + var dx = -(B * dy + C) / A; + + // move the node + nodes[m].x += dx; + nodes[m].y += dy; + } + }, { + key: "_createL_matrix", + value: function _createL_matrix(D_matrix) { + var nodesArray = this.body.nodeIndices; + var edgeLength = this.springLength; + + this.L_matrix = []; + for (var i = 0; i < nodesArray.length; i++) { + this.L_matrix[nodesArray[i]] = {}; + for (var j = 0; j < nodesArray.length; j++) { + this.L_matrix[nodesArray[i]][nodesArray[j]] = edgeLength * D_matrix[nodesArray[i]][nodesArray[j]]; + } + } + } + }, { + key: "_createK_matrix", + value: function _createK_matrix(D_matrix) { + var nodesArray = this.body.nodeIndices; + var edgeStrength = this.springConstant; + + this.K_matrix = []; + for (var i = 0; i < nodesArray.length; i++) { + this.K_matrix[nodesArray[i]] = {}; + for (var j = 0; j < nodesArray.length; j++) { + this.K_matrix[nodesArray[i]][nodesArray[j]] = edgeStrength * Math.pow(D_matrix[nodesArray[i]][nodesArray[j]], -2); + } + } + } + }]); + + return KamadaKawai; + })(); + + exports["default"] = KamadaKawai; + module.exports = exports["default"]; + +/***/ }, +/* 110 */ +/***/ function(module, exports) { + + /** + * Created by Alex on 10-Aug-15. + */ + + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var FloydWarshall = (function () { + function FloydWarshall() { + _classCallCheck(this, FloydWarshall); + } + + _createClass(FloydWarshall, [{ + key: "getDistances", + value: function getDistances(body, nodesArray, edgesArray) { + var D_matrix = {}; + var edges = body.edges; + + // prepare matrix with large numbers + for (var i = 0; i < nodesArray.length; i++) { + D_matrix[nodesArray[i]] = {}; + for (var j = 0; j < nodesArray.length; j++) { + D_matrix[nodesArray[i]][nodesArray[j]] = 1e9; + } + } + + // put the weights for the edges in. This assumes unidirectionality. + for (var i = 0; i < edgesArray.length; i++) { + var edge = edges[edgesArray[i]]; + D_matrix[edge.fromId][edge.toId] = 1; + D_matrix[edge.toId][edge.fromId] = 1; + } + + // calculate all pair distances + for (var k = 0; k < nodesArray.length; k++) { + for (var i = 0; i < nodesArray.length; i++) { + for (var j = 0; j < nodesArray.length; j++) { + D_matrix[nodesArray[i]][nodesArray[j]] = Math.min(D_matrix[nodesArray[i]][nodesArray[j]], D_matrix[nodesArray[i]][nodesArray[k]] + D_matrix[nodesArray[k]][nodesArray[j]]); + } + } + } + + // remove the self references from the matrix + for (var i = 0; i < nodesArray.length; i++) { + delete D_matrix[nodesArray[i]][nodesArray[i]]; + } + + return D_matrix; + } + }]); + + return FloydWarshall; + })(); + + exports["default"] = FloydWarshall; + module.exports = exports["default"]; + +/***/ }, +/* 111 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -40390,7 +40677,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 110 */ +/* 112 */ /***/ function(module, exports) { /** @@ -40886,7 +41173,7 @@ return /******/ (function(modules) { // webpackBootstrap exports.configureOptions = configureOptions; /***/ }, -/* 111 */ +/* 113 */ /***/ function(module, exports) { /** @@ -41173,7 +41460,7 @@ return /******/ (function(modules) { // webpackBootstrap } /***/ }, -/* 112 */ +/* 114 */ /***/ function(module, exports) { /** @@ -42071,7 +42358,7 @@ return /******/ (function(modules) { // webpackBootstrap exports.DOTToGraph = DOTToGraph; /***/ }, -/* 113 */ +/* 115 */ /***/ function(module, exports) { 'use strict'; @@ -42143,7 +42430,7 @@ return /******/ (function(modules) { // webpackBootstrap exports.parseGephi = parseGephi; /***/ }, -/* 114 */ +/* 116 */ /***/ function(module, exports) { /** @@ -42269,7 +42556,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 115 */ +/* 117 */ /***/ function(module, exports) { // English diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index be54e151..3a2a46fd 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -1,4 +1,5 @@ 'use strict' +import KamadaKawai from "./components/KamadaKawai.js" var util = require('../../util'); @@ -177,6 +178,10 @@ class LayoutEngine { * @private */ setupHierarchicalLayout() { + let kk = new KamadaKawai(this.body,100,0.05); + kk.solve(this.body.nodeIndices, this.body.edgeIndices); + return + if (this.options.hierarchical.enabled === true && this.body.nodeIndices.length > 0) { // get the size of the largest hubs and check if the user has defined a level for a node. let node, nodeId; diff --git a/lib/network/modules/components/FloydWarshall.js b/lib/network/modules/components/FloydWarshall.js new file mode 100644 index 00000000..dd1ec067 --- /dev/null +++ b/lib/network/modules/components/FloydWarshall.js @@ -0,0 +1,46 @@ +/** + * Created by Alex on 10-Aug-15. + */ + + +class FloydWarshall { + constructor(){} + + getDistances(body, nodesArray, edgesArray) { + let D_matrix = {} + let edges = body.edges; + + // prepare matrix with large numbers + for (let i = 0; i < nodesArray.length; i++) { + D_matrix[nodesArray[i]] = {}; + for (let j = 0; j < nodesArray.length; j++) { + D_matrix[nodesArray[i]][nodesArray[j]] = 1e9; + } + } + + // put the weights for the edges in. This assumes unidirectionality. + for (let i = 0; i < edgesArray.length; i++) { + let edge = edges[edgesArray[i]]; + D_matrix[edge.fromId][edge.toId] = 1; + D_matrix[edge.toId][edge.fromId] = 1; + } + + // calculate all pair distances + for (let k = 0; k < nodesArray.length; k++) { + for (let i = 0; i < nodesArray.length; i++) { + for (let j = 0; j < nodesArray.length; j++) { + D_matrix[nodesArray[i]][nodesArray[j]] = Math.min(D_matrix[nodesArray[i]][nodesArray[j]],D_matrix[nodesArray[i]][nodesArray[k]] + D_matrix[nodesArray[k]][nodesArray[j]]) + } + } + } + + // remove the self references from the matrix + for (let i = 0; i < nodesArray.length; i++) { + delete D_matrix[nodesArray[i]][nodesArray[i]]; + } + + return D_matrix; + } +} + +export default FloydWarshall; \ No newline at end of file diff --git a/lib/network/modules/components/KamadaKawai.js b/lib/network/modules/components/KamadaKawai.js new file mode 100644 index 00000000..1023d1d5 --- /dev/null +++ b/lib/network/modules/components/KamadaKawai.js @@ -0,0 +1,158 @@ +/** + * Created by Alex on 8/7/2015. + */ + +import FloydWarshall from "./FloydWarshall.js" + +class KamadaKawai { + constructor(body, edgeLength, edgeStrength) { + this.body = body; + this.springLength = edgeLength; + this.springConstant = edgeStrength; + this.distanceSolver = new FloydWarshall(); + } + + setOptions(options) { + if (options) { + if (options.springLength) { + this.springLength = options.springLength; + } + if (options.springConstant) { + this.springConstant = options.springConstant; + } + } + } + + solve(nodesArray, edgesArray) { + console.time("FLOYD - getDistances"); + let D_matrix = this.distanceSolver.getDistances(this.body, nodesArray, edgesArray); // distance matrix + console.timeEnd("FLOYD - getDistances"); + + // get the L Matrix + this._createL_matrix(D_matrix); + + // get the K Matrix + this._createK_matrix(D_matrix); + + console.time("positioning") + let threshold = 0.01; + let counter = 0; + let maxIterations = Math.min(10*this.body.nodeIndices.length);; + let maxEnergy = 1e9; // just to pass the first check. + let highE_nodeId = 0, dE_dx = 0, dE_dy = 0; + + while (maxEnergy > threshold && counter < maxIterations) { + counter += 1; + [highE_nodeId, maxEnergy, dE_dx, dE_dy] = this._getHighestEnergyNode(); + this._moveNode(highE_nodeId, dE_dx, dE_dy); + } + console.timeEnd("positioning") + } + + + _getHighestEnergyNode() { + let nodesArray = this.body.nodeIndices; + let maxEnergy = 0; + let maxEnergyNode = nodesArray[0]; + let energies = {dE_dx: 0, dE_dy: 0}; + + for (let nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) { + let m = nodesArray[nodeIdx]; + let [delta_m,dE_dx,dE_dy] = this._getEnergy(m); + if (maxEnergy < delta_m) { + maxEnergy = delta_m; + maxEnergyNode = m; + energies.dE_dx = dE_dx; + energies.dE_dy = dE_dy; + } + } + + return [maxEnergyNode, maxEnergy, energies.dE_dx, energies.dE_dy]; + } + + _getEnergy(m) { + let nodesArray = this.body.nodeIndices; + let nodes = this.body.nodes; + + let x_m = nodes[m].x; + let y_m = nodes[m].y; + let dE_dx = 0; + let dE_dy = 0; + for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) { + let i = nodesArray[iIdx]; + if (i !== m) { + let x_i = nodes[i].x; + let y_i = nodes[i].y; + let denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2)); + dE_dx += this.K_matrix[m][i] * ((x_m - x_i) - this.L_matrix[m][i] * (x_m - x_i) * denominator); + dE_dy += this.K_matrix[m][i] * ((y_m - y_i) - this.L_matrix[m][i] * (y_m - y_i) * denominator); + } + } + + let delta_m = Math.sqrt(Math.pow(dE_dx, 2) + Math.pow(dE_dy, 2)); + return [delta_m, dE_dx, dE_dy]; + } + + _moveNode(m, dE_dx, dE_dy) { + let nodesArray = this.body.nodeIndices; + let nodes = this.body.nodes; + let d2E_dx2 = 0; + let d2E_dxdy = 0; + let d2E_dy2 = 0; + + let x_m = nodes[m].x; + let y_m = nodes[m].y; + for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) { + let i = nodesArray[iIdx]; + if (i !== m) { + let x_i = nodes[i].x; + let y_i = nodes[i].y; + let denominator = 1.0 / Math.pow(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2), 1.5); + d2E_dx2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(y_m - y_i, 2) * denominator); + d2E_dxdy += this.K_matrix[m][i] * (this.L_matrix[m][i] * (x_m - x_i) * (y_m - y_i) * denominator); + d2E_dy2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(x_m - x_i, 2) * denominator); + } + } + // make the variable names easier to make the solving of the linear system easier to read + let A = d2E_dx2, B = d2E_dxdy, C = dE_dx, D = d2E_dy2, E = dE_dy; + + // solve the linear system for dx and dy + let dy = (C / A + E / B) / (B / A - D / B); + let dx = -(B * dy + C) / A; + + // move the node + nodes[m].x += dx; + nodes[m].y += dy; + } + + _createL_matrix(D_matrix) { + let nodesArray = this.body.nodeIndices; + let edgeLength = this.springLength; + + this.L_matrix = []; + for (let i = 0; i < nodesArray.length; i++) { + this.L_matrix[nodesArray[i]] = {}; + for (let j = 0; j < nodesArray.length; j++) { + this.L_matrix[nodesArray[i]][nodesArray[j]] = edgeLength * D_matrix[nodesArray[i]][nodesArray[j]]; + } + } + } + + _createK_matrix(D_matrix) { + let nodesArray = this.body.nodeIndices; + let edgeStrength = this.springConstant; + + this.K_matrix = []; + for (let i = 0; i < nodesArray.length; i++) { + this.K_matrix[nodesArray[i]] = {}; + for (let j = 0; j < nodesArray.length; j++) { + this.K_matrix[nodesArray[i]][nodesArray[j]] = edgeStrength * Math.pow(D_matrix[nodesArray[i]][nodesArray[j]], -2); + } + } + } + + + +} + +export default KamadaKawai; \ No newline at end of file From 09d3dfbb09ca9cd3b617c94bba93f9d9bde4e4bf Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 13 Aug 2015 19:41:41 +0200 Subject: [PATCH 02/10] - made it pretty, fixed bugs in clustering --- dist/vis.js | 896 +++++++++++------- lib/network/Network.js | 11 +- lib/network/modules/Clustering.js | 89 +- .../modules/{components => }/KamadaKawai.js | 113 ++- lib/network/modules/LayoutEngine.js | 82 +- lib/network/modules/PhysicsEngine.js | 16 +- .../{ => algorithms}/FloydWarshall.js | 22 +- .../components/edges/BezierEdgeDynamic.js | 3 + 8 files changed, 793 insertions(+), 439 deletions(-) rename lib/network/modules/{components => }/KamadaKawai.js (58%) rename lib/network/modules/components/{ => algorithms}/FloydWarshall.js (59%) diff --git a/dist/vis.js b/dist/vis.js index e080d0f2..f6deb51c 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 4.7.1-SNAPSHOT - * @date 2015-08-12 + * @date 2015-08-13 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -142,7 +142,7 @@ return /******/ (function(modules) { // webpackBootstrap Images: __webpack_require__(116), dotparser: __webpack_require__(114), gephiParser: __webpack_require__(115), - allOptions: __webpack_require__(112) + allOptions: __webpack_require__(110) }; exports.network.convertDot = function (input) { return exports.network.dotparser.DOTToGraph(input); @@ -26823,7 +26823,7 @@ return /******/ (function(modules) { // webpackBootstrap var _modulesLayoutEngine2 = _interopRequireDefault(_modulesLayoutEngine); - var _modulesManipulationSystem = __webpack_require__(111); + var _modulesManipulationSystem = __webpack_require__(109); var _modulesManipulationSystem2 = _interopRequireDefault(_modulesManipulationSystem); @@ -26835,7 +26835,9 @@ return /******/ (function(modules) { // webpackBootstrap var _sharedValidator2 = _interopRequireDefault(_sharedValidator); - var _optionsJs = __webpack_require__(112); + var _optionsJs = __webpack_require__(110); + + var _modulesKamadaKawaiJs = __webpack_require__(111); /** * @constructor Network @@ -26848,6 +26850,9 @@ return /******/ (function(modules) { // webpackBootstrap * {Array} edges * @param {Object} options Options */ + + var _modulesKamadaKawaiJs2 = _interopRequireDefault(_modulesKamadaKawaiJs); + __webpack_require__(113); var Emitter = __webpack_require__(19); @@ -26913,6 +26918,7 @@ return /******/ (function(modules) { // webpackBootstrap createEdge: function createEdge() {}, getPointer: function getPointer() {} }, + modules: {}, view: { scale: 1, translation: { x: 0, y: 0 } @@ -26940,6 +26946,9 @@ return /******/ (function(modules) { // webpackBootstrap this.nodesHandler = new _modulesNodesHandler2['default'](this.body, this.images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options this.edgesHandler = new _modulesEdgesHandler2['default'](this.body, this.images, this.groups); // Handle adding, deleting and updating of edges as well as global options + this.body.modules["kamadaKawai"] = new _modulesKamadaKawaiJs2['default'](this.body, 150, 0.05); // Layouting algorithm. + this.body.modules["clustering"] = this.clustering; + // create the DOM elements this.canvas._create(); @@ -27149,6 +27158,9 @@ return /******/ (function(modules) { // webpackBootstrap // emit change in data this.body.emitter.emit("_dataChanged"); + // emit data loaded + this.body.emitter.emit("_dataLoaded"); + // find a stable position or start animating to a stable position this.body.emitter.emit("initPhysics"); }; @@ -32666,21 +32678,21 @@ return /******/ (function(modules) { // webpackBootstrap /* 88 */ /***/ function(module, exports, __webpack_require__) { - 'use strict'; + "use strict"; - Object.defineProperty(exports, '__esModule', { + Object.defineProperty(exports, "__esModule", { value: true }); - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } var _utilBezierEdgeBase = __webpack_require__(84); @@ -32690,14 +32702,20 @@ return /******/ (function(modules) { // webpackBootstrap _inherits(BezierEdgeDynamic, _BezierEdgeBase); function BezierEdgeDynamic(options, body, labelModule) { + var _this = this; + _classCallCheck(this, BezierEdgeDynamic); //this.via = undefined; // Here for completeness but not allowed to defined before super() is invoked. - _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), 'constructor', this).call(this, options, body, labelModule); // --> this calls the setOptions below + _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), "constructor", this).call(this, options, body, labelModule); // --> this calls the setOptions below + this._boundFunction = function () { + _this.positionBezierNode(); + }; + this.body.emitter.on("_repositionBezierNodes", this._boundFunction); } _createClass(BezierEdgeDynamic, [{ - key: 'setOptions', + key: "setOptions", value: function setOptions(options) { this.options = options; this.id = this.options.id; @@ -32711,7 +32729,7 @@ return /******/ (function(modules) { // webpackBootstrap this.connect(); } }, { - key: 'connect', + key: "connect", value: function connect() { this.from = this.body.nodes[this.options.from]; this.to = this.body.nodes[this.options.to]; @@ -32732,8 +32750,9 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {boolean} */ }, { - key: 'cleanup', + key: "cleanup", value: function cleanup() { + this.body.emitter.off("_repositionBezierNodes", this._boundFunction); if (this.via !== undefined) { delete this.body.nodes[this.via.id]; this.via = undefined; @@ -32750,7 +32769,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ }, { - key: 'setupSupportNode', + key: "setupSupportNode", value: function setupSupportNode() { if (this.via === undefined) { var nodeId = "edgeId:" + this.id; @@ -32767,7 +32786,7 @@ return /******/ (function(modules) { // webpackBootstrap } } }, { - key: 'positionBezierNode', + key: "positionBezierNode", value: function positionBezierNode() { if (this.via !== undefined && this.from !== undefined && this.to !== undefined) { this.via.x = 0.5 * (this.from.x + this.to.x); @@ -32784,7 +32803,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ }, { - key: '_line', + key: "_line", value: function _line(ctx) { // draw a straight line ctx.beginPath(); @@ -32805,7 +32824,7 @@ return /******/ (function(modules) { // webpackBootstrap * @private */ }, { - key: 'getPoint', + key: "getPoint", value: function getPoint(percentage) { var t = percentage; var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * this.via.x + Math.pow(t, 2) * this.to.x; @@ -32814,12 +32833,12 @@ return /******/ (function(modules) { // webpackBootstrap return { x: x, y: y }; } }, { - key: '_findBorderPosition', + key: "_findBorderPosition", value: function _findBorderPosition(nearNode, ctx) { return this._findBorderPositionBezier(nearNode, ctx, this.via); } }, { - key: '_getDistanceToEdge', + key: "_getDistanceToEdge", value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { // x3,y3 is the point return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via); @@ -32827,10 +32846,10 @@ return /******/ (function(modules) { // webpackBootstrap }]); return BezierEdgeDynamic; - })(_utilBezierEdgeBase2['default']); + })(_utilBezierEdgeBase2["default"]); - exports['default'] = BezierEdgeDynamic; - module.exports = exports['default']; + exports["default"] = BezierEdgeDynamic; + module.exports = exports["default"]; /***/ }, /* 89 */ @@ -33000,7 +33019,6 @@ return /******/ (function(modules) { // webpackBootstrap this.previousStates = {}; this.freezeCache = {}; this.renderTimer = undefined; - this.initialStabilizationEmitted = false; this.stabilized = false; this.startedStabilization = false; @@ -33229,14 +33247,6 @@ return /******/ (function(modules) { // webpackBootstrap } if (this.stabilized === true) { - if (this.stabilizationIterations > 1) { - // trigger the 'stabilized' event. - // The event is triggered on the next tick, to prevent the case that - // it is fired while initializing the Network, in which case you would not - // be able to catch it - this.startedStabilization = false; - //this._emitStabilized(); - } this.stopSimulation(); } } @@ -33245,8 +33255,7 @@ return /******/ (function(modules) { // webpackBootstrap value: function _emitStabilized() { var _this2 = this; - if (this.stabilizationIterations > 1 || this.initialStabilizationEmitted === false) { - this.initialStabilizationEmitted = true; + if (this.stabilizationIterations > 1) { setTimeout(function () { _this2.body.emitter.emit('stabilized', { iterations: _this2.stabilizationIterations }); _this2.stabilizationIterations = 0; @@ -33562,6 +33571,7 @@ return /******/ (function(modules) { // webpackBootstrap this.body.emitter.emit('_requestRedraw'); if (this.stabilized === true) { + console.log("emitted"); this._emitStabilized(); } else { this.startSimulation(); @@ -34772,6 +34782,7 @@ return /******/ (function(modules) { // webpackBootstrap for (var i = 0; i < nodesToCluster.length; i++) { this.clusterByConnection(nodesToCluster[i], options, false); } + this.body.emitter.emit('_dataChanged'); } @@ -34816,14 +34827,15 @@ return /******/ (function(modules) { // webpackBootstrap } /** - * Cluster all nodes in the network that have only 1 edge - * @param options - * @param refreshData - */ + * Cluster all nodes in the network that have only X edges + * @param edgeCount + * @param options + * @param refreshData + */ }, { - key: 'clusterOutliers', - value: function clusterOutliers(options) { - var refreshData = arguments.length <= 1 || arguments[1] === undefined ? true : arguments[1]; + key: 'clusterByEdgeCount', + value: function clusterByEdgeCount(edgeCount, options) { + var refreshData = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2]; options = this._checkOptions(options); var clusters = []; @@ -34842,8 +34854,8 @@ return /******/ (function(modules) { // webpackBootstrap } } - if (visibleEdges === 1) { - // this is an outlier + if (visibleEdges === edgeCount) { + // this is a qualifying node var childNodeId = this._getConnectedId(edge, nodeId); if (childNodeId !== nodeId) { if (options.joinCondition === undefined) { @@ -34880,6 +34892,32 @@ return /******/ (function(modules) { // webpackBootstrap this.body.emitter.emit('_dataChanged'); } } + + /** + * Cluster all nodes in the network that have only 1 edge + * @param options + * @param refreshData + */ + }, { + key: 'clusterOutliers', + value: function clusterOutliers(options) { + var refreshData = arguments.length <= 1 || arguments[1] === undefined ? true : arguments[1]; + + this.clusterByEdgeCount(1, options, refreshData); + } + + /** + * Cluster all nodes in the network that have only 2 edge + * @param options + * @param refreshData + */ + }, { + key: 'clusterBridges', + value: function clusterBridges(options) { + var refreshData = arguments.length <= 1 || arguments[1] === undefined ? true : arguments[1]; + + this.clusterByEdgeCount(2, options, refreshData); + } }, { key: '_checkIfUsed', value: function _checkIfUsed(clusters, nodeId, edgeId) { @@ -34936,23 +34974,24 @@ return /******/ (function(modules) { // webpackBootstrap var edge = node.edges[i]; var childNodeId = this._getConnectedId(edge, parentNodeId); - if (childNodeId !== parentNodeId) { - if (options.joinCondition === undefined) { - childEdgesObj[edge.id] = edge; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } else { - // clone the options and insert some additional parameters that could be interesting. - var childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]); - if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) { + if (this.clusteredNodes[childNodeId] === undefined) { + if (childNodeId !== parentNodeId) { + if (options.joinCondition === undefined) { childEdgesObj[edge.id] = edge; childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } else { + // clone the options and insert some additional parameters that could be interesting. + var childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]); + if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } } + } else { + childEdgesObj[edge.id] = edge; } - } else { - childEdgesObj[edge.id] = edge; } } - this._cluster(childNodesObj, childEdgesObj, options, refreshData); } @@ -35067,11 +35106,20 @@ return /******/ (function(modules) { // webpackBootstrap value: function _cluster(childNodesObj, childEdgesObj, options) { var refreshData = arguments.length <= 3 || arguments[3] === undefined ? true : arguments[3]; - // kill condition: no children so cant cluster + // kill condition: no children so can't cluster if (Object.keys(childNodesObj).length === 0) { return; } + // check if this cluster call is not trying to cluster anything that is in another cluster. + for (var nodeId in childNodesObj) { + if (childNodesObj.hasOwnProperty(nodeId)) { + if (this.clusteredNodes[nodeId] !== undefined) { + return; + } + } + } + var clusterNodeProperties = util.deepExtend({}, options.clusterNodeProperties); // construct the clusterNodeProperties @@ -35079,17 +35127,21 @@ return /******/ (function(modules) { // webpackBootstrap // get the childNode options var childNodesOptions = []; for (var nodeId in childNodesObj) { - var clonedOptions = this._cloneOptions(childNodesObj[nodeId]); - childNodesOptions.push(clonedOptions); + if (childNodesObj.hasOwnProperty(nodeId)) { + var clonedOptions = this._cloneOptions(childNodesObj[nodeId]); + childNodesOptions.push(clonedOptions); + } } // get clusterproperties based on childNodes var childEdgesOptions = []; for (var edgeId in childEdgesObj) { - // these cluster edges will be removed on creation of the cluster. - if (edgeId.substr(0, 12) !== "clusterEdge:") { - var clonedOptions = this._cloneOptions(childEdgesObj[edgeId], 'edge'); - childEdgesOptions.push(clonedOptions); + if (childEdgesObj.hasOwnProperty(edgeId)) { + // these cluster edges will be removed on creation of the cluster. + if (edgeId.substr(0, 12) !== "clusterEdge:") { + var clonedOptions = this._cloneOptions(childEdgesObj[edgeId], 'edge'); + childEdgesOptions.push(clonedOptions); + } } } @@ -38663,20 +38715,15 @@ return /******/ (function(modules) { // webpackBootstrap /***/ function(module, exports, __webpack_require__) { 'use strict'; + Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _componentsKamadaKawaiJs = __webpack_require__(109); - - var _componentsKamadaKawaiJs2 = _interopRequireDefault(_componentsKamadaKawaiJs); - var util = __webpack_require__(7); var LayoutEngine = (function () { @@ -38714,6 +38761,9 @@ return /******/ (function(modules) { // webpackBootstrap this.body.emitter.on('_dataChanged', function () { _this.setupHierarchicalLayout(); }); + this.body.emitter.on('_dataLoaded', function () { + _this.layoutNetwork(); + }); this.body.emitter.on('_resetHierarchicalLayout', function () { _this.setupHierarchicalLayout(); }); @@ -38845,6 +38895,73 @@ return /******/ (function(modules) { // webpackBootstrap } } } + }, { + key: 'layoutNetwork', + value: function layoutNetwork() { + // first check if we should KamadaKawai to layout. The threshold is if less than half of the visible + // nodes have predefined positions we use this. + var positionDefined = 0; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.predefinedPosition === true) { + positionDefined += 1; + } + } + + // if less than half of the nodes have a predefined position we continue + if (positionDefined < 0.5 * this.body.nodeIndices.length) { + var levels = 0; + // if there are a lot of nodes, we cluster before we run the algorithm. + if (this.body.nodeIndices.length > 100) { + var startLength = this.body.nodeIndices.length; + while (this.body.nodeIndices.length > 150) { + levels += 1; + if (levels % 5 === 0) { + this.body.modules.clustering.clusterByHubsize(); + } else if (levels % 3 === 0) { + this.body.modules.clustering.clusterBridges(); + } else { + this.body.modules.clustering.clusterOutliers(); + } + console.log('levels', levels); + } + this.body.modules.kamadaKawai.setOptions({ springLength: Math.max(150, 2 * startLength) }); + } + + // position the system for these nodes and edges + this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true); + + // uncluster all clusters + if (levels > 0) { + var clustersPresent = true; + while (clustersPresent === true) { + clustersPresent = false; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) { + clustersPresent = true; + this.body.modules.clustering.openCluster(this.body.nodeIndices[i], { + releaseFunction: function releaseFunction(clusterPosition, containedNodesPositions) { + var newPositions = {}; + for (var nodeId in containedNodesPositions) { + if (containedNodesPositions.hasOwnProperty(nodeId)) { + newPositions[nodeId] = { x: clusterPosition.x, y: clusterPosition.y }; + } + } + return newPositions; + } + }, false); + } + } + if (clustersPresent === true) { + this.body.emitter.emit('_dataChanged'); + } + } + } + + // reposition all bezier nodes. + this.body.emitter.emit("_repositionBezierNodes"); + } + } }, { key: 'getSeed', value: function getSeed() { @@ -38860,10 +38977,6 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: 'setupHierarchicalLayout', value: function setupHierarchicalLayout() { - var kk = new _componentsKamadaKawaiJs2['default'](this.body, 100, 0.05); - kk.solve(this.body.nodeIndices, this.body.edgeIndices); - return; - if (this.options.hierarchical.enabled === true && this.body.nodeIndices.length > 0) { // get the size of the largest hubs and check if the user has defined a level for a node. var node = undefined, @@ -39184,286 +39297,6 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, /* 109 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Created by Alex on 8/7/2015. - */ - - "use strict"; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - - var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; })(); - - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - - var _FloydWarshallJs = __webpack_require__(110); - - var _FloydWarshallJs2 = _interopRequireDefault(_FloydWarshallJs); - - var KamadaKawai = (function () { - function KamadaKawai(body, edgeLength, edgeStrength) { - _classCallCheck(this, KamadaKawai); - - this.body = body; - this.springLength = edgeLength; - this.springConstant = edgeStrength; - this.distanceSolver = new _FloydWarshallJs2["default"](); - } - - _createClass(KamadaKawai, [{ - key: "setOptions", - value: function setOptions(options) { - if (options) { - if (options.springLength) { - this.springLength = options.springLength; - } - if (options.springConstant) { - this.springConstant = options.springConstant; - } - } - } - }, { - key: "solve", - value: function solve(nodesArray, edgesArray) { - console.time("FLOYD - getDistances"); - var D_matrix = this.distanceSolver.getDistances(this.body, nodesArray, edgesArray); // distance matrix - console.timeEnd("FLOYD - getDistances"); - - // get the L Matrix - this._createL_matrix(D_matrix); - - // get the K Matrix - this._createK_matrix(D_matrix); - - console.time("positioning"); - var threshold = 0.01; - var counter = 0; - var maxIterations = 1500; - var maxEnergy = 2 * threshold; - var highE_nodeId = 0, - dE_dx = 0, - dE_dy = 0; - - while (maxEnergy > threshold && counter < maxIterations) { - counter += 1; - - var _getHighestEnergyNode2 = this._getHighestEnergyNode(); - - var _getHighestEnergyNode22 = _slicedToArray(_getHighestEnergyNode2, 4); - - highE_nodeId = _getHighestEnergyNode22[0]; - maxEnergy = _getHighestEnergyNode22[1]; - dE_dx = _getHighestEnergyNode22[2]; - dE_dy = _getHighestEnergyNode22[3]; - - this._moveNode(highE_nodeId, dE_dx, dE_dy); - } - console.timeEnd("positioning"); - } - }, { - key: "_getHighestEnergyNode", - value: function _getHighestEnergyNode() { - var nodesArray = this.body.nodeIndices; - var maxEnergy = 0; - var maxEnergyNode = nodesArray[0]; - var energies = { dE_dx: 0, dE_dy: 0 }; - - for (var nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) { - var m = nodesArray[nodeIdx]; - - var _getEnergy2 = this._getEnergy(m); - - var _getEnergy22 = _slicedToArray(_getEnergy2, 3); - - var delta_m = _getEnergy22[0]; - var dE_dx = _getEnergy22[1]; - var dE_dy = _getEnergy22[2]; - - if (maxEnergy < delta_m) { - maxEnergy = delta_m; - maxEnergyNode = m; - energies.dE_dx = dE_dx; - energies.dE_dy = dE_dy; - } - } - - return [maxEnergyNode, maxEnergy, energies.dE_dx, energies.dE_dy]; - } - }, { - key: "_getEnergy", - value: function _getEnergy(m) { - var nodesArray = this.body.nodeIndices; - var nodes = this.body.nodes; - - var x_m = nodes[m].x; - var y_m = nodes[m].y; - var dE_dx = 0; - var dE_dy = 0; - for (var iIdx = 0; iIdx < nodesArray.length; iIdx++) { - var i = nodesArray[iIdx]; - if (i !== m) { - var x_i = nodes[i].x; - var y_i = nodes[i].y; - var denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2)); - dE_dx += this.K_matrix[m][i] * (x_m - x_i - this.L_matrix[m][i] * (x_m - x_i) * denominator); - dE_dy += this.K_matrix[m][i] * (y_m - y_i - this.L_matrix[m][i] * (y_m - y_i) * denominator); - } - } - - var delta_m = Math.sqrt(Math.pow(dE_dx, 2) + Math.pow(dE_dy, 2)); - return [delta_m, dE_dx, dE_dy]; - } - }, { - key: "_moveNode", - value: function _moveNode(m, dE_dx, dE_dy) { - var nodesArray = this.body.nodeIndices; - var nodes = this.body.nodes; - var d2E_dx2 = 0; - var d2E_dxdy = 0; - var d2E_dy2 = 0; - - var x_m = nodes[m].x; - var y_m = nodes[m].y; - for (var iIdx = 0; iIdx < nodesArray.length; iIdx++) { - var i = nodesArray[iIdx]; - if (i !== m) { - var x_i = nodes[i].x; - var y_i = nodes[i].y; - var denominator = 1.0 / Math.pow(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2), 1.5); - d2E_dx2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(y_m - y_i, 2) * denominator); - d2E_dxdy += this.K_matrix[m][i] * (this.L_matrix[m][i] * (x_m - x_i) * (y_m - y_i) * denominator); - d2E_dy2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(x_m - x_i, 2) * denominator); - } - } - // make the variable names easier to make the solving of the linear system easier to read - var A = d2E_dx2, - B = d2E_dxdy, - C = dE_dx, - D = d2E_dy2, - E = dE_dy; - - // solve the linear system for dx and dy - var dy = (C / A + E / B) / (B / A - D / B); - var dx = -(B * dy + C) / A; - - // move the node - nodes[m].x += dx; - nodes[m].y += dy; - } - }, { - key: "_createL_matrix", - value: function _createL_matrix(D_matrix) { - var nodesArray = this.body.nodeIndices; - var edgeLength = this.springLength; - - this.L_matrix = []; - for (var i = 0; i < nodesArray.length; i++) { - this.L_matrix[nodesArray[i]] = {}; - for (var j = 0; j < nodesArray.length; j++) { - this.L_matrix[nodesArray[i]][nodesArray[j]] = edgeLength * D_matrix[nodesArray[i]][nodesArray[j]]; - } - } - } - }, { - key: "_createK_matrix", - value: function _createK_matrix(D_matrix) { - var nodesArray = this.body.nodeIndices; - var edgeStrength = this.springConstant; - - this.K_matrix = []; - for (var i = 0; i < nodesArray.length; i++) { - this.K_matrix[nodesArray[i]] = {}; - for (var j = 0; j < nodesArray.length; j++) { - this.K_matrix[nodesArray[i]][nodesArray[j]] = edgeStrength * Math.pow(D_matrix[nodesArray[i]][nodesArray[j]], -2); - } - } - } - }]); - - return KamadaKawai; - })(); - - exports["default"] = KamadaKawai; - module.exports = exports["default"]; - -/***/ }, -/* 110 */ -/***/ function(module, exports) { - - /** - * Created by Alex on 10-Aug-15. - */ - - "use strict"; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - - var FloydWarshall = (function () { - function FloydWarshall() { - _classCallCheck(this, FloydWarshall); - } - - _createClass(FloydWarshall, [{ - key: "getDistances", - value: function getDistances(body, nodesArray, edgesArray) { - var D_matrix = {}; - var edges = body.edges; - - // prepare matrix with large numbers - for (var i = 0; i < nodesArray.length; i++) { - D_matrix[nodesArray[i]] = {}; - for (var j = 0; j < nodesArray.length; j++) { - D_matrix[nodesArray[i]][nodesArray[j]] = 1e9; - } - } - - // put the weights for the edges in. This assumes unidirectionality. - for (var i = 0; i < edgesArray.length; i++) { - var edge = edges[edgesArray[i]]; - D_matrix[edge.fromId][edge.toId] = 1; - D_matrix[edge.toId][edge.fromId] = 1; - } - - // calculate all pair distances - for (var k = 0; k < nodesArray.length; k++) { - for (var i = 0; i < nodesArray.length; i++) { - for (var j = 0; j < nodesArray.length; j++) { - D_matrix[nodesArray[i]][nodesArray[j]] = Math.min(D_matrix[nodesArray[i]][nodesArray[j]], D_matrix[nodesArray[i]][nodesArray[k]] + D_matrix[nodesArray[k]][nodesArray[j]]); - } - } - } - - // remove the self references from the matrix - for (var i = 0; i < nodesArray.length; i++) { - delete D_matrix[nodesArray[i]][nodesArray[i]]; - } - - return D_matrix; - } - }]); - - return FloydWarshall; - })(); - - exports["default"] = FloydWarshall; - module.exports = exports["default"]; - -/***/ }, -/* 111 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -40677,7 +40510,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 112 */ +/* 110 */ /***/ function(module, exports) { /** @@ -41172,6 +41005,363 @@ return /******/ (function(modules) { // webpackBootstrap exports.allOptions = allOptions; exports.configureOptions = configureOptions; +/***/ }, +/* 111 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * Created by Alex on 8/7/2015. + */ + + // distance finding algorithm + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; })(); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var _componentsAlgorithmsFloydWarshallJs = __webpack_require__(112); + + /** + * KamadaKawai positions the nodes initially based on + * + * "AN ALGORITHM FOR DRAWING GENERAL UNDIRECTED GRAPHS" + * -- Tomihisa KAMADA and Satoru KAWAI in 1989 + * + * Possible optimizations in the distance calculation can be implemented. + */ + + var _componentsAlgorithmsFloydWarshallJs2 = _interopRequireDefault(_componentsAlgorithmsFloydWarshallJs); + + var KamadaKawai = (function () { + function KamadaKawai(body, edgeLength, edgeStrength) { + _classCallCheck(this, KamadaKawai); + + this.body = body; + this.springLength = edgeLength; + this.springConstant = edgeStrength; + this.distanceSolver = new _componentsAlgorithmsFloydWarshallJs2["default"](); + } + + /** + * Not sure if needed but can be used to update the spring length and spring constant + * @param options + */ + + _createClass(KamadaKawai, [{ + key: "setOptions", + value: function setOptions(options) { + if (options) { + if (options.springLength) { + this.springLength = options.springLength; + } + if (options.springConstant) { + this.springConstant = options.springConstant; + } + } + } + + /** + * Position the system + * @param nodesArray + * @param edgesArray + */ + }, { + key: "solve", + value: function solve(nodesArray, edgesArray) { + var ignoreClusters = arguments.length <= 2 || arguments[2] === undefined ? false : arguments[2]; + + // get distance matrix + var D_matrix = this.distanceSolver.getDistances(this.body, nodesArray, edgesArray); // distance matrix + + // get the L Matrix + this._createL_matrix(D_matrix); + + // get the K Matrix + this._createK_matrix(D_matrix); + + // calculate positions + var threshold = 0.01; + var innerThreshold = 1; + var iterations = 0; + var maxIterations = Math.max(1000, Math.min(10 * this.body.nodeIndices.length, 6000)); + var maxInnerIterations = 5; + + var maxEnergy = 1e9; + var highE_nodeId = 0, + dE_dx = 0, + dE_dy = 0, + delta_m = 0, + subIterations = 0; + + while (maxEnergy > threshold && iterations < maxIterations) { + iterations += 1; + + var _getHighestEnergyNode2 = this._getHighestEnergyNode(ignoreClusters); + + var _getHighestEnergyNode22 = _slicedToArray(_getHighestEnergyNode2, 4); + + highE_nodeId = _getHighestEnergyNode22[0]; + maxEnergy = _getHighestEnergyNode22[1]; + dE_dx = _getHighestEnergyNode22[2]; + dE_dy = _getHighestEnergyNode22[3]; + + delta_m = maxEnergy; + subIterations = 0; + while (delta_m > innerThreshold && subIterations < maxInnerIterations) { + subIterations += 1; + this._moveNode(highE_nodeId, dE_dx, dE_dy); + + var _getEnergy2 = this._getEnergy(highE_nodeId); + + var _getEnergy22 = _slicedToArray(_getEnergy2, 3); + + delta_m = _getEnergy22[0]; + dE_dx = _getEnergy22[1]; + dE_dy = _getEnergy22[2]; + } + } + } + + /** + * get the node with the highest energy + * @returns {*[]} + * @private + */ + }, { + key: "_getHighestEnergyNode", + value: function _getHighestEnergyNode(ignoreClusters) { + var nodesArray = this.body.nodeIndices; + var nodes = this.body.nodes; + var maxEnergy = 0; + var maxEnergyNodeId = nodesArray[0]; + var dE_dx_max = 0, + dE_dy_max = 0; + + for (var nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) { + var m = nodesArray[nodeIdx]; + // by not evaluating nodes with predefined positions we should only move nodes that have no positions. + if (nodes[m].predefinedPosition === false || nodes[m].isCluster === true && ignoreClusters === true || nodes[m].options.fixed.x === true || nodes[m].options.fixed.y === true) { + var _getEnergy3 = this._getEnergy(m); + + var _getEnergy32 = _slicedToArray(_getEnergy3, 3); + + var delta_m = _getEnergy32[0]; + var dE_dx = _getEnergy32[1]; + var dE_dy = _getEnergy32[2]; + + if (maxEnergy < delta_m) { + maxEnergy = delta_m; + maxEnergyNodeId = m; + dE_dx_max = dE_dx; + dE_dy_max = dE_dy; + } + } + } + + return [maxEnergyNodeId, maxEnergy, dE_dx_max, dE_dy_max]; + } + + /** + * calculate the energy of a single node + * @param m + * @returns {*[]} + * @private + */ + }, { + key: "_getEnergy", + value: function _getEnergy(m) { + var nodesArray = this.body.nodeIndices; + var nodes = this.body.nodes; + + var x_m = nodes[m].x; + var y_m = nodes[m].y; + var dE_dx = 0; + var dE_dy = 0; + for (var iIdx = 0; iIdx < nodesArray.length; iIdx++) { + var i = nodesArray[iIdx]; + if (i !== m) { + var x_i = nodes[i].x; + var y_i = nodes[i].y; + var denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2)); + dE_dx += this.K_matrix[m][i] * (x_m - x_i - this.L_matrix[m][i] * (x_m - x_i) * denominator); + dE_dy += this.K_matrix[m][i] * (y_m - y_i - this.L_matrix[m][i] * (y_m - y_i) * denominator); + } + } + + var delta_m = Math.sqrt(Math.pow(dE_dx, 2) + Math.pow(dE_dy, 2)); + return [delta_m, dE_dx, dE_dy]; + } + + /** + * move the node based on it's energy + * the dx and dy are calculated from the linear system proposed by Kamada and Kawai + * @param m + * @param dE_dx + * @param dE_dy + * @private + */ + }, { + key: "_moveNode", + value: function _moveNode(m, dE_dx, dE_dy) { + var nodesArray = this.body.nodeIndices; + var nodes = this.body.nodes; + var d2E_dx2 = 0; + var d2E_dxdy = 0; + var d2E_dy2 = 0; + + var x_m = nodes[m].x; + var y_m = nodes[m].y; + for (var iIdx = 0; iIdx < nodesArray.length; iIdx++) { + var i = nodesArray[iIdx]; + if (i !== m) { + var x_i = nodes[i].x; + var y_i = nodes[i].y; + var denominator = 1.0 / Math.pow(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2), 1.5); + d2E_dx2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(y_m - y_i, 2) * denominator); + d2E_dxdy += this.K_matrix[m][i] * (this.L_matrix[m][i] * (x_m - x_i) * (y_m - y_i) * denominator); + d2E_dy2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(x_m - x_i, 2) * denominator); + } + } + // make the variable names easier to make the solving of the linear system easier to read + var A = d2E_dx2, + B = d2E_dxdy, + C = dE_dx, + D = d2E_dy2, + E = dE_dy; + + // solve the linear system for dx and dy + var dy = (C / A + E / B) / (B / A - D / B); + var dx = -(B * dy + C) / A; + + // move the node + nodes[m].x += dx; + nodes[m].y += dy; + } + + /** + * Create the L matrix: edge length times shortest path + * @param D_matrix + * @private + */ + }, { + key: "_createL_matrix", + value: function _createL_matrix(D_matrix) { + var nodesArray = this.body.nodeIndices; + var edgeLength = this.springLength; + + this.L_matrix = []; + for (var i = 0; i < nodesArray.length; i++) { + this.L_matrix[nodesArray[i]] = {}; + for (var j = 0; j < nodesArray.length; j++) { + this.L_matrix[nodesArray[i]][nodesArray[j]] = edgeLength * D_matrix[nodesArray[i]][nodesArray[j]]; + } + } + } + + /** + * Create the K matrix: spring constants times shortest path + * @param D_matrix + * @private + */ + }, { + key: "_createK_matrix", + value: function _createK_matrix(D_matrix) { + var nodesArray = this.body.nodeIndices; + var edgeStrength = this.springConstant; + + this.K_matrix = []; + for (var i = 0; i < nodesArray.length; i++) { + this.K_matrix[nodesArray[i]] = {}; + for (var j = 0; j < nodesArray.length; j++) { + this.K_matrix[nodesArray[i]][nodesArray[j]] = edgeStrength * Math.pow(D_matrix[nodesArray[i]][nodesArray[j]], -2); + } + } + } + }]); + + return KamadaKawai; + })(); + + exports["default"] = KamadaKawai; + module.exports = exports["default"]; + +/***/ }, +/* 112 */ +/***/ function(module, exports) { + + /** + * Created by Alex on 10-Aug-15. + */ + + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var FloydWarshall = (function () { + function FloydWarshall() { + _classCallCheck(this, FloydWarshall); + } + + _createClass(FloydWarshall, [{ + key: "getDistances", + value: function getDistances(body, nodesArray, edgesArray) { + var D_matrix = {}; + var edges = body.edges; + + // prepare matrix with large numbers + for (var i = 0; i < nodesArray.length; i++) { + D_matrix[nodesArray[i]] = {}; + D_matrix[nodesArray[i]] = {}; + for (var j = 0; j < nodesArray.length; j++) { + D_matrix[nodesArray[i]][nodesArray[j]] = i == j ? 0 : 1e9; + D_matrix[nodesArray[i]][nodesArray[j]] = i == j ? 0 : 1e9; + } + } + + // put the weights for the edges in. This assumes unidirectionality. + for (var i = 0; i < edgesArray.length; i++) { + var edge = edges[edgesArray[i]]; + D_matrix[edge.fromId][edge.toId] = 1; + D_matrix[edge.toId][edge.fromId] = 1; + } + + var nodeCount = nodesArray.length; + + // Adapted FloydWarshall based on unidirectionality to greatly reduce complexity. + for (var k = 0; k < nodeCount; k++) { + for (var i = 0; i < nodeCount - 1; i++) { + for (var j = i + 1; j < nodeCount; j++) { + D_matrix[nodesArray[i]][nodesArray[j]] = Math.min(D_matrix[nodesArray[i]][nodesArray[j]], D_matrix[nodesArray[i]][nodesArray[k]] + D_matrix[nodesArray[k]][nodesArray[j]]); + D_matrix[nodesArray[j]][nodesArray[i]] = D_matrix[nodesArray[i]][nodesArray[j]]; + } + } + } + + return D_matrix; + } + }]); + + return FloydWarshall; + })(); + + exports["default"] = FloydWarshall; + module.exports = exports["default"]; + /***/ }, /* 113 */ /***/ function(module, exports) { diff --git a/lib/network/Network.js b/lib/network/Network.js index deea2d88..3675945d 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -24,11 +24,11 @@ import InteractionHandler from './modules/InteractionHandler'; import SelectionHandler from "./modules/SelectionHandler"; import LayoutEngine from "./modules/LayoutEngine"; import ManipulationSystem from "./modules/ManipulationSystem"; -import Configurator from "./../shared/Configurator"; +import Configurator from "./../shared/Configurator"; import Validator from "./../shared/Validator"; import {printStyle} from "./../shared/Validator"; import {allOptions, configureOptions} from './options.js'; - +import KamadaKawai from "./modules/KamadaKawai.js" /** @@ -92,6 +92,7 @@ function Network(container, data, options) { createEdge: function() {}, getPointer: function() {} }, + modules: {}, view: { scale: 1, translation: {x: 0, y: 0} @@ -119,6 +120,9 @@ function Network(container, data, options) { this.nodesHandler = new NodesHandler(this.body, this.images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options this.edgesHandler = new EdgesHandler(this.body, this.images, this.groups); // Handle adding, deleting and updating of edges as well as global options + this.body.modules["kamadaKawai"] = new KamadaKawai(this.body,150,0.05); // Layouting algorithm. + this.body.modules["clustering"] = this.clustering; + // create the DOM elements this.canvas._create(); @@ -332,6 +336,9 @@ Network.prototype.setData = function (data) { // emit change in data this.body.emitter.emit("_dataChanged"); + // emit data loaded + this.body.emitter.emit("_dataLoaded"); + // find a stable position or start animating to a stable position this.body.emitter.emit("initPhysics"); }; diff --git a/lib/network/modules/Clustering.js b/lib/network/modules/Clustering.js index 4348ad57..f41c7af7 100644 --- a/lib/network/modules/Clustering.js +++ b/lib/network/modules/Clustering.js @@ -44,6 +44,7 @@ class ClusterEngine { for (let i = 0; i < nodesToCluster.length; i++) { this.clusterByConnection(nodesToCluster[i],options,false); } + this.body.emitter.emit('_dataChanged'); } @@ -83,11 +84,12 @@ class ClusterEngine { /** - * Cluster all nodes in the network that have only 1 edge - * @param options - * @param refreshData - */ - clusterOutliers(options, refreshData = true) { + * Cluster all nodes in the network that have only X edges + * @param edgeCount + * @param options + * @param refreshData + */ + clusterByEdgeCount(edgeCount, options, refreshData = true) { options = this._checkOptions(options); let clusters = []; @@ -105,8 +107,8 @@ class ClusterEngine { } } - if (visibleEdges === 1) { - // this is an outlier + if (visibleEdges === edgeCount) { + // this is a qualifying node let childNodeId = this._getConnectedId(edge, nodeId); if (childNodeId !== nodeId) { if (options.joinCondition === undefined) { @@ -145,6 +147,24 @@ class ClusterEngine { } } + /** + * Cluster all nodes in the network that have only 1 edge + * @param options + * @param refreshData + */ + clusterOutliers(options, refreshData = true) { + this.clusterByEdgeCount(1,options,refreshData); + } + + /** + * Cluster all nodes in the network that have only 2 edge + * @param options + * @param refreshData + */ + clusterBridges(options, refreshData = true) { + this.clusterByEdgeCount(2,options,refreshData); + } + _checkIfUsed(clusters, nodeId, edgeId) { for (let i = 0; i < clusters.length; i++) { @@ -189,25 +209,26 @@ class ClusterEngine { let edge = node.edges[i]; let childNodeId = this._getConnectedId(edge, parentNodeId); - if (childNodeId !== parentNodeId) { - if (options.joinCondition === undefined) { - childEdgesObj[edge.id] = edge; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } - else { - // clone the options and insert some additional parameters that could be interesting. - let childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]); - if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) { + if (this.clusteredNodes[childNodeId] === undefined) { + if (childNodeId !== parentNodeId) { + if (options.joinCondition === undefined) { childEdgesObj[edge.id] = edge; childNodesObj[childNodeId] = this.body.nodes[childNodeId]; } + else { + // clone the options and insert some additional parameters that could be interesting. + let childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]); + if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } + } + } + else { + childEdgesObj[edge.id] = edge; } - } - else { - childEdgesObj[edge.id] = edge; } } - this._cluster(childNodesObj, childEdgesObj, options, refreshData); } @@ -304,9 +325,19 @@ class ClusterEngine { * @private */ _cluster(childNodesObj, childEdgesObj, options, refreshData = true) { - // kill condition: no children so cant cluster + // kill condition: no children so can't cluster if (Object.keys(childNodesObj).length === 0) {return;} + + // check if this cluster call is not trying to cluster anything that is in another cluster. + for (let nodeId in childNodesObj) { + if (childNodesObj.hasOwnProperty(nodeId)) { + if (this.clusteredNodes[nodeId] !== undefined) { + return; + } + } + } + let clusterNodeProperties = util.deepExtend({},options.clusterNodeProperties); // construct the clusterNodeProperties @@ -314,17 +345,21 @@ class ClusterEngine { // get the childNode options let childNodesOptions = []; for (let nodeId in childNodesObj) { - let clonedOptions = this._cloneOptions(childNodesObj[nodeId]); - childNodesOptions.push(clonedOptions); + if (childNodesObj.hasOwnProperty(nodeId)) { + let clonedOptions = this._cloneOptions(childNodesObj[nodeId]); + childNodesOptions.push(clonedOptions); + } } // get clusterproperties based on childNodes let childEdgesOptions = []; for (let edgeId in childEdgesObj) { - // these cluster edges will be removed on creation of the cluster. - if (edgeId.substr(0,12) !== "clusterEdge:") { - let clonedOptions = this._cloneOptions(childEdgesObj[edgeId], 'edge'); - childEdgesOptions.push(clonedOptions); + if (childEdgesObj.hasOwnProperty(edgeId)) { + // these cluster edges will be removed on creation of the cluster. + if (edgeId.substr(0, 12) !== "clusterEdge:") { + let clonedOptions = this._cloneOptions(childEdgesObj[edgeId], 'edge'); + childEdgesOptions.push(clonedOptions); + } } } diff --git a/lib/network/modules/components/KamadaKawai.js b/lib/network/modules/KamadaKawai.js similarity index 58% rename from lib/network/modules/components/KamadaKawai.js rename to lib/network/modules/KamadaKawai.js index 1023d1d5..a54994d3 100644 --- a/lib/network/modules/components/KamadaKawai.js +++ b/lib/network/modules/KamadaKawai.js @@ -2,8 +2,18 @@ * Created by Alex on 8/7/2015. */ -import FloydWarshall from "./FloydWarshall.js" +// distance finding algorithm +import FloydWarshall from "./components/algorithms/FloydWarshall.js" + +/** + * KamadaKawai positions the nodes initially based on + * + * "AN ALGORITHM FOR DRAWING GENERAL UNDIRECTED GRAPHS" + * -- Tomihisa KAMADA and Satoru KAWAI in 1989 + * + * Possible optimizations in the distance calculation can be implemented. + */ class KamadaKawai { constructor(body, edgeLength, edgeStrength) { this.body = body; @@ -12,6 +22,10 @@ class KamadaKawai { this.distanceSolver = new FloydWarshall(); } + /** + * Not sure if needed but can be used to update the spring length and spring constant + * @param options + */ setOptions(options) { if (options) { if (options.springLength) { @@ -23,10 +37,15 @@ class KamadaKawai { } } - solve(nodesArray, edgesArray) { - console.time("FLOYD - getDistances"); + + /** + * Position the system + * @param nodesArray + * @param edgesArray + */ + solve(nodesArray, edgesArray, ignoreClusters = false) { + // get distance matrix let D_matrix = this.distanceSolver.getDistances(this.body, nodesArray, edgesArray); // distance matrix - console.timeEnd("FLOYD - getDistances"); // get the L Matrix this._createL_matrix(D_matrix); @@ -34,42 +53,64 @@ class KamadaKawai { // get the K Matrix this._createK_matrix(D_matrix); - console.time("positioning") + // calculate positions let threshold = 0.01; - let counter = 0; - let maxIterations = Math.min(10*this.body.nodeIndices.length);; - let maxEnergy = 1e9; // just to pass the first check. - let highE_nodeId = 0, dE_dx = 0, dE_dy = 0; - - while (maxEnergy > threshold && counter < maxIterations) { - counter += 1; - [highE_nodeId, maxEnergy, dE_dx, dE_dy] = this._getHighestEnergyNode(); - this._moveNode(highE_nodeId, dE_dx, dE_dy); + let innerThreshold = 1; + let iterations = 0; + let maxIterations = Math.max(1000,Math.min(10*this.body.nodeIndices.length,6000)); + let maxInnerIterations = 5; + + let maxEnergy = 1e9; + let highE_nodeId = 0, dE_dx = 0, dE_dy = 0, delta_m = 0, subIterations = 0; + + while (maxEnergy > threshold && iterations < maxIterations) { + iterations += 1; + [highE_nodeId, maxEnergy, dE_dx, dE_dy] = this._getHighestEnergyNode(ignoreClusters); + delta_m = maxEnergy; + subIterations = 0; + while(delta_m > innerThreshold && subIterations < maxInnerIterations) { + subIterations += 1; + this._moveNode(highE_nodeId, dE_dx, dE_dy); + [delta_m,dE_dx,dE_dy] = this._getEnergy(highE_nodeId); + } } - console.timeEnd("positioning") } - - _getHighestEnergyNode() { + /** + * get the node with the highest energy + * @returns {*[]} + * @private + */ + _getHighestEnergyNode(ignoreClusters) { let nodesArray = this.body.nodeIndices; + let nodes = this.body.nodes; let maxEnergy = 0; - let maxEnergyNode = nodesArray[0]; - let energies = {dE_dx: 0, dE_dy: 0}; + let maxEnergyNodeId = nodesArray[0]; + let dE_dx_max = 0, dE_dy_max = 0; for (let nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) { let m = nodesArray[nodeIdx]; - let [delta_m,dE_dx,dE_dy] = this._getEnergy(m); - if (maxEnergy < delta_m) { - maxEnergy = delta_m; - maxEnergyNode = m; - energies.dE_dx = dE_dx; - energies.dE_dy = dE_dy; + // by not evaluating nodes with predefined positions we should only move nodes that have no positions. + if ((nodes[m].predefinedPosition === false || nodes[m].isCluster === true && ignoreClusters === true) || nodes[m].options.fixed.x === true || nodes[m].options.fixed.y === true) { + let [delta_m,dE_dx,dE_dy] = this._getEnergy(m); + if (maxEnergy < delta_m) { + maxEnergy = delta_m; + maxEnergyNodeId = m; + dE_dx_max = dE_dx; + dE_dy_max = dE_dy; + } } } - return [maxEnergyNode, maxEnergy, energies.dE_dx, energies.dE_dy]; + return [maxEnergyNodeId, maxEnergy, dE_dx_max, dE_dy_max]; } + /** + * calculate the energy of a single node + * @param m + * @returns {*[]} + * @private + */ _getEnergy(m) { let nodesArray = this.body.nodeIndices; let nodes = this.body.nodes; @@ -93,6 +134,14 @@ class KamadaKawai { return [delta_m, dE_dx, dE_dy]; } + /** + * move the node based on it's energy + * the dx and dy are calculated from the linear system proposed by Kamada and Kawai + * @param m + * @param dE_dx + * @param dE_dy + * @private + */ _moveNode(m, dE_dx, dE_dy) { let nodesArray = this.body.nodeIndices; let nodes = this.body.nodes; @@ -125,6 +174,12 @@ class KamadaKawai { nodes[m].y += dy; } + + /** + * Create the L matrix: edge length times shortest path + * @param D_matrix + * @private + */ _createL_matrix(D_matrix) { let nodesArray = this.body.nodeIndices; let edgeLength = this.springLength; @@ -138,6 +193,12 @@ class KamadaKawai { } } + + /** + * Create the K matrix: spring constants times shortest path + * @param D_matrix + * @private + */ _createK_matrix(D_matrix) { let nodesArray = this.body.nodeIndices; let edgeStrength = this.springConstant; diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index 3a2a46fd..dc5ca4a3 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -1,7 +1,6 @@ 'use strict' -import KamadaKawai from "./components/KamadaKawai.js" -var util = require('../../util'); +let util = require('../../util'); class LayoutEngine { constructor(body) { @@ -12,6 +11,7 @@ class LayoutEngine { this.options = {}; this.optionsBackup = {}; + this.defaultOptions = { randomSeed: undefined, hierarchical: { @@ -32,6 +32,9 @@ class LayoutEngine { this.body.emitter.on('_dataChanged', () => { this.setupHierarchicalLayout(); }); + this.body.emitter.on('_dataLoaded', () => { + this.layoutNetwork(); + }); this.body.emitter.on('_resetHierarchicalLayout', () => { this.setupHierarchicalLayout(); }); @@ -146,7 +149,7 @@ class LayoutEngine { } seededRandom() { - var x = Math.sin(this.randomSeed++) * 10000; + let x = Math.sin(this.randomSeed++) * 10000; return x - Math.floor(x); } @@ -167,6 +170,75 @@ class LayoutEngine { } } + layoutNetwork() { + // first check if we should KamadaKawai to layout. The threshold is if less than half of the visible + // nodes have predefined positions we use this. + let positionDefined = 0; + for (let i = 0; i < this.body.nodeIndices.length; i++) { + let node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.predefinedPosition === true) { + positionDefined += 1; + } + } + + // if less than half of the nodes have a predefined position we continue + if (positionDefined < 0.5 * this.body.nodeIndices.length) { + let levels = 0; + // if there are a lot of nodes, we cluster before we run the algorithm. + if (this.body.nodeIndices.length > 100) { + let startLength = this.body.nodeIndices.length; + while(this.body.nodeIndices.length > 150) { + levels += 1; + if (levels % 5 === 0) { + this.body.modules.clustering.clusterByHubsize(); + } + else if (levels % 3 === 0) { + this.body.modules.clustering.clusterBridges(); + } + else { + this.body.modules.clustering.clusterOutliers(); + } + console.log('levels', levels) + + } + this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150,2*startLength)}) + } + + // position the system for these nodes and edges + this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true); + + // uncluster all clusters + if (levels > 0) { + let clustersPresent = true; + while (clustersPresent === true) { + clustersPresent = false; + for (let i = 0; i < this.body.nodeIndices.length; i++) { + if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) { + clustersPresent = true; + this.body.modules.clustering.openCluster(this.body.nodeIndices[i], { + releaseFunction: function (clusterPosition, containedNodesPositions) { + var newPositions = {}; + for (let nodeId in containedNodesPositions) { + if (containedNodesPositions.hasOwnProperty(nodeId)) { + newPositions[nodeId] = {x:clusterPosition.x, y:clusterPosition.y}; + } + } + return newPositions; + } + }, false); + } + } + if (clustersPresent === true) { + this.body.emitter.emit('_dataChanged'); + } + } + } + + // reposition all bezier nodes. + this.body.emitter.emit("_repositionBezierNodes"); + } + } + getSeed() { return this.initialRandomSeed; } @@ -178,10 +250,6 @@ class LayoutEngine { * @private */ setupHierarchicalLayout() { - let kk = new KamadaKawai(this.body,100,0.05); - kk.solve(this.body.nodeIndices, this.body.edgeIndices); - return - if (this.options.hierarchical.enabled === true && this.body.nodeIndices.length > 0) { // get the size of the largest hubs and check if the user has defined a level for a node. let node, nodeId; diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index 762469a5..3f669d34 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -21,7 +21,6 @@ class PhysicsEngine { this.previousStates = {}; this.freezeCache = {}; this.renderTimer = undefined; - this.initialStabilizationEmitted = false; this.stabilized = false; this.startedStabilization = false; @@ -236,21 +235,12 @@ class PhysicsEngine { } if (this.stabilized === true) { - if (this.stabilizationIterations > 1) { - // trigger the 'stabilized' event. - // The event is triggered on the next tick, to prevent the case that - // it is fired while initializing the Network, in which case you would not - // be able to catch it - this.startedStabilization = false; - //this._emitStabilized(); - } this.stopSimulation(); } } _emitStabilized() { - if (this.stabilizationIterations > 1 || this.initialStabilizationEmitted === false) { - this.initialStabilizationEmitted = true; + if (this.stabilizationIterations > 1) { setTimeout(() => { this.body.emitter.emit('stabilized', {iterations: this.stabilizationIterations}); this.stabilizationIterations = 0; @@ -377,7 +367,6 @@ class PhysicsEngine { nodesPresent = true; } - if (nodesPresent === true) { if (vminCorrected > 0.5*this.options.maxVelocity) { return false; @@ -548,11 +537,12 @@ class PhysicsEngine { if (this.options.stabilization.onlyDynamicEdges === true) { this._restoreFrozenNodes(); } - + this.body.emitter.emit('stabilizationIterationsDone'); this.body.emitter.emit('_requestRedraw'); if (this.stabilized === true) { + console.log("emitted") this._emitStabilized(); } else { diff --git a/lib/network/modules/components/FloydWarshall.js b/lib/network/modules/components/algorithms/FloydWarshall.js similarity index 59% rename from lib/network/modules/components/FloydWarshall.js rename to lib/network/modules/components/algorithms/FloydWarshall.js index dd1ec067..a41b7c79 100644 --- a/lib/network/modules/components/FloydWarshall.js +++ b/lib/network/modules/components/algorithms/FloydWarshall.js @@ -7,14 +7,16 @@ class FloydWarshall { constructor(){} getDistances(body, nodesArray, edgesArray) { - let D_matrix = {} + let D_matrix = {}; let edges = body.edges; // prepare matrix with large numbers for (let i = 0; i < nodesArray.length; i++) { + D_matrix[nodesArray[i]] = {}; D_matrix[nodesArray[i]] = {}; for (let j = 0; j < nodesArray.length; j++) { - D_matrix[nodesArray[i]][nodesArray[j]] = 1e9; + D_matrix[nodesArray[i]][nodesArray[j]] = (i == j ? 0 : 1e9); + D_matrix[nodesArray[i]][nodesArray[j]] = (i == j ? 0 : 1e9); } } @@ -25,20 +27,18 @@ class FloydWarshall { D_matrix[edge.toId][edge.fromId] = 1; } - // calculate all pair distances - for (let k = 0; k < nodesArray.length; k++) { - for (let i = 0; i < nodesArray.length; i++) { - for (let j = 0; j < nodesArray.length; j++) { + let nodeCount = nodesArray.length; + + // Adapted FloydWarshall based on unidirectionality to greatly reduce complexity. + for (let k = 0; k < nodeCount; k++) { + for (let i = 0; i < nodeCount-1; i++) { + for (let j = i+1; j < nodeCount; j++) { D_matrix[nodesArray[i]][nodesArray[j]] = Math.min(D_matrix[nodesArray[i]][nodesArray[j]],D_matrix[nodesArray[i]][nodesArray[k]] + D_matrix[nodesArray[k]][nodesArray[j]]) + D_matrix[nodesArray[j]][nodesArray[i]] = D_matrix[nodesArray[i]][nodesArray[j]]; } } } - // remove the self references from the matrix - for (let i = 0; i < nodesArray.length; i++) { - delete D_matrix[nodesArray[i]][nodesArray[i]]; - } - return D_matrix; } } diff --git a/lib/network/modules/components/edges/BezierEdgeDynamic.js b/lib/network/modules/components/edges/BezierEdgeDynamic.js index 893f00e4..60bb9dbf 100644 --- a/lib/network/modules/components/edges/BezierEdgeDynamic.js +++ b/lib/network/modules/components/edges/BezierEdgeDynamic.js @@ -4,6 +4,8 @@ class BezierEdgeDynamic extends BezierEdgeBase { constructor(options, body, labelModule) { //this.via = undefined; // Here for completeness but not allowed to defined before super() is invoked. super(options, body, labelModule); // --> this calls the setOptions below + this._boundFunction = () => {this.positionBezierNode();}; + this.body.emitter.on("_repositionBezierNodes", this._boundFunction); } setOptions(options) { @@ -41,6 +43,7 @@ class BezierEdgeDynamic extends BezierEdgeBase { * @returns {boolean} */ cleanup() { + this.body.emitter.off("_repositionBezierNodes", this._boundFunction); if (this.via !== undefined) { delete this.body.nodes[this.via.id]; this.via = undefined; From 8744d63b5cf48ffe4943afaa64ca642ad381f50b Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 13 Aug 2015 20:30:07 +0200 Subject: [PATCH 03/10] - added adaptive timestep to physics. --- dist/vis.js | 117 +++++++++++++++++++++++++-- lib/network/modules/PhysicsEngine.js | 114 ++++++++++++++++++++++++-- 2 files changed, 217 insertions(+), 14 deletions(-) diff --git a/dist/vis.js b/dist/vis.js index f6deb51c..39649873 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -33017,6 +33017,10 @@ return /******/ (function(modules) { // webpackBootstrap this.simulationInterval = 1000 / 60; this.requiresTimeout = true; this.previousStates = {}; + this.referenceState = {}; + this.adaptive = false; + this.adaptiveCounter = 0; + this.adaptiveInterval = 3; this.freezeCache = {}; this.renderTimer = undefined; @@ -33075,6 +33079,7 @@ return /******/ (function(modules) { // webpackBootstrap timestep: 0.5 }; util.extend(this.options, this.defaultOptions); + this.timestep = 0.5; this.bindEventListeners(); } @@ -33133,10 +33138,17 @@ return /******/ (function(modules) { // webpackBootstrap this.physicsEnabled = false; this.stopSimulation(); } + + // set the timestep + this.timestep = this.options.timestep; } } this.init(); } + + /** + * configure the engine. + */ }, { key: 'init', value: function init() { @@ -33166,6 +33178,10 @@ return /******/ (function(modules) { // webpackBootstrap this.modelOptions = options; } + + /** + * initialize the engine + */ }, { key: 'initPhysics', value: function initPhysics() { @@ -33193,6 +33209,9 @@ return /******/ (function(modules) { // webpackBootstrap if (this.physicsEnabled === true && this.options.enabled === true) { this.stabilized = false; + // when visible, adaptivity is disabled. + this.adaptive = false; + // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes"); if (this.viewFunction === undefined) { @@ -33250,6 +33269,11 @@ return /******/ (function(modules) { // webpackBootstrap this.stopSimulation(); } } + + /** + * trigger the stabilized event. + * @private + */ }, { key: '_emitStabilized', value: function _emitStabilized() { @@ -33272,8 +33296,53 @@ return /******/ (function(modules) { // webpackBootstrap key: 'physicsTick', value: function physicsTick() { if (this.stabilized === false) { - this.calculateForces(); - this.stabilized = this.moveNodes(); + // adaptivity means the timestep adapts to the situation, only applicable for stabilization + if (this.adaptive === true) { + this.adaptiveCounter += 1; + if (this.adaptiveCounter % this.adaptiveInterval === 0) { + // we leave the timestep stable for "interval" iterations. + // first the big step and revert. Revert saves the reference state. + this.timestep = 2 * this.timestep; + this.calculateForces(); + this.moveNodes(); + this.revert(); + + // now the normal step. Since this is the last step, it is the more stable one and we will take this. + this.timestep = 0.5 * this.timestep; + + // since it's half the step, we do it twice. + this.calculateForces(); + this.moveNodes(); + this.calculateForces(); + this.moveNodes(); + + // we compare the two steps. if it is acceptable we double the step. + if (this.compare() === true) { + this.timestep = 2 * this.timestep; + } else { + // if not, we half the step to a minimum of the options timestep. + // if the half the timestep is smaller than the options step, we do not reset the counter + // we assume that the options timestep is stable enough. + if (0.5 * this.timestep < this.options.timestep) { + this.timestep = this.options.timestep; + } else { + // if the timestep was larger than 2 times the option one we check the adaptivity again to ensure + // that large instabilities do not form. + this.adaptiveCounter = -1; // check again next iteration + this.timestep = 0.5 * this.timestep; + } + } + } else { + // normal step, keeping timestep constant + this.calculateForces(); + this.moveNodes(); + } + } else { + // case for the static timestep, we reset it to the one in options and take a normal step. + this.timestep = this.options.timestep; + this.calculateForces(); + this.moveNodes(); + } // determine if the network has stabilzied if (this.stabilized === true) { @@ -33355,6 +33424,9 @@ return /******/ (function(modules) { // webpackBootstrap var nodeId = nodeIds[i]; if (nodes[nodeId] !== undefined) { if (nodes[nodeId].options.physics === true) { + this.referenceState[nodeId] = { + positions: { x: nodes[nodeId].x, y: nodes[nodeId].y } + }; velocities[nodeId].x = this.previousStates[nodeId].vx; velocities[nodeId].y = this.previousStates[nodeId].vy; nodes[nodeId].x = this.previousStates[nodeId].x; @@ -33366,6 +33438,34 @@ return /******/ (function(modules) { // webpackBootstrap } } + /** + * This compares the reference state to the current state + */ + }, { + key: 'compare', + value: function compare() { + var dx = undefined, + dy = undefined, + dpos = undefined; + var nodes = this.body.nodes; + var reference = this.referenceState; + var posThreshold = 0.25; + + for (var nodeId in this.referenceState) { + if (this.referenceState.hasOwnProperty(nodeId)) { + dx = nodes[nodeId].x - reference[nodeId].positions.x; + dy = nodes[nodeId].y - reference[nodeId].positions.y; + + dpos = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); + + if (dpos > posThreshold) { + return false; + } + } + } + return true; + } + /** * move the nodes one timestap and check if they are stabilized * @returns {boolean} @@ -33389,12 +33489,13 @@ return /******/ (function(modules) { // webpackBootstrap if (nodesPresent === true) { if (vminCorrected > 0.5 * this.options.maxVelocity) { - return false; + this.stabilized = false; } else { - return stabilized; + this.stabilized = stabilized; } + return; } - return true; + this.stabilized = true; } /** @@ -33409,7 +33510,7 @@ return /******/ (function(modules) { // webpackBootstrap key: '_performStep', value: function _performStep(nodeId, maxVelocity) { var node = this.body.nodes[nodeId]; - var timestep = this.options.timestep; + var timestep = this.timestep; var forces = this.physicsBody.forces; var velocities = this.physicsBody.velocities; @@ -33515,6 +33616,9 @@ return /******/ (function(modules) { // webpackBootstrap return; } + // enable adaptive timesteps + this.adaptive = true; + // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes"); @@ -33571,7 +33675,6 @@ return /******/ (function(modules) { // webpackBootstrap this.body.emitter.emit('_requestRedraw'); if (this.stabilized === true) { - console.log("emitted"); this._emitStabilized(); } else { this.startSimulation(); diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index 3f669d34..502e8e69 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -19,6 +19,10 @@ class PhysicsEngine { this.simulationInterval = 1000 / 60; this.requiresTimeout = true; this.previousStates = {}; + this.referenceState = {}; + this.adaptive = false; + this.adaptiveCounter = 0; + this.adaptiveInterval = 3; this.freezeCache = {}; this.renderTimer = undefined; @@ -77,6 +81,7 @@ class PhysicsEngine { timestep: 0.5 }; util.extend(this.options, this.defaultOptions); + this.timestep = 0.5; this.bindEventListeners(); } @@ -123,12 +128,18 @@ class PhysicsEngine { this.physicsEnabled = false; this.stopSimulation(); } + + // set the timestep + this.timestep = this.options.timestep; } } this.init(); } + /** + * configure the engine. + */ init() { var options; if (this.options.solver === 'forceAtlas2Based') { @@ -159,6 +170,10 @@ class PhysicsEngine { this.modelOptions = options; } + + /** + * initialize the engine + */ initPhysics() { if (this.physicsEnabled === true && this.options.enabled === true) { if (this.options.stabilization.enabled === true) { @@ -184,6 +199,9 @@ class PhysicsEngine { if (this.physicsEnabled === true && this.options.enabled === true) { this.stabilized = false; + // when visible, adaptivity is disabled. + this.adaptive = false; + // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes"); if (this.viewFunction === undefined) { @@ -239,6 +257,11 @@ class PhysicsEngine { } } + + /** + * trigger the stabilized event. + * @private + */ _emitStabilized() { if (this.stabilizationIterations > 1) { setTimeout(() => { @@ -255,8 +278,56 @@ class PhysicsEngine { */ physicsTick() { if (this.stabilized === false) { - this.calculateForces(); - this.stabilized = this.moveNodes(); + // adaptivity means the timestep adapts to the situation, only applicable for stabilization + if (this.adaptive === true) { + this.adaptiveCounter += 1; + if (this.adaptiveCounter % this.adaptiveInterval === 0) { // we leave the timestep stable for "interval" iterations. + // first the big step and revert. Revert saves the reference state. + this.timestep = 2 * this.timestep; + this.calculateForces(); + this.moveNodes(); + this.revert(); + + // now the normal step. Since this is the last step, it is the more stable one and we will take this. + this.timestep = 0.5 * this.timestep; + + // since it's half the step, we do it twice. + this.calculateForces(); + this.moveNodes(); + this.calculateForces(); + this.moveNodes(); + + // we compare the two steps. if it is acceptable we double the step. + if (this.compare() === true) { + this.timestep = 2 * this.timestep; + } + else { + // if not, we half the step to a minimum of the options timestep. + // if the half the timestep is smaller than the options step, we do not reset the counter + // we assume that the options timestep is stable enough. + if (0.5 * this.timestep < this.options.timestep) { + this.timestep = this.options.timestep; + } + else { + // if the timestep was larger than 2 times the option one we check the adaptivity again to ensure + // that large instabilities do not form. + this.adaptiveCounter = -1; // check again next iteration + this.timestep = 0.5 * this.timestep; + } + } + } + else { + // normal step, keeping timestep constant + this.calculateForces(); + this.moveNodes(); + } + } + else { + // case for the static timestep, we reset it to the one in options and take a normal step. + this.timestep = this.options.timestep; + this.calculateForces(); + this.moveNodes(); + } // determine if the network has stabilzied if (this.stabilized === true) { @@ -336,6 +407,9 @@ class PhysicsEngine { let nodeId = nodeIds[i]; if (nodes[nodeId] !== undefined) { if (nodes[nodeId].options.physics === true) { + this.referenceState[nodeId] = { + positions: {x:nodes[nodeId].x, y:nodes[nodeId].y} + }; velocities[nodeId].x = this.previousStates[nodeId].vx; velocities[nodeId].y = this.previousStates[nodeId].vy; nodes[nodeId].x = this.previousStates[nodeId].x; @@ -348,6 +422,30 @@ class PhysicsEngine { } } + /** + * This compares the reference state to the current state + */ + compare() { + let dx, dy, dpos; + let nodes = this.body.nodes; + let reference = this.referenceState; + let posThreshold = 0.25; + + for (let nodeId in this.referenceState) { + if (this.referenceState.hasOwnProperty(nodeId)) { + dx = nodes[nodeId].x - reference[nodeId].positions.x; + dy = nodes[nodeId].y - reference[nodeId].positions.y; + + dpos = Math.sqrt(Math.pow(dx,2) + Math.pow(dy,2)) + + if (dpos > posThreshold) { + return false; + } + } + } + return true; + } + /** * move the nodes one timestap and check if they are stabilized * @returns {boolean} @@ -369,13 +467,14 @@ class PhysicsEngine { if (nodesPresent === true) { if (vminCorrected > 0.5*this.options.maxVelocity) { - return false; + this.stabilized = false; } else { - return stabilized; + this.stabilized = stabilized; } + return; } - return true; + this.stabilized = true; } @@ -389,7 +488,7 @@ class PhysicsEngine { */ _performStep(nodeId,maxVelocity) { let node = this.body.nodes[nodeId]; - let timestep = this.options.timestep; + let timestep = this.timestep; let forces = this.physicsBody.forces; let velocities = this.physicsBody.velocities; @@ -488,6 +587,8 @@ class PhysicsEngine { return; } + // enable adaptive timesteps + this.adaptive = true; // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes"); @@ -542,7 +643,6 @@ class PhysicsEngine { this.body.emitter.emit('_requestRedraw'); if (this.stabilized === true) { - console.log("emitted") this._emitStabilized(); } else { From 36ecb2c93396770023d401d613c80d5e77699906 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Fri, 14 Aug 2015 11:21:57 +0200 Subject: [PATCH 04/10] - added improved timestep adaption, fixed bugs in stabilization --- dist/vis.js | 39 +++++++++++----------------- lib/network/modules/LayoutEngine.js | 3 +-- lib/network/modules/PhysicsEngine.js | 37 ++++++++++---------------- 3 files changed, 30 insertions(+), 49 deletions(-) diff --git a/dist/vis.js b/dist/vis.js index 39649873..e3761ba1 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 4.7.1-SNAPSHOT - * @date 2015-08-13 + * @date 2015-08-14 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -33018,9 +33018,9 @@ return /******/ (function(modules) { // webpackBootstrap this.requiresTimeout = true; this.previousStates = {}; this.referenceState = {}; - this.adaptive = false; + this.adaptiveTimestep = true; this.adaptiveCounter = 0; - this.adaptiveInterval = 3; + this.adaptiveInterval = 2; this.freezeCache = {}; this.renderTimer = undefined; @@ -33067,7 +33067,7 @@ return /******/ (function(modules) { // webpackBootstrap damping: 0.09 }, maxVelocity: 50, - minVelocity: 0.1, // px/s + minVelocity: 0.75, // px/s solver: 'barnesHut', stabilization: { enabled: true, @@ -33210,7 +33210,7 @@ return /******/ (function(modules) { // webpackBootstrap this.stabilized = false; // when visible, adaptivity is disabled. - this.adaptive = false; + //this.adaptiveTimestep = false; // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes"); @@ -33297,10 +33297,11 @@ return /******/ (function(modules) { // webpackBootstrap value: function physicsTick() { if (this.stabilized === false) { // adaptivity means the timestep adapts to the situation, only applicable for stabilization - if (this.adaptive === true) { + if (this.adaptiveTimestep === true) { this.adaptiveCounter += 1; if (this.adaptiveCounter % this.adaptiveInterval === 0) { // we leave the timestep stable for "interval" iterations. + //console.log(this.timestep) // first the big step and revert. Revert saves the reference state. this.timestep = 2 * this.timestep; this.calculateForces(); @@ -33318,18 +33319,18 @@ return /******/ (function(modules) { // webpackBootstrap // we compare the two steps. if it is acceptable we double the step. if (this.compare() === true) { - this.timestep = 2 * this.timestep; + this.timestep = 1.1 * this.timestep; } else { // if not, we half the step to a minimum of the options timestep. // if the half the timestep is smaller than the options step, we do not reset the counter // we assume that the options timestep is stable enough. - if (0.5 * this.timestep < this.options.timestep) { + if (0.9 * this.timestep < this.options.timestep) { this.timestep = this.options.timestep; } else { // if the timestep was larger than 2 times the option one we check the adaptivity again to ensure // that large instabilities do not form. this.adaptiveCounter = -1; // check again next iteration - this.timestep = 0.5 * this.timestep; + this.timestep = 0.9 * this.timestep; } } } else { @@ -33476,26 +33477,17 @@ return /******/ (function(modules) { // webpackBootstrap var nodesPresent = false; var nodeIndices = this.physicsBody.physicsNodeIndices; var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9; - var stabilized = true; - var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale, 0.05); + var maxNodeVelocity = 0; for (var i = 0; i < nodeIndices.length; i++) { var nodeId = nodeIndices[i]; var nodeVelocity = this._performStep(nodeId, maxVelocity); // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized - stabilized = nodeVelocity < vminCorrected && stabilized === true; + maxNodeVelocity = Math.max(nodeVelocity, maxNodeVelocity); nodesPresent = true; } - if (nodesPresent === true) { - if (vminCorrected > 0.5 * this.options.maxVelocity) { - this.stabilized = false; - } else { - this.stabilized = stabilized; - } - return; - } - this.stabilized = true; + this.stabilized = maxNodeVelocity <= this.options.minVelocity; } /** @@ -33617,7 +33609,7 @@ return /******/ (function(modules) { // webpackBootstrap } // enable adaptive timesteps - this.adaptive = true; + this.adaptiveTimestep = true; // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes"); @@ -33648,7 +33640,6 @@ return /******/ (function(modules) { // webpackBootstrap var count = 0; while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.targetIterations) { this.physicsTick(); - this.stabilizationIterations++; count++; } @@ -39026,8 +39017,8 @@ return /******/ (function(modules) { // webpackBootstrap } else { this.body.modules.clustering.clusterOutliers(); } - console.log('levels', levels); } + // increase the size of the edges this.body.modules.kamadaKawai.setOptions({ springLength: Math.max(150, 2 * startLength) }); } diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index dc5ca4a3..80666b72 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -198,9 +198,8 @@ class LayoutEngine { else { this.body.modules.clustering.clusterOutliers(); } - console.log('levels', levels) - } + // increase the size of the edges this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150,2*startLength)}) } diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index 502e8e69..a447b79b 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -20,9 +20,9 @@ class PhysicsEngine { this.requiresTimeout = true; this.previousStates = {}; this.referenceState = {}; - this.adaptive = false; + this.adaptiveTimestep = true; this.adaptiveCounter = 0; - this.adaptiveInterval = 3; + this.adaptiveInterval = 2; this.freezeCache = {}; this.renderTimer = undefined; @@ -69,7 +69,7 @@ class PhysicsEngine { damping: 0.09 }, maxVelocity: 50, - minVelocity: 0.1, // px/s + minVelocity: 0.75, // px/s solver: 'barnesHut', stabilization: { enabled: true, @@ -200,7 +200,7 @@ class PhysicsEngine { this.stabilized = false; // when visible, adaptivity is disabled. - this.adaptive = false; + //this.adaptiveTimestep = false; // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes"); @@ -279,9 +279,10 @@ class PhysicsEngine { physicsTick() { if (this.stabilized === false) { // adaptivity means the timestep adapts to the situation, only applicable for stabilization - if (this.adaptive === true) { + if (this.adaptiveTimestep === true) { this.adaptiveCounter += 1; if (this.adaptiveCounter % this.adaptiveInterval === 0) { // we leave the timestep stable for "interval" iterations. + //console.log(this.timestep) // first the big step and revert. Revert saves the reference state. this.timestep = 2 * this.timestep; this.calculateForces(); @@ -299,20 +300,20 @@ class PhysicsEngine { // we compare the two steps. if it is acceptable we double the step. if (this.compare() === true) { - this.timestep = 2 * this.timestep; + this.timestep = 1.1 * this.timestep; } else { // if not, we half the step to a minimum of the options timestep. // if the half the timestep is smaller than the options step, we do not reset the counter // we assume that the options timestep is stable enough. - if (0.5 * this.timestep < this.options.timestep) { + if (0.9 * this.timestep < this.options.timestep) { this.timestep = this.options.timestep; } else { // if the timestep was larger than 2 times the option one we check the adaptivity again to ensure // that large instabilities do not form. this.adaptiveCounter = -1; // check again next iteration - this.timestep = 0.5 * this.timestep; + this.timestep = 0.9 * this.timestep; } } } @@ -454,27 +455,18 @@ class PhysicsEngine { var nodesPresent = false; var nodeIndices = this.physicsBody.physicsNodeIndices; var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9; - var stabilized = true; - var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale,0.05); + var maxNodeVelocity = 0; + for (let i = 0; i < nodeIndices.length; i++) { let nodeId = nodeIndices[i]; let nodeVelocity = this._performStep(nodeId, maxVelocity); // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized - stabilized = nodeVelocity < vminCorrected && stabilized === true; + maxNodeVelocity = Math.max(nodeVelocity,maxNodeVelocity); nodesPresent = true; } - if (nodesPresent === true) { - if (vminCorrected > 0.5*this.options.maxVelocity) { - this.stabilized = false; - } - else { - this.stabilized = stabilized; - } - return; - } - this.stabilized = true; + this.stabilized = maxNodeVelocity <= this.options.minVelocity; } @@ -588,7 +580,7 @@ class PhysicsEngine { } // enable adaptive timesteps - this.adaptive = true; + this.adaptiveTimestep = true; // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes"); @@ -616,7 +608,6 @@ class PhysicsEngine { var count = 0; while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.targetIterations) { this.physicsTick(); - this.stabilizationIterations++; count++; } From 93f1480af39c1eadf130c069f95416be55a0a970 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Fri, 14 Aug 2015 11:33:12 +0200 Subject: [PATCH 05/10] - improved performance --- dist/vis.js | 14 +++++++++----- lib/network/modules/PhysicsEngine.js | 15 +++++++++------ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/dist/vis.js b/dist/vis.js index e3761ba1..900e65c5 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -33019,6 +33019,7 @@ return /******/ (function(modules) { // webpackBootstrap this.previousStates = {}; this.referenceState = {}; this.adaptiveTimestep = true; + this.adaptiveTimestepEnabled = false; this.adaptiveCounter = 0; this.adaptiveInterval = 2; this.freezeCache = {}; @@ -33297,11 +33298,11 @@ return /******/ (function(modules) { // webpackBootstrap value: function physicsTick() { if (this.stabilized === false) { // adaptivity means the timestep adapts to the situation, only applicable for stabilization - if (this.adaptiveTimestep === true) { + if (this.adaptiveTimestep === true && this.adaptiveTimestepEnabled === true) { this.adaptiveCounter += 1; + var factor = 1.2; if (this.adaptiveCounter % this.adaptiveInterval === 0) { // we leave the timestep stable for "interval" iterations. - //console.log(this.timestep) // first the big step and revert. Revert saves the reference state. this.timestep = 2 * this.timestep; this.calculateForces(); @@ -33319,18 +33320,18 @@ return /******/ (function(modules) { // webpackBootstrap // we compare the two steps. if it is acceptable we double the step. if (this.compare() === true) { - this.timestep = 1.1 * this.timestep; + this.timestep = factor * this.timestep; } else { // if not, we half the step to a minimum of the options timestep. // if the half the timestep is smaller than the options step, we do not reset the counter // we assume that the options timestep is stable enough. - if (0.9 * this.timestep < this.options.timestep) { + if (this.timestep / factor < this.options.timestep) { this.timestep = this.options.timestep; } else { // if the timestep was larger than 2 times the option one we check the adaptivity again to ensure // that large instabilities do not form. this.adaptiveCounter = -1; // check again next iteration - this.timestep = 0.9 * this.timestep; + this.timestep = Math.max(this.options.timestep, this.timestep / factor); } } } else { @@ -33478,6 +33479,7 @@ return /******/ (function(modules) { // webpackBootstrap var nodeIndices = this.physicsBody.physicsNodeIndices; var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9; var maxNodeVelocity = 0; + var velocityAdaptiveThreshold = 5; for (var i = 0; i < nodeIndices.length; i++) { var nodeId = nodeIndices[i]; @@ -33487,6 +33489,8 @@ return /******/ (function(modules) { // webpackBootstrap nodesPresent = true; } + this.adaptiveTimestepEnabled = maxNodeVelocity < velocityAdaptiveThreshold; + this.stabilized = maxNodeVelocity <= this.options.minVelocity; } diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index a447b79b..c136352b 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -21,6 +21,7 @@ class PhysicsEngine { this.previousStates = {}; this.referenceState = {}; this.adaptiveTimestep = true; + this.adaptiveTimestepEnabled = false; this.adaptiveCounter = 0; this.adaptiveInterval = 2; this.freezeCache = {}; @@ -279,10 +280,10 @@ class PhysicsEngine { physicsTick() { if (this.stabilized === false) { // adaptivity means the timestep adapts to the situation, only applicable for stabilization - if (this.adaptiveTimestep === true) { + if (this.adaptiveTimestep === true && this.adaptiveTimestepEnabled === true) { this.adaptiveCounter += 1; + let factor = 1.2; if (this.adaptiveCounter % this.adaptiveInterval === 0) { // we leave the timestep stable for "interval" iterations. - //console.log(this.timestep) // first the big step and revert. Revert saves the reference state. this.timestep = 2 * this.timestep; this.calculateForces(); @@ -300,20 +301,20 @@ class PhysicsEngine { // we compare the two steps. if it is acceptable we double the step. if (this.compare() === true) { - this.timestep = 1.1 * this.timestep; + this.timestep = factor * this.timestep; } else { // if not, we half the step to a minimum of the options timestep. // if the half the timestep is smaller than the options step, we do not reset the counter // we assume that the options timestep is stable enough. - if (0.9 * this.timestep < this.options.timestep) { + if (this.timestep/factor < this.options.timestep) { this.timestep = this.options.timestep; } else { // if the timestep was larger than 2 times the option one we check the adaptivity again to ensure // that large instabilities do not form. this.adaptiveCounter = -1; // check again next iteration - this.timestep = 0.9 * this.timestep; + this.timestep = Math.max(this.options.timestep, this.timestep/factor); } } } @@ -456,7 +457,7 @@ class PhysicsEngine { var nodeIndices = this.physicsBody.physicsNodeIndices; var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9; var maxNodeVelocity = 0; - + var velocityAdaptiveThreshold = 5; for (let i = 0; i < nodeIndices.length; i++) { let nodeId = nodeIndices[i]; @@ -466,6 +467,8 @@ class PhysicsEngine { nodesPresent = true; } + this.adaptiveTimestepEnabled = maxNodeVelocity < velocityAdaptiveThreshold; + this.stabilized = maxNodeVelocity <= this.options.minVelocity; } From 8eea19a7560c58d2aed37fbe720fd033f4e53e7c Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Fri, 14 Aug 2015 12:23:16 +0200 Subject: [PATCH 06/10] - cleaning up --- dist/vis.js | 61 ++++++++++++++++++++-------- lib/network/modules/LayoutEngine.js | 1 + lib/network/modules/PhysicsEngine.js | 58 ++++++++++++++++++-------- 3 files changed, 85 insertions(+), 35 deletions(-) diff --git a/dist/vis.js b/dist/vis.js index 900e65c5..b774962f 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -33018,13 +33018,15 @@ return /******/ (function(modules) { // webpackBootstrap this.requiresTimeout = true; this.previousStates = {}; this.referenceState = {}; - this.adaptiveTimestep = true; - this.adaptiveTimestepEnabled = false; - this.adaptiveCounter = 0; - this.adaptiveInterval = 2; this.freezeCache = {}; this.renderTimer = undefined; + // parameters for the adaptive timestep + this.adaptiveTimestep = false; + this.adaptiveTimestepEnabled = false; + this.adaptiveCounter = 0; + this.adaptiveInterval = 3; + this.stabilized = false; this.startedStabilization = false; this.stabilizationIterations = 0; @@ -33118,6 +33120,11 @@ return /******/ (function(modules) { // webpackBootstrap _this.body.emitter.off(); }); } + + /** + * set the physics options + * @param options + */ }, { key: 'setOptions', value: function setOptions(options) { @@ -33211,7 +33218,7 @@ return /******/ (function(modules) { // webpackBootstrap this.stabilized = false; // when visible, adaptivity is disabled. - //this.adaptiveTimestep = false; + this.adaptiveTimestep = false; // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes"); @@ -33299,8 +33306,10 @@ return /******/ (function(modules) { // webpackBootstrap if (this.stabilized === false) { // adaptivity means the timestep adapts to the situation, only applicable for stabilization if (this.adaptiveTimestep === true && this.adaptiveTimestepEnabled === true) { - this.adaptiveCounter += 1; + // this is the factor for increasing the timestep on success. var factor = 1.2; + + // we assume the adaptive interval is if (this.adaptiveCounter % this.adaptiveInterval === 0) { // we leave the timestep stable for "interval" iterations. // first the big step and revert. Revert saves the reference state. @@ -33319,11 +33328,11 @@ return /******/ (function(modules) { // webpackBootstrap this.moveNodes(); // we compare the two steps. if it is acceptable we double the step. - if (this.compare() === true) { + if (this._evaluateStepQuality() === true) { this.timestep = factor * this.timestep; } else { - // if not, we half the step to a minimum of the options timestep. - // if the half the timestep is smaller than the options step, we do not reset the counter + // if not, we decrease the step to a minimum of the options timestep. + // if the decreased timestep is smaller than the options step, we do not reset the counter // we assume that the options timestep is stable enough. if (this.timestep / factor < this.options.timestep) { this.timestep = this.options.timestep; @@ -33339,6 +33348,9 @@ return /******/ (function(modules) { // webpackBootstrap this.calculateForces(); this.moveNodes(); } + + // increment the counter + this.adaptiveCounter += 1; } else { // case for the static timestep, we reset it to the one in options and take a normal step. this.timestep = this.options.timestep; @@ -33444,14 +33456,14 @@ return /******/ (function(modules) { // webpackBootstrap * This compares the reference state to the current state */ }, { - key: 'compare', - value: function compare() { + key: '_evaluateStepQuality', + value: function _evaluateStepQuality() { var dx = undefined, dy = undefined, dpos = undefined; var nodes = this.body.nodes; var reference = this.referenceState; - var posThreshold = 0.25; + var posThreshold = 0.3; for (var nodeId in this.referenceState) { if (this.referenceState.hasOwnProperty(nodeId)) { @@ -33475,23 +33487,25 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: 'moveNodes', value: function moveNodes() { - var nodesPresent = false; var nodeIndices = this.physicsBody.physicsNodeIndices; var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9; var maxNodeVelocity = 0; + var averageNodeVelocity = 0; + + // the velocity threshold (energy in the system) for the adaptivity toggle var velocityAdaptiveThreshold = 5; for (var i = 0; i < nodeIndices.length; i++) { var nodeId = nodeIndices[i]; var nodeVelocity = this._performStep(nodeId, maxVelocity); // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized - maxNodeVelocity = Math.max(nodeVelocity, maxNodeVelocity); - nodesPresent = true; + maxNodeVelocity = Math.max(maxNodeVelocity, nodeVelocity); + averageNodeVelocity += nodeVelocity; } - this.adaptiveTimestepEnabled = maxNodeVelocity < velocityAdaptiveThreshold; - - this.stabilized = maxNodeVelocity <= this.options.minVelocity; + // evaluating the stabilized and adaptiveTimestepEnabled conditions + this.adaptiveTimestepEnabled = averageNodeVelocity / nodeIndices.length < velocityAdaptiveThreshold; + this.stabilized = maxNodeVelocity < this.options.minVelocity; } /** @@ -33638,6 +33652,11 @@ return /******/ (function(modules) { // webpackBootstrap return _this3._stabilizationBatch(); }, 0); } + + /** + * One batch of stabilization + * @private + */ }, { key: '_stabilizationBatch', value: function _stabilizationBatch() { @@ -33654,6 +33673,11 @@ return /******/ (function(modules) { // webpackBootstrap this._finalizeStabilization(); } } + + /** + * Wrap up the stabilization, fit and emit the events. + * @private + */ }, { key: '_finalizeStabilization', value: function _finalizeStabilization() { @@ -39014,6 +39038,7 @@ return /******/ (function(modules) { // webpackBootstrap var startLength = this.body.nodeIndices.length; while (this.body.nodeIndices.length > 150) { levels += 1; + // if there are many nodes we do a hubsize cluster if (levels % 5 === 0) { this.body.modules.clustering.clusterByHubsize(); } else if (levels % 3 === 0) { diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index 80666b72..9ef2aea3 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -189,6 +189,7 @@ class LayoutEngine { let startLength = this.body.nodeIndices.length; while(this.body.nodeIndices.length > 150) { levels += 1; + // if there are many nodes we do a hubsize cluster if (levels % 5 === 0) { this.body.modules.clustering.clusterByHubsize(); } diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index c136352b..a22af54a 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -20,13 +20,15 @@ class PhysicsEngine { this.requiresTimeout = true; this.previousStates = {}; this.referenceState = {}; - this.adaptiveTimestep = true; - this.adaptiveTimestepEnabled = false; - this.adaptiveCounter = 0; - this.adaptiveInterval = 2; this.freezeCache = {}; this.renderTimer = undefined; + // parameters for the adaptive timestep + this.adaptiveTimestep = false; + this.adaptiveTimestepEnabled = false; + this.adaptiveCounter = 0; + this.adaptiveInterval = 3; + this.stabilized = false; this.startedStabilization = false; this.stabilizationIterations = 0; @@ -109,6 +111,11 @@ class PhysicsEngine { }); } + + /** + * set the physics options + * @param options + */ setOptions(options) { if (options !== undefined) { if (options === false) { @@ -201,7 +208,7 @@ class PhysicsEngine { this.stabilized = false; // when visible, adaptivity is disabled. - //this.adaptiveTimestep = false; + this.adaptiveTimestep = false; // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes"); @@ -281,8 +288,10 @@ class PhysicsEngine { if (this.stabilized === false) { // adaptivity means the timestep adapts to the situation, only applicable for stabilization if (this.adaptiveTimestep === true && this.adaptiveTimestepEnabled === true) { - this.adaptiveCounter += 1; + // this is the factor for increasing the timestep on success. let factor = 1.2; + + // we assume the adaptive interval is if (this.adaptiveCounter % this.adaptiveInterval === 0) { // we leave the timestep stable for "interval" iterations. // first the big step and revert. Revert saves the reference state. this.timestep = 2 * this.timestep; @@ -300,12 +309,12 @@ class PhysicsEngine { this.moveNodes(); // we compare the two steps. if it is acceptable we double the step. - if (this.compare() === true) { + if (this._evaluateStepQuality() === true) { this.timestep = factor * this.timestep; } else { - // if not, we half the step to a minimum of the options timestep. - // if the half the timestep is smaller than the options step, we do not reset the counter + // if not, we decrease the step to a minimum of the options timestep. + // if the decreased timestep is smaller than the options step, we do not reset the counter // we assume that the options timestep is stable enough. if (this.timestep/factor < this.options.timestep) { this.timestep = this.options.timestep; @@ -323,6 +332,9 @@ class PhysicsEngine { this.calculateForces(); this.moveNodes(); } + + // increment the counter + this.adaptiveCounter += 1; } else { // case for the static timestep, we reset it to the one in options and take a normal step. @@ -427,11 +439,11 @@ class PhysicsEngine { /** * This compares the reference state to the current state */ - compare() { + _evaluateStepQuality() { let dx, dy, dpos; let nodes = this.body.nodes; let reference = this.referenceState; - let posThreshold = 0.25; + let posThreshold = 0.3; for (let nodeId in this.referenceState) { if (this.referenceState.hasOwnProperty(nodeId)) { @@ -453,23 +465,25 @@ class PhysicsEngine { * @returns {boolean} */ moveNodes() { - var nodesPresent = false; var nodeIndices = this.physicsBody.physicsNodeIndices; var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9; var maxNodeVelocity = 0; + var averageNodeVelocity = 0; + + // the velocity threshold (energy in the system) for the adaptivity toggle var velocityAdaptiveThreshold = 5; for (let i = 0; i < nodeIndices.length; i++) { let nodeId = nodeIndices[i]; let nodeVelocity = this._performStep(nodeId, maxVelocity); // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized - maxNodeVelocity = Math.max(nodeVelocity,maxNodeVelocity); - nodesPresent = true; + maxNodeVelocity = Math.max(maxNodeVelocity,nodeVelocity); + averageNodeVelocity += nodeVelocity; } - this.adaptiveTimestepEnabled = maxNodeVelocity < velocityAdaptiveThreshold; - - this.stabilized = maxNodeVelocity <= this.options.minVelocity; + // evaluating the stabilized and adaptiveTimestepEnabled conditions + this.adaptiveTimestepEnabled = (averageNodeVelocity/nodeIndices.length) < velocityAdaptiveThreshold; + this.stabilized = maxNodeVelocity < this.options.minVelocity; } @@ -607,6 +621,11 @@ class PhysicsEngine { setTimeout(() => this._stabilizationBatch(),0); } + + /** + * One batch of stabilization + * @private + */ _stabilizationBatch() { var count = 0; while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.targetIterations) { @@ -623,6 +642,11 @@ class PhysicsEngine { } } + + /** + * Wrap up the stabilization, fit and emit the events. + * @private + */ _finalizeStabilization() { this.body.emitter.emit('_allowRedraw'); if (this.options.stabilization.fit === true) { From d0197226c02469677f8988aba927cacb26785a0e Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Sat, 15 Aug 2015 13:28:52 +0200 Subject: [PATCH 07/10] - added labels for edges and titles for nodes and edges in gephi import --- lib/network/gephiParser.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/network/gephiParser.js b/lib/network/gephiParser.js index a7deb34e..0e492964 100644 --- a/lib/network/gephiParser.js +++ b/lib/network/gephiParser.js @@ -27,6 +27,8 @@ function parseGephi(gephiJSON, optionsObj) { edge['from'] = gEdge.source; edge['to'] = gEdge.target; edge['attributes'] = gEdge.attributes; + edge['label'] = gEdge.label; + edge['title'] = gEdge.attributes !== undefined ? gEdge.attributes.title : undefined; // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; if (gEdge.color && options.inheritColor === false) { @@ -44,6 +46,7 @@ function parseGephi(gephiJSON, optionsObj) { node['x'] = gNode.x; node['y'] = gNode.y; node['label'] = gNode.label; + node['title'] = gNode.attributes !== undefined ? gNode.attributes.title : undefined; if (options.nodes.parseColor === true) { node['color'] = gNode.color; } From 26a1fc588a7c1a3d4d921107f6c7774f33011c67 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Sun, 16 Aug 2015 15:30:48 +0200 Subject: [PATCH 08/10] - Fixed bugs in clustering algorithm. - Greatly improved performance in clustering. --- HISTORY.md | 6 +- dist/vis.js | 383 ++++++++++++++------------- lib/network/modules/Clustering.js | 359 +++++++++++++------------ lib/network/modules/LayoutEngine.js | 29 +- lib/network/modules/PhysicsEngine.js | 2 +- test/networkTest.html | 60 +++-- 6 files changed, 449 insertions(+), 390 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 10e9ef66..b5d76dad 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -14,7 +14,11 @@ http://visjs.org - Fixed cleaning up of nodes. - Improved the positioning and CSS of the configurator and the color picker. - Fixed dynamic updating of label properties. - +- Added support for labels in edges and titles for both nodes and edges during gephi import. +- Added KamadaKawai layout engine for improved initial layout. +- Added Adaptive timestep to the physics solvers for increased performance during stabilization. +- Fixed bugs in clustering algorithm. +- Greatly improved performance in clustering. ## 2015-07-27, version 4.7.0 diff --git a/dist/vis.js b/dist/vis.js index b774962f..759a85b5 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 4.7.1-SNAPSHOT - * @date 2015-08-14 + * @date 2015-08-16 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -33199,7 +33199,7 @@ return /******/ (function(modules) { // webpackBootstrap } else { this.stabilized = false; this.ready = true; - this.body.emitter.emit('fit', {}, true); + this.body.emitter.emit('fit', {}, false); this.startSimulation(); } } else { @@ -34902,7 +34902,7 @@ return /******/ (function(modules) { // webpackBootstrap } for (var i = 0; i < nodesToCluster.length; i++) { - this.clusterByConnection(nodesToCluster[i], options, false); + this.clusterByConnection(nodesToCluster[i], options, true); } this.body.emitter.emit('_dataChanged'); @@ -34940,7 +34940,9 @@ return /******/ (function(modules) { // webpackBootstrap // collect the nodes that will be in the cluster for (var _i = 0; _i < node.edges.length; _i++) { var edge = node.edges[_i]; - childEdgesObj[edge.id] = edge; + if (edge.hiddenByCluster !== true) { + childEdgesObj[edge.id] = edge; + } } } } @@ -34961,45 +34963,65 @@ return /******/ (function(modules) { // webpackBootstrap options = this._checkOptions(options); var clusters = []; - + var usedNodes = {}; + var edge = undefined, + edges = undefined, + node = undefined, + nodeId = undefined, + visibleEdges = undefined; // collect the nodes that will be in the cluster for (var i = 0; i < this.body.nodeIndices.length; i++) { var childNodesObj = {}; var childEdgesObj = {}; - var nodeId = this.body.nodeIndices[i]; - var visibleEdges = 0; - var edge = undefined; - for (var j = 0; j < this.body.nodes[nodeId].edges.length; j++) { - if (this.body.nodes[nodeId].edges[j].options.hidden === false) { - visibleEdges++; - edge = this.body.nodes[nodeId].edges[j]; + nodeId = this.body.nodeIndices[i]; + + // if this node is already used in another cluster this session, we do not have to re-evaluate it. + if (usedNodes[nodeId] === undefined) { + visibleEdges = 0; + node = this.body.nodes[nodeId]; + edges = []; + for (var j = 0; j < node.edges.length; j++) { + edge = node.edges[j]; + if (edge.hiddenByCluster !== true) { + edges.push(edge); + } } - } - if (visibleEdges === edgeCount) { - // this is a qualifying node - var childNodeId = this._getConnectedId(edge, nodeId); - if (childNodeId !== nodeId) { - if (options.joinCondition === undefined) { - if (this._checkIfUsed(clusters, nodeId, edge.id) === false && this._checkIfUsed(clusters, childNodeId, edge.id) === false) { - childEdgesObj[edge.id] = edge; - childNodesObj[nodeId] = this.body.nodes[nodeId]; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } - } else { - var clonedOptions = this._cloneOptions(this.body.nodes[nodeId]); - if (options.joinCondition(clonedOptions) === true && this._checkIfUsed(clusters, nodeId, edge.id) === false) { - childEdgesObj[edge.id] = edge; - childNodesObj[nodeId] = this.body.nodes[nodeId]; - } - clonedOptions = this._cloneOptions(this.body.nodes[childNodeId]); - if (options.joinCondition(clonedOptions) === true && this._checkIfUsed(clusters, nodeId, edge.id) === false) { - childEdgesObj[edge.id] = edge; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + // this node qualifies, we collect its neighbours to start the clustering process. + if (edges.length === edgeCount) { + var gatheringSuccessful = true; + for (var j = 0; j < edges.length; j++) { + edge = edges[j]; + var childNodeId = this._getConnectedId(edge, nodeId); + // if unused and if not referencing itself + if (childNodeId !== nodeId && usedNodes[nodeId] === undefined) { + // add the nodes to the list by the join condition. + if (options.joinCondition === undefined) { + childEdgesObj[edge.id] = edge; + childNodesObj[nodeId] = this.body.nodes[nodeId]; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + usedNodes[nodeId] = true; + } else { + var clonedOptions = this._cloneOptions(this.body.nodes[nodeId]); + if (options.joinCondition(clonedOptions) === true) { + childEdgesObj[edge.id] = edge; + childNodesObj[nodeId] = this.body.nodes[nodeId]; + usedNodes[nodeId] = true; + } else { + // this node does not qualify after all. + gatheringSuccessful = false; + break; + } + } + } else { + // this node does not qualify after all. + gatheringSuccessful = false; + break; } } - if (Object.keys(childNodesObj).length > 0 && Object.keys(childEdgesObj).length > 0) { + // add to the cluster queue + if (Object.keys(childNodesObj).length > 0 && Object.keys(childEdgesObj).length > 0 && gatheringSuccessful === true) { clusters.push({ nodes: childNodesObj, edges: childEdgesObj }); } } @@ -35040,17 +35062,6 @@ return /******/ (function(modules) { // webpackBootstrap this.clusterByEdgeCount(2, options, refreshData); } - }, { - key: '_checkIfUsed', - value: function _checkIfUsed(clusters, nodeId, edgeId) { - for (var i = 0; i < clusters.length; i++) { - var cluster = clusters[i]; - if (cluster.nodes[nodeId] !== undefined || cluster.edges[edgeId] !== undefined) { - return true; - } - } - return false; - } /** * suck all connected nodes of a node into the node. @@ -35094,26 +35105,31 @@ return /******/ (function(modules) { // webpackBootstrap // collect the nodes that will be in the cluster for (var i = 0; i < node.edges.length; i++) { var edge = node.edges[i]; - var childNodeId = this._getConnectedId(edge, parentNodeId); + if (edge.hiddenByCluster !== true) { + var childNodeId = this._getConnectedId(edge, parentNodeId); - if (this.clusteredNodes[childNodeId] === undefined) { - if (childNodeId !== parentNodeId) { - if (options.joinCondition === undefined) { - childEdgesObj[edge.id] = edge; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } else { - // clone the options and insert some additional parameters that could be interesting. - var childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]); - if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) { + // if the child node is not in a cluster (may not be needed now with the edge.hiddenByCluster check) + if (this.clusteredNodes[childNodeId] === undefined) { + if (childNodeId !== parentNodeId) { + if (options.joinCondition === undefined) { childEdgesObj[edge.id] = edge; childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } else { + // clone the options and insert some additional parameters that could be interesting. + var childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]); + if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } } + } else { + // swallow the edge if it is self-referencing. + childEdgesObj[edge.id] = edge; } - } else { - childEdgesObj[edge.id] = edge; } } } + this._cluster(childNodesObj, childEdgesObj, options, refreshData); } @@ -35140,17 +35156,17 @@ return /******/ (function(modules) { // webpackBootstrap } /** - * This function creates the edges that will be attached to the cluster. + * This function creates the edges that will be attached to the cluster + * It looks for edges that are connected to the nodes from the "outside' of the cluster. * * @param childNodesObj - * @param childEdgesObj * @param newEdges * @param options * @private */ }, { key: '_createClusterEdges', - value: function _createClusterEdges(childNodesObj, childEdgesObj, newEdges, clusterNodeProperties, clusterEdgeProperties) { + value: function _createClusterEdges(childNodesObj, clusterNodeProperties, clusterEdgeProperties) { var edge = undefined, childNodeId = undefined, childNode = undefined, @@ -35158,7 +35174,10 @@ return /******/ (function(modules) { // webpackBootstrap fromId = undefined, otherNodeId = undefined; + // loop over all child nodes and their edges to find edges going out of the cluster + // these edges will be replaced by clusterEdges. var childKeys = Object.keys(childNodesObj); + var createEdges = []; for (var i = 0; i < childKeys.length; i++) { childNodeId = childKeys[i]; childNode = childNodesObj[childNodeId]; @@ -35166,31 +35185,55 @@ return /******/ (function(modules) { // webpackBootstrap // construct new edges from the cluster to others for (var j = 0; j < childNode.edges.length; j++) { edge = childNode.edges[j]; - childEdgesObj[edge.id] = edge; - - // childNodeId position will be replaced by the cluster. - if (edge.toId == childNodeId) { - // this is a double equals because ints and strings can be interchanged here. - toId = clusterNodeProperties.id; - fromId = edge.fromId; - otherNodeId = fromId; - } else { - toId = edge.toId; - fromId = clusterNodeProperties.id; - otherNodeId = toId; - } + // we only handle edges that are visible to the system, not the disabled ones from the clustering process. + if (edge.hiddenByCluster !== true) { + // set up the from and to. + if (edge.toId == childNodeId) { + // this is a double equals because ints and strings can be interchanged here. + toId = clusterNodeProperties.id; + fromId = edge.fromId; + otherNodeId = fromId; + } else { + toId = edge.toId; + fromId = clusterNodeProperties.id; + otherNodeId = toId; + } - // if the node connected to the cluster is also in the cluster we do not need a new edge. - if (childNodesObj[otherNodeId] === undefined) { - var clonedOptions = this._cloneOptions(edge, 'edge'); - util.deepExtend(clonedOptions, clusterEdgeProperties); - clonedOptions.from = fromId; - clonedOptions.to = toId; - clonedOptions.id = 'clusterEdge:' + util.randomUUID(); - newEdges.push(this.body.functions.createEdge(clonedOptions)); + // Only edges from the cluster outwards are being replaced. + if (childNodesObj[otherNodeId] === undefined) { + createEdges.push({ edge: edge, fromId: fromId, toId: toId }); + } } } } + + // here we actually create the replacement edges. We could not do this in the loop above as the creation process + // would add an edge to the edges array we are iterating over. + for (var j = 0; j < createEdges.length; j++) { + var _edge = createEdges[j].edge; + // copy the options of the edge we will replace + var clonedOptions = this._cloneOptions(_edge, 'edge'); + // make sure the properties of clusterEdges are superimposed on it + util.deepExtend(clonedOptions, clusterEdgeProperties); + + // set up the edge + clonedOptions.from = createEdges[j].fromId; + clonedOptions.to = createEdges[j].toId; + clonedOptions.id = 'clusterEdge:' + util.randomUUID(); + //clonedOptions.id = '(cf: ' + createEdges[j].fromId + " to: " + createEdges[j].toId + ")" + Math.random(); + + // create the edge and give a reference to the one it replaced. + var newEdge = this.body.functions.createEdge(clonedOptions); + newEdge.clusteringEdgeReplacingId = _edge.id; + + // connect the edge. + this.body.edges[newEdge.id] = newEdge; + newEdge.connect(); + + // hide the replaced edge + _edge.setOptions({ physics: false, hidden: true }); + _edge.hiddenByCluster = true; + } } /** @@ -35228,8 +35271,8 @@ return /******/ (function(modules) { // webpackBootstrap value: function _cluster(childNodesObj, childEdgesObj, options) { var refreshData = arguments.length <= 3 || arguments[3] === undefined ? true : arguments[3]; - // kill condition: no children so can't cluster - if (Object.keys(childNodesObj).length === 0) { + // kill condition: no children so can't cluster or only one node in the cluster, dont bother + if (Object.keys(childNodesObj).length < 2) { return; } @@ -35311,27 +35354,15 @@ return /******/ (function(modules) { // webpackBootstrap this.body.nodes[clusterNodeProperties.id] = clusterNode; // create the new edges that will connect to the cluster - var newEdges = []; - this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, clusterNodeProperties, options.clusterEdgeProperties); + this._createClusterEdges(childNodesObj, clusterNodeProperties, options.clusterEdgeProperties); // disable the childEdges for (var edgeId in childEdgesObj) { if (childEdgesObj.hasOwnProperty(edgeId)) { if (this.body.edges[edgeId] !== undefined) { var edge = this.body.edges[edgeId]; - - // if this is a cluster edge that is fully encompassed in the cluster, we want to delete it - // this check verifies that both of the connected nodes are in this cluster - if (edgeId.substr(0, 12) === "clusterEdge:" && childNodesObj[edge.fromId] !== undefined && childNodesObj[edge.toId] !== undefined) { - edge.cleanup(); - // this removes the edge from node.edges, which is why edgeIds is formed - edge.disconnect(); - delete childEdgesObj[edgeId]; - delete this.body.edges[edgeId]; - } else { - edge.setOptions({ physics: false, hidden: true }); - //edge.options.hidden = true; - } + edge.setOptions({ physics: false, hidden: true }); + edge.hiddenByCluster = true; } } } @@ -35344,12 +35375,6 @@ return /******/ (function(modules) { // webpackBootstrap } } - // push new edges to global - for (var i = 0; i < newEdges.length; i++) { - this.body.edges[newEdges[i].id] = newEdges[i]; - this.body.edges[newEdges[i].id].connect(); - } - // set ID to undefined so no duplicates arise clusterNodeProperties.id = undefined; @@ -35442,8 +35467,8 @@ return /******/ (function(modules) { // webpackBootstrap if (containedNodes.hasOwnProperty(nodeId)) { var containedNode = this.body.nodes[nodeId]; if (newPositions[nodeId] !== undefined) { - containedNode.x = newPositions[nodeId].x || clusterNode.x; - containedNode.y = newPositions[nodeId].y || clusterNode.y; + containedNode.x = newPositions[nodeId].x === undefined ? clusterNode.x : newPositions[nodeId].x; + containedNode.y = newPositions[nodeId].y === undefined ? clusterNode.y : newPositions[nodeId].y; } } } @@ -35470,76 +35495,77 @@ return /******/ (function(modules) { // webpackBootstrap containedNode.vy = clusterNode.vy; // we use these methods to avoid reinstantiating the shape, which happens with setOptions. - //containedNode.toggleHidden(false); - //containedNode.togglePhysics(true); containedNode.setOptions({ hidden: false, physics: true }); delete this.clusteredNodes[nodeId]; } } - // release edges - for (var edgeId in containedEdges) { - if (containedEdges.hasOwnProperty(edgeId)) { - var edge = containedEdges[edgeId]; - // if this edge was a temporary edge and it's connected nodes do not exist anymore, we remove it from the data - if (this.body.nodes[edge.fromId] === undefined || this.body.nodes[edge.toId] === undefined || edge.toId == clusterNodeId || edge.fromId == clusterNodeId) { - edge.cleanup(); - // this removes the edge from node.edges, which is why edgeIds is formed - edge.disconnect(); - delete this.body.edges[edgeId]; - } else { - // one of the nodes connected to this edge is in a cluster. We give the edge to that cluster so it will be released when that cluster is opened. - if (this.clusteredNodes[edge.fromId] !== undefined || this.clusteredNodes[edge.toId] !== undefined) { - var fromId = undefined, - toId = undefined; - var clusteredNode = this.clusteredNodes[edge.fromId] || this.clusteredNodes[edge.toId]; - var clusterId = clusteredNode.clusterId; - var _clusterNode = this.body.nodes[clusterId]; - _clusterNode.containedEdges[edgeId] = edge; - - if (this.clusteredNodes[edge.fromId] !== undefined) { - fromId = clusterId; - toId = edge.toId; - } else { - fromId = edge.fromId; - toId = clusterId; - } - - // if both from and to nodes are visible, we create a new temporary edge - if (this.body.nodes[fromId].options.hidden !== true && this.body.nodes[toId].options.hidden !== true) { - var clonedOptions = this._cloneOptions(edge, 'edge'); - var id = 'clusterEdge:' + util.randomUUID(); - util.deepExtend(clonedOptions, _clusterNode.clusterEdgeProperties); - util.deepExtend(clonedOptions, { from: fromId, to: toId, hidden: false, physics: true, id: id }); - var newEdge = this.body.functions.createEdge(clonedOptions); - - this.body.edges[id] = newEdge; - this.body.edges[id].connect(); - } + // copy the clusterNode edges because we cannot iterate over an object that we add or remove from. + var edgesToBeDeleted = []; + for (var i = 0; i < clusterNode.edges.length; i++) { + edgesToBeDeleted.push(clusterNode.edges[i]); + } + + // actually handling the deleting. + for (var i = 0; i < edgesToBeDeleted.length; i++) { + var edge = edgesToBeDeleted[i]; + + var otherNodeId = this._getConnectedId(edge, clusterNodeId); + // if the other node is in another cluster, we transfer ownership of this edge to the other cluster + if (this.clusteredNodes[otherNodeId] !== undefined) { + // transfer ownership: + var otherCluster = this.body.nodes[this.clusteredNodes[otherNodeId].clusterId]; + var transferEdge = this.body.edges[edge.clusteringEdgeReplacingId]; + if (transferEdge !== undefined) { + otherCluster.containedEdges[transferEdge.id] = transferEdge; + + // delete local reference + delete containedEdges[transferEdge.id]; + + // create new cluster edge from the otherCluster: + // get to and from + var fromId = transferEdge.fromId; + var toId = transferEdge.toId; + if (transferEdge.toId == otherNodeId) { + toId = this.clusteredNodes[otherNodeId].clusterId; } else { - edge.setOptions({ physics: true, hidden: false }); - //edge.options.hidden = false; - //edge.togglePhysics(true); + fromId = this.clusteredNodes[otherNodeId].clusterId; } + + // clone the options and apply the cluster options to them + var clonedOptions = this._cloneOptions(transferEdge, 'edge'); + util.deepExtend(clonedOptions, otherCluster.clusterEdgeProperties); + + // apply the edge specific options to it. + var id = 'clusterEdge:' + util.randomUUID(); + util.deepExtend(clonedOptions, { from: fromId, to: toId, hidden: false, physics: true, id: id }); + + // create it + var newEdge = this.body.functions.createEdge(clonedOptions); + newEdge.clusteringEdgeReplacingId = transferEdge.id; + this.body.edges[id] = newEdge; + this.body.edges[id].connect(); + } + } else { + var replacedEdge = this.body.edges[edge.clusteringEdgeReplacingId]; + if (replacedEdge !== undefined) { + replacedEdge.setOptions({ physics: true, hidden: false }); + replacedEdge.hiddenByCluster = false; } } + edge.cleanup(); + // this removes the edge from node.edges, which is why edgeIds is formed + edge.disconnect(); + delete this.body.edges[edge.id]; } - // remove all temporary edges, make an array of ids so we don't remove from the list we're iterating over. - var removeIds = []; - for (var i = 0; i < clusterNode.edges.length; i++) { - var edgeId = clusterNode.edges[i].id; - removeIds.push(edgeId); - } - - // actually removing the edges - for (var i = 0; i < removeIds.length; i++) { - var edgeId = removeIds[i]; - this.body.edges[edgeId].cleanup(); - // this removes the edge from node.edges, which is why edgeIds is formed - this.body.edges[edgeId].disconnect(); - delete this.body.edges[edgeId]; + // handle the releasing of the edges + for (var edgeId in containedEdges) { + if (containedEdges.hasOwnProperty(edgeId)) { + var edge = containedEdges[edgeId]; + edge.setOptions({ physics: true, hidden: false }); + } } // remove clusterNode @@ -39017,6 +39043,11 @@ return /******/ (function(modules) { // webpackBootstrap } } } + + /** + * Use KamadaKawai to position nodes. This is quite a heavy algorithm so if there are a lot of nodes we + * cluster them first to reduce the amount. + */ }, { key: 'layoutNetwork', value: function layoutNetwork() { @@ -39033,15 +39064,14 @@ return /******/ (function(modules) { // webpackBootstrap // if less than half of the nodes have a predefined position we continue if (positionDefined < 0.5 * this.body.nodeIndices.length) { var levels = 0; + var clusterThreshold = 100; // if there are a lot of nodes, we cluster before we run the algorithm. - if (this.body.nodeIndices.length > 100) { + if (this.body.nodeIndices.length > clusterThreshold) { var startLength = this.body.nodeIndices.length; - while (this.body.nodeIndices.length > 150) { + while (this.body.nodeIndices.length > clusterThreshold) { levels += 1; // if there are many nodes we do a hubsize cluster - if (levels % 5 === 0) { - this.body.modules.clustering.clusterByHubsize(); - } else if (levels % 3 === 0) { + if (levels % 3 === 0) { this.body.modules.clustering.clusterBridges(); } else { this.body.modules.clustering.clusterOutliers(); @@ -39062,17 +39092,7 @@ return /******/ (function(modules) { // webpackBootstrap for (var i = 0; i < this.body.nodeIndices.length; i++) { if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) { clustersPresent = true; - this.body.modules.clustering.openCluster(this.body.nodeIndices[i], { - releaseFunction: function releaseFunction(clusterPosition, containedNodesPositions) { - var newPositions = {}; - for (var nodeId in containedNodesPositions) { - if (containedNodesPositions.hasOwnProperty(nodeId)) { - newPositions[nodeId] = { x: clusterPosition.x, y: clusterPosition.y }; - } - } - return newPositions; - } - }, false); + this.body.modules.clustering.openCluster(this.body.nodeIndices[i], {}, false); } } if (clustersPresent === true) { @@ -42710,6 +42730,8 @@ return /******/ (function(modules) { // webpackBootstrap edge['from'] = gEdge.source; edge['to'] = gEdge.target; edge['attributes'] = gEdge.attributes; + edge['label'] = gEdge.label; + edge['title'] = gEdge.attributes !== undefined ? gEdge.attributes.title : undefined; // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; if (gEdge.color && options.inheritColor === false) { @@ -42727,6 +42749,7 @@ return /******/ (function(modules) { // webpackBootstrap node['x'] = gNode.x; node['y'] = gNode.y; node['label'] = gNode.label; + node['title'] = gNode.attributes !== undefined ? gNode.attributes.title : undefined; if (options.nodes.parseColor === true) { node['color'] = gNode.color; } else { diff --git a/lib/network/modules/Clustering.js b/lib/network/modules/Clustering.js index f41c7af7..9c1c5889 100644 --- a/lib/network/modules/Clustering.js +++ b/lib/network/modules/Clustering.js @@ -42,7 +42,7 @@ class ClusterEngine { } for (let i = 0; i < nodesToCluster.length; i++) { - this.clusterByConnection(nodesToCluster[i],options,false); + this.clusterByConnection(nodesToCluster[i],options,true); } this.body.emitter.emit('_dataChanged'); @@ -74,7 +74,9 @@ class ClusterEngine { // collect the nodes that will be in the cluster for (let i = 0; i < node.edges.length; i++) { let edge = node.edges[i]; - childEdgesObj[edge.id] = edge; + if (edge.hiddenByCluster !== true) { + childEdgesObj[edge.id] = edge; + } } } } @@ -92,46 +94,64 @@ class ClusterEngine { clusterByEdgeCount(edgeCount, options, refreshData = true) { options = this._checkOptions(options); let clusters = []; - + let usedNodes = {}; + let edge, edges, node, nodeId, visibleEdges; // collect the nodes that will be in the cluster for (let i = 0; i < this.body.nodeIndices.length; i++) { let childNodesObj = {}; let childEdgesObj = {}; - let nodeId = this.body.nodeIndices[i]; - let visibleEdges = 0; - let edge; - for (let j = 0; j < this.body.nodes[nodeId].edges.length; j++) { - if (this.body.nodes[nodeId].edges[j].options.hidden === false) { - visibleEdges++; - edge = this.body.nodes[nodeId].edges[j]; + nodeId = this.body.nodeIndices[i]; + + // if this node is already used in another cluster this session, we do not have to re-evaluate it. + if (usedNodes[nodeId] === undefined) { + visibleEdges = 0; + node = this.body.nodes[nodeId]; + edges = []; + for (let j = 0; j < node.edges.length; j++) { + edge = node.edges[j]; + if (edge.hiddenByCluster !== true) { + edges.push(edge); + } } - } - if (visibleEdges === edgeCount) { - // this is a qualifying node - let childNodeId = this._getConnectedId(edge, nodeId); - if (childNodeId !== nodeId) { - if (options.joinCondition === undefined) { - if (this._checkIfUsed(clusters,nodeId,edge.id) === false && this._checkIfUsed(clusters,childNodeId,edge.id) === false) { - childEdgesObj[edge.id] = edge; - childNodesObj[nodeId] = this.body.nodes[nodeId]; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + // this node qualifies, we collect its neighbours to start the clustering process. + if (edges.length === edgeCount) { + let gatheringSuccessful = true; + for (let j = 0; j < edges.length; j++) { + edge = edges[j]; + let childNodeId = this._getConnectedId(edge, nodeId); + // if unused and if not referencing itself + if (childNodeId !== nodeId && usedNodes[nodeId] === undefined) { + // add the nodes to the list by the join condition. + if (options.joinCondition === undefined) { + childEdgesObj[edge.id] = edge; + childNodesObj[nodeId] = this.body.nodes[nodeId]; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + usedNodes[nodeId] = true; + } + else { + let clonedOptions = this._cloneOptions(this.body.nodes[nodeId]); + if (options.joinCondition(clonedOptions) === true) { + childEdgesObj[edge.id] = edge; + childNodesObj[nodeId] = this.body.nodes[nodeId]; + usedNodes[nodeId] = true; + } + else { + // this node does not qualify after all. + gatheringSuccessful = false; + break; + } + } } - } - else { - let clonedOptions = this._cloneOptions(this.body.nodes[nodeId]); - if (options.joinCondition(clonedOptions) === true && this._checkIfUsed(clusters,nodeId,edge.id) === false) { - childEdgesObj[edge.id] = edge; - childNodesObj[nodeId] = this.body.nodes[nodeId]; - } - clonedOptions = this._cloneOptions(this.body.nodes[childNodeId]); - if (options.joinCondition(clonedOptions) === true && this._checkIfUsed(clusters,nodeId,edge.id) === false) { - childEdgesObj[edge.id] = edge; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + else { + // this node does not qualify after all. + gatheringSuccessful = false; + break; } } - if (Object.keys(childNodesObj).length > 0 && Object.keys(childEdgesObj).length > 0) { + // add to the cluster queue + if (Object.keys(childNodesObj).length > 0 && Object.keys(childEdgesObj).length > 0 && gatheringSuccessful === true) { clusters.push({nodes: childNodesObj, edges: childEdgesObj}) } } @@ -166,15 +186,6 @@ class ClusterEngine { } - _checkIfUsed(clusters, nodeId, edgeId) { - for (let i = 0; i < clusters.length; i++) { - let cluster = clusters[i]; - if (cluster.nodes[nodeId] !== undefined || cluster.edges[edgeId] !== undefined) { - return true; - } - } - return false; - } /** * suck all connected nodes of a node into the node. @@ -207,28 +218,33 @@ class ClusterEngine { // collect the nodes that will be in the cluster for (let i = 0; i < node.edges.length; i++) { let edge = node.edges[i]; - let childNodeId = this._getConnectedId(edge, parentNodeId); + if (edge.hiddenByCluster !== true) { + let childNodeId = this._getConnectedId(edge, parentNodeId); - if (this.clusteredNodes[childNodeId] === undefined) { - if (childNodeId !== parentNodeId) { - if (options.joinCondition === undefined) { - childEdgesObj[edge.id] = edge; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } - else { - // clone the options and insert some additional parameters that could be interesting. - let childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]); - if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) { + // if the child node is not in a cluster (may not be needed now with the edge.hiddenByCluster check) + if (this.clusteredNodes[childNodeId] === undefined) { + if (childNodeId !== parentNodeId) { + if (options.joinCondition === undefined) { childEdgesObj[edge.id] = edge; childNodesObj[childNodeId] = this.body.nodes[childNodeId]; } + else { + // clone the options and insert some additional parameters that could be interesting. + let childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]); + if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } + } + } + else { + // swallow the edge if it is self-referencing. + childEdgesObj[edge.id] = edge; } - } - else { - childEdgesObj[edge.id] = edge; } } } + this._cluster(childNodesObj, childEdgesObj, options, refreshData); } @@ -256,18 +272,21 @@ class ClusterEngine { /** - * This function creates the edges that will be attached to the cluster. + * This function creates the edges that will be attached to the cluster + * It looks for edges that are connected to the nodes from the "outside' of the cluster. * * @param childNodesObj - * @param childEdgesObj * @param newEdges * @param options * @private */ - _createClusterEdges (childNodesObj, childEdgesObj, newEdges, clusterNodeProperties, clusterEdgeProperties) { + _createClusterEdges (childNodesObj, clusterNodeProperties, clusterEdgeProperties) { let edge, childNodeId, childNode, toId, fromId, otherNodeId; + // loop over all child nodes and their edges to find edges going out of the cluster + // these edges will be replaced by clusterEdges. let childKeys = Object.keys(childNodesObj); + let createEdges = []; for (let i = 0; i < childKeys.length; i++) { childNodeId = childKeys[i]; childNode = childNodesObj[childNodeId]; @@ -275,31 +294,56 @@ class ClusterEngine { // construct new edges from the cluster to others for (let j = 0; j < childNode.edges.length; j++) { edge = childNode.edges[j]; - childEdgesObj[edge.id] = edge; - - // childNodeId position will be replaced by the cluster. - if (edge.toId == childNodeId) { // this is a double equals because ints and strings can be interchanged here. - toId = clusterNodeProperties.id; - fromId = edge.fromId; - otherNodeId = fromId; - } - else { - toId = edge.toId; - fromId = clusterNodeProperties.id; - otherNodeId = toId; - } + // we only handle edges that are visible to the system, not the disabled ones from the clustering process. + if (edge.hiddenByCluster !== true) { + // set up the from and to. + if (edge.toId == childNodeId) { // this is a double equals because ints and strings can be interchanged here. + toId = clusterNodeProperties.id; + fromId = edge.fromId; + otherNodeId = fromId; + } + else { + toId = edge.toId; + fromId = clusterNodeProperties.id; + otherNodeId = toId; + } - // if the node connected to the cluster is also in the cluster we do not need a new edge. - if (childNodesObj[otherNodeId] === undefined) { - let clonedOptions = this._cloneOptions(edge, 'edge'); - util.deepExtend(clonedOptions, clusterEdgeProperties); - clonedOptions.from = fromId; - clonedOptions.to = toId; - clonedOptions.id = 'clusterEdge:' + util.randomUUID(); - newEdges.push(this.body.functions.createEdge(clonedOptions)); + // Only edges from the cluster outwards are being replaced. + if (childNodesObj[otherNodeId] === undefined) { + createEdges.push({edge: edge, fromId: fromId, toId: toId}); + } } } } + + // here we actually create the replacement edges. We could not do this in the loop above as the creation process + // would add an edge to the edges array we are iterating over. + for (let j = 0; j < createEdges.length; j++) { + let edge = createEdges[j].edge; + // copy the options of the edge we will replace + let clonedOptions = this._cloneOptions(edge, 'edge'); + // make sure the properties of clusterEdges are superimposed on it + util.deepExtend(clonedOptions, clusterEdgeProperties); + + // set up the edge + clonedOptions.from = createEdges[j].fromId; + clonedOptions.to = createEdges[j].toId; + clonedOptions.id = 'clusterEdge:' + util.randomUUID(); + //clonedOptions.id = '(cf: ' + createEdges[j].fromId + " to: " + createEdges[j].toId + ")" + Math.random(); + + // create the edge and give a reference to the one it replaced. + let newEdge = this.body.functions.createEdge(clonedOptions); + newEdge.clusteringEdgeReplacingId = edge.id; + + // connect the edge. + this.body.edges[newEdge.id] = newEdge; + newEdge.connect(); + + // hide the replaced edge + edge.setOptions({physics:false, hidden:true}); + edge.hiddenByCluster = true; + } + } /** @@ -325,9 +369,8 @@ class ClusterEngine { * @private */ _cluster(childNodesObj, childEdgesObj, options, refreshData = true) { - // kill condition: no children so can't cluster - if (Object.keys(childNodesObj).length === 0) {return;} - + // kill condition: no children so can't cluster or only one node in the cluster, dont bother + if (Object.keys(childNodesObj).length < 2) {return;} // check if this cluster call is not trying to cluster anything that is in another cluster. for (let nodeId in childNodesObj) { @@ -385,9 +428,7 @@ class ClusterEngine { clusterNodeProperties.x = pos.x; } if (clusterNodeProperties.y === undefined) { - if (pos === undefined) { - pos = this._getClusterPosition(childNodesObj); - } + if (pos === undefined) {pos = this._getClusterPosition(childNodesObj);} clusterNodeProperties.y = pos.y; } @@ -406,28 +447,15 @@ class ClusterEngine { this.body.nodes[clusterNodeProperties.id] = clusterNode; // create the new edges that will connect to the cluster - let newEdges = []; - this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, clusterNodeProperties, options.clusterEdgeProperties); + this._createClusterEdges(childNodesObj, clusterNodeProperties, options.clusterEdgeProperties); // disable the childEdges for (let edgeId in childEdgesObj) { if (childEdgesObj.hasOwnProperty(edgeId)) { if (this.body.edges[edgeId] !== undefined) { let edge = this.body.edges[edgeId]; - - // if this is a cluster edge that is fully encompassed in the cluster, we want to delete it - // this check verifies that both of the connected nodes are in this cluster - if (edgeId.substr(0,12) === "clusterEdge:" && childNodesObj[edge.fromId] !== undefined && childNodesObj[edge.toId] !== undefined) { - edge.cleanup(); - // this removes the edge from node.edges, which is why edgeIds is formed - edge.disconnect(); - delete childEdgesObj[edgeId]; - delete this.body.edges[edgeId]; - } - else { - edge.setOptions({physics:false, hidden:true}); - //edge.options.hidden = true; - } + edge.setOptions({physics:false, hidden:true}); + edge.hiddenByCluster = true; } } } @@ -440,12 +468,6 @@ class ClusterEngine { } } - // push new edges to global - for (let i = 0; i < newEdges.length; i++) { - this.body.edges[newEdges[i].id] = newEdges[i]; - this.body.edges[newEdges[i].id].connect(); - } - // set ID to undefined so no duplicates arise clusterNodeProperties.id = undefined; @@ -531,8 +553,8 @@ class ClusterEngine { if (containedNodes.hasOwnProperty(nodeId)) { let containedNode = this.body.nodes[nodeId]; if (newPositions[nodeId] !== undefined) { - containedNode.x = newPositions[nodeId].x || clusterNode.x; - containedNode.y = newPositions[nodeId].y || clusterNode.y; + containedNode.x = (newPositions[nodeId].x === undefined ? clusterNode.x : newPositions[nodeId].x); + containedNode.y = (newPositions[nodeId].y === undefined ? clusterNode.y : newPositions[nodeId].y); } } } @@ -560,78 +582,79 @@ class ClusterEngine { containedNode.vy = clusterNode.vy; // we use these methods to avoid reinstantiating the shape, which happens with setOptions. - //containedNode.toggleHidden(false); - //containedNode.togglePhysics(true); containedNode.setOptions({hidden:false, physics:true}); delete this.clusteredNodes[nodeId]; } } - // release edges - for (let edgeId in containedEdges) { - if (containedEdges.hasOwnProperty(edgeId)) { - let edge = containedEdges[edgeId]; - // if this edge was a temporary edge and it's connected nodes do not exist anymore, we remove it from the data - if (this.body.nodes[edge.fromId] === undefined || this.body.nodes[edge.toId] === undefined || edge.toId == clusterNodeId || edge.fromId == clusterNodeId) { - edge.cleanup(); - // this removes the edge from node.edges, which is why edgeIds is formed - edge.disconnect(); - delete this.body.edges[edgeId]; - } - else { - // one of the nodes connected to this edge is in a cluster. We give the edge to that cluster so it will be released when that cluster is opened. - if (this.clusteredNodes[edge.fromId] !== undefined || this.clusteredNodes[edge.toId] !== undefined) { - let fromId, toId; - let clusteredNode = this.clusteredNodes[edge.fromId] || this.clusteredNodes[edge.toId]; - let clusterId = clusteredNode.clusterId; - let clusterNode = this.body.nodes[clusterId]; - clusterNode.containedEdges[edgeId] = edge; - - if (this.clusteredNodes[edge.fromId] !== undefined) { - fromId = clusterId; - toId = edge.toId; - } - else { - fromId = edge.fromId; - toId = clusterId; - } - - // if both from and to nodes are visible, we create a new temporary edge - if (this.body.nodes[fromId].options.hidden !== true && this.body.nodes[toId].options.hidden !== true) { - let clonedOptions = this._cloneOptions(edge, 'edge'); - let id = 'clusterEdge:' + util.randomUUID(); - util.deepExtend(clonedOptions, clusterNode.clusterEdgeProperties); - util.deepExtend(clonedOptions, {from: fromId, to: toId, hidden: false, physics: true, id: id}); - let newEdge = this.body.functions.createEdge(clonedOptions); - - this.body.edges[id] = newEdge; - this.body.edges[id].connect(); - } + // copy the clusterNode edges because we cannot iterate over an object that we add or remove from. + let edgesToBeDeleted = []; + for (let i = 0; i < clusterNode.edges.length; i++) { + edgesToBeDeleted.push(clusterNode.edges[i]); + } + + // actually handling the deleting. + for (let i = 0; i < edgesToBeDeleted.length; i++) { + let edge = edgesToBeDeleted[i]; + + let otherNodeId = this._getConnectedId(edge, clusterNodeId); + // if the other node is in another cluster, we transfer ownership of this edge to the other cluster + if (this.clusteredNodes[otherNodeId] !== undefined) { + // transfer ownership: + let otherCluster = this.body.nodes[this.clusteredNodes[otherNodeId].clusterId]; + let transferEdge = this.body.edges[edge.clusteringEdgeReplacingId]; + if (transferEdge !== undefined) { + otherCluster.containedEdges[transferEdge.id] = transferEdge; + + // delete local reference + delete containedEdges[transferEdge.id]; + + // create new cluster edge from the otherCluster: + // get to and from + let fromId = transferEdge.fromId; + let toId = transferEdge.toId; + if (transferEdge.toId == otherNodeId) { + toId = this.clusteredNodes[otherNodeId].clusterId; } else { - edge.setOptions({physics:true, hidden:false}); - //edge.options.hidden = false; - //edge.togglePhysics(true); + fromId = this.clusteredNodes[otherNodeId].clusterId; } + + // clone the options and apply the cluster options to them + let clonedOptions = this._cloneOptions(transferEdge, 'edge'); + util.deepExtend(clonedOptions, otherCluster.clusterEdgeProperties); + + // apply the edge specific options to it. + let id = 'clusterEdge:' + util.randomUUID(); + util.deepExtend(clonedOptions, {from: fromId, to: toId, hidden: false, physics: true, id: id}); + + // create it + let newEdge = this.body.functions.createEdge(clonedOptions); + newEdge.clusteringEdgeReplacingId = transferEdge.id; + this.body.edges[id] = newEdge; + this.body.edges[id].connect(); } } + else { + let replacedEdge = this.body.edges[edge.clusteringEdgeReplacingId]; + if (replacedEdge !== undefined) { + replacedEdge.setOptions({physics: true, hidden: false}); + replacedEdge.hiddenByCluster = false; + } + } + edge.cleanup(); + // this removes the edge from node.edges, which is why edgeIds is formed + edge.disconnect(); + delete this.body.edges[edge.id]; } - // remove all temporary edges, make an array of ids so we don't remove from the list we're iterating over. - let removeIds = []; - for (let i = 0; i < clusterNode.edges.length; i++) { - let edgeId = clusterNode.edges[i].id; - removeIds.push(edgeId); - } - - // actually removing the edges - for (let i = 0; i < removeIds.length; i++) { - let edgeId = removeIds[i]; - this.body.edges[edgeId].cleanup(); - // this removes the edge from node.edges, which is why edgeIds is formed - this.body.edges[edgeId].disconnect(); - delete this.body.edges[edgeId]; + // handle the releasing of the edges + for (let edgeId in containedEdges) { + if (containedEdges.hasOwnProperty(edgeId)) { + let edge = containedEdges[edgeId]; + edge.setOptions({physics: true, hidden: false}); + } } // remove clusterNode diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index 9ef2aea3..e2de9308 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -170,6 +170,11 @@ class LayoutEngine { } } + + /** + * Use KamadaKawai to position nodes. This is quite a heavy algorithm so if there are a lot of nodes we + * cluster them first to reduce the amount. + */ layoutNetwork() { // first check if we should KamadaKawai to layout. The threshold is if less than half of the visible // nodes have predefined positions we use this. @@ -184,16 +189,14 @@ class LayoutEngine { // if less than half of the nodes have a predefined position we continue if (positionDefined < 0.5 * this.body.nodeIndices.length) { let levels = 0; + let clusterThreshold = 100; // if there are a lot of nodes, we cluster before we run the algorithm. - if (this.body.nodeIndices.length > 100) { + if (this.body.nodeIndices.length > clusterThreshold) { let startLength = this.body.nodeIndices.length; - while(this.body.nodeIndices.length > 150) { + while(this.body.nodeIndices.length > clusterThreshold) { levels += 1; // if there are many nodes we do a hubsize cluster - if (levels % 5 === 0) { - this.body.modules.clustering.clusterByHubsize(); - } - else if (levels % 3 === 0) { + if (levels % 3 === 0) { this.body.modules.clustering.clusterBridges(); } else { @@ -215,17 +218,7 @@ class LayoutEngine { for (let i = 0; i < this.body.nodeIndices.length; i++) { if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) { clustersPresent = true; - this.body.modules.clustering.openCluster(this.body.nodeIndices[i], { - releaseFunction: function (clusterPosition, containedNodesPositions) { - var newPositions = {}; - for (let nodeId in containedNodesPositions) { - if (containedNodesPositions.hasOwnProperty(nodeId)) { - newPositions[nodeId] = {x:clusterPosition.x, y:clusterPosition.y}; - } - } - return newPositions; - } - }, false); + this.body.modules.clustering.openCluster(this.body.nodeIndices[i], {}, false); } } if (clustersPresent === true) { @@ -233,7 +226,7 @@ class LayoutEngine { } } } - + // reposition all bezier nodes. this.body.emitter.emit("_repositionBezierNodes"); } diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index a22af54a..08d86bb5 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -190,7 +190,7 @@ class PhysicsEngine { else { this.stabilized = false; this.ready = true; - this.body.emitter.emit('fit', {}, true); + this.body.emitter.emit('fit', {}, false); this.startSimulation(); } } diff --git a/test/networkTest.html b/test/networkTest.html index 0454c4c9..441a5a08 100644 --- a/test/networkTest.html +++ b/test/networkTest.html @@ -24,33 +24,49 @@ From 64c1995d81e913ddb23b06ebfa45dc3090e69d83 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Wed, 19 Aug 2015 21:13:01 +0200 Subject: [PATCH 09/10] added public ready versions of kamadakawai and adaptive layout, clustering bugfixes and reactive network. --- HISTORY.md | 4 + dist/vis.js | 162 ++++++++++++++++++--------- docs/network/layout.html | 2 + docs/network/physics.html | 4 +- lib/network/modules/Canvas.js | 36 +++++- lib/network/modules/LayoutEngine.js | 110 +++++++++--------- lib/network/modules/PhysicsEngine.js | 5 +- lib/network/options.js | 6 +- 8 files changed, 221 insertions(+), 108 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index b5d76dad..5894cb3a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -19,6 +19,10 @@ http://visjs.org - Added Adaptive timestep to the physics solvers for increased performance during stabilization. - Fixed bugs in clustering algorithm. - Greatly improved performance in clustering. +- Fixed find node return types. +- Made the network keep its 'view' during a change of the size of the container. +- Added improvedLayout as experimental option for greatly improved stabilization times. +- Added adaptiveTimestep as experimental option for greatly improved stabilization times. ## 2015-07-27, version 4.7.0 diff --git a/dist/vis.js b/dist/vis.js index 759a85b5..99f03193 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 4.7.1-SNAPSHOT - * @date 2015-08-16 + * @date 2015-08-19 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -10642,7 +10642,7 @@ return /******/ (function(modules) { // webpackBootstrap if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax; if (options.backgroundColor !== undefined) this._setBackgroundColor(options.backgroundColor); - if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; + if (options.cameraState !== undefined) cameraPosition = options.cameraState; if (cameraPosition !== undefined) { this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); @@ -33079,7 +33079,8 @@ return /******/ (function(modules) { // webpackBootstrap onlyDynamicEdges: false, fit: true }, - timestep: 0.5 + timestep: 0.5, + adaptiveTimestep: true }; util.extend(this.options, this.defaultOptions); this.timestep = 0.5; @@ -33627,7 +33628,7 @@ return /******/ (function(modules) { // webpackBootstrap } // enable adaptive timesteps - this.adaptiveTimestep = true; + this.adaptiveTimestep = true && this.options.adaptiveTimestep; // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes"); @@ -36147,6 +36148,7 @@ return /******/ (function(modules) { // webpackBootstrap this.pixelRatio = 1; this.resizeTimer = undefined; this.resizeFunction = this._onResize.bind(this); + this.cameraState = {}; this.options = {}; this.defaultOptions = { @@ -36219,6 +36221,42 @@ return /******/ (function(modules) { // webpackBootstrap this.setSize(); this.body.emitter.emit("_redraw"); } + + /** + * Get and store the cameraState + * @private + */ + }, { + key: '_getCameraState', + value: function _getCameraState() { + this.cameraState.previousWidth = this.frame.canvas.width; + this.cameraState.scale = this.body.view.scale; + this.cameraState.position = this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.width, y: 0.5 * this.frame.canvas.height }); + } + + /** + * Set the cameraState + * @private + */ + }, { + key: '_setCameraState', + value: function _setCameraState() { + if (this.cameraState.scale !== undefined) { + this.body.view.scale = this.body.view.scale * (this.frame.canvas.clientWidth / this.cameraState.previousWidth); + + // this comes from the view module. + var viewCenter = this.DOMtoCanvas({ + x: 0.5 * this.frame.canvas.clientWidth, + y: 0.5 * this.frame.canvas.clientHeight + }); + var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node + x: viewCenter.x - this.cameraState.position.x, + y: viewCenter.y - this.cameraState.position.y + }; + this.body.view.translation.x += distanceFromCenter.x * this.body.view.scale; + this.body.view.translation.y += distanceFromCenter.y * this.body.view.scale; + } + } }, { key: '_prepareValue', value: function _prepareValue(value) { @@ -36360,6 +36398,7 @@ return /******/ (function(modules) { // webpackBootstrap var width = arguments.length <= 0 || arguments[0] === undefined ? this.options.width : arguments[0]; var height = arguments.length <= 1 || arguments[1] === undefined ? this.options.height : arguments[1]; + this._getCameraState(); width = this._prepareValue(width); height = this._prepareValue(height); @@ -36403,7 +36442,7 @@ return /******/ (function(modules) { // webpackBootstrap oldHeight: Math.round(oldHeight / this.pixelRatio) }); } - + this._setCameraState(); return emitEvent; } }, { @@ -38887,6 +38926,7 @@ return /******/ (function(modules) { // webpackBootstrap this.defaultOptions = { randomSeed: undefined, + improvedLayout: true, hierarchical: { enabled: false, levelSeparation: 150, @@ -38921,7 +38961,7 @@ return /******/ (function(modules) { // webpackBootstrap value: function setOptions(options, allOptions) { if (options !== undefined) { var prevHierarchicalState = this.options.hierarchical.enabled; - + util.selectiveDeepExtend(["randomSeed", "improvedLayout"], this.options, options); util.mergeOptions(this.options, options, 'hierarchical'); if (options.randomSeed !== undefined) { this.initialRandomSeed = options.randomSeed; @@ -39051,58 +39091,70 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: 'layoutNetwork', value: function layoutNetwork() { - // first check if we should KamadaKawai to layout. The threshold is if less than half of the visible - // nodes have predefined positions we use this. - var positionDefined = 0; - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var node = this.body.nodes[this.body.nodeIndices[i]]; - if (node.predefinedPosition === true) { - positionDefined += 1; - } - } - - // if less than half of the nodes have a predefined position we continue - if (positionDefined < 0.5 * this.body.nodeIndices.length) { - var levels = 0; - var clusterThreshold = 100; - // if there are a lot of nodes, we cluster before we run the algorithm. - if (this.body.nodeIndices.length > clusterThreshold) { - var startLength = this.body.nodeIndices.length; - while (this.body.nodeIndices.length > clusterThreshold) { - levels += 1; - // if there are many nodes we do a hubsize cluster - if (levels % 3 === 0) { - this.body.modules.clustering.clusterBridges(); - } else { - this.body.modules.clustering.clusterOutliers(); - } + if (this.options.hierarchical.enabled !== true && this.options.improvedLayout === true) { + // first check if we should KamadaKawai to layout. The threshold is if less than half of the visible + // nodes have predefined positions we use this. + var positionDefined = 0; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.predefinedPosition === true) { + positionDefined += 1; } - // increase the size of the edges - this.body.modules.kamadaKawai.setOptions({ springLength: Math.max(150, 2 * startLength) }); - } - - // position the system for these nodes and edges - this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true); - - // uncluster all clusters - if (levels > 0) { - var clustersPresent = true; - while (clustersPresent === true) { - clustersPresent = false; - for (var i = 0; i < this.body.nodeIndices.length; i++) { - if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) { - clustersPresent = true; - this.body.modules.clustering.openCluster(this.body.nodeIndices[i], {}, false); + } + + // if less than half of the nodes have a predefined position we continue + if (positionDefined < 0.5 * this.body.nodeIndices.length) { + var levels = 0; + var clusterThreshold = 100; + // if there are a lot of nodes, we cluster before we run the algorithm. + if (this.body.nodeIndices.length > clusterThreshold) { + var startLength = this.body.nodeIndices.length; + while (this.body.nodeIndices.length > clusterThreshold) { + levels += 1; + var before = this.body.nodeIndices.length; + // if there are many nodes we do a hubsize cluster + if (levels % 3 === 0) { + this.body.modules.clustering.clusterBridges(); + } else { + this.body.modules.clustering.clusterOutliers(); + } + var after = this.body.nodeIndices.length; + if (before == after && levels % 3 !== 0) { + this._declusterAll(); + console.info("This network could not be positioned by this version of the improved layout algorithm."); + return; } } - if (clustersPresent === true) { - this.body.emitter.emit('_dataChanged'); - } + // increase the size of the edges + this.body.modules.kamadaKawai.setOptions({ springLength: Math.max(150, 2 * startLength) }); } - } - // reposition all bezier nodes. - this.body.emitter.emit("_repositionBezierNodes"); + // position the system for these nodes and edges + this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true); + + // uncluster all clusters + this._declusterAll(); + + // reposition all bezier nodes. + this.body.emitter.emit("_repositionBezierNodes"); + } + } + } + }, { + key: '_declusterAll', + value: function _declusterAll() { + var clustersPresent = true; + while (clustersPresent === true) { + clustersPresent = false; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) { + clustersPresent = true; + this.body.modules.clustering.openCluster(this.body.nodeIndices[i], {}, false); + } + } + if (clustersPresent === true) { + this.body.emitter.emit('_dataChanged'); + } } } }, { @@ -40779,6 +40831,7 @@ return /******/ (function(modules) { // webpackBootstrap }, layout: { randomSeed: { 'undefined': 'undefined', number: number }, + improvedLayout: { boolean: boolean }, hierarchical: { enabled: { boolean: boolean }, levelSeparation: { number: number }, @@ -40932,6 +40985,7 @@ return /******/ (function(modules) { // webpackBootstrap __type__: { object: object, boolean: boolean } }, timestep: { number: number }, + adaptiveTimestep: { boolean: boolean }, __type__: { object: object, boolean: boolean } }, @@ -41071,6 +41125,7 @@ return /******/ (function(modules) { // webpackBootstrap }, layout: { //randomSeed: [0, 0, 500, 1], + //improvedLayout: true, hierarchical: { enabled: false, levelSeparation: [150, 20, 500, 5], @@ -41140,6 +41195,7 @@ return /******/ (function(modules) { // webpackBootstrap solver: ['barnesHut', 'forceAtlas2Based', 'repulsion', 'hierarchicalRepulsion'], timestep: [0.5, 0.01, 1, 0.01] }, + //adaptiveTimestep: true global: { locale: ['en', 'nl'] } diff --git a/docs/network/layout.html b/docs/network/layout.html index ea33c7dd..c43a838c 100644 --- a/docs/network/layout.html +++ b/docs/network/layout.html @@ -101,6 +101,7 @@ var options = { layout: { randomSeed: undefined, + improvedLayout:true, hierarchical: { enabled:false, levelSeparation: 150, @@ -127,6 +128,7 @@ network.setOptions(options); + diff --git a/docs/network/physics.html b/docs/network/physics.html index cb9acb07..c8fe9920 100644 --- a/docs/network/physics.html +++ b/docs/network/physics.html @@ -138,7 +138,8 @@ var options = { onlyDynamicEdges: false, fit: true }, - timestep: 0.5 + timestep: 0.5, + adaptiveTimestep: true } } @@ -201,6 +202,7 @@ network.setOptions(options); +
NameTypeDefaultDescription
randomSeedNumberundefined When NOT using the hierarchical layout, the nodes are randomly positioned initially. This means that the settled result is different every time. If you provide a random seed manually, the layout will be the same every time. Ideally you try with an undefined seed, reload until you are happy with the layout and use the getSeed() method to ascertain the seed.
improvedLayoutBooleantrue When enabled, the network will use the Kamada Kawai algorithm for initial layout. For networks larger than 100 nodes, clustering will be performed automatically to reduce the amount of nodes. This can greatly improve the stabilization times. If the network is very interconnected (no or few leaf nodes), this may not work and it will revert back to the old method. Performance will be improved in the future.
hierarchicalObject or BooleanObject When true, the layout engine positions the nodes in a hierarchical fashion using default settings. For customization you can supply an object.
timestep Number 0.5 The physics simulation is discrete. This means we take a step in time, calculate the forces, move the nodes and take another step. If you increase this number the steps will be too large and the network can get unstable. If you see a lot of jittery movement in the network, you may want to reduce this value a little.
adaptiveTimestep Boolean true If this is enabled, the timestep will intelligently be adapted (only during the stabilization stage if stabilization is enabled!) to greatly decrease stabilization times. The timestep configured above is taken as the minimum timestep. This can be further improved by using the improvedLayout algorithm.
diff --git a/lib/network/modules/Canvas.js b/lib/network/modules/Canvas.js index 5589712e..0b7fe972 100644 --- a/lib/network/modules/Canvas.js +++ b/lib/network/modules/Canvas.js @@ -16,6 +16,7 @@ class Canvas { this.pixelRatio = 1; this.resizeTimer = undefined; this.resizeFunction = this._onResize.bind(this); + this.cameraState = {}; this.options = {}; this.defaultOptions = { @@ -82,6 +83,38 @@ class Canvas { this.body.emitter.emit("_redraw"); } + /** + * Get and store the cameraState + * @private + */ + _getCameraState() { + this.cameraState.previousWidth = this.frame.canvas.width; + this.cameraState.scale = this.body.view.scale; + this.cameraState.position = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.width, y: 0.5 * this.frame.canvas.height}); + } + + /** + * Set the cameraState + * @private + */ + _setCameraState() { + if (this.cameraState.scale !== undefined) { + this.body.view.scale = this.body.view.scale * (this.frame.canvas.clientWidth / this.cameraState.previousWidth); + + // this comes from the view module. + var viewCenter = this.DOMtoCanvas({ + x: 0.5 * this.frame.canvas.clientWidth, + y: 0.5 * this.frame.canvas.clientHeight + }); + var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node + x: viewCenter.x - this.cameraState.position.x, + y: viewCenter.y - this.cameraState.position.y + }; + this.body.view.translation.x += distanceFromCenter.x * this.body.view.scale; + this.body.view.translation.y += distanceFromCenter.y * this.body.view.scale; + } + } + _prepareValue(value) { if (typeof value === 'number') { return value + 'px'; @@ -194,6 +227,7 @@ class Canvas { * or '30%') */ setSize(width = this.options.width, height = this.options.height) { + this._getCameraState(); width = this._prepareValue(width); height= this._prepareValue(height); @@ -238,7 +272,7 @@ class Canvas { oldHeight: Math.round(oldHeight / this.pixelRatio) }); } - + this._setCameraState(); return emitEvent; }; diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index e2de9308..16c90538 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -14,6 +14,7 @@ class LayoutEngine { this.defaultOptions = { randomSeed: undefined, + improvedLayout: true, hierarchical: { enabled:false, levelSeparation: 150, @@ -43,11 +44,9 @@ class LayoutEngine { setOptions(options, allOptions) { if (options !== undefined) { let prevHierarchicalState = this.options.hierarchical.enabled; - + util.selectiveDeepExtend(["randomSeed", "improvedLayout"],this.options, options); util.mergeOptions(this.options, options, 'hierarchical'); - if (options.randomSeed !== undefined) { - this.initialRandomSeed = options.randomSeed; - } + if (options.randomSeed !== undefined) {this.initialRandomSeed = options.randomSeed;} if (this.options.hierarchical.enabled === true) { if (prevHierarchicalState === true) { @@ -176,59 +175,70 @@ class LayoutEngine { * cluster them first to reduce the amount. */ layoutNetwork() { - // first check if we should KamadaKawai to layout. The threshold is if less than half of the visible - // nodes have predefined positions we use this. - let positionDefined = 0; - for (let i = 0; i < this.body.nodeIndices.length; i++) { - let node = this.body.nodes[this.body.nodeIndices[i]]; - if (node.predefinedPosition === true) { - positionDefined += 1; + if (this.options.hierarchical.enabled !== true && this.options.improvedLayout === true) { + // first check if we should KamadaKawai to layout. The threshold is if less than half of the visible + // nodes have predefined positions we use this. + let positionDefined = 0; + for (let i = 0; i < this.body.nodeIndices.length; i++) { + let node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.predefinedPosition === true) { + positionDefined += 1; + } } - } - // if less than half of the nodes have a predefined position we continue - if (positionDefined < 0.5 * this.body.nodeIndices.length) { - let levels = 0; - let clusterThreshold = 100; - // if there are a lot of nodes, we cluster before we run the algorithm. - if (this.body.nodeIndices.length > clusterThreshold) { - let startLength = this.body.nodeIndices.length; - while(this.body.nodeIndices.length > clusterThreshold) { - levels += 1; - // if there are many nodes we do a hubsize cluster - if (levels % 3 === 0) { - this.body.modules.clustering.clusterBridges(); - } - else { - this.body.modules.clustering.clusterOutliers(); - } - } - // increase the size of the edges - this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150,2*startLength)}) - } - - // position the system for these nodes and edges - this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true); - - // uncluster all clusters - if (levels > 0) { - let clustersPresent = true; - while (clustersPresent === true) { - clustersPresent = false; - for (let i = 0; i < this.body.nodeIndices.length; i++) { - if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) { - clustersPresent = true; - this.body.modules.clustering.openCluster(this.body.nodeIndices[i], {}, false); + // if less than half of the nodes have a predefined position we continue + if (positionDefined < 0.5 * this.body.nodeIndices.length) { + let levels = 0; + let clusterThreshold = 100; + // if there are a lot of nodes, we cluster before we run the algorithm. + if (this.body.nodeIndices.length > clusterThreshold) { + let startLength = this.body.nodeIndices.length; + while (this.body.nodeIndices.length > clusterThreshold) { + levels += 1; + let before = this.body.nodeIndices.length; + // if there are many nodes we do a hubsize cluster + if (levels % 3 === 0) { + this.body.modules.clustering.clusterBridges(); + } + else { + this.body.modules.clustering.clusterOutliers(); + } + let after = this.body.nodeIndices.length; + if (before == after && levels % 3 !== 0) { + this._declusterAll(); + console.info("This network could not be positioned by this version of the improved layout algorithm."); + return; } } - if (clustersPresent === true) { - this.body.emitter.emit('_dataChanged'); - } + // increase the size of the edges + this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150, 2 * startLength)}) } + + // position the system for these nodes and edges + this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true); + + // uncluster all clusters + this._declusterAll(); + + // reposition all bezier nodes. + this.body.emitter.emit("_repositionBezierNodes"); } + } + } - // reposition all bezier nodes. - this.body.emitter.emit("_repositionBezierNodes"); + _declusterAll() { + let clustersPresent = true; + while (clustersPresent === true) { + clustersPresent = false; + for (let i = 0; i < this.body.nodeIndices.length; i++) { + if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) { + clustersPresent = true; + this.body.modules.clustering.openCluster(this.body.nodeIndices[i], {}, false); + } + } + if (clustersPresent === true) { + this.body.emitter.emit('_dataChanged'); + } } } diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index 08d86bb5..8b3e9184 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -81,7 +81,8 @@ class PhysicsEngine { onlyDynamicEdges: false, fit: true }, - timestep: 0.5 + timestep: 0.5, + adaptiveTimestep: true }; util.extend(this.options, this.defaultOptions); this.timestep = 0.5; @@ -597,7 +598,7 @@ class PhysicsEngine { } // enable adaptive timesteps - this.adaptiveTimestep = true; + this.adaptiveTimestep = true && this.options.adaptiveTimestep; // this sets the width of all nodes initially which could be required for the avoidOverlap this.body.emitter.emit("_resizeNodes"); diff --git a/lib/network/options.js b/lib/network/options.js index 7f4de30d..ffab67fc 100644 --- a/lib/network/options.js +++ b/lib/network/options.js @@ -117,6 +117,7 @@ let allOptions = { }, layout: { randomSeed: { 'undefined': 'undefined', number }, + improvedLayout: { boolean }, hierarchical: { enabled: { boolean }, levelSeparation: { number }, @@ -270,6 +271,7 @@ let allOptions = { __type__: { object, boolean } }, timestep: { number }, + adaptiveTimestep: { boolean }, __type__: { object, boolean } }, @@ -410,6 +412,7 @@ let configureOptions = { }, layout: { //randomSeed: [0, 0, 500, 1], + //improvedLayout: true, hierarchical: { enabled: false, levelSeparation: [150, 20, 500, 5], @@ -477,7 +480,8 @@ let configureOptions = { maxVelocity: [50, 0, 150, 1], minVelocity: [0.1, 0.01, 0.5, 0.01], solver: ['barnesHut', 'forceAtlas2Based', 'repulsion', 'hierarchicalRepulsion'], - timestep: [0.5, 0.01, 1, 0.01] + timestep: [0.5, 0.01, 1, 0.01], + //adaptiveTimestep: true }, global: { locale: ['en', 'nl'] From 0bcaee2598eb407dc6c0e985aebb32227363c104 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Wed, 19 Aug 2015 21:13:17 +0200 Subject: [PATCH 10/10] rebuilt --- dist/vis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/vis.js b/dist/vis.js index 99f03193..3c7e48f2 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -10642,7 +10642,7 @@ return /******/ (function(modules) { // webpackBootstrap if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax; if (options.backgroundColor !== undefined) this._setBackgroundColor(options.backgroundColor); - if (options.cameraState !== undefined) cameraPosition = options.cameraState; + if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; if (cameraPosition !== undefined) { this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);