From 09d3dfbb09ca9cd3b617c94bba93f9d9bde4e4bf Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 13 Aug 2015 19:41:41 +0200 Subject: [PATCH] - 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;