Browse Source

- Improved the hierarchical layout algorithm by adding a condensing method to remove whitespace.

codeClimate
Alex de Mulder 8 years ago
parent
commit
2977dfd565
27 changed files with 1064 additions and 137 deletions
  1. +1
    -0
      HISTORY.md
  2. BIN
      dist/img/network/acceptDeleteIcon.png
  3. BIN
      dist/img/network/addNodeIcon.png
  4. BIN
      dist/img/network/backIcon.png
  5. BIN
      dist/img/network/connectIcon.png
  6. BIN
      dist/img/network/cross.png
  7. BIN
      dist/img/network/cross2.png
  8. BIN
      dist/img/network/deleteIcon.png
  9. BIN
      dist/img/network/downArrow.png
  10. BIN
      dist/img/network/editIcon.png
  11. BIN
      dist/img/network/leftArrow.png
  12. BIN
      dist/img/network/minus.png
  13. BIN
      dist/img/network/plus.png
  14. BIN
      dist/img/network/rightArrow.png
  15. BIN
      dist/img/network/upArrow.png
  16. BIN
      dist/img/network/zoomExtends.png
  17. BIN
      dist/img/timeline/delete.png
  18. +559
    -90
      dist/vis.js
  19. +3
    -3
      docs/network/edges.html
  20. +54
    -6
      examples/network/layout/hierarchicalLayout.html
  21. +11
    -2
      examples/network/layout/hierarchicalLayoutMethods.html
  22. +3
    -3
      gulpfile.js
  23. +1
    -1
      lib/network/modules/EdgesHandler.js
  24. +366
    -23
      lib/network/modules/LayoutEngine.js
  25. +2
    -2
      lib/network/modules/components/Edge.js
  26. +1
    -1
      lib/network/options.js
  27. +63
    -6
      test/networkTest.html

+ 1
- 0
HISTORY.md View File

@ -7,6 +7,7 @@ http://visjs.org
### Network
- Altered edges for arrows and added the arrowStrikethrough option.
- Improved the hierarchical layout algorithm by adding a condensing method to remove whitespace.
## 2016-01-08, version 4.12.0

BIN
dist/img/network/acceptDeleteIcon.png View File

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

BIN
dist/img/network/addNodeIcon.png View File

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

BIN
dist/img/network/backIcon.png View File

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

BIN
dist/img/network/connectIcon.png View File

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

BIN
dist/img/network/cross.png View File

Before After
Width: 7  |  Height: 7  |  Size: 18 KiB

BIN
dist/img/network/cross2.png View File

Before After
Width: 5  |  Height: 5  |  Size: 17 KiB

BIN
dist/img/network/deleteIcon.png View File

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

BIN
dist/img/network/downArrow.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

BIN
dist/img/network/editIcon.png View File

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

BIN
dist/img/network/leftArrow.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

BIN
dist/img/network/minus.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.0 KiB

BIN
dist/img/network/plus.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.2 KiB

BIN
dist/img/network/rightArrow.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

BIN
dist/img/network/upArrow.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

BIN
dist/img/network/zoomExtends.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

BIN
dist/img/timeline/delete.png View File

Before After
Width: 16  |  Height: 16  |  Size: 665 B

+ 559
- 90
dist/vis.js View File

@ -5,7 +5,7 @@
* A dynamic, browser-based visualization library.
*
* @version 4.12.1-SNAPSHOT
* @date 2016-01-11
* @date 2016-01-12
*
* @license
* Copyright (C) 2011-2016 Almende B.V, http://almende.com
@ -1577,7 +1577,7 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(module) {//! moment.js
//! version : 2.11.0
//! version : 2.11.1
//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
//! license : MIT
//! momentjs.com
@ -1848,7 +1848,7 @@ return /******/ (function(modules) { // webpackBootstrap
function loadLocale(name) {
var oldLocale = null;
// TODO: Find a better way to register and load all the locales in Node
if (!locales[name] && !isUndefined(module) &&
if (!locales[name] && (typeof module !== 'undefined') &&
module && module.exports) {
try {
oldLocale = globalLocale._abbr;
@ -2116,13 +2116,13 @@ return /******/ (function(modules) { // webpackBootstrap
// any word (or two) characters or numbers including two/three word month in arabic.
// includes scottish gaelic two word and hyphenated months
var matchWord = /[0-9]*(a[mn]\s?)?['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF\-]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i;
var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i;
var regexes = {};
function addRegexToken (token, regex, strictRegex) {
regexes[token] = isFunction(regex) ? regex : function (isStrict) {
regexes[token] = isFunction(regex) ? regex : function (isStrict, localeData) {
return (isStrict && strictRegex) ? strictRegex : regex;
};
}
@ -2137,9 +2137,13 @@ return /******/ (function(modules) { // webpackBootstrap
// Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
function unescapeFormat(s) {
return s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
return regexEscape(s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
return p1 || p2 || p3 || p4;
}).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}));
}
function regexEscape(s) {
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
var tokens = {};
@ -2208,8 +2212,12 @@ return /******/ (function(modules) { // webpackBootstrap
addRegexToken('M', match1to2);
addRegexToken('MM', match1to2, match2);
addRegexToken('MMM', matchWord);
addRegexToken('MMMM', matchWord);
addRegexToken('MMM', function (isStrict, locale) {
return locale.monthsShortRegex(isStrict);
});
addRegexToken('MMMM', function (isStrict, locale) {
return locale.monthsRegex(isStrict);
});
addParseToken(['M', 'MM'], function (input, array) {
array[MONTH] = toInt(input) - 1;
@ -2234,7 +2242,7 @@ return /******/ (function(modules) { // webpackBootstrap
this._months[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()];
}
var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sept_Oct_Nov_Dec'.split('_');
var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_');
function localeMonthsShort (m, format) {
return isArray(this._monthsShort) ? this._monthsShort[m.month()] :
this._monthsShort[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()];
@ -2309,6 +2317,72 @@ return /******/ (function(modules) { // webpackBootstrap
return daysInMonth(this.year(), this.month());
}
var defaultMonthsShortRegex = matchWord;
function monthsShortRegex (isStrict) {
if (this._monthsParseExact) {
if (!hasOwnProp(this, '_monthsRegex')) {
computeMonthsParse.call(this);
}
if (isStrict) {
return this._monthsShortStrictRegex;
} else {
return this._monthsShortRegex;
}
} else {
return this._monthsShortStrictRegex && isStrict ?
this._monthsShortStrictRegex : this._monthsShortRegex;
}
}
var defaultMonthsRegex = matchWord;
function monthsRegex (isStrict) {
if (this._monthsParseExact) {
if (!hasOwnProp(this, '_monthsRegex')) {
computeMonthsParse.call(this);
}
if (isStrict) {
return this._monthsStrictRegex;
} else {
return this._monthsRegex;
}
} else {
return this._monthsStrictRegex && isStrict ?
this._monthsStrictRegex : this._monthsRegex;
}
}
function computeMonthsParse () {
function cmpLenRev(a, b) {
return b.length - a.length;
}
var shortPieces = [], longPieces = [], mixedPieces = [],
i, mom;
for (i = 0; i < 12; i++) {
// make the regex if we don't have it already
mom = create_utc__createUTC([2000, i]);
shortPieces.push(this.monthsShort(mom, ''));
longPieces.push(this.months(mom, ''));
mixedPieces.push(this.months(mom, ''));
mixedPieces.push(this.monthsShort(mom, ''));
}
// Sorting makes sure if one month (or abbr) is a prefix of another it
// will match the longer piece.
shortPieces.sort(cmpLenRev);
longPieces.sort(cmpLenRev);
mixedPieces.sort(cmpLenRev);
for (i = 0; i < 12; i++) {
shortPieces[i] = regexEscape(shortPieces[i]);
longPieces[i] = regexEscape(longPieces[i]);
mixedPieces[i] = regexEscape(mixedPieces[i]);
}
this._monthsRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i');
this._monthsShortRegex = this._monthsRegex;
this._monthsStrictRegex = new RegExp('^(' + longPieces.join('|') + ')$', 'i');
this._monthsShortStrictRegex = new RegExp('^(' + shortPieces.join('|') + ')$', 'i');
}
function checkOverflow (m) {
var overflow;
var a = m._a;
@ -2340,7 +2414,8 @@ return /******/ (function(modules) { // webpackBootstrap
}
function warn(msg) {
if (utils_hooks__hooks.suppressDeprecationWarnings === false && !isUndefined(console) && console.warn) {
if (utils_hooks__hooks.suppressDeprecationWarnings === false &&
(typeof console !== 'undefined') && console.warn) {
console.warn('Deprecation warning: ' + msg);
}
}
@ -2508,6 +2583,11 @@ return /******/ (function(modules) { // webpackBootstrap
// FORMATTING
addFormatToken('Y', 0, 0, function () {
var y = this.year();
return y <= 9999 ? '' + y : '+' + y;
});
addFormatToken(0, ['YY', 2], 0, function () {
return this.year() % 100;
});
@ -2535,6 +2615,9 @@ return /******/ (function(modules) { // webpackBootstrap
addParseToken('YY', function (input, array) {
array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input);
});
addParseToken('Y', function (input, array) {
array[YEAR] = parseInt(input, 10);
});
// HELPERS
@ -2786,6 +2869,8 @@ return /******/ (function(modules) { // webpackBootstrap
for (i = 0; i < tokens.length; i++) {
token = tokens[i];
parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
// console.log('token', token, 'parsedInput', parsedInput,
// 'regex', getParseRegexForToken(token, config));
if (parsedInput) {
skipped = string.substr(0, string.indexOf(parsedInput));
if (skipped.length > 0) {
@ -3061,8 +3146,8 @@ return /******/ (function(modules) { // webpackBootstrap
return pickBy('isAfter', args);
}
var now = Date.now || function () {
return +(new Date());
var now = function () {
return Date.now ? Date.now() : +(new Date());
};
function Duration (duration) {
@ -4620,11 +4705,15 @@ return /******/ (function(modules) { // webpackBootstrap
prototype__proto.set = locale_set__set;
// Month
prototype__proto.months = localeMonths;
prototype__proto._months = defaultLocaleMonths;
prototype__proto.monthsShort = localeMonthsShort;
prototype__proto._monthsShort = defaultLocaleMonthsShort;
prototype__proto.monthsParse = localeMonthsParse;
prototype__proto.months = localeMonths;
prototype__proto._months = defaultLocaleMonths;
prototype__proto.monthsShort = localeMonthsShort;
prototype__proto._monthsShort = defaultLocaleMonthsShort;
prototype__proto.monthsParse = localeMonthsParse;
prototype__proto._monthsRegex = defaultMonthsRegex;
prototype__proto.monthsRegex = monthsRegex;
prototype__proto._monthsShortRegex = defaultMonthsShortRegex;
prototype__proto.monthsShortRegex = monthsShortRegex;
// Week
prototype__proto.week = localeWeek;
@ -4693,9 +4782,6 @@ return /******/ (function(modules) { // webpackBootstrap
}
locale_locales__getSetGlobalLocale('en', {
monthsParse : [/^jan/i, /^feb/i, /^mar/i, /^apr/i, /^may/i, /^jun/i, /^jul/i, /^aug/i, /^sep/i, /^oct/i, /^nov/i, /^dec/i],
longMonthsParse : [/^january$/i, /^february$/i, /^march$/i, /^april$/i, /^may$/i, /^june$/i, /^july$/i, /^august$/i, /^september$/i, /^october$/i, /^november$/i, /^december$/i],
shortMonthsParse : [/^jan$/i, /^feb$/i, /^mar$/i, /^apr$/i, /^may$/i, /^jun$/i, /^jul$/i, /^aug/i, /^sept?$/i, /^oct$/i, /^nov$/i, /^dec$/i],
ordinalParse: /\d{1,2}(th|st|nd|rd)/,
ordinal : function (number) {
var b = number % 10,
@ -5063,7 +5149,7 @@ return /******/ (function(modules) { // webpackBootstrap
// Side effect imports
utils_hooks__hooks.version = '2.11.0';
utils_hooks__hooks.version = '2.11.1';
setHookCallback(local__createLocal);
@ -11047,14 +11133,14 @@ return /******/ (function(modules) { // webpackBootstrap
var PropagatingHammer = function(element, options) {
var o = Object.create(_options);
if (options) Hammer.extend(o, options);
if (options) Hammer.assign(o, options);
return propagating(new Hammer(element, o), o);
};
Hammer.extend(PropagatingHammer, Hammer);
Hammer.assign(PropagatingHammer, Hammer);
PropagatingHammer.Manager = function (element, options) {
var o = Object.create(_options);
if (options) Hammer.extend(o, options);
if (options) Hammer.assign(o, options);
return propagating(new Hammer.Manager(element, o), o);
};
@ -11067,7 +11153,9 @@ return /******/ (function(modules) { // webpackBootstrap
// attach to DOM element
var element = hammer.element;
element.hammer = wrapper;
if(!element.hammer) element.hammer = [];
element.hammer.push(wrapper);
// register an event to catch the start of a gesture and store the
// target in a singleton
@ -11148,7 +11236,10 @@ return /******/ (function(modules) { // webpackBootstrap
wrapper.destroy = function () {
// Detach from DOM element
delete hammer.element.hammer;
var hammers = hammer.element.hammer;
var idx = hammers.indexOf(wrapper);
if(idx !== -1) hammers.splice(idx,1);
if(!hammers.length) delete hammer.element.hammer;
// clear all handlers
wrapper._handlers = {};
@ -11189,19 +11280,30 @@ return /******/ (function(modules) { // webpackBootstrap
stopped = true;
};
//wrap the srcEvent's stopPropagation to also stop hammer propagation:
var srcStop = event.srcEvent.stopPropagation;
if(typeof srcStop == "function") {
event.srcEvent.stopPropagation = function(){
srcStop();
event.stopPropagation();
}
}
// attach firstTarget property to the event
event.firstTarget = _firstTarget;
// propagate over all elements (until stopped)
var elem = _firstTarget;
while (elem && !stopped) {
var _handlers = elem.hammer && elem.hammer._handlers[event.type];
if (_handlers) {
for (var i = 0; i < _handlers.length && !stopped; i++) {
_handlers[i](event);
if(elem.hammer){
var _handlers;
for(var k = 0; k < elem.hammer.length; k++){
_handlers = elem.hammer[k]._handlers[event.type];
if(_handlers) for (var i = 0; i < _handlers.length && !stopped; i++) {
_handlers[i](event);
}
}
}
elem = elem.parentNode;
}
}
@ -31341,7 +31443,7 @@ return /******/ (function(modules) { // webpackBootstrap
middle: { enabled: false, scaleFactor: 1 },
from: { enabled: false, scaleFactor: 1 }
},
arrowStrikethrough: false,
arrowStrikethrough: true,
color: {
color: '#848484',
highlight: '#848484',
@ -32059,11 +32161,11 @@ return /******/ (function(modules) { // webpackBootstrap
// from and to arrows give a different end point for edges. we set them here
if (this.options.arrows.from.enabled === true) {
arrowData.from = this.edgeType.getArrowData(ctx, 'from', viaNode, this.selected, this.hover);
if (this.options.arrowStrikethrough === true) this.edgeType.fromPoint = arrowData.from.core;
if (this.options.arrowStrikethrough === false) this.edgeType.fromPoint = arrowData.from.core;
}
if (this.options.arrows.to.enabled === true) {
arrowData.to = this.edgeType.getArrowData(ctx, 'to', viaNode, this.selected, this.hover);
if (this.options.arrowStrikethrough === true) this.edgeType.toPoint = arrowData.to.core;
if (this.options.arrowStrikethrough === false) this.edgeType.toPoint = arrowData.to.core;
}
// the middle arrow depends on the line, which can depend on the to and from arrows so we do this one lastly.
@ -39969,6 +40071,8 @@ return /******/ (function(modules) { // webpackBootstrap
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 }; }
@ -40003,12 +40107,6 @@ return /******/ (function(modules) { // webpackBootstrap
}
};
util.extend(this.options, this.defaultOptions);
this.lastNodeOnLevel = {};
this.hierarchicalParents = {};
this.hierarchicalChildren = {};
this.distributionOrdering = {};
this.distributionOrderingPresence = {};
this.bindEventListeners();
}
@ -40280,7 +40378,19 @@ return /******/ (function(modules) { // webpackBootstrap
var definedLevel = false;
var undefinedLevel = false;
this.hierarchicalLevels = {};
this.lastNodeOnLevel = {};
this.hierarchicalParents = {};
this.hierarchicalChildren = {};
this.hierarchicalTrees = {};
this.treeIndex = -1;
this.whiteSpaceReductionFactor = 0.5;
this.nodeSpacing = 100;
this.treeSpacing = 2 * this.nodeSpacing;
this.distributionOrdering = {};
this.distributionIndex = {};
this.distributionOrderingPresence = {};
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
@ -40319,11 +40429,12 @@ return /******/ (function(modules) { // webpackBootstrap
// place the nodes on the canvas.
this._placeNodesByHierarchy(distribution);
// Todo: condense the whitespace.
// condense the whitespace.
console.time("bla");
this._condenseHierarchy(distribution);
console.timeEnd("bla");
// shift to center so gravity does not have to do much
this._shiftToCenter();
//this._shiftToCenter();
}
}
}
@ -40334,16 +40445,349 @@ return /******/ (function(modules) { // webpackBootstrap
}, {
key: '_condenseHierarchy',
value: function _condenseHierarchy(distribution) {
//console.log(this.distributionOrdering);
var _this2 = this;
//let iterations = 10;
//for (let i = 0; i < iterations; i++) {
var shiftTrees = function shiftTrees() {
var treeSizes = getTreeSizes();
for (var i = 0; i < treeSizes.length - 1; i++) {
var diff = treeSizes[i].max - treeSizes[i + 1].min;
if (diff !== _this2.treeSpacing) {
shiftTree(i + 1, diff - _this2.treeSpacing);
}
}
};
var shiftTree = function shiftTree(index, offset) {
for (var nodeId in _this2.hierarchicalTrees) {
if (_this2.hierarchicalTrees.hasOwnProperty(nodeId)) {
if (_this2.hierarchicalTrees[nodeId] === index) {
_this2._setPositionForHierarchy(_this2.body.nodes[nodeId], offset, undefined, true);
}
}
}
};
var getTreeSize = function getTreeSize(index) {
var min = 1e9;
var max = -1e9;
for (var nodeId in _this2.hierarchicalTrees) {
if (_this2.hierarchicalTrees.hasOwnProperty(nodeId)) {
if (_this2.hierarchicalTrees[nodeId] === index) {
var pos = _this2._getPositionForHierarchy(_this2.body.nodes[nodeId]);
min = Math.min(pos, min);
max = Math.max(pos, max);
}
}
}
return [min, max];
};
var getTreeSizes = function getTreeSizes() {
var treeWidths = [];
for (var i = 0; i < _this2.treeIndex; i++) {
treeWidths.push(getTreeSize(i));
}
return treeWidths;
};
var nodeInBranch = function nodeInBranch(source, target) {
if (source.id == target.id) {
return true;
}
var match = false;
if (_this2.hierarchicalParents[source.id]) {
var children = _this2.hierarchicalParents[source.id].children;
if (children.length > 0) {
for (var i = 0; i < children.length; i++) {
match = nodeInBranch(_this2.body.nodes[children[i]], target);
if (match == true) return match;
}
}
}
return match;
};
var getBranchNodes = function getBranchNodes(source, map) {
map[source.id] = true;
if (_this2.hierarchicalParents[source.id]) {
var children = _this2.hierarchicalParents[source.id].children;
if (children.length > 0) {
for (var i = 0; i < children.length; i++) {
getBranchNodes(_this2.body.nodes[children[i]], map);
}
}
}
};
var getBranchBoundary = function getBranchBoundary(branchMap) {
var maxLevel = arguments.length <= 1 || arguments[1] === undefined ? 1e9 : arguments[1];
var minSpace = 1e9;
var maxSpace = -1e9;
var min = 1e9;
var max = -1e9;
for (var branchNode in branchMap) {
if (branchMap.hasOwnProperty(branchNode)) {
var node = _this2.body.nodes[branchNode];
var level = _this2.hierarchicalLevels[node.id];
var index = _this2.distributionIndex[node.id];
var position = _this2._getPositionForHierarchy(_this2.body.nodes[node.id]);
if (index != 0) {
var prevNode = _this2.distributionOrdering[level][index - 1];
if (branchMap[prevNode.id] === undefined) {
var prevPos = _this2._getPositionForHierarchy(prevNode);
minSpace = Math.min(minSpace, position - prevPos);
}
}
if (index != _this2.distributionOrdering[level].length - 1) {
var nextNode = _this2.distributionOrdering[level][index + 1];
if (branchMap[nextNode.id] === undefined) {
var nextPos = _this2._getPositionForHierarchy(nextNode);
maxSpace = Math.max(maxSpace, nextPos - position);
}
}
if (level <= maxLevel) {
min = Math.min(position, min);
max = Math.max(position, max);
}
}
}
//}
return [min, max, minSpace, maxSpace];
};
var getMaxLevel = function getMaxLevel(nodeId) {
var level = _this2.hierarchicalLevels[nodeId];
if (_this2.hierarchicalParents[nodeId]) {
var children = _this2.hierarchicalParents[nodeId].children;
if (children.length > 0) {
for (var i = 0; i < children.length; i++) {
level = Math.max(level, getMaxLevel(children[i]));
}
}
}
return level;
};
var getCollisionLevel = function getCollisionLevel(node1, node2) {
var maxLevel1 = getMaxLevel(node1.id);
var maxLevel2 = getMaxLevel(node2.id);
return Math.min(maxLevel1, maxLevel2);
};
var hasSameParent = function hasSameParent(node1, node2) {
var parents1 = _this2.hierarchicalChildren[node1.id];
var parents2 = _this2.hierarchicalChildren[node2.id];
if (parents1 === undefined || parents2 === undefined) {
return false;
}
parents1 = parents1.parents;
parents2 = parents2.parents;
for (var i = 0; i < parents1.length; i++) {
for (var j = 0; j < parents2.length; j++) {
if (parents1[i] == parents2[j]) {
return true;
}
}
}
return false;
};
var shiftElementsCloser = function shiftElementsCloser(callback, levels, centerParents) {
for (var i = 0; i < levels.length; i++) {
var level = levels[i];
var levelNodes = _this2.distributionOrdering[level];
if (levelNodes.length > 1) {
for (var _i = 0; _i < levelNodes.length - 1; _i++) {
if (hasSameParent(levelNodes[_i], levelNodes[_i + 1]) === true) {
if (_this2.hierarchicalTrees[levelNodes[_i].id] === _this2.hierarchicalTrees[levelNodes[_i + 1].id]) {
callback(levelNodes[_i], levelNodes[_i + 1], centerParents);
}
}
}
}
}
};
var stillShifting = false;
var blockShiftCallback = function blockShiftCallback(node1, node2) {
var centerParent = arguments.length <= 2 || arguments[2] === undefined ? false : arguments[2];
//window.CALLBACKS.push(() => {
var pos1 = _this2._getPositionForHierarchy(node1);
var pos2 = _this2._getPositionForHierarchy(node2);
var diffAbs = Math.abs(pos2 - pos1);
//console.log("NOW CHEcKING:", node1.id, node2.id, diffAbs);
if (diffAbs > _this2.nodeSpacing) {
var branchNodes1 = {};branchNodes1[node1.id] = true;
var branchNodes2 = {};branchNodes2[node2.id] = true;
getBranchNodes(node1, branchNodes1);
getBranchNodes(node2, branchNodes2);
// check the largest distance between the branches
var maxLevel = getCollisionLevel(node1, node2);
var _getBranchBoundary = getBranchBoundary(branchNodes1, maxLevel);
var _getBranchBoundary2 = _slicedToArray(_getBranchBoundary, 4);
var min1 = _getBranchBoundary2[0];
var max1 = _getBranchBoundary2[1];
var minSpace1 = _getBranchBoundary2[2];
var maxSpace1 = _getBranchBoundary2[3];
var _getBranchBoundary3 = getBranchBoundary(branchNodes2, maxLevel);
var _getBranchBoundary32 = _slicedToArray(_getBranchBoundary3, 4);
var min2 = _getBranchBoundary32[0];
var max2 = _getBranchBoundary32[1];
var minSpace2 = _getBranchBoundary32[2];
var maxSpace2 = _getBranchBoundary32[3];
//console.log(node1.id, getBranchBoundary(branchNodes1, maxLevel), node2.id, getBranchBoundary(branchNodes2, maxLevel), maxLevel);
var diffBranch = Math.abs(max1 - min2);
if (diffBranch > _this2.nodeSpacing) {
var offset = max1 - min2 + _this2.nodeSpacing;
if (offset < -minSpace2 + _this2.nodeSpacing) {
offset = -minSpace2 + _this2.nodeSpacing;
//console.log("RESETTING OFFSET", max1 - min2 + this.nodeSpacing, -minSpace2, offset);
}
if (offset < 0) {
//console.log("SHIFTING", node2.id, offset);
_this2._shiftBlock(node2.id, offset);
stillShifting = true;
if (centerParent === true) _this2._centerParent(node2);
}
}
}
//this.body.emitter.emit("_redraw");})
};
var unitShiftCallback = function unitShiftCallback(node1, node2, centerParent) {
var pos1 = _this2._getPositionForHierarchy(node1);
var pos2 = _this2._getPositionForHierarchy(node2);
var diffAbs = Math.abs(pos2 - pos1);
//console.log("NOW CHEcKING:", node1.id, node2.id, diffAbs);
if (diffAbs > _this2.nodeSpacing) {
var diff = (pos1 + _this2.nodeSpacing - pos2) * _this2.whiteSpaceReductionFactor;
if (diff != 0) {
stillShifting = true;
}
var factor = node2.edges.length / (node1.edges.length + node2.edges.length);
_this2._setPositionForHierarchy(node2, pos2 + factor * diff, undefined, true);
_this2._setPositionForHierarchy(node1, pos1 - (1 - factor) * diff, undefined, true);
if (centerParent === true) {
_this2._centerParent(node2);
}
}
};
var shiftUnitsCloser = function shiftUnitsCloser(iterations) {
var levels = Object.keys(_this2.distributionOrdering);
for (var i = 0; i < iterations; i++) {
stillShifting = false;
shiftElementsCloser(unitShiftCallback, levels, false);
if (stillShifting !== true) {
console.log("FINISHED shiftUnitsCloser IN " + i);
break;
}
}
console.log("FINISHED shiftUnitsCloser IN " + iterations);
};
var shiftBranchesCloserBottomUp = function shiftBranchesCloserBottomUp(iterations) {
var levels = Object.keys(_this2.distributionOrdering);
levels = levels.reverse();
for (var i = 0; i < iterations; i++) {
stillShifting = false;
shiftElementsCloser(blockShiftCallback, levels, true);
if (stillShifting !== true) {
console.log("FINISHED shiftBranchesCloserBottomUp IN " + i);
break;
}
}
};
var shiftBranchesCloserTopDown = function shiftBranchesCloserTopDown(iterations) {
var levels = Object.keys(_this2.distributionOrdering);
for (var i = 0; i < iterations; i++) {
stillShifting = false;
shiftElementsCloser(blockShiftCallback, levels, true);
if (stillShifting !== true) {
console.log("FINISHED shiftBranchesCloserBottomUp IN " + i);
break;
}
}
};
var centerAllParents = function centerAllParents() {
for (var node in _this2.body.nodes) {
_this2._centerParent(_this2.body.nodes[node]);
}
};
shiftBranchesCloserBottomUp(5);
centerAllParents();
shiftUnitsCloser(2);
//centerAllParents();
shiftTrees();
}
}, {
key: '_centerParent',
value: function _centerParent(node) {
//console.log("CENTERING DADDY:", node.id)
if (this.hierarchicalChildren[node.id]) {
var parents = this.hierarchicalChildren[node.id].parents;
//console.log(parents)
for (var i = 0; i < parents.length; i++) {
var parentId = parents[i];
var parentNode = this.body.nodes[parentId];
if (this.hierarchicalParents[parentId]) {
var minPos = 1e9;
var maxPos = -1e9;
var children = this.hierarchicalParents[parentId].children;
//console.log(children)
if (children.length > 0) {
for (var _i2 = 0; _i2 < children.length; _i2++) {
var childNode = this.body.nodes[children[_i2]];
minPos = Math.min(minPos, this._getPositionForHierarchy(childNode));
maxPos = Math.max(maxPos, this._getPositionForHierarchy(childNode));
}
}
var level = this.hierarchicalLevels[parentId];
var index = this.distributionIndex[parentId];
var position = this._getPositionForHierarchy(parentNode);
var minSpace = 1e9;
var maxSpace = 1e9;
if (index != 0) {
var prevNode = this.distributionOrdering[level][index - 1];
var prevPos = this._getPositionForHierarchy(prevNode);
minSpace = position - prevPos;
}
if (index != this.distributionOrdering[level].length - 1) {
var nextNode = this.distributionOrdering[level][index + 1];
var nextPos = this._getPositionForHierarchy(nextNode);
maxSpace = Math.min(maxSpace, nextPos - position);
}
var newPosition = 0.5 * (minPos + maxPos);
if (newPosition < position + maxSpace && newPosition > position - minSpace) {
this._setPositionForHierarchy(parentNode, newPosition, undefined, true);
} else {
//console.log("CANNOT CENTER:", parentId, minSpace, maxSpace, newPosition);
}
}
}
}
}
}, {
key: '_removeWhiteSpace',
value: function _removeWhiteSpace(distribution) {}
/**
* This function places the nodes on the canvas based on the hierarchial distribution.
@ -40455,18 +40899,18 @@ return /******/ (function(modules) { // webpackBootstrap
}, {
key: '_determineLevelsByHubsize',
value: function _determineLevelsByHubsize() {
var _this2 = this;
var _this3 = this;
var hubSize = 1;
var levelDownstream = function levelDownstream(nodeA, nodeB) {
if (_this2.hierarchicalLevels[nodeB.id] === undefined) {
if (_this3.hierarchicalLevels[nodeB.id] === undefined) {
// set initial level
if (_this2.hierarchicalLevels[nodeA.id] === undefined) {
_this2.hierarchicalLevels[nodeA.id] = 0;
if (_this3.hierarchicalLevels[nodeA.id] === undefined) {
_this3.hierarchicalLevels[nodeA.id] = 0;
}
// set level
_this2.hierarchicalLevels[nodeB.id] = _this2.hierarchicalLevels[nodeA.id] + 1;
_this3.hierarchicalLevels[nodeB.id] = _this3.hierarchicalLevels[nodeA.id] + 1;
}
};
@ -40493,7 +40937,7 @@ return /******/ (function(modules) { // webpackBootstrap
}, {
key: '_determineLevelsCustomCallback',
value: function _determineLevelsCustomCallback() {
var _this3 = this;
var _this4 = this;
var minLevel = 100000;
@ -40501,15 +40945,15 @@ return /******/ (function(modules) { // webpackBootstrap
var customCallback = function customCallback(nodeA, nodeB, edge) {};
var levelByDirection = function levelByDirection(nodeA, nodeB, edge) {
var levelA = _this3.hierarchicalLevels[nodeA.id];
var levelA = _this4.hierarchicalLevels[nodeA.id];
// set initial level
if (levelA === undefined) {
_this3.hierarchicalLevels[nodeA.id] = minLevel;
_this4.hierarchicalLevels[nodeA.id] = minLevel;
}
var diff = customCallback(_NetworkUtil2['default'].cloneOptions(nodeA, 'node'), _NetworkUtil2['default'].cloneOptions(nodeB, 'node'), _NetworkUtil2['default'].cloneOptions(edge, 'edge'));
_this3.hierarchicalLevels[nodeB.id] = _this3.hierarchicalLevels[nodeA.id] + diff;
_this4.hierarchicalLevels[nodeB.id] = _this4.hierarchicalLevels[nodeA.id] + diff;
};
this._crawlNetwork(levelByDirection);
@ -40525,19 +40969,19 @@ return /******/ (function(modules) { // webpackBootstrap
}, {
key: '_determineLevelsDirected',
value: function _determineLevelsDirected() {
var _this4 = this;
var _this5 = this;
var minLevel = 10000;
var levelByDirection = function levelByDirection(nodeA, nodeB, edge) {
var levelA = _this4.hierarchicalLevels[nodeA.id];
var levelA = _this5.hierarchicalLevels[nodeA.id];
// set initial level
if (levelA === undefined) {
_this4.hierarchicalLevels[nodeA.id] = minLevel;
_this5.hierarchicalLevels[nodeA.id] = minLevel;
}
if (edge.toId == nodeB.id) {
_this4.hierarchicalLevels[nodeB.id] = _this4.hierarchicalLevels[nodeA.id] + 1;
_this5.hierarchicalLevels[nodeB.id] = _this5.hierarchicalLevels[nodeA.id] + 1;
} else {
_this4.hierarchicalLevels[nodeB.id] = _this4.hierarchicalLevels[nodeA.id] - 1;
_this5.hierarchicalLevels[nodeB.id] = _this5.hierarchicalLevels[nodeA.id] - 1;
}
};
this._crawlNetwork(levelByDirection);
@ -40576,20 +41020,20 @@ return /******/ (function(modules) { // webpackBootstrap
}, {
key: '_generateMap',
value: function _generateMap() {
var _this5 = this;
var _this6 = this;
var fillInRelations = function fillInRelations(parentNode, childNode) {
if (_this5.hierarchicalLevels[childNode.id] > _this5.hierarchicalLevels[parentNode.id]) {
if (_this6.hierarchicalLevels[childNode.id] > _this6.hierarchicalLevels[parentNode.id]) {
var parentNodeId = parentNode.id;
var childNodeId = childNode.id;
if (_this5.hierarchicalParents[parentNodeId] === undefined) {
_this5.hierarchicalParents[parentNodeId] = { children: [], amount: 0 };
if (_this6.hierarchicalParents[parentNodeId] === undefined) {
_this6.hierarchicalParents[parentNodeId] = { children: [], amount: 0 };
}
_this5.hierarchicalParents[parentNodeId].children.push(childNodeId);
if (_this5.hierarchicalChildren[childNodeId] === undefined) {
_this5.hierarchicalChildren[childNodeId] = { parents: [], amount: 0 };
_this6.hierarchicalParents[parentNodeId].children.push(childNodeId);
if (_this6.hierarchicalChildren[childNodeId] === undefined) {
_this6.hierarchicalChildren[childNodeId] = { parents: [], amount: 0 };
}
_this5.hierarchicalChildren[childNodeId].parents.push(parentNodeId);
_this6.hierarchicalChildren[childNodeId].parents.push(parentNodeId);
}
};
@ -40597,7 +41041,7 @@ return /******/ (function(modules) { // webpackBootstrap
}
/**
* Crawl over the entire network and use a callback on each node couple that is connected to eachother.
* Crawl over the entire network and use a callback on each node couple that is connected to each other.
* @param callback | will receive nodeA nodeB and the connecting edge. A and B are unique.
* @param startingNodeId
* @private
@ -40748,22 +41192,22 @@ return /******/ (function(modules) { // webpackBootstrap
}, {
key: '_findCommonParent',
value: function _findCommonParent(childA, childB) {
var _this6 = this;
var _this7 = this;
var parents = {};
var iterateParents = function iterateParents(parents, child) {
if (_this6.hierarchicalChildren[child] !== undefined) {
for (var i = 0; i < _this6.hierarchicalChildren[child].parents.length; i++) {
var _parent = _this6.hierarchicalChildren[child].parents[i];
if (_this7.hierarchicalChildren[child] !== undefined) {
for (var i = 0; i < _this7.hierarchicalChildren[child].parents.length; i++) {
var _parent = _this7.hierarchicalChildren[child].parents[i];
parents[_parent] = true;
iterateParents(parents, _parent);
}
}
};
var findParent = function findParent(parents, child) {
if (_this6.hierarchicalChildren[child] !== undefined) {
for (var i = 0; i < _this6.hierarchicalChildren[child].parents.length; i++) {
var _parent2 = _this6.hierarchicalChildren[child].parents[i];
if (_this7.hierarchicalChildren[child] !== undefined) {
for (var i = 0; i < _this7.hierarchicalChildren[child].parents.length; i++) {
var _parent2 = _this7.hierarchicalChildren[child].parents[i];
if (parents[_parent2] !== undefined) {
return { foundParent: _parent2, withChild: child };
}
@ -40790,16 +41234,41 @@ return /******/ (function(modules) { // webpackBootstrap
}, {
key: '_setPositionForHierarchy',
value: function _setPositionForHierarchy(node, position, level) {
if (this.distributionOrdering[level] === undefined) {
this.distributionOrdering[level] = [];
this.distributionOrderingPresence[level] = {};
}
var doNotUpdate = arguments.length <= 3 || arguments[3] === undefined ? false : arguments[3];
if (this.distributionOrderingPresence[level][node.id] === undefined) {
this.distributionOrdering[level].push(node);
}
this.distributionOrderingPresence[level][node.id] = true;
if (doNotUpdate !== true) {
if (this.distributionOrdering[level] === undefined) {
this.distributionOrdering[level] = [];
this.distributionOrderingPresence[level] = {};
}
if (this.distributionOrderingPresence[level][node.id] === undefined) {
this.distributionOrdering[level].push(node);
this.distributionIndex[node.id] = this.distributionOrdering[level].length - 1;
}
this.distributionOrderingPresence[level][node.id] = true;
if (this.hierarchicalTrees[node.id] === undefined) {
if (this.hierarchicalChildren[node.id] !== undefined) {
var tree = 1;
// get the lowest tree denominator.
for (var i = 0; i < this.hierarchicalChildren[node.id].parents.length; i++) {
var parentId = this.hierarchicalChildren[node.id].parents[i];
if (this.hierarchicalTrees[parentId] !== undefined) {
//tree = Math.min(tree,this.hierarchicalTrees[parentId]);
tree = this.hierarchicalTrees[parentId];
}
}
//for (let i = 0; i < this.hierarchicalChildren.parents.length; i++) {
// let parentId = this.hierarchicalChildren.parents[i];
// this.hierarchicalTrees[parentId] = tree;
//}
this.hierarchicalTrees[node.id] = tree;
} else {
this.hierarchicalTrees[node.id] = ++this.treeIndex;
}
}
}
if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
node.x = position;
} else {
@ -42440,7 +42909,7 @@ return /******/ (function(modules) { // webpackBootstrap
middle: { enabled: false, scaleFactor: [1, 0, 3, 0.05] },
from: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }
},
arrowStrikethrough: false,
arrowStrikethrough: true,
color: {
color: ['color', '#848484'],
highlight: ['color', '#848484'],

+ 3
- 3
docs/network/edges.html View File

@ -119,7 +119,7 @@ var options = {
middle: {enabled: false, scaleFactor:1},
from: {enabled: false, scaleFactor:1}
},
arrowsEnding: false,
arrowStrikethrough: true,
color: {
color:'#848484',
highlight:'#848484',
@ -258,8 +258,8 @@ network.setOptions(options);
<tr>
<td class="indent">arrowStrikethrough</td>
<td>Boolean</td>
<td><code>false</code></td>
<td>When true, the edge stops at the arrow. This can be useful if you have thick lines and you want the arrow to end in a point. Middle arrows are not affected by this.</td>
<td><code>true</code></td>
<td>When false, the edge stops at the arrow. This can be useful if you have thick lines and you want the arrow to end in a point. Middle arrows are not affected by this.</td>
</tr>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','color', this);">
<td><span parent="color" class="right-caret"></span> color</td>

+ 54
- 6
examples/network/layout/hierarchicalLayout.html View File

@ -9,8 +9,8 @@
}
#mynetwork {
width: 600px;
height: 600px;
width: 1700px;
height: 800px;
border: 1px solid lightgray;
}
</style>
@ -34,28 +34,76 @@
function draw() {
destroy();
nodes = [];
edges = [];
// randomly create some nodes and edges
var nodeCount = document.getElementById('nodeCount').value;
var data = getScaleFreeNetwork(nodeCount)
for (var i = 0; i < 19; i++) {
nodes.push({id: i, label: String(i)});
}
edges.push({from: 0, to: 1});
edges.push({from: 0, to: 6});
edges.push({from: 0, to: 13});
edges.push({from: 0, to: 11});
edges.push({from: 1, to: 2});
edges.push({from: 2, to: 3});
edges.push({from: 2, to: 4});
edges.push({from: 3, to: 5});
edges.push({from: 1, to: 10});
edges.push({from: 1, to: 7});
edges.push({from: 2, to: 8});
edges.push({from: 2, to: 9});
edges.push({from: 3, to: 14});
edges.push({from: 1, to: 12});
edges.push({from: 16, to: 15});
edges.push({from: 15, to: 17});
edges.push({from: 18, to: 17});
// create a network
var container = document.getElementById('mynetwork');
var data = {
nodes: nodes,
edges: edges
};
var directionInput = document.getElementById("direction").value;
var options = {
layout: {
hierarchical: {
sortMethod: 'hubsize',
direction: directionInput
}
}
},
physics:false
};
network = new vis.Network(container, data, options);
fireall()
// add event listeners
network.on('select', function (params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
});
}
function fireall() {
if (callbackIndex < CALLBACKS.length -1) {
CALLBACKS[callbackIndex]();
callbackIndex++;
fireall()
}
}
function fireCallback() {
if (callbackIndex < CALLBACKS.length -1) {
CALLBACKS[callbackIndex]();
callbackIndex++;
}
else {
alert("no more callbacks");
}
}
var callbackIndex = 0;
var CALLBACKS = [];
</script>
<script src="../../googleAnalytics.js"></script>
</head>

+ 11
- 2
examples/network/layout/hierarchicalLayoutMethods.html View File

@ -34,7 +34,7 @@
var nodes = [];
var edges = [];
// randomly create some nodes and edges
for (var i = 0; i < 19; i++) {
for (var i = 0; i < 20; i++) {
nodes.push({id: i, label: String(i)});
}
edges.push({from: 0, to: 1});
@ -51,6 +51,13 @@
edges.push({from: 2, to: 9});
edges.push({from: 3, to: 14});
edges.push({from: 1, to: 12});
edges.push({from: 0, to: 19});
edges.push({from: 19, to: 2});
edges.push({from: 19, to: 10});
edges.push({from: 19, to: 7});
edges.push({from: 19, to: 12});
edges.push({from: 16, to: 15});
edges.push({from: 15, to: 17});
edges.push({from: 18, to: 17});
@ -71,7 +78,9 @@
edges: {
smooth: true,
arrows: {to : true }
}
},
// physics:false
};
network = new vis.Network(container, data, options);
}

+ 3
- 3
gulpfile.js View File

@ -132,13 +132,13 @@ gulp.task('bundle-css', ['clean'], function () {
});
gulp.task('copy', ['clean'], function () {
var network = gulp.src('./lib/network/img/**/*')
var network = gulp.src('./lib/network/img/**/*')
.pipe(gulp.dest(DIST + '/img/network'));
var timeline = gulp.src('./lib/timeline/img/**/*')
var timeline = gulp.src('./lib/timeline/img/**/*')
.pipe(gulp.dest(DIST + '/img/timeline'));
return merge(network, timeline);
return merge(network, timeline);
});
gulp.task('minify', ['bundle-js'], function (cb) {

+ 1
- 1
lib/network/modules/EdgesHandler.js View File

@ -27,7 +27,7 @@ class EdgesHandler {
middle: {enabled: false, scaleFactor:1},
from: {enabled: false, scaleFactor:1}
},
arrowStrikethrough: false,
arrowStrikethrough: true,
color: {
color:'#848484',
highlight:'#848484',

+ 366
- 23
lib/network/modules/LayoutEngine.js View File

@ -23,12 +23,6 @@ class LayoutEngine {
}
};
util.extend(this.options, this.defaultOptions);
this.lastNodeOnLevel = {};
this.hierarchicalParents = {};
this.hierarchicalChildren = {};
this.distributionOrdering = {};
this.distributionOrderingPresence = {};
this.bindEventListeners();
}
@ -290,7 +284,20 @@ class LayoutEngine {
let definedLevel = false;
let undefinedLevel = false;
this.hierarchicalLevels = {};
this.lastNodeOnLevel = {};
this.hierarchicalParents = {};
this.hierarchicalChildren = {};
this.hierarchicalTrees = {};
this.treeIndex = -1;
this.whiteSpaceReductionFactor = 0.5;
this.nodeSpacing = 100;
this.treeSpacing = 2 * this.nodeSpacing;
this.distributionOrdering = {};
this.distributionIndex = {};
this.distributionOrderingPresence = {};
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
@ -334,7 +341,7 @@ class LayoutEngine {
// place the nodes on the canvas.
this._placeNodesByHierarchy(distribution);
// Todo: condense the whitespace.
// condense the whitespace.
this._condenseHierarchy(distribution);
// shift to center so gravity does not have to do much
@ -347,20 +354,332 @@ class LayoutEngine {
* @private
*/
_condenseHierarchy(distribution) {
//console.log(this.distributionOrdering);
// first we have some methods to help shifting trees around.
// the main method to shift the trees
let shiftTrees = () => {
let treeSizes = getTreeSizes();
for (let i = 0; i < treeSizes.length - 1; i++) {
let diff = treeSizes[i].max - treeSizes[i+1].min;
if (diff !== this.treeSpacing) {
shiftTree(i + 1, diff - this.treeSpacing);
}
}
};
// shift a single tree by an offset
let shiftTree = (index, offset) => {
for (let nodeId in this.hierarchicalTrees) {
if (this.hierarchicalTrees.hasOwnProperty(nodeId)) {
if (this.hierarchicalTrees[nodeId] === index) {
this._setPositionForHierarchy(this.body.nodes[nodeId], offset, undefined, true);
}
}
}
};
// get the width of a tree
let getTreeSize = (index) => {
let min = 1e9;
let max = -1e9;
for (let nodeId in this.hierarchicalTrees) {
if (this.hierarchicalTrees.hasOwnProperty(nodeId)) {
if (this.hierarchicalTrees[nodeId] === index) {
let pos = this._getPositionForHierarchy(this.body.nodes[nodeId]);
min = Math.min(pos, min);
max = Math.max(pos, max);
}
}
}
return [min, max];
};
// get the width of all trees
let getTreeSizes = () => {
let treeWidths = [];
for (let i = 0; i < this.treeIndex; i++) {
treeWidths.push(getTreeSize(i));
}
return treeWidths;
};
// get a map of all nodes in this branch
let getBranchNodes = (source, map) => {
map[source.id] = true;
if (this.hierarchicalParents[source.id]) {
let children = this.hierarchicalParents[source.id].children;
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
getBranchNodes(this.body.nodes[children[i]], map);
}
}
}
};
// get a min max width as well as the maximum movement space it has on either sides
// we use min max terminology because width and height can interchange depending on the direction of the layout
let getBranchBoundary = (branchMap, maxLevel = 1e9) => {
let minSpace = 1e9;
let maxSpace = -1e9;
let min = 1e9;
let max = -1e9;
for (let branchNode in branchMap) {
if (branchMap.hasOwnProperty(branchNode)) {
let node = this.body.nodes[branchNode];
let level = this.hierarchicalLevels[node.id];
let index = this.distributionIndex[node.id];
let position = this._getPositionForHierarchy(this.body.nodes[node.id]);
// if this is the node at the side, there is no previous node
if (index != 0) {
let prevNode = this.distributionOrdering[level][index - 1];
if (branchMap[prevNode.id] === undefined) {
let prevPos = this._getPositionForHierarchy(prevNode);
minSpace = Math.min(minSpace, position - prevPos);
}
}
// if this is the node at the end there is no next node
if (index != this.distributionOrdering[level].length - 1) {
let nextNode = this.distributionOrdering[level][index + 1];
if (branchMap[nextNode.id] === undefined) {
let nextPos = this._getPositionForHierarchy(nextNode);
maxSpace = Math.max(maxSpace, nextPos - position);
}
}
// the width is only relevant for the levels two nodes have in common. This is why we filter on this.
if (level <= maxLevel) {
min = Math.min(position, min);
max = Math.max(position, max);
}
}
}
// if there was no next node, the max space is infinite (1e9 ~ close enough)
maxSpace = maxSpace < 0 ? 1e9 : maxSpace;
return [min, max, minSpace, maxSpace];
};
// get the maximum level of a branch.
let getMaxLevel = (nodeId) => {
let level = this.hierarchicalLevels[nodeId];
if (this.hierarchicalParents[nodeId]) {
let children = this.hierarchicalParents[nodeId].children;
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
level = Math.max(level,getMaxLevel(children[i]));
}
}
}
return level;
};
// check what the maximum level is these nodes have in common.
let getCollisionLevel = (node1, node2) => {
let maxLevel1 = getMaxLevel(node1.id);
let maxLevel2 = getMaxLevel(node2.id);
return Math.min(maxLevel1, maxLevel2);
};
// check if two nodes have the same parent(s)
let hasSameParent = (node1, node2) => {
let parents1 = this.hierarchicalChildren[node1.id];
let parents2 = this.hierarchicalChildren[node2.id];
if (parents1 === undefined || parents2 === undefined) {
return false;
}
parents1 = parents1.parents;
parents2 = parents2.parents;
for (let i = 0; i < parents1.length; i++) {
for (let j = 0; j < parents2.length; j++) {
if (parents1[i] == parents2[j]) {
return true;
}
}
}
return false;
};
// condense elements. These can be nodes or branches depending on the callback.
let shiftElementsCloser = (callback, levels, centerParents) => {
for (let i = 0; i < levels.length; i++) {
let level = levels[i];
let levelNodes = this.distributionOrdering[level];
if (levelNodes.length > 1) {
for (let i = 0; i < levelNodes.length - 1; i++) {
if (hasSameParent(levelNodes[i],levelNodes[i+1]) === true) {
if (this.hierarchicalTrees[levelNodes[i].id] === this.hierarchicalTrees[levelNodes[i+1].id]) {
callback(levelNodes[i],levelNodes[i+1], centerParents);
}
}}
}
}
};
// Global var in this scope to define when the movement has stopped.
let stillShifting = false;
// callback for shifting branches
let branchShiftCallback = (node1, node2, centerParent = false) => {
//window.CALLBACKS.push(() => {
let pos1 = this._getPositionForHierarchy(node1);
let pos2 = this._getPositionForHierarchy(node2);
let diffAbs = Math.abs(pos2 - pos1);
//console.log("NOW CHEcKING:", node1.id, node2.id, diffAbs);
if (diffAbs > this.nodeSpacing) {
let branchNodes1 = {}; branchNodes1[node1.id] = true;
let branchNodes2 = {}; branchNodes2[node2.id] = true;
getBranchNodes(node1, branchNodes1);
getBranchNodes(node2, branchNodes2);
// check the largest distance between the branches
let maxLevel = getCollisionLevel(node1, node2);
let [min1,max1, minSpace1, maxSpace1] = getBranchBoundary(branchNodes1, maxLevel);
let [min2,max2, minSpace2, maxSpace2] = getBranchBoundary(branchNodes2, maxLevel);
//console.log(node1.id, getBranchBoundary(branchNodes1, maxLevel), node2.id, getBranchBoundary(branchNodes2, maxLevel), maxLevel);
let diffBranch = Math.abs(max1 - min2);
if (diffBranch > this.nodeSpacing) {
let offset = max1 - min2 + this.nodeSpacing;
if (offset < -minSpace2 + this.nodeSpacing) {
offset = -minSpace2 + this.nodeSpacing;
//console.log("RESETTING OFFSET", max1 - min2 + this.nodeSpacing, -minSpace2, offset);
}
if (offset < 0) {
//console.log("SHIFTING", node2.id, offset);
this._shiftBlock(node2.id, offset);
stillShifting = true;
if (centerParent === true)
this._centerParent(node2);
}
}
//let iterations = 10;
//for (let i = 0; i < iterations; i++) {
}
//this.body.emitter.emit("_redraw");})
};
//}
// callback for shifting individual nodes
let unitShiftCallback = (node1, node2, centerParent) => {
let pos1 = this._getPositionForHierarchy(node1);
let pos2 = this._getPositionForHierarchy(node2);
let diffAbs = Math.abs(pos2 - pos1);
//console.log("NOW CHEcKING:", node1.id, node2.id, diffAbs);
if (diffAbs > this.nodeSpacing) {
let diff = (pos1 + this.nodeSpacing - pos2) * this.whiteSpaceReductionFactor;
if (diff != 0) {
stillShifting = true;
}
let factor = node2.edges.length / (node1.edges.length + node2.edges.length);
this._setPositionForHierarchy(node2, pos2 + factor * diff, undefined, true);
this._setPositionForHierarchy(node1, pos1 - (1-factor) * diff, undefined, true);
if (centerParent === true) {
this._centerParent(node2);
}
}
};
// method to shift all nodes closer together iteratively
let shiftUnitsCloser = (iterations) => {
let levels = Object.keys(this.distributionOrdering);
for (let i = 0; i < iterations; i++) {
stillShifting = false;
shiftElementsCloser(unitShiftCallback, levels, false);
if (stillShifting !== true) {
//console.log("FINISHED shiftUnitsCloser IN " + i);
break;
}
}
//console.log("FINISHED shiftUnitsCloser IN " + iterations);
};
// method to remove whitespace between branches. Because we do bottom up, we can center the parents.
let shiftBranchesCloserBottomUp = (iterations) => {
let levels = Object.keys(this.distributionOrdering);
levels = levels.reverse();
for (let i = 0; i < iterations; i++) {
stillShifting = false;
shiftElementsCloser(branchShiftCallback, levels, true);
if (stillShifting !== true) {
//console.log("FINISHED shiftBranchesCloserBottomUp IN " + i);
break;
}
}
};
// center all parents
let centerAllParents = () => {
for (let node in this.body.nodes) {
this._centerParent(this.body.nodes[node]);
}
};
// the actual work is done here.
shiftBranchesCloserBottomUp(5);
centerAllParents();
shiftUnitsCloser(2);
shiftTrees();
}
_removeWhiteSpace(distribution) {
/**
* We use this method to center a parent node and check if it does not cross other nodes when it does.
* @param node
* @private
*/
_centerParent(node) {
if (this.hierarchicalChildren[node.id]) {
let parents = this.hierarchicalChildren[node.id].parents;
for (var i = 0; i < parents.length; i++) {
let parentId = parents[i];
let parentNode = this.body.nodes[parentId];
if (this.hierarchicalParents[parentId]) {
// get the range of the children
let minPos = 1e9;
let maxPos = -1e9;
let children = this.hierarchicalParents[parentId].children;
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
let childNode = this.body.nodes[children[i]];
minPos = Math.min(minPos, this._getPositionForHierarchy(childNode));
maxPos = Math.max(maxPos, this._getPositionForHierarchy(childNode));
}
}
let level = this.hierarchicalLevels[parentId];
let index = this.distributionIndex[parentId];
let position = this._getPositionForHierarchy(parentNode);
let minSpace = 1e9;
let maxSpace = 1e9;
if (index != 0) {
let prevNode = this.distributionOrdering[level][index - 1];
let prevPos = this._getPositionForHierarchy(prevNode);
minSpace = position - prevPos;
}
if (index != this.distributionOrdering[level].length - 1) {
let nextNode = this.distributionOrdering[level][index + 1];
let nextPos = this._getPositionForHierarchy(nextNode);
maxSpace = Math.min(maxSpace, nextPos - position);
}
let newPosition = 0.5 * (minPos + maxPos);
if (newPosition < position + maxSpace && newPosition > position - minSpace) {
this._setPositionForHierarchy(parentNode, newPosition, undefined, true);
}
}
}
}
}
/**
* This function places the nodes on the canvas based on the hierarchial distribution.
*
@ -596,7 +915,7 @@ class LayoutEngine {
/**
* Crawl over the entire network and use a callback on each node couple that is connected to eachother.
* Crawl over the entire network and use a callback on each node couple that is connected to each other.
* @param callback | will receive nodeA nodeB and the connecting edge. A and B are unique.
* @param startingNodeId
* @private
@ -693,7 +1012,7 @@ class LayoutEngine {
this._placeBranchNodes(childNode.id, childNodeLevel);
}
else {
return
return;
}
}
@ -775,17 +1094,41 @@ class LayoutEngine {
* @param level
* @private
*/
_setPositionForHierarchy(node, position, level) {
if (this.distributionOrdering[level] === undefined) {
this.distributionOrdering[level] = [];
this.distributionOrderingPresence[level] = {};
}
_setPositionForHierarchy(node, position, level, doNotUpdate = false) {
if (doNotUpdate !== true) {
if (this.distributionOrdering[level] === undefined) {
this.distributionOrdering[level] = [];
this.distributionOrderingPresence[level] = {};
}
if (this.distributionOrderingPresence[level][node.id] === undefined) {
this.distributionOrdering[level].push(node);
if (this.distributionOrderingPresence[level][node.id] === undefined) {
this.distributionOrdering[level].push(node);
this.distributionIndex[node.id] = this.distributionOrdering[level].length - 1;
}
this.distributionOrderingPresence[level][node.id] = true;
if (this.hierarchicalTrees[node.id] === undefined) {
if (this.hierarchicalChildren[node.id] !== undefined) {
let tree = 1;
// get the lowest tree denominator.
for (let i = 0; i < this.hierarchicalChildren[node.id].parents.length; i++) {
let parentId = this.hierarchicalChildren[node.id].parents[i];
if (this.hierarchicalTrees[parentId] !== undefined) {
//tree = Math.min(tree,this.hierarchicalTrees[parentId]);
tree = this.hierarchicalTrees[parentId];
}
}
//for (let i = 0; i < this.hierarchicalChildren.parents.length; i++) {
// let parentId = this.hierarchicalChildren.parents[i];
// this.hierarchicalTrees[parentId] = tree;
//}
this.hierarchicalTrees[node.id] = tree;
}
else {
this.hierarchicalTrees[node.id] = ++this.treeIndex;
}
}
}
this.distributionOrderingPresence[level][node.id] = true;
if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
node.x = position;
}

+ 2
- 2
lib/network/modules/components/Edge.js View File

@ -385,12 +385,12 @@ class Edge {
// from and to arrows give a different end point for edges. we set them here
if (this.options.arrows.from.enabled === true) {
arrowData.from = this.edgeType.getArrowData(ctx,'from', viaNode, this.selected, this.hover);
if (this.options.arrowStrikethrough === true)
if (this.options.arrowStrikethrough === false)
this.edgeType.fromPoint = arrowData.from.core;
}
if (this.options.arrows.to.enabled === true) {
arrowData.to = this.edgeType.getArrowData(ctx,'to', viaNode, this.selected, this.hover);
if (this.options.arrowStrikethrough === true)
if (this.options.arrowStrikethrough === false)
this.edgeType.toPoint = arrowData.to.core;
}

+ 1
- 1
lib/network/options.js View File

@ -367,7 +367,7 @@ let configureOptions = {
middle: { enabled: false, scaleFactor: [1, 0, 3, 0.05] },
from: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }
},
arrowStrikethrough: false,
arrowStrikethrough: true,
color: {
color: ['color', '#848484'],
highlight: ['color', '#848484'],

+ 63
- 6
test/networkTest.html
File diff suppressed because it is too large
View File


Loading…
Cancel
Save