diff --git a/dist/vis.js b/dist/vis.js
index 944ec693..c6de6a40 100644
--- a/dist/vis.js
+++ b/dist/vis.js
@@ -103,13 +103,13 @@ return /******/ (function(modules) { // webpackBootstrap
// Timeline
exports.Timeline = __webpack_require__(18);
- exports.Graph2d = __webpack_require__(42);
+ exports.Graph2d = __webpack_require__(41);
exports.timeline = {
DateUtil: __webpack_require__(24),
- DataStep: __webpack_require__(45),
+ DataStep: __webpack_require__(44),
Range: __webpack_require__(21),
stack: __webpack_require__(28),
- TimeStep: __webpack_require__(38),
+ TimeStep: __webpack_require__(50),
components: {
items: {
@@ -121,15 +121,15 @@ return /******/ (function(modules) { // webpackBootstrap
},
Component: __webpack_require__(23),
- CurrentTime: __webpack_require__(39),
- CustomTime: __webpack_require__(41),
- DataAxis: __webpack_require__(44),
- GraphGroup: __webpack_require__(46),
+ CurrentTime: __webpack_require__(38),
+ CustomTime: __webpack_require__(40),
+ DataAxis: __webpack_require__(43),
+ GraphGroup: __webpack_require__(45),
Group: __webpack_require__(27),
BackgroundGroup: __webpack_require__(31),
ItemSet: __webpack_require__(26),
- Legend: __webpack_require__(50),
- LineGraph: __webpack_require__(43),
+ Legend: __webpack_require__(49),
+ LineGraph: __webpack_require__(42),
TimeAxis: __webpack_require__(37)
}
};
@@ -137,13 +137,13 @@ return /******/ (function(modules) { // webpackBootstrap
// Network
exports.Network = __webpack_require__(51);
exports.network = {
- Edge: __webpack_require__(52),
+ Edge: __webpack_require__(57),
Groups: __webpack_require__(54),
Images: __webpack_require__(55),
- Node: __webpack_require__(53),
- Popup: __webpack_require__(56),
- dotparser: __webpack_require__(57),
- gephiParser: __webpack_require__(58)
+ Node: __webpack_require__(56),
+ Popup: __webpack_require__(58),
+ dotparser: __webpack_require__(52),
+ gephiParser: __webpack_require__(53)
};
// Deprecated since v3.0.0
@@ -9548,8 +9548,8 @@ return /******/ (function(modules) { // webpackBootstrap
var Range = __webpack_require__(21);
var Core = __webpack_require__(25);
var TimeAxis = __webpack_require__(37);
- var CurrentTime = __webpack_require__(39);
- var CustomTime = __webpack_require__(41);
+ var CurrentTime = __webpack_require__(38);
+ var CustomTime = __webpack_require__(40);
var ItemSet = __webpack_require__(26);
/**
@@ -18034,7 +18034,7 @@ return /******/ (function(modules) { // webpackBootstrap
var util = __webpack_require__(1);
var Component = __webpack_require__(23);
- var TimeStep = __webpack_require__(38);
+ var TimeStep = __webpack_require__(50);
var DateUtil = __webpack_require__(24);
var moment = __webpack_require__(2);
@@ -18074,8 +18074,6 @@ return /******/ (function(modules) { // webpackBootstrap
// TODO: implement timeaxis orientations 'left' and 'right'
showMinorLabels: true,
showMajorLabels: true,
- showMajorLines: true,
- showMinorLines: true,
format: null
};
this.options = util.extend({}, this.defaultOptions);
@@ -18101,7 +18099,13 @@ return /******/ (function(modules) { // webpackBootstrap
TimeAxis.prototype.setOptions = function(options) {
if (options) {
// copy all options that we know
- util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels', 'showMinorLines', 'showMajorLines','hiddenDates', 'format'], this.options, options);
+ util.selectiveExtend([
+ 'orientation',
+ 'showMinorLabels',
+ 'showMajorLabels',
+ 'hiddenDates',
+ 'format'
+ ], this.options, options);
// apply locale to moment.js
// TODO: not so nice, this is applied globally to moment.js
@@ -18260,11 +18264,9 @@ return /******/ (function(modules) { // webpackBootstrap
}
this._repaintMajorText(x, step.getLabelMajor(), orientation);
}
- if (this.options.showMajorLines == true) {
- this._repaintMajorLine(x, orientation);
- }
+ this._repaintMajorLine(x, orientation);
}
- else if (this.options.showMinorLines == true) {
+ else {
this._repaintMinorLine(x, orientation);
}
@@ -18458,835 +18460,300 @@ return /******/ (function(modules) { // webpackBootstrap
/* 38 */
/***/ function(module, exports, __webpack_require__) {
- var moment = __webpack_require__(2);
- var DateUtil = __webpack_require__(24);
var util = __webpack_require__(1);
+ var Component = __webpack_require__(23);
+ var moment = __webpack_require__(2);
+ var locales = __webpack_require__(39);
/**
- * @constructor TimeStep
- * The class TimeStep is an iterator for dates. You provide a start date and an
- * end date. The class itself determines the best scale (step size) based on the
- * provided start Date, end Date, and minimumStep.
- *
- * If minimumStep is provided, the step size is chosen as close as possible
- * to the minimumStep but larger than minimumStep. If minimumStep is not
- * provided, the scale is set to 1 DAY.
- * The minimumStep should correspond with the onscreen size of about 6 characters
- *
- * Alternatively, you can set a scale by hand.
- * After creation, you can initialize the class by executing first(). Then you
- * can iterate from the start date to the end date via next(). You can check if
- * the end date is reached with the function hasNext(). After each step, you can
- * retrieve the current date via getCurrent().
- * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
- * days, to years.
- *
- * Version: 1.2
- *
- * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
- * or new Date(2010, 9, 21, 23, 45, 00)
- * @param {Date} [end] The end date
- * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
+ * A current time bar
+ * @param {{range: Range, dom: Object, domProps: Object}} body
+ * @param {Object} [options] Available parameters:
+ * {Boolean} [showCurrentTime]
+ * @constructor CurrentTime
+ * @extends Component
*/
- function TimeStep(start, end, minimumStep, hiddenDates) {
- // variables
- this.current = new Date();
- this._start = new Date();
- this._end = new Date();
+ function CurrentTime (body, options) {
+ this.body = body;
- this.autoScale = true;
- this.scale = 'day';
- this.step = 1;
+ // default options
+ this.defaultOptions = {
+ showCurrentTime: true,
- // initialize the range
- this.setRange(start, end, minimumStep);
+ locales: locales,
+ locale: 'en'
+ };
+ this.options = util.extend({}, this.defaultOptions);
+ this.offset = 0;
- // hidden Dates options
- this.switchedDay = false;
- this.switchedMonth = false;
- this.switchedYear = false;
- this.hiddenDates = hiddenDates;
- if (hiddenDates === undefined) {
- this.hiddenDates = [];
- }
+ this._create();
- this.format = TimeStep.FORMAT; // default formatting
+ this.setOptions(options);
}
- // Time formatting
- TimeStep.FORMAT = {
- minorLabels: {
- millisecond:'SSS',
- second: 's',
- minute: 'HH:mm',
- hour: 'HH:mm',
- weekday: 'ddd D',
- day: 'D',
- month: 'MMM',
- year: 'YYYY'
- },
- majorLabels: {
- millisecond:'HH:mm:ss',
- second: 'D MMMM HH:mm',
- minute: 'ddd D MMMM',
- hour: 'ddd D MMMM',
- weekday: 'MMMM YYYY',
- day: 'MMMM YYYY',
- month: 'YYYY',
- year: ''
- }
- };
+ CurrentTime.prototype = new Component();
/**
- * Set custom formatting for the minor an major labels of the TimeStep.
- * Both `minorLabels` and `majorLabels` are an Object with properties:
- * 'millisecond, 'second, 'minute', 'hour', 'weekday, 'day, 'month, 'year'.
- * @param {{minorLabels: Object, majorLabels: Object}} format
+ * Create the HTML DOM for the current time bar
+ * @private
*/
- TimeStep.prototype.setFormat = function (format) {
- var defaultFormat = util.deepExtend({}, TimeStep.FORMAT);
- this.format = util.deepExtend(defaultFormat, format);
+ CurrentTime.prototype._create = function() {
+ var bar = document.createElement('div');
+ bar.className = 'currenttime';
+ bar.style.position = 'absolute';
+ bar.style.top = '0px';
+ bar.style.height = '100%';
+
+ this.bar = bar;
};
/**
- * Set a new range
- * If minimumStep is provided, the step size is chosen as close as possible
- * to the minimumStep but larger than minimumStep. If minimumStep is not
- * provided, the scale is set to 1 DAY.
- * The minimumStep should correspond with the onscreen size of about 6 characters
- * @param {Date} [start] The start date and time.
- * @param {Date} [end] The end date and time.
- * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
+ * Destroy the CurrentTime bar
*/
- TimeStep.prototype.setRange = function(start, end, minimumStep) {
- if (!(start instanceof Date) || !(end instanceof Date)) {
- throw "No legal start or end date in method setRange";
- }
-
- this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
- this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
+ CurrentTime.prototype.destroy = function () {
+ this.options.showCurrentTime = false;
+ this.redraw(); // will remove the bar from the DOM and stop refreshing
- if (this.autoScale) {
- this.setMinimumStep(minimumStep);
- }
+ this.body = null;
};
/**
- * Set the range iterator to the start date.
+ * Set options for the component. Options will be merged in current options.
+ * @param {Object} options Available parameters:
+ * {boolean} [showCurrentTime]
*/
- TimeStep.prototype.first = function() {
- this.current = new Date(this._start.valueOf());
- this.roundToMinor();
+ CurrentTime.prototype.setOptions = function(options) {
+ if (options) {
+ // copy all options that we know
+ util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options);
+ }
};
/**
- * Round the current date to the first minor date value
- * This must be executed once when the current date is set to start Date
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
*/
- TimeStep.prototype.roundToMinor = function() {
- // round to floor
- // IMPORTANT: we have no breaks in this switch! (this is no bug)
- // noinspection FallThroughInSwitchStatementJS
- switch (this.scale) {
- case 'year':
- this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
- this.current.setMonth(0);
- case 'month': this.current.setDate(1);
- case 'day': // intentional fall through
- case 'weekday': this.current.setHours(0);
- case 'hour': this.current.setMinutes(0);
- case 'minute': this.current.setSeconds(0);
- case 'second': this.current.setMilliseconds(0);
- //case 'millisecond': // nothing to do for milliseconds
- }
+ CurrentTime.prototype.redraw = function() {
+ if (this.options.showCurrentTime) {
+ var parent = this.body.dom.backgroundVertical;
+ if (this.bar.parentNode != parent) {
+ // attach to the dom
+ if (this.bar.parentNode) {
+ this.bar.parentNode.removeChild(this.bar);
+ }
+ parent.appendChild(this.bar);
- if (this.step != 1) {
- // round down to the first minor value that is a multiple of the current step size
- switch (this.scale) {
- case 'millisecond': this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
- case 'second': this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
- case 'minute': this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
- case 'hour': this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
- case 'weekday': // intentional fall through
- case 'day': this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
- case 'month': this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
- case 'year': this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
- default: break;
+ this.start();
+ }
+
+ var now = new Date(new Date().valueOf() + this.offset);
+ var x = this.body.util.toScreen(now);
+
+ var locale = this.options.locales[this.options.locale];
+ var title = locale.current + ' ' + locale.time + ': ' + moment(now).format('dddd, MMMM Do YYYY, H:mm:ss');
+ title = title.charAt(0).toUpperCase() + title.substring(1);
+
+ this.bar.style.left = x + 'px';
+ this.bar.title = title;
+ }
+ else {
+ // remove the line from the DOM
+ if (this.bar.parentNode) {
+ this.bar.parentNode.removeChild(this.bar);
}
+ this.stop();
}
- };
- /**
- * Check if the there is a next step
- * @return {boolean} true if the current date has not passed the end date
- */
- TimeStep.prototype.hasNext = function () {
- return (this.current.valueOf() <= this._end.valueOf());
+ return false;
};
/**
- * Do the next step
+ * Start auto refreshing the current time bar
*/
- TimeStep.prototype.next = function() {
- var prev = this.current.valueOf();
+ CurrentTime.prototype.start = function() {
+ var me = this;
- // Two cases, needed to prevent issues with switching daylight savings
- // (end of March and end of October)
- if (this.current.getMonth() < 6) {
- switch (this.scale) {
- case 'millisecond':
+ function update () {
+ me.stop();
- this.current = new Date(this.current.valueOf() + this.step); break;
- case 'second': this.current = new Date(this.current.valueOf() + this.step * 1000); break;
- case 'minute': this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
- case 'hour':
- this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
- // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
- var h = this.current.getHours();
- this.current.setHours(h - (h % this.step));
- break;
- case 'weekday': // intentional fall through
- case 'day': this.current.setDate(this.current.getDate() + this.step); break;
- case 'month': this.current.setMonth(this.current.getMonth() + this.step); break;
- case 'year': this.current.setFullYear(this.current.getFullYear() + this.step); break;
- default: break;
- }
- }
- else {
- switch (this.scale) {
- case 'millisecond': this.current = new Date(this.current.valueOf() + this.step); break;
- case 'second': this.current.setSeconds(this.current.getSeconds() + this.step); break;
- case 'minute': this.current.setMinutes(this.current.getMinutes() + this.step); break;
- case 'hour': this.current.setHours(this.current.getHours() + this.step); break;
- case 'weekday': // intentional fall through
- case 'day': this.current.setDate(this.current.getDate() + this.step); break;
- case 'month': this.current.setMonth(this.current.getMonth() + this.step); break;
- case 'year': this.current.setFullYear(this.current.getFullYear() + this.step); break;
- default: break;
- }
- }
+ // determine interval to refresh
+ var scale = me.body.range.conversion(me.body.domProps.center.width).scale;
+ var interval = 1 / scale / 10;
+ if (interval < 30) interval = 30;
+ if (interval > 1000) interval = 1000;
- if (this.step != 1) {
- // round down to the correct major value
- switch (this.scale) {
- case 'millisecond': if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
- case 'second': if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
- case 'minute': if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
- case 'hour': if(this.current.getHours() < this.step) this.current.setHours(0); break;
- case 'weekday': // intentional fall through
- case 'day': if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
- case 'month': if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
- case 'year': break; // nothing to do for year
- default: break;
- }
- }
+ me.redraw();
- // safety mechanism: if current time is still unchanged, move to the end
- if (this.current.valueOf() == prev) {
- this.current = new Date(this._end.valueOf());
+ // start a timer to adjust for the new time
+ me.currentTimeTimer = setTimeout(update, interval);
}
- DateUtil.stepOverHiddenDates(this, prev);
+ update();
};
+ /**
+ * Stop auto refreshing the current time bar
+ */
+ CurrentTime.prototype.stop = function() {
+ if (this.currentTimeTimer !== undefined) {
+ clearTimeout(this.currentTimeTimer);
+ delete this.currentTimeTimer;
+ }
+ };
/**
- * Get the current datetime
- * @return {Date} current The current date
+ * Set a current time. This can be used for example to ensure that a client's
+ * time is synchronized with a shared server time.
+ * @param {Date | String | Number} time A Date, unix timestamp, or
+ * ISO date string.
*/
- TimeStep.prototype.getCurrent = function() {
- return this.current;
+ CurrentTime.prototype.setCurrentTime = function(time) {
+ var t = util.convert(time, 'Date').valueOf();
+ var now = new Date().valueOf();
+ this.offset = t - now;
+ this.redraw();
};
/**
- * Set a custom scale. Autoscaling will be disabled.
- * For example setScale(SCALE.MINUTES, 5) will result
- * in minor steps of 5 minutes, and major steps of an hour.
- *
- * @param {string} newScale
- * A scale. Choose from 'millisecond, 'second,
- * 'minute', 'hour', 'weekday, 'day, 'month, 'year'.
- * @param {Number} newStep A step size, by default 1. Choose for
- * example 1, 2, 5, or 10.
+ * Get the current time.
+ * @return {Date} Returns the current time.
*/
- TimeStep.prototype.setScale = function(newScale, newStep) {
- this.scale = newScale;
+ CurrentTime.prototype.getCurrentTime = function() {
+ return new Date(new Date().valueOf() + this.offset);
+ };
- if (newStep > 0) {
- this.step = newStep;
- }
+ module.exports = CurrentTime;
- this.autoScale = false;
+
+/***/ },
+/* 39 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // English
+ exports['en'] = {
+ current: 'current',
+ time: 'time'
+ };
+ exports['en_EN'] = exports['en'];
+ exports['en_US'] = exports['en'];
+
+ // Dutch
+ exports['nl'] = {
+ custom: 'aangepaste',
+ time: 'tijd'
};
+ exports['nl_NL'] = exports['nl'];
+ exports['nl_BE'] = exports['nl'];
+
+
+/***/ },
+/* 40 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Hammer = __webpack_require__(19);
+ var util = __webpack_require__(1);
+ var Component = __webpack_require__(23);
+ var moment = __webpack_require__(2);
+ var locales = __webpack_require__(39);
/**
- * Enable or disable autoscaling
- * @param {boolean} enable If true, autoascaling is set true
+ * A custom time bar
+ * @param {{range: Range, dom: Object}} body
+ * @param {Object} [options] Available parameters:
+ * {Boolean} [showCustomTime]
+ * @constructor CustomTime
+ * @extends Component
*/
- TimeStep.prototype.setAutoScale = function (enable) {
- this.autoScale = enable;
- };
+ function CustomTime (body, options) {
+ this.body = body;
+
+ // default options
+ this.defaultOptions = {
+ showCustomTime: false,
+ locales: locales,
+ locale: 'en'
+ };
+ this.options = util.extend({}, this.defaultOptions);
+
+ this.customTime = new Date();
+ this.eventParams = {}; // stores state parameters while dragging the bar
+
+ // create the DOM
+ this._create();
+
+ this.setOptions(options);
+ }
+
+ CustomTime.prototype = new Component();
/**
- * Automatically determine the scale that bests fits the provided minimum step
- * @param {Number} [minimumStep] The minimum step size in milliseconds
+ * Set options for the component. Options will be merged in current options.
+ * @param {Object} options Available parameters:
+ * {boolean} [showCustomTime]
*/
- TimeStep.prototype.setMinimumStep = function(minimumStep) {
- if (minimumStep == undefined) {
- return;
+ CustomTime.prototype.setOptions = function(options) {
+ if (options) {
+ // copy all options that we know
+ util.selectiveExtend(['showCustomTime', 'locale', 'locales'], this.options, options);
}
+ };
- //var b = asc + ds;
+ /**
+ * Create the DOM for the custom time
+ * @private
+ */
+ CustomTime.prototype._create = function() {
+ var bar = document.createElement('div');
+ bar.className = 'customtime';
+ bar.style.position = 'absolute';
+ bar.style.top = '0px';
+ bar.style.height = '100%';
+ this.bar = bar;
- var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
- var stepMonth = (1000 * 60 * 60 * 24 * 30);
- var stepDay = (1000 * 60 * 60 * 24);
- var stepHour = (1000 * 60 * 60);
- var stepMinute = (1000 * 60);
- var stepSecond = (1000);
- var stepMillisecond= (1);
+ var drag = document.createElement('div');
+ drag.style.position = 'relative';
+ drag.style.top = '0px';
+ drag.style.left = '-10px';
+ drag.style.height = '100%';
+ drag.style.width = '20px';
+ bar.appendChild(drag);
- // find the smallest step that is larger than the provided minimumStep
- if (stepYear*1000 > minimumStep) {this.scale = 'year'; this.step = 1000;}
- if (stepYear*500 > minimumStep) {this.scale = 'year'; this.step = 500;}
- if (stepYear*100 > minimumStep) {this.scale = 'year'; this.step = 100;}
- if (stepYear*50 > minimumStep) {this.scale = 'year'; this.step = 50;}
- if (stepYear*10 > minimumStep) {this.scale = 'year'; this.step = 10;}
- if (stepYear*5 > minimumStep) {this.scale = 'year'; this.step = 5;}
- if (stepYear > minimumStep) {this.scale = 'year'; this.step = 1;}
- if (stepMonth*3 > minimumStep) {this.scale = 'month'; this.step = 3;}
- if (stepMonth > minimumStep) {this.scale = 'month'; this.step = 1;}
- if (stepDay*5 > minimumStep) {this.scale = 'day'; this.step = 5;}
- if (stepDay*2 > minimumStep) {this.scale = 'day'; this.step = 2;}
- if (stepDay > minimumStep) {this.scale = 'day'; this.step = 1;}
- if (stepDay/2 > minimumStep) {this.scale = 'weekday'; this.step = 1;}
- if (stepHour*4 > minimumStep) {this.scale = 'hour'; this.step = 4;}
- if (stepHour > minimumStep) {this.scale = 'hour'; this.step = 1;}
- if (stepMinute*15 > minimumStep) {this.scale = 'minute'; this.step = 15;}
- if (stepMinute*10 > minimumStep) {this.scale = 'minute'; this.step = 10;}
- if (stepMinute*5 > minimumStep) {this.scale = 'minute'; this.step = 5;}
- if (stepMinute > minimumStep) {this.scale = 'minute'; this.step = 1;}
- if (stepSecond*15 > minimumStep) {this.scale = 'second'; this.step = 15;}
- if (stepSecond*10 > minimumStep) {this.scale = 'second'; this.step = 10;}
- if (stepSecond*5 > minimumStep) {this.scale = 'second'; this.step = 5;}
- if (stepSecond > minimumStep) {this.scale = 'second'; this.step = 1;}
- if (stepMillisecond*200 > minimumStep) {this.scale = 'millisecond'; this.step = 200;}
- if (stepMillisecond*100 > minimumStep) {this.scale = 'millisecond'; this.step = 100;}
- if (stepMillisecond*50 > minimumStep) {this.scale = 'millisecond'; this.step = 50;}
- if (stepMillisecond*10 > minimumStep) {this.scale = 'millisecond'; this.step = 10;}
- if (stepMillisecond*5 > minimumStep) {this.scale = 'millisecond'; this.step = 5;}
- if (stepMillisecond > minimumStep) {this.scale = 'millisecond'; this.step = 1;}
+ // attach event listeners
+ this.hammer = Hammer(bar, {
+ prevent_default: true
+ });
+ this.hammer.on('dragstart', this._onDragStart.bind(this));
+ this.hammer.on('drag', this._onDrag.bind(this));
+ this.hammer.on('dragend', this._onDragEnd.bind(this));
};
/**
- * Snap a date to a rounded value.
- * The snap intervals are dependent on the current scale and step.
- * @param {Date} date the date to be snapped.
- * @return {Date} snappedDate
+ * Destroy the CustomTime bar
*/
- TimeStep.prototype.snap = function(date) {
- var clone = new Date(date.valueOf());
+ CustomTime.prototype.destroy = function () {
+ this.options.showCustomTime = false;
+ this.redraw(); // will remove the bar from the DOM
- if (this.scale == 'year') {
- var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
- clone.setFullYear(Math.round(year / this.step) * this.step);
- clone.setMonth(0);
- clone.setDate(0);
- clone.setHours(0);
- clone.setMinutes(0);
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- }
- else if (this.scale == 'month') {
- if (clone.getDate() > 15) {
- clone.setDate(1);
- clone.setMonth(clone.getMonth() + 1);
- // important: first set Date to 1, after that change the month.
- }
- else {
- clone.setDate(1);
- }
-
- clone.setHours(0);
- clone.setMinutes(0);
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- }
- else if (this.scale == 'day') {
- //noinspection FallthroughInSwitchStatementJS
- switch (this.step) {
- case 5:
- case 2:
- clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
- default:
- clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
- }
- clone.setMinutes(0);
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- }
- else if (this.scale == 'weekday') {
- //noinspection FallthroughInSwitchStatementJS
- switch (this.step) {
- case 5:
- case 2:
- clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
- default:
- clone.setHours(Math.round(clone.getHours() / 6) * 6); break;
- }
- clone.setMinutes(0);
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- }
- else if (this.scale == 'hour') {
- switch (this.step) {
- case 4:
- clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
- default:
- clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
- }
- clone.setSeconds(0);
- clone.setMilliseconds(0);
- } else if (this.scale == 'minute') {
- //noinspection FallthroughInSwitchStatementJS
- switch (this.step) {
- case 15:
- case 10:
- clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
- clone.setSeconds(0);
- break;
- case 5:
- clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
- default:
- clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
- }
- clone.setMilliseconds(0);
- }
- else if (this.scale == 'second') {
- //noinspection FallthroughInSwitchStatementJS
- switch (this.step) {
- case 15:
- case 10:
- clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
- clone.setMilliseconds(0);
- break;
- case 5:
- clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
- default:
- clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
- }
- }
- else if (this.scale == 'millisecond') {
- var step = this.step > 5 ? this.step / 2 : 1;
- clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
- }
-
- return clone;
- };
-
- /**
- * Check if the current value is a major value (for example when the step
- * is DAY, a major value is each first day of the MONTH)
- * @return {boolean} true if current date is major, else false.
- */
- TimeStep.prototype.isMajor = function() {
- if (this.switchedYear == true) {
- this.switchedYear = false;
- switch (this.scale) {
- case 'year':
- case 'month':
- case 'weekday':
- case 'day':
- case 'hour':
- case 'minute':
- case 'second':
- case 'millisecond':
- return true;
- default:
- return false;
- }
- }
- else if (this.switchedMonth == true) {
- this.switchedMonth = false;
- switch (this.scale) {
- case 'weekday':
- case 'day':
- case 'hour':
- case 'minute':
- case 'second':
- case 'millisecond':
- return true;
- default:
- return false;
- }
- }
- else if (this.switchedDay == true) {
- this.switchedDay = false;
- switch (this.scale) {
- case 'millisecond':
- case 'second':
- case 'minute':
- case 'hour':
- return true;
- default:
- return false;
- }
- }
-
- switch (this.scale) {
- case 'millisecond':
- return (this.current.getMilliseconds() == 0);
- case 'second':
- return (this.current.getSeconds() == 0);
- case 'minute':
- return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
- case 'hour':
- return (this.current.getHours() == 0);
- case 'weekday': // intentional fall through
- case 'day':
- return (this.current.getDate() == 1);
- case 'month':
- return (this.current.getMonth() == 0);
- case 'year':
- return false;
- default:
- return false;
- }
- };
-
-
- /**
- * Returns formatted text for the minor axislabel, depending on the current
- * date and the scale. For example when scale is MINUTE, the current time is
- * formatted as "hh:mm".
- * @param {Date} [date] custom date. if not provided, current date is taken
- */
- TimeStep.prototype.getLabelMinor = function(date) {
- if (date == undefined) {
- date = this.current;
- }
-
- var format = this.format.minorLabels[this.scale];
- return (format && format.length > 0) ? moment(date).format(format) : '';
- };
-
- /**
- * Returns formatted text for the major axis label, depending on the current
- * date and the scale. For example when scale is MINUTE, the major scale is
- * hours, and the hour will be formatted as "hh".
- * @param {Date} [date] custom date. if not provided, current date is taken
- */
- TimeStep.prototype.getLabelMajor = function(date) {
- if (date == undefined) {
- date = this.current;
- }
-
- var format = this.format.majorLabels[this.scale];
- return (format && format.length > 0) ? moment(date).format(format) : '';
- };
-
- module.exports = TimeStep;
-
-
-/***/ },
-/* 39 */
-/***/ function(module, exports, __webpack_require__) {
-
- var util = __webpack_require__(1);
- var Component = __webpack_require__(23);
- var moment = __webpack_require__(2);
- var locales = __webpack_require__(40);
-
- /**
- * A current time bar
- * @param {{range: Range, dom: Object, domProps: Object}} body
- * @param {Object} [options] Available parameters:
- * {Boolean} [showCurrentTime]
- * @constructor CurrentTime
- * @extends Component
- */
- function CurrentTime (body, options) {
- this.body = body;
-
- // default options
- this.defaultOptions = {
- showCurrentTime: true,
-
- locales: locales,
- locale: 'en'
- };
- this.options = util.extend({}, this.defaultOptions);
- this.offset = 0;
-
- this._create();
-
- this.setOptions(options);
- }
-
- CurrentTime.prototype = new Component();
-
- /**
- * Create the HTML DOM for the current time bar
- * @private
- */
- CurrentTime.prototype._create = function() {
- var bar = document.createElement('div');
- bar.className = 'currenttime';
- bar.style.position = 'absolute';
- bar.style.top = '0px';
- bar.style.height = '100%';
-
- this.bar = bar;
- };
-
- /**
- * Destroy the CurrentTime bar
- */
- CurrentTime.prototype.destroy = function () {
- this.options.showCurrentTime = false;
- this.redraw(); // will remove the bar from the DOM and stop refreshing
-
- this.body = null;
- };
-
- /**
- * Set options for the component. Options will be merged in current options.
- * @param {Object} options Available parameters:
- * {boolean} [showCurrentTime]
- */
- CurrentTime.prototype.setOptions = function(options) {
- if (options) {
- // copy all options that we know
- util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options);
- }
- };
-
- /**
- * Repaint the component
- * @return {boolean} Returns true if the component is resized
- */
- CurrentTime.prototype.redraw = function() {
- if (this.options.showCurrentTime) {
- var parent = this.body.dom.backgroundVertical;
- if (this.bar.parentNode != parent) {
- // attach to the dom
- if (this.bar.parentNode) {
- this.bar.parentNode.removeChild(this.bar);
- }
- parent.appendChild(this.bar);
-
- this.start();
- }
-
- var now = new Date(new Date().valueOf() + this.offset);
- var x = this.body.util.toScreen(now);
-
- var locale = this.options.locales[this.options.locale];
- var title = locale.current + ' ' + locale.time + ': ' + moment(now).format('dddd, MMMM Do YYYY, H:mm:ss');
- title = title.charAt(0).toUpperCase() + title.substring(1);
-
- this.bar.style.left = x + 'px';
- this.bar.title = title;
- }
- else {
- // remove the line from the DOM
- if (this.bar.parentNode) {
- this.bar.parentNode.removeChild(this.bar);
- }
- this.stop();
- }
-
- return false;
- };
-
- /**
- * Start auto refreshing the current time bar
- */
- CurrentTime.prototype.start = function() {
- var me = this;
-
- function update () {
- me.stop();
-
- // determine interval to refresh
- var scale = me.body.range.conversion(me.body.domProps.center.width).scale;
- var interval = 1 / scale / 10;
- if (interval < 30) interval = 30;
- if (interval > 1000) interval = 1000;
-
- me.redraw();
-
- // start a timer to adjust for the new time
- me.currentTimeTimer = setTimeout(update, interval);
- }
-
- update();
- };
-
- /**
- * Stop auto refreshing the current time bar
- */
- CurrentTime.prototype.stop = function() {
- if (this.currentTimeTimer !== undefined) {
- clearTimeout(this.currentTimeTimer);
- delete this.currentTimeTimer;
- }
- };
-
- /**
- * Set a current time. This can be used for example to ensure that a client's
- * time is synchronized with a shared server time.
- * @param {Date | String | Number} time A Date, unix timestamp, or
- * ISO date string.
- */
- CurrentTime.prototype.setCurrentTime = function(time) {
- var t = util.convert(time, 'Date').valueOf();
- var now = new Date().valueOf();
- this.offset = t - now;
- this.redraw();
- };
-
- /**
- * Get the current time.
- * @return {Date} Returns the current time.
- */
- CurrentTime.prototype.getCurrentTime = function() {
- return new Date(new Date().valueOf() + this.offset);
- };
-
- module.exports = CurrentTime;
-
-
-/***/ },
-/* 40 */
-/***/ function(module, exports, __webpack_require__) {
-
- // English
- exports['en'] = {
- current: 'current',
- time: 'time'
- };
- exports['en_EN'] = exports['en'];
- exports['en_US'] = exports['en'];
-
- // Dutch
- exports['nl'] = {
- custom: 'aangepaste',
- time: 'tijd'
- };
- exports['nl_NL'] = exports['nl'];
- exports['nl_BE'] = exports['nl'];
-
-
-/***/ },
-/* 41 */
-/***/ function(module, exports, __webpack_require__) {
-
- var Hammer = __webpack_require__(19);
- var util = __webpack_require__(1);
- var Component = __webpack_require__(23);
- var moment = __webpack_require__(2);
- var locales = __webpack_require__(40);
-
- /**
- * A custom time bar
- * @param {{range: Range, dom: Object}} body
- * @param {Object} [options] Available parameters:
- * {Boolean} [showCustomTime]
- * @constructor CustomTime
- * @extends Component
- */
-
- function CustomTime (body, options) {
- this.body = body;
-
- // default options
- this.defaultOptions = {
- showCustomTime: false,
- locales: locales,
- locale: 'en'
- };
- this.options = util.extend({}, this.defaultOptions);
-
- this.customTime = new Date();
- this.eventParams = {}; // stores state parameters while dragging the bar
-
- // create the DOM
- this._create();
-
- this.setOptions(options);
- }
-
- CustomTime.prototype = new Component();
-
- /**
- * Set options for the component. Options will be merged in current options.
- * @param {Object} options Available parameters:
- * {boolean} [showCustomTime]
- */
- CustomTime.prototype.setOptions = function(options) {
- if (options) {
- // copy all options that we know
- util.selectiveExtend(['showCustomTime', 'locale', 'locales'], this.options, options);
- }
- };
-
- /**
- * Create the DOM for the custom time
- * @private
- */
- CustomTime.prototype._create = function() {
- var bar = document.createElement('div');
- bar.className = 'customtime';
- bar.style.position = 'absolute';
- bar.style.top = '0px';
- bar.style.height = '100%';
- this.bar = bar;
-
- var drag = document.createElement('div');
- drag.style.position = 'relative';
- drag.style.top = '0px';
- drag.style.left = '-10px';
- drag.style.height = '100%';
- drag.style.width = '20px';
- bar.appendChild(drag);
-
- // attach event listeners
- this.hammer = Hammer(bar, {
- prevent_default: true
- });
- this.hammer.on('dragstart', this._onDragStart.bind(this));
- this.hammer.on('drag', this._onDrag.bind(this));
- this.hammer.on('dragend', this._onDragEnd.bind(this));
- };
-
- /**
- * Destroy the CustomTime bar
- */
- CustomTime.prototype.destroy = function () {
- this.options.showCustomTime = false;
- this.redraw(); // will remove the bar from the DOM
-
- this.hammer.enable(false);
- this.hammer = null;
-
- this.body = null;
- };
-
- /**
- * Repaint the component
- * @return {boolean} Returns true if the component is resized
- */
- CustomTime.prototype.redraw = function () {
- if (this.options.showCustomTime) {
- var parent = this.body.dom.backgroundVertical;
- if (this.bar.parentNode != parent) {
- // attach to the dom
- if (this.bar.parentNode) {
- this.bar.parentNode.removeChild(this.bar);
- }
- parent.appendChild(this.bar);
+ this.hammer.enable(false);
+ this.hammer = null;
+
+ this.body = null;
+ };
+
+ /**
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
+ */
+ CustomTime.prototype.redraw = function () {
+ if (this.options.showCustomTime) {
+ var parent = this.body.dom.backgroundVertical;
+ if (this.bar.parentNode != parent) {
+ // attach to the dom
+ if (this.bar.parentNode) {
+ this.bar.parentNode.removeChild(this.bar);
+ }
+ parent.appendChild(this.bar);
}
var x = this.body.util.toScreen(this.customTime);
@@ -19382,7 +18849,7 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ },
-/* 42 */
+/* 41 */
/***/ function(module, exports, __webpack_require__) {
var Emitter = __webpack_require__(11);
@@ -19393,9 +18860,9 @@ return /******/ (function(modules) { // webpackBootstrap
var Range = __webpack_require__(21);
var Core = __webpack_require__(25);
var TimeAxis = __webpack_require__(37);
- var CurrentTime = __webpack_require__(39);
- var CustomTime = __webpack_require__(41);
- var LineGraph = __webpack_require__(43);
+ var CurrentTime = __webpack_require__(38);
+ var CustomTime = __webpack_require__(40);
+ var LineGraph = __webpack_require__(42);
/**
* Create a timeline visualization
@@ -19632,7 +19099,7 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ },
-/* 43 */
+/* 42 */
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
@@ -19640,10 +19107,10 @@ return /******/ (function(modules) { // webpackBootstrap
var DataSet = __webpack_require__(7);
var DataView = __webpack_require__(9);
var Component = __webpack_require__(23);
- var DataAxis = __webpack_require__(44);
- var GraphGroup = __webpack_require__(46);
- var Legend = __webpack_require__(50);
- var BarGraphFunctions = __webpack_require__(49);
+ var DataAxis = __webpack_require__(43);
+ var GraphGroup = __webpack_require__(45);
+ var Legend = __webpack_require__(49);
+ var BarGraphFunctions = __webpack_require__(48);
var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
@@ -19687,8 +19154,6 @@ return /******/ (function(modules) { // webpackBootstrap
dataAxis: {
showMinorLabels: true,
showMajorLabels: true,
- showMinorLines: true,
- showMajorLines: true,
icons: false,
width: '40px',
visible: true,
@@ -20633,13 +20098,13 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ },
-/* 44 */
+/* 43 */
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
var DOMutil = __webpack_require__(6);
var Component = __webpack_require__(23);
- var DataStep = __webpack_require__(45);
+ var DataStep = __webpack_require__(44);
/**
* A horizontal time axis
@@ -20657,8 +20122,6 @@ return /******/ (function(modules) { // webpackBootstrap
orientation: 'left', // supported: 'left', 'right'
showMinorLabels: true,
showMajorLabels: true,
- showMinorLines: true,
- showMajorLines: true,
icons: true,
majorLinesOffset: 7,
minorLinesOffset: 4,
@@ -20758,8 +20221,6 @@ return /******/ (function(modules) { // webpackBootstrap
'orientation',
'showMinorLabels',
'showMajorLabels',
- 'showMajorLines',
- 'showMinorLines',
'icons',
'majorLinesOffset',
'minorLinesOffset',
@@ -21066,11 +20527,9 @@ return /******/ (function(modules) { // webpackBootstrap
if (y >= 0) {
this._redrawLabel(y - 2, step.getCurrent(decimals), orientation, 'yAxis major', this.props.majorCharHeight);
}
- if (this.options.showMajorLines == true) {
- this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
- }
+ this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
}
- else if (this.options.showMinorLines == true) {
+ else {
this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth);
}
@@ -21284,7 +20743,7 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ },
-/* 45 */
+/* 44 */
/***/ function(module, exports, __webpack_require__) {
/**
@@ -21565,14 +21024,14 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ },
-/* 46 */
+/* 45 */
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
var DOMutil = __webpack_require__(6);
- var Line = __webpack_require__(47);
- var Bar = __webpack_require__(49);
- var Points = __webpack_require__(48);
+ var Line = __webpack_require__(46);
+ var Bar = __webpack_require__(48);
+ var Points = __webpack_require__(47);
/**
* /**
@@ -21770,14 +21229,14 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ },
-/* 47 */
+/* 46 */
/***/ function(module, exports, __webpack_require__) {
/**
* Created by Alex on 11/11/2014.
*/
var DOMutil = __webpack_require__(6);
- var Points = __webpack_require__(48);
+ var Points = __webpack_require__(47);
function Line(groupId, options) {
this.groupId = groupId;
@@ -21994,7 +21453,7 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ },
-/* 48 */
+/* 47 */
/***/ function(module, exports, __webpack_require__) {
/**
@@ -22042,14 +21501,14 @@ return /******/ (function(modules) { // webpackBootstrap
module.exports = Points;
/***/ },
-/* 49 */
+/* 48 */
/***/ function(module, exports, __webpack_require__) {
/**
* Created by Alex on 11/11/2014.
*/
var DOMutil = __webpack_require__(6);
- var Points = __webpack_require__(48);
+ var Points = __webpack_require__(47);
function Bargraph(groupId, options) {
this.groupId = groupId;
@@ -22276,7 +21735,7 @@ return /******/ (function(modules) { // webpackBootstrap
module.exports = Bargraph;
/***/ },
-/* 50 */
+/* 49 */
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
@@ -22486,746 +21945,1281 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ },
-/* 51 */
+/* 50 */
/***/ function(module, exports, __webpack_require__) {
- var Emitter = __webpack_require__(11);
- var Hammer = __webpack_require__(19);
- var keycharm = __webpack_require__(36);
+ var moment = __webpack_require__(2);
+ var DateUtil = __webpack_require__(24);
var util = __webpack_require__(1);
- var hammerUtil = __webpack_require__(22);
- var DataSet = __webpack_require__(7);
- var DataView = __webpack_require__(9);
- var dotparser = __webpack_require__(57);
- var gephiParser = __webpack_require__(58);
- var Groups = __webpack_require__(54);
- var Images = __webpack_require__(55);
- var Node = __webpack_require__(53);
- var Edge = __webpack_require__(52);
- var Popup = __webpack_require__(56);
- var MixinLoader = __webpack_require__(59);
- var Activator = __webpack_require__(35);
- var locales = __webpack_require__(60);
-
- // Load custom shapes into CanvasRenderingContext2D
- __webpack_require__(61);
/**
- * @constructor Network
- * Create a network visualization, displaying nodes and edges.
+ * @constructor TimeStep
+ * The class TimeStep is an iterator for dates. You provide a start date and an
+ * end date. The class itself determines the best scale (step size) based on the
+ * provided start Date, end Date, and minimumStep.
*
- * @param {Element} container The DOM element in which the Network will
- * be created. Normally a div element.
- * @param {Object} data An object containing parameters
- * {Array} nodes
- * {Array} edges
- * @param {Object} options Options
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ *
+ * Alternatively, you can set a scale by hand.
+ * After creation, you can initialize the class by executing first(). Then you
+ * can iterate from the start date to the end date via next(). You can check if
+ * the end date is reached with the function hasNext(). After each step, you can
+ * retrieve the current date via getCurrent().
+ * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
+ * days, to years.
+ *
+ * Version: 1.2
+ *
+ * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
+ * or new Date(2010, 9, 21, 23, 45, 00)
+ * @param {Date} [end] The end date
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
*/
- function Network (container, data, options) {
- if (!(this instanceof Network)) {
- throw new SyntaxError('Constructor must be called with the new operator');
- }
-
- this._determineBrowserMethod();
- this._initializeMixinLoaders();
-
- // create variables and set default values
- this.containerElement = container;
-
- // render and calculation settings
- this.renderRefreshRate = 60; // hz (fps)
- this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
- this.renderTime = 0; // measured time it takes to render a frame
- this.physicsTime = 0; // measured time it takes to render a frame
- this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation
-
- this.initializing = true;
-
- this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
-
- // set constant values
- this.defaultOptions = {
- nodes: {
- mass: 1,
- radiusMin: 10,
- radiusMax: 30,
- radius: 10,
- shape: 'ellipse',
- image: undefined,
- widthMin: 16, // px
- widthMax: 64, // px
- fontColor: 'black',
- fontSize: 14, // px
- fontFace: 'verdana',
- fontFill: undefined,
- level: -1,
- color: {
- border: '#2B7CE9',
- background: '#97C2FC',
- highlight: {
- border: '#2B7CE9',
- background: '#D2E5FF'
- },
- hover: {
- border: '#2B7CE9',
- background: '#D2E5FF'
- }
- },
- group: undefined,
- borderWidth: 1,
- borderWidthSelected: undefined
- },
- edges: {
- widthMin: 1, //
- widthMax: 15,//
- width: 1,
- widthSelectionMultiplier: 2,
- hoverWidth: 1.5,
- style: 'line',
- color: {
- color:'#848484',
- highlight:'#848484',
- hover: '#848484'
- },
- fontColor: '#343434',
- fontSize: 14, // px
- fontFace: 'arial',
- fontFill: 'white',
- arrowScaleFactor: 1,
- dash: {
- length: 10,
- gap: 5,
- altLength: undefined
- },
- inheritColor: "from" // to, from, false, true (== from)
- },
- configurePhysics:false,
- physics: {
- barnesHut: {
- enabled: true,
- thetaInverted: 1 / 0.5, // inverted to save time during calculation
- gravitationalConstant: -2000,
- centralGravity: 0.3,
- springLength: 95,
- springConstant: 0.04,
- damping: 0.09
- },
- repulsion: {
- centralGravity: 0.0,
- springLength: 200,
- springConstant: 0.05,
- nodeDistance: 100,
- damping: 0.09
- },
- hierarchicalRepulsion: {
- enabled: false,
- centralGravity: 0.0,
- springLength: 100,
- springConstant: 0.01,
- nodeDistance: 150,
- damping: 0.09
- },
- damping: null,
- centralGravity: null,
- springLength: null,
- springConstant: null
- },
- clustering: { // Per Node in Cluster = PNiC
- enabled: false, // (Boolean) | global on/off switch for clustering.
- initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
- clusterThreshold:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToNodes
- reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this
- chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
- clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
- sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
- screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node.
- fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
- maxFontSize: 1000,
- forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
- distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
- edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
- nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
- height: 1, // (px PNiC) | growth of the height per node in cluster.
- radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
- maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
- activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
- clusterLevelDifference: 2
- },
- navigation: {
- enabled: false
- },
- keyboard: {
- enabled: false,
- speed: {x: 10, y: 10, zoom: 0.02}
- },
- dataManipulation: {
- enabled: false,
- initiallyVisible: false
- },
- hierarchicalLayout: {
- enabled:false,
- levelSeparation: 150,
- nodeSpacing: 100,
- direction: "UD", // UD, DU, LR, RL
- layout: "hubsize" // hubsize, directed
- },
- freezeForStabilization: false,
- smoothCurves: {
- enabled: true,
- dynamic: true,
- type: "continuous",
- roundness: 0.5
- },
- maxVelocity: 30,
- minVelocity: 0.1, // px/s
- stabilize: true, // stabilize before displaying the network
- stabilizationIterations: 1000, // maximum number of iteration to stabilize
- zoomExtentOnStabilize: true,
- locale: 'en',
- locales: locales,
- tooltip: {
- delay: 300,
- fontColor: 'black',
- fontSize: 14, // px
- fontFace: 'verdana',
- color: {
- border: '#666',
- background: '#FFFFC6'
- }
- },
- dragNetwork: true,
- dragNodes: true,
- zoomable: true,
- hover: false,
- hideEdgesOnDrag: false,
- hideNodesOnDrag: false,
- width : '100%',
- height : '100%',
- selectable: true
- };
- this.constants = util.extend({}, this.defaultOptions);
- this.pixelRatio = 1;
-
-
- this.hoverObj = {nodes:{},edges:{}};
- this.controlNodesActive = false;
- this.navigationHammers = {existing:[], _new: []};
-
- // animation properties
- this.animationSpeed = 1/this.renderRefreshRate;
- this.animationEasingFunction = "easeInOutQuint";
- this.easingTime = 0;
- this.sourceScale = 0;
- this.targetScale = 0;
- this.sourceTranslation = 0;
- this.targetTranslation = 0;
- this.lockedOnNodeId = null;
- this.lockedOnNodeOffset = null;
- this.touchTime = 0;
-
- // Node variables
- var network = this;
- this.groups = new Groups(); // object with groups
- this.images = new Images(); // object with images
- this.images.setOnloadCallback(function () {
- network._redraw();
- });
-
- // keyboard navigation variables
- this.xIncrement = 0;
- this.yIncrement = 0;
- this.zoomIncrement = 0;
-
- // loading all the mixins:
- // load the force calculation functions, grouped under the physics system.
- this._loadPhysicsSystem();
- // create a frame and canvas
- this._create();
- // load the sector system. (mandatory, fully integrated with Network)
- this._loadSectorSystem();
- // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
- this._loadClusterSystem();
- // load the selection system. (mandatory, required by Network)
- this._loadSelectionSystem();
- // load the selection system. (mandatory, required by Network)
- this._loadHierarchySystem();
-
-
- // apply options
- this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
- this._setScale(1);
- this.setOptions(options);
-
- // other vars
- this.freezeSimulation = false;// freeze the simulation
- this.cachedFunctions = {};
- this.startedStabilization = false;
- this.stabilized = false;
- this.stabilizationIterations = null;
- this.draggingNodes = false;
-
- // containers for nodes and edges
- this.calculationNodes = {};
- this.calculationNodeIndices = [];
- this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
- this.nodes = {}; // object with Node objects
- this.edges = {}; // object with Edge objects
-
- // position and scale variables and objects
- this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
- this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
- this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
- this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
- this.scale = 1; // defining the global scale variable in the constructor
- this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
-
- // datasets or dataviews
- this.nodesData = null; // A DataSet or DataView
- this.edgesData = null; // A DataSet or DataView
-
- // create event listeners used to subscribe on the DataSets of the nodes and edges
- this.nodesListeners = {
- 'add': function (event, params) {
- network._addNodes(params.items);
- network.start();
- },
- 'update': function (event, params) {
- network._updateNodes(params.items, params.data);
- network.start();
- },
- 'remove': function (event, params) {
- network._removeNodes(params.items);
- network.start();
- }
- };
- this.edgesListeners = {
- 'add': function (event, params) {
- network._addEdges(params.items);
- network.start();
- },
- 'update': function (event, params) {
- network._updateEdges(params.items);
- network.start();
- },
- 'remove': function (event, params) {
- network._removeEdges(params.items);
- network.start();
- }
- };
+ function TimeStep(start, end, minimumStep, hiddenDates) {
+ // variables
+ this.current = new Date();
+ this._start = new Date();
+ this._end = new Date();
- // properties for the animation
- this.moving = true;
- this.timer = undefined; // Scheduling function. Is definded in this.start();
+ this.autoScale = true;
+ this.scale = 'day';
+ this.step = 1;
- // load data (the disable start variable will be the same as the enabled clustering)
- this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
+ // initialize the range
+ this.setRange(start, end, minimumStep);
- // hierarchical layout
- this.initializing = false;
- if (this.constants.hierarchicalLayout.enabled == true) {
- this._setupHierarchicalLayout();
- }
- else {
- // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
- if (this.constants.stabilize == false) {
- this.zoomExtent(undefined, true,this.constants.clustering.enabled);
- }
+ // hidden Dates options
+ this.switchedDay = false;
+ this.switchedMonth = false;
+ this.switchedYear = false;
+ this.hiddenDates = hiddenDates;
+ if (hiddenDates === undefined) {
+ this.hiddenDates = [];
}
- // if clustering is disabled, the simulation will have started in the setData function
- if (this.constants.clustering.enabled) {
- this.startWithClustering();
- }
+ this.format = TimeStep.FORMAT; // default formatting
}
- // Extend Network with an Emitter mixin
- Emitter(Network.prototype);
-
-
- Network.prototype._determineBrowserMethod = function() {
- var ua = navigator.userAgent.toLowerCase();
-
- this.requiresTimeout = false;
- if (ua.indexOf('msie 9.0') != -1) { // IE 9
- this.requiresTimeout = true;
- }
- else if (ua.indexOf('safari') != -1) { // safari
- if (ua.indexOf('chrome') <= -1) {
- this.requiresTimeout = true;
- }
+ // Time formatting
+ TimeStep.FORMAT = {
+ minorLabels: {
+ millisecond:'SSS',
+ second: 's',
+ minute: 'HH:mm',
+ hour: 'HH:mm',
+ weekday: 'ddd D',
+ day: 'D',
+ month: 'MMM',
+ year: 'YYYY'
+ },
+ majorLabels: {
+ millisecond:'HH:mm:ss',
+ second: 'D MMMM HH:mm',
+ minute: 'ddd D MMMM',
+ hour: 'ddd D MMMM',
+ weekday: 'MMMM YYYY',
+ day: 'MMMM YYYY',
+ month: 'YYYY',
+ year: ''
}
- }
-
+ };
/**
- * Get the script path where the vis.js library is located
- *
- * @returns {string | null} path Path or null when not found. Path does not
- * end with a slash.
- * @private
+ * Set custom formatting for the minor an major labels of the TimeStep.
+ * Both `minorLabels` and `majorLabels` are an Object with properties:
+ * 'millisecond, 'second, 'minute', 'hour', 'weekday, 'day, 'month, 'year'.
+ * @param {{minorLabels: Object, majorLabels: Object}} format
*/
- Network.prototype._getScriptPath = function() {
- var scripts = document.getElementsByTagName( 'script' );
-
- // find script named vis.js or vis.min.js
- for (var i = 0; i < scripts.length; i++) {
- var src = scripts[i].src;
- var match = src && /\/?vis(.min)?\.js$/.exec(src);
- if (match) {
- // return path without the script name
- return src.substring(0, src.length - match[0].length);
- }
- }
-
- return null;
+ TimeStep.prototype.setFormat = function (format) {
+ var defaultFormat = util.deepExtend({}, TimeStep.FORMAT);
+ this.format = util.deepExtend(defaultFormat, format);
};
-
/**
- * Find the center position of the network
- * @private
+ * Set a new range
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ * @param {Date} [start] The start date and time.
+ * @param {Date} [end] The end date and time.
+ * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
*/
- Network.prototype._getRange = function() {
- var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- if (minX > (node.boundingBox.left)) {minX = node.boundingBox.left;}
- if (maxX < (node.boundingBox.right)) {maxX = node.boundingBox.right;}
- if (minY > (node.boundingBox.bottom)) {minY = node.boundingBox.bottom;}
- if (maxY < (node.boundingBox.top)) {maxY = node.boundingBox.top;}
- }
+ TimeStep.prototype.setRange = function(start, end, minimumStep) {
+ if (!(start instanceof Date) || !(end instanceof Date)) {
+ throw "No legal start or end date in method setRange";
}
- if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
- minY = 0, maxY = 0, minX = 0, maxX = 0;
+
+ this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
+ this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
+
+ if (this.autoScale) {
+ this.setMinimumStep(minimumStep);
}
- return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
};
-
/**
- * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
- * @returns {{x: number, y: number}}
- * @private
+ * Set the range iterator to the start date.
*/
- Network.prototype._findCenter = function(range) {
- return {x: (0.5 * (range.maxX + range.minX)),
- y: (0.5 * (range.maxY + range.minY))};
+ TimeStep.prototype.first = function() {
+ this.current = new Date(this._start.valueOf());
+ this.roundToMinor();
};
-
/**
- * This function zooms out to fit all data on screen based on amount of nodes
- *
- * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
- * @param {Boolean} [disableStart] | If true, start is not called.
+ * Round the current date to the first minor date value
+ * This must be executed once when the current date is set to start Date
*/
- Network.prototype.zoomExtent = function(animationOptions, initialZoom, disableStart) {
- this._redraw(true);
-
- if (initialZoom === undefined) {
- initialZoom = false;
- }
- if (disableStart === undefined) {
- disableStart = false;
+ TimeStep.prototype.roundToMinor = function() {
+ // round to floor
+ // IMPORTANT: we have no breaks in this switch! (this is no bug)
+ // noinspection FallThroughInSwitchStatementJS
+ switch (this.scale) {
+ case 'year':
+ this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
+ this.current.setMonth(0);
+ case 'month': this.current.setDate(1);
+ case 'day': // intentional fall through
+ case 'weekday': this.current.setHours(0);
+ case 'hour': this.current.setMinutes(0);
+ case 'minute': this.current.setSeconds(0);
+ case 'second': this.current.setMilliseconds(0);
+ //case 'millisecond': // nothing to do for milliseconds
}
- if (animationOptions === undefined) {
- animationOptions = false;
+
+ if (this.step != 1) {
+ // round down to the first minor value that is a multiple of the current step size
+ switch (this.scale) {
+ case 'millisecond': this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
+ case 'second': this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
+ case 'minute': this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
+ case 'hour': this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
+ case 'weekday': // intentional fall through
+ case 'day': this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
+ case 'month': this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
+ case 'year': this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
+ default: break;
+ }
}
+ };
- var range = this._getRange();
- var zoomLevel;
+ /**
+ * Check if the there is a next step
+ * @return {boolean} true if the current date has not passed the end date
+ */
+ TimeStep.prototype.hasNext = function () {
+ return (this.current.valueOf() <= this._end.valueOf());
+ };
- if (initialZoom == true) {
- var numberOfNodes = this.nodeIndices.length;
- if (this.constants.smoothCurves == true) {
- if (this.constants.clustering.enabled == true &&
- numberOfNodes >= this.constants.clustering.initialMaxNodes) {
- zoomLevel = 49.07548 / (numberOfNodes + 142.05338) + 9.1444e-04; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
- }
- else {
- zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
- }
- }
- else {
- if (this.constants.clustering.enabled == true &&
- numberOfNodes >= this.constants.clustering.initialMaxNodes) {
- zoomLevel = 77.5271985 / (numberOfNodes + 187.266146) + 4.76710517e-05; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
- }
- else {
- zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
- }
- }
+ /**
+ * Do the next step
+ */
+ TimeStep.prototype.next = function() {
+ var prev = this.current.valueOf();
- // correct for larger canvasses.
- var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
- zoomLevel *= factor;
+ // Two cases, needed to prevent issues with switching daylight savings
+ // (end of March and end of October)
+ if (this.current.getMonth() < 6) {
+ switch (this.scale) {
+ case 'millisecond':
+
+ this.current = new Date(this.current.valueOf() + this.step); break;
+ case 'second': this.current = new Date(this.current.valueOf() + this.step * 1000); break;
+ case 'minute': this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
+ case 'hour':
+ this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
+ // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
+ var h = this.current.getHours();
+ this.current.setHours(h - (h % this.step));
+ break;
+ case 'weekday': // intentional fall through
+ case 'day': this.current.setDate(this.current.getDate() + this.step); break;
+ case 'month': this.current.setMonth(this.current.getMonth() + this.step); break;
+ case 'year': this.current.setFullYear(this.current.getFullYear() + this.step); break;
+ default: break;
+ }
}
else {
- var xDistance = Math.abs(range.maxX - range.minX) * 1.1;
- var yDistance = Math.abs(range.maxY - range.minY) * 1.1;
-
- var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
- var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
-
- zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
+ switch (this.scale) {
+ case 'millisecond': this.current = new Date(this.current.valueOf() + this.step); break;
+ case 'second': this.current.setSeconds(this.current.getSeconds() + this.step); break;
+ case 'minute': this.current.setMinutes(this.current.getMinutes() + this.step); break;
+ case 'hour': this.current.setHours(this.current.getHours() + this.step); break;
+ case 'weekday': // intentional fall through
+ case 'day': this.current.setDate(this.current.getDate() + this.step); break;
+ case 'month': this.current.setMonth(this.current.getMonth() + this.step); break;
+ case 'year': this.current.setFullYear(this.current.getFullYear() + this.step); break;
+ default: break;
+ }
}
- if (zoomLevel > 1.0) {
- zoomLevel = 1.0;
+ if (this.step != 1) {
+ // round down to the correct major value
+ switch (this.scale) {
+ case 'millisecond': if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
+ case 'second': if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
+ case 'minute': if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
+ case 'hour': if(this.current.getHours() < this.step) this.current.setHours(0); break;
+ case 'weekday': // intentional fall through
+ case 'day': if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
+ case 'month': if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
+ case 'year': break; // nothing to do for year
+ default: break;
+ }
}
-
- var center = this._findCenter(range);
- if (disableStart == false) {
- var options = {position: center, scale: zoomLevel, animation: animationOptions};
- this.moveTo(options);
- this.moving = true;
- this.start();
- }
- else {
- center.x *= zoomLevel;
- center.y *= zoomLevel;
- center.x -= 0.5 * this.frame.canvas.clientWidth;
- center.y -= 0.5 * this.frame.canvas.clientHeight;
- this._setScale(zoomLevel);
- this._setTranslation(-center.x,-center.y);
+ // safety mechanism: if current time is still unchanged, move to the end
+ if (this.current.valueOf() == prev) {
+ this.current = new Date(this._end.valueOf());
}
+
+ DateUtil.stepOverHiddenDates(this, prev);
};
/**
- * Update the this.nodeIndices with the most recent node index list
- * @private
+ * Get the current datetime
+ * @return {Date} current The current date
*/
- Network.prototype._updateNodeIndexList = function() {
- this._clearNodeIndexList();
- for (var idx in this.nodes) {
- if (this.nodes.hasOwnProperty(idx)) {
- this.nodeIndices.push(idx);
- }
- }
+ TimeStep.prototype.getCurrent = function() {
+ return this.current;
};
-
/**
- * Set nodes and edges, and optionally options as well.
+ * Set a custom scale. Autoscaling will be disabled.
+ * For example setScale(SCALE.MINUTES, 5) will result
+ * in minor steps of 5 minutes, and major steps of an hour.
*
- * @param {Object} data Object containing parameters:
- * {Array | DataSet | DataView} [nodes] Array with nodes
- * {Array | DataSet | DataView} [edges] Array with edges
- * {String} [dot] String containing data in DOT format
- * {String} [gephi] String containing data in gephi JSON format
- * {Options} [options] Object with options
- * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
+ * @param {string} newScale
+ * A scale. Choose from 'millisecond, 'second,
+ * 'minute', 'hour', 'weekday, 'day, 'month, 'year'.
+ * @param {Number} newStep A step size, by default 1. Choose for
+ * example 1, 2, 5, or 10.
*/
- Network.prototype.setData = function(data, disableStart) {
- if (disableStart === undefined) {
- disableStart = false;
- }
- // we set initializing to true to ensure that the hierarchical layout is not performed until both nodes and edges are added.
- this.initializing = true;
+ TimeStep.prototype.setScale = function(newScale, newStep) {
+ this.scale = newScale;
- if (data && data.dot && (data.nodes || data.edges)) {
- throw new SyntaxError('Data must contain either parameter "dot" or ' +
- ' parameter pair "nodes" and "edges", but not both.');
+ if (newStep > 0) {
+ this.step = newStep;
}
- // set options
- this.setOptions(data && data.options);
- // set all data
- if (data && data.dot) {
- // parse DOT file
- if(data && data.dot) {
- var dotData = dotparser.DOTToGraph(data.dot);
- this.setData(dotData);
- return;
- }
- }
- else if (data && data.gephi) {
- // parse DOT file
- if(data && data.gephi) {
- var gephiData = gephiParser.parseGephi(data.gephi);
- this.setData(gephiData);
- return;
- }
- }
- else {
- this._setNodes(data && data.nodes);
- this._setEdges(data && data.edges);
- }
- this._putDataInSector();
- if (disableStart == false) {
- if (this.constants.hierarchicalLayout.enabled == true) {
- this._resetLevels();
- this._setupHierarchicalLayout();
- }
- else {
- // find a stable position or start animating to a stable position
- if (this.constants.stabilize) {
- this._stabilize();
- }
- }
- this.start();
- }
- this.initializing = false;
+ this.autoScale = false;
};
/**
- * Set options
- * @param {Object} options
+ * Enable or disable autoscaling
+ * @param {boolean} enable If true, autoascaling is set true
*/
- Network.prototype.setOptions = function (options) {
- if (options) {
- var prop;
- var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','clustering','navigation',
- 'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse'
- ];
- // extend all but the values in fields
- util.selectiveNotDeepExtend(fields,this.constants, options);
- util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes);
- util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges);
-
- if (options.physics) {
- util.mergeOptions(this.constants.physics, options.physics,'barnesHut');
- util.mergeOptions(this.constants.physics, options.physics,'repulsion');
-
- if (options.physics.hierarchicalRepulsion) {
- this.constants.hierarchicalLayout.enabled = true;
- this.constants.physics.hierarchicalRepulsion.enabled = true;
- this.constants.physics.barnesHut.enabled = false;
- for (prop in options.physics.hierarchicalRepulsion) {
- if (options.physics.hierarchicalRepulsion.hasOwnProperty(prop)) {
- this.constants.physics.hierarchicalRepulsion[prop] = options.physics.hierarchicalRepulsion[prop];
- }
- }
- }
- }
+ TimeStep.prototype.setAutoScale = function (enable) {
+ this.autoScale = enable;
+ };
- if (options.onAdd) {this.triggerFunctions.add = options.onAdd;}
- if (options.onEdit) {this.triggerFunctions.edit = options.onEdit;}
- if (options.onEditEdge) {this.triggerFunctions.editEdge = options.onEditEdge;}
- if (options.onConnect) {this.triggerFunctions.connect = options.onConnect;}
- if (options.onDelete) {this.triggerFunctions.del = options.onDelete;}
- util.mergeOptions(this.constants, options,'smoothCurves');
- util.mergeOptions(this.constants, options,'hierarchicalLayout');
- util.mergeOptions(this.constants, options,'clustering');
- util.mergeOptions(this.constants, options,'navigation');
- util.mergeOptions(this.constants, options,'keyboard');
- util.mergeOptions(this.constants, options,'dataManipulation');
+ /**
+ * Automatically determine the scale that bests fits the provided minimum step
+ * @param {Number} [minimumStep] The minimum step size in milliseconds
+ */
+ TimeStep.prototype.setMinimumStep = function(minimumStep) {
+ if (minimumStep == undefined) {
+ return;
+ }
+ //var b = asc + ds;
- if (options.dataManipulation) {
- this.editMode = this.constants.dataManipulation.initiallyVisible;
- }
+ var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
+ var stepMonth = (1000 * 60 * 60 * 24 * 30);
+ var stepDay = (1000 * 60 * 60 * 24);
+ var stepHour = (1000 * 60 * 60);
+ var stepMinute = (1000 * 60);
+ var stepSecond = (1000);
+ var stepMillisecond= (1);
+ // find the smallest step that is larger than the provided minimumStep
+ if (stepYear*1000 > minimumStep) {this.scale = 'year'; this.step = 1000;}
+ if (stepYear*500 > minimumStep) {this.scale = 'year'; this.step = 500;}
+ if (stepYear*100 > minimumStep) {this.scale = 'year'; this.step = 100;}
+ if (stepYear*50 > minimumStep) {this.scale = 'year'; this.step = 50;}
+ if (stepYear*10 > minimumStep) {this.scale = 'year'; this.step = 10;}
+ if (stepYear*5 > minimumStep) {this.scale = 'year'; this.step = 5;}
+ if (stepYear > minimumStep) {this.scale = 'year'; this.step = 1;}
+ if (stepMonth*3 > minimumStep) {this.scale = 'month'; this.step = 3;}
+ if (stepMonth > minimumStep) {this.scale = 'month'; this.step = 1;}
+ if (stepDay*5 > minimumStep) {this.scale = 'day'; this.step = 5;}
+ if (stepDay*2 > minimumStep) {this.scale = 'day'; this.step = 2;}
+ if (stepDay > minimumStep) {this.scale = 'day'; this.step = 1;}
+ if (stepDay/2 > minimumStep) {this.scale = 'weekday'; this.step = 1;}
+ if (stepHour*4 > minimumStep) {this.scale = 'hour'; this.step = 4;}
+ if (stepHour > minimumStep) {this.scale = 'hour'; this.step = 1;}
+ if (stepMinute*15 > minimumStep) {this.scale = 'minute'; this.step = 15;}
+ if (stepMinute*10 > minimumStep) {this.scale = 'minute'; this.step = 10;}
+ if (stepMinute*5 > minimumStep) {this.scale = 'minute'; this.step = 5;}
+ if (stepMinute > minimumStep) {this.scale = 'minute'; this.step = 1;}
+ if (stepSecond*15 > minimumStep) {this.scale = 'second'; this.step = 15;}
+ if (stepSecond*10 > minimumStep) {this.scale = 'second'; this.step = 10;}
+ if (stepSecond*5 > minimumStep) {this.scale = 'second'; this.step = 5;}
+ if (stepSecond > minimumStep) {this.scale = 'second'; this.step = 1;}
+ if (stepMillisecond*200 > minimumStep) {this.scale = 'millisecond'; this.step = 200;}
+ if (stepMillisecond*100 > minimumStep) {this.scale = 'millisecond'; this.step = 100;}
+ if (stepMillisecond*50 > minimumStep) {this.scale = 'millisecond'; this.step = 50;}
+ if (stepMillisecond*10 > minimumStep) {this.scale = 'millisecond'; this.step = 10;}
+ if (stepMillisecond*5 > minimumStep) {this.scale = 'millisecond'; this.step = 5;}
+ if (stepMillisecond > minimumStep) {this.scale = 'millisecond'; this.step = 1;}
+ };
- // TODO: work out these options and document them
- if (options.edges) {
- if (options.edges.color !== undefined) {
- if (util.isString(options.edges.color)) {
- this.constants.edges.color = {};
- this.constants.edges.color.color = options.edges.color;
- this.constants.edges.color.highlight = options.edges.color;
- this.constants.edges.color.hover = options.edges.color;
- }
- else {
- if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
- if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
- if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;}
- }
- this.constants.edges.inheritColor = false;
- }
+ /**
+ * Snap a date to a rounded value.
+ * The snap intervals are dependent on the current scale and step.
+ * @param {Date} date the date to be snapped.
+ * @return {Date} snappedDate
+ */
+ TimeStep.prototype.snap = function(date) {
+ var clone = new Date(date.valueOf());
- if (!options.edges.fontColor) {
- if (options.edges.color !== undefined) {
- if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
- else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
- }
- }
+ if (this.scale == 'year') {
+ var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
+ clone.setFullYear(Math.round(year / this.step) * this.step);
+ clone.setMonth(0);
+ clone.setDate(0);
+ clone.setHours(0);
+ clone.setMinutes(0);
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
+ }
+ else if (this.scale == 'month') {
+ if (clone.getDate() > 15) {
+ clone.setDate(1);
+ clone.setMonth(clone.getMonth() + 1);
+ // important: first set Date to 1, after that change the month.
+ }
+ else {
+ clone.setDate(1);
}
- if (options.nodes) {
- if (options.nodes.color) {
- var newColorObj = util.parseColor(options.nodes.color);
- this.constants.nodes.color.background = newColorObj.background;
- this.constants.nodes.color.border = newColorObj.border;
- this.constants.nodes.color.highlight.background = newColorObj.highlight.background;
- this.constants.nodes.color.highlight.border = newColorObj.highlight.border;
- this.constants.nodes.color.hover.background = newColorObj.hover.background;
- this.constants.nodes.color.hover.border = newColorObj.hover.border;
- }
+ clone.setHours(0);
+ clone.setMinutes(0);
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
+ }
+ else if (this.scale == 'day') {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 5:
+ case 2:
+ clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
+ default:
+ clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
}
- if (options.groups) {
- for (var groupname in options.groups) {
- if (options.groups.hasOwnProperty(groupname)) {
- var group = options.groups[groupname];
- this.groups.add(groupname, group);
- }
- }
+ clone.setMinutes(0);
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
+ }
+ else if (this.scale == 'weekday') {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 5:
+ case 2:
+ clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
+ default:
+ clone.setHours(Math.round(clone.getHours() / 6) * 6); break;
}
-
- if (options.tooltip) {
- for (prop in options.tooltip) {
- if (options.tooltip.hasOwnProperty(prop)) {
- this.constants.tooltip[prop] = options.tooltip[prop];
- }
- }
- if (options.tooltip.color) {
- this.constants.tooltip.color = util.parseColor(options.tooltip.color);
- }
+ clone.setMinutes(0);
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
+ }
+ else if (this.scale == 'hour') {
+ switch (this.step) {
+ case 4:
+ clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
+ default:
+ clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
}
-
- if ('clickToUse' in options) {
- if (options.clickToUse) {
- if (!this.activator) {
- this.activator = new Activator(this.frame);
- this.activator.on('change', this._createKeyBinds.bind(this));
- }
- }
- else {
- if (this.activator) {
- this.activator.destroy();
- delete this.activator;
- }
- }
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
+ } else if (this.scale == 'minute') {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 15:
+ case 10:
+ clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
+ clone.setSeconds(0);
+ break;
+ case 5:
+ clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
+ default:
+ clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
}
-
- if (options.labels) {
- throw new Error('Option "labels" is deprecated. Use options "locale" and "locales" instead.');
+ clone.setMilliseconds(0);
+ }
+ else if (this.scale == 'second') {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 15:
+ case 10:
+ clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
+ clone.setMilliseconds(0);
+ break;
+ case 5:
+ clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
+ default:
+ clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
}
-
- // (Re)loading the mixins that can be enabled or disabled in the options.
- // load the force calculation functions, grouped under the physics system.
- this._loadPhysicsSystem();
- // load the navigation system.
- this._loadNavigationControls();
- // load the data manipulation system
- this._loadManipulationSystem();
- // configure the smooth curves
- this._configureSmoothCurves();
-
-
- // bind keys. If disabled, this will not do anything;
- this._createKeyBinds();
-
- this.setSize(this.constants.width, this.constants.height);
- this.moving = true;
- this.start();
}
+ else if (this.scale == 'millisecond') {
+ var step = this.step > 5 ? this.step / 2 : 1;
+ clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
+ }
+
+ return clone;
};
-
-
/**
- * Create the main frame for the Network.
- * This function is executed once when a Network object is created. The frame
- * contains a canvas, and this canvas contains all objects like the axis and
- * nodes.
- * @private
+ * Check if the current value is a major value (for example when the step
+ * is DAY, a major value is each first day of the MONTH)
+ * @return {boolean} true if current date is major, else false.
*/
- Network.prototype._create = function () {
+ TimeStep.prototype.isMajor = function() {
+ if (this.switchedYear == true) {
+ this.switchedYear = false;
+ switch (this.scale) {
+ case 'year':
+ case 'month':
+ case 'weekday':
+ case 'day':
+ case 'hour':
+ case 'minute':
+ case 'second':
+ case 'millisecond':
+ return true;
+ default:
+ return false;
+ }
+ }
+ else if (this.switchedMonth == true) {
+ this.switchedMonth = false;
+ switch (this.scale) {
+ case 'weekday':
+ case 'day':
+ case 'hour':
+ case 'minute':
+ case 'second':
+ case 'millisecond':
+ return true;
+ default:
+ return false;
+ }
+ }
+ else if (this.switchedDay == true) {
+ this.switchedDay = false;
+ switch (this.scale) {
+ case 'millisecond':
+ case 'second':
+ case 'minute':
+ case 'hour':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ switch (this.scale) {
+ case 'millisecond':
+ return (this.current.getMilliseconds() == 0);
+ case 'second':
+ return (this.current.getSeconds() == 0);
+ case 'minute':
+ return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
+ case 'hour':
+ return (this.current.getHours() == 0);
+ case 'weekday': // intentional fall through
+ case 'day':
+ return (this.current.getDate() == 1);
+ case 'month':
+ return (this.current.getMonth() == 0);
+ case 'year':
+ return false;
+ default:
+ return false;
+ }
+ };
+
+
+ /**
+ * Returns formatted text for the minor axislabel, depending on the current
+ * date and the scale. For example when scale is MINUTE, the current time is
+ * formatted as "hh:mm".
+ * @param {Date} [date] custom date. if not provided, current date is taken
+ */
+ TimeStep.prototype.getLabelMinor = function(date) {
+ if (date == undefined) {
+ date = this.current;
+ }
+
+ var format = this.format.minorLabels[this.scale];
+ return (format && format.length > 0) ? moment(date).format(format) : '';
+ };
+
+ /**
+ * Returns formatted text for the major axis label, depending on the current
+ * date and the scale. For example when scale is MINUTE, the major scale is
+ * hours, and the hour will be formatted as "hh".
+ * @param {Date} [date] custom date. if not provided, current date is taken
+ */
+ TimeStep.prototype.getLabelMajor = function(date) {
+ if (date == undefined) {
+ date = this.current;
+ }
+
+ var format = this.format.majorLabels[this.scale];
+ return (format && format.length > 0) ? moment(date).format(format) : '';
+ };
+
+ module.exports = TimeStep;
+
+
+/***/ },
+/* 51 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Emitter = __webpack_require__(11);
+ var Hammer = __webpack_require__(19);
+ var keycharm = __webpack_require__(36);
+ var util = __webpack_require__(1);
+ var hammerUtil = __webpack_require__(22);
+ var DataSet = __webpack_require__(7);
+ var DataView = __webpack_require__(9);
+ var dotparser = __webpack_require__(52);
+ var gephiParser = __webpack_require__(53);
+ var Groups = __webpack_require__(54);
+ var Images = __webpack_require__(55);
+ var Node = __webpack_require__(56);
+ var Edge = __webpack_require__(57);
+ var Popup = __webpack_require__(58);
+ var MixinLoader = __webpack_require__(59);
+ var Activator = __webpack_require__(35);
+ var locales = __webpack_require__(70);
+
+ // Load custom shapes into CanvasRenderingContext2D
+ __webpack_require__(71);
+
+ /**
+ * @constructor Network
+ * Create a network visualization, displaying nodes and edges.
+ *
+ * @param {Element} container The DOM element in which the Network will
+ * be created. Normally a div element.
+ * @param {Object} data An object containing parameters
+ * {Array} nodes
+ * {Array} edges
+ * @param {Object} options Options
+ */
+ function Network (container, data, options) {
+ if (!(this instanceof Network)) {
+ throw new SyntaxError('Constructor must be called with the new operator');
+ }
+
+ this._determineBrowserMethod();
+ this._initializeMixinLoaders();
+
+ // create variables and set default values
+ this.containerElement = container;
+
+ // render and calculation settings
+ this.renderRefreshRate = 60; // hz (fps)
+ this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
+ this.renderTime = 0; // measured time it takes to render a frame
+ this.physicsTime = 0; // measured time it takes to render a frame
+ this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation
+
+ this.initializing = true;
+
+ this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
+
+ // set constant values
+ this.defaultOptions = {
+ nodes: {
+ mass: 1,
+ radiusMin: 10,
+ radiusMax: 30,
+ radius: 10,
+ shape: 'ellipse',
+ image: undefined,
+ widthMin: 16, // px
+ widthMax: 64, // px
+ fontColor: 'black',
+ fontSize: 14, // px
+ fontFace: 'verdana',
+ fontFill: undefined,
+ level: -1,
+ color: {
+ border: '#2B7CE9',
+ background: '#97C2FC',
+ highlight: {
+ border: '#2B7CE9',
+ background: '#D2E5FF'
+ },
+ hover: {
+ border: '#2B7CE9',
+ background: '#D2E5FF'
+ }
+ },
+ group: undefined,
+ borderWidth: 1,
+ borderWidthSelected: undefined
+ },
+ edges: {
+ widthMin: 1, //
+ widthMax: 15,//
+ width: 1,
+ widthSelectionMultiplier: 2,
+ hoverWidth: 1.5,
+ style: 'line',
+ color: {
+ color:'#848484',
+ highlight:'#848484',
+ hover: '#848484'
+ },
+ fontColor: '#343434',
+ fontSize: 14, // px
+ fontFace: 'arial',
+ fontFill: 'white',
+ arrowScaleFactor: 1,
+ dash: {
+ length: 10,
+ gap: 5,
+ altLength: undefined
+ },
+ inheritColor: "from" // to, from, false, true (== from)
+ },
+ configurePhysics:false,
+ physics: {
+ barnesHut: {
+ enabled: true,
+ thetaInverted: 1 / 0.5, // inverted to save time during calculation
+ gravitationalConstant: -2000,
+ centralGravity: 0.3,
+ springLength: 95,
+ springConstant: 0.04,
+ damping: 0.09
+ },
+ repulsion: {
+ centralGravity: 0.0,
+ springLength: 200,
+ springConstant: 0.05,
+ nodeDistance: 100,
+ damping: 0.09
+ },
+ hierarchicalRepulsion: {
+ enabled: false,
+ centralGravity: 0.0,
+ springLength: 100,
+ springConstant: 0.01,
+ nodeDistance: 150,
+ damping: 0.09
+ },
+ damping: null,
+ centralGravity: null,
+ springLength: null,
+ springConstant: null
+ },
+ clustering: { // Per Node in Cluster = PNiC
+ enabled: false, // (Boolean) | global on/off switch for clustering.
+ initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
+ clusterThreshold:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToNodes
+ reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this
+ chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
+ clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
+ sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
+ screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node.
+ fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
+ maxFontSize: 1000,
+ forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
+ distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
+ edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
+ nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
+ height: 1, // (px PNiC) | growth of the height per node in cluster.
+ radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
+ maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
+ activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
+ clusterLevelDifference: 2
+ },
+ navigation: {
+ enabled: false
+ },
+ keyboard: {
+ enabled: false,
+ speed: {x: 10, y: 10, zoom: 0.02}
+ },
+ dataManipulation: {
+ enabled: false,
+ initiallyVisible: false
+ },
+ hierarchicalLayout: {
+ enabled:false,
+ levelSeparation: 150,
+ nodeSpacing: 100,
+ direction: "UD", // UD, DU, LR, RL
+ layout: "hubsize" // hubsize, directed
+ },
+ freezeForStabilization: false,
+ smoothCurves: {
+ enabled: true,
+ dynamic: true,
+ type: "continuous",
+ roundness: 0.5
+ },
+ maxVelocity: 30,
+ minVelocity: 0.1, // px/s
+ stabilize: true, // stabilize before displaying the network
+ stabilizationIterations: 1000, // maximum number of iteration to stabilize
+ zoomExtentOnStabilize: true,
+ locale: 'en',
+ locales: locales,
+ tooltip: {
+ delay: 300,
+ fontColor: 'black',
+ fontSize: 14, // px
+ fontFace: 'verdana',
+ color: {
+ border: '#666',
+ background: '#FFFFC6'
+ }
+ },
+ dragNetwork: true,
+ dragNodes: true,
+ zoomable: true,
+ hover: false,
+ hideEdgesOnDrag: false,
+ hideNodesOnDrag: false,
+ width : '100%',
+ height : '100%',
+ selectable: true
+ };
+ this.constants = util.extend({}, this.defaultOptions);
+ this.pixelRatio = 1;
+
+
+ this.hoverObj = {nodes:{},edges:{}};
+ this.controlNodesActive = false;
+ this.navigationHammers = {existing:[], _new: []};
+
+ // animation properties
+ this.animationSpeed = 1/this.renderRefreshRate;
+ this.animationEasingFunction = "easeInOutQuint";
+ this.easingTime = 0;
+ this.sourceScale = 0;
+ this.targetScale = 0;
+ this.sourceTranslation = 0;
+ this.targetTranslation = 0;
+ this.lockedOnNodeId = null;
+ this.lockedOnNodeOffset = null;
+ this.touchTime = 0;
+
+ // Node variables
+ var network = this;
+ this.groups = new Groups(); // object with groups
+ this.images = new Images(); // object with images
+ this.images.setOnloadCallback(function () {
+ network._redraw();
+ });
+
+ // keyboard navigation variables
+ this.xIncrement = 0;
+ this.yIncrement = 0;
+ this.zoomIncrement = 0;
+
+ // loading all the mixins:
+ // load the force calculation functions, grouped under the physics system.
+ this._loadPhysicsSystem();
+ // create a frame and canvas
+ this._create();
+ // load the sector system. (mandatory, fully integrated with Network)
+ this._loadSectorSystem();
+ // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
+ this._loadClusterSystem();
+ // load the selection system. (mandatory, required by Network)
+ this._loadSelectionSystem();
+ // load the selection system. (mandatory, required by Network)
+ this._loadHierarchySystem();
+
+
+ // apply options
+ this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
+ this._setScale(1);
+ this.setOptions(options);
+
+ // other vars
+ this.freezeSimulation = false;// freeze the simulation
+ this.cachedFunctions = {};
+ this.startedStabilization = false;
+ this.stabilized = false;
+ this.stabilizationIterations = null;
+ this.draggingNodes = false;
+
+ // containers for nodes and edges
+ this.calculationNodes = {};
+ this.calculationNodeIndices = [];
+ this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
+ this.nodes = {}; // object with Node objects
+ this.edges = {}; // object with Edge objects
+
+ // position and scale variables and objects
+ this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
+ this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
+ this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
+ this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
+ this.scale = 1; // defining the global scale variable in the constructor
+ this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
+
+ // datasets or dataviews
+ this.nodesData = null; // A DataSet or DataView
+ this.edgesData = null; // A DataSet or DataView
+
+ // create event listeners used to subscribe on the DataSets of the nodes and edges
+ this.nodesListeners = {
+ 'add': function (event, params) {
+ network._addNodes(params.items);
+ network.start();
+ },
+ 'update': function (event, params) {
+ network._updateNodes(params.items, params.data);
+ network.start();
+ },
+ 'remove': function (event, params) {
+ network._removeNodes(params.items);
+ network.start();
+ }
+ };
+ this.edgesListeners = {
+ 'add': function (event, params) {
+ network._addEdges(params.items);
+ network.start();
+ },
+ 'update': function (event, params) {
+ network._updateEdges(params.items);
+ network.start();
+ },
+ 'remove': function (event, params) {
+ network._removeEdges(params.items);
+ network.start();
+ }
+ };
+
+ // properties for the animation
+ this.moving = true;
+ this.timer = undefined; // Scheduling function. Is definded in this.start();
+
+ // load data (the disable start variable will be the same as the enabled clustering)
+ this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
+
+ // hierarchical layout
+ this.initializing = false;
+ if (this.constants.hierarchicalLayout.enabled == true) {
+ this._setupHierarchicalLayout();
+ }
+ else {
+ // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
+ if (this.constants.stabilize == false) {
+ this.zoomExtent(undefined, true,this.constants.clustering.enabled);
+ }
+ }
+
+ // if clustering is disabled, the simulation will have started in the setData function
+ if (this.constants.clustering.enabled) {
+ this.startWithClustering();
+ }
+ }
+
+ // Extend Network with an Emitter mixin
+ Emitter(Network.prototype);
+
+
+ Network.prototype._determineBrowserMethod = function() {
+ var ua = navigator.userAgent.toLowerCase();
+
+ this.requiresTimeout = false;
+ if (ua.indexOf('msie 9.0') != -1) { // IE 9
+ this.requiresTimeout = true;
+ }
+ else if (ua.indexOf('safari') != -1) { // safari
+ if (ua.indexOf('chrome') <= -1) {
+ this.requiresTimeout = true;
+ }
+ }
+ }
+
+
+ /**
+ * Get the script path where the vis.js library is located
+ *
+ * @returns {string | null} path Path or null when not found. Path does not
+ * end with a slash.
+ * @private
+ */
+ Network.prototype._getScriptPath = function() {
+ var scripts = document.getElementsByTagName( 'script' );
+
+ // find script named vis.js or vis.min.js
+ for (var i = 0; i < scripts.length; i++) {
+ var src = scripts[i].src;
+ var match = src && /\/?vis(.min)?\.js$/.exec(src);
+ if (match) {
+ // return path without the script name
+ return src.substring(0, src.length - match[0].length);
+ }
+ }
+
+ return null;
+ };
+
+
+ /**
+ * Find the center position of the network
+ * @private
+ */
+ Network.prototype._getRange = function() {
+ var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (minX > (node.boundingBox.left)) {minX = node.boundingBox.left;}
+ if (maxX < (node.boundingBox.right)) {maxX = node.boundingBox.right;}
+ if (minY > (node.boundingBox.bottom)) {minY = node.boundingBox.bottom;}
+ if (maxY < (node.boundingBox.top)) {maxY = node.boundingBox.top;}
+ }
+ }
+ if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
+ minY = 0, maxY = 0, minX = 0, maxX = 0;
+ }
+ return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
+ };
+
+
+ /**
+ * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
+ * @returns {{x: number, y: number}}
+ * @private
+ */
+ Network.prototype._findCenter = function(range) {
+ return {x: (0.5 * (range.maxX + range.minX)),
+ y: (0.5 * (range.maxY + range.minY))};
+ };
+
+
+ /**
+ * This function zooms out to fit all data on screen based on amount of nodes
+ *
+ * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
+ * @param {Boolean} [disableStart] | If true, start is not called.
+ */
+ Network.prototype.zoomExtent = function(animationOptions, initialZoom, disableStart) {
+ this._redraw(true);
+
+ if (initialZoom === undefined) {
+ initialZoom = false;
+ }
+ if (disableStart === undefined) {
+ disableStart = false;
+ }
+ if (animationOptions === undefined) {
+ animationOptions = false;
+ }
+
+ var range = this._getRange();
+ var zoomLevel;
+
+ if (initialZoom == true) {
+ var numberOfNodes = this.nodeIndices.length;
+ if (this.constants.smoothCurves == true) {
+ if (this.constants.clustering.enabled == true &&
+ numberOfNodes >= this.constants.clustering.initialMaxNodes) {
+ zoomLevel = 49.07548 / (numberOfNodes + 142.05338) + 9.1444e-04; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ }
+ else {
+ zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ }
+ }
+ else {
+ if (this.constants.clustering.enabled == true &&
+ numberOfNodes >= this.constants.clustering.initialMaxNodes) {
+ zoomLevel = 77.5271985 / (numberOfNodes + 187.266146) + 4.76710517e-05; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ }
+ else {
+ zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ }
+ }
+
+ // correct for larger canvasses.
+ var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
+ zoomLevel *= factor;
+ }
+ else {
+ var xDistance = Math.abs(range.maxX - range.minX) * 1.1;
+ var yDistance = Math.abs(range.maxY - range.minY) * 1.1;
+
+ var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
+ var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
+
+ zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
+ }
+
+ if (zoomLevel > 1.0) {
+ zoomLevel = 1.0;
+ }
+
+
+ var center = this._findCenter(range);
+ if (disableStart == false) {
+ var options = {position: center, scale: zoomLevel, animation: animationOptions};
+ this.moveTo(options);
+ this.moving = true;
+ this.start();
+ }
+ else {
+ center.x *= zoomLevel;
+ center.y *= zoomLevel;
+ center.x -= 0.5 * this.frame.canvas.clientWidth;
+ center.y -= 0.5 * this.frame.canvas.clientHeight;
+ this._setScale(zoomLevel);
+ this._setTranslation(-center.x,-center.y);
+ }
+ };
+
+
+ /**
+ * Update the this.nodeIndices with the most recent node index list
+ * @private
+ */
+ Network.prototype._updateNodeIndexList = function() {
+ this._clearNodeIndexList();
+ for (var idx in this.nodes) {
+ if (this.nodes.hasOwnProperty(idx)) {
+ this.nodeIndices.push(idx);
+ }
+ }
+ };
+
+
+ /**
+ * Set nodes and edges, and optionally options as well.
+ *
+ * @param {Object} data Object containing parameters:
+ * {Array | DataSet | DataView} [nodes] Array with nodes
+ * {Array | DataSet | DataView} [edges] Array with edges
+ * {String} [dot] String containing data in DOT format
+ * {String} [gephi] String containing data in gephi JSON format
+ * {Options} [options] Object with options
+ * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
+ */
+ Network.prototype.setData = function(data, disableStart) {
+ if (disableStart === undefined) {
+ disableStart = false;
+ }
+ // we set initializing to true to ensure that the hierarchical layout is not performed until both nodes and edges are added.
+ this.initializing = true;
+
+ if (data && data.dot && (data.nodes || data.edges)) {
+ throw new SyntaxError('Data must contain either parameter "dot" or ' +
+ ' parameter pair "nodes" and "edges", but not both.');
+ }
+
+ // set options
+ this.setOptions(data && data.options);
+ // set all data
+ if (data && data.dot) {
+ // parse DOT file
+ if(data && data.dot) {
+ var dotData = dotparser.DOTToGraph(data.dot);
+ this.setData(dotData);
+ return;
+ }
+ }
+ else if (data && data.gephi) {
+ // parse DOT file
+ if(data && data.gephi) {
+ var gephiData = gephiParser.parseGephi(data.gephi);
+ this.setData(gephiData);
+ return;
+ }
+ }
+ else {
+ this._setNodes(data && data.nodes);
+ this._setEdges(data && data.edges);
+ }
+ this._putDataInSector();
+ if (disableStart == false) {
+ if (this.constants.hierarchicalLayout.enabled == true) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ else {
+ // find a stable position or start animating to a stable position
+ if (this.constants.stabilize) {
+ this._stabilize();
+ }
+ }
+ this.start();
+ }
+ this.initializing = false;
+ };
+
+ /**
+ * Set options
+ * @param {Object} options
+ */
+ Network.prototype.setOptions = function (options) {
+ if (options) {
+ var prop;
+ var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','clustering','navigation',
+ 'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse'
+ ];
+ // extend all but the values in fields
+ util.selectiveNotDeepExtend(fields,this.constants, options);
+ util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes);
+ util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges);
+
+ if (options.physics) {
+ util.mergeOptions(this.constants.physics, options.physics,'barnesHut');
+ util.mergeOptions(this.constants.physics, options.physics,'repulsion');
+
+ if (options.physics.hierarchicalRepulsion) {
+ this.constants.hierarchicalLayout.enabled = true;
+ this.constants.physics.hierarchicalRepulsion.enabled = true;
+ this.constants.physics.barnesHut.enabled = false;
+ for (prop in options.physics.hierarchicalRepulsion) {
+ if (options.physics.hierarchicalRepulsion.hasOwnProperty(prop)) {
+ this.constants.physics.hierarchicalRepulsion[prop] = options.physics.hierarchicalRepulsion[prop];
+ }
+ }
+ }
+ }
+
+ if (options.onAdd) {this.triggerFunctions.add = options.onAdd;}
+ if (options.onEdit) {this.triggerFunctions.edit = options.onEdit;}
+ if (options.onEditEdge) {this.triggerFunctions.editEdge = options.onEditEdge;}
+ if (options.onConnect) {this.triggerFunctions.connect = options.onConnect;}
+ if (options.onDelete) {this.triggerFunctions.del = options.onDelete;}
+
+ util.mergeOptions(this.constants, options,'smoothCurves');
+ util.mergeOptions(this.constants, options,'hierarchicalLayout');
+ util.mergeOptions(this.constants, options,'clustering');
+ util.mergeOptions(this.constants, options,'navigation');
+ util.mergeOptions(this.constants, options,'keyboard');
+ util.mergeOptions(this.constants, options,'dataManipulation');
+
+
+ if (options.dataManipulation) {
+ this.editMode = this.constants.dataManipulation.initiallyVisible;
+ }
+
+
+ // TODO: work out these options and document them
+ if (options.edges) {
+ if (options.edges.color !== undefined) {
+ if (util.isString(options.edges.color)) {
+ this.constants.edges.color = {};
+ this.constants.edges.color.color = options.edges.color;
+ this.constants.edges.color.highlight = options.edges.color;
+ this.constants.edges.color.hover = options.edges.color;
+ }
+ else {
+ if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
+ if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
+ if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;}
+ }
+ this.constants.edges.inheritColor = false;
+ }
+
+ if (!options.edges.fontColor) {
+ if (options.edges.color !== undefined) {
+ if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
+ else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
+ }
+ }
+ }
+
+ if (options.nodes) {
+ if (options.nodes.color) {
+ var newColorObj = util.parseColor(options.nodes.color);
+ this.constants.nodes.color.background = newColorObj.background;
+ this.constants.nodes.color.border = newColorObj.border;
+ this.constants.nodes.color.highlight.background = newColorObj.highlight.background;
+ this.constants.nodes.color.highlight.border = newColorObj.highlight.border;
+ this.constants.nodes.color.hover.background = newColorObj.hover.background;
+ this.constants.nodes.color.hover.border = newColorObj.hover.border;
+ }
+ }
+ if (options.groups) {
+ for (var groupname in options.groups) {
+ if (options.groups.hasOwnProperty(groupname)) {
+ var group = options.groups[groupname];
+ this.groups.add(groupname, group);
+ }
+ }
+ }
+
+ if (options.tooltip) {
+ for (prop in options.tooltip) {
+ if (options.tooltip.hasOwnProperty(prop)) {
+ this.constants.tooltip[prop] = options.tooltip[prop];
+ }
+ }
+ if (options.tooltip.color) {
+ this.constants.tooltip.color = util.parseColor(options.tooltip.color);
+ }
+ }
+
+ if ('clickToUse' in options) {
+ if (options.clickToUse) {
+ if (!this.activator) {
+ this.activator = new Activator(this.frame);
+ this.activator.on('change', this._createKeyBinds.bind(this));
+ }
+ }
+ else {
+ if (this.activator) {
+ this.activator.destroy();
+ delete this.activator;
+ }
+ }
+ }
+
+ if (options.labels) {
+ throw new Error('Option "labels" is deprecated. Use options "locale" and "locales" instead.');
+ }
+
+ // (Re)loading the mixins that can be enabled or disabled in the options.
+ // load the force calculation functions, grouped under the physics system.
+ this._loadPhysicsSystem();
+ // load the navigation system.
+ this._loadNavigationControls();
+ // load the data manipulation system
+ this._loadManipulationSystem();
+ // configure the smooth curves
+ this._configureSmoothCurves();
+
+
+ // bind keys. If disabled, this will not do anything;
+ this._createKeyBinds();
+
+ this.setSize(this.constants.width, this.constants.height);
+ this.moving = true;
+ this.start();
+ }
+ };
+
+
+
+ /**
+ * Create the main frame for the Network.
+ * This function is executed once when a Network object is created. The frame
+ * contains a canvas, and this canvas contains all objects like the axis and
+ * nodes.
+ * @private
+ */
+ Network.prototype._create = function () {
// remove all elements from the container element.
while (this.containerElement.hasChildNodes()) {
this.containerElement.removeChild(this.containerElement.firstChild);
@@ -25144,1219 +25138,1045 @@ return /******/ (function(modules) { // webpackBootstrap
/* 52 */
/***/ function(module, exports, __webpack_require__) {
- var util = __webpack_require__(1);
- var Node = __webpack_require__(53);
-
/**
- * @class Edge
+ * Parse a text source containing data in DOT language into a JSON object.
+ * The object contains two lists: one with nodes and one with edges.
*
- * A edge connects two nodes
- * @param {Object} properties Object with properties. Must contain
- * At least properties from and to.
- * Available properties: from (number),
- * to (number), label (string, color (string),
- * width (number), style (string),
- * length (number), title (string)
- * @param {Network} network A Network object, used to find and edge to
- * nodes.
- * @param {Object} constants An object with default values for
- * example for the color
+ * DOT language reference: http://www.graphviz.org/doc/info/lang.html
+ *
+ * @param {String} data Text containing a graph in DOT-notation
+ * @return {Object} graph An object containing two parameters:
+ * {Object[]} nodes
+ * {Object[]} edges
*/
- function Edge (properties, network, networkConstants) {
- if (!network) {
- throw "No network provided";
- }
- var fields = ['edges','physics'];
- var constants = util.selectiveBridgeObject(fields,networkConstants);
- this.options = constants.edges;
- this.physics = constants.physics;
- this.options['smoothCurves'] = networkConstants['smoothCurves'];
-
-
- this.network = network;
+ function parseDOT (data) {
+ dot = data;
+ return parseGraph();
+ }
- // initialize variables
- this.id = undefined;
- this.fromId = undefined;
- this.toId = undefined;
- this.title = undefined;
- this.widthSelected = this.options.width * this.options.widthSelectionMultiplier;
- this.value = undefined;
- this.selected = false;
- this.hover = false;
- this.labelDimensions = {top:0,left:0,width:0,height:0,yLine:0}; // could be cached
- this.dirtyLabel = true;
+ // token types enumeration
+ var TOKENTYPE = {
+ NULL : 0,
+ DELIMITER : 1,
+ IDENTIFIER: 2,
+ UNKNOWN : 3
+ };
- this.from = null; // a node
- this.to = null; // a node
- this.via = null; // a temp node
+ // map with all delimiters
+ var DELIMITERS = {
+ '{': true,
+ '}': true,
+ '[': true,
+ ']': true,
+ ';': true,
+ '=': true,
+ ',': true,
- this.fromBackup = null; // used to clean up after reconnect
- this.toBackup = null;; // used to clean up after reconnect
+ '->': true,
+ '--': true
+ };
- // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
- // by storing the original information we can revert to the original connection when the cluser is opened.
- this.originalFromId = [];
- this.originalToId = [];
+ var dot = ''; // current dot file
+ var index = 0; // current index in dot file
+ var c = ''; // current token character in expr
+ var token = ''; // current token
+ var tokenType = TOKENTYPE.NULL; // type of the token
- this.connected = false;
+ /**
+ * Get the first character from the dot file.
+ * The character is stored into the char c. If the end of the dot file is
+ * reached, the function puts an empty string in c.
+ */
+ function first() {
+ index = 0;
+ c = dot.charAt(0);
+ }
- this.widthFixed = false;
- this.lengthFixed = false;
+ /**
+ * Get the next character from the dot file.
+ * The character is stored into the char c. If the end of the dot file is
+ * reached, the function puts an empty string in c.
+ */
+ function next() {
+ index++;
+ c = dot.charAt(index);
+ }
- this.setProperties(properties);
+ /**
+ * Preview the next character from the dot file.
+ * @return {String} cNext
+ */
+ function nextPreview() {
+ return dot.charAt(index + 1);
+ }
- this.controlNodesEnabled = false;
- this.controlNodes = {from:null, to:null, positions:{}};
- this.connectedNode = null;
+ /**
+ * Test whether given character is alphabetic or numeric
+ * @param {String} c
+ * @return {Boolean} isAlphaNumeric
+ */
+ var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
+ function isAlphaNumeric(c) {
+ return regexAlphaNumeric.test(c);
}
/**
- * Set or overwrite properties for the edge
- * @param {Object} properties an object with properties
- * @param {Object} constants and object with default, global properties
+ * Merge all properties of object b into object b
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Object} a
*/
- Edge.prototype.setProperties = function(properties) {
- if (!properties) {
- return;
+ function merge (a, b) {
+ if (!a) {
+ a = {};
}
- var fields = ['style','fontSize','fontFace','fontColor','fontFill','width',
- 'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor'
- ];
- util.selectiveDeepExtend(fields, this.options, properties);
-
- if (properties.from !== undefined) {this.fromId = properties.from;}
- if (properties.to !== undefined) {this.toId = properties.to;}
-
- if (properties.id !== undefined) {this.id = properties.id;}
- if (properties.label !== undefined) {this.label = properties.label; this.dirtyLabel = true;}
-
- if (properties.title !== undefined) {this.title = properties.title;}
- if (properties.value !== undefined) {this.value = properties.value;}
- if (properties.length !== undefined) {this.physics.springLength = properties.length;}
-
- if (properties.color !== undefined) {
- this.options.inheritColor = false;
- if (util.isString(properties.color)) {
- this.options.color.color = properties.color;
- this.options.color.highlight = properties.color;
- }
- else {
- if (properties.color.color !== undefined) {this.options.color.color = properties.color.color;}
- if (properties.color.highlight !== undefined) {this.options.color.highlight = properties.color.highlight;}
- if (properties.color.hover !== undefined) {this.options.color.hover = properties.color.hover;}
+ if (b) {
+ for (var name in b) {
+ if (b.hasOwnProperty(name)) {
+ a[name] = b[name];
+ }
}
}
-
- // A node is connected when it has a from and to node.
- this.connect();
-
- this.widthFixed = this.widthFixed || (properties.width !== undefined);
- this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
-
- this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
-
- // set draw method based on style
- switch (this.options.style) {
- case 'line': this.draw = this._drawLine; break;
- case 'arrow': this.draw = this._drawArrow; break;
- case 'arrow-center': this.draw = this._drawArrowCenter; break;
- case 'dash-line': this.draw = this._drawDashLine; break;
- default: this.draw = this._drawLine; break;
- }
- };
+ return a;
+ }
/**
- * Connect an edge to its nodes
+ * Set a value in an object, where the provided parameter name can be a
+ * path with nested parameters. For example:
+ *
+ * var obj = {a: 2};
+ * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
+ *
+ * @param {Object} obj
+ * @param {String} path A parameter name or dot-separated parameter path,
+ * like "color.highlight.border".
+ * @param {*} value
*/
- Edge.prototype.connect = function () {
- this.disconnect();
-
- this.from = this.network.nodes[this.fromId] || null;
- this.to = this.network.nodes[this.toId] || null;
- this.connected = (this.from && this.to);
-
- if (this.connected) {
- this.from.attachEdge(this);
- this.to.attachEdge(this);
- }
- else {
- if (this.from) {
- this.from.detachEdge(this);
+ function setValue(obj, path, value) {
+ var keys = path.split('.');
+ var o = obj;
+ while (keys.length) {
+ var key = keys.shift();
+ if (keys.length) {
+ // this isn't the end point
+ if (!o[key]) {
+ o[key] = {};
+ }
+ o = o[key];
}
- if (this.to) {
- this.to.detachEdge(this);
+ else {
+ // this is the end point
+ o[key] = value;
}
}
- };
-
- /**
- * Disconnect an edge from its nodes
- */
- Edge.prototype.disconnect = function () {
- if (this.from) {
- this.from.detachEdge(this);
- this.from = null;
- }
- if (this.to) {
- this.to.detachEdge(this);
- this.to = null;
- }
-
- this.connected = false;
- };
-
- /**
- * get the title of this edge.
- * @return {string} title The title of the edge, or undefined when no title
- * has been set.
- */
- Edge.prototype.getTitle = function() {
- return typeof this.title === "function" ? this.title() : this.title;
- };
-
+ }
/**
- * Retrieve the value of the edge. Can be undefined
- * @return {Number} value
+ * Add a node to a graph object. If there is already a node with
+ * the same id, their attributes will be merged.
+ * @param {Object} graph
+ * @param {Object} node
*/
- Edge.prototype.getValue = function() {
- return this.value;
- };
+ function addNode(graph, node) {
+ var i, len;
+ var current = null;
- /**
- * Adjust the value range of the edge. The edge will adjust it's width
- * based on its value.
- * @param {Number} min
- * @param {Number} max
- */
- Edge.prototype.setValueRange = function(min, max) {
- if (!this.widthFixed && this.value !== undefined) {
- var scale = (this.options.widthMax - this.options.widthMin) / (max - min);
- this.options.width= (this.value - min) * scale + this.options.widthMin;
- this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
+ // find root graph (in case of subgraph)
+ var graphs = [graph]; // list with all graphs from current graph to root graph
+ var root = graph;
+ while (root.parent) {
+ graphs.push(root.parent);
+ root = root.parent;
}
- };
- /**
- * Redraw a edge
- * Draw this edge in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- */
- Edge.prototype.draw = function(ctx) {
- throw "Method draw not initialized in edge";
- };
+ // find existing node (at root level) by its id
+ if (root.nodes) {
+ for (i = 0, len = root.nodes.length; i < len; i++) {
+ if (node.id === root.nodes[i].id) {
+ current = root.nodes[i];
+ break;
+ }
+ }
+ }
- /**
- * Check if this object is overlapping with the provided object
- * @param {Object} obj an object with parameters left, top
- * @return {boolean} True if location is located on the edge
- */
- Edge.prototype.isOverlappingWith = function(obj) {
- if (this.connected) {
- var distMax = 10;
- var xFrom = this.from.x;
- var yFrom = this.from.y;
- var xTo = this.to.x;
- var yTo = this.to.y;
- var xObj = obj.left;
- var yObj = obj.top;
+ if (!current) {
+ // this is a new node
+ current = {
+ id: node.id
+ };
+ if (graph.node) {
+ // clone default attributes
+ current.attr = merge(current.attr, graph.node);
+ }
+ }
- var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
+ // add node to this (sub)graph and all its parent graphs
+ for (i = graphs.length - 1; i >= 0; i--) {
+ var g = graphs[i];
- return (dist < distMax);
+ if (!g.nodes) {
+ g.nodes = [];
+ }
+ if (g.nodes.indexOf(current) == -1) {
+ g.nodes.push(current);
+ }
}
- else {
- return false
+
+ // merge attributes
+ if (node.attr) {
+ current.attr = merge(current.attr, node.attr);
}
- };
+ }
- Edge.prototype._getColor = function() {
- var colorObj = this.options.color;
- if (this.options.inheritColor == "to") {
- colorObj = {
- highlight: this.to.options.color.highlight.border,
- hover: this.to.options.color.hover.border,
- color: this.to.options.color.border
- };
+ /**
+ * Add an edge to a graph object
+ * @param {Object} graph
+ * @param {Object} edge
+ */
+ function addEdge(graph, edge) {
+ if (!graph.edges) {
+ graph.edges = [];
}
- else if (this.options.inheritColor == "from" || this.options.inheritColor == true) {
- colorObj = {
- highlight: this.from.options.color.highlight.border,
- hover: this.from.options.color.hover.border,
- color: this.from.options.color.border
- };
+ graph.edges.push(edge);
+ if (graph.edge) {
+ var attr = merge({}, graph.edge); // clone default attributes
+ edge.attr = merge(attr, edge.attr); // merge attributes
}
-
- if (this.selected == true) {return colorObj.highlight;}
- else if (this.hover == true) {return colorObj.hover;}
- else {return colorObj.color;}
- };
-
+ }
/**
- * Redraw a edge as a line
- * Draw this edge in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
+ * Create an edge to a graph object
+ * @param {Object} graph
+ * @param {String | Number | Object} from
+ * @param {String | Number | Object} to
+ * @param {String} type
+ * @param {Object | null} attr
+ * @return {Object} edge
*/
- Edge.prototype._drawLine = function(ctx) {
- // set style
- ctx.strokeStyle = this._getColor();
- ctx.lineWidth = this._getLineWidth();
-
- if (this.from != this.to) {
- // draw line
- var via = this._line(ctx);
+ function createEdge(graph, from, to, type, attr) {
+ var edge = {
+ from: from,
+ to: to,
+ type: type
+ };
- // draw label
- var point;
- if (this.label) {
- if (this.options.smoothCurves.enabled == true && via != null) {
- var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
- var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
- point = {x:midpointX, y:midpointY};
- }
- else {
- point = this._pointOnLine(0.5);
- }
- this._label(ctx, this.label, point.x, point.y);
- }
- }
- else {
- var x, y;
- var radius = this.physics.springLength / 4;
- var node = this.from;
- if (!node.width) {
- node.resize(ctx);
- }
- if (node.width > node.height) {
- x = node.x + node.width / 2;
- y = node.y - radius;
- }
- else {
- x = node.x + radius;
- y = node.y - node.height / 2;
- }
- this._circle(ctx, x, y, radius);
- point = this._pointOnCircle(x, y, radius, 0.5);
- this._label(ctx, this.label, point.x, point.y);
+ if (graph.edge) {
+ edge.attr = merge({}, graph.edge); // clone default attributes
}
- };
+ edge.attr = merge(edge.attr || {}, attr); // merge attributes
+
+ return edge;
+ }
/**
- * Get the line width of the edge. Depends on width and whether one of the
- * connected nodes is selected.
- * @return {Number} width
- * @private
+ * Get next token in the current dot file.
+ * The token and token type are available as token and tokenType
*/
- Edge.prototype._getLineWidth = function() {
- if (this.selected == true) {
- return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3*this.networkScaleInv);
- }
- else {
- if (this.hover == true) {
- return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3*this.networkScaleInv);
- }
- else {
- return Math.max(this.options.width, 0.3*this.networkScaleInv);
- }
+ function getToken() {
+ tokenType = TOKENTYPE.NULL;
+ token = '';
+
+ // skip over whitespaces
+ while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
+ next();
}
- };
- Edge.prototype._getViaCoordinates = function () {
- var xVia = null;
- var yVia = null;
- var factor = this.options.smoothCurves.roundness;
- var type = this.options.smoothCurves.type;
+ do {
+ var isComment = false;
- var dx = Math.abs(this.from.x - this.to.x);
- var dy = Math.abs(this.from.y - this.to.y);
- if (type == 'discrete' || type == 'diagonalCross') {
- if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
- if (this.from.y > this.to.y) {
- if (this.from.x < this.to.x) {
- xVia = this.from.x + factor * dy;
- yVia = this.from.y - factor * dy;
- }
- else if (this.from.x > this.to.x) {
- xVia = this.from.x - factor * dy;
- yVia = this.from.y - factor * dy;
- }
+ // skip comment
+ if (c == '#') {
+ // find the previous non-space character
+ var i = index - 1;
+ while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
+ i--;
}
- else if (this.from.y < this.to.y) {
- if (this.from.x < this.to.x) {
- xVia = this.from.x + factor * dy;
- yVia = this.from.y + factor * dy;
- }
- else if (this.from.x > this.to.x) {
- xVia = this.from.x - factor * dy;
- yVia = this.from.y + factor * dy;
+ if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
+ // the # is at the start of a line, this is indeed a line comment
+ while (c != '' && c != '\n') {
+ next();
}
- }
- if (type == "discrete") {
- xVia = dx < factor * dy ? this.from.x : xVia;
+ isComment = true;
}
}
- else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
- if (this.from.y > this.to.y) {
- if (this.from.x < this.to.x) {
- xVia = this.from.x + factor * dx;
- yVia = this.from.y - factor * dx;
- }
- else if (this.from.x > this.to.x) {
- xVia = this.from.x - factor * dx;
- yVia = this.from.y - factor * dx;
- }
+ if (c == '/' && nextPreview() == '/') {
+ // skip line comment
+ while (c != '' && c != '\n') {
+ next();
}
- else if (this.from.y < this.to.y) {
- if (this.from.x < this.to.x) {
- xVia = this.from.x + factor * dx;
- yVia = this.from.y + factor * dx;
+ isComment = true;
+ }
+ if (c == '/' && nextPreview() == '*') {
+ // skip block comment
+ while (c != '') {
+ if (c == '*' && nextPreview() == '/') {
+ // end of block comment found. skip these last two characters
+ next();
+ next();
+ break;
}
- else if (this.from.x > this.to.x) {
- xVia = this.from.x - factor * dx;
- yVia = this.from.y + factor * dx;
+ else {
+ next();
}
}
- if (type == "discrete") {
- yVia = dy < factor * dx ? this.from.y : yVia;
- }
- }
- }
- else if (type == "straightCross") {
- if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down
- xVia = this.from.x;
- if (this.from.y < this.to.y) {
- yVia = this.to.y - (1-factor) * dy;
- }
- else {
- yVia = this.to.y + (1-factor) * dy;
- }
+ isComment = true;
}
- else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right
- if (this.from.x < this.to.x) {
- xVia = this.to.x - (1-factor) * dx;
- }
- else {
- xVia = this.to.x + (1-factor) * dx;
- }
- yVia = this.from.y;
+
+ // skip over whitespaces
+ while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
+ next();
}
}
- else if (type == 'horizontal') {
- if (this.from.x < this.to.x) {
- xVia = this.to.x - (1-factor) * dx;
- }
- else {
- xVia = this.to.x + (1-factor) * dx;
- }
- yVia = this.from.y;
+ while (isComment);
+
+ // check for end of dot file
+ if (c == '') {
+ // token is still empty
+ tokenType = TOKENTYPE.DELIMITER;
+ return;
}
- else if (type == 'vertical') {
- xVia = this.from.x;
- if (this.from.y < this.to.y) {
- yVia = this.to.y - (1-factor) * dy;
+
+ // check for delimiters consisting of 2 characters
+ var c2 = c + nextPreview();
+ if (DELIMITERS[c2]) {
+ tokenType = TOKENTYPE.DELIMITER;
+ token = c2;
+ next();
+ next();
+ return;
+ }
+
+ // check for delimiters consisting of 1 character
+ if (DELIMITERS[c]) {
+ tokenType = TOKENTYPE.DELIMITER;
+ token = c;
+ next();
+ return;
+ }
+
+ // check for an identifier (number or string)
+ // TODO: more precise parsing of numbers/strings (and the port separator ':')
+ if (isAlphaNumeric(c) || c == '-') {
+ token += c;
+ next();
+
+ while (isAlphaNumeric(c)) {
+ token += c;
+ next();
}
- else {
- yVia = this.to.y + (1-factor) * dy;
+ if (token == 'false') {
+ token = false; // convert to boolean
}
- }
- else { // continuous
- if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
- if (this.from.y > this.to.y) {
- if (this.from.x < this.to.x) {
- // console.log(1)
- xVia = this.from.x + factor * dy;
- yVia = this.from.y - factor * dy;
- xVia = this.to.x < xVia ? this.to.x : xVia;
- }
- else if (this.from.x > this.to.x) {
- // console.log(2)
- xVia = this.from.x - factor * dy;
- yVia = this.from.y - factor * dy;
- xVia = this.to.x > xVia ? this.to.x :xVia;
- }
- }
- else if (this.from.y < this.to.y) {
- if (this.from.x < this.to.x) {
- // console.log(3)
- xVia = this.from.x + factor * dy;
- yVia = this.from.y + factor * dy;
- xVia = this.to.x < xVia ? this.to.x : xVia;
- }
- else if (this.from.x > this.to.x) {
- // console.log(4, this.from.x, this.to.x)
- xVia = this.from.x - factor * dy;
- yVia = this.from.y + factor * dy;
- xVia = this.to.x > xVia ? this.to.x : xVia;
- }
- }
+ else if (token == 'true') {
+ token = true; // convert to boolean
}
- else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
- if (this.from.y > this.to.y) {
- if (this.from.x < this.to.x) {
- // console.log(5)
- xVia = this.from.x + factor * dx;
- yVia = this.from.y - factor * dx;
- yVia = this.to.y > yVia ? this.to.y : yVia;
- }
- else if (this.from.x > this.to.x) {
- // console.log(6)
- xVia = this.from.x - factor * dx;
- yVia = this.from.y - factor * dx;
- yVia = this.to.y > yVia ? this.to.y : yVia;
- }
- }
- else if (this.from.y < this.to.y) {
- if (this.from.x < this.to.x) {
- // console.log(7)
- xVia = this.from.x + factor * dx;
- yVia = this.from.y + factor * dx;
- yVia = this.to.y < yVia ? this.to.y : yVia;
- }
- else if (this.from.x > this.to.x) {
- // console.log(8)
- xVia = this.from.x - factor * dx;
- yVia = this.from.y + factor * dx;
- yVia = this.to.y < yVia ? this.to.y : yVia;
- }
- }
+ else if (!isNaN(Number(token))) {
+ token = Number(token); // convert to number
}
+ tokenType = TOKENTYPE.IDENTIFIER;
+ return;
}
-
- return {x:xVia, y:yVia};
- };
-
- /**
- * Draw a line between two nodes
- * @param {CanvasRenderingContext2D} ctx
- * @private
- */
- Edge.prototype._line = function (ctx) {
- // draw a straight line
- ctx.beginPath();
- ctx.moveTo(this.from.x, this.from.y);
- if (this.options.smoothCurves.enabled == true) {
- if (this.options.smoothCurves.dynamic == false) {
- var via = this._getViaCoordinates();
- if (via.x == null) {
- ctx.lineTo(this.to.x, this.to.y);
- ctx.stroke();
- return null;
- }
- else {
- // this.via.x = via.x;
- // this.via.y = via.y;
- ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y);
- ctx.stroke();
- return via;
+ // check for a string enclosed by double quotes
+ if (c == '"') {
+ next();
+ while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
+ token += c;
+ if (c == '"') { // skip the escape character
+ next();
}
+ next();
}
- else {
- ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
- ctx.stroke();
- return this.via;
+ if (c != '"') {
+ throw newSyntaxError('End of string " expected');
}
+ next();
+ tokenType = TOKENTYPE.IDENTIFIER;
+ return;
}
- else {
- ctx.lineTo(this.to.x, this.to.y);
- ctx.stroke();
- return null;
+
+ // something unknown is found, wrong characters, a syntax error
+ tokenType = TOKENTYPE.UNKNOWN;
+ while (c != '') {
+ token += c;
+ next();
}
- };
+ throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
+ }
/**
- * Draw a line from a node to itself, a circle
- * @param {CanvasRenderingContext2D} ctx
- * @param {Number} x
- * @param {Number} y
- * @param {Number} radius
- * @private
+ * Parse a graph.
+ * @returns {Object} graph
*/
- Edge.prototype._circle = function (ctx, x, y, radius) {
- // draw a circle
- ctx.beginPath();
- ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
- ctx.stroke();
- };
+ function parseGraph() {
+ var graph = {};
- /**
- * Draw label with white background and with the middle at (x, y)
- * @param {CanvasRenderingContext2D} ctx
- * @param {String} text
- * @param {Number} x
- * @param {Number} y
- * @private
- */
- Edge.prototype._label = function (ctx, text, x, y) {
- if (text) {
- ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
- this.options.fontSize + "px " + this.options.fontFace;
- var yLine;
+ first();
+ getToken();
- if (this.dirtyLabel == true) {
- var lines = String(text).split('\n');
- var lineCount = lines.length;
- var fontSize = (Number(this.options.fontSize) + 4);
- yLine = y + (1 - lineCount) / 2 * fontSize;
+ // optional strict keyword
+ if (token == 'strict') {
+ graph.strict = true;
+ getToken();
+ }
- var width = ctx.measureText(lines[0]).width;
- for (var i = 1; i < lineCount; i++) {
- var lineWidth = ctx.measureText(lines[i]).width;
- width = lineWidth > width ? lineWidth : width;
- }
- var height = this.options.fontSize * lineCount;
- var left = x - width / 2;
- var top = y - height / 2;
+ // graph or digraph keyword
+ if (token == 'graph' || token == 'digraph') {
+ graph.type = token;
+ getToken();
+ }
- // cache
- this.labelDimensions = {top:top,left:left,width:width,height:height,yLine:yLine};
- }
+ // optional graph id
+ if (tokenType == TOKENTYPE.IDENTIFIER) {
+ graph.id = token;
+ getToken();
+ }
+ // open angle bracket
+ if (token != '{') {
+ throw newSyntaxError('Angle bracket { expected');
+ }
+ getToken();
- if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") {
- ctx.fillStyle = this.options.fontFill;
- ctx.fillRect(this.labelDimensions.left,
- this.labelDimensions.top,
- this.labelDimensions.width,
- this.labelDimensions.height);
- }
+ // statements
+ parseStatements(graph);
+
+ // close angle bracket
+ if (token != '}') {
+ throw newSyntaxError('Angle bracket } expected');
+ }
+ getToken();
+
+ // end of file
+ if (token !== '') {
+ throw newSyntaxError('End of file expected');
+ }
+ getToken();
+
+ // remove temporary default properties
+ delete graph.node;
+ delete graph.edge;
+ delete graph.graph;
- // draw text
- ctx.fillStyle = this.options.fontColor || "black";
- ctx.textAlign = "center";
- ctx.textBaseline = "middle";
- yLine = this.labelDimensions.yLine;
- for (var i = 0; i < lineCount; i++) {
- ctx.fillText(lines[i], x, yLine);
- yLine += fontSize;
+ return graph;
+ }
+
+ /**
+ * Parse a list with statements.
+ * @param {Object} graph
+ */
+ function parseStatements (graph) {
+ while (token !== '' && token != '}') {
+ parseStatement(graph);
+ if (token == ';') {
+ getToken();
}
}
- };
+ }
/**
- * Redraw a edge as a dashed line
- * Draw this edge in the given canvas
- * @author David Jordan
- * @date 2012-08-08
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
+ * Parse a single statement. Can be a an attribute statement, node
+ * statement, a series of node statements and edge statements, or a
+ * parameter.
+ * @param {Object} graph
*/
- Edge.prototype._drawDashLine = function(ctx) {
- // set style
- ctx.strokeStyle = this._getColor();
- ctx.lineWidth = this._getLineWidth();
+ function parseStatement(graph) {
+ // parse subgraph
+ var subgraph = parseSubgraph(graph);
+ if (subgraph) {
+ // edge statements
+ parseEdge(graph, subgraph);
- var via = null;
- // only firefox and chrome support this method, else we use the legacy one.
- if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
- // configure the dash pattern
- var pattern = [0];
- if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) {
- pattern = [this.options.dash.length,this.options.dash.gap];
- }
- else {
- pattern = [5,5];
- }
+ return;
+ }
- // set dash settings for chrome or firefox
- if (typeof ctx.setLineDash !== 'undefined') { //Chrome
- ctx.setLineDash(pattern);
- ctx.lineDashOffset = 0;
+ // parse an attribute statement
+ var attr = parseAttributeStatement(graph);
+ if (attr) {
+ return;
+ }
- } else { //Firefox
- ctx.mozDash = pattern;
- ctx.mozDashOffset = 0;
+ // parse node
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Identifier expected');
+ }
+ var id = token; // id can be a string or a number
+ getToken();
+
+ if (token == '=') {
+ // id statement
+ getToken();
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Identifier expected');
}
+ graph[id] = token;
+ getToken();
+ // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
+ }
+ else {
+ parseNodeStatement(graph, id);
+ }
+ }
- // draw the line
- via = this._line(ctx);
+ /**
+ * Parse a subgraph
+ * @param {Object} graph parent graph object
+ * @return {Object | null} subgraph
+ */
+ function parseSubgraph (graph) {
+ var subgraph = null;
- // restore the dash settings.
- if (typeof ctx.setLineDash !== 'undefined') { //Chrome
- ctx.setLineDash([0]);
- ctx.lineDashOffset = 0;
+ // optional subgraph keyword
+ if (token == 'subgraph') {
+ subgraph = {};
+ subgraph.type = 'subgraph';
+ getToken();
- } else { //Firefox
- ctx.mozDash = [0];
- ctx.mozDashOffset = 0;
+ // optional graph id
+ if (tokenType == TOKENTYPE.IDENTIFIER) {
+ subgraph.id = token;
+ getToken();
}
}
- else { // unsupporting smooth lines
- // draw dashed line
- ctx.beginPath();
- ctx.lineCap = 'round';
- if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
- {
- ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
- [this.options.dash.length,this.options.dash.gap,this.options.dash.altLength,this.options.dash.gap]);
- }
- else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value
- {
- ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
- [this.options.dash.length,this.options.dash.gap]);
- }
- else //If all else fails draw a line
- {
- ctx.moveTo(this.from.x, this.from.y);
- ctx.lineTo(this.to.x, this.to.y);
+
+ // open angle bracket
+ if (token == '{') {
+ getToken();
+
+ if (!subgraph) {
+ subgraph = {};
}
- ctx.stroke();
- }
+ subgraph.parent = graph;
+ subgraph.node = graph.node;
+ subgraph.edge = graph.edge;
+ subgraph.graph = graph.graph;
- // draw label
- if (this.label) {
- var point;
- if (this.options.smoothCurves.enabled == true && via != null) {
- var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
- var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
- point = {x:midpointX, y:midpointY};
+ // statements
+ parseStatements(subgraph);
+
+ // close angle bracket
+ if (token != '}') {
+ throw newSyntaxError('Angle bracket } expected');
}
- else {
- point = this._pointOnLine(0.5);
+ getToken();
+
+ // remove temporary default properties
+ delete subgraph.node;
+ delete subgraph.edge;
+ delete subgraph.graph;
+ delete subgraph.parent;
+
+ // register at the parent graph
+ if (!graph.subgraphs) {
+ graph.subgraphs = [];
}
- this._label(ctx, this.label, point.x, point.y);
+ graph.subgraphs.push(subgraph);
}
- };
+
+ return subgraph;
+ }
/**
- * Get a point on a line
- * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
- * @return {Object} point
- * @private
+ * parse an attribute statement like "node [shape=circle fontSize=16]".
+ * Available keywords are 'node', 'edge', 'graph'.
+ * The previous list with default attributes will be replaced
+ * @param {Object} graph
+ * @returns {String | null} keyword Returns the name of the parsed attribute
+ * (node, edge, graph), or null if nothing
+ * is parsed.
*/
- Edge.prototype._pointOnLine = function (percentage) {
- return {
- x: (1 - percentage) * this.from.x + percentage * this.to.x,
- y: (1 - percentage) * this.from.y + percentage * this.to.y
+ function parseAttributeStatement (graph) {
+ // attribute statements
+ if (token == 'node') {
+ getToken();
+
+ // node attributes
+ graph.node = parseAttributeList();
+ return 'node';
+ }
+ else if (token == 'edge') {
+ getToken();
+
+ // edge attributes
+ graph.edge = parseAttributeList();
+ return 'edge';
+ }
+ else if (token == 'graph') {
+ getToken();
+
+ // graph attributes
+ graph.graph = parseAttributeList();
+ return 'graph';
}
- };
+
+ return null;
+ }
/**
- * Get a point on a circle
- * @param {Number} x
- * @param {Number} y
- * @param {Number} radius
- * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
- * @return {Object} point
- * @private
+ * parse a node statement
+ * @param {Object} graph
+ * @param {String | Number} id
*/
- Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
- var angle = (percentage - 3/8) * 2 * Math.PI;
- return {
- x: x + radius * Math.cos(angle),
- y: y - radius * Math.sin(angle)
+ function parseNodeStatement(graph, id) {
+ // node statement
+ var node = {
+ id: id
+ };
+ var attr = parseAttributeList();
+ if (attr) {
+ node.attr = attr;
}
- };
+ addNode(graph, node);
+
+ // edge statements
+ parseEdge(graph, id);
+ }
/**
- * Redraw a edge as a line with an arrow halfway the line
- * Draw this edge in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
+ * Parse an edge or a series of edges
+ * @param {Object} graph
+ * @param {String | Number} from Id of the from node
*/
- Edge.prototype._drawArrowCenter = function(ctx) {
- var point;
- // set style
- ctx.strokeStyle = this._getColor();
- ctx.fillStyle = ctx.strokeStyle;
- ctx.lineWidth = this._getLineWidth();
-
- if (this.from != this.to) {
- // draw line
- var via = this._line(ctx);
+ function parseEdge(graph, from) {
+ while (token == '->' || token == '--') {
+ var to;
+ var type = token;
+ getToken();
- var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
- var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
- // draw an arrow halfway the line
- if (this.options.smoothCurves.enabled == true && via != null) {
- var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
- var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
- point = {x:midpointX, y:midpointY};
+ var subgraph = parseSubgraph(graph);
+ if (subgraph) {
+ to = subgraph;
}
else {
- point = this._pointOnLine(0.5);
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Identifier or subgraph expected');
+ }
+ to = token;
+ addNode(graph, {
+ id: to
+ });
+ getToken();
}
- ctx.arrow(point.x, point.y, angle, length);
- ctx.fill();
- ctx.stroke();
-
- // draw label
- if (this.label) {
- this._label(ctx, this.label, point.x, point.y);
- }
- }
- else {
- // draw circle
- var x, y;
- var radius = 0.25 * Math.max(100,this.physics.springLength);
- var node = this.from;
- if (!node.width) {
- node.resize(ctx);
- }
- if (node.width > node.height) {
- x = node.x + node.width * 0.5;
- y = node.y - radius;
- }
- else {
- x = node.x + radius;
- y = node.y - node.height * 0.5;
- }
- this._circle(ctx, x, y, radius);
+ // parse edge attributes
+ var attr = parseAttributeList();
- // draw all arrows
- var angle = 0.2 * Math.PI;
- var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
- point = this._pointOnCircle(x, y, radius, 0.5);
- ctx.arrow(point.x, point.y, angle, length);
- ctx.fill();
- ctx.stroke();
+ // create edge
+ var edge = createEdge(graph, from, to, type, attr);
+ addEdge(graph, edge);
- // draw label
- if (this.label) {
- point = this._pointOnCircle(x, y, radius, 0.5);
- this._label(ctx, this.label, point.x, point.y);
- }
+ from = to;
}
- };
-
-
+ }
/**
- * Redraw a edge as a line with an arrow
- * Draw this edge in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
+ * Parse a set with attributes,
+ * for example [label="1.000", shape=solid]
+ * @return {Object | null} attr
*/
- Edge.prototype._drawArrow = function(ctx) {
- // set style
- ctx.strokeStyle = this._getColor();
- ctx.fillStyle = ctx.strokeStyle;
- ctx.lineWidth = this._getLineWidth();
+ function parseAttributeList() {
+ var attr = null;
- var angle, length;
- //draw a line
- if (this.from != this.to) {
- angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
- var dx = (this.to.x - this.from.x);
- var dy = (this.to.y - this.from.y);
- var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
+ while (token == '[') {
+ getToken();
+ attr = {};
+ while (token !== '' && token != ']') {
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Attribute name expected');
+ }
+ var name = token;
- var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
- var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
- var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
- var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
+ getToken();
+ if (token != '=') {
+ throw newSyntaxError('Equal sign = expected');
+ }
+ getToken();
- var via;
- if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true ) {
- via = this.via;
- }
- else if (this.options.smoothCurves.enabled == true) {
- via = this._getViaCoordinates();
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Attribute value expected');
+ }
+ var value = token;
+ setValue(attr, name, value); // name can be a path
+
+ getToken();
+ if (token ==',') {
+ getToken();
+ }
}
- if (this.options.smoothCurves.enabled == true && via.x != null) {
- angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
- dx = (this.to.x - via.x);
- dy = (this.to.y - via.y);
- edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
+ if (token != ']') {
+ throw newSyntaxError('Bracket ] expected');
}
- var toBorderDist = this.to.distanceToBorder(ctx, angle);
- var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
+ getToken();
+ }
- var xTo,yTo;
- if (this.options.smoothCurves.enabled == true && via.x != null) {
- xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
- yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
+ return attr;
+ }
+
+ /**
+ * Create a syntax error with extra information on current token and index.
+ * @param {String} message
+ * @returns {SyntaxError} err
+ */
+ function newSyntaxError(message) {
+ return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
+ }
+
+ /**
+ * Chop off text after a maximum length
+ * @param {String} text
+ * @param {Number} maxLength
+ * @returns {String}
+ */
+ function chop (text, maxLength) {
+ return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
+ }
+
+ /**
+ * Execute a function fn for each pair of elements in two arrays
+ * @param {Array | *} array1
+ * @param {Array | *} array2
+ * @param {function} fn
+ */
+ function forEach2(array1, array2, fn) {
+ if (Array.isArray(array1)) {
+ array1.forEach(function (elem1) {
+ if (Array.isArray(array2)) {
+ array2.forEach(function (elem2) {
+ fn(elem1, elem2);
+ });
+ }
+ else {
+ fn(elem1, array2);
+ }
+ });
+ }
+ else {
+ if (Array.isArray(array2)) {
+ array2.forEach(function (elem2) {
+ fn(array1, elem2);
+ });
}
else {
- xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
- yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
+ fn(array1, array2);
}
+ }
+ }
- ctx.beginPath();
- ctx.moveTo(xFrom,yFrom);
- if (this.options.smoothCurves.enabled == true && via.x != null) {
- ctx.quadraticCurveTo(via.x,via.y,xTo, yTo);
- }
- else {
- ctx.lineTo(xTo, yTo);
+ /**
+ * Convert a string containing a graph in DOT language into a map containing
+ * with nodes and edges in the format of graph.
+ * @param {String} data Text containing a graph in DOT-notation
+ * @return {Object} graphData
+ */
+ function DOTToGraph (data) {
+ // parse the DOT file
+ var dotData = parseDOT(data);
+ var graphData = {
+ nodes: [],
+ edges: [],
+ options: {}
+ };
+
+ // copy the nodes
+ if (dotData.nodes) {
+ dotData.nodes.forEach(function (dotNode) {
+ var graphNode = {
+ id: dotNode.id,
+ label: String(dotNode.label || dotNode.id)
+ };
+ merge(graphNode, dotNode.attr);
+ if (graphNode.image) {
+ graphNode.shape = 'image';
+ }
+ graphData.nodes.push(graphNode);
+ });
+ }
+
+ // copy the edges
+ if (dotData.edges) {
+ /**
+ * Convert an edge in DOT format to an edge with VisGraph format
+ * @param {Object} dotEdge
+ * @returns {Object} graphEdge
+ */
+ var convertEdge = function (dotEdge) {
+ var graphEdge = {
+ from: dotEdge.from,
+ to: dotEdge.to
+ };
+ merge(graphEdge, dotEdge.attr);
+ graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
+ return graphEdge;
}
- ctx.stroke();
- // draw arrow at the end of the line
- length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
- ctx.arrow(xTo, yTo, angle, length);
- ctx.fill();
- ctx.stroke();
+ dotData.edges.forEach(function (dotEdge) {
+ var from, to;
+ if (dotEdge.from instanceof Object) {
+ from = dotEdge.from.nodes;
+ }
+ else {
+ from = {
+ id: dotEdge.from
+ }
+ }
- // draw label
- if (this.label) {
- var point;
- if (this.options.smoothCurves.enabled == true && via != null) {
- var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
- var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
- point = {x:midpointX, y:midpointY};
+ if (dotEdge.to instanceof Object) {
+ to = dotEdge.to.nodes;
}
else {
- point = this._pointOnLine(0.5);
+ to = {
+ id: dotEdge.to
+ }
}
- this._label(ctx, this.label, point.x, point.y);
- }
- }
- else {
- // draw circle
- var node = this.from;
- var x, y, arrow;
- var radius = 0.25 * Math.max(100,this.physics.springLength);
- if (!node.width) {
- node.resize(ctx);
- }
- if (node.width > node.height) {
- x = node.x + node.width * 0.5;
- y = node.y - radius;
- arrow = {
- x: x,
- y: node.y,
- angle: 0.9 * Math.PI
- };
- }
- else {
- x = node.x + radius;
- y = node.y - node.height * 0.5;
- arrow = {
- x: node.x,
- y: y,
- angle: 0.6 * Math.PI
- };
- }
- ctx.beginPath();
- // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
- ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
- ctx.stroke();
- // draw all arrows
- var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
- ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
- ctx.fill();
- ctx.stroke();
+ if (dotEdge.from instanceof Object && dotEdge.from.edges) {
+ dotEdge.from.edges.forEach(function (subEdge) {
+ var graphEdge = convertEdge(subEdge);
+ graphData.edges.push(graphEdge);
+ });
+ }
- // draw label
- if (this.label) {
- point = this._pointOnCircle(x, y, radius, 0.5);
- this._label(ctx, this.label, point.x, point.y);
- }
+ forEach2(from, to, function (from, to) {
+ var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
+ var graphEdge = convertEdge(subEdge);
+ graphData.edges.push(graphEdge);
+ });
+
+ if (dotEdge.to instanceof Object && dotEdge.to.edges) {
+ dotEdge.to.edges.forEach(function (subEdge) {
+ var graphEdge = convertEdge(subEdge);
+ graphData.edges.push(graphEdge);
+ });
+ }
+ });
}
- };
+ // copy the options
+ if (dotData.attr) {
+ graphData.options = dotData.attr;
+ }
+ return graphData;
+ }
- /**
- * Calculate the distance between a point (x3,y3) and a line segment from
- * (x1,y1) to (x2,y2).
- * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
- * @param {number} x1
- * @param {number} y1
- * @param {number} x2
- * @param {number} y2
- * @param {number} x3
- * @param {number} y3
- * @private
- */
- Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
- var returnValue = 0;
- if (this.from != this.to) {
- if (this.options.smoothCurves.enabled == true) {
- var xVia, yVia;
- if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) {
- xVia = this.via.x;
- yVia = this.via.y;
- }
- else {
- var via = this._getViaCoordinates();
- xVia = via.x;
- yVia = via.y;
- }
- var minDistance = 1e9;
- var distance;
- var i,t,x,y, lastX, lastY;
- for (i = 0; i < 10; i++) {
- t = 0.1*i;
- x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2;
- y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2;
- if (i > 0) {
- distance = this._getDistanceToLine(lastX,lastY,x,y, x3,y3);
- minDistance = distance < minDistance ? distance : minDistance;
- }
- lastX = x; lastY = y;
- }
- returnValue = minDistance;
- }
- else {
- returnValue = this._getDistanceToLine(x1,y1,x2,y2,x3,y3);
+ // exports
+ exports.parseDOT = parseDOT;
+ exports.DOTToGraph = DOTToGraph;
+
+
+/***/ },
+/* 53 */
+/***/ function(module, exports, __webpack_require__) {
+
+
+ function parseGephi(gephiJSON, options) {
+ var edges = [];
+ var nodes = [];
+ this.options = {
+ edges: {
+ inheritColor: true
+ },
+ nodes: {
+ allowedToMove: false,
+ parseColor: false
}
+ };
+
+ if (options !== undefined) {
+ this.options.nodes['allowedToMove'] = options.allowedToMove | false;
+ this.options.nodes['parseColor'] = options.parseColor | false;
+ this.options.edges['inheritColor'] = options.inheritColor | true;
}
- else {
- var x, y, dx, dy;
- var radius = 0.25 * this.physics.springLength;
- var node = this.from;
- if (node.width > node.height) {
- x = node.x + 0.5 * node.width;
- y = node.y - radius;
+
+ var gEdges = gephiJSON.edges;
+ var gNodes = gephiJSON.nodes;
+ for (var i = 0; i < gEdges.length; i++) {
+ var edge = {};
+ var gEdge = gEdges[i];
+ edge['id'] = gEdge.id;
+ edge['from'] = gEdge.source;
+ edge['to'] = gEdge.target;
+ edge['attributes'] = gEdge.attributes;
+ // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined;
+ // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size;
+ edge['color'] = gEdge.color;
+ edge['inheritColor'] = edge['color'] !== undefined ? false : this.options.inheritColor;
+ edges.push(edge);
+ }
+
+ for (var i = 0; i < gNodes.length; i++) {
+ var node = {};
+ var gNode = gNodes[i];
+ node['id'] = gNode.id;
+ node['attributes'] = gNode.attributes;
+ node['x'] = gNode.x;
+ node['y'] = gNode.y;
+ node['label'] = gNode.label;
+ if (this.options.nodes.parseColor == true) {
+ node['color'] = gNode.color;
}
else {
- x = node.x + radius;
- y = node.y - 0.5 * node.height;
+ node['color'] = gNode.color !== undefined ? {background:gNode.color, border:gNode.color} : undefined;
}
- dx = x - x3;
- dy = y - y3;
- returnValue = Math.abs(Math.sqrt(dx*dx + dy*dy) - radius);
- }
-
- if (this.labelDimensions.left < x3 &&
- this.labelDimensions.left + this.labelDimensions.width > x3 &&
- this.labelDimensions.top < y3 &&
- this.labelDimensions.top + this.labelDimensions.height > y3) {
- return 0;
- }
- else {
- return returnValue;
+ node['radius'] = gNode.size;
+ node['allowedToMoveX'] = this.options.nodes.allowedToMove;
+ node['allowedToMoveY'] = this.options.nodes.allowedToMove;
+ nodes.push(node);
}
- };
-
- Edge.prototype._getDistanceToLine = function(x1,y1,x2,y2,x3,y3) {
- var px = x2-x1,
- py = y2-y1,
- something = px*px + py*py,
- u = ((x3 - x1) * px + (y3 - y1) * py) / something;
- if (u > 1) {
- u = 1;
- }
- else if (u < 0) {
- u = 0;
- }
+ return {nodes:nodes, edges:edges};
+ }
- var x = x1 + u * px,
- y = y1 + u * py,
- dx = x - x3,
- dy = y - y3;
+ exports.parseGephi = parseGephi;
- //# Note: If the actual distance does not matter,
- //# if you only want to compare what this function
- //# returns to other results of this function, you
- //# can just return the squared distance instead
- //# (i.e. remove the sqrt) to gain a little performance
+/***/ },
+/* 54 */
+/***/ function(module, exports, __webpack_require__) {
- return Math.sqrt(dx*dx + dy*dy);
- };
+ var util = __webpack_require__(1);
/**
- * This allows the zoom level of the network to influence the rendering
- *
- * @param scale
+ * @class Groups
+ * This class can store groups and properties specific for groups.
*/
- Edge.prototype.setScale = function(scale) {
- this.networkScaleInv = 1.0/scale;
- };
-
+ function Groups() {
+ this.clear();
+ this.defaultIndex = 0;
+ }
- Edge.prototype.select = function() {
- this.selected = true;
- };
- Edge.prototype.unselect = function() {
- this.selected = false;
- };
+ /**
+ * default constants for group colors
+ */
+ Groups.DEFAULT = [
+ {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
+ {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
+ {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // red
+ {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // green
+ {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
+ {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
+ {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // orange
+ {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
+ {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
+ {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}} // mint
+ ];
- Edge.prototype.positionBezierNode = function() {
- if (this.via !== null && this.from !== null && this.to !== null) {
- this.via.x = 0.5 * (this.from.x + this.to.x);
- this.via.y = 0.5 * (this.from.y + this.to.y);
- }
- else {
- this.via.x = 0;
- this.via.y = 0;
- }
- };
/**
- * This function draws the control nodes for the manipulator.
- * In order to enable this, only set the this.controlNodesEnabled to true.
- * @param ctx
+ * Clear all groups
*/
- Edge.prototype._drawControlNodes = function(ctx) {
- if (this.controlNodesEnabled == true) {
- if (this.controlNodes.from === null && this.controlNodes.to === null) {
- var nodeIdFrom = "edgeIdFrom:".concat(this.id);
- var nodeIdTo = "edgeIdTo:".concat(this.id);
- var constants = {
- nodes:{group:'', radius:8},
- physics:{damping:0},
- clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}}
- };
- this.controlNodes.from = new Node(
- {id:nodeIdFrom,
- shape:'dot',
- color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
- },{},{},constants);
- this.controlNodes.to = new Node(
- {id:nodeIdTo,
- shape:'dot',
- color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
- },{},{},constants);
- }
-
- if (this.controlNodes.from.selected == false && this.controlNodes.to.selected == false) {
- this.controlNodes.positions = this.getControlNodePositions(ctx);
- this.controlNodes.from.x = this.controlNodes.positions.from.x;
- this.controlNodes.from.y = this.controlNodes.positions.from.y;
- this.controlNodes.to.x = this.controlNodes.positions.to.x;
- this.controlNodes.to.y = this.controlNodes.positions.to.y;
+ Groups.prototype.clear = function () {
+ this.groups = {};
+ this.groups.length = function()
+ {
+ var i = 0;
+ for ( var p in this ) {
+ if (this.hasOwnProperty(p)) {
+ i++;
+ }
}
-
- this.controlNodes.from.draw(ctx);
- this.controlNodes.to.draw(ctx);
- }
- else {
- this.controlNodes = {from:null, to:null, positions:{}};
+ return i;
}
};
+
/**
- * Enable control nodes.
- * @private
+ * get group properties of a groupname. If groupname is not found, a new group
+ * is added.
+ * @param {*} groupname Can be a number, string, Date, etc.
+ * @return {Object} group The created group, containing all group properties
*/
- Edge.prototype._enableControlNodes = function() {
- this.fromBackup = this.from;
- this.toBackup = this.to;
- this.controlNodesEnabled = true;
+ Groups.prototype.get = function (groupname) {
+ var group = this.groups[groupname];
+ if (group == undefined) {
+ // create new group
+ var index = this.defaultIndex % Groups.DEFAULT.length;
+ this.defaultIndex++;
+ group = {};
+ group.color = Groups.DEFAULT[index];
+ this.groups[groupname] = group;
+ }
+
+ return group;
};
/**
- * disable control nodes and remove from dynamicEdges from old node
- * @private
+ * Add a custom group style
+ * @param {String} groupname
+ * @param {Object} style An object containing borderColor,
+ * backgroundColor, etc.
+ * @return {Object} group The created group object
*/
- Edge.prototype._disableControlNodes = function() {
- this.fromId = this.from.id;
- this.toId = this.to.id;
- if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges
- this.fromBackup.detachEdge(this);
- }
- else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges
- this.toBackup.detachEdge(this);
- }
-
- this.fromBackup = null;
- this.toBackup = null;
- this.controlNodesEnabled = false;
+ Groups.prototype.add = function (groupname, style) {
+ this.groups[groupname] = style;
+ return style;
};
+ module.exports = Groups;
+
+
+/***/ },
+/* 55 */
+/***/ function(module, exports, __webpack_require__) {
/**
- * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null.
- * @param x
- * @param y
- * @returns {null}
- * @private
+ * @class Images
+ * This class loads images and keeps them stored.
*/
- Edge.prototype._getSelectedControlNode = function(x,y) {
- var positions = this.controlNodes.positions;
- var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2));
- var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2));
-
- if (fromDistance < 15) {
- this.connectedNode = this.from;
- this.from = this.controlNodes.from;
- return this.controlNodes.from;
- }
- else if (toDistance < 15) {
- this.connectedNode = this.to;
- this.to = this.controlNodes.to;
- return this.controlNodes.to;
- }
- else {
- return null;
- }
- };
+ function Images() {
+ this.images = {};
+ this.callback = undefined;
+ }
/**
- * this resets the control nodes to their original position.
- * @private
+ * Set an onload callback function. This will be called each time an image
+ * is loaded
+ * @param {function} callback
*/
- Edge.prototype._restoreControlNodes = function() {
- if (this.controlNodes.from.selected == true) {
- this.from = this.connectedNode;
- this.connectedNode = null;
- this.controlNodes.from.unselect();
- }
- else if (this.controlNodes.to.selected == true) {
- this.to = this.connectedNode;
- this.connectedNode = null;
- this.controlNodes.to.unselect();
- }
+ Images.prototype.setOnloadCallback = function(callback) {
+ this.callback = callback;
};
/**
- * this calculates the position of the control nodes on the edges of the parent nodes.
*
- * @param ctx
- * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
+ * @param {string} url Url of the image
+ * @param {string} url Url of an image to use if the url image is not found
+ * @return {Image} img The image object
*/
- Edge.prototype.getControlNodePositions = function(ctx) {
- var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
- var dx = (this.to.x - this.from.x);
- var dy = (this.to.y - this.from.y);
- var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
- var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
- var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
- var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
- var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
-
- var via;
- if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true) {
- via = this.via;
- }
- else if (this.options.smoothCurves.enabled == true) {
- via = this._getViaCoordinates();
- }
-
- if (this.options.smoothCurves.enabled == true && via.x != null) {
- angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
- dx = (this.to.x - via.x);
- dy = (this.to.y - via.y);
- edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
- }
- var toBorderDist = this.to.distanceToBorder(ctx, angle);
- var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
-
- var xTo,yTo;
- if (this.options.smoothCurves.enabled == true && via.x != null) {
- xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
- yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
- }
- else {
- xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
- yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
+ Images.prototype.load = function(url, brokenUrl) {
+ var img = this.images[url];
+ if (img == undefined) {
+ // create the image
+ var images = this;
+ img = new Image();
+ this.images[url] = img;
+ img.onload = function() {
+ if (images.callback) {
+ images.callback(this);
+ }
+ };
+
+ img.onerror = function () {
+ this.src = brokenUrl;
+ if (images.callback) {
+ images.callback(this);
+ }
+ };
+
+ img.src = url;
}
- return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}};
+ return img;
};
- module.exports = Edge;
+ module.exports = Images;
+
/***/ },
-/* 53 */
+/* 56 */
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
@@ -27423,1203 +27243,1377 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ },
-/* 54 */
+/* 57 */
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
+ var Node = __webpack_require__(56);
/**
- * @class Groups
- * This class can store groups and properties specific for groups.
+ * @class Edge
+ *
+ * A edge connects two nodes
+ * @param {Object} properties Object with properties. Must contain
+ * At least properties from and to.
+ * Available properties: from (number),
+ * to (number), label (string, color (string),
+ * width (number), style (string),
+ * length (number), title (string)
+ * @param {Network} network A Network object, used to find and edge to
+ * nodes.
+ * @param {Object} constants An object with default values for
+ * example for the color
*/
- function Groups() {
- this.clear();
- this.defaultIndex = 0;
- }
+ function Edge (properties, network, networkConstants) {
+ if (!network) {
+ throw "No network provided";
+ }
+ var fields = ['edges','physics'];
+ var constants = util.selectiveBridgeObject(fields,networkConstants);
+ this.options = constants.edges;
+ this.physics = constants.physics;
+ this.options['smoothCurves'] = networkConstants['smoothCurves'];
+
+
+ this.network = network;
+
+ // initialize variables
+ this.id = undefined;
+ this.fromId = undefined;
+ this.toId = undefined;
+ this.title = undefined;
+ this.widthSelected = this.options.width * this.options.widthSelectionMultiplier;
+ this.value = undefined;
+ this.selected = false;
+ this.hover = false;
+ this.labelDimensions = {top:0,left:0,width:0,height:0,yLine:0}; // could be cached
+ this.dirtyLabel = true;
+
+ this.from = null; // a node
+ this.to = null; // a node
+ this.via = null; // a temp node
+
+ this.fromBackup = null; // used to clean up after reconnect
+ this.toBackup = null;; // used to clean up after reconnect
+
+ // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
+ // by storing the original information we can revert to the original connection when the cluser is opened.
+ this.originalFromId = [];
+ this.originalToId = [];
+
+ this.connected = false;
+
+ this.widthFixed = false;
+ this.lengthFixed = false;
+
+ this.setProperties(properties);
+ this.controlNodesEnabled = false;
+ this.controlNodes = {from:null, to:null, positions:{}};
+ this.connectedNode = null;
+ }
/**
- * default constants for group colors
+ * Set or overwrite properties for the edge
+ * @param {Object} properties an object with properties
+ * @param {Object} constants and object with default, global properties
*/
- Groups.DEFAULT = [
- {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
- {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
- {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // red
- {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // green
- {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
- {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
- {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // orange
- {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
- {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
- {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}} // mint
- ];
+ Edge.prototype.setProperties = function(properties) {
+ if (!properties) {
+ return;
+ }
+
+ var fields = ['style','fontSize','fontFace','fontColor','fontFill','width',
+ 'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor'
+ ];
+ util.selectiveDeepExtend(fields, this.options, properties);
+
+ if (properties.from !== undefined) {this.fromId = properties.from;}
+ if (properties.to !== undefined) {this.toId = properties.to;}
+
+ if (properties.id !== undefined) {this.id = properties.id;}
+ if (properties.label !== undefined) {this.label = properties.label; this.dirtyLabel = true;}
+
+ if (properties.title !== undefined) {this.title = properties.title;}
+ if (properties.value !== undefined) {this.value = properties.value;}
+ if (properties.length !== undefined) {this.physics.springLength = properties.length;}
+
+ if (properties.color !== undefined) {
+ this.options.inheritColor = false;
+ if (util.isString(properties.color)) {
+ this.options.color.color = properties.color;
+ this.options.color.highlight = properties.color;
+ }
+ else {
+ if (properties.color.color !== undefined) {this.options.color.color = properties.color.color;}
+ if (properties.color.highlight !== undefined) {this.options.color.highlight = properties.color.highlight;}
+ if (properties.color.hover !== undefined) {this.options.color.hover = properties.color.hover;}
+ }
+ }
+
+ // A node is connected when it has a from and to node.
+ this.connect();
+ this.widthFixed = this.widthFixed || (properties.width !== undefined);
+ this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
+
+ this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
+
+ // set draw method based on style
+ switch (this.options.style) {
+ case 'line': this.draw = this._drawLine; break;
+ case 'arrow': this.draw = this._drawArrow; break;
+ case 'arrow-center': this.draw = this._drawArrowCenter; break;
+ case 'dash-line': this.draw = this._drawDashLine; break;
+ default: this.draw = this._drawLine; break;
+ }
+ };
/**
- * Clear all groups
+ * Connect an edge to its nodes
*/
- Groups.prototype.clear = function () {
- this.groups = {};
- this.groups.length = function()
- {
- var i = 0;
- for ( var p in this ) {
- if (this.hasOwnProperty(p)) {
- i++;
- }
+ Edge.prototype.connect = function () {
+ this.disconnect();
+
+ this.from = this.network.nodes[this.fromId] || null;
+ this.to = this.network.nodes[this.toId] || null;
+ this.connected = (this.from && this.to);
+
+ if (this.connected) {
+ this.from.attachEdge(this);
+ this.to.attachEdge(this);
+ }
+ else {
+ if (this.from) {
+ this.from.detachEdge(this);
+ }
+ if (this.to) {
+ this.to.detachEdge(this);
}
- return i;
}
};
-
/**
- * get group properties of a groupname. If groupname is not found, a new group
- * is added.
- * @param {*} groupname Can be a number, string, Date, etc.
- * @return {Object} group The created group, containing all group properties
+ * Disconnect an edge from its nodes
*/
- Groups.prototype.get = function (groupname) {
- var group = this.groups[groupname];
- if (group == undefined) {
- // create new group
- var index = this.defaultIndex % Groups.DEFAULT.length;
- this.defaultIndex++;
- group = {};
- group.color = Groups.DEFAULT[index];
- this.groups[groupname] = group;
+ Edge.prototype.disconnect = function () {
+ if (this.from) {
+ this.from.detachEdge(this);
+ this.from = null;
+ }
+ if (this.to) {
+ this.to.detachEdge(this);
+ this.to = null;
}
- return group;
+ this.connected = false;
};
/**
- * Add a custom group style
- * @param {String} groupname
- * @param {Object} style An object containing borderColor,
- * backgroundColor, etc.
- * @return {Object} group The created group object
+ * get the title of this edge.
+ * @return {string} title The title of the edge, or undefined when no title
+ * has been set.
*/
- Groups.prototype.add = function (groupname, style) {
- this.groups[groupname] = style;
- return style;
+ Edge.prototype.getTitle = function() {
+ return typeof this.title === "function" ? this.title() : this.title;
};
- module.exports = Groups;
+ /**
+ * Retrieve the value of the edge. Can be undefined
+ * @return {Number} value
+ */
+ Edge.prototype.getValue = function() {
+ return this.value;
+ };
-/***/ },
-/* 55 */
-/***/ function(module, exports, __webpack_require__) {
+ /**
+ * Adjust the value range of the edge. The edge will adjust it's width
+ * based on its value.
+ * @param {Number} min
+ * @param {Number} max
+ */
+ Edge.prototype.setValueRange = function(min, max) {
+ if (!this.widthFixed && this.value !== undefined) {
+ var scale = (this.options.widthMax - this.options.widthMin) / (max - min);
+ this.options.width= (this.value - min) * scale + this.options.widthMin;
+ this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
+ }
+ };
/**
- * @class Images
- * This class loads images and keeps them stored.
+ * Redraw a edge
+ * Draw this edge in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
*/
- function Images() {
- this.images = {};
+ Edge.prototype.draw = function(ctx) {
+ throw "Method draw not initialized in edge";
+ };
+
+ /**
+ * Check if this object is overlapping with the provided object
+ * @param {Object} obj an object with parameters left, top
+ * @return {boolean} True if location is located on the edge
+ */
+ Edge.prototype.isOverlappingWith = function(obj) {
+ if (this.connected) {
+ var distMax = 10;
+ var xFrom = this.from.x;
+ var yFrom = this.from.y;
+ var xTo = this.to.x;
+ var yTo = this.to.y;
+ var xObj = obj.left;
+ var yObj = obj.top;
+
+ var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
+
+ return (dist < distMax);
+ }
+ else {
+ return false
+ }
+ };
+
+ Edge.prototype._getColor = function() {
+ var colorObj = this.options.color;
+ if (this.options.inheritColor == "to") {
+ colorObj = {
+ highlight: this.to.options.color.highlight.border,
+ hover: this.to.options.color.hover.border,
+ color: this.to.options.color.border
+ };
+ }
+ else if (this.options.inheritColor == "from" || this.options.inheritColor == true) {
+ colorObj = {
+ highlight: this.from.options.color.highlight.border,
+ hover: this.from.options.color.hover.border,
+ color: this.from.options.color.border
+ };
+ }
+
+ if (this.selected == true) {return colorObj.highlight;}
+ else if (this.hover == true) {return colorObj.hover;}
+ else {return colorObj.color;}
+ };
- this.callback = undefined;
- }
/**
- * Set an onload callback function. This will be called each time an image
- * is loaded
- * @param {function} callback
+ * Redraw a edge as a line
+ * Draw this edge in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
*/
- Images.prototype.setOnloadCallback = function(callback) {
- this.callback = callback;
+ Edge.prototype._drawLine = function(ctx) {
+ // set style
+ ctx.strokeStyle = this._getColor();
+ ctx.lineWidth = this._getLineWidth();
+
+ if (this.from != this.to) {
+ // draw line
+ var via = this._line(ctx);
+
+ // draw label
+ var point;
+ if (this.label) {
+ if (this.options.smoothCurves.enabled == true && via != null) {
+ var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
+ point = {x:midpointX, y:midpointY};
+ }
+ else {
+ point = this._pointOnLine(0.5);
+ }
+ this._label(ctx, this.label, point.x, point.y);
+ }
+ }
+ else {
+ var x, y;
+ var radius = this.physics.springLength / 4;
+ var node = this.from;
+ if (!node.width) {
+ node.resize(ctx);
+ }
+ if (node.width > node.height) {
+ x = node.x + node.width / 2;
+ y = node.y - radius;
+ }
+ else {
+ x = node.x + radius;
+ y = node.y - node.height / 2;
+ }
+ this._circle(ctx, x, y, radius);
+ point = this._pointOnCircle(x, y, radius, 0.5);
+ this._label(ctx, this.label, point.x, point.y);
+ }
};
/**
- *
- * @param {string} url Url of the image
- * @param {string} url Url of an image to use if the url image is not found
- * @return {Image} img The image object
+ * Get the line width of the edge. Depends on width and whether one of the
+ * connected nodes is selected.
+ * @return {Number} width
+ * @private
*/
- Images.prototype.load = function(url, brokenUrl) {
- var img = this.images[url];
- if (img == undefined) {
- // create the image
- var images = this;
- img = new Image();
- this.images[url] = img;
- img.onload = function() {
- if (images.callback) {
- images.callback(this);
+ Edge.prototype._getLineWidth = function() {
+ if (this.selected == true) {
+ return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3*this.networkScaleInv);
+ }
+ else {
+ if (this.hover == true) {
+ return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3*this.networkScaleInv);
+ }
+ else {
+ return Math.max(this.options.width, 0.3*this.networkScaleInv);
+ }
+ }
+ };
+
+ Edge.prototype._getViaCoordinates = function () {
+ var xVia = null;
+ var yVia = null;
+ var factor = this.options.smoothCurves.roundness;
+ var type = this.options.smoothCurves.type;
+
+ var dx = Math.abs(this.from.x - this.to.x);
+ var dy = Math.abs(this.from.y - this.to.y);
+ if (type == 'discrete' || type == 'diagonalCross') {
+ if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
+ if (this.from.y > this.to.y) {
+ if (this.from.x < this.to.x) {
+ xVia = this.from.x + factor * dy;
+ yVia = this.from.y - factor * dy;
+ }
+ else if (this.from.x > this.to.x) {
+ xVia = this.from.x - factor * dy;
+ yVia = this.from.y - factor * dy;
+ }
}
- };
-
- img.onerror = function () {
- this.src = brokenUrl;
- if (images.callback) {
- images.callback(this);
- }
- };
-
- img.src = url;
+ else if (this.from.y < this.to.y) {
+ if (this.from.x < this.to.x) {
+ xVia = this.from.x + factor * dy;
+ yVia = this.from.y + factor * dy;
+ }
+ else if (this.from.x > this.to.x) {
+ xVia = this.from.x - factor * dy;
+ yVia = this.from.y + factor * dy;
+ }
+ }
+ if (type == "discrete") {
+ xVia = dx < factor * dy ? this.from.x : xVia;
+ }
+ }
+ else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
+ if (this.from.y > this.to.y) {
+ if (this.from.x < this.to.x) {
+ xVia = this.from.x + factor * dx;
+ yVia = this.from.y - factor * dx;
+ }
+ else if (this.from.x > this.to.x) {
+ xVia = this.from.x - factor * dx;
+ yVia = this.from.y - factor * dx;
+ }
+ }
+ else if (this.from.y < this.to.y) {
+ if (this.from.x < this.to.x) {
+ xVia = this.from.x + factor * dx;
+ yVia = this.from.y + factor * dx;
+ }
+ else if (this.from.x > this.to.x) {
+ xVia = this.from.x - factor * dx;
+ yVia = this.from.y + factor * dx;
+ }
+ }
+ if (type == "discrete") {
+ yVia = dy < factor * dx ? this.from.y : yVia;
+ }
+ }
+ }
+ else if (type == "straightCross") {
+ if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down
+ xVia = this.from.x;
+ if (this.from.y < this.to.y) {
+ yVia = this.to.y - (1-factor) * dy;
+ }
+ else {
+ yVia = this.to.y + (1-factor) * dy;
+ }
+ }
+ else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right
+ if (this.from.x < this.to.x) {
+ xVia = this.to.x - (1-factor) * dx;
+ }
+ else {
+ xVia = this.to.x + (1-factor) * dx;
+ }
+ yVia = this.from.y;
+ }
}
-
- return img;
- };
-
- module.exports = Images;
-
-
-/***/ },
-/* 56 */
-/***/ function(module, exports, __webpack_require__) {
-
- /**
- * Popup is a class to create a popup window with some text
- * @param {Element} container The container object.
- * @param {Number} [x]
- * @param {Number} [y]
- * @param {String} [text]
- * @param {Object} [style] An object containing borderColor,
- * backgroundColor, etc.
- */
- function Popup(container, x, y, text, style) {
- if (container) {
- this.container = container;
+ else if (type == 'horizontal') {
+ if (this.from.x < this.to.x) {
+ xVia = this.to.x - (1-factor) * dx;
+ }
+ else {
+ xVia = this.to.x + (1-factor) * dx;
+ }
+ yVia = this.from.y;
}
- else {
- this.container = document.body;
+ else if (type == 'vertical') {
+ xVia = this.from.x;
+ if (this.from.y < this.to.y) {
+ yVia = this.to.y - (1-factor) * dy;
+ }
+ else {
+ yVia = this.to.y + (1-factor) * dy;
+ }
}
-
- // x, y and text are optional, see if a style object was passed in their place
- if (style === undefined) {
- if (typeof x === "object") {
- style = x;
- x = undefined;
- } else if (typeof text === "object") {
- style = text;
- text = undefined;
- } else {
- // for backwards compatibility, in case clients other than Network are creating Popup directly
- style = {
- fontColor: 'black',
- fontSize: 14, // px
- fontFace: 'verdana',
- color: {
- border: '#666',
- background: '#FFFFC6'
+ else { // continuous
+ if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
+ if (this.from.y > this.to.y) {
+ if (this.from.x < this.to.x) {
+ // console.log(1)
+ xVia = this.from.x + factor * dy;
+ yVia = this.from.y - factor * dy;
+ xVia = this.to.x < xVia ? this.to.x : xVia;
+ }
+ else if (this.from.x > this.to.x) {
+ // console.log(2)
+ xVia = this.from.x - factor * dy;
+ yVia = this.from.y - factor * dy;
+ xVia = this.to.x > xVia ? this.to.x :xVia;
+ }
+ }
+ else if (this.from.y < this.to.y) {
+ if (this.from.x < this.to.x) {
+ // console.log(3)
+ xVia = this.from.x + factor * dy;
+ yVia = this.from.y + factor * dy;
+ xVia = this.to.x < xVia ? this.to.x : xVia;
+ }
+ else if (this.from.x > this.to.x) {
+ // console.log(4, this.from.x, this.to.x)
+ xVia = this.from.x - factor * dy;
+ yVia = this.from.y + factor * dy;
+ xVia = this.to.x > xVia ? this.to.x : xVia;
+ }
+ }
+ }
+ else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
+ if (this.from.y > this.to.y) {
+ if (this.from.x < this.to.x) {
+ // console.log(5)
+ xVia = this.from.x + factor * dx;
+ yVia = this.from.y - factor * dx;
+ yVia = this.to.y > yVia ? this.to.y : yVia;
+ }
+ else if (this.from.x > this.to.x) {
+ // console.log(6)
+ xVia = this.from.x - factor * dx;
+ yVia = this.from.y - factor * dx;
+ yVia = this.to.y > yVia ? this.to.y : yVia;
+ }
+ }
+ else if (this.from.y < this.to.y) {
+ if (this.from.x < this.to.x) {
+ // console.log(7)
+ xVia = this.from.x + factor * dx;
+ yVia = this.from.y + factor * dx;
+ yVia = this.to.y < yVia ? this.to.y : yVia;
+ }
+ else if (this.from.x > this.to.x) {
+ // console.log(8)
+ xVia = this.from.x - factor * dx;
+ yVia = this.from.y + factor * dx;
+ yVia = this.to.y < yVia ? this.to.y : yVia;
}
}
}
}
- this.x = 0;
- this.y = 0;
- this.padding = 5;
-
- if (x !== undefined && y !== undefined ) {
- this.setPosition(x, y);
- }
- if (text !== undefined) {
- this.setText(text);
- }
-
- // create the frame
- this.frame = document.createElement("div");
- var styleAttr = this.frame.style;
- styleAttr.position = "absolute";
- styleAttr.visibility = "hidden";
- styleAttr.border = "1px solid " + style.color.border;
- styleAttr.color = style.fontColor;
- styleAttr.fontSize = style.fontSize + "px";
- styleAttr.fontFamily = style.fontFace;
- styleAttr.padding = this.padding + "px";
- styleAttr.backgroundColor = style.color.background;
- styleAttr.borderRadius = "3px";
- styleAttr.MozBorderRadius = "3px";
- styleAttr.WebkitBorderRadius = "3px";
- styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
- styleAttr.whiteSpace = "nowrap";
- this.container.appendChild(this.frame);
- }
-
- /**
- * @param {number} x Horizontal position of the popup window
- * @param {number} y Vertical position of the popup window
- */
- Popup.prototype.setPosition = function(x, y) {
- this.x = parseInt(x);
- this.y = parseInt(y);
- };
- /**
- * Set the content for the popup window. This can be HTML code or text.
- * @param {string | Element} content
- */
- Popup.prototype.setText = function(content) {
- if (content instanceof Element) {
- this.frame.innerHTML = '';
- this.frame.appendChild(content);
- }
- else {
- this.frame.innerHTML = content; // string containing text or HTML
- }
+ return {x:xVia, y:yVia};
};
/**
- * Show the popup window
- * @param {boolean} show Optional. Show or hide the window
+ * Draw a line between two nodes
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
*/
- Popup.prototype.show = function (show) {
- if (show === undefined) {
- show = true;
- }
-
- if (show) {
- var height = this.frame.clientHeight;
- var width = this.frame.clientWidth;
- var maxHeight = this.frame.parentNode.clientHeight;
- var maxWidth = this.frame.parentNode.clientWidth;
-
- var top = (this.y - height);
- if (top + height + this.padding > maxHeight) {
- top = maxHeight - height - this.padding;
- }
- if (top < this.padding) {
- top = this.padding;
- }
-
- var left = this.x;
- if (left + width + this.padding > maxWidth) {
- left = maxWidth - width - this.padding;
+ Edge.prototype._line = function (ctx) {
+ // draw a straight line
+ ctx.beginPath();
+ ctx.moveTo(this.from.x, this.from.y);
+ if (this.options.smoothCurves.enabled == true) {
+ if (this.options.smoothCurves.dynamic == false) {
+ var via = this._getViaCoordinates();
+ if (via.x == null) {
+ ctx.lineTo(this.to.x, this.to.y);
+ ctx.stroke();
+ return null;
+ }
+ else {
+ // this.via.x = via.x;
+ // this.via.y = via.y;
+ ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y);
+ ctx.stroke();
+ return via;
+ }
}
- if (left < this.padding) {
- left = this.padding;
+ else {
+ ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
+ ctx.stroke();
+ return this.via;
}
-
- this.frame.style.left = left + "px";
- this.frame.style.top = top + "px";
- this.frame.style.visibility = "visible";
}
else {
- this.hide();
+ ctx.lineTo(this.to.x, this.to.y);
+ ctx.stroke();
+ return null;
}
};
/**
- * Hide the popup window
+ * Draw a line from a node to itself, a circle
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} radius
+ * @private
*/
- Popup.prototype.hide = function () {
- this.frame.style.visibility = "hidden";
+ Edge.prototype._circle = function (ctx, x, y, radius) {
+ // draw a circle
+ ctx.beginPath();
+ ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
+ ctx.stroke();
};
- module.exports = Popup;
-
-
-/***/ },
-/* 57 */
-/***/ function(module, exports, __webpack_require__) {
-
/**
- * Parse a text source containing data in DOT language into a JSON object.
- * The object contains two lists: one with nodes and one with edges.
- *
- * DOT language reference: http://www.graphviz.org/doc/info/lang.html
- *
- * @param {String} data Text containing a graph in DOT-notation
- * @return {Object} graph An object containing two parameters:
- * {Object[]} nodes
- * {Object[]} edges
+ * Draw label with white background and with the middle at (x, y)
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {String} text
+ * @param {Number} x
+ * @param {Number} y
+ * @private
*/
- function parseDOT (data) {
- dot = data;
- return parseGraph();
- }
-
- // token types enumeration
- var TOKENTYPE = {
- NULL : 0,
- DELIMITER : 1,
- IDENTIFIER: 2,
- UNKNOWN : 3
- };
-
- // map with all delimiters
- var DELIMITERS = {
- '{': true,
- '}': true,
- '[': true,
- ']': true,
- ';': true,
- '=': true,
- ',': true,
-
- '->': true,
- '--': true
- };
-
- var dot = ''; // current dot file
- var index = 0; // current index in dot file
- var c = ''; // current token character in expr
- var token = ''; // current token
- var tokenType = TOKENTYPE.NULL; // type of the token
+ Edge.prototype._label = function (ctx, text, x, y) {
+ if (text) {
+ ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
+ this.options.fontSize + "px " + this.options.fontFace;
+ var yLine;
- /**
- * Get the first character from the dot file.
- * The character is stored into the char c. If the end of the dot file is
- * reached, the function puts an empty string in c.
- */
- function first() {
- index = 0;
- c = dot.charAt(0);
- }
+ if (this.dirtyLabel == true) {
+ var lines = String(text).split('\n');
+ var lineCount = lines.length;
+ var fontSize = (Number(this.options.fontSize) + 4);
+ yLine = y + (1 - lineCount) / 2 * fontSize;
- /**
- * Get the next character from the dot file.
- * The character is stored into the char c. If the end of the dot file is
- * reached, the function puts an empty string in c.
- */
- function next() {
- index++;
- c = dot.charAt(index);
- }
+ var width = ctx.measureText(lines[0]).width;
+ for (var i = 1; i < lineCount; i++) {
+ var lineWidth = ctx.measureText(lines[i]).width;
+ width = lineWidth > width ? lineWidth : width;
+ }
+ var height = this.options.fontSize * lineCount;
+ var left = x - width / 2;
+ var top = y - height / 2;
- /**
- * Preview the next character from the dot file.
- * @return {String} cNext
- */
- function nextPreview() {
- return dot.charAt(index + 1);
- }
+ // cache
+ this.labelDimensions = {top:top,left:left,width:width,height:height,yLine:yLine};
+ }
- /**
- * Test whether given character is alphabetic or numeric
- * @param {String} c
- * @return {Boolean} isAlphaNumeric
- */
- var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
- function isAlphaNumeric(c) {
- return regexAlphaNumeric.test(c);
- }
- /**
- * Merge all properties of object b into object b
- * @param {Object} a
- * @param {Object} b
- * @return {Object} a
- */
- function merge (a, b) {
- if (!a) {
- a = {};
- }
+ if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") {
+ ctx.fillStyle = this.options.fontFill;
+ ctx.fillRect(this.labelDimensions.left,
+ this.labelDimensions.top,
+ this.labelDimensions.width,
+ this.labelDimensions.height);
+ }
- if (b) {
- for (var name in b) {
- if (b.hasOwnProperty(name)) {
- a[name] = b[name];
- }
+ // draw text
+ ctx.fillStyle = this.options.fontColor || "black";
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ yLine = this.labelDimensions.yLine;
+ for (var i = 0; i < lineCount; i++) {
+ ctx.fillText(lines[i], x, yLine);
+ yLine += fontSize;
}
}
- return a;
- }
+ };
/**
- * Set a value in an object, where the provided parameter name can be a
- * path with nested parameters. For example:
- *
- * var obj = {a: 2};
- * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
- *
- * @param {Object} obj
- * @param {String} path A parameter name or dot-separated parameter path,
- * like "color.highlight.border".
- * @param {*} value
+ * Redraw a edge as a dashed line
+ * Draw this edge in the given canvas
+ * @author David Jordan
+ * @date 2012-08-08
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
*/
- function setValue(obj, path, value) {
- var keys = path.split('.');
- var o = obj;
- while (keys.length) {
- var key = keys.shift();
- if (keys.length) {
- // this isn't the end point
- if (!o[key]) {
- o[key] = {};
- }
- o = o[key];
+ Edge.prototype._drawDashLine = function(ctx) {
+ // set style
+ ctx.strokeStyle = this._getColor();
+ ctx.lineWidth = this._getLineWidth();
+
+ var via = null;
+ // only firefox and chrome support this method, else we use the legacy one.
+ if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
+ // configure the dash pattern
+ var pattern = [0];
+ if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) {
+ pattern = [this.options.dash.length,this.options.dash.gap];
}
else {
- // this is the end point
- o[key] = value;
+ pattern = [5,5];
}
- }
- }
-
- /**
- * Add a node to a graph object. If there is already a node with
- * the same id, their attributes will be merged.
- * @param {Object} graph
- * @param {Object} node
- */
- function addNode(graph, node) {
- var i, len;
- var current = null;
- // find root graph (in case of subgraph)
- var graphs = [graph]; // list with all graphs from current graph to root graph
- var root = graph;
- while (root.parent) {
- graphs.push(root.parent);
- root = root.parent;
- }
+ // set dash settings for chrome or firefox
+ if (typeof ctx.setLineDash !== 'undefined') { //Chrome
+ ctx.setLineDash(pattern);
+ ctx.lineDashOffset = 0;
- // find existing node (at root level) by its id
- if (root.nodes) {
- for (i = 0, len = root.nodes.length; i < len; i++) {
- if (node.id === root.nodes[i].id) {
- current = root.nodes[i];
- break;
- }
+ } else { //Firefox
+ ctx.mozDash = pattern;
+ ctx.mozDashOffset = 0;
}
- }
- if (!current) {
- // this is a new node
- current = {
- id: node.id
- };
- if (graph.node) {
- // clone default attributes
- current.attr = merge(current.attr, graph.node);
- }
- }
+ // draw the line
+ via = this._line(ctx);
- // add node to this (sub)graph and all its parent graphs
- for (i = graphs.length - 1; i >= 0; i--) {
- var g = graphs[i];
+ // restore the dash settings.
+ if (typeof ctx.setLineDash !== 'undefined') { //Chrome
+ ctx.setLineDash([0]);
+ ctx.lineDashOffset = 0;
- if (!g.nodes) {
- g.nodes = [];
+ } else { //Firefox
+ ctx.mozDash = [0];
+ ctx.mozDashOffset = 0;
}
- if (g.nodes.indexOf(current) == -1) {
- g.nodes.push(current);
+ }
+ else { // unsupporting smooth lines
+ // draw dashed line
+ ctx.beginPath();
+ ctx.lineCap = 'round';
+ if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
+ {
+ ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
+ [this.options.dash.length,this.options.dash.gap,this.options.dash.altLength,this.options.dash.gap]);
+ }
+ else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value
+ {
+ ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
+ [this.options.dash.length,this.options.dash.gap]);
+ }
+ else //If all else fails draw a line
+ {
+ ctx.moveTo(this.from.x, this.from.y);
+ ctx.lineTo(this.to.x, this.to.y);
}
+ ctx.stroke();
}
- // merge attributes
- if (node.attr) {
- current.attr = merge(current.attr, node.attr);
+ // draw label
+ if (this.label) {
+ var point;
+ if (this.options.smoothCurves.enabled == true && via != null) {
+ var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
+ point = {x:midpointX, y:midpointY};
+ }
+ else {
+ point = this._pointOnLine(0.5);
+ }
+ this._label(ctx, this.label, point.x, point.y);
}
- }
+ };
/**
- * Add an edge to a graph object
- * @param {Object} graph
- * @param {Object} edge
+ * Get a point on a line
+ * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
+ * @return {Object} point
+ * @private
*/
- function addEdge(graph, edge) {
- if (!graph.edges) {
- graph.edges = [];
- }
- graph.edges.push(edge);
- if (graph.edge) {
- var attr = merge({}, graph.edge); // clone default attributes
- edge.attr = merge(attr, edge.attr); // merge attributes
+ Edge.prototype._pointOnLine = function (percentage) {
+ return {
+ x: (1 - percentage) * this.from.x + percentage * this.to.x,
+ y: (1 - percentage) * this.from.y + percentage * this.to.y
}
- }
+ };
/**
- * Create an edge to a graph object
- * @param {Object} graph
- * @param {String | Number | Object} from
- * @param {String | Number | Object} to
- * @param {String} type
- * @param {Object | null} attr
- * @return {Object} edge
+ * Get a point on a circle
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} radius
+ * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
+ * @return {Object} point
+ * @private
*/
- function createEdge(graph, from, to, type, attr) {
- var edge = {
- from: from,
- to: to,
- type: type
- };
-
- if (graph.edge) {
- edge.attr = merge({}, graph.edge); // clone default attributes
+ Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
+ var angle = (percentage - 3/8) * 2 * Math.PI;
+ return {
+ x: x + radius * Math.cos(angle),
+ y: y - radius * Math.sin(angle)
}
- edge.attr = merge(edge.attr || {}, attr); // merge attributes
-
- return edge;
- }
+ };
/**
- * Get next token in the current dot file.
- * The token and token type are available as token and tokenType
+ * Redraw a edge as a line with an arrow halfway the line
+ * Draw this edge in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
*/
- function getToken() {
- tokenType = TOKENTYPE.NULL;
- token = '';
-
- // skip over whitespaces
- while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
- next();
- }
+ Edge.prototype._drawArrowCenter = function(ctx) {
+ var point;
+ // set style
+ ctx.strokeStyle = this._getColor();
+ ctx.fillStyle = ctx.strokeStyle;
+ ctx.lineWidth = this._getLineWidth();
- do {
- var isComment = false;
+ if (this.from != this.to) {
+ // draw line
+ var via = this._line(ctx);
- // skip comment
- if (c == '#') {
- // find the previous non-space character
- var i = index - 1;
- while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
- i--;
- }
- if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
- // the # is at the start of a line, this is indeed a line comment
- while (c != '' && c != '\n') {
- next();
- }
- isComment = true;
- }
- }
- if (c == '/' && nextPreview() == '/') {
- // skip line comment
- while (c != '' && c != '\n') {
- next();
- }
- isComment = true;
- }
- if (c == '/' && nextPreview() == '*') {
- // skip block comment
- while (c != '') {
- if (c == '*' && nextPreview() == '/') {
- // end of block comment found. skip these last two characters
- next();
- next();
- break;
- }
- else {
- next();
- }
- }
- isComment = true;
+ var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
+ var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
+ // draw an arrow halfway the line
+ if (this.options.smoothCurves.enabled == true && via != null) {
+ var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
+ point = {x:midpointX, y:midpointY};
}
-
- // skip over whitespaces
- while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
- next();
+ else {
+ point = this._pointOnLine(0.5);
}
- }
- while (isComment);
-
- // check for end of dot file
- if (c == '') {
- // token is still empty
- tokenType = TOKENTYPE.DELIMITER;
- return;
- }
-
- // check for delimiters consisting of 2 characters
- var c2 = c + nextPreview();
- if (DELIMITERS[c2]) {
- tokenType = TOKENTYPE.DELIMITER;
- token = c2;
- next();
- next();
- return;
- }
-
- // check for delimiters consisting of 1 character
- if (DELIMITERS[c]) {
- tokenType = TOKENTYPE.DELIMITER;
- token = c;
- next();
- return;
- }
- // check for an identifier (number or string)
- // TODO: more precise parsing of numbers/strings (and the port separator ':')
- if (isAlphaNumeric(c) || c == '-') {
- token += c;
- next();
+ ctx.arrow(point.x, point.y, angle, length);
+ ctx.fill();
+ ctx.stroke();
- while (isAlphaNumeric(c)) {
- token += c;
- next();
+ // draw label
+ if (this.label) {
+ this._label(ctx, this.label, point.x, point.y);
}
- if (token == 'false') {
- token = false; // convert to boolean
+ }
+ else {
+ // draw circle
+ var x, y;
+ var radius = 0.25 * Math.max(100,this.physics.springLength);
+ var node = this.from;
+ if (!node.width) {
+ node.resize(ctx);
}
- else if (token == 'true') {
- token = true; // convert to boolean
+ if (node.width > node.height) {
+ x = node.x + node.width * 0.5;
+ y = node.y - radius;
}
- else if (!isNaN(Number(token))) {
- token = Number(token); // convert to number
+ else {
+ x = node.x + radius;
+ y = node.y - node.height * 0.5;
}
- tokenType = TOKENTYPE.IDENTIFIER;
- return;
- }
+ this._circle(ctx, x, y, radius);
- // check for a string enclosed by double quotes
- if (c == '"') {
- next();
- while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
- token += c;
- if (c == '"') { // skip the escape character
- next();
- }
- next();
- }
- if (c != '"') {
- throw newSyntaxError('End of string " expected');
+ // draw all arrows
+ var angle = 0.2 * Math.PI;
+ var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
+ point = this._pointOnCircle(x, y, radius, 0.5);
+ ctx.arrow(point.x, point.y, angle, length);
+ ctx.fill();
+ ctx.stroke();
+
+ // draw label
+ if (this.label) {
+ point = this._pointOnCircle(x, y, radius, 0.5);
+ this._label(ctx, this.label, point.x, point.y);
}
- next();
- tokenType = TOKENTYPE.IDENTIFIER;
- return;
}
+ };
+
- // something unknown is found, wrong characters, a syntax error
- tokenType = TOKENTYPE.UNKNOWN;
- while (c != '') {
- token += c;
- next();
- }
- throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
- }
/**
- * Parse a graph.
- * @returns {Object} graph
+ * Redraw a edge as a line with an arrow
+ * Draw this edge in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
*/
- function parseGraph() {
- var graph = {};
+ Edge.prototype._drawArrow = function(ctx) {
+ // set style
+ ctx.strokeStyle = this._getColor();
+ ctx.fillStyle = ctx.strokeStyle;
+ ctx.lineWidth = this._getLineWidth();
- first();
- getToken();
+ var angle, length;
+ //draw a line
+ if (this.from != this.to) {
+ angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
+ var dx = (this.to.x - this.from.x);
+ var dy = (this.to.y - this.from.y);
+ var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
- // optional strict keyword
- if (token == 'strict') {
- graph.strict = true;
- getToken();
- }
+ var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
+ var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
+ var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
+ var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
- // graph or digraph keyword
- if (token == 'graph' || token == 'digraph') {
- graph.type = token;
- getToken();
- }
+ var via;
+ if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true ) {
+ via = this.via;
+ }
+ else if (this.options.smoothCurves.enabled == true) {
+ via = this._getViaCoordinates();
+ }
- // optional graph id
- if (tokenType == TOKENTYPE.IDENTIFIER) {
- graph.id = token;
- getToken();
- }
+ if (this.options.smoothCurves.enabled == true && via.x != null) {
+ angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
+ dx = (this.to.x - via.x);
+ dy = (this.to.y - via.y);
+ edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
+ }
+ var toBorderDist = this.to.distanceToBorder(ctx, angle);
+ var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
- // open angle bracket
- if (token != '{') {
- throw newSyntaxError('Angle bracket { expected');
- }
- getToken();
+ var xTo,yTo;
+ if (this.options.smoothCurves.enabled == true && via.x != null) {
+ xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
+ yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
+ }
+ else {
+ xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
+ yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
+ }
- // statements
- parseStatements(graph);
+ ctx.beginPath();
+ ctx.moveTo(xFrom,yFrom);
+ if (this.options.smoothCurves.enabled == true && via.x != null) {
+ ctx.quadraticCurveTo(via.x,via.y,xTo, yTo);
+ }
+ else {
+ ctx.lineTo(xTo, yTo);
+ }
+ ctx.stroke();
- // close angle bracket
- if (token != '}') {
- throw newSyntaxError('Angle bracket } expected');
- }
- getToken();
+ // draw arrow at the end of the line
+ length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
+ ctx.arrow(xTo, yTo, angle, length);
+ ctx.fill();
+ ctx.stroke();
- // end of file
- if (token !== '') {
- throw newSyntaxError('End of file expected');
+ // draw label
+ if (this.label) {
+ var point;
+ if (this.options.smoothCurves.enabled == true && via != null) {
+ var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
+ point = {x:midpointX, y:midpointY};
+ }
+ else {
+ point = this._pointOnLine(0.5);
+ }
+ this._label(ctx, this.label, point.x, point.y);
+ }
}
- getToken();
-
- // remove temporary default properties
- delete graph.node;
- delete graph.edge;
- delete graph.graph;
+ else {
+ // draw circle
+ var node = this.from;
+ var x, y, arrow;
+ var radius = 0.25 * Math.max(100,this.physics.springLength);
+ if (!node.width) {
+ node.resize(ctx);
+ }
+ if (node.width > node.height) {
+ x = node.x + node.width * 0.5;
+ y = node.y - radius;
+ arrow = {
+ x: x,
+ y: node.y,
+ angle: 0.9 * Math.PI
+ };
+ }
+ else {
+ x = node.x + radius;
+ y = node.y - node.height * 0.5;
+ arrow = {
+ x: node.x,
+ y: y,
+ angle: 0.6 * Math.PI
+ };
+ }
+ ctx.beginPath();
+ // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
+ ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
+ ctx.stroke();
- return graph;
- }
+ // draw all arrows
+ var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
+ ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
+ ctx.fill();
+ ctx.stroke();
- /**
- * Parse a list with statements.
- * @param {Object} graph
- */
- function parseStatements (graph) {
- while (token !== '' && token != '}') {
- parseStatement(graph);
- if (token == ';') {
- getToken();
+ // draw label
+ if (this.label) {
+ point = this._pointOnCircle(x, y, radius, 0.5);
+ this._label(ctx, this.label, point.x, point.y);
}
}
- }
+ };
- /**
- * Parse a single statement. Can be a an attribute statement, node
- * statement, a series of node statements and edge statements, or a
- * parameter.
- * @param {Object} graph
- */
- function parseStatement(graph) {
- // parse subgraph
- var subgraph = parseSubgraph(graph);
- if (subgraph) {
- // edge statements
- parseEdge(graph, subgraph);
- return;
- }
- // parse an attribute statement
- var attr = parseAttributeStatement(graph);
- if (attr) {
- return;
+ /**
+ * Calculate the distance between a point (x3,y3) and a line segment from
+ * (x1,y1) to (x2,y2).
+ * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
+ * @param {number} x1
+ * @param {number} y1
+ * @param {number} x2
+ * @param {number} y2
+ * @param {number} x3
+ * @param {number} y3
+ * @private
+ */
+ Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
+ var returnValue = 0;
+ if (this.from != this.to) {
+ if (this.options.smoothCurves.enabled == true) {
+ var xVia, yVia;
+ if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) {
+ xVia = this.via.x;
+ yVia = this.via.y;
+ }
+ else {
+ var via = this._getViaCoordinates();
+ xVia = via.x;
+ yVia = via.y;
+ }
+ var minDistance = 1e9;
+ var distance;
+ var i,t,x,y, lastX, lastY;
+ for (i = 0; i < 10; i++) {
+ t = 0.1*i;
+ x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2;
+ y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2;
+ if (i > 0) {
+ distance = this._getDistanceToLine(lastX,lastY,x,y, x3,y3);
+ minDistance = distance < minDistance ? distance : minDistance;
+ }
+ lastX = x; lastY = y;
+ }
+ returnValue = minDistance;
+ }
+ else {
+ returnValue = this._getDistanceToLine(x1,y1,x2,y2,x3,y3);
+ }
}
-
- // parse node
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Identifier expected');
+ else {
+ var x, y, dx, dy;
+ var radius = 0.25 * this.physics.springLength;
+ var node = this.from;
+ if (node.width > node.height) {
+ x = node.x + 0.5 * node.width;
+ y = node.y - radius;
+ }
+ else {
+ x = node.x + radius;
+ y = node.y - 0.5 * node.height;
+ }
+ dx = x - x3;
+ dy = y - y3;
+ returnValue = Math.abs(Math.sqrt(dx*dx + dy*dy) - radius);
}
- var id = token; // id can be a string or a number
- getToken();
- if (token == '=') {
- // id statement
- getToken();
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Identifier expected');
- }
- graph[id] = token;
- getToken();
- // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
+ if (this.labelDimensions.left < x3 &&
+ this.labelDimensions.left + this.labelDimensions.width > x3 &&
+ this.labelDimensions.top < y3 &&
+ this.labelDimensions.top + this.labelDimensions.height > y3) {
+ return 0;
}
else {
- parseNodeStatement(graph, id);
+ return returnValue;
}
- }
-
- /**
- * Parse a subgraph
- * @param {Object} graph parent graph object
- * @return {Object | null} subgraph
- */
- function parseSubgraph (graph) {
- var subgraph = null;
+ };
- // optional subgraph keyword
- if (token == 'subgraph') {
- subgraph = {};
- subgraph.type = 'subgraph';
- getToken();
+ Edge.prototype._getDistanceToLine = function(x1,y1,x2,y2,x3,y3) {
+ var px = x2-x1,
+ py = y2-y1,
+ something = px*px + py*py,
+ u = ((x3 - x1) * px + (y3 - y1) * py) / something;
- // optional graph id
- if (tokenType == TOKENTYPE.IDENTIFIER) {
- subgraph.id = token;
- getToken();
- }
+ if (u > 1) {
+ u = 1;
+ }
+ else if (u < 0) {
+ u = 0;
}
- // open angle bracket
- if (token == '{') {
- getToken();
+ var x = x1 + u * px,
+ y = y1 + u * py,
+ dx = x - x3,
+ dy = y - y3;
- if (!subgraph) {
- subgraph = {};
- }
- subgraph.parent = graph;
- subgraph.node = graph.node;
- subgraph.edge = graph.edge;
- subgraph.graph = graph.graph;
+ //# Note: If the actual distance does not matter,
+ //# if you only want to compare what this function
+ //# returns to other results of this function, you
+ //# can just return the squared distance instead
+ //# (i.e. remove the sqrt) to gain a little performance
- // statements
- parseStatements(subgraph);
+ return Math.sqrt(dx*dx + dy*dy);
+ };
- // close angle bracket
- if (token != '}') {
- throw newSyntaxError('Angle bracket } expected');
- }
- getToken();
+ /**
+ * This allows the zoom level of the network to influence the rendering
+ *
+ * @param scale
+ */
+ Edge.prototype.setScale = function(scale) {
+ this.networkScaleInv = 1.0/scale;
+ };
- // remove temporary default properties
- delete subgraph.node;
- delete subgraph.edge;
- delete subgraph.graph;
- delete subgraph.parent;
- // register at the parent graph
- if (!graph.subgraphs) {
- graph.subgraphs = [];
- }
- graph.subgraphs.push(subgraph);
- }
+ Edge.prototype.select = function() {
+ this.selected = true;
+ };
- return subgraph;
- }
+ Edge.prototype.unselect = function() {
+ this.selected = false;
+ };
+
+ Edge.prototype.positionBezierNode = function() {
+ if (this.via !== null && this.from !== null && this.to !== null) {
+ this.via.x = 0.5 * (this.from.x + this.to.x);
+ this.via.y = 0.5 * (this.from.y + this.to.y);
+ }
+ else {
+ this.via.x = 0;
+ this.via.y = 0;
+ }
+ };
/**
- * parse an attribute statement like "node [shape=circle fontSize=16]".
- * Available keywords are 'node', 'edge', 'graph'.
- * The previous list with default attributes will be replaced
- * @param {Object} graph
- * @returns {String | null} keyword Returns the name of the parsed attribute
- * (node, edge, graph), or null if nothing
- * is parsed.
+ * This function draws the control nodes for the manipulator.
+ * In order to enable this, only set the this.controlNodesEnabled to true.
+ * @param ctx
*/
- function parseAttributeStatement (graph) {
- // attribute statements
- if (token == 'node') {
- getToken();
+ Edge.prototype._drawControlNodes = function(ctx) {
+ if (this.controlNodesEnabled == true) {
+ if (this.controlNodes.from === null && this.controlNodes.to === null) {
+ var nodeIdFrom = "edgeIdFrom:".concat(this.id);
+ var nodeIdTo = "edgeIdTo:".concat(this.id);
+ var constants = {
+ nodes:{group:'', radius:8},
+ physics:{damping:0},
+ clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}}
+ };
+ this.controlNodes.from = new Node(
+ {id:nodeIdFrom,
+ shape:'dot',
+ color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
+ },{},{},constants);
+ this.controlNodes.to = new Node(
+ {id:nodeIdTo,
+ shape:'dot',
+ color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
+ },{},{},constants);
+ }
- // node attributes
- graph.node = parseAttributeList();
- return 'node';
- }
- else if (token == 'edge') {
- getToken();
+ if (this.controlNodes.from.selected == false && this.controlNodes.to.selected == false) {
+ this.controlNodes.positions = this.getControlNodePositions(ctx);
+ this.controlNodes.from.x = this.controlNodes.positions.from.x;
+ this.controlNodes.from.y = this.controlNodes.positions.from.y;
+ this.controlNodes.to.x = this.controlNodes.positions.to.x;
+ this.controlNodes.to.y = this.controlNodes.positions.to.y;
+ }
- // edge attributes
- graph.edge = parseAttributeList();
- return 'edge';
+ this.controlNodes.from.draw(ctx);
+ this.controlNodes.to.draw(ctx);
}
- else if (token == 'graph') {
- getToken();
-
- // graph attributes
- graph.graph = parseAttributeList();
- return 'graph';
+ else {
+ this.controlNodes = {from:null, to:null, positions:{}};
}
+ };
- return null;
- }
+ /**
+ * Enable control nodes.
+ * @private
+ */
+ Edge.prototype._enableControlNodes = function() {
+ this.fromBackup = this.from;
+ this.toBackup = this.to;
+ this.controlNodesEnabled = true;
+ };
/**
- * parse a node statement
- * @param {Object} graph
- * @param {String | Number} id
+ * disable control nodes and remove from dynamicEdges from old node
+ * @private
*/
- function parseNodeStatement(graph, id) {
- // node statement
- var node = {
- id: id
- };
- var attr = parseAttributeList();
- if (attr) {
- node.attr = attr;
+ Edge.prototype._disableControlNodes = function() {
+ this.fromId = this.from.id;
+ this.toId = this.to.id;
+ if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges
+ this.fromBackup.detachEdge(this);
+ }
+ else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges
+ this.toBackup.detachEdge(this);
}
- addNode(graph, node);
- // edge statements
- parseEdge(graph, id);
- }
+ this.fromBackup = null;
+ this.toBackup = null;
+ this.controlNodesEnabled = false;
+ };
+
/**
- * Parse an edge or a series of edges
- * @param {Object} graph
- * @param {String | Number} from Id of the from node
+ * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null.
+ * @param x
+ * @param y
+ * @returns {null}
+ * @private
*/
- function parseEdge(graph, from) {
- while (token == '->' || token == '--') {
- var to;
- var type = token;
- getToken();
-
- var subgraph = parseSubgraph(graph);
- if (subgraph) {
- to = subgraph;
- }
- else {
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Identifier or subgraph expected');
- }
- to = token;
- addNode(graph, {
- id: to
- });
- getToken();
- }
+ Edge.prototype._getSelectedControlNode = function(x,y) {
+ var positions = this.controlNodes.positions;
+ var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2));
+ var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2));
- // parse edge attributes
- var attr = parseAttributeList();
+ if (fromDistance < 15) {
+ this.connectedNode = this.from;
+ this.from = this.controlNodes.from;
+ return this.controlNodes.from;
+ }
+ else if (toDistance < 15) {
+ this.connectedNode = this.to;
+ this.to = this.controlNodes.to;
+ return this.controlNodes.to;
+ }
+ else {
+ return null;
+ }
+ };
- // create edge
- var edge = createEdge(graph, from, to, type, attr);
- addEdge(graph, edge);
- from = to;
+ /**
+ * this resets the control nodes to their original position.
+ * @private
+ */
+ Edge.prototype._restoreControlNodes = function() {
+ if (this.controlNodes.from.selected == true) {
+ this.from = this.connectedNode;
+ this.connectedNode = null;
+ this.controlNodes.from.unselect();
}
- }
+ else if (this.controlNodes.to.selected == true) {
+ this.to = this.connectedNode;
+ this.connectedNode = null;
+ this.controlNodes.to.unselect();
+ }
+ };
/**
- * Parse a set with attributes,
- * for example [label="1.000", shape=solid]
- * @return {Object | null} attr
+ * this calculates the position of the control nodes on the edges of the parent nodes.
+ *
+ * @param ctx
+ * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
*/
- function parseAttributeList() {
- var attr = null;
-
- while (token == '[') {
- getToken();
- attr = {};
- while (token !== '' && token != ']') {
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Attribute name expected');
- }
- var name = token;
-
- getToken();
- if (token != '=') {
- throw newSyntaxError('Equal sign = expected');
- }
- getToken();
+ Edge.prototype.getControlNodePositions = function(ctx) {
+ var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
+ var dx = (this.to.x - this.from.x);
+ var dy = (this.to.y - this.from.y);
+ var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
+ var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
+ var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
+ var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
+ var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Attribute value expected');
- }
- var value = token;
- setValue(attr, name, value); // name can be a path
+ var via;
+ if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true) {
+ via = this.via;
+ }
+ else if (this.options.smoothCurves.enabled == true) {
+ via = this._getViaCoordinates();
+ }
- getToken();
- if (token ==',') {
- getToken();
- }
- }
+ if (this.options.smoothCurves.enabled == true && via.x != null) {
+ angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
+ dx = (this.to.x - via.x);
+ dy = (this.to.y - via.y);
+ edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
+ }
+ var toBorderDist = this.to.distanceToBorder(ctx, angle);
+ var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
- if (token != ']') {
- throw newSyntaxError('Bracket ] expected');
- }
- getToken();
+ var xTo,yTo;
+ if (this.options.smoothCurves.enabled == true && via.x != null) {
+ xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
+ yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
+ }
+ else {
+ xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
+ yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
}
- return attr;
- }
+ return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}};
+ };
- /**
- * Create a syntax error with extra information on current token and index.
- * @param {String} message
- * @returns {SyntaxError} err
- */
- function newSyntaxError(message) {
- return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
- }
+ module.exports = Edge;
- /**
- * Chop off text after a maximum length
- * @param {String} text
- * @param {Number} maxLength
- * @returns {String}
- */
- function chop (text, maxLength) {
- return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
- }
+/***/ },
+/* 58 */
+/***/ function(module, exports, __webpack_require__) {
/**
- * Execute a function fn for each pair of elements in two arrays
- * @param {Array | *} array1
- * @param {Array | *} array2
- * @param {function} fn
+ * Popup is a class to create a popup window with some text
+ * @param {Element} container The container object.
+ * @param {Number} [x]
+ * @param {Number} [y]
+ * @param {String} [text]
+ * @param {Object} [style] An object containing borderColor,
+ * backgroundColor, etc.
*/
- function forEach2(array1, array2, fn) {
- if (Array.isArray(array1)) {
- array1.forEach(function (elem1) {
- if (Array.isArray(array2)) {
- array2.forEach(function (elem2) {
- fn(elem1, elem2);
- });
- }
- else {
- fn(elem1, array2);
- }
- });
+ function Popup(container, x, y, text, style) {
+ if (container) {
+ this.container = container;
}
else {
- if (Array.isArray(array2)) {
- array2.forEach(function (elem2) {
- fn(array1, elem2);
- });
- }
- else {
- fn(array1, array2);
- }
+ this.container = document.body;
}
- }
-
- /**
- * Convert a string containing a graph in DOT language into a map containing
- * with nodes and edges in the format of graph.
- * @param {String} data Text containing a graph in DOT-notation
- * @return {Object} graphData
- */
- function DOTToGraph (data) {
- // parse the DOT file
- var dotData = parseDOT(data);
- var graphData = {
- nodes: [],
- edges: [],
- options: {}
- };
- // copy the nodes
- if (dotData.nodes) {
- dotData.nodes.forEach(function (dotNode) {
- var graphNode = {
- id: dotNode.id,
- label: String(dotNode.label || dotNode.id)
- };
- merge(graphNode, dotNode.attr);
- if (graphNode.image) {
- graphNode.shape = 'image';
+ // x, y and text are optional, see if a style object was passed in their place
+ if (style === undefined) {
+ if (typeof x === "object") {
+ style = x;
+ x = undefined;
+ } else if (typeof text === "object") {
+ style = text;
+ text = undefined;
+ } else {
+ // for backwards compatibility, in case clients other than Network are creating Popup directly
+ style = {
+ fontColor: 'black',
+ fontSize: 14, // px
+ fontFace: 'verdana',
+ color: {
+ border: '#666',
+ background: '#FFFFC6'
+ }
}
- graphData.nodes.push(graphNode);
- });
- }
-
- // copy the edges
- if (dotData.edges) {
- /**
- * Convert an edge in DOT format to an edge with VisGraph format
- * @param {Object} dotEdge
- * @returns {Object} graphEdge
- */
- var convertEdge = function (dotEdge) {
- var graphEdge = {
- from: dotEdge.from,
- to: dotEdge.to
- };
- merge(graphEdge, dotEdge.attr);
- graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
- return graphEdge;
}
+ }
- dotData.edges.forEach(function (dotEdge) {
- var from, to;
- if (dotEdge.from instanceof Object) {
- from = dotEdge.from.nodes;
- }
- else {
- from = {
- id: dotEdge.from
- }
- }
+ this.x = 0;
+ this.y = 0;
+ this.padding = 5;
- if (dotEdge.to instanceof Object) {
- to = dotEdge.to.nodes;
- }
- else {
- to = {
- id: dotEdge.to
- }
- }
+ if (x !== undefined && y !== undefined ) {
+ this.setPosition(x, y);
+ }
+ if (text !== undefined) {
+ this.setText(text);
+ }
- if (dotEdge.from instanceof Object && dotEdge.from.edges) {
- dotEdge.from.edges.forEach(function (subEdge) {
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
- }
+ // create the frame
+ this.frame = document.createElement("div");
+ var styleAttr = this.frame.style;
+ styleAttr.position = "absolute";
+ styleAttr.visibility = "hidden";
+ styleAttr.border = "1px solid " + style.color.border;
+ styleAttr.color = style.fontColor;
+ styleAttr.fontSize = style.fontSize + "px";
+ styleAttr.fontFamily = style.fontFace;
+ styleAttr.padding = this.padding + "px";
+ styleAttr.backgroundColor = style.color.background;
+ styleAttr.borderRadius = "3px";
+ styleAttr.MozBorderRadius = "3px";
+ styleAttr.WebkitBorderRadius = "3px";
+ styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
+ styleAttr.whiteSpace = "nowrap";
+ this.container.appendChild(this.frame);
+ }
- forEach2(from, to, function (from, to) {
- var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
+ /**
+ * @param {number} x Horizontal position of the popup window
+ * @param {number} y Vertical position of the popup window
+ */
+ Popup.prototype.setPosition = function(x, y) {
+ this.x = parseInt(x);
+ this.y = parseInt(y);
+ };
- if (dotEdge.to instanceof Object && dotEdge.to.edges) {
- dotEdge.to.edges.forEach(function (subEdge) {
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
- }
- });
+ /**
+ * Set the content for the popup window. This can be HTML code or text.
+ * @param {string | Element} content
+ */
+ Popup.prototype.setText = function(content) {
+ if (content instanceof Element) {
+ this.frame.innerHTML = '';
+ this.frame.appendChild(content);
}
-
- // copy the options
- if (dotData.attr) {
- graphData.options = dotData.attr;
+ else {
+ this.frame.innerHTML = content; // string containing text or HTML
}
+ };
- return graphData;
- }
-
- // exports
- exports.parseDOT = parseDOT;
- exports.DOTToGraph = DOTToGraph;
+ /**
+ * Show the popup window
+ * @param {boolean} show Optional. Show or hide the window
+ */
+ Popup.prototype.show = function (show) {
+ if (show === undefined) {
+ show = true;
+ }
+ if (show) {
+ var height = this.frame.clientHeight;
+ var width = this.frame.clientWidth;
+ var maxHeight = this.frame.parentNode.clientHeight;
+ var maxWidth = this.frame.parentNode.clientWidth;
-/***/ },
-/* 58 */
-/***/ function(module, exports, __webpack_require__) {
+ var top = (this.y - height);
+ if (top + height + this.padding > maxHeight) {
+ top = maxHeight - height - this.padding;
+ }
+ if (top < this.padding) {
+ top = this.padding;
+ }
-
- function parseGephi(gephiJSON, options) {
- var edges = [];
- var nodes = [];
- this.options = {
- edges: {
- inheritColor: true
- },
- nodes: {
- allowedToMove: false,
- parseColor: false
+ var left = this.x;
+ if (left + width + this.padding > maxWidth) {
+ left = maxWidth - width - this.padding;
+ }
+ if (left < this.padding) {
+ left = this.padding;
}
- };
- if (options !== undefined) {
- this.options.nodes['allowedToMove'] = options.allowedToMove | false;
- this.options.nodes['parseColor'] = options.parseColor | false;
- this.options.edges['inheritColor'] = options.inheritColor | true;
+ this.frame.style.left = left + "px";
+ this.frame.style.top = top + "px";
+ this.frame.style.visibility = "visible";
}
-
- var gEdges = gephiJSON.edges;
- var gNodes = gephiJSON.nodes;
- for (var i = 0; i < gEdges.length; i++) {
- var edge = {};
- var gEdge = gEdges[i];
- edge['id'] = gEdge.id;
- edge['from'] = gEdge.source;
- edge['to'] = gEdge.target;
- edge['attributes'] = gEdge.attributes;
- // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined;
- // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size;
- edge['color'] = gEdge.color;
- edge['inheritColor'] = edge['color'] !== undefined ? false : this.options.inheritColor;
- edges.push(edge);
+ else {
+ this.hide();
}
+ };
- for (var i = 0; i < gNodes.length; i++) {
- var node = {};
- var gNode = gNodes[i];
- node['id'] = gNode.id;
- node['attributes'] = gNode.attributes;
- node['x'] = gNode.x;
- node['y'] = gNode.y;
- node['label'] = gNode.label;
- if (this.options.nodes.parseColor == true) {
- node['color'] = gNode.color;
- }
- else {
- node['color'] = gNode.color !== undefined ? {background:gNode.color, border:gNode.color} : undefined;
- }
- node['radius'] = gNode.size;
- node['allowedToMoveX'] = this.options.nodes.allowedToMove;
- node['allowedToMoveY'] = this.options.nodes.allowedToMove;
- nodes.push(node);
- }
+ /**
+ * Hide the popup window
+ */
+ Popup.prototype.hide = function () {
+ this.frame.style.visibility = "hidden";
+ };
- return {nodes:nodes, edges:edges};
- }
+ module.exports = Popup;
- exports.parseGephi = parseGephi;
/***/ },
/* 59 */
/***/ function(module, exports, __webpack_require__) {
- var PhysicsMixin = __webpack_require__(62);
- var ClusterMixin = __webpack_require__(63);
- var SectorsMixin = __webpack_require__(64);
- var SelectionMixin = __webpack_require__(65);
- var ManipulationMixin = __webpack_require__(66);
- var NavigationMixin = __webpack_require__(67);
- var HierarchicalLayoutMixin = __webpack_require__(68);
+ var PhysicsMixin = __webpack_require__(60);
+ var ClusterMixin = __webpack_require__(64);
+ var SectorsMixin = __webpack_require__(65);
+ var SelectionMixin = __webpack_require__(66);
+ var ManipulationMixin = __webpack_require__(67);
+ var NavigationMixin = __webpack_require__(68);
+ var HierarchicalLayoutMixin = __webpack_require__(69);
/**
* Load a mixin into the network object
@@ -28817,478 +28811,876 @@ return /******/ (function(modules) { // webpackBootstrap
/* 60 */
/***/ function(module, exports, __webpack_require__) {
- // English
- exports['en'] = {
- edit: 'Edit',
- del: 'Delete selected',
- back: 'Back',
- addNode: 'Add Node',
- addEdge: 'Add Edge',
- editNode: 'Edit Node',
- editEdge: 'Edit Edge',
- addDescription: 'Click in an empty space to place a new node.',
- edgeDescription: 'Click on a node and drag the edge to another node to connect them.',
- editEdgeDescription: 'Click on the control points and drag them to a node to connect to it.',
- createEdgeError: 'Cannot link edges to a cluster.',
- deleteClusterError: 'Clusters cannot be deleted.'
+ var util = __webpack_require__(1);
+ var RepulsionMixin = __webpack_require__(61);
+ var HierarchialRepulsionMixin = __webpack_require__(62);
+ var BarnesHutMixin = __webpack_require__(63);
+
+ /**
+ * Toggling barnes Hut calculation on and off.
+ *
+ * @private
+ */
+ exports._toggleBarnesHut = function () {
+ this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
+ this._loadSelectedForceSolver();
+ this.moving = true;
+ this.start();
};
- exports['en_EN'] = exports['en'];
- exports['en_US'] = exports['en'];
- // Dutch
- exports['nl'] = {
- edit: 'Wijzigen',
- del: 'Selectie verwijderen',
- back: 'Terug',
- addNode: 'Node toevoegen',
- addEdge: 'Link toevoegen',
- editNode: 'Node wijzigen',
- editEdge: 'Link wijzigen',
- addDescription: 'Klik op een leeg gebied om een nieuwe node te maken.',
- edgeDescription: 'Klik op een node en sleep de link naar een andere node om ze te verbinden.',
- editEdgeDescription: 'Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.',
- createEdgeError: 'Kan geen link maken naar een cluster.',
- deleteClusterError: 'Clusters kunnen niet worden verwijderd.'
+
+ /**
+ * This loads the node force solver based on the barnes hut or repulsion algorithm
+ *
+ * @private
+ */
+ exports._loadSelectedForceSolver = function () {
+ // this overloads the this._calculateNodeForces
+ if (this.constants.physics.barnesHut.enabled == true) {
+ this._clearMixin(RepulsionMixin);
+ this._clearMixin(HierarchialRepulsionMixin);
+
+ this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
+ this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
+ this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
+ this.constants.physics.damping = this.constants.physics.barnesHut.damping;
+
+ this._loadMixin(BarnesHutMixin);
+ }
+ else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
+ this._clearMixin(BarnesHutMixin);
+ this._clearMixin(RepulsionMixin);
+
+ this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
+ this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
+ this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
+ this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
+
+ this._loadMixin(HierarchialRepulsionMixin);
+ }
+ else {
+ this._clearMixin(BarnesHutMixin);
+ this._clearMixin(HierarchialRepulsionMixin);
+ this.barnesHutTree = undefined;
+
+ this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
+ this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
+ this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
+ this.constants.physics.damping = this.constants.physics.repulsion.damping;
+
+ this._loadMixin(RepulsionMixin);
+ }
};
- exports['nl_NL'] = exports['nl'];
- exports['nl_BE'] = exports['nl'];
+ /**
+ * Before calculating the forces, we check if we need to cluster to keep up performance and we check
+ * if there is more than one node. If it is just one node, we dont calculate anything.
+ *
+ * @private
+ */
+ exports._initializeForceCalculation = function () {
+ // stop calculation if there is only one node
+ if (this.nodeIndices.length == 1) {
+ this.nodes[this.nodeIndices[0]]._setForce(0, 0);
+ }
+ else {
+ // if there are too many nodes on screen, we cluster without repositioning
+ if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
+ this.clusterToFit(this.constants.clustering.reduceToNodes, false);
+ }
+
+ // we now start the force calculation
+ this._calculateForces();
+ }
+ };
-/***/ },
-/* 61 */
-/***/ function(module, exports, __webpack_require__) {
/**
- * Canvas shapes used by Network
+ * Calculate the external forces acting on the nodes
+ * Forces are caused by: edges, repulsing forces between nodes, gravity
+ * @private
*/
- if (typeof CanvasRenderingContext2D !== 'undefined') {
+ exports._calculateForces = function () {
+ // Gravity is required to keep separated groups from floating off
+ // the forces are reset to zero in this loop by using _setForce instead
+ // of _addForce
- /**
- * Draw a circle shape
- */
- CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
- this.beginPath();
- this.arc(x, y, r, 0, 2*Math.PI, false);
- };
+ this._calculateGravitationalForces();
+ this._calculateNodeForces();
- /**
- * Draw a square shape
- * @param {Number} x horizontal center
- * @param {Number} y vertical center
- * @param {Number} r size, width and height of the square
- */
- CanvasRenderingContext2D.prototype.square = function(x, y, r) {
- this.beginPath();
- this.rect(x - r, y - r, r * 2, r * 2);
- };
+ if (this.constants.physics.springConstant > 0) {
+ if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
+ this._calculateSpringForcesWithSupport();
+ }
+ else {
+ if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
+ this._calculateHierarchicalSpringForces();
+ }
+ else {
+ this._calculateSpringForces();
+ }
+ }
+ }
+ };
- /**
- * Draw a triangle shape
- * @param {Number} x horizontal center
- * @param {Number} y vertical center
- * @param {Number} r radius, half the length of the sides of the triangle
- */
- CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
- // http://en.wikipedia.org/wiki/Equilateral_triangle
- this.beginPath();
- var s = r * 2;
- var s2 = s / 2;
- var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
- var h = Math.sqrt(s * s - s2 * s2); // height
+ /**
+ * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
+ * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
+ * This function joins the datanodes and invisible (called support) nodes into one object.
+ * We do this so we do not contaminate this.nodes with the support nodes.
+ *
+ * @private
+ */
+ exports._updateCalculationNodes = function () {
+ if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
+ this.calculationNodes = {};
+ this.calculationNodeIndices = [];
- this.moveTo(x, y - (h - ir));
- this.lineTo(x + s2, y + ir);
- this.lineTo(x - s2, y + ir);
- this.lineTo(x, y - (h - ir));
- this.closePath();
- };
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ this.calculationNodes[nodeId] = this.nodes[nodeId];
+ }
+ }
+ var supportNodes = this.sectors['support']['nodes'];
+ for (var supportNodeId in supportNodes) {
+ if (supportNodes.hasOwnProperty(supportNodeId)) {
+ if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
+ this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
+ }
+ else {
+ supportNodes[supportNodeId]._setForce(0, 0);
+ }
+ }
+ }
- /**
- * Draw a triangle shape in downward orientation
- * @param {Number} x horizontal center
- * @param {Number} y vertical center
- * @param {Number} r radius
- */
- CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
- // http://en.wikipedia.org/wiki/Equilateral_triangle
- this.beginPath();
+ for (var idx in this.calculationNodes) {
+ if (this.calculationNodes.hasOwnProperty(idx)) {
+ this.calculationNodeIndices.push(idx);
+ }
+ }
+ }
+ else {
+ this.calculationNodes = this.nodes;
+ this.calculationNodeIndices = this.nodeIndices;
+ }
+ };
- var s = r * 2;
- var s2 = s / 2;
- var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
- var h = Math.sqrt(s * s - s2 * s2); // height
- this.moveTo(x, y + (h - ir));
- this.lineTo(x + s2, y - ir);
- this.lineTo(x - s2, y - ir);
- this.lineTo(x, y + (h - ir));
- this.closePath();
- };
+ /**
+ * this function applies the central gravity effect to keep groups from floating off
+ *
+ * @private
+ */
+ exports._calculateGravitationalForces = function () {
+ var dx, dy, distance, node, i;
+ var nodes = this.calculationNodes;
+ var gravity = this.constants.physics.centralGravity;
+ var gravityForce = 0;
- /**
- * Draw a star shape, a star with 5 points
- * @param {Number} x horizontal center
- * @param {Number} y vertical center
- * @param {Number} r radius, half the length of the sides of the triangle
- */
- CanvasRenderingContext2D.prototype.star = function(x, y, r) {
- // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
- this.beginPath();
+ for (i = 0; i < this.calculationNodeIndices.length; i++) {
+ node = nodes[this.calculationNodeIndices[i]];
+ node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
+ // gravity does not apply when we are in a pocket sector
+ if (this._sector() == "default" && gravity != 0) {
+ dx = -node.x;
+ dy = -node.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
- for (var n = 0; n < 10; n++) {
- var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
- this.lineTo(
- x + radius * Math.sin(n * 2 * Math.PI / 10),
- y - radius * Math.cos(n * 2 * Math.PI / 10)
- );
+ gravityForce = (distance == 0) ? 0 : (gravity / distance);
+ node.fx = dx * gravityForce;
+ node.fy = dy * gravityForce;
+ }
+ else {
+ node.fx = 0;
+ node.fy = 0;
}
+ }
+ };
- this.closePath();
- };
- /**
- * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
- */
- CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
- var r2d = Math.PI/180;
- if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
- if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
- this.beginPath();
- this.moveTo(x+r,y);
- this.lineTo(x+w-r,y);
- this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
- this.lineTo(x+w,y+h-r);
- this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
- this.lineTo(x+r,y+h);
- this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
- this.lineTo(x,y+r);
- this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
- };
- /**
- * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
- */
- CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
- var kappa = .5522848,
- ox = (w / 2) * kappa, // control point offset horizontal
- oy = (h / 2) * kappa, // control point offset vertical
- xe = x + w, // x-end
- ye = y + h, // y-end
- xm = x + w / 2, // x-middle
- ym = y + h / 2; // y-middle
- this.beginPath();
- this.moveTo(x, ym);
- this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
- this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
- this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
- this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
- };
+ /**
+ * this function calculates the effects of the springs in the case of unsmooth curves.
+ *
+ * @private
+ */
+ exports._calculateSpringForces = function () {
+ var edgeLength, edge, edgeId;
+ var dx, dy, fx, fy, springForce, distance;
+ var edges = this.edges;
+
+ // forces caused by the edges, modelled as springs
+ for (edgeId in edges) {
+ if (edges.hasOwnProperty(edgeId)) {
+ edge = edges[edgeId];
+ if (edge.connected) {
+ // only calculate forces if nodes are in the same sector
+ if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
+ edgeLength = edge.physics.springLength;
+ // this implies that the edges between big clusters are longer
+ edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
+
+ dx = (edge.from.x - edge.to.x);
+ dy = (edge.from.y - edge.to.y);
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance == 0) {
+ distance = 0.01;
+ }
+
+ // the 1/distance is so the fx and fy can be calculated without sine or cosine.
+ springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
+
+ fx = dx * springForce;
+ fy = dy * springForce;
+
+ edge.from.fx += fx;
+ edge.from.fy += fy;
+ edge.to.fx -= fx;
+ edge.to.fy -= fy;
+ }
+ }
+ }
+ }
+ };
+
+
+
+
+ /**
+ * This function calculates the springforces on the nodes, accounting for the support nodes.
+ *
+ * @private
+ */
+ exports._calculateSpringForcesWithSupport = function () {
+ var edgeLength, edge, edgeId, combinedClusterSize;
+ var edges = this.edges;
+
+ // forces caused by the edges, modelled as springs
+ for (edgeId in edges) {
+ if (edges.hasOwnProperty(edgeId)) {
+ edge = edges[edgeId];
+ if (edge.connected) {
+ // only calculate forces if nodes are in the same sector
+ if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
+ if (edge.via != null) {
+ var node1 = edge.to;
+ var node2 = edge.via;
+ var node3 = edge.from;
+
+ edgeLength = edge.physics.springLength;
+
+ combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
+ // this implies that the edges between big clusters are longer
+ edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
+ this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
+ this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
+ }
+ }
+ }
+ }
+ }
+ };
- /**
- * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
- */
- CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
- var f = 1/3;
- var wEllipse = w;
- var hEllipse = h * f;
+ /**
+ * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
+ *
+ * @param node1
+ * @param node2
+ * @param edgeLength
+ * @private
+ */
+ exports._calculateSpringForce = function (node1, node2, edgeLength) {
+ var dx, dy, fx, fy, springForce, distance;
- var kappa = .5522848,
- ox = (wEllipse / 2) * kappa, // control point offset horizontal
- oy = (hEllipse / 2) * kappa, // control point offset vertical
- xe = x + wEllipse, // x-end
- ye = y + hEllipse, // y-end
- xm = x + wEllipse / 2, // x-middle
- ym = y + hEllipse / 2, // y-middle
- ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
- yeb = y + h; // y-end, bottom ellipse
+ dx = (node1.x - node2.x);
+ dy = (node1.y - node2.y);
+ distance = Math.sqrt(dx * dx + dy * dy);
- this.beginPath();
- this.moveTo(xe, ym);
+ if (distance == 0) {
+ distance = 0.01;
+ }
- this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
- this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
+ // the 1/distance is so the fx and fy can be calculated without sine or cosine.
+ springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
- this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
- this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
+ fx = dx * springForce;
+ fy = dy * springForce;
- this.lineTo(xe, ymb);
+ node1.fx += fx;
+ node1.fy += fy;
+ node2.fx -= fx;
+ node2.fy -= fy;
+ };
- this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
- this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
- this.lineTo(x, ym);
- };
+ exports._cleanupPhysicsConfiguration = function() {
+ if (this.physicsConfiguration !== undefined) {
+ while (this.physicsConfiguration.hasChildNodes()) {
+ this.physicsConfiguration.removeChild(this.physicsConfiguration.firstChild);
+ }
+ this.physicsConfiguration.parentNode.removeChild(this.physicsConfiguration);
+ this.physicsConfiguration = undefined;
+ }
+ }
- /**
- * Draw an arrow point (no line)
- */
- CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
- // tail
- var xt = x - length * Math.cos(angle);
- var yt = y - length * Math.sin(angle);
+ /**
+ * Load the HTML for the physics config and bind it
+ * @private
+ */
+ exports._loadPhysicsConfiguration = function () {
+ if (this.physicsConfiguration === undefined) {
+ this.backupConstants = {};
+ util.deepExtend(this.backupConstants,this.constants);
- // inner tail
- // TODO: allow to customize different shapes
- var xi = x - length * 0.9 * Math.cos(angle);
- var yi = y - length * 0.9 * Math.sin(angle);
+ var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
+ this.physicsConfiguration = document.createElement('div');
+ this.physicsConfiguration.className = "PhysicsConfiguration";
+ this.physicsConfiguration.innerHTML = '' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ ''
+ this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
+ this.optionsDiv = document.createElement("div");
+ this.optionsDiv.style.fontSize = "14px";
+ this.optionsDiv.style.fontFamily = "verdana";
+ this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
- // left
- var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
- var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
+ var rangeElement;
+ rangeElement = document.getElementById('graph_BH_gc');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
+ rangeElement = document.getElementById('graph_BH_cg');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
+ rangeElement = document.getElementById('graph_BH_sc');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
+ rangeElement = document.getElementById('graph_BH_sl');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
+ rangeElement = document.getElementById('graph_BH_damp');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
- // right
- var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
- var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
+ rangeElement = document.getElementById('graph_R_nd');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
+ rangeElement = document.getElementById('graph_R_cg');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
+ rangeElement = document.getElementById('graph_R_sc');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
+ rangeElement = document.getElementById('graph_R_sl');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
+ rangeElement = document.getElementById('graph_R_damp');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
- this.beginPath();
- this.moveTo(x, y);
- this.lineTo(xl, yl);
- this.lineTo(xi, yi);
- this.lineTo(xr, yr);
- this.closePath();
- };
+ rangeElement = document.getElementById('graph_H_nd');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
+ rangeElement = document.getElementById('graph_H_cg');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
+ rangeElement = document.getElementById('graph_H_sc');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
+ rangeElement = document.getElementById('graph_H_sl');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
+ rangeElement = document.getElementById('graph_H_damp');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
+ rangeElement = document.getElementById('graph_H_direction');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
+ rangeElement = document.getElementById('graph_H_levsep');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
+ rangeElement = document.getElementById('graph_H_nspac');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
- /**
- * Sets up the dashedLine functionality for drawing
- * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
- * @author David Jordan
- * @date 2012-08-08
- */
- CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
- if (!dashArray) dashArray=[10,5];
- if (dashLength==0) dashLength = 0.001; // Hack for Safari
- var dashCount = dashArray.length;
- this.moveTo(x, y);
- var dx = (x2-x), dy = (y2-y);
- var slope = dy/dx;
- var distRemaining = Math.sqrt( dx*dx + dy*dy );
- var dashIndex=0, draw=true;
- while (distRemaining>=0.1){
- var dashLength = dashArray[dashIndex++%dashCount];
- if (dashLength > distRemaining) dashLength = distRemaining;
- var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
- if (dx<0) xStep = -xStep;
- x += xStep;
- y += slope*xStep;
- this[draw ? 'lineTo' : 'moveTo'](x,y);
- distRemaining -= dashLength;
- draw = !draw;
+ var radioButton1 = document.getElementById("graph_physicsMethod1");
+ var radioButton2 = document.getElementById("graph_physicsMethod2");
+ var radioButton3 = document.getElementById("graph_physicsMethod3");
+ radioButton2.checked = true;
+ if (this.constants.physics.barnesHut.enabled) {
+ radioButton1.checked = true;
+ }
+ if (this.constants.hierarchicalLayout.enabled) {
+ radioButton3.checked = true;
}
- };
- // TODO: add diamond shape
- }
+ var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
+ var graph_repositionNodes = document.getElementById("graph_repositionNodes");
+ var graph_generateOptions = document.getElementById("graph_generateOptions");
+ graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
+ graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
+ graph_generateOptions.onclick = graphGenerateOptions.bind(this);
+ if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) {
+ graph_toggleSmooth.style.background = "#A4FF56";
+ }
+ else {
+ graph_toggleSmooth.style.background = "#FF8532";
+ }
-/***/ },
-/* 62 */
-/***/ function(module, exports, __webpack_require__) {
- var util = __webpack_require__(1);
- var RepulsionMixin = __webpack_require__(69);
- var HierarchialRepulsionMixin = __webpack_require__(70);
- var BarnesHutMixin = __webpack_require__(71);
+ switchConfigurations.apply(this);
+
+ radioButton1.onchange = switchConfigurations.bind(this);
+ radioButton2.onchange = switchConfigurations.bind(this);
+ radioButton3.onchange = switchConfigurations.bind(this);
+ }
+ };
/**
- * Toggling barnes Hut calculation on and off.
+ * This overwrites the this.constants.
*
+ * @param constantsVariableName
+ * @param value
* @private
*/
- exports._toggleBarnesHut = function () {
- this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
- this._loadSelectedForceSolver();
- this.moving = true;
- this.start();
+ exports._overWriteGraphConstants = function (constantsVariableName, value) {
+ var nameArray = constantsVariableName.split("_");
+ if (nameArray.length == 1) {
+ this.constants[nameArray[0]] = value;
+ }
+ else if (nameArray.length == 2) {
+ this.constants[nameArray[0]][nameArray[1]] = value;
+ }
+ else if (nameArray.length == 3) {
+ this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
+ }
};
/**
- * This loads the node force solver based on the barnes hut or repulsion algorithm
- *
- * @private
+ * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
*/
- exports._loadSelectedForceSolver = function () {
- // this overloads the this._calculateNodeForces
- if (this.constants.physics.barnesHut.enabled == true) {
- this._clearMixin(RepulsionMixin);
- this._clearMixin(HierarchialRepulsionMixin);
+ function graphToggleSmoothCurves () {
+ this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled;
+ var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
+ if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";}
+ else {graph_toggleSmooth.style.background = "#FF8532";}
- this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
- this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
- this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
- this.constants.physics.damping = this.constants.physics.barnesHut.damping;
+ this._configureSmoothCurves(false);
+ }
- this._loadMixin(BarnesHutMixin);
+ /**
+ * this function is used to scramble the nodes
+ *
+ */
+ function graphRepositionNodes () {
+ for (var nodeId in this.calculationNodes) {
+ if (this.calculationNodes.hasOwnProperty(nodeId)) {
+ this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
+ this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
+ }
}
- else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
- this._clearMixin(BarnesHutMixin);
- this._clearMixin(RepulsionMixin);
-
- this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
- this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
- this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
- this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
+ if (this.constants.hierarchicalLayout.enabled == true) {
+ this._setupHierarchicalLayout();
+ showValueOfRange.call(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
+ showValueOfRange.call(this, 'graph_H_cg', 1, "physics_centralGravity");
+ showValueOfRange.call(this, 'graph_H_sc', 1, "physics_springConstant");
+ showValueOfRange.call(this, 'graph_H_sl', 1, "physics_springLength");
+ showValueOfRange.call(this, 'graph_H_damp', 1, "physics_damping");
+ }
+ else {
+ this.repositionNodes();
+ }
+ this.moving = true;
+ this.start();
+ }
- this._loadMixin(HierarchialRepulsionMixin);
+ /**
+ * this is used to generate an options file from the playing with physics system.
+ */
+ function graphGenerateOptions () {
+ var options = "No options are required, default values used.";
+ var optionsSpecific = [];
+ var radioButton1 = document.getElementById("graph_physicsMethod1");
+ var radioButton2 = document.getElementById("graph_physicsMethod2");
+ if (radioButton1.checked == true) {
+ if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
+ if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
+ if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
+ if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
+ if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
+ if (optionsSpecific.length != 0) {
+ options = "var options = {";
+ options += "physics: {barnesHut: {";
+ for (var i = 0; i < optionsSpecific.length; i++) {
+ options += optionsSpecific[i];
+ if (i < optionsSpecific.length - 1) {
+ options += ", "
+ }
+ }
+ options += '}}'
+ }
+ if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) {
+ if (optionsSpecific.length == 0) {options = "var options = {";}
+ else {options += ", "}
+ options += "smoothCurves: " + this.constants.smoothCurves.enabled;
+ }
+ if (options != "No options are required, default values used.") {
+ options += '};'
+ }
+ }
+ else if (radioButton2.checked == true) {
+ options = "var options = {";
+ options += "physics: {barnesHut: {enabled: false}";
+ if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
+ if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
+ if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
+ if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
+ if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
+ if (optionsSpecific.length != 0) {
+ options += ", repulsion: {";
+ for (var i = 0; i < optionsSpecific.length; i++) {
+ options += optionsSpecific[i];
+ if (i < optionsSpecific.length - 1) {
+ options += ", "
+ }
+ }
+ options += '}}'
+ }
+ if (optionsSpecific.length == 0) {options += "}"}
+ if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
+ options += ", smoothCurves: " + this.constants.smoothCurves;
+ }
+ options += '};'
}
else {
- this._clearMixin(BarnesHutMixin);
- this._clearMixin(HierarchialRepulsionMixin);
- this.barnesHutTree = undefined;
+ options = "var options = {";
+ if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
+ if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
+ if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
+ if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
+ if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
+ if (optionsSpecific.length != 0) {
+ options += "physics: {hierarchicalRepulsion: {";
+ for (var i = 0; i < optionsSpecific.length; i++) {
+ options += optionsSpecific[i];
+ if (i < optionsSpecific.length - 1) {
+ options += ", ";
+ }
+ }
+ options += '}},';
+ }
+ options += 'hierarchicalLayout: {';
+ optionsSpecific = [];
+ if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
+ if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
+ if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
+ if (optionsSpecific.length != 0) {
+ for (var i = 0; i < optionsSpecific.length; i++) {
+ options += optionsSpecific[i];
+ if (i < optionsSpecific.length - 1) {
+ options += ", "
+ }
+ }
+ options += '}'
+ }
+ else {
+ options += "enabled:true}";
+ }
+ options += '};'
+ }
- this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
- this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
- this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
- this.constants.physics.damping = this.constants.physics.repulsion.damping;
- this._loadMixin(RepulsionMixin);
- }
- };
+ this.optionsDiv.innerHTML = options;
+ }
+
+ /**
+ * this is used to switch between barnesHut, repulsion and hierarchical.
+ *
+ */
+ function switchConfigurations () {
+ var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
+ var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
+ var tableId = "graph_" + radioButton + "_table";
+ var table = document.getElementById(tableId);
+ table.style.display = "block";
+ for (var i = 0; i < ids.length; i++) {
+ if (ids[i] != tableId) {
+ table = document.getElementById(ids[i]);
+ table.style.display = "none";
+ }
+ }
+ this._restoreNodes();
+ if (radioButton == "R") {
+ this.constants.hierarchicalLayout.enabled = false;
+ this.constants.physics.hierarchicalRepulsion.enabled = false;
+ this.constants.physics.barnesHut.enabled = false;
+ }
+ else if (radioButton == "H") {
+ if (this.constants.hierarchicalLayout.enabled == false) {
+ this.constants.hierarchicalLayout.enabled = true;
+ this.constants.physics.hierarchicalRepulsion.enabled = true;
+ this.constants.physics.barnesHut.enabled = false;
+ this.constants.smoothCurves.enabled = false;
+ this._setupHierarchicalLayout();
+ }
+ }
+ else {
+ this.constants.hierarchicalLayout.enabled = false;
+ this.constants.physics.hierarchicalRepulsion.enabled = false;
+ this.constants.physics.barnesHut.enabled = true;
+ }
+ this._loadSelectedForceSolver();
+ var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
+ if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";}
+ else {graph_toggleSmooth.style.background = "#FF8532";}
+ this.moving = true;
+ this.start();
+ }
+
/**
- * Before calculating the forces, we check if we need to cluster to keep up performance and we check
- * if there is more than one node. If it is just one node, we dont calculate anything.
+ * this generates the ranges depending on the iniital values.
*
- * @private
+ * @param id
+ * @param map
+ * @param constantsVariableName
*/
- exports._initializeForceCalculation = function () {
- // stop calculation if there is only one node
- if (this.nodeIndices.length == 1) {
- this.nodes[this.nodeIndices[0]]._setForce(0, 0);
+ function showValueOfRange (id,map,constantsVariableName) {
+ var valueId = id + "_value";
+ var rangeValue = document.getElementById(id).value;
+
+ if (Array.isArray(map)) {
+ document.getElementById(valueId).value = map[parseInt(rangeValue)];
+ this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
}
else {
- // if there are too many nodes on screen, we cluster without repositioning
- if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
- this.clusterToFit(this.constants.clustering.reduceToNodes, false);
- }
-
- // we now start the force calculation
- this._calculateForces();
+ document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
+ this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
}
- };
+ if (constantsVariableName == "hierarchicalLayout_direction" ||
+ constantsVariableName == "hierarchicalLayout_levelSeparation" ||
+ constantsVariableName == "hierarchicalLayout_nodeSpacing") {
+ this._setupHierarchicalLayout();
+ }
+ this.moving = true;
+ this.start();
+ }
- /**
- * Calculate the external forces acting on the nodes
- * Forces are caused by: edges, repulsing forces between nodes, gravity
- * @private
- */
- exports._calculateForces = function () {
- // Gravity is required to keep separated groups from floating off
- // the forces are reset to zero in this loop by using _setForce instead
- // of _addForce
- this._calculateGravitationalForces();
- this._calculateNodeForces();
- if (this.constants.physics.springConstant > 0) {
- if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
- this._calculateSpringForcesWithSupport();
- }
- else {
- if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
- this._calculateHierarchicalSpringForces();
- }
- else {
- this._calculateSpringForces();
- }
- }
- }
- };
+/***/ },
+/* 61 */
+/***/ function(module, exports, __webpack_require__) {
/**
- * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
- * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
- * This function joins the datanodes and invisible (called support) nodes into one object.
- * We do this so we do not contaminate this.nodes with the support nodes.
+ * Calculate the forces the nodes apply on each other based on a repulsion field.
+ * This field is linearly approximated.
*
* @private
*/
- exports._updateCalculationNodes = function () {
- if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
- this.calculationNodes = {};
- this.calculationNodeIndices = [];
+ exports._calculateNodeForces = function () {
+ var dx, dy, angle, distance, fx, fy, combinedClusterSize,
+ repulsingForce, node1, node2, i, j;
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- this.calculationNodes[nodeId] = this.nodes[nodeId];
- }
- }
- var supportNodes = this.sectors['support']['nodes'];
- for (var supportNodeId in supportNodes) {
- if (supportNodes.hasOwnProperty(supportNodeId)) {
- if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
- this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
+ var nodes = this.calculationNodes;
+ var nodeIndices = this.calculationNodeIndices;
+
+ // approximation constants
+ var a_base = -2 / 3;
+ var b = 4 / 3;
+
+ // repulsing forces between nodes
+ var nodeDistance = this.constants.physics.repulsion.nodeDistance;
+ var minimumDistance = nodeDistance;
+
+ // we loop from i over all but the last entree in the array
+ // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
+ for (i = 0; i < nodeIndices.length - 1; i++) {
+ node1 = nodes[nodeIndices[i]];
+ for (j = i + 1; j < nodeIndices.length; j++) {
+ node2 = nodes[nodeIndices[j]];
+ combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
+
+ dx = node2.x - node1.x;
+ dy = node2.y - node1.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+ minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
+ var a = a_base / minimumDistance;
+ if (distance < 2 * minimumDistance) {
+ if (distance < 0.5 * minimumDistance) {
+ repulsingForce = 1.0;
}
else {
- supportNodes[supportNodeId]._setForce(0, 0);
+ repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
}
- }
- }
+ // amplify the repulsion for clusters.
+ repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
+ repulsingForce = repulsingForce / Math.max(distance,0.01*minimumDistance);
+
+ fx = dx * repulsingForce;
+ fy = dy * repulsingForce;
+
+ node1.fx -= fx;
+ node1.fy -= fy;
+ node2.fx += fx;
+ node2.fy += fy;
- for (var idx in this.calculationNodes) {
- if (this.calculationNodes.hasOwnProperty(idx)) {
- this.calculationNodeIndices.push(idx);
}
}
}
- else {
- this.calculationNodes = this.nodes;
- this.calculationNodeIndices = this.nodeIndices;
- }
};
+/***/ },
+/* 62 */
+/***/ function(module, exports, __webpack_require__) {
+
/**
- * this function applies the central gravity effect to keep groups from floating off
+ * Calculate the forces the nodes apply on eachother based on a repulsion field.
+ * This field is linearly approximated.
*
* @private
*/
- exports._calculateGravitationalForces = function () {
- var dx, dy, distance, node, i;
+ exports._calculateNodeForces = function () {
+ var dx, dy, distance, fx, fy,
+ repulsingForce, node1, node2, i, j;
+
var nodes = this.calculationNodes;
- var gravity = this.constants.physics.centralGravity;
- var gravityForce = 0;
+ var nodeIndices = this.calculationNodeIndices;
- for (i = 0; i < this.calculationNodeIndices.length; i++) {
- node = nodes[this.calculationNodeIndices[i]];
- node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
- // gravity does not apply when we are in a pocket sector
- if (this._sector() == "default" && gravity != 0) {
- dx = -node.x;
- dy = -node.y;
- distance = Math.sqrt(dx * dx + dy * dy);
+ // repulsing forces between nodes
+ var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
- gravityForce = (distance == 0) ? 0 : (gravity / distance);
- node.fx = dx * gravityForce;
- node.fy = dy * gravityForce;
- }
- else {
- node.fx = 0;
- node.fy = 0;
+ // we loop from i over all but the last entree in the array
+ // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
+ for (i = 0; i < nodeIndices.length - 1; i++) {
+ node1 = nodes[nodeIndices[i]];
+ for (j = i + 1; j < nodeIndices.length; j++) {
+ node2 = nodes[nodeIndices[j]];
+
+ // nodes only affect nodes on their level
+ if (node1.level == node2.level) {
+
+ dx = node2.x - node1.x;
+ dy = node2.y - node1.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+
+ var steepness = 0.05;
+ if (distance < nodeDistance) {
+ repulsingForce = -Math.pow(steepness*distance,2) + Math.pow(steepness*nodeDistance,2);
+ }
+ else {
+ repulsingForce = 0;
+ }
+ // normalize force with
+ if (distance == 0) {
+ distance = 0.01;
+ }
+ else {
+ repulsingForce = repulsingForce / distance;
+ }
+ fx = dx * repulsingForce;
+ fy = dy * repulsingForce;
+
+ node1.fx -= fx;
+ node1.fy -= fy;
+ node2.fx += fx;
+ node2.fy += fy;
+ }
}
}
};
-
-
/**
* this function calculates the effects of the springs in the case of unsmooth curves.
*
* @private
*/
- exports._calculateSpringForces = function () {
+ exports._calculateHierarchicalSpringForces = function () {
var edgeLength, edge, edgeId;
var dx, dy, fx, fy, springForce, distance;
var edges = this.edges;
+ var nodes = this.calculationNodes;
+ var nodeIndices = this.calculationNodeIndices;
+
+
+ for (var i = 0; i < nodeIndices.length; i++) {
+ var node1 = nodes[nodeIndices[i]];
+ node1.springFx = 0;
+ node1.springFy = 0;
+ }
+
+
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
@@ -29314,506 +29706,464 @@ return /******/ (function(modules) { // webpackBootstrap
fx = dx * springForce;
fy = dy * springForce;
- edge.from.fx += fx;
- edge.from.fy += fy;
- edge.to.fx -= fx;
- edge.to.fy -= fy;
- }
- }
- }
- }
- };
-
-
-
-
- /**
- * This function calculates the springforces on the nodes, accounting for the support nodes.
- *
- * @private
- */
- exports._calculateSpringForcesWithSupport = function () {
- var edgeLength, edge, edgeId, combinedClusterSize;
- var edges = this.edges;
-
- // forces caused by the edges, modelled as springs
- for (edgeId in edges) {
- if (edges.hasOwnProperty(edgeId)) {
- edge = edges[edgeId];
- if (edge.connected) {
- // only calculate forces if nodes are in the same sector
- if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
- if (edge.via != null) {
- var node1 = edge.to;
- var node2 = edge.via;
- var node3 = edge.from;
-
- edgeLength = edge.physics.springLength;
- combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
- // this implies that the edges between big clusters are longer
- edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
- this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
- this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
+ if (edge.to.level != edge.from.level) {
+ edge.to.springFx -= fx;
+ edge.to.springFy -= fy;
+ edge.from.springFx += fx;
+ edge.from.springFy += fy;
+ }
+ else {
+ var factor = 0.5;
+ edge.to.fx -= factor*fx;
+ edge.to.fy -= factor*fy;
+ edge.from.fx += factor*fx;
+ edge.from.fy += factor*fy;
}
}
}
}
}
- };
-
-
- /**
- * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
- *
- * @param node1
- * @param node2
- * @param edgeLength
- * @private
- */
- exports._calculateSpringForce = function (node1, node2, edgeLength) {
- var dx, dy, fx, fy, springForce, distance;
- dx = (node1.x - node2.x);
- dy = (node1.y - node2.y);
- distance = Math.sqrt(dx * dx + dy * dy);
+ // normalize spring forces
+ var springForce = 1;
+ var springFx, springFy;
+ for (i = 0; i < nodeIndices.length; i++) {
+ var node = nodes[nodeIndices[i]];
+ springFx = Math.min(springForce,Math.max(-springForce,node.springFx));
+ springFy = Math.min(springForce,Math.max(-springForce,node.springFy));
- if (distance == 0) {
- distance = 0.01;
+ node.fx += springFx;
+ node.fy += springFy;
}
- // the 1/distance is so the fx and fy can be calculated without sine or cosine.
- springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
+ // retain energy balance
+ var totalFx = 0;
+ var totalFy = 0;
+ for (i = 0; i < nodeIndices.length; i++) {
+ var node = nodes[nodeIndices[i]];
+ totalFx += node.fx;
+ totalFy += node.fy;
+ }
+ var correctionFx = totalFx / nodeIndices.length;
+ var correctionFy = totalFy / nodeIndices.length;
- fx = dx * springForce;
- fy = dy * springForce;
+ for (i = 0; i < nodeIndices.length; i++) {
+ var node = nodes[nodeIndices[i]];
+ node.fx -= correctionFx;
+ node.fy -= correctionFy;
+ }
- node1.fx += fx;
- node1.fy += fy;
- node2.fx -= fx;
- node2.fy -= fy;
};
-
- exports._cleanupPhysicsConfiguration = function() {
- if (this.physicsConfiguration !== undefined) {
- while (this.physicsConfiguration.hasChildNodes()) {
- this.physicsConfiguration.removeChild(this.physicsConfiguration.firstChild);
- }
-
- this.physicsConfiguration.parentNode.removeChild(this.physicsConfiguration);
- this.physicsConfiguration = undefined;
- }
- }
+/***/ },
+/* 63 */
+/***/ function(module, exports, __webpack_require__) {
/**
- * Load the HTML for the physics config and bind it
+ * This function calculates the forces the nodes apply on eachother based on a gravitational model.
+ * The Barnes Hut method is used to speed up this N-body simulation.
+ *
* @private
*/
- exports._loadPhysicsConfiguration = function () {
- if (this.physicsConfiguration === undefined) {
- this.backupConstants = {};
- util.deepExtend(this.backupConstants,this.constants);
+ exports._calculateNodeForces = function() {
+ if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
+ var node;
+ var nodes = this.calculationNodes;
+ var nodeIndices = this.calculationNodeIndices;
+ var nodeCount = nodeIndices.length;
- var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
- this.physicsConfiguration = document.createElement('div');
- this.physicsConfiguration.className = "PhysicsConfiguration";
- this.physicsConfiguration.innerHTML = '' +
- '' +
- '' +
- '' +
- '' +
- ''
- this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
- this.optionsDiv = document.createElement("div");
- this.optionsDiv.style.fontSize = "14px";
- this.optionsDiv.style.fontFamily = "verdana";
- this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
+ this._formBarnesHutTree(nodes,nodeIndices);
- var rangeElement;
- rangeElement = document.getElementById('graph_BH_gc');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
- rangeElement = document.getElementById('graph_BH_cg');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
- rangeElement = document.getElementById('graph_BH_sc');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
- rangeElement = document.getElementById('graph_BH_sl');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
- rangeElement = document.getElementById('graph_BH_damp');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
+ var barnesHutTree = this.barnesHutTree;
- rangeElement = document.getElementById('graph_R_nd');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
- rangeElement = document.getElementById('graph_R_cg');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
- rangeElement = document.getElementById('graph_R_sc');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
- rangeElement = document.getElementById('graph_R_sl');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
- rangeElement = document.getElementById('graph_R_damp');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
+ // place the nodes one by one recursively
+ for (var i = 0; i < nodeCount; i++) {
+ node = nodes[nodeIndices[i]];
+ if (node.options.mass > 0) {
+ // starting with root is irrelevant, it never passes the BarnesHut condition
+ this._getForceContribution(barnesHutTree.root.children.NW,node);
+ this._getForceContribution(barnesHutTree.root.children.NE,node);
+ this._getForceContribution(barnesHutTree.root.children.SW,node);
+ this._getForceContribution(barnesHutTree.root.children.SE,node);
+ }
+ }
+ }
+ };
- rangeElement = document.getElementById('graph_H_nd');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
- rangeElement = document.getElementById('graph_H_cg');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
- rangeElement = document.getElementById('graph_H_sc');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
- rangeElement = document.getElementById('graph_H_sl');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
- rangeElement = document.getElementById('graph_H_damp');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
- rangeElement = document.getElementById('graph_H_direction');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
- rangeElement = document.getElementById('graph_H_levsep');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
- rangeElement = document.getElementById('graph_H_nspac');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
- var radioButton1 = document.getElementById("graph_physicsMethod1");
- var radioButton2 = document.getElementById("graph_physicsMethod2");
- var radioButton3 = document.getElementById("graph_physicsMethod3");
- radioButton2.checked = true;
- if (this.constants.physics.barnesHut.enabled) {
- radioButton1.checked = true;
- }
- if (this.constants.hierarchicalLayout.enabled) {
- radioButton3.checked = true;
- }
+ /**
+ * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
+ * If a region contains a single node, we check if it is not itself, then we apply the force.
+ *
+ * @param parentBranch
+ * @param node
+ * @private
+ */
+ exports._getForceContribution = function(parentBranch,node) {
+ // we get no force contribution from an empty region
+ if (parentBranch.childrenCount > 0) {
+ var dx,dy,distance;
- var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
- var graph_repositionNodes = document.getElementById("graph_repositionNodes");
- var graph_generateOptions = document.getElementById("graph_generateOptions");
+ // get the distance from the center of mass to the node.
+ dx = parentBranch.centerOfMass.x - node.x;
+ dy = parentBranch.centerOfMass.y - node.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
- graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
- graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
- graph_generateOptions.onclick = graphGenerateOptions.bind(this);
- if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) {
- graph_toggleSmooth.style.background = "#A4FF56";
+ // BarnesHut condition
+ // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed
+ // calcSize = 1/s --> d * 1/s > 1/theta = passed
+ if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.thetaInverted) {
+ // duplicate code to reduce function calls to speed up program
+ if (distance == 0) {
+ distance = 0.1*Math.random();
+ dx = distance;
+ }
+ var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
+ var fx = dx * gravityForce;
+ var fy = dy * gravityForce;
+ node.fx += fx;
+ node.fy += fy;
}
else {
- graph_toggleSmooth.style.background = "#FF8532";
+ // Did not pass the condition, go into children if available
+ if (parentBranch.childrenCount == 4) {
+ this._getForceContribution(parentBranch.children.NW,node);
+ this._getForceContribution(parentBranch.children.NE,node);
+ this._getForceContribution(parentBranch.children.SW,node);
+ this._getForceContribution(parentBranch.children.SE,node);
+ }
+ else { // parentBranch must have only one node, if it was empty we wouldnt be here
+ if (parentBranch.children.data.id != node.id) { // if it is not self
+ // duplicate code to reduce function calls to speed up program
+ if (distance == 0) {
+ distance = 0.5*Math.random();
+ dx = distance;
+ }
+ var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
+ var fx = dx * gravityForce;
+ var fy = dy * gravityForce;
+ node.fx += fx;
+ node.fy += fy;
+ }
+ }
}
-
-
- switchConfigurations.apply(this);
-
- radioButton1.onchange = switchConfigurations.bind(this);
- radioButton2.onchange = switchConfigurations.bind(this);
- radioButton3.onchange = switchConfigurations.bind(this);
}
};
/**
- * This overwrites the this.constants.
+ * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
*
- * @param constantsVariableName
- * @param value
+ * @param nodes
+ * @param nodeIndices
* @private
*/
- exports._overWriteGraphConstants = function (constantsVariableName, value) {
- var nameArray = constantsVariableName.split("_");
- if (nameArray.length == 1) {
- this.constants[nameArray[0]] = value;
- }
- else if (nameArray.length == 2) {
- this.constants[nameArray[0]][nameArray[1]] = value;
+ exports._formBarnesHutTree = function(nodes,nodeIndices) {
+ var node;
+ var nodeCount = nodeIndices.length;
+
+ var minX = Number.MAX_VALUE,
+ minY = Number.MAX_VALUE,
+ maxX =-Number.MAX_VALUE,
+ maxY =-Number.MAX_VALUE;
+
+ // get the range of the nodes
+ for (var i = 0; i < nodeCount; i++) {
+ var x = nodes[nodeIndices[i]].x;
+ var y = nodes[nodeIndices[i]].y;
+ if (nodes[nodeIndices[i]].options.mass > 0) {
+ if (x < minX) { minX = x; }
+ if (x > maxX) { maxX = x; }
+ if (y < minY) { minY = y; }
+ if (y > maxY) { maxY = y; }
+ }
}
- else if (nameArray.length == 3) {
- this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
+ // make the range a square
+ var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
+ if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
+ else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
+
+
+ var minimumTreeSize = 1e-5;
+ var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
+ var halfRootSize = 0.5 * rootSize;
+ var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
+
+ // construct the barnesHutTree
+ var barnesHutTree = {
+ root:{
+ centerOfMass: {x:0, y:0},
+ mass:0,
+ range: {
+ minX: centerX-halfRootSize,maxX:centerX+halfRootSize,
+ minY: centerY-halfRootSize,maxY:centerY+halfRootSize
+ },
+ size: rootSize,
+ calcSize: 1 / rootSize,
+ children: { data:null},
+ maxWidth: 0,
+ level: 0,
+ childrenCount: 4
+ }
+ };
+ this._splitBranch(barnesHutTree.root);
+
+ // place the nodes one by one recursively
+ for (i = 0; i < nodeCount; i++) {
+ node = nodes[nodeIndices[i]];
+ if (node.options.mass > 0) {
+ this._placeInTree(barnesHutTree.root,node);
+ }
}
+
+ // make global
+ this.barnesHutTree = barnesHutTree
};
/**
- * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
+ * this updates the mass of a branch. this is increased by adding a node.
+ *
+ * @param parentBranch
+ * @param node
+ * @private
*/
- function graphToggleSmoothCurves () {
- this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled;
- var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
- if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";}
- else {graph_toggleSmooth.style.background = "#FF8532";}
+ exports._updateBranchMass = function(parentBranch, node) {
+ var totalMass = parentBranch.mass + node.options.mass;
+ var totalMassInv = 1/totalMass;
+
+ parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
+ parentBranch.centerOfMass.x *= totalMassInv;
+
+ parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
+ parentBranch.centerOfMass.y *= totalMassInv;
+
+ parentBranch.mass = totalMass;
+ var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
+ parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
+
+ };
- this._configureSmoothCurves(false);
- }
/**
- * this function is used to scramble the nodes
+ * determine in which branch the node will be placed.
*
+ * @param parentBranch
+ * @param node
+ * @param skipMassUpdate
+ * @private
*/
- function graphRepositionNodes () {
- for (var nodeId in this.calculationNodes) {
- if (this.calculationNodes.hasOwnProperty(nodeId)) {
- this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
- this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
- }
- }
- if (this.constants.hierarchicalLayout.enabled == true) {
- this._setupHierarchicalLayout();
- showValueOfRange.call(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
- showValueOfRange.call(this, 'graph_H_cg', 1, "physics_centralGravity");
- showValueOfRange.call(this, 'graph_H_sc', 1, "physics_springConstant");
- showValueOfRange.call(this, 'graph_H_sl', 1, "physics_springLength");
- showValueOfRange.call(this, 'graph_H_damp', 1, "physics_damping");
- }
- else {
- this.repositionNodes();
+ exports._placeInTree = function(parentBranch,node,skipMassUpdate) {
+ if (skipMassUpdate != true || skipMassUpdate === undefined) {
+ // update the mass of the branch.
+ this._updateBranchMass(parentBranch,node);
}
- this.moving = true;
- this.start();
- }
- /**
- * this is used to generate an options file from the playing with physics system.
- */
- function graphGenerateOptions () {
- var options = "No options are required, default values used.";
- var optionsSpecific = [];
- var radioButton1 = document.getElementById("graph_physicsMethod1");
- var radioButton2 = document.getElementById("graph_physicsMethod2");
- if (radioButton1.checked == true) {
- if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
- if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
- if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
- if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
- if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
- if (optionsSpecific.length != 0) {
- options = "var options = {";
- options += "physics: {barnesHut: {";
- for (var i = 0; i < optionsSpecific.length; i++) {
- options += optionsSpecific[i];
- if (i < optionsSpecific.length - 1) {
- options += ", "
- }
- }
- options += '}}'
- }
- if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) {
- if (optionsSpecific.length == 0) {options = "var options = {";}
- else {options += ", "}
- options += "smoothCurves: " + this.constants.smoothCurves.enabled;
+ if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
+ if (parentBranch.children.NW.range.maxY > node.y) { // in NW
+ this._placeInRegion(parentBranch,node,"NW");
}
- if (options != "No options are required, default values used.") {
- options += '};'
+ else { // in SW
+ this._placeInRegion(parentBranch,node,"SW");
}
}
- else if (radioButton2.checked == true) {
- options = "var options = {";
- options += "physics: {barnesHut: {enabled: false}";
- if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
- if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
- if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
- if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
- if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
- if (optionsSpecific.length != 0) {
- options += ", repulsion: {";
- for (var i = 0; i < optionsSpecific.length; i++) {
- options += optionsSpecific[i];
- if (i < optionsSpecific.length - 1) {
- options += ", "
- }
- }
- options += '}}'
+ else { // in NE or SE
+ if (parentBranch.children.NW.range.maxY > node.y) { // in NE
+ this._placeInRegion(parentBranch,node,"NE");
}
- if (optionsSpecific.length == 0) {options += "}"}
- if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
- options += ", smoothCurves: " + this.constants.smoothCurves;
+ else { // in SE
+ this._placeInRegion(parentBranch,node,"SE");
}
- options += '};'
}
- else {
- options = "var options = {";
- if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
- if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
- if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
- if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
- if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
- if (optionsSpecific.length != 0) {
- options += "physics: {hierarchicalRepulsion: {";
- for (var i = 0; i < optionsSpecific.length; i++) {
- options += optionsSpecific[i];
- if (i < optionsSpecific.length - 1) {
- options += ", ";
- }
+ };
+
+
+ /**
+ * actually place the node in a region (or branch)
+ *
+ * @param parentBranch
+ * @param node
+ * @param region
+ * @private
+ */
+ exports._placeInRegion = function(parentBranch,node,region) {
+ switch (parentBranch.children[region].childrenCount) {
+ case 0: // place node here
+ parentBranch.children[region].children.data = node;
+ parentBranch.children[region].childrenCount = 1;
+ this._updateBranchMass(parentBranch.children[region],node);
+ break;
+ case 1: // convert into children
+ // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
+ // we move one node a pixel and we do not put it in the tree.
+ if (parentBranch.children[region].children.data.x == node.x &&
+ parentBranch.children[region].children.data.y == node.y) {
+ node.x += Math.random();
+ node.y += Math.random();
}
- options += '}},';
- }
- options += 'hierarchicalLayout: {';
- optionsSpecific = [];
- if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
- if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
- if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
- if (optionsSpecific.length != 0) {
- for (var i = 0; i < optionsSpecific.length; i++) {
- options += optionsSpecific[i];
- if (i < optionsSpecific.length - 1) {
- options += ", "
- }
+ else {
+ this._splitBranch(parentBranch.children[region]);
+ this._placeInTree(parentBranch.children[region],node);
}
- options += '}'
- }
- else {
- options += "enabled:true}";
- }
- options += '};'
+ break;
+ case 4: // place in branch
+ this._placeInTree(parentBranch.children[region],node);
+ break;
}
+ };
- this.optionsDiv.innerHTML = options;
- }
-
/**
- * this is used to switch between barnesHut, repulsion and hierarchical.
+ * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
+ * after the split is complete.
*
+ * @param parentBranch
+ * @private
*/
- function switchConfigurations () {
- var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
- var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
- var tableId = "graph_" + radioButton + "_table";
- var table = document.getElementById(tableId);
- table.style.display = "block";
- for (var i = 0; i < ids.length; i++) {
- if (ids[i] != tableId) {
- table = document.getElementById(ids[i]);
- table.style.display = "none";
- }
- }
- this._restoreNodes();
- if (radioButton == "R") {
- this.constants.hierarchicalLayout.enabled = false;
- this.constants.physics.hierarchicalRepulsion.enabled = false;
- this.constants.physics.barnesHut.enabled = false;
+ exports._splitBranch = function(parentBranch) {
+ // if the branch is shaded with a node, replace the node in the new subset.
+ var containedNode = null;
+ if (parentBranch.childrenCount == 1) {
+ containedNode = parentBranch.children.data;
+ parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
}
- else if (radioButton == "H") {
- if (this.constants.hierarchicalLayout.enabled == false) {
- this.constants.hierarchicalLayout.enabled = true;
- this.constants.physics.hierarchicalRepulsion.enabled = true;
- this.constants.physics.barnesHut.enabled = false;
- this.constants.smoothCurves.enabled = false;
- this._setupHierarchicalLayout();
- }
+ parentBranch.childrenCount = 4;
+ parentBranch.children.data = null;
+ this._insertRegion(parentBranch,"NW");
+ this._insertRegion(parentBranch,"NE");
+ this._insertRegion(parentBranch,"SW");
+ this._insertRegion(parentBranch,"SE");
+
+ if (containedNode != null) {
+ this._placeInTree(parentBranch,containedNode);
}
- else {
- this.constants.hierarchicalLayout.enabled = false;
- this.constants.physics.hierarchicalRepulsion.enabled = false;
- this.constants.physics.barnesHut.enabled = true;
+ };
+
+
+ /**
+ * This function subdivides the region into four new segments.
+ * Specifically, this inserts a single new segment.
+ * It fills the children section of the parentBranch
+ *
+ * @param parentBranch
+ * @param region
+ * @param parentRange
+ * @private
+ */
+ exports._insertRegion = function(parentBranch, region) {
+ var minX,maxX,minY,maxY;
+ var childSize = 0.5 * parentBranch.size;
+ switch (region) {
+ case "NW":
+ minX = parentBranch.range.minX;
+ maxX = parentBranch.range.minX + childSize;
+ minY = parentBranch.range.minY;
+ maxY = parentBranch.range.minY + childSize;
+ break;
+ case "NE":
+ minX = parentBranch.range.minX + childSize;
+ maxX = parentBranch.range.maxX;
+ minY = parentBranch.range.minY;
+ maxY = parentBranch.range.minY + childSize;
+ break;
+ case "SW":
+ minX = parentBranch.range.minX;
+ maxX = parentBranch.range.minX + childSize;
+ minY = parentBranch.range.minY + childSize;
+ maxY = parentBranch.range.maxY;
+ break;
+ case "SE":
+ minX = parentBranch.range.minX + childSize;
+ maxX = parentBranch.range.maxX;
+ minY = parentBranch.range.minY + childSize;
+ maxY = parentBranch.range.maxY;
+ break;
}
- this._loadSelectedForceSolver();
- var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
- if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";}
- else {graph_toggleSmooth.style.background = "#FF8532";}
- this.moving = true;
- this.start();
- }
+
+
+ parentBranch.children[region] = {
+ centerOfMass:{x:0,y:0},
+ mass:0,
+ range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
+ size: 0.5 * parentBranch.size,
+ calcSize: 2 * parentBranch.calcSize,
+ children: {data:null},
+ maxWidth: 0,
+ level: parentBranch.level+1,
+ childrenCount: 0
+ };
+ };
/**
- * this generates the ranges depending on the iniital values.
+ * This function is for debugging purposed, it draws the tree.
*
- * @param id
- * @param map
- * @param constantsVariableName
+ * @param ctx
+ * @param color
+ * @private
*/
- function showValueOfRange (id,map,constantsVariableName) {
- var valueId = id + "_value";
- var rangeValue = document.getElementById(id).value;
+ exports._drawTree = function(ctx,color) {
+ if (this.barnesHutTree !== undefined) {
- if (Array.isArray(map)) {
- document.getElementById(valueId).value = map[parseInt(rangeValue)];
- this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
+ ctx.lineWidth = 1;
+
+ this._drawBranch(this.barnesHutTree.root,ctx,color);
}
- else {
- document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
- this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
+ };
+
+
+ /**
+ * This function is for debugging purposes. It draws the branches recursively.
+ *
+ * @param branch
+ * @param ctx
+ * @param color
+ * @private
+ */
+ exports._drawBranch = function(branch,ctx,color) {
+ if (color === undefined) {
+ color = "#FF0000";
}
- if (constantsVariableName == "hierarchicalLayout_direction" ||
- constantsVariableName == "hierarchicalLayout_levelSeparation" ||
- constantsVariableName == "hierarchicalLayout_nodeSpacing") {
- this._setupHierarchicalLayout();
+ if (branch.childrenCount == 4) {
+ this._drawBranch(branch.children.NW,ctx);
+ this._drawBranch(branch.children.NE,ctx);
+ this._drawBranch(branch.children.SE,ctx);
+ this._drawBranch(branch.children.SW,ctx);
}
- this.moving = true;
- this.start();
- }
+ ctx.strokeStyle = color;
+ ctx.beginPath();
+ ctx.moveTo(branch.range.minX,branch.range.minY);
+ ctx.lineTo(branch.range.maxX,branch.range.minY);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(branch.range.maxX,branch.range.minY);
+ ctx.lineTo(branch.range.maxX,branch.range.maxY);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(branch.range.maxX,branch.range.maxY);
+ ctx.lineTo(branch.range.minX,branch.range.maxY);
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.moveTo(branch.range.minX,branch.range.maxY);
+ ctx.lineTo(branch.range.minX,branch.range.minY);
+ ctx.stroke();
+ /*
+ if (branch.mass > 0) {
+ ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
+ ctx.stroke();
+ }
+ */
+ };
/***/ },
-/* 63 */
+/* 64 */
/***/ function(module, exports, __webpack_require__) {
/**
@@ -30956,11 +31306,11 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ },
-/* 64 */
+/* 65 */
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
- var Node = __webpack_require__(53);
+ var Node = __webpack_require__(56);
/**
* Creation of the SectorMixin var.
@@ -31515,10 +31865,10 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ },
-/* 65 */
+/* 66 */
/***/ function(module, exports, __webpack_require__) {
- var Node = __webpack_require__(53);
+ var Node = __webpack_require__(56);
/**
* This function can be called from the _doInAllSectors function
@@ -31741,2402 +32091,2046 @@ return /******/ (function(modules) { // webpackBootstrap
if (doNotTrigger == false) {
this.emit('select', this.getSelection());
- }
- };
-
-
- /**
- * return the number of selected nodes
- *
- * @returns {number}
- * @private
- */
- exports._getSelectedNodeCount = function() {
- var count = 0;
- for (var nodeId in this.selectionObj.nodes) {
- if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- count += 1;
- }
- }
- return count;
- };
-
- /**
- * return the selected node
- *
- * @returns {number}
- * @private
- */
- exports._getSelectedNode = function() {
- for (var nodeId in this.selectionObj.nodes) {
- if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- return this.selectionObj.nodes[nodeId];
- }
- }
- return null;
- };
-
- /**
- * return the selected edge
- *
- * @returns {number}
- * @private
- */
- exports._getSelectedEdge = function() {
- for (var edgeId in this.selectionObj.edges) {
- if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
- return this.selectionObj.edges[edgeId];
- }
- }
- return null;
- };
-
-
- /**
- * return the number of selected edges
- *
- * @returns {number}
- * @private
- */
- exports._getSelectedEdgeCount = function() {
- var count = 0;
- for (var edgeId in this.selectionObj.edges) {
- if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
- count += 1;
- }
- }
- return count;
- };
-
-
- /**
- * return the number of selected objects.
- *
- * @returns {number}
- * @private
- */
- exports._getSelectedObjectCount = function() {
- var count = 0;
- for(var nodeId in this.selectionObj.nodes) {
- if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- count += 1;
- }
- }
- for(var edgeId in this.selectionObj.edges) {
- if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
- count += 1;
- }
- }
- return count;
- };
-
- /**
- * Check if anything is selected
- *
- * @returns {boolean}
- * @private
- */
- exports._selectionIsEmpty = function() {
- for(var nodeId in this.selectionObj.nodes) {
- if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- return false;
- }
- }
- for(var edgeId in this.selectionObj.edges) {
- if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
- return false;
- }
- }
- return true;
- };
-
-
- /**
- * check if one of the selected nodes is a cluster.
- *
- * @returns {boolean}
- * @private
- */
- exports._clusterInSelection = function() {
- for(var nodeId in this.selectionObj.nodes) {
- if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
- return true;
- }
- }
- }
- return false;
- };
-
- /**
- * select the edges connected to the node that is being selected
- *
- * @param {Node} node
- * @private
- */
- exports._selectConnectedEdges = function(node) {
- for (var i = 0; i < node.dynamicEdges.length; i++) {
- var edge = node.dynamicEdges[i];
- edge.select();
- this._addToSelection(edge);
- }
- };
-
- /**
- * select the edges connected to the node that is being selected
- *
- * @param {Node} node
- * @private
- */
- exports._hoverConnectedEdges = function(node) {
- for (var i = 0; i < node.dynamicEdges.length; i++) {
- var edge = node.dynamicEdges[i];
- edge.hover = true;
- this._addToHover(edge);
- }
- };
-
-
- /**
- * unselect the edges connected to the node that is being selected
- *
- * @param {Node} node
- * @private
- */
- exports._unselectConnectedEdges = function(node) {
- for (var i = 0; i < node.dynamicEdges.length; i++) {
- var edge = node.dynamicEdges[i];
- edge.unselect();
- this._removeFromSelection(edge);
- }
- };
-
-
-
-
- /**
- * This is called when someone clicks on a node. either select or deselect it.
- * If there is an existing selection and we don't want to append to it, clear the existing selection
- *
- * @param {Node || Edge} object
- * @param {Boolean} append
- * @param {Boolean} [doNotTrigger] | ignore trigger
- * @private
- */
- exports._selectObject = function(object, append, doNotTrigger, highlightEdges, overrideSelectable) {
- if (doNotTrigger === undefined) {
- doNotTrigger = false;
- }
- if (highlightEdges === undefined) {
- highlightEdges = true;
- }
-
- if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
- this._unselectAll(true);
- }
-
- // selectable allows the object to be selected. Override can be used if needed to bypass this.
- if (object.selected == false && (this.constants.selectable == true || overrideSelectable)) {
- object.select();
- this._addToSelection(object);
- if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) {
- this._selectConnectedEdges(object);
- }
- }
- // do not select the object if selectable is false, only add it to selection to allow drag to work
- else if (object.selected == false) {
- this._addToSelection(object);
- doNotTrigger = true;
- }
- else {
- object.unselect();
- this._removeFromSelection(object);
- }
-
- if (doNotTrigger == false) {
- this.emit('select', this.getSelection());
- }
- };
-
-
- /**
- * This is called when someone clicks on a node. either select or deselect it.
- * If there is an existing selection and we don't want to append to it, clear the existing selection
- *
- * @param {Node || Edge} object
- * @private
- */
- exports._blurObject = function(object) {
- if (object.hover == true) {
- object.hover = false;
- this.emit("blurNode",{node:object.id});
- }
- };
-
- /**
- * This is called when someone clicks on a node. either select or deselect it.
- * If there is an existing selection and we don't want to append to it, clear the existing selection
- *
- * @param {Node || Edge} object
- * @private
- */
- exports._hoverObject = function(object) {
- if (object.hover == false) {
- object.hover = true;
- this._addToHover(object);
- if (object instanceof Node) {
- this.emit("hoverNode",{node:object.id});
- }
- }
- if (object instanceof Node) {
- this._hoverConnectedEdges(object);
- }
- };
-
-
- /**
- * handles the selection part of the touch, only for navigation controls elements;
- * Touch is triggered before tap, also before hold. Hold triggers after a while.
- * This is the most responsive solution
- *
- * @param {Object} pointer
- * @private
- */
- exports._handleTouch = function(pointer) {
- };
-
-
- /**
- * handles the selection part of the tap;
- *
- * @param {Object} pointer
- * @private
- */
- exports._handleTap = function(pointer) {
- var node = this._getNodeAt(pointer);
- if (node != null) {
- this._selectObject(node, false);
- }
- else {
- var edge = this._getEdgeAt(pointer);
- if (edge != null) {
- this._selectObject(edge, false);
- }
- else {
- this._unselectAll();
- }
- }
- var properties = this.getSelection();
- properties['pointer'] = {
- DOM: {x: pointer.x, y: pointer.y},
- canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)}
- }
- this.emit("click", properties);
- this._redraw();
- };
-
-
- /**
- * handles the selection part of the double tap and opens a cluster if needed
- *
- * @param {Object} pointer
- * @private
- */
- exports._handleDoubleTap = function(pointer) {
- var node = this._getNodeAt(pointer);
- if (node != null && node !== undefined) {
- // we reset the areaCenter here so the opening of the node will occur
- this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
- "y" : this._YconvertDOMtoCanvas(pointer.y)};
- this.openCluster(node);
- }
- var properties = this.getSelection();
- properties['pointer'] = {
- DOM: {x: pointer.x, y: pointer.y},
- canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)}
- }
- this.emit("doubleClick", properties);
- };
-
-
- /**
- * Handle the onHold selection part
- *
- * @param pointer
- * @private
- */
- exports._handleOnHold = function(pointer) {
- var node = this._getNodeAt(pointer);
- if (node != null) {
- this._selectObject(node,true);
- }
- else {
- var edge = this._getEdgeAt(pointer);
- if (edge != null) {
- this._selectObject(edge,true);
- }
- }
- this._redraw();
- };
-
-
- /**
- * handle the onRelease event. These functions are here for the navigation controls module
- * and data manipulation module.
- *
- * @private
- */
- exports._handleOnRelease = function(pointer) {
- this._manipulationReleaseOverload(pointer);
- this._navigationReleaseOverload(pointer);
+ }
};
- exports._manipulationReleaseOverload = function (pointer) {};
- exports._navigationReleaseOverload = function (pointer) {};
/**
+ * return the number of selected nodes
*
- * retrieve the currently selected objects
- * @return {{nodes: Array., edges: Array.}} selection
+ * @returns {number}
+ * @private
*/
- exports.getSelection = function() {
- var nodeIds = this.getSelectedNodes();
- var edgeIds = this.getSelectedEdges();
- return {nodes:nodeIds, edges:edgeIds};
+ exports._getSelectedNodeCount = function() {
+ var count = 0;
+ for (var nodeId in this.selectionObj.nodes) {
+ if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ count += 1;
+ }
+ }
+ return count;
};
/**
+ * return the selected node
*
- * retrieve the currently selected nodes
- * @return {String[]} selection An array with the ids of the
- * selected nodes.
+ * @returns {number}
+ * @private
*/
- exports.getSelectedNodes = function() {
- var idArray = [];
- if (this.constants.selectable == true) {
- for (var nodeId in this.selectionObj.nodes) {
- if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- idArray.push(nodeId);
- }
+ exports._getSelectedNode = function() {
+ for (var nodeId in this.selectionObj.nodes) {
+ if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ return this.selectionObj.nodes[nodeId];
}
}
- return idArray
+ return null;
};
/**
+ * return the selected edge
*
- * retrieve the currently selected edges
- * @return {Array} selection An array with the ids of the
- * selected nodes.
+ * @returns {number}
+ * @private
*/
- exports.getSelectedEdges = function() {
- var idArray = [];
- if (this.constants.selectable == true) {
- for (var edgeId in this.selectionObj.edges) {
- if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
- idArray.push(edgeId);
- }
+ exports._getSelectedEdge = function() {
+ for (var edgeId in this.selectionObj.edges) {
+ if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ return this.selectionObj.edges[edgeId];
}
}
- return idArray;
- };
-
-
- /**
- * select zero or more nodes DEPRICATED
- * @param {Number[] | String[]} selection An array with the ids of the
- * selected nodes.
- */
- exports.setSelection = function() {
- console.log("setSelection is deprecated. Please use selectNodes instead.")
+ return null;
};
/**
- * select zero or more nodes with the option to highlight edges
- * @param {Number[] | String[]} selection An array with the ids of the
- * selected nodes.
- * @param {boolean} [highlightEdges]
+ * return the number of selected edges
+ *
+ * @returns {number}
+ * @private
*/
- exports.selectNodes = function(selection, highlightEdges) {
- var i, iMax, id;
-
- if (!selection || (selection.length == undefined))
- throw 'Selection must be an array with ids';
-
- // first unselect any selected node
- this._unselectAll(true);
-
- for (i = 0, iMax = selection.length; i < iMax; i++) {
- id = selection[i];
-
- var node = this.nodes[id];
- if (!node) {
- throw new RangeError('Node with id "' + id + '" not found');
+ exports._getSelectedEdgeCount = function() {
+ var count = 0;
+ for (var edgeId in this.selectionObj.edges) {
+ if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ count += 1;
}
- this._selectObject(node,true,true,highlightEdges,true);
}
- this.redraw();
+ return count;
};
/**
- * select zero or more edges
- * @param {Number[] | String[]} selection An array with the ids of the
- * selected nodes.
+ * return the number of selected objects.
+ *
+ * @returns {number}
+ * @private
*/
- exports.selectEdges = function(selection) {
- var i, iMax, id;
-
- if (!selection || (selection.length == undefined))
- throw 'Selection must be an array with ids';
-
- // first unselect any selected node
- this._unselectAll(true);
-
- for (i = 0, iMax = selection.length; i < iMax; i++) {
- id = selection[i];
-
- var edge = this.edges[id];
- if (!edge) {
- throw new RangeError('Edge with id "' + id + '" not found');
+ exports._getSelectedObjectCount = function() {
+ var count = 0;
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ count += 1;
}
- this._selectObject(edge,true,true,false,true);
}
- this.redraw();
+ for(var edgeId in this.selectionObj.edges) {
+ if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ count += 1;
+ }
+ }
+ return count;
};
/**
- * Validate the selection: remove ids of nodes which no longer exist
+ * Check if anything is selected
+ *
+ * @returns {boolean}
* @private
*/
- exports._updateSelection = function () {
+ exports._selectionIsEmpty = function() {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- if (!this.nodes.hasOwnProperty(nodeId)) {
- delete this.selectionObj.nodes[nodeId];
- }
+ return false;
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
- if (!this.edges.hasOwnProperty(edgeId)) {
- delete this.selectionObj.edges[edgeId];
- }
+ return false;
}
}
+ return true;
};
-/***/ },
-/* 66 */
-/***/ function(module, exports, __webpack_require__) {
-
- var util = __webpack_require__(1);
- var Node = __webpack_require__(53);
- var Edge = __webpack_require__(52);
-
/**
- * clears the toolbar div element of children
+ * check if one of the selected nodes is a cluster.
*
+ * @returns {boolean}
* @private
*/
- exports._clearManipulatorBar = function() {
- while (this.manipulationDiv.hasChildNodes()) {
- this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
+ exports._clusterInSelection = function() {
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
+ return true;
+ }
+ }
}
- this.manipulationDOM = {};
-
- this._manipulationReleaseOverload = function () {};
- delete this.sectors['support']['nodes']['targetNode'];
- delete this.sectors['support']['nodes']['targetViaNode'];
- this.controlNodesActive = false;
+ return false;
};
/**
- * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
- * these functions to their original functionality, we saved them in this.cachedFunctions.
- * This function restores these functions to their original function.
+ * select the edges connected to the node that is being selected
*
+ * @param {Node} node
* @private
*/
- exports._restoreOverloadedFunctions = function() {
- for (var functionName in this.cachedFunctions) {
- if (this.cachedFunctions.hasOwnProperty(functionName)) {
- this[functionName] = this.cachedFunctions[functionName];
- }
+ exports._selectConnectedEdges = function(node) {
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
+ var edge = node.dynamicEdges[i];
+ edge.select();
+ this._addToSelection(edge);
}
};
/**
- * Enable or disable edit-mode.
+ * select the edges connected to the node that is being selected
*
+ * @param {Node} node
* @private
*/
- exports._toggleEditMode = function() {
- this.editMode = !this.editMode;
- var toolbar = this.manipulationDiv;
- var closeDiv = this.closeDiv;
- var editModeDiv = this.editModeDiv;
- if (this.editMode == true) {
- toolbar.style.display="block";
- closeDiv.style.display="block";
- editModeDiv.style.display="none";
- closeDiv.onclick = this._toggleEditMode.bind(this);
- }
- else {
- toolbar.style.display="none";
- closeDiv.style.display="none";
- editModeDiv.style.display="block";
- closeDiv.onclick = null;
+ exports._hoverConnectedEdges = function(node) {
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
+ var edge = node.dynamicEdges[i];
+ edge.hover = true;
+ this._addToHover(edge);
}
- this._createManipulatorBar()
};
+
/**
- * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
+ * unselect the edges connected to the node that is being selected
*
+ * @param {Node} node
* @private
*/
- exports._createManipulatorBar = function() {
- // remove bound functions
- if (this.boundFunction) {
- this.off('select', this.boundFunction);
- }
-
- var locale = this.constants.locales[this.constants.locale];
-
- if (this.edgeBeingEdited !== undefined) {
- this.edgeBeingEdited._disableControlNodes();
- this.edgeBeingEdited = undefined;
- this.selectedControlNode = null;
- this.controlNodesActive = false;
- this._redraw();
+ exports._unselectConnectedEdges = function(node) {
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
+ var edge = node.dynamicEdges[i];
+ edge.unselect();
+ this._removeFromSelection(edge);
}
+ };
- // restore overloaded functions
- this._restoreOverloadedFunctions();
-
- // resume calculation
- this.freezeSimulation = false;
-
- // reset global variables
- this.blockConnectingEdgeSelection = false;
- this.forceAppendSelection = false;
- this.manipulationDOM = {};
-
- if (this.editMode == true) {
- while (this.manipulationDiv.hasChildNodes()) {
- this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
- }
-
- this.manipulationDOM['addNodeSpan'] = document.createElement('span');
- this.manipulationDOM['addNodeSpan'].className = 'network-manipulationUI add';
- this.manipulationDOM['addNodeLabelSpan'] = document.createElement('span');
- this.manipulationDOM['addNodeLabelSpan'].className = 'network-manipulationLabel';
- this.manipulationDOM['addNodeLabelSpan'].innerHTML = locale['addNode'];
- this.manipulationDOM['addNodeSpan'].appendChild(this.manipulationDOM['addNodeLabelSpan']);
-
- this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div');
- this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine';
-
- this.manipulationDOM['addEdgeSpan'] = document.createElement('span');
- this.manipulationDOM['addEdgeSpan'].className = 'network-manipulationUI connect';
- this.manipulationDOM['addEdgeLabelSpan'] = document.createElement('span');
- this.manipulationDOM['addEdgeLabelSpan'].className = 'network-manipulationLabel';
- this.manipulationDOM['addEdgeLabelSpan'].innerHTML = locale['addEdge'];
- this.manipulationDOM['addEdgeSpan'].appendChild(this.manipulationDOM['addEdgeLabelSpan']);
-
- this.manipulationDiv.appendChild(this.manipulationDOM['addNodeSpan']);
- this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv1']);
- this.manipulationDiv.appendChild(this.manipulationDOM['addEdgeSpan']);
-
- if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
- this.manipulationDOM['seperatorLineDiv2'] = document.createElement('div');
- this.manipulationDOM['seperatorLineDiv2'].className = 'network-seperatorLine';
-
- this.manipulationDOM['editNodeSpan'] = document.createElement('span');
- this.manipulationDOM['editNodeSpan'].className = 'network-manipulationUI edit';
- this.manipulationDOM['editNodeLabelSpan'] = document.createElement('span');
- this.manipulationDOM['editNodeLabelSpan'].className = 'network-manipulationLabel';
- this.manipulationDOM['editNodeLabelSpan'].innerHTML = locale['editNode'];
- this.manipulationDOM['editNodeSpan'].appendChild(this.manipulationDOM['editNodeLabelSpan']);
-
- this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv2']);
- this.manipulationDiv.appendChild(this.manipulationDOM['editNodeSpan']);
- }
- else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
- this.manipulationDOM['seperatorLineDiv3'] = document.createElement('div');
- this.manipulationDOM['seperatorLineDiv3'].className = 'network-seperatorLine';
-
- this.manipulationDOM['editEdgeSpan'] = document.createElement('span');
- this.manipulationDOM['editEdgeSpan'].className = 'network-manipulationUI edit';
- this.manipulationDOM['editEdgeLabelSpan'] = document.createElement('span');
- this.manipulationDOM['editEdgeLabelSpan'].className = 'network-manipulationLabel';
- this.manipulationDOM['editEdgeLabelSpan'].innerHTML = locale['editEdge'];
- this.manipulationDOM['editEdgeSpan'].appendChild(this.manipulationDOM['editEdgeLabelSpan']);
- this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv3']);
- this.manipulationDiv.appendChild(this.manipulationDOM['editEdgeSpan']);
- }
- if (this._selectionIsEmpty() == false) {
- this.manipulationDOM['seperatorLineDiv4'] = document.createElement('div');
- this.manipulationDOM['seperatorLineDiv4'].className = 'network-seperatorLine';
- this.manipulationDOM['deleteSpan'] = document.createElement('span');
- this.manipulationDOM['deleteSpan'].className = 'network-manipulationUI delete';
- this.manipulationDOM['deleteLabelSpan'] = document.createElement('span');
- this.manipulationDOM['deleteLabelSpan'].className = 'network-manipulationLabel';
- this.manipulationDOM['deleteLabelSpan'].innerHTML = locale['del'];
- this.manipulationDOM['deleteSpan'].appendChild(this.manipulationDOM['deleteLabelSpan']);
- this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv4']);
- this.manipulationDiv.appendChild(this.manipulationDOM['deleteSpan']);
- }
+ /**
+ * This is called when someone clicks on a node. either select or deselect it.
+ * If there is an existing selection and we don't want to append to it, clear the existing selection
+ *
+ * @param {Node || Edge} object
+ * @param {Boolean} append
+ * @param {Boolean} [doNotTrigger] | ignore trigger
+ * @private
+ */
+ exports._selectObject = function(object, append, doNotTrigger, highlightEdges, overrideSelectable) {
+ if (doNotTrigger === undefined) {
+ doNotTrigger = false;
+ }
+ if (highlightEdges === undefined) {
+ highlightEdges = true;
+ }
+ if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
+ this._unselectAll(true);
+ }
- // bind the icons
- this.manipulationDOM['addNodeSpan'].onclick = this._createAddNodeToolbar.bind(this);
- this.manipulationDOM['addEdgeSpan'].onclick = this._createAddEdgeToolbar.bind(this);
- if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
- this.manipulationDOM['editNodeSpan'].onclick = this._editNode.bind(this);
- }
- else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
- this.manipulationDOM['editEdgeSpan'].onclick = this._createEditEdgeToolbar.bind(this);
- }
- if (this._selectionIsEmpty() == false) {
- this.manipulationDOM['deleteSpan'].onclick = this._deleteSelected.bind(this);
+ // selectable allows the object to be selected. Override can be used if needed to bypass this.
+ if (object.selected == false && (this.constants.selectable == true || overrideSelectable)) {
+ object.select();
+ this._addToSelection(object);
+ if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) {
+ this._selectConnectedEdges(object);
}
- this.closeDiv.onclick = this._toggleEditMode.bind(this);
-
- this.boundFunction = this._createManipulatorBar.bind(this);
- this.on('select', this.boundFunction);
+ }
+ // do not select the object if selectable is false, only add it to selection to allow drag to work
+ else if (object.selected == false) {
+ this._addToSelection(object);
+ doNotTrigger = true;
}
else {
- while (this.editModeDiv.hasChildNodes()) {
- this.editModeDiv.removeChild(this.editModeDiv.firstChild);
- }
-
- this.manipulationDOM['editModeSpan'] = document.createElement('span');
- this.manipulationDOM['editModeSpan'].className = 'network-manipulationUI edit editmode';
- this.manipulationDOM['editModeLabelSpan'] = document.createElement('span');
- this.manipulationDOM['editModeLabelSpan'].className = 'network-manipulationLabel';
- this.manipulationDOM['editModeLabelSpan'].innerHTML = locale['edit'];
- this.manipulationDOM['editModeSpan'].appendChild(this.manipulationDOM['editModeLabelSpan']);
-
- this.editModeDiv.appendChild(this.manipulationDOM['editModeSpan']);
+ object.unselect();
+ this._removeFromSelection(object);
+ }
- this.manipulationDOM['editModeSpan'].onclick = this._toggleEditMode.bind(this);
+ if (doNotTrigger == false) {
+ this.emit('select', this.getSelection());
}
};
-
/**
- * Create the toolbar for adding Nodes
+ * This is called when someone clicks on a node. either select or deselect it.
+ * If there is an existing selection and we don't want to append to it, clear the existing selection
*
+ * @param {Node || Edge} object
* @private
*/
- exports._createAddNodeToolbar = function() {
- // clear the toolbar
- this._clearManipulatorBar();
- if (this.boundFunction) {
- this.off('select', this.boundFunction);
+ exports._blurObject = function(object) {
+ if (object.hover == true) {
+ object.hover = false;
+ this.emit("blurNode",{node:object.id});
}
-
- var locale = this.constants.locales[this.constants.locale];
-
- this.manipulationDOM = {};
- this.manipulationDOM['backSpan'] = document.createElement('span');
- this.manipulationDOM['backSpan'].className = 'network-manipulationUI back';
- this.manipulationDOM['backLabelSpan'] = document.createElement('span');
- this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel';
- this.manipulationDOM['backLabelSpan'].innerHTML = locale['back'];
- this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']);
-
- this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div');
- this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine';
-
- this.manipulationDOM['descriptionSpan'] = document.createElement('span');
- this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none';
- this.manipulationDOM['descriptionLabelSpan'] = document.createElement('span');
- this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel';
- this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['addDescription'];
- this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']);
-
- this.manipulationDiv.appendChild(this.manipulationDOM['backSpan']);
- this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv1']);
- this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']);
-
- // bind the icon
- this.manipulationDOM['backSpan'].onclick = this._createManipulatorBar.bind(this);
-
- // we use the boundFunction so we can reference it when we unbind it from the "select" event.
- this.boundFunction = this._addNode.bind(this);
- this.on('select', this.boundFunction);
};
-
/**
- * create the toolbar to connect nodes
+ * This is called when someone clicks on a node. either select or deselect it.
+ * If there is an existing selection and we don't want to append to it, clear the existing selection
*
+ * @param {Node || Edge} object
* @private
*/
- exports._createAddEdgeToolbar = function() {
- // clear the toolbar
- this._clearManipulatorBar();
- this._unselectAll(true);
- this.freezeSimulation = true;
-
- var locale = this.constants.locales[this.constants.locale];
-
- if (this.boundFunction) {
- this.off('select', this.boundFunction);
+ exports._hoverObject = function(object) {
+ if (object.hover == false) {
+ object.hover = true;
+ this._addToHover(object);
+ if (object instanceof Node) {
+ this.emit("hoverNode",{node:object.id});
+ }
}
+ if (object instanceof Node) {
+ this._hoverConnectedEdges(object);
+ }
+ };
- this._unselectAll();
- this.forceAppendSelection = false;
- this.blockConnectingEdgeSelection = true;
-
- this.manipulationDOM = {};
- this.manipulationDOM['backSpan'] = document.createElement('span');
- this.manipulationDOM['backSpan'].className = 'network-manipulationUI back';
- this.manipulationDOM['backLabelSpan'] = document.createElement('span');
- this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel';
- this.manipulationDOM['backLabelSpan'].innerHTML = locale['back'];
- this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']);
-
- this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div');
- this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine';
-
- this.manipulationDOM['descriptionSpan'] = document.createElement('span');
- this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none';
- this.manipulationDOM['descriptionLabelSpan'] = document.createElement('span');
- this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel';
- this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['edgeDescription'];
- this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']);
-
- this.manipulationDiv.appendChild(this.manipulationDOM['backSpan']);
- this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv1']);
- this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']);
-
- // bind the icon
- this.manipulationDOM['backSpan'].onclick = this._createManipulatorBar.bind(this);
- // we use the boundFunction so we can reference it when we unbind it from the "select" event.
- this.boundFunction = this._handleConnect.bind(this);
- this.on('select', this.boundFunction);
+ /**
+ * handles the selection part of the touch, only for navigation controls elements;
+ * Touch is triggered before tap, also before hold. Hold triggers after a while.
+ * This is the most responsive solution
+ *
+ * @param {Object} pointer
+ * @private
+ */
+ exports._handleTouch = function(pointer) {
+ };
- // temporarily overload functions
- this.cachedFunctions["_handleTouch"] = this._handleTouch;
- this.cachedFunctions["_manipulationReleaseOverload"] = this._manipulationReleaseOverload;
- this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
- this.cachedFunctions["_handleDragEnd"] = this._handleDragEnd;
- this._handleTouch = this._handleConnect;
- this._manipulationReleaseOverload = function () {};
- this._handleDragStart = function () {};
- this._handleDragEnd = this._finishConnect;
- // redraw to show the unselect
+ /**
+ * handles the selection part of the tap;
+ *
+ * @param {Object} pointer
+ * @private
+ */
+ exports._handleTap = function(pointer) {
+ var node = this._getNodeAt(pointer);
+ if (node != null) {
+ this._selectObject(node, false);
+ }
+ else {
+ var edge = this._getEdgeAt(pointer);
+ if (edge != null) {
+ this._selectObject(edge, false);
+ }
+ else {
+ this._unselectAll();
+ }
+ }
+ var properties = this.getSelection();
+ properties['pointer'] = {
+ DOM: {x: pointer.x, y: pointer.y},
+ canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)}
+ }
+ this.emit("click", properties);
this._redraw();
};
+
/**
- * create the toolbar to edit edges
+ * handles the selection part of the double tap and opens a cluster if needed
*
+ * @param {Object} pointer
* @private
*/
- exports._createEditEdgeToolbar = function() {
- // clear the toolbar
- this._clearManipulatorBar();
- this.controlNodesActive = true;
-
- if (this.boundFunction) {
- this.off('select', this.boundFunction);
+ exports._handleDoubleTap = function(pointer) {
+ var node = this._getNodeAt(pointer);
+ if (node != null && node !== undefined) {
+ // we reset the areaCenter here so the opening of the node will occur
+ this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
+ "y" : this._YconvertDOMtoCanvas(pointer.y)};
+ this.openCluster(node);
}
-
- this.edgeBeingEdited = this._getSelectedEdge();
- this.edgeBeingEdited._enableControlNodes();
-
- var locale = this.constants.locales[this.constants.locale];
-
- this.manipulationDOM = {};
- this.manipulationDOM['backSpan'] = document.createElement('span');
- this.manipulationDOM['backSpan'].className = 'network-manipulationUI back';
- this.manipulationDOM['backLabelSpan'] = document.createElement('span');
- this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel';
- this.manipulationDOM['backLabelSpan'].innerHTML = locale['back'];
- this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']);
-
- this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div');
- this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine';
-
- this.manipulationDOM['descriptionSpan'] = document.createElement('span');
- this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none';
- this.manipulationDOM['descriptionLabelSpan'] = document.createElement('span');
- this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel';
- this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['editEdgeDescription'];
- this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']);
-
- this.manipulationDiv.appendChild(this.manipulationDOM['backSpan']);
- this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv1']);
- this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']);
-
- // bind the icon
- this.manipulationDOM['backSpan'].onclick = this._createManipulatorBar.bind(this);
-
- // temporarily overload functions
- this.cachedFunctions["_handleTouch"] = this._handleTouch;
- this.cachedFunctions["_manipulationReleaseOverload"] = this._manipulationReleaseOverload;
- this.cachedFunctions["_handleTap"] = this._handleTap;
- this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
- this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
- this._handleTouch = this._selectControlNode;
- this._handleTap = function () {};
- this._handleOnDrag = this._controlNodeDrag;
- this._handleDragStart = function () {}
- this._manipulationReleaseOverload = this._releaseControlNode;
-
- // redraw to show the unselect
- this._redraw();
+ var properties = this.getSelection();
+ properties['pointer'] = {
+ DOM: {x: pointer.x, y: pointer.y},
+ canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)}
+ }
+ this.emit("doubleClick", properties);
};
/**
- * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
- * to walk the user through the process.
+ * Handle the onHold selection part
*
+ * @param pointer
* @private
*/
- exports._selectControlNode = function(pointer) {
- this.edgeBeingEdited.controlNodes.from.unselect();
- this.edgeBeingEdited.controlNodes.to.unselect();
- this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
- if (this.selectedControlNode !== null) {
- this.selectedControlNode.select();
- this.freezeSimulation = true;
+ exports._handleOnHold = function(pointer) {
+ var node = this._getNodeAt(pointer);
+ if (node != null) {
+ this._selectObject(node,true);
+ }
+ else {
+ var edge = this._getEdgeAt(pointer);
+ if (edge != null) {
+ this._selectObject(edge,true);
+ }
}
this._redraw();
};
/**
- * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
- * to walk the user through the process.
+ * handle the onRelease event. These functions are here for the navigation controls module
+ * and data manipulation module.
*
- * @private
+ * @private
*/
- exports._controlNodeDrag = function(event) {
- var pointer = this._getPointer(event.gesture.center);
- if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
- this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
- this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
- }
- this._redraw();
+ exports._handleOnRelease = function(pointer) {
+ this._manipulationReleaseOverload(pointer);
+ this._navigationReleaseOverload(pointer);
};
- exports._releaseControlNode = function(pointer) {
- var newNode = this._getNodeAt(pointer);
- if (newNode !== null) {
- if (this.edgeBeingEdited.controlNodes.from.selected == true) {
- this._editEdge(newNode.id, this.edgeBeingEdited.to.id);
- this.edgeBeingEdited.controlNodes.from.unselect();
- }
- if (this.edgeBeingEdited.controlNodes.to.selected == true) {
- this._editEdge(this.edgeBeingEdited.from.id, newNode.id);
- this.edgeBeingEdited.controlNodes.to.unselect();
+ exports._manipulationReleaseOverload = function (pointer) {};
+ exports._navigationReleaseOverload = function (pointer) {};
+
+ /**
+ *
+ * retrieve the currently selected objects
+ * @return {{nodes: Array., edges: Array.}} selection
+ */
+ exports.getSelection = function() {
+ var nodeIds = this.getSelectedNodes();
+ var edgeIds = this.getSelectedEdges();
+ return {nodes:nodeIds, edges:edgeIds};
+ };
+
+ /**
+ *
+ * retrieve the currently selected nodes
+ * @return {String[]} selection An array with the ids of the
+ * selected nodes.
+ */
+ exports.getSelectedNodes = function() {
+ var idArray = [];
+ if (this.constants.selectable == true) {
+ for (var nodeId in this.selectionObj.nodes) {
+ if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ idArray.push(nodeId);
+ }
}
}
- else {
- this.edgeBeingEdited._restoreControlNodes();
- }
- this.freezeSimulation = false;
- this._redraw();
+ return idArray
};
/**
- * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
- * to walk the user through the process.
*
- * @private
+ * retrieve the currently selected edges
+ * @return {Array} selection An array with the ids of the
+ * selected nodes.
*/
- exports._handleConnect = function(pointer) {
- if (this._getSelectedNodeCount() == 0) {
- var node = this._getNodeAt(pointer);
-
- if (node != null) {
- if (node.clusterSize > 1) {
- alert(this.constants.locales[this.constants.locale]['createEdgeError'])
+ exports.getSelectedEdges = function() {
+ var idArray = [];
+ if (this.constants.selectable == true) {
+ for (var edgeId in this.selectionObj.edges) {
+ if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ idArray.push(edgeId);
}
- else {
- this._selectObject(node,false);
- var supportNodes = this.sectors['support']['nodes'];
+ }
+ }
+ return idArray;
+ };
- // create a node the temporary line can look at
- supportNodes['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
- var targetNode = supportNodes['targetNode'];
- targetNode.x = node.x;
- targetNode.y = node.y;
- // create a temporary edge
- this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:targetNode.id}, this, this.constants);
- var connectionEdge = this.edges['connectionEdge'];
- connectionEdge.from = node;
- connectionEdge.connected = true;
- connectionEdge.options.smoothCurves = {enabled: true,
- dynamic: false,
- type: "continuous",
- roundness: 0.5
- };
- connectionEdge.selected = true;
- connectionEdge.to = targetNode;
+ /**
+ * select zero or more nodes DEPRICATED
+ * @param {Number[] | String[]} selection An array with the ids of the
+ * selected nodes.
+ */
+ exports.setSelection = function() {
+ console.log("setSelection is deprecated. Please use selectNodes instead.")
+ };
- this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
- this._handleOnDrag = function(event) {
- var pointer = this._getPointer(event.gesture.center);
- var connectionEdge = this.edges['connectionEdge'];
- connectionEdge.to.x = this._XconvertDOMtoCanvas(pointer.x);
- connectionEdge.to.y = this._YconvertDOMtoCanvas(pointer.y);
- };
- this.moving = true;
- this.start();
- }
+ /**
+ * select zero or more nodes with the option to highlight edges
+ * @param {Number[] | String[]} selection An array with the ids of the
+ * selected nodes.
+ * @param {boolean} [highlightEdges]
+ */
+ exports.selectNodes = function(selection, highlightEdges) {
+ var i, iMax, id;
+
+ if (!selection || (selection.length == undefined))
+ throw 'Selection must be an array with ids';
+
+ // first unselect any selected node
+ this._unselectAll(true);
+
+ for (i = 0, iMax = selection.length; i < iMax; i++) {
+ id = selection[i];
+
+ var node = this.nodes[id];
+ if (!node) {
+ throw new RangeError('Node with id "' + id + '" not found');
}
+ this._selectObject(node,true,true,highlightEdges,true);
}
+ this.redraw();
};
- exports._finishConnect = function(event) {
- if (this._getSelectedNodeCount() == 1) {
- var pointer = this._getPointer(event.gesture.center);
- // restore the drag function
- this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
- delete this.cachedFunctions["_handleOnDrag"];
- // remember the edge id
- var connectFromId = this.edges['connectionEdge'].fromId;
+ /**
+ * select zero or more edges
+ * @param {Number[] | String[]} selection An array with the ids of the
+ * selected nodes.
+ */
+ exports.selectEdges = function(selection) {
+ var i, iMax, id;
- // remove the temporary nodes and edge
- delete this.edges['connectionEdge'];
- delete this.sectors['support']['nodes']['targetNode'];
- delete this.sectors['support']['nodes']['targetViaNode'];
+ if (!selection || (selection.length == undefined))
+ throw 'Selection must be an array with ids';
- var node = this._getNodeAt(pointer);
- if (node != null) {
- if (node.clusterSize > 1) {
- alert(this.constants.locales[this.constants.locale]["createEdgeError"])
- }
- else {
- this._createEdge(connectFromId,node.id);
- this._createManipulatorBar();
- }
+ // first unselect any selected node
+ this._unselectAll(true);
+
+ for (i = 0, iMax = selection.length; i < iMax; i++) {
+ id = selection[i];
+
+ var edge = this.edges[id];
+ if (!edge) {
+ throw new RangeError('Edge with id "' + id + '" not found');
}
- this._unselectAll();
+ this._selectObject(edge,true,true,false,true);
}
+ this.redraw();
};
-
/**
- * Adds a node on the specified location
+ * Validate the selection: remove ids of nodes which no longer exist
+ * @private
*/
- exports._addNode = function() {
- if (this._selectionIsEmpty() && this.editMode == true) {
- var positionObject = this._pointerToPositionObject(this.pointerPosition);
- var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
- if (this.triggerFunctions.add) {
- if (this.triggerFunctions.add.length == 2) {
- var me = this;
- this.triggerFunctions.add(defaultData, function(finalizedData) {
- me.nodesData.add(finalizedData);
- me._createManipulatorBar();
- me.moving = true;
- me.start();
- });
- }
- else {
- throw new Error('The function for add does not support two arguments (data,callback)');
- this._createManipulatorBar();
- this.moving = true;
- this.start();
+ exports._updateSelection = function () {
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ if (!this.nodes.hasOwnProperty(nodeId)) {
+ delete this.selectionObj.nodes[nodeId];
}
}
- else {
- this.nodesData.add(defaultData);
- this._createManipulatorBar();
- this.moving = true;
- this.start();
+ }
+ for(var edgeId in this.selectionObj.edges) {
+ if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ if (!this.edges.hasOwnProperty(edgeId)) {
+ delete this.selectionObj.edges[edgeId];
+ }
}
}
};
+/***/ },
+/* 67 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var util = __webpack_require__(1);
+ var Node = __webpack_require__(56);
+ var Edge = __webpack_require__(57);
+
/**
- * connect two nodes with a new edge.
+ * clears the toolbar div element of children
*
* @private
*/
- exports._createEdge = function(sourceNodeId,targetNodeId) {
- if (this.editMode == true) {
- var defaultData = {from:sourceNodeId, to:targetNodeId};
- if (this.triggerFunctions.connect) {
- if (this.triggerFunctions.connect.length == 2) {
- var me = this;
- this.triggerFunctions.connect(defaultData, function(finalizedData) {
- me.edgesData.add(finalizedData);
- me.moving = true;
- me.start();
- });
- }
- else {
- throw new Error('The function for connect does not support two arguments (data,callback)');
- this.moving = true;
- this.start();
- }
- }
- else {
- this.edgesData.add(defaultData);
- this.moving = true;
- this.start();
- }
+ exports._clearManipulatorBar = function() {
+ while (this.manipulationDiv.hasChildNodes()) {
+ this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
}
+ this.manipulationDOM = {};
+
+ this._manipulationReleaseOverload = function () {};
+ delete this.sectors['support']['nodes']['targetNode'];
+ delete this.sectors['support']['nodes']['targetViaNode'];
+ this.controlNodesActive = false;
};
/**
- * connect two nodes with a new edge.
+ * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
+ * these functions to their original functionality, we saved them in this.cachedFunctions.
+ * This function restores these functions to their original function.
*
* @private
*/
- exports._editEdge = function(sourceNodeId,targetNodeId) {
- if (this.editMode == true) {
- var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
- if (this.triggerFunctions.editEdge) {
- if (this.triggerFunctions.editEdge.length == 2) {
- var me = this;
- this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
- me.edgesData.update(finalizedData);
- me.moving = true;
- me.start();
- });
- }
- else {
- throw new Error('The function for edit does not support two arguments (data, callback)');
- this.moving = true;
- this.start();
- }
- }
- else {
- this.edgesData.update(defaultData);
- this.moving = true;
- this.start();
+ exports._restoreOverloadedFunctions = function() {
+ for (var functionName in this.cachedFunctions) {
+ if (this.cachedFunctions.hasOwnProperty(functionName)) {
+ this[functionName] = this.cachedFunctions[functionName];
}
}
};
/**
- * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
+ * Enable or disable edit-mode.
*
* @private
*/
- exports._editNode = function() {
- if (this.triggerFunctions.edit && this.editMode == true) {
- var node = this._getSelectedNode();
- var data = {id:node.id,
- label: node.label,
- group: node.options.group,
- shape: node.options.shape,
- color: {
- background:node.options.color.background,
- border:node.options.color.border,
- highlight: {
- background:node.options.color.highlight.background,
- border:node.options.color.highlight.border
- }
- }};
- if (this.triggerFunctions.edit.length == 2) {
- var me = this;
- this.triggerFunctions.edit(data, function (finalizedData) {
- me.nodesData.update(finalizedData);
- me._createManipulatorBar();
- me.moving = true;
- me.start();
- });
- }
- else {
- throw new Error('The function for edit does not support two arguments (data, callback)');
- }
+ exports._toggleEditMode = function() {
+ this.editMode = !this.editMode;
+ var toolbar = this.manipulationDiv;
+ var closeDiv = this.closeDiv;
+ var editModeDiv = this.editModeDiv;
+ if (this.editMode == true) {
+ toolbar.style.display="block";
+ closeDiv.style.display="block";
+ editModeDiv.style.display="none";
+ closeDiv.onclick = this._toggleEditMode.bind(this);
}
else {
- throw new Error('No edit function has been bound to this button');
+ toolbar.style.display="none";
+ closeDiv.style.display="none";
+ editModeDiv.style.display="block";
+ closeDiv.onclick = null;
}
+ this._createManipulatorBar()
};
-
-
-
/**
- * delete everything in the selection
+ * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
*
* @private
*/
- exports._deleteSelected = function() {
- if (!this._selectionIsEmpty() && this.editMode == true) {
- if (!this._clusterInSelection()) {
- var selectedNodes = this.getSelectedNodes();
- var selectedEdges = this.getSelectedEdges();
- if (this.triggerFunctions.del) {
- var me = this;
- var data = {nodes: selectedNodes, edges: selectedEdges};
- if (this.triggerFunctions.del.length == 2) {
- this.triggerFunctions.del(data, function (finalizedData) {
- me.edgesData.remove(finalizedData.edges);
- me.nodesData.remove(finalizedData.nodes);
- me._unselectAll();
- me.moving = true;
- me.start();
- });
- }
- else {
- throw new Error('The function for delete does not support two arguments (data, callback)')
- }
- }
- else {
- this.edgesData.remove(selectedEdges);
- this.nodesData.remove(selectedNodes);
- this._unselectAll();
- this.moving = true;
- this.start();
- }
- }
- else {
- alert(this.constants.locales[this.constants.locale]["deleteClusterError"]);
- }
+ exports._createManipulatorBar = function() {
+ // remove bound functions
+ if (this.boundFunction) {
+ this.off('select', this.boundFunction);
}
- };
+ var locale = this.constants.locales[this.constants.locale];
+
+ if (this.edgeBeingEdited !== undefined) {
+ this.edgeBeingEdited._disableControlNodes();
+ this.edgeBeingEdited = undefined;
+ this.selectedControlNode = null;
+ this.controlNodesActive = false;
+ this._redraw();
+ }
+
+ // restore overloaded functions
+ this._restoreOverloadedFunctions();
+
+ // resume calculation
+ this.freezeSimulation = false;
+
+ // reset global variables
+ this.blockConnectingEdgeSelection = false;
+ this.forceAppendSelection = false;
+ this.manipulationDOM = {};
+
+ if (this.editMode == true) {
+ while (this.manipulationDiv.hasChildNodes()) {
+ this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
+ }
+
+ this.manipulationDOM['addNodeSpan'] = document.createElement('span');
+ this.manipulationDOM['addNodeSpan'].className = 'network-manipulationUI add';
+ this.manipulationDOM['addNodeLabelSpan'] = document.createElement('span');
+ this.manipulationDOM['addNodeLabelSpan'].className = 'network-manipulationLabel';
+ this.manipulationDOM['addNodeLabelSpan'].innerHTML = locale['addNode'];
+ this.manipulationDOM['addNodeSpan'].appendChild(this.manipulationDOM['addNodeLabelSpan']);
+
+ this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div');
+ this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine';
-/***/ },
-/* 67 */
-/***/ function(module, exports, __webpack_require__) {
+ this.manipulationDOM['addEdgeSpan'] = document.createElement('span');
+ this.manipulationDOM['addEdgeSpan'].className = 'network-manipulationUI connect';
+ this.manipulationDOM['addEdgeLabelSpan'] = document.createElement('span');
+ this.manipulationDOM['addEdgeLabelSpan'].className = 'network-manipulationLabel';
+ this.manipulationDOM['addEdgeLabelSpan'].innerHTML = locale['addEdge'];
+ this.manipulationDOM['addEdgeSpan'].appendChild(this.manipulationDOM['addEdgeLabelSpan']);
- var util = __webpack_require__(1);
- var Hammer = __webpack_require__(19);
+ this.manipulationDiv.appendChild(this.manipulationDOM['addNodeSpan']);
+ this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv1']);
+ this.manipulationDiv.appendChild(this.manipulationDOM['addEdgeSpan']);
- exports._cleanNavigation = function() {
- // clean hammer bindings
- if (this.navigationHammers.existing.length != 0) {
- for (var i = 0; i < this.navigationHammers.existing.length; i++) {
- this.navigationHammers.existing[i].dispose();
+ if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
+ this.manipulationDOM['seperatorLineDiv2'] = document.createElement('div');
+ this.manipulationDOM['seperatorLineDiv2'].className = 'network-seperatorLine';
+
+ this.manipulationDOM['editNodeSpan'] = document.createElement('span');
+ this.manipulationDOM['editNodeSpan'].className = 'network-manipulationUI edit';
+ this.manipulationDOM['editNodeLabelSpan'] = document.createElement('span');
+ this.manipulationDOM['editNodeLabelSpan'].className = 'network-manipulationLabel';
+ this.manipulationDOM['editNodeLabelSpan'].innerHTML = locale['editNode'];
+ this.manipulationDOM['editNodeSpan'].appendChild(this.manipulationDOM['editNodeLabelSpan']);
+
+ this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv2']);
+ this.manipulationDiv.appendChild(this.manipulationDOM['editNodeSpan']);
}
- this.navigationHammers.existing = [];
- }
+ else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
+ this.manipulationDOM['seperatorLineDiv3'] = document.createElement('div');
+ this.manipulationDOM['seperatorLineDiv3'].className = 'network-seperatorLine';
- this._navigationReleaseOverload = function () {};
+ this.manipulationDOM['editEdgeSpan'] = document.createElement('span');
+ this.manipulationDOM['editEdgeSpan'].className = 'network-manipulationUI edit';
+ this.manipulationDOM['editEdgeLabelSpan'] = document.createElement('span');
+ this.manipulationDOM['editEdgeLabelSpan'].className = 'network-manipulationLabel';
+ this.manipulationDOM['editEdgeLabelSpan'].innerHTML = locale['editEdge'];
+ this.manipulationDOM['editEdgeSpan'].appendChild(this.manipulationDOM['editEdgeLabelSpan']);
- // clean up previous navigation items
- if (this.navigationDivs && this.navigationDivs['wrapper'] && this.navigationDivs['wrapper'].parentNode) {
- this.navigationDivs['wrapper'].parentNode.removeChild(this.navigationDivs['wrapper']);
- }
- };
+ this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv3']);
+ this.manipulationDiv.appendChild(this.manipulationDOM['editEdgeSpan']);
+ }
+ if (this._selectionIsEmpty() == false) {
+ this.manipulationDOM['seperatorLineDiv4'] = document.createElement('div');
+ this.manipulationDOM['seperatorLineDiv4'].className = 'network-seperatorLine';
- /**
- * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
- * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
- * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
- * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
- *
- * @private
- */
- exports._loadNavigationElements = function() {
- this._cleanNavigation();
+ this.manipulationDOM['deleteSpan'] = document.createElement('span');
+ this.manipulationDOM['deleteSpan'].className = 'network-manipulationUI delete';
+ this.manipulationDOM['deleteLabelSpan'] = document.createElement('span');
+ this.manipulationDOM['deleteLabelSpan'].className = 'network-manipulationLabel';
+ this.manipulationDOM['deleteLabelSpan'].innerHTML = locale['del'];
+ this.manipulationDOM['deleteSpan'].appendChild(this.manipulationDOM['deleteLabelSpan']);
- this.navigationDivs = {};
- var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
- var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','_zoomExtent'];
+ this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv4']);
+ this.manipulationDiv.appendChild(this.manipulationDOM['deleteSpan']);
+ }
- this.navigationDivs['wrapper'] = document.createElement('div');
- this.frame.appendChild(this.navigationDivs['wrapper']);
- for (var i = 0; i < navigationDivs.length; i++) {
- this.navigationDivs[navigationDivs[i]] = document.createElement('div');
- this.navigationDivs[navigationDivs[i]].className = 'network-navigation ' + navigationDivs[i];
- this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
+ // bind the icons
+ this.manipulationDOM['addNodeSpan'].onclick = this._createAddNodeToolbar.bind(this);
+ this.manipulationDOM['addEdgeSpan'].onclick = this._createAddEdgeToolbar.bind(this);
+ if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
+ this.manipulationDOM['editNodeSpan'].onclick = this._editNode.bind(this);
+ }
+ else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
+ this.manipulationDOM['editEdgeSpan'].onclick = this._createEditEdgeToolbar.bind(this);
+ }
+ if (this._selectionIsEmpty() == false) {
+ this.manipulationDOM['deleteSpan'].onclick = this._deleteSelected.bind(this);
+ }
+ this.closeDiv.onclick = this._toggleEditMode.bind(this);
- var hammer = Hammer(this.navigationDivs[navigationDivs[i]], {prevent_default: true});
- hammer.on('touch', this[navigationDivActions[i]].bind(this));
- this.navigationHammers._new.push(hammer);
+ this.boundFunction = this._createManipulatorBar.bind(this);
+ this.on('select', this.boundFunction);
}
+ else {
+ while (this.editModeDiv.hasChildNodes()) {
+ this.editModeDiv.removeChild(this.editModeDiv.firstChild);
+ }
- this._navigationReleaseOverload = this._stopMovement;
-
- this.navigationHammers.existing = this.navigationHammers._new;
- };
+ this.manipulationDOM['editModeSpan'] = document.createElement('span');
+ this.manipulationDOM['editModeSpan'].className = 'network-manipulationUI edit editmode';
+ this.manipulationDOM['editModeLabelSpan'] = document.createElement('span');
+ this.manipulationDOM['editModeLabelSpan'].className = 'network-manipulationLabel';
+ this.manipulationDOM['editModeLabelSpan'].innerHTML = locale['edit'];
+ this.manipulationDOM['editModeSpan'].appendChild(this.manipulationDOM['editModeLabelSpan']);
+ this.editModeDiv.appendChild(this.manipulationDOM['editModeSpan']);
- /**
- * this stops all movement induced by the navigation buttons
- *
- * @private
- */
- exports._zoomExtent = function(event) {
- this.zoomExtent({duration:700});
- event.stopPropagation();
+ this.manipulationDOM['editModeSpan'].onclick = this._toggleEditMode.bind(this);
+ }
};
- /**
- * this stops all movement induced by the navigation buttons
- *
- * @private
- */
- exports._stopMovement = function() {
- this._xStopMoving();
- this._yStopMoving();
- this._stopZoom();
- };
/**
- * move the screen up
- * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
- * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
- * To avoid this behaviour, we do the translation in the start loop.
+ * Create the toolbar for adding Nodes
*
* @private
*/
- exports._moveUp = function(event) {
- this.yIncrement = this.constants.keyboard.speed.y;
- this.start(); // if there is no node movement, the calculation wont be done
- event.preventDefault();
- };
+ exports._createAddNodeToolbar = function() {
+ // clear the toolbar
+ this._clearManipulatorBar();
+ if (this.boundFunction) {
+ this.off('select', this.boundFunction);
+ }
+ var locale = this.constants.locales[this.constants.locale];
- /**
- * move the screen down
- * @private
- */
- exports._moveDown = function(event) {
- this.yIncrement = -this.constants.keyboard.speed.y;
- this.start(); // if there is no node movement, the calculation wont be done
- event.preventDefault();
- };
+ this.manipulationDOM = {};
+ this.manipulationDOM['backSpan'] = document.createElement('span');
+ this.manipulationDOM['backSpan'].className = 'network-manipulationUI back';
+ this.manipulationDOM['backLabelSpan'] = document.createElement('span');
+ this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel';
+ this.manipulationDOM['backLabelSpan'].innerHTML = locale['back'];
+ this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']);
+ this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div');
+ this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine';
- /**
- * move the screen left
- * @private
- */
- exports._moveLeft = function(event) {
- this.xIncrement = this.constants.keyboard.speed.x;
- this.start(); // if there is no node movement, the calculation wont be done
- event.preventDefault();
- };
+ this.manipulationDOM['descriptionSpan'] = document.createElement('span');
+ this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none';
+ this.manipulationDOM['descriptionLabelSpan'] = document.createElement('span');
+ this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel';
+ this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['addDescription'];
+ this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']);
+ this.manipulationDiv.appendChild(this.manipulationDOM['backSpan']);
+ this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv1']);
+ this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']);
- /**
- * move the screen right
- * @private
- */
- exports._moveRight = function(event) {
- this.xIncrement = -this.constants.keyboard.speed.y;
- this.start(); // if there is no node movement, the calculation wont be done
- event.preventDefault();
+ // bind the icon
+ this.manipulationDOM['backSpan'].onclick = this._createManipulatorBar.bind(this);
+
+ // we use the boundFunction so we can reference it when we unbind it from the "select" event.
+ this.boundFunction = this._addNode.bind(this);
+ this.on('select', this.boundFunction);
};
/**
- * Zoom in, using the same method as the movement.
+ * create the toolbar to connect nodes
+ *
* @private
*/
- exports._zoomIn = function(event) {
- this.zoomIncrement = this.constants.keyboard.speed.zoom;
- this.start(); // if there is no node movement, the calculation wont be done
- event.preventDefault();
- };
+ exports._createAddEdgeToolbar = function() {
+ // clear the toolbar
+ this._clearManipulatorBar();
+ this._unselectAll(true);
+ this.freezeSimulation = true;
+ var locale = this.constants.locales[this.constants.locale];
- /**
- * Zoom out
- * @private
- */
- exports._zoomOut = function(event) {
- this.zoomIncrement = -this.constants.keyboard.speed.zoom;
- this.start(); // if there is no node movement, the calculation wont be done
- event.preventDefault();
- };
+ if (this.boundFunction) {
+ this.off('select', this.boundFunction);
+ }
+ this._unselectAll();
+ this.forceAppendSelection = false;
+ this.blockConnectingEdgeSelection = true;
- /**
- * Stop zooming and unhighlight the zoom controls
- * @private
- */
- exports._stopZoom = function(event) {
- this.zoomIncrement = 0;
- event && event.preventDefault();
- };
+ this.manipulationDOM = {};
+ this.manipulationDOM['backSpan'] = document.createElement('span');
+ this.manipulationDOM['backSpan'].className = 'network-manipulationUI back';
+ this.manipulationDOM['backLabelSpan'] = document.createElement('span');
+ this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel';
+ this.manipulationDOM['backLabelSpan'].innerHTML = locale['back'];
+ this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']);
+ this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div');
+ this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine';
- /**
- * Stop moving in the Y direction and unHighlight the up and down
- * @private
- */
- exports._yStopMoving = function(event) {
- this.yIncrement = 0;
- event && event.preventDefault();
- };
+ this.manipulationDOM['descriptionSpan'] = document.createElement('span');
+ this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none';
+ this.manipulationDOM['descriptionLabelSpan'] = document.createElement('span');
+ this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel';
+ this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['edgeDescription'];
+ this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']);
+ this.manipulationDiv.appendChild(this.manipulationDOM['backSpan']);
+ this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv1']);
+ this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']);
- /**
- * Stop moving in the X direction and unHighlight left and right.
- * @private
- */
- exports._xStopMoving = function(event) {
- this.xIncrement = 0;
- event && event.preventDefault();
- };
+ // bind the icon
+ this.manipulationDOM['backSpan'].onclick = this._createManipulatorBar.bind(this);
+ // we use the boundFunction so we can reference it when we unbind it from the "select" event.
+ this.boundFunction = this._handleConnect.bind(this);
+ this.on('select', this.boundFunction);
-/***/ },
-/* 68 */
-/***/ function(module, exports, __webpack_require__) {
+ // temporarily overload functions
+ this.cachedFunctions["_handleTouch"] = this._handleTouch;
+ this.cachedFunctions["_manipulationReleaseOverload"] = this._manipulationReleaseOverload;
+ this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
+ this.cachedFunctions["_handleDragEnd"] = this._handleDragEnd;
+ this._handleTouch = this._handleConnect;
+ this._manipulationReleaseOverload = function () {};
+ this._handleDragStart = function () {};
+ this._handleDragEnd = this._finishConnect;
- exports._resetLevels = function() {
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- var node = this.nodes[nodeId];
- if (node.preassignedLevel == false) {
- node.level = -1;
- node.hierarchyEnumerated = false;
- }
- }
- }
+ // redraw to show the unselect
+ this._redraw();
};
/**
- * This is the main function to layout the nodes in a hierarchical way.
- * It checks if the node details are supplied correctly
+ * create the toolbar to edit edges
*
* @private
*/
- exports._setupHierarchicalLayout = function() {
- if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
- if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
- this.constants.hierarchicalLayout.levelSeparation = this.constants.hierarchicalLayout.levelSeparation < 0 ? this.constants.hierarchicalLayout.levelSeparation : this.constants.hierarchicalLayout.levelSeparation * -1;
- }
- else {
- this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
- }
-
- if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "LR") {
- if (this.constants.smoothCurves.enabled == true) {
- this.constants.smoothCurves.type = "vertical";
- }
- }
- else {
- if (this.constants.smoothCurves.enabled == true) {
- this.constants.smoothCurves.type = "horizontal";
- }
- }
- // get the size of the largest hubs and check if the user has defined a level for a node.
- var hubsize = 0;
- var node, nodeId;
- var definedLevel = false;
- var undefinedLevel = false;
-
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- if (node.level != -1) {
- definedLevel = true;
- }
- else {
- undefinedLevel = true;
- }
- if (hubsize < node.edges.length) {
- hubsize = node.edges.length;
- }
- }
- }
-
- // if the user defined some levels but not all, alert and run without hierarchical layout
- if (undefinedLevel == true && definedLevel == true) {
- throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
- this.zoomExtent(undefined,true,this.constants.clustering.enabled);
- if (!this.constants.clustering.enabled) {
- this.start();
- }
- }
- else {
- // setup the system to use hierarchical method.
- this._changeConstants();
-
- // define levels if undefined by the users. Based on hubsize
- if (undefinedLevel == true) {
- if (this.constants.hierarchicalLayout.layout == "hubsize") {
- this._determineLevels(hubsize);
- }
- else {
- this._determineLevelsDirected();
- }
+ exports._createEditEdgeToolbar = function() {
+ // clear the toolbar
+ this._clearManipulatorBar();
+ this.controlNodesActive = true;
- }
- // check the distribution of the nodes per level.
- var distribution = this._getDistribution();
+ if (this.boundFunction) {
+ this.off('select', this.boundFunction);
+ }
- // place the nodes on the canvas. This also stablilizes the system.
- this._placeNodesByHierarchy(distribution);
+ this.edgeBeingEdited = this._getSelectedEdge();
+ this.edgeBeingEdited._enableControlNodes();
- // start the simulation.
- this.start();
- }
- }
- };
+ var locale = this.constants.locales[this.constants.locale];
+ this.manipulationDOM = {};
+ this.manipulationDOM['backSpan'] = document.createElement('span');
+ this.manipulationDOM['backSpan'].className = 'network-manipulationUI back';
+ this.manipulationDOM['backLabelSpan'] = document.createElement('span');
+ this.manipulationDOM['backLabelSpan'].className = 'network-manipulationLabel';
+ this.manipulationDOM['backLabelSpan'].innerHTML = locale['back'];
+ this.manipulationDOM['backSpan'].appendChild(this.manipulationDOM['backLabelSpan']);
- /**
- * This function places the nodes on the canvas based on the hierarchial distribution.
- *
- * @param {Object} distribution | obtained by the function this._getDistribution()
- * @private
- */
- exports._placeNodesByHierarchy = function(distribution) {
- var nodeId, node;
+ this.manipulationDOM['seperatorLineDiv1'] = document.createElement('div');
+ this.manipulationDOM['seperatorLineDiv1'].className = 'network-seperatorLine';
- // start placing all the level 0 nodes first. Then recursively position their branches.
- for (var level in distribution) {
- if (distribution.hasOwnProperty(level)) {
+ this.manipulationDOM['descriptionSpan'] = document.createElement('span');
+ this.manipulationDOM['descriptionSpan'].className = 'network-manipulationUI none';
+ this.manipulationDOM['descriptionLabelSpan'] = document.createElement('span');
+ this.manipulationDOM['descriptionLabelSpan'].className = 'network-manipulationLabel';
+ this.manipulationDOM['descriptionLabelSpan'].innerHTML = locale['editEdgeDescription'];
+ this.manipulationDOM['descriptionSpan'].appendChild(this.manipulationDOM['descriptionLabelSpan']);
- for (nodeId in distribution[level].nodes) {
- if (distribution[level].nodes.hasOwnProperty(nodeId)) {
- node = distribution[level].nodes[nodeId];
- if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
- if (node.xFixed) {
- node.x = distribution[level].minPos;
- node.xFixed = false;
+ this.manipulationDiv.appendChild(this.manipulationDOM['backSpan']);
+ this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv1']);
+ this.manipulationDiv.appendChild(this.manipulationDOM['descriptionSpan']);
- distribution[level].minPos += distribution[level].nodeSpacing;
- }
- }
- else {
- if (node.yFixed) {
- node.y = distribution[level].minPos;
- node.yFixed = false;
+ // bind the icon
+ this.manipulationDOM['backSpan'].onclick = this._createManipulatorBar.bind(this);
- distribution[level].minPos += distribution[level].nodeSpacing;
- }
- }
- this._placeBranchNodes(node.edges,node.id,distribution,node.level);
- }
- }
- }
- }
+ // temporarily overload functions
+ this.cachedFunctions["_handleTouch"] = this._handleTouch;
+ this.cachedFunctions["_manipulationReleaseOverload"] = this._manipulationReleaseOverload;
+ this.cachedFunctions["_handleTap"] = this._handleTap;
+ this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
+ this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
+ this._handleTouch = this._selectControlNode;
+ this._handleTap = function () {};
+ this._handleOnDrag = this._controlNodeDrag;
+ this._handleDragStart = function () {}
+ this._manipulationReleaseOverload = this._releaseControlNode;
- // stabilize the system after positioning. This function calls zoomExtent.
- this._stabilize();
+ // redraw to show the unselect
+ this._redraw();
};
/**
- * This function get the distribution of levels based on hubsize
+ * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
+ * to walk the user through the process.
*
- * @returns {Object}
* @private
*/
- exports._getDistribution = function() {
- var distribution = {};
- var nodeId, node, level;
-
- // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
- // the fix of X is removed after the x value has been set.
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- node.xFixed = true;
- node.yFixed = true;
- if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
- node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
- }
- else {
- node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
- }
- if (distribution[node.level] === undefined) {
- distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
- }
- distribution[node.level].amount += 1;
- distribution[node.level].nodes[nodeId] = node;
- }
- }
-
- // determine the largest amount of nodes of all levels
- var maxCount = 0;
- for (level in distribution) {
- if (distribution.hasOwnProperty(level)) {
- if (maxCount < distribution[level].amount) {
- maxCount = distribution[level].amount;
- }
- }
- }
-
- // set the initial position and spacing of each nodes accordingly
- for (level in distribution) {
- if (distribution.hasOwnProperty(level)) {
- distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
- distribution[level].nodeSpacing /= (distribution[level].amount + 1);
- distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
- }
+ exports._selectControlNode = function(pointer) {
+ this.edgeBeingEdited.controlNodes.from.unselect();
+ this.edgeBeingEdited.controlNodes.to.unselect();
+ this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
+ if (this.selectedControlNode !== null) {
+ this.selectedControlNode.select();
+ this.freezeSimulation = true;
}
-
- return distribution;
+ this._redraw();
};
/**
- * this function allocates nodes in levels based on the recursive branching from the largest hubs.
+ * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
+ * to walk the user through the process.
*
- * @param hubsize
* @private
*/
- exports._determineLevels = function(hubsize) {
- var nodeId, node;
-
- // determine hubs
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- if (node.edges.length == hubsize) {
- node.level = 0;
- }
- }
+ exports._controlNodeDrag = function(event) {
+ var pointer = this._getPointer(event.gesture.center);
+ if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
+ this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
+ this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
}
+ this._redraw();
+ };
- // branch from hubs
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- if (node.level == 0) {
- this._setLevel(1,node.edges,node.id);
- }
+ exports._releaseControlNode = function(pointer) {
+ var newNode = this._getNodeAt(pointer);
+ if (newNode !== null) {
+ if (this.edgeBeingEdited.controlNodes.from.selected == true) {
+ this._editEdge(newNode.id, this.edgeBeingEdited.to.id);
+ this.edgeBeingEdited.controlNodes.from.unselect();
+ }
+ if (this.edgeBeingEdited.controlNodes.to.selected == true) {
+ this._editEdge(this.edgeBeingEdited.from.id, newNode.id);
+ this.edgeBeingEdited.controlNodes.to.unselect();
}
}
+ else {
+ this.edgeBeingEdited._restoreControlNodes();
+ }
+ this.freezeSimulation = false;
+ this._redraw();
};
/**
- * this function allocates nodes in levels based on the recursive branching from the largest hubs.
+ * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
+ * to walk the user through the process.
*
- * @param hubsize
* @private
*/
- exports._determineLevelsDirected = function() {
- var nodeId, node;
+ exports._handleConnect = function(pointer) {
+ if (this._getSelectedNodeCount() == 0) {
+ var node = this._getNodeAt(pointer);
- // set first node to source
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- this.nodes[nodeId].level = 10000;
- break;
- }
- }
+ if (node != null) {
+ if (node.clusterSize > 1) {
+ alert(this.constants.locales[this.constants.locale]['createEdgeError'])
+ }
+ else {
+ this._selectObject(node,false);
+ var supportNodes = this.sectors['support']['nodes'];
- // branch from hubs
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- if (node.level == 10000) {
- this._setLevelDirected(10000,node.edges,node.id);
+ // create a node the temporary line can look at
+ supportNodes['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
+ var targetNode = supportNodes['targetNode'];
+ targetNode.x = node.x;
+ targetNode.y = node.y;
+
+ // create a temporary edge
+ this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:targetNode.id}, this, this.constants);
+ var connectionEdge = this.edges['connectionEdge'];
+ connectionEdge.from = node;
+ connectionEdge.connected = true;
+ connectionEdge.options.smoothCurves = {enabled: true,
+ dynamic: false,
+ type: "continuous",
+ roundness: 0.5
+ };
+ connectionEdge.selected = true;
+ connectionEdge.to = targetNode;
+
+ this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
+ this._handleOnDrag = function(event) {
+ var pointer = this._getPointer(event.gesture.center);
+ var connectionEdge = this.edges['connectionEdge'];
+ connectionEdge.to.x = this._XconvertDOMtoCanvas(pointer.x);
+ connectionEdge.to.y = this._YconvertDOMtoCanvas(pointer.y);
+ };
+
+ this.moving = true;
+ this.start();
}
}
}
+ };
+ exports._finishConnect = function(event) {
+ if (this._getSelectedNodeCount() == 1) {
+ var pointer = this._getPointer(event.gesture.center);
+ // restore the drag function
+ this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
+ delete this.cachedFunctions["_handleOnDrag"];
- // branch from hubs
- var minLevel = 10000;
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- minLevel = node.level < minLevel ? node.level : minLevel;
- }
- }
+ // remember the edge id
+ var connectFromId = this.edges['connectionEdge'].fromId;
- // branch from hubs
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- node.level -= minLevel;
+ // remove the temporary nodes and edge
+ delete this.edges['connectionEdge'];
+ delete this.sectors['support']['nodes']['targetNode'];
+ delete this.sectors['support']['nodes']['targetViaNode'];
+
+ var node = this._getNodeAt(pointer);
+ if (node != null) {
+ if (node.clusterSize > 1) {
+ alert(this.constants.locales[this.constants.locale]["createEdgeError"])
+ }
+ else {
+ this._createEdge(connectFromId,node.id);
+ this._createManipulatorBar();
+ }
}
+ this._unselectAll();
}
};
/**
- * Since hierarchical layout does not support:
- * - smooth curves (based on the physics),
- * - clustering (based on dynamic node counts)
- *
- * We disable both features so there will be no problems.
- *
- * @private
+ * Adds a node on the specified location
*/
- exports._changeConstants = function() {
- this.constants.clustering.enabled = false;
- this.constants.physics.barnesHut.enabled = false;
- this.constants.physics.hierarchicalRepulsion.enabled = true;
- this._loadSelectedForceSolver();
- if (this.constants.smoothCurves.enabled == true) {
- this.constants.smoothCurves.dynamic = false;
+ exports._addNode = function() {
+ if (this._selectionIsEmpty() && this.editMode == true) {
+ var positionObject = this._pointerToPositionObject(this.pointerPosition);
+ var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
+ if (this.triggerFunctions.add) {
+ if (this.triggerFunctions.add.length == 2) {
+ var me = this;
+ this.triggerFunctions.add(defaultData, function(finalizedData) {
+ me.nodesData.add(finalizedData);
+ me._createManipulatorBar();
+ me.moving = true;
+ me.start();
+ });
+ }
+ else {
+ throw new Error('The function for add does not support two arguments (data,callback)');
+ this._createManipulatorBar();
+ this.moving = true;
+ this.start();
+ }
+ }
+ else {
+ this.nodesData.add(defaultData);
+ this._createManipulatorBar();
+ this.moving = true;
+ this.start();
+ }
}
- this._configureSmoothCurves();
};
/**
- * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
- * on a X position that ensures there will be no overlap.
+ * connect two nodes with a new edge.
*
- * @param edges
- * @param parentId
- * @param distribution
- * @param parentLevel
* @private
*/
- exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) {
- for (var i = 0; i < edges.length; i++) {
- var childNode = null;
- if (edges[i].toId == parentId) {
- childNode = edges[i].from;
+ exports._createEdge = function(sourceNodeId,targetNodeId) {
+ if (this.editMode == true) {
+ var defaultData = {from:sourceNodeId, to:targetNodeId};
+ if (this.triggerFunctions.connect) {
+ if (this.triggerFunctions.connect.length == 2) {
+ var me = this;
+ this.triggerFunctions.connect(defaultData, function(finalizedData) {
+ me.edgesData.add(finalizedData);
+ me.moving = true;
+ me.start();
+ });
+ }
+ else {
+ throw new Error('The function for connect does not support two arguments (data,callback)');
+ this.moving = true;
+ this.start();
+ }
}
else {
- childNode = edges[i].to;
+ this.edgesData.add(defaultData);
+ this.moving = true;
+ this.start();
}
+ }
+ };
- // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
- var nodeMoved = false;
- if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
- if (childNode.xFixed && childNode.level > parentLevel) {
- childNode.xFixed = false;
- childNode.x = distribution[childNode.level].minPos;
- nodeMoved = true;
+ /**
+ * connect two nodes with a new edge.
+ *
+ * @private
+ */
+ exports._editEdge = function(sourceNodeId,targetNodeId) {
+ if (this.editMode == true) {
+ var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
+ if (this.triggerFunctions.editEdge) {
+ if (this.triggerFunctions.editEdge.length == 2) {
+ var me = this;
+ this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
+ me.edgesData.update(finalizedData);
+ me.moving = true;
+ me.start();
+ });
}
- }
- else {
- if (childNode.yFixed && childNode.level > parentLevel) {
- childNode.yFixed = false;
- childNode.y = distribution[childNode.level].minPos;
- nodeMoved = true;
+ else {
+ throw new Error('The function for edit does not support two arguments (data, callback)');
+ this.moving = true;
+ this.start();
}
}
-
- if (nodeMoved == true) {
- distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
- if (childNode.edges.length > 1) {
- this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
- }
+ else {
+ this.edgesData.update(defaultData);
+ this.moving = true;
+ this.start();
}
}
};
-
/**
- * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
+ * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
*
- * @param level
- * @param edges
- * @param parentId
* @private
*/
- exports._setLevel = function(level, edges, parentId) {
- for (var i = 0; i < edges.length; i++) {
- var childNode = null;
- if (edges[i].toId == parentId) {
- childNode = edges[i].from;
+ exports._editNode = function() {
+ if (this.triggerFunctions.edit && this.editMode == true) {
+ var node = this._getSelectedNode();
+ var data = {id:node.id,
+ label: node.label,
+ group: node.options.group,
+ shape: node.options.shape,
+ color: {
+ background:node.options.color.background,
+ border:node.options.color.border,
+ highlight: {
+ background:node.options.color.highlight.background,
+ border:node.options.color.highlight.border
+ }
+ }};
+ if (this.triggerFunctions.edit.length == 2) {
+ var me = this;
+ this.triggerFunctions.edit(data, function (finalizedData) {
+ me.nodesData.update(finalizedData);
+ me._createManipulatorBar();
+ me.moving = true;
+ me.start();
+ });
}
else {
- childNode = edges[i].to;
- }
- if (childNode.level == -1 || childNode.level > level) {
- childNode.level = level;
- if (childNode.edges.length > 1) {
- this._setLevel(level+1, childNode.edges, childNode.id);
- }
+ throw new Error('The function for edit does not support two arguments (data, callback)');
}
}
+ else {
+ throw new Error('No edit function has been bound to this button');
+ }
};
+
+
/**
- * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
+ * delete everything in the selection
*
- * @param level
- * @param edges
- * @param parentId
* @private
*/
- exports._setLevelDirected = function(level, edges, parentId) {
- this.nodes[parentId].hierarchyEnumerated = true;
- for (var i = 0; i < edges.length; i++) {
- var childNode = null;
- var direction = 1;
- if (edges[i].toId == parentId) {
- childNode = edges[i].from;
- direction = -1;
+ exports._deleteSelected = function() {
+ if (!this._selectionIsEmpty() && this.editMode == true) {
+ if (!this._clusterInSelection()) {
+ var selectedNodes = this.getSelectedNodes();
+ var selectedEdges = this.getSelectedEdges();
+ if (this.triggerFunctions.del) {
+ var me = this;
+ var data = {nodes: selectedNodes, edges: selectedEdges};
+ if (this.triggerFunctions.del.length == 2) {
+ this.triggerFunctions.del(data, function (finalizedData) {
+ me.edgesData.remove(finalizedData.edges);
+ me.nodesData.remove(finalizedData.nodes);
+ me._unselectAll();
+ me.moving = true;
+ me.start();
+ });
+ }
+ else {
+ throw new Error('The function for delete does not support two arguments (data, callback)')
+ }
+ }
+ else {
+ this.edgesData.remove(selectedEdges);
+ this.nodesData.remove(selectedNodes);
+ this._unselectAll();
+ this.moving = true;
+ this.start();
+ }
}
else {
- childNode = edges[i].to;
- }
- if (childNode.level == -1) {
- childNode.level = level + direction;
+ alert(this.constants.locales[this.constants.locale]["deleteClusterError"]);
}
}
+ };
- for (var i = 0; i < edges.length; i++) {
- var childNode = null;
- if (edges[i].toId == parentId) {childNode = edges[i].from;}
- else {childNode = edges[i].to;}
- if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) {
- this._setLevelDirected(childNode.level, childNode.edges, childNode.id);
+
+/***/ },
+/* 68 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var util = __webpack_require__(1);
+ var Hammer = __webpack_require__(19);
+
+ exports._cleanNavigation = function() {
+ // clean hammer bindings
+ if (this.navigationHammers.existing.length != 0) {
+ for (var i = 0; i < this.navigationHammers.existing.length; i++) {
+ this.navigationHammers.existing[i].dispose();
}
+ this.navigationHammers.existing = [];
}
- };
+ this._navigationReleaseOverload = function () {};
+
+ // clean up previous navigation items
+ if (this.navigationDivs && this.navigationDivs['wrapper'] && this.navigationDivs['wrapper'].parentNode) {
+ this.navigationDivs['wrapper'].parentNode.removeChild(this.navigationDivs['wrapper']);
+ }
+ };
/**
- * Unfix nodes
+ * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
+ * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
+ * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
+ * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
*
* @private
*/
- exports._restoreNodes = function() {
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- this.nodes[nodeId].xFixed = false;
- this.nodes[nodeId].yFixed = false;
- }
+ exports._loadNavigationElements = function() {
+ this._cleanNavigation();
+
+ this.navigationDivs = {};
+ var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
+ var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','_zoomExtent'];
+
+ this.navigationDivs['wrapper'] = document.createElement('div');
+ this.frame.appendChild(this.navigationDivs['wrapper']);
+
+ for (var i = 0; i < navigationDivs.length; i++) {
+ this.navigationDivs[navigationDivs[i]] = document.createElement('div');
+ this.navigationDivs[navigationDivs[i]].className = 'network-navigation ' + navigationDivs[i];
+ this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
+
+ var hammer = Hammer(this.navigationDivs[navigationDivs[i]], {prevent_default: true});
+ hammer.on('touch', this[navigationDivActions[i]].bind(this));
+ this.navigationHammers._new.push(hammer);
}
- };
+ this._navigationReleaseOverload = this._stopMovement;
+
+ this.navigationHammers.existing = this.navigationHammers._new;
+ };
-/***/ },
-/* 69 */
-/***/ function(module, exports, __webpack_require__) {
/**
- * Calculate the forces the nodes apply on each other based on a repulsion field.
- * This field is linearly approximated.
+ * this stops all movement induced by the navigation buttons
*
* @private
*/
- exports._calculateNodeForces = function () {
- var dx, dy, angle, distance, fx, fy, combinedClusterSize,
- repulsingForce, node1, node2, i, j;
-
- var nodes = this.calculationNodes;
- var nodeIndices = this.calculationNodeIndices;
+ exports._zoomExtent = function(event) {
+ this.zoomExtent({duration:700});
+ event.stopPropagation();
+ };
- // approximation constants
- var a_base = -2 / 3;
- var b = 4 / 3;
+ /**
+ * this stops all movement induced by the navigation buttons
+ *
+ * @private
+ */
+ exports._stopMovement = function() {
+ this._xStopMoving();
+ this._yStopMoving();
+ this._stopZoom();
+ };
- // repulsing forces between nodes
- var nodeDistance = this.constants.physics.repulsion.nodeDistance;
- var minimumDistance = nodeDistance;
- // we loop from i over all but the last entree in the array
- // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
- for (i = 0; i < nodeIndices.length - 1; i++) {
- node1 = nodes[nodeIndices[i]];
- for (j = i + 1; j < nodeIndices.length; j++) {
- node2 = nodes[nodeIndices[j]];
- combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
+ /**
+ * move the screen up
+ * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
+ * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
+ * To avoid this behaviour, we do the translation in the start loop.
+ *
+ * @private
+ */
+ exports._moveUp = function(event) {
+ this.yIncrement = this.constants.keyboard.speed.y;
+ this.start(); // if there is no node movement, the calculation wont be done
+ event.preventDefault();
+ };
- dx = node2.x - node1.x;
- dy = node2.y - node1.y;
- distance = Math.sqrt(dx * dx + dy * dy);
- minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
- var a = a_base / minimumDistance;
- if (distance < 2 * minimumDistance) {
- if (distance < 0.5 * minimumDistance) {
- repulsingForce = 1.0;
- }
- else {
- repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
- }
- // amplify the repulsion for clusters.
- repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
- repulsingForce = repulsingForce / Math.max(distance,0.01*minimumDistance);
+ /**
+ * move the screen down
+ * @private
+ */
+ exports._moveDown = function(event) {
+ this.yIncrement = -this.constants.keyboard.speed.y;
+ this.start(); // if there is no node movement, the calculation wont be done
+ event.preventDefault();
+ };
- fx = dx * repulsingForce;
- fy = dy * repulsingForce;
- node1.fx -= fx;
- node1.fy -= fy;
- node2.fx += fx;
- node2.fy += fy;
+ /**
+ * move the screen left
+ * @private
+ */
+ exports._moveLeft = function(event) {
+ this.xIncrement = this.constants.keyboard.speed.x;
+ this.start(); // if there is no node movement, the calculation wont be done
+ event.preventDefault();
+ };
- }
- }
- }
+
+ /**
+ * move the screen right
+ * @private
+ */
+ exports._moveRight = function(event) {
+ this.xIncrement = -this.constants.keyboard.speed.y;
+ this.start(); // if there is no node movement, the calculation wont be done
+ event.preventDefault();
};
-/***/ },
-/* 70 */
-/***/ function(module, exports, __webpack_require__) {
+ /**
+ * Zoom in, using the same method as the movement.
+ * @private
+ */
+ exports._zoomIn = function(event) {
+ this.zoomIncrement = this.constants.keyboard.speed.zoom;
+ this.start(); // if there is no node movement, the calculation wont be done
+ event.preventDefault();
+ };
+
/**
- * Calculate the forces the nodes apply on eachother based on a repulsion field.
- * This field is linearly approximated.
- *
+ * Zoom out
* @private
*/
- exports._calculateNodeForces = function () {
- var dx, dy, distance, fx, fy,
- repulsingForce, node1, node2, i, j;
+ exports._zoomOut = function(event) {
+ this.zoomIncrement = -this.constants.keyboard.speed.zoom;
+ this.start(); // if there is no node movement, the calculation wont be done
+ event.preventDefault();
+ };
- var nodes = this.calculationNodes;
- var nodeIndices = this.calculationNodeIndices;
- // repulsing forces between nodes
- var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
+ /**
+ * Stop zooming and unhighlight the zoom controls
+ * @private
+ */
+ exports._stopZoom = function(event) {
+ this.zoomIncrement = 0;
+ event && event.preventDefault();
+ };
- // we loop from i over all but the last entree in the array
- // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
- for (i = 0; i < nodeIndices.length - 1; i++) {
- node1 = nodes[nodeIndices[i]];
- for (j = i + 1; j < nodeIndices.length; j++) {
- node2 = nodes[nodeIndices[j]];
- // nodes only affect nodes on their level
- if (node1.level == node2.level) {
+ /**
+ * Stop moving in the Y direction and unHighlight the up and down
+ * @private
+ */
+ exports._yStopMoving = function(event) {
+ this.yIncrement = 0;
+ event && event.preventDefault();
+ };
- dx = node2.x - node1.x;
- dy = node2.y - node1.y;
- distance = Math.sqrt(dx * dx + dy * dy);
+ /**
+ * Stop moving in the X direction and unHighlight left and right.
+ * @private
+ */
+ exports._xStopMoving = function(event) {
+ this.xIncrement = 0;
+ event && event.preventDefault();
+ };
- var steepness = 0.05;
- if (distance < nodeDistance) {
- repulsingForce = -Math.pow(steepness*distance,2) + Math.pow(steepness*nodeDistance,2);
- }
- else {
- repulsingForce = 0;
- }
- // normalize force with
- if (distance == 0) {
- distance = 0.01;
- }
- else {
- repulsingForce = repulsingForce / distance;
- }
- fx = dx * repulsingForce;
- fy = dy * repulsingForce;
- node1.fx -= fx;
- node1.fy -= fy;
- node2.fx += fx;
- node2.fy += fy;
+/***/ },
+/* 69 */
+/***/ function(module, exports, __webpack_require__) {
+
+ exports._resetLevels = function() {
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ var node = this.nodes[nodeId];
+ if (node.preassignedLevel == false) {
+ node.level = -1;
+ node.hierarchyEnumerated = false;
}
}
}
};
-
/**
- * this function calculates the effects of the springs in the case of unsmooth curves.
+ * This is the main function to layout the nodes in a hierarchical way.
+ * It checks if the node details are supplied correctly
*
* @private
*/
- exports._calculateHierarchicalSpringForces = function () {
- var edgeLength, edge, edgeId;
- var dx, dy, fx, fy, springForce, distance;
- var edges = this.edges;
+ exports._setupHierarchicalLayout = function() {
+ if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
+ if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
+ this.constants.hierarchicalLayout.levelSeparation = this.constants.hierarchicalLayout.levelSeparation < 0 ? this.constants.hierarchicalLayout.levelSeparation : this.constants.hierarchicalLayout.levelSeparation * -1;
+ }
+ else {
+ this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
+ }
- var nodes = this.calculationNodes;
- var nodeIndices = this.calculationNodeIndices;
+ if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "LR") {
+ if (this.constants.smoothCurves.enabled == true) {
+ this.constants.smoothCurves.type = "vertical";
+ }
+ }
+ else {
+ if (this.constants.smoothCurves.enabled == true) {
+ this.constants.smoothCurves.type = "horizontal";
+ }
+ }
+ // get the size of the largest hubs and check if the user has defined a level for a node.
+ var hubsize = 0;
+ var node, nodeId;
+ var definedLevel = false;
+ var undefinedLevel = false;
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (node.level != -1) {
+ definedLevel = true;
+ }
+ else {
+ undefinedLevel = true;
+ }
+ if (hubsize < node.edges.length) {
+ hubsize = node.edges.length;
+ }
+ }
+ }
- for (var i = 0; i < nodeIndices.length; i++) {
- var node1 = nodes[nodeIndices[i]];
- node1.springFx = 0;
- node1.springFy = 0;
- }
+ // if the user defined some levels but not all, alert and run without hierarchical layout
+ if (undefinedLevel == true && definedLevel == true) {
+ throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
+ this.zoomExtent(undefined,true,this.constants.clustering.enabled);
+ if (!this.constants.clustering.enabled) {
+ this.start();
+ }
+ }
+ else {
+ // setup the system to use hierarchical method.
+ this._changeConstants();
+ // define levels if undefined by the users. Based on hubsize
+ if (undefinedLevel == true) {
+ if (this.constants.hierarchicalLayout.layout == "hubsize") {
+ this._determineLevels(hubsize);
+ }
+ else {
+ this._determineLevelsDirected();
+ }
- // forces caused by the edges, modelled as springs
- for (edgeId in edges) {
- if (edges.hasOwnProperty(edgeId)) {
- edge = edges[edgeId];
- if (edge.connected) {
- // only calculate forces if nodes are in the same sector
- if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
- edgeLength = edge.physics.springLength;
- // this implies that the edges between big clusters are longer
- edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
+ }
+ // check the distribution of the nodes per level.
+ var distribution = this._getDistribution();
- dx = (edge.from.x - edge.to.x);
- dy = (edge.from.y - edge.to.y);
- distance = Math.sqrt(dx * dx + dy * dy);
+ // place the nodes on the canvas. This also stablilizes the system.
+ this._placeNodesByHierarchy(distribution);
- if (distance == 0) {
- distance = 0.01;
- }
+ // start the simulation.
+ this.start();
+ }
+ }
+ };
- // the 1/distance is so the fx and fy can be calculated without sine or cosine.
- springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
- fx = dx * springForce;
- fy = dy * springForce;
+ /**
+ * This function places the nodes on the canvas based on the hierarchial distribution.
+ *
+ * @param {Object} distribution | obtained by the function this._getDistribution()
+ * @private
+ */
+ exports._placeNodesByHierarchy = function(distribution) {
+ var nodeId, node;
+ // start placing all the level 0 nodes first. Then recursively position their branches.
+ for (var level in distribution) {
+ if (distribution.hasOwnProperty(level)) {
+ for (nodeId in distribution[level].nodes) {
+ if (distribution[level].nodes.hasOwnProperty(nodeId)) {
+ node = distribution[level].nodes[nodeId];
+ if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
+ if (node.xFixed) {
+ node.x = distribution[level].minPos;
+ node.xFixed = false;
- if (edge.to.level != edge.from.level) {
- edge.to.springFx -= fx;
- edge.to.springFy -= fy;
- edge.from.springFx += fx;
- edge.from.springFy += fy;
+ distribution[level].minPos += distribution[level].nodeSpacing;
+ }
}
else {
- var factor = 0.5;
- edge.to.fx -= factor*fx;
- edge.to.fy -= factor*fy;
- edge.from.fx += factor*fx;
- edge.from.fy += factor*fy;
+ if (node.yFixed) {
+ node.y = distribution[level].minPos;
+ node.yFixed = false;
+
+ distribution[level].minPos += distribution[level].nodeSpacing;
+ }
}
+ this._placeBranchNodes(node.edges,node.id,distribution,node.level);
}
}
}
}
- // normalize spring forces
- var springForce = 1;
- var springFx, springFy;
- for (i = 0; i < nodeIndices.length; i++) {
- var node = nodes[nodeIndices[i]];
- springFx = Math.min(springForce,Math.max(-springForce,node.springFx));
- springFy = Math.min(springForce,Math.max(-springForce,node.springFy));
-
- node.fx += springFx;
- node.fy += springFy;
- }
-
- // retain energy balance
- var totalFx = 0;
- var totalFy = 0;
- for (i = 0; i < nodeIndices.length; i++) {
- var node = nodes[nodeIndices[i]];
- totalFx += node.fx;
- totalFy += node.fy;
- }
- var correctionFx = totalFx / nodeIndices.length;
- var correctionFy = totalFy / nodeIndices.length;
-
- for (i = 0; i < nodeIndices.length; i++) {
- var node = nodes[nodeIndices[i]];
- node.fx -= correctionFx;
- node.fy -= correctionFy;
- }
-
+ // stabilize the system after positioning. This function calls zoomExtent.
+ this._stabilize();
};
-/***/ },
-/* 71 */
-/***/ function(module, exports, __webpack_require__) {
/**
- * This function calculates the forces the nodes apply on eachother based on a gravitational model.
- * The Barnes Hut method is used to speed up this N-body simulation.
+ * This function get the distribution of levels based on hubsize
*
+ * @returns {Object}
* @private
*/
- exports._calculateNodeForces = function() {
- if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
- var node;
- var nodes = this.calculationNodes;
- var nodeIndices = this.calculationNodeIndices;
- var nodeCount = nodeIndices.length;
-
- this._formBarnesHutTree(nodes,nodeIndices);
+ exports._getDistribution = function() {
+ var distribution = {};
+ var nodeId, node, level;
- var barnesHutTree = this.barnesHutTree;
+ // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
+ // the fix of X is removed after the x value has been set.
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ node.xFixed = true;
+ node.yFixed = true;
+ if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
+ node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
+ }
+ else {
+ node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
+ }
+ if (distribution[node.level] === undefined) {
+ distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
+ }
+ distribution[node.level].amount += 1;
+ distribution[node.level].nodes[nodeId] = node;
+ }
+ }
- // place the nodes one by one recursively
- for (var i = 0; i < nodeCount; i++) {
- node = nodes[nodeIndices[i]];
- if (node.options.mass > 0) {
- // starting with root is irrelevant, it never passes the BarnesHut condition
- this._getForceContribution(barnesHutTree.root.children.NW,node);
- this._getForceContribution(barnesHutTree.root.children.NE,node);
- this._getForceContribution(barnesHutTree.root.children.SW,node);
- this._getForceContribution(barnesHutTree.root.children.SE,node);
+ // determine the largest amount of nodes of all levels
+ var maxCount = 0;
+ for (level in distribution) {
+ if (distribution.hasOwnProperty(level)) {
+ if (maxCount < distribution[level].amount) {
+ maxCount = distribution[level].amount;
}
}
}
+
+ // set the initial position and spacing of each nodes accordingly
+ for (level in distribution) {
+ if (distribution.hasOwnProperty(level)) {
+ distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
+ distribution[level].nodeSpacing /= (distribution[level].amount + 1);
+ distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
+ }
+ }
+
+ return distribution;
};
/**
- * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
- * If a region contains a single node, we check if it is not itself, then we apply the force.
+ * this function allocates nodes in levels based on the recursive branching from the largest hubs.
*
- * @param parentBranch
- * @param node
+ * @param hubsize
* @private
*/
- exports._getForceContribution = function(parentBranch,node) {
- // we get no force contribution from an empty region
- if (parentBranch.childrenCount > 0) {
- var dx,dy,distance;
-
- // get the distance from the center of mass to the node.
- dx = parentBranch.centerOfMass.x - node.x;
- dy = parentBranch.centerOfMass.y - node.y;
- distance = Math.sqrt(dx * dx + dy * dy);
+ exports._determineLevels = function(hubsize) {
+ var nodeId, node;
- // BarnesHut condition
- // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed
- // calcSize = 1/s --> d * 1/s > 1/theta = passed
- if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.thetaInverted) {
- // duplicate code to reduce function calls to speed up program
- if (distance == 0) {
- distance = 0.1*Math.random();
- dx = distance;
+ // determine hubs
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (node.edges.length == hubsize) {
+ node.level = 0;
}
- var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
- var fx = dx * gravityForce;
- var fy = dy * gravityForce;
- node.fx += fx;
- node.fy += fy;
}
- else {
- // Did not pass the condition, go into children if available
- if (parentBranch.childrenCount == 4) {
- this._getForceContribution(parentBranch.children.NW,node);
- this._getForceContribution(parentBranch.children.NE,node);
- this._getForceContribution(parentBranch.children.SW,node);
- this._getForceContribution(parentBranch.children.SE,node);
- }
- else { // parentBranch must have only one node, if it was empty we wouldnt be here
- if (parentBranch.children.data.id != node.id) { // if it is not self
- // duplicate code to reduce function calls to speed up program
- if (distance == 0) {
- distance = 0.5*Math.random();
- dx = distance;
- }
- var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
- var fx = dx * gravityForce;
- var fy = dy * gravityForce;
- node.fx += fx;
- node.fy += fy;
- }
+ }
+
+ // branch from hubs
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (node.level == 0) {
+ this._setLevel(1,node.edges,node.id);
}
}
}
};
/**
- * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
+ * this function allocates nodes in levels based on the recursive branching from the largest hubs.
*
- * @param nodes
- * @param nodeIndices
+ * @param hubsize
* @private
*/
- exports._formBarnesHutTree = function(nodes,nodeIndices) {
- var node;
- var nodeCount = nodeIndices.length;
-
- var minX = Number.MAX_VALUE,
- minY = Number.MAX_VALUE,
- maxX =-Number.MAX_VALUE,
- maxY =-Number.MAX_VALUE;
+ exports._determineLevelsDirected = function() {
+ var nodeId, node;
- // get the range of the nodes
- for (var i = 0; i < nodeCount; i++) {
- var x = nodes[nodeIndices[i]].x;
- var y = nodes[nodeIndices[i]].y;
- if (nodes[nodeIndices[i]].options.mass > 0) {
- if (x < minX) { minX = x; }
- if (x > maxX) { maxX = x; }
- if (y < minY) { minY = y; }
- if (y > maxY) { maxY = y; }
+ // set first node to source
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ this.nodes[nodeId].level = 10000;
+ break;
}
}
- // make the range a square
- var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
- if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
- else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
+ // branch from hubs
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (node.level == 10000) {
+ this._setLevelDirected(10000,node.edges,node.id);
+ }
+ }
+ }
- var minimumTreeSize = 1e-5;
- var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
- var halfRootSize = 0.5 * rootSize;
- var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
- // construct the barnesHutTree
- var barnesHutTree = {
- root:{
- centerOfMass: {x:0, y:0},
- mass:0,
- range: {
- minX: centerX-halfRootSize,maxX:centerX+halfRootSize,
- minY: centerY-halfRootSize,maxY:centerY+halfRootSize
- },
- size: rootSize,
- calcSize: 1 / rootSize,
- children: { data:null},
- maxWidth: 0,
- level: 0,
- childrenCount: 4
+ // branch from hubs
+ var minLevel = 10000;
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ minLevel = node.level < minLevel ? node.level : minLevel;
}
- };
- this._splitBranch(barnesHutTree.root);
+ }
- // place the nodes one by one recursively
- for (i = 0; i < nodeCount; i++) {
- node = nodes[nodeIndices[i]];
- if (node.options.mass > 0) {
- this._placeInTree(barnesHutTree.root,node);
+ // branch from hubs
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ node.level -= minLevel;
}
}
-
- // make global
- this.barnesHutTree = barnesHutTree
};
/**
- * this updates the mass of a branch. this is increased by adding a node.
+ * Since hierarchical layout does not support:
+ * - smooth curves (based on the physics),
+ * - clustering (based on dynamic node counts)
+ *
+ * We disable both features so there will be no problems.
*
- * @param parentBranch
- * @param node
* @private
*/
- exports._updateBranchMass = function(parentBranch, node) {
- var totalMass = parentBranch.mass + node.options.mass;
- var totalMassInv = 1/totalMass;
-
- parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
- parentBranch.centerOfMass.x *= totalMassInv;
-
- parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
- parentBranch.centerOfMass.y *= totalMassInv;
-
- parentBranch.mass = totalMass;
- var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
- parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
-
+ exports._changeConstants = function() {
+ this.constants.clustering.enabled = false;
+ this.constants.physics.barnesHut.enabled = false;
+ this.constants.physics.hierarchicalRepulsion.enabled = true;
+ this._loadSelectedForceSolver();
+ if (this.constants.smoothCurves.enabled == true) {
+ this.constants.smoothCurves.dynamic = false;
+ }
+ this._configureSmoothCurves();
};
/**
- * determine in which branch the node will be placed.
+ * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
+ * on a X position that ensures there will be no overlap.
*
- * @param parentBranch
- * @param node
- * @param skipMassUpdate
+ * @param edges
+ * @param parentId
+ * @param distribution
+ * @param parentLevel
* @private
*/
- exports._placeInTree = function(parentBranch,node,skipMassUpdate) {
- if (skipMassUpdate != true || skipMassUpdate === undefined) {
- // update the mass of the branch.
- this._updateBranchMass(parentBranch,node);
- }
-
- if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
- if (parentBranch.children.NW.range.maxY > node.y) { // in NW
- this._placeInRegion(parentBranch,node,"NW");
+ exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) {
+ for (var i = 0; i < edges.length; i++) {
+ var childNode = null;
+ if (edges[i].toId == parentId) {
+ childNode = edges[i].from;
}
- else { // in SW
- this._placeInRegion(parentBranch,node,"SW");
+ else {
+ childNode = edges[i].to;
}
- }
- else { // in NE or SE
- if (parentBranch.children.NW.range.maxY > node.y) { // in NE
- this._placeInRegion(parentBranch,node,"NE");
+
+ // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
+ var nodeMoved = false;
+ if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
+ if (childNode.xFixed && childNode.level > parentLevel) {
+ childNode.xFixed = false;
+ childNode.x = distribution[childNode.level].minPos;
+ nodeMoved = true;
+ }
}
- else { // in SE
- this._placeInRegion(parentBranch,node,"SE");
+ else {
+ if (childNode.yFixed && childNode.level > parentLevel) {
+ childNode.yFixed = false;
+ childNode.y = distribution[childNode.level].minPos;
+ nodeMoved = true;
+ }
+ }
+
+ if (nodeMoved == true) {
+ distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
+ if (childNode.edges.length > 1) {
+ this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
+ }
}
}
};
/**
- * actually place the node in a region (or branch)
+ * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
*
- * @param parentBranch
- * @param node
- * @param region
+ * @param level
+ * @param edges
+ * @param parentId
* @private
*/
- exports._placeInRegion = function(parentBranch,node,region) {
- switch (parentBranch.children[region].childrenCount) {
- case 0: // place node here
- parentBranch.children[region].children.data = node;
- parentBranch.children[region].childrenCount = 1;
- this._updateBranchMass(parentBranch.children[region],node);
- break;
- case 1: // convert into children
- // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
- // we move one node a pixel and we do not put it in the tree.
- if (parentBranch.children[region].children.data.x == node.x &&
- parentBranch.children[region].children.data.y == node.y) {
- node.x += Math.random();
- node.y += Math.random();
- }
- else {
- this._splitBranch(parentBranch.children[region]);
- this._placeInTree(parentBranch.children[region],node);
+ exports._setLevel = function(level, edges, parentId) {
+ for (var i = 0; i < edges.length; i++) {
+ var childNode = null;
+ if (edges[i].toId == parentId) {
+ childNode = edges[i].from;
+ }
+ else {
+ childNode = edges[i].to;
+ }
+ if (childNode.level == -1 || childNode.level > level) {
+ childNode.level = level;
+ if (childNode.edges.length > 1) {
+ this._setLevel(level+1, childNode.edges, childNode.id);
}
- break;
- case 4: // place in branch
- this._placeInTree(parentBranch.children[region],node);
- break;
+ }
}
};
/**
- * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
- * after the split is complete.
+ * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
*
- * @param parentBranch
+ * @param level
+ * @param edges
+ * @param parentId
* @private
*/
- exports._splitBranch = function(parentBranch) {
- // if the branch is shaded with a node, replace the node in the new subset.
- var containedNode = null;
- if (parentBranch.childrenCount == 1) {
- containedNode = parentBranch.children.data;
- parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
+ exports._setLevelDirected = function(level, edges, parentId) {
+ this.nodes[parentId].hierarchyEnumerated = true;
+ for (var i = 0; i < edges.length; i++) {
+ var childNode = null;
+ var direction = 1;
+ if (edges[i].toId == parentId) {
+ childNode = edges[i].from;
+ direction = -1;
+ }
+ else {
+ childNode = edges[i].to;
+ }
+ if (childNode.level == -1) {
+ childNode.level = level + direction;
+ }
}
- parentBranch.childrenCount = 4;
- parentBranch.children.data = null;
- this._insertRegion(parentBranch,"NW");
- this._insertRegion(parentBranch,"NE");
- this._insertRegion(parentBranch,"SW");
- this._insertRegion(parentBranch,"SE");
- if (containedNode != null) {
- this._placeInTree(parentBranch,containedNode);
+ for (var i = 0; i < edges.length; i++) {
+ var childNode = null;
+ if (edges[i].toId == parentId) {childNode = edges[i].from;}
+ else {childNode = edges[i].to;}
+ if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) {
+ this._setLevelDirected(childNode.level, childNode.edges, childNode.id);
+ }
}
};
/**
- * This function subdivides the region into four new segments.
- * Specifically, this inserts a single new segment.
- * It fills the children section of the parentBranch
+ * Unfix nodes
*
- * @param parentBranch
- * @param region
- * @param parentRange
* @private
*/
- exports._insertRegion = function(parentBranch, region) {
- var minX,maxX,minY,maxY;
- var childSize = 0.5 * parentBranch.size;
- switch (region) {
- case "NW":
- minX = parentBranch.range.minX;
- maxX = parentBranch.range.minX + childSize;
- minY = parentBranch.range.minY;
- maxY = parentBranch.range.minY + childSize;
- break;
- case "NE":
- minX = parentBranch.range.minX + childSize;
- maxX = parentBranch.range.maxX;
- minY = parentBranch.range.minY;
- maxY = parentBranch.range.minY + childSize;
- break;
- case "SW":
- minX = parentBranch.range.minX;
- maxX = parentBranch.range.minX + childSize;
- minY = parentBranch.range.minY + childSize;
- maxY = parentBranch.range.maxY;
- break;
- case "SE":
- minX = parentBranch.range.minX + childSize;
- maxX = parentBranch.range.maxX;
- minY = parentBranch.range.minY + childSize;
- maxY = parentBranch.range.maxY;
- break;
+ exports._restoreNodes = function() {
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ this.nodes[nodeId].xFixed = false;
+ this.nodes[nodeId].yFixed = false;
+ }
}
+ };
- parentBranch.children[region] = {
- centerOfMass:{x:0,y:0},
- mass:0,
- range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
- size: 0.5 * parentBranch.size,
- calcSize: 2 * parentBranch.calcSize,
- children: {data:null},
- maxWidth: 0,
- level: parentBranch.level+1,
- childrenCount: 0
- };
+/***/ },
+/* 70 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // English
+ exports['en'] = {
+ edit: 'Edit',
+ del: 'Delete selected',
+ back: 'Back',
+ addNode: 'Add Node',
+ addEdge: 'Add Edge',
+ editNode: 'Edit Node',
+ editEdge: 'Edit Edge',
+ addDescription: 'Click in an empty space to place a new node.',
+ edgeDescription: 'Click on a node and drag the edge to another node to connect them.',
+ editEdgeDescription: 'Click on the control points and drag them to a node to connect to it.',
+ createEdgeError: 'Cannot link edges to a cluster.',
+ deleteClusterError: 'Clusters cannot be deleted.'
+ };
+ exports['en_EN'] = exports['en'];
+ exports['en_US'] = exports['en'];
+
+ // Dutch
+ exports['nl'] = {
+ edit: 'Wijzigen',
+ del: 'Selectie verwijderen',
+ back: 'Terug',
+ addNode: 'Node toevoegen',
+ addEdge: 'Link toevoegen',
+ editNode: 'Node wijzigen',
+ editEdge: 'Link wijzigen',
+ addDescription: 'Klik op een leeg gebied om een nieuwe node te maken.',
+ edgeDescription: 'Klik op een node en sleep de link naar een andere node om ze te verbinden.',
+ editEdgeDescription: 'Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.',
+ createEdgeError: 'Kan geen link maken naar een cluster.',
+ deleteClusterError: 'Clusters kunnen niet worden verwijderd.'
};
+ exports['nl_NL'] = exports['nl'];
+ exports['nl_BE'] = exports['nl'];
+
+/***/ },
+/* 71 */
+/***/ function(module, exports, __webpack_require__) {
/**
- * This function is for debugging purposed, it draws the tree.
- *
- * @param ctx
- * @param color
- * @private
+ * Canvas shapes used by Network
*/
- exports._drawTree = function(ctx,color) {
- if (this.barnesHutTree !== undefined) {
+ if (typeof CanvasRenderingContext2D !== 'undefined') {
- ctx.lineWidth = 1;
+ /**
+ * Draw a circle shape
+ */
+ CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
+ this.beginPath();
+ this.arc(x, y, r, 0, 2*Math.PI, false);
+ };
- this._drawBranch(this.barnesHutTree.root,ctx,color);
- }
- };
+ /**
+ * Draw a square shape
+ * @param {Number} x horizontal center
+ * @param {Number} y vertical center
+ * @param {Number} r size, width and height of the square
+ */
+ CanvasRenderingContext2D.prototype.square = function(x, y, r) {
+ this.beginPath();
+ this.rect(x - r, y - r, r * 2, r * 2);
+ };
+ /**
+ * Draw a triangle shape
+ * @param {Number} x horizontal center
+ * @param {Number} y vertical center
+ * @param {Number} r radius, half the length of the sides of the triangle
+ */
+ CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
+ // http://en.wikipedia.org/wiki/Equilateral_triangle
+ this.beginPath();
- /**
- * This function is for debugging purposes. It draws the branches recursively.
- *
- * @param branch
- * @param ctx
- * @param color
- * @private
- */
- exports._drawBranch = function(branch,ctx,color) {
- if (color === undefined) {
- color = "#FF0000";
- }
+ var s = r * 2;
+ var s2 = s / 2;
+ var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
+ var h = Math.sqrt(s * s - s2 * s2); // height
- if (branch.childrenCount == 4) {
- this._drawBranch(branch.children.NW,ctx);
- this._drawBranch(branch.children.NE,ctx);
- this._drawBranch(branch.children.SE,ctx);
- this._drawBranch(branch.children.SW,ctx);
- }
- ctx.strokeStyle = color;
- ctx.beginPath();
- ctx.moveTo(branch.range.minX,branch.range.minY);
- ctx.lineTo(branch.range.maxX,branch.range.minY);
- ctx.stroke();
+ this.moveTo(x, y - (h - ir));
+ this.lineTo(x + s2, y + ir);
+ this.lineTo(x - s2, y + ir);
+ this.lineTo(x, y - (h - ir));
+ this.closePath();
+ };
- ctx.beginPath();
- ctx.moveTo(branch.range.maxX,branch.range.minY);
- ctx.lineTo(branch.range.maxX,branch.range.maxY);
- ctx.stroke();
+ /**
+ * Draw a triangle shape in downward orientation
+ * @param {Number} x horizontal center
+ * @param {Number} y vertical center
+ * @param {Number} r radius
+ */
+ CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
+ // http://en.wikipedia.org/wiki/Equilateral_triangle
+ this.beginPath();
- ctx.beginPath();
- ctx.moveTo(branch.range.maxX,branch.range.maxY);
- ctx.lineTo(branch.range.minX,branch.range.maxY);
- ctx.stroke();
+ var s = r * 2;
+ var s2 = s / 2;
+ var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
+ var h = Math.sqrt(s * s - s2 * s2); // height
- ctx.beginPath();
- ctx.moveTo(branch.range.minX,branch.range.maxY);
- ctx.lineTo(branch.range.minX,branch.range.minY);
- ctx.stroke();
+ this.moveTo(x, y + (h - ir));
+ this.lineTo(x + s2, y - ir);
+ this.lineTo(x - s2, y - ir);
+ this.lineTo(x, y + (h - ir));
+ this.closePath();
+ };
- /*
- if (branch.mass > 0) {
- ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
- ctx.stroke();
- }
+ /**
+ * Draw a star shape, a star with 5 points
+ * @param {Number} x horizontal center
+ * @param {Number} y vertical center
+ * @param {Number} r radius, half the length of the sides of the triangle
*/
- };
+ CanvasRenderingContext2D.prototype.star = function(x, y, r) {
+ // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
+ this.beginPath();
+
+ for (var n = 0; n < 10; n++) {
+ var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
+ this.lineTo(
+ x + radius * Math.sin(n * 2 * Math.PI / 10),
+ y - radius * Math.cos(n * 2 * Math.PI / 10)
+ );
+ }
+
+ this.closePath();
+ };
+
+ /**
+ * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
+ */
+ CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
+ var r2d = Math.PI/180;
+ if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
+ if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
+ this.beginPath();
+ this.moveTo(x+r,y);
+ this.lineTo(x+w-r,y);
+ this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
+ this.lineTo(x+w,y+h-r);
+ this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
+ this.lineTo(x+r,y+h);
+ this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
+ this.lineTo(x,y+r);
+ this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
+ };
+
+ /**
+ * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
+ */
+ CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
+ var kappa = .5522848,
+ ox = (w / 2) * kappa, // control point offset horizontal
+ oy = (h / 2) * kappa, // control point offset vertical
+ xe = x + w, // x-end
+ ye = y + h, // y-end
+ xm = x + w / 2, // x-middle
+ ym = y + h / 2; // y-middle
+
+ this.beginPath();
+ this.moveTo(x, ym);
+ this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
+ this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
+ this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
+ this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
+ };
+
+
+
+ /**
+ * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
+ */
+ CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
+ var f = 1/3;
+ var wEllipse = w;
+ var hEllipse = h * f;
+
+ var kappa = .5522848,
+ ox = (wEllipse / 2) * kappa, // control point offset horizontal
+ oy = (hEllipse / 2) * kappa, // control point offset vertical
+ xe = x + wEllipse, // x-end
+ ye = y + hEllipse, // y-end
+ xm = x + wEllipse / 2, // x-middle
+ ym = y + hEllipse / 2, // y-middle
+ ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
+ yeb = y + h; // y-end, bottom ellipse
+
+ this.beginPath();
+ this.moveTo(xe, ym);
+
+ this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
+ this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
+
+ this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
+ this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
+
+ this.lineTo(xe, ymb);
+
+ this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
+ this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
+
+ this.lineTo(x, ym);
+ };
+
+
+ /**
+ * Draw an arrow point (no line)
+ */
+ CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
+ // tail
+ var xt = x - length * Math.cos(angle);
+ var yt = y - length * Math.sin(angle);
+
+ // inner tail
+ // TODO: allow to customize different shapes
+ var xi = x - length * 0.9 * Math.cos(angle);
+ var yi = y - length * 0.9 * Math.sin(angle);
+
+ // left
+ var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
+ var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
+
+ // right
+ var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
+ var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
+
+ this.beginPath();
+ this.moveTo(x, y);
+ this.lineTo(xl, yl);
+ this.lineTo(xi, yi);
+ this.lineTo(xr, yr);
+ this.closePath();
+ };
+
+ /**
+ * Sets up the dashedLine functionality for drawing
+ * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
+ * @author David Jordan
+ * @date 2012-08-08
+ */
+ CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
+ if (!dashArray) dashArray=[10,5];
+ if (dashLength==0) dashLength = 0.001; // Hack for Safari
+ var dashCount = dashArray.length;
+ this.moveTo(x, y);
+ var dx = (x2-x), dy = (y2-y);
+ var slope = dy/dx;
+ var distRemaining = Math.sqrt( dx*dx + dy*dy );
+ var dashIndex=0, draw=true;
+ while (distRemaining>=0.1){
+ var dashLength = dashArray[dashIndex++%dashCount];
+ if (dashLength > distRemaining) dashLength = distRemaining;
+ var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
+ if (dx<0) xStep = -xStep;
+ x += xStep;
+ y += slope*xStep;
+ this[draw ? 'lineTo' : 'moveTo'](x,y);
+ distRemaining -= dashLength;
+ draw = !draw;
+ }
+ };
+
+ // TODO: add diamond shape
+ }
/***/ }
diff --git a/docs/graph2d.html b/docs/graph2d.html
index bcab9602..df23f03d 100644
--- a/docs/graph2d.html
+++ b/docs/graph2d.html
@@ -452,18 +452,6 @@ The options colored in green can also be used as options for the groups. All opt
true |
Toggle the drawing of the major labels on the Y axis. |
-
- dataAxis.showMajorLines |
- Boolean |
- true |
- Toggle the drawing of the major lines on the Y axis. |
-
-
- dataAxis.showMinorLines |
- Boolean |
- true |
- Toggle the drawing of the major lines on the Y axis. |
-
dataAxis.icons |
Boolean |
diff --git a/docs/timeline.html b/docs/timeline.html
index d537db95..4491a6e1 100644
--- a/docs/timeline.html
+++ b/docs/timeline.html
@@ -742,24 +742,6 @@ var options = {
showMinorLabels
are false, no horizontal axis will be
visible.
-
-
-
- showMajorLines |
- boolean |
- true |
- By default, the timeline shows both minor and major date lines on the
- time axis. You can use this option to hide the lines from the major dates.
- |
-
-
- showMinorLines |
- boolean |
- true |
- By default, the timeline shows both minor and major date lines on the
- time axis. You can use this option to hide the lines from the minor dates.
- |
-
stack |
Boolean |
diff --git a/lib/timeline/component/DataAxis.js b/lib/timeline/component/DataAxis.js
index f605ba26..c82c8361 100644
--- a/lib/timeline/component/DataAxis.js
+++ b/lib/timeline/component/DataAxis.js
@@ -19,8 +19,6 @@ function DataAxis (body, options, svg, linegraphOptions) {
orientation: 'left', // supported: 'left', 'right'
showMinorLabels: true,
showMajorLabels: true,
- showMinorLines: true,
- showMajorLines: true,
icons: true,
majorLinesOffset: 7,
minorLinesOffset: 4,
@@ -120,8 +118,6 @@ DataAxis.prototype.setOptions = function (options) {
'orientation',
'showMinorLabels',
'showMajorLabels',
- 'showMajorLines',
- 'showMinorLines',
'icons',
'majorLinesOffset',
'minorLinesOffset',
@@ -428,11 +424,9 @@ DataAxis.prototype._redrawLabels = function () {
if (y >= 0) {
this._redrawLabel(y - 2, step.getCurrent(decimals), orientation, 'yAxis major', this.props.majorCharHeight);
}
- if (this.options.showMajorLines == true) {
- this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
- }
+ this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
}
- else if (this.options.showMinorLines == true) {
+ else {
this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth);
}
diff --git a/lib/timeline/component/LineGraph.js b/lib/timeline/component/LineGraph.js
index f962b730..15cea901 100644
--- a/lib/timeline/component/LineGraph.js
+++ b/lib/timeline/component/LineGraph.js
@@ -50,8 +50,6 @@ function LineGraph(body, options) {
dataAxis: {
showMinorLabels: true,
showMajorLabels: true,
- showMinorLines: true,
- showMajorLines: true,
icons: false,
width: '40px',
visible: true,
diff --git a/lib/timeline/component/TimeAxis.js b/lib/timeline/component/TimeAxis.js
index f0622f6f..9992e28d 100644
--- a/lib/timeline/component/TimeAxis.js
+++ b/lib/timeline/component/TimeAxis.js
@@ -40,8 +40,6 @@ function TimeAxis (body, options) {
// TODO: implement timeaxis orientations 'left' and 'right'
showMinorLabels: true,
showMajorLabels: true,
- showMajorLines: true,
- showMinorLines: true,
format: null
};
this.options = util.extend({}, this.defaultOptions);
@@ -67,7 +65,13 @@ TimeAxis.prototype = new Component();
TimeAxis.prototype.setOptions = function(options) {
if (options) {
// copy all options that we know
- util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels', 'showMinorLines', 'showMajorLines','hiddenDates', 'format'], this.options, options);
+ util.selectiveExtend([
+ 'orientation',
+ 'showMinorLabels',
+ 'showMajorLabels',
+ 'hiddenDates',
+ 'format'
+ ], this.options, options);
// apply locale to moment.js
// TODO: not so nice, this is applied globally to moment.js
@@ -226,11 +230,9 @@ TimeAxis.prototype._repaintLabels = function () {
}
this._repaintMajorText(x, step.getLabelMajor(), orientation);
}
- if (this.options.showMajorLines == true) {
- this._repaintMajorLine(x, orientation);
- }
+ this._repaintMajorLine(x, orientation);
}
- else if (this.options.showMinorLines == true) {
+ else {
this._repaintMinorLine(x, orientation);
}