From 7b0feeffc23c2c1b598843ea9c10fb19e84d72b7 Mon Sep 17 00:00:00 2001
From: Alex de Mulder
';
- }
- }
- }
- this.dom.textArea.innerHTML = content;
- this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px';
- }
- };
+ var moment = __webpack_require__(22);
- Legend.prototype.drawLegendIcons = function() {
- if (this.dom.frame.parentNode) {
- DOMutil.prepareElements(this.svgElements);
- var padding = window.getComputedStyle(this.dom.frame).paddingTop;
- var iconOffset = Number(padding.replace('px',''));
- var x = iconOffset;
- var iconWidth = this.options.iconSize;
- var iconHeight = 0.75 * this.options.iconSize;
- var y = iconOffset + 0.5 * iconHeight + 3;
+ /**
+ * @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
+ */
+ function TimeStep(start, end, minimumStep) {
+ // variables
+ this.current = new Date();
+ this._start = new Date();
+ this._end = new Date();
- this.svg.style.width = iconWidth + 5 + iconOffset + 'px';
+ this.autoScale = true;
+ this.scale = TimeStep.SCALE.DAY;
+ this.step = 1;
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
- this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
- y += iconHeight + this.options.iconSpacing;
- }
- }
- }
+ // initialize the range
+ this.setRange(start, end, minimumStep);
+ }
- DOMutil.cleanupElements(this.svgElements);
- }
+ /// enum scale
+ TimeStep.SCALE = {
+ MILLISECOND: 1,
+ SECOND: 2,
+ MINUTE: 3,
+ HOUR: 4,
+ DAY: 5,
+ WEEKDAY: 6,
+ MONTH: 7,
+ YEAR: 8
};
- module.exports = Legend;
+ /**
+ * 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
+ */
+ TimeStep.prototype.setRange = function(start, end, minimumStep) {
+ if (!(start instanceof Date) || !(end instanceof Date)) {
+ throw "No legal start or end date in method setRange";
+ }
-/***/ },
-/* 26 */
-/***/ function(module, exports, __webpack_require__) {
+ this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
+ this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
- var util = __webpack_require__(1);
- var DOMutil = __webpack_require__(2);
- var DataSet = __webpack_require__(3);
- var DataView = __webpack_require__(4);
- var Component = __webpack_require__(18);
- var DataAxis = __webpack_require__(21);
- var GraphGroup = __webpack_require__(22);
- var Legend = __webpack_require__(25);
+ if (this.autoScale) {
+ this.setMinimumStep(minimumStep);
+ }
+ };
- var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
+ /**
+ * Set the range iterator to the start date.
+ */
+ TimeStep.prototype.first = function() {
+ this.current = new Date(this._start.valueOf());
+ this.roundToMinor();
+ };
/**
- * This is the constructor of the LineGraph. It requires a Timeline body and options.
- *
- * @param body
- * @param options
- * @constructor
+ * Round the current date to the first minor date value
+ * This must be executed once when the current date is set to start Date
*/
- function LineGraph(body, options) {
- this.id = util.randomUUID();
- this.body = body;
+ 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 TimeStep.SCALE.YEAR:
+ this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
+ this.current.setMonth(0);
+ case TimeStep.SCALE.MONTH: this.current.setDate(1);
+ case TimeStep.SCALE.DAY: // intentional fall through
+ case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
+ case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
+ case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
+ case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
+ //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
+ }
- this.defaultOptions = {
- yAxisOrientation: 'left',
- defaultGroup: 'default',
- sort: true,
- sampling: true,
- graphHeight: '400px',
- shaded: {
- enabled: false,
- orientation: 'bottom' // top, bottom
- },
- style: 'line', // line, bar
- barChart: {
- width: 50,
- handleOverlap: 'overlap',
- align: 'center' // left, center, right
- },
- catmullRom: {
- enabled: true,
- parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
- alpha: 0.5
- },
- drawPoints: {
- enabled: true,
- size: 6,
- style: 'square' // square, circle
- },
- dataAxis: {
- showMinorLabels: true,
- showMajorLabels: true,
- icons: false,
- width: '40px',
- visible: true,
- customRange: {
- left: {min:undefined, max:undefined},
- right: {min:undefined, max:undefined}
- }
- },
- legend: {
- enabled: false,
- icons: true,
- left: {
- visible: true,
- position: 'top-left' // top/bottom - left,right
- },
- right: {
- visible: true,
- position: 'top-right' // top/bottom - left,right
- }
- },
- groups: {
- visibility: {}
+ if (this.step != 1) {
+ // round down to the first minor value that is a multiple of the current step size
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
+ case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
+ case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
+ case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
+ case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
+ case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
+ default: break;
}
- };
+ }
+ };
- // options is shared by this ItemSet and all its items
- this.options = util.extend({}, this.defaultOptions);
- this.dom = {};
- this.props = {};
- this.hammer = null;
- this.groups = {};
- this.abortedGraphUpdate = false;
+ /**
+ * 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());
+ };
- var me = this;
- this.itemsData = null; // DataSet
- this.groupsData = null; // DataSet
+ /**
+ * Do the next step
+ */
+ TimeStep.prototype.next = function() {
+ var prev = this.current.valueOf();
- // listeners for the DataSet of the items
- this.itemListeners = {
- 'add': function (event, params, senderId) {
- me._onAdd(params.items);
- },
- 'update': function (event, params, senderId) {
- me._onUpdate(params.items);
- },
- 'remove': function (event, params, senderId) {
- me._onRemove(params.items);
- }
- };
+ // 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 TimeStep.SCALE.MILLISECOND:
- // listeners for the DataSet of the groups
- this.groupListeners = {
- 'add': function (event, params, senderId) {
- me._onAddGroups(params.items);
- },
- 'update': function (event, params, senderId) {
- me._onUpdateGroups(params.items);
- },
- 'remove': function (event, params, senderId) {
- me._onRemoveGroups(params.items);
+ this.current = new Date(this.current.valueOf() + this.step); break;
+ case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
+ case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
+ case TimeStep.SCALE.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 TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
+ case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
+ case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
+ default: break;
}
- };
-
- this.items = {}; // object with an Item for every data item
- this.selection = []; // list with the ids of all selected nodes
- this.lastStart = this.body.range.start;
- this.touchParams = {}; // stores properties while dragging
-
- this.svgElements = {};
- this.setOptions(options);
- this.groupsUsingDefaultStyles = [0];
+ }
+ else {
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
+ case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
+ case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
+ case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
+ case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
+ case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
+ default: break;
+ }
+ }
- this.body.emitter.on("rangechange",function() {
- if (me.lastStart != 0) {
- var offset = me.body.range.start - me.lastStart;
- var range = me.body.range.end - me.body.range.start;
- if (me.width != 0) {
- var rangePerPixelInv = me.width/range;
- var xOffset = offset * rangePerPixelInv;
- me.svg.style.left = (-me.width - xOffset) + "px";
- }
- }
- });
- this.body.emitter.on("rangechanged", function() {
- me.lastStart = me.body.range.start;
- me.svg.style.left = util.option.asSize(-me.width);
- me._updateGraph.apply(me);
- });
+ if (this.step != 1) {
+ // round down to the correct major value
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
+ case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
+ case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
+ case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
+ case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
+ case TimeStep.SCALE.YEAR: break; // nothing to do for year
+ default: break;
+ }
+ }
- // create the HTML DOM
- this._create();
- this.body.emitter.emit("change");
- }
+ // safety mechanism: if current time is still unchanged, move to the end
+ if (this.current.valueOf() == prev) {
+ this.current = new Date(this._end.valueOf());
+ }
+ };
- LineGraph.prototype = new Component();
/**
- * Create the HTML DOM for the ItemSet
+ * Get the current datetime
+ * @return {Date} current The current date
*/
- LineGraph.prototype._create = function(){
- var frame = document.createElement('div');
- frame.className = 'LineGraph';
- this.dom.frame = frame;
-
- // create svg element for graph drawing.
- this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
- this.svg.style.position = "relative";
- this.svg.style.height = ('' + this.options.graphHeight).replace("px",'') + 'px';
- this.svg.style.display = "block";
- frame.appendChild(this.svg);
-
- // data axis
- this.options.dataAxis.orientation = 'left';
- this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
-
- this.options.dataAxis.orientation = 'right';
- this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
- delete this.options.dataAxis.orientation;
-
- // legends
- this.legendLeft = new Legend(this.body, this.options.legend, 'left', this.options.groups);
- this.legendRight = new Legend(this.body, this.options.legend, 'right', this.options.groups);
-
- this.show();
+ TimeStep.prototype.getCurrent = function() {
+ return this.current;
};
/**
- * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
- * @param options
+ * 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 {TimeStep.SCALE} newScale
+ * A scale. Choose from SCALE.MILLISECOND,
+ * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
+ * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
+ * SCALE.YEAR.
+ * @param {Number} newStep A step size, by default 1. Choose for
+ * example 1, 2, 5, or 10.
*/
- LineGraph.prototype.setOptions = function(options) {
- if (options) {
- var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort','groups'];
- util.selectiveDeepExtend(fields, this.options, options);
- util.mergeOptions(this.options, options,'catmullRom');
- util.mergeOptions(this.options, options,'drawPoints');
- util.mergeOptions(this.options, options,'shaded');
- util.mergeOptions(this.options, options,'legend');
-
- if (options.catmullRom) {
- if (typeof options.catmullRom == 'object') {
- if (options.catmullRom.parametrization) {
- if (options.catmullRom.parametrization == 'uniform') {
- this.options.catmullRom.alpha = 0;
- }
- else if (options.catmullRom.parametrization == 'chordal') {
- this.options.catmullRom.alpha = 1.0;
- }
- else {
- this.options.catmullRom.parametrization = 'centripetal';
- this.options.catmullRom.alpha = 0.5;
- }
- }
- }
- }
-
- if (this.yAxisLeft) {
- if (options.dataAxis !== undefined) {
- this.yAxisLeft.setOptions(this.options.dataAxis);
- this.yAxisRight.setOptions(this.options.dataAxis);
- }
- }
-
- if (this.legendLeft) {
- if (options.legend !== undefined) {
- this.legendLeft.setOptions(this.options.legend);
- this.legendRight.setOptions(this.options.legend);
- }
- }
+ TimeStep.prototype.setScale = function(newScale, newStep) {
+ this.scale = newScale;
- if (this.groups.hasOwnProperty(UNGROUPED)) {
- this.groups[UNGROUPED].setOptions(options);
- }
- }
- if (this.dom.frame) {
- this._updateGraph();
+ if (newStep > 0) {
+ this.step = newStep;
}
- };
- /**
- * Hide the component from the DOM
- */
- LineGraph.prototype.hide = function() {
- // remove the frame containing the items
- if (this.dom.frame.parentNode) {
- this.dom.frame.parentNode.removeChild(this.dom.frame);
- }
+ this.autoScale = false;
};
/**
- * Show the component in the DOM (when not already visible).
- * @return {Boolean} changed
+ * Enable or disable autoscaling
+ * @param {boolean} enable If true, autoascaling is set true
*/
- LineGraph.prototype.show = function() {
- // show frame containing the items
- if (!this.dom.frame.parentNode) {
- this.body.dom.center.appendChild(this.dom.frame);
- }
+ TimeStep.prototype.setAutoScale = function (enable) {
+ this.autoScale = enable;
};
/**
- * Set items
- * @param {vis.DataSet | null} items
+ * Automatically determine the scale that bests fits the provided minimum step
+ * @param {Number} [minimumStep] The minimum step size in milliseconds
*/
- LineGraph.prototype.setItems = function(items) {
- var me = this,
- ids,
- oldItemsData = this.itemsData;
-
- // replace the dataset
- if (!items) {
- this.itemsData = null;
- }
- else if (items instanceof DataSet || items instanceof DataView) {
- this.itemsData = items;
- }
- else {
- throw new TypeError('Data must be an instance of DataSet or DataView');
- }
-
- if (oldItemsData) {
- // unsubscribe from old dataset
- util.forEach(this.itemListeners, function (callback, event) {
- oldItemsData.off(event, callback);
- });
-
- // remove all drawn items
- ids = oldItemsData.getIds();
- this._onRemove(ids);
+ TimeStep.prototype.setMinimumStep = function(minimumStep) {
+ if (minimumStep == undefined) {
+ return;
}
- if (this.itemsData) {
- // subscribe to new dataset
- var id = this.id;
- util.forEach(this.itemListeners, function (callback, event) {
- me.itemsData.on(event, callback, id);
- });
+ 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);
- // add all new items
- ids = this.itemsData.getIds();
- this._onAdd(ids);
- }
- this._updateUngrouped();
- this._updateGraph();
- this.redraw();
+ // find the smallest step that is larger than the provided minimumStep
+ if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
+ if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
+ if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
+ if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
+ if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
+ if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
+ if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
+ if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
+ if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
+ if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
+ if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
+ if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
+ if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
+ if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
+ if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
+ if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
+ if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
+ if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
+ if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
+ if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
+ if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
+ if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
+ if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
+ if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
+ if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
+ if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
+ if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
+ if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
+ if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
};
/**
- * Set groups
- * @param {vis.DataSet} groups
+ * 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
*/
- LineGraph.prototype.setGroups = function(groups) {
- var me = this,
- ids;
-
- // unsubscribe from current dataset
- if (this.groupsData) {
- util.forEach(this.groupListeners, function (callback, event) {
- me.groupsData.unsubscribe(event, callback);
- });
+ TimeStep.prototype.snap = function(date) {
+ var clone = new Date(date.valueOf());
- // remove all drawn groups
- ids = this.groupsData.getIds();
- this.groupsData = null;
- this._onRemoveGroups(ids); // note: this will cause a redraw
+ if (this.scale == TimeStep.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 == TimeStep.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);
+ }
- // replace the dataset
- if (!groups) {
- this.groupsData = null;
+ clone.setHours(0);
+ clone.setMinutes(0);
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
}
- else if (groups instanceof DataSet || groups instanceof DataView) {
- this.groupsData = groups;
+ else if (this.scale == TimeStep.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 {
- throw new TypeError('Data must be an instance of DataSet or DataView');
+ else if (this.scale == TimeStep.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 == TimeStep.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 == TimeStep.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 == TimeStep.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 == TimeStep.SCALE.MILLISECOND) {
+ var step = this.step > 5 ? this.step / 2 : 1;
+ clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
+ }
+
+ return clone;
+ };
- if (this.groupsData) {
- // subscribe to new dataset
- var id = this.id;
- util.forEach(this.groupListeners, function (callback, event) {
- me.groupsData.on(event, callback, id);
- });
-
- // draw all ms
- ids = this.groupsData.getIds();
- this._onAddGroups(ids);
+ /**
+ * 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() {
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND:
+ return (this.current.getMilliseconds() == 0);
+ case TimeStep.SCALE.SECOND:
+ return (this.current.getSeconds() == 0);
+ case TimeStep.SCALE.MINUTE:
+ return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
+ // Note: this is no bug. Major label is equal for both minute and hour scale
+ case TimeStep.SCALE.HOUR:
+ return (this.current.getHours() == 0);
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY:
+ return (this.current.getDate() == 1);
+ case TimeStep.SCALE.MONTH:
+ return (this.current.getMonth() == 0);
+ case TimeStep.SCALE.YEAR:
+ return false;
+ default:
+ return false;
}
- this._onUpdate();
};
/**
- * Update the datapoints
- * @param [ids]
- * @private
+ * 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
*/
- LineGraph.prototype._onUpdate = function(ids) {
- this._updateUngrouped();
- this._updateAllGroupData();
- this._updateGraph();
- this.redraw();
- };
- LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);};
- LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);};
- LineGraph.prototype._onUpdateGroups = function (groupIds) {
- for (var i = 0; i < groupIds.length; i++) {
- var group = this.groupsData.get(groupIds[i]);
- this._updateGroup(group, groupIds[i]);
+ TimeStep.prototype.getLabelMinor = function(date) {
+ if (date == undefined) {
+ date = this.current;
}
- this._updateGraph();
- this.redraw();
- };
- LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
-
- LineGraph.prototype._onRemoveGroups = function (groupIds) {
- for (var i = 0; i < groupIds.length; i++) {
- if (!this.groups.hasOwnProperty(groupIds[i])) {
- if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') {
- this.yAxisRight.removeGroup(groupIds[i]);
- this.legendRight.removeGroup(groupIds[i]);
- this.legendRight.redraw();
- }
- else {
- this.yAxisLeft.removeGroup(groupIds[i]);
- this.legendLeft.removeGroup(groupIds[i]);
- this.legendLeft.redraw();
- }
- delete this.groups[groupIds[i]];
- }
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
+ case TimeStep.SCALE.SECOND: return moment(date).format('s');
+ case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
+ case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
+ case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
+ case TimeStep.SCALE.DAY: return moment(date).format('D');
+ case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
+ case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
+ default: return '';
}
- this._updateUngrouped();
- this._updateGraph();
- this.redraw();
};
+
/**
- * update a group object
- *
- * @param group
- * @param groupId
- * @private
- */
- LineGraph.prototype._updateGroup = function (group, groupId) {
- if (!this.groups.hasOwnProperty(groupId)) {
- this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
- if (this.groups[groupId].options.yAxisOrientation == 'right') {
- this.yAxisRight.addGroup(groupId, this.groups[groupId]);
- this.legendRight.addGroup(groupId, this.groups[groupId]);
- }
- else {
- this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
- this.legendLeft.addGroup(groupId, this.groups[groupId]);
- }
- }
- else {
- this.groups[groupId].update(group);
- if (this.groups[groupId].options.yAxisOrientation == 'right') {
- this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
- this.legendRight.updateGroup(groupId, this.groups[groupId]);
- }
- else {
- this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
- this.legendLeft.updateGroup(groupId, this.groups[groupId]);
- }
+ * 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;
}
- this.legendLeft.redraw();
- this.legendRight.redraw();
- };
- LineGraph.prototype._updateAllGroupData = function () {
- if (this.itemsData != null) {
- var groupsContent = {};
- var groupId;
- for (groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- groupsContent[groupId] = [];
- }
- }
- for (var itemId in this.itemsData._data) {
- if (this.itemsData._data.hasOwnProperty(itemId)) {
- var item = this.itemsData._data[itemId];
- item.x = util.convert(item.x,"Date");
- groupsContent[item.group].push(item);
- }
- }
- for (groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- this.groups[groupId].setItems(groupsContent[groupId]);
- }
- }
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
+ case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
+ case TimeStep.SCALE.MINUTE:
+ case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
+ case TimeStep.SCALE.WEEKDAY:
+ case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
+ case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
+ case TimeStep.SCALE.YEAR: return '';
+ default: return '';
}
};
- /**
- * Create or delete the group holding all ungrouped items. This group is used when
- * there are no groups specified. This anonymous group is called 'graph'.
- * @protected
- */
- LineGraph.prototype._updateUngrouped = function() {
- if (this.itemsData != null) {
- // var t0 = new Date();
- var group = {id: UNGROUPED, content: this.options.defaultGroup};
- this._updateGroup(group, UNGROUPED);
- var ungroupedCounter = 0;
- if (this.itemsData) {
- for (var itemId in this.itemsData._data) {
- if (this.itemsData._data.hasOwnProperty(itemId)) {
- var item = this.itemsData._data[itemId];
- if (item != undefined) {
- if (item.hasOwnProperty('group')) {
- if (item.group === undefined) {
- item.group = UNGROUPED;
- }
- }
- else {
- item.group = UNGROUPED;
- }
- ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter;
- }
- }
- }
- }
-
- if (ungroupedCounter == 0) {
- delete this.groups[UNGROUPED];
- this.legendLeft.removeGroup(UNGROUPED);
- this.legendRight.removeGroup(UNGROUPED);
- this.yAxisLeft.removeGroup(UNGROUPED);
- this.yAxisRight.removeGroup(UNGROUPED);
- }
- }
- else {
- delete this.groups[UNGROUPED];
- this.legendLeft.removeGroup(UNGROUPED);
- this.legendRight.removeGroup(UNGROUPED);
- this.yAxisLeft.removeGroup(UNGROUPED);
- this.yAxisRight.removeGroup(UNGROUPED);
- }
+ module.exports = TimeStep;
- this.legendLeft.redraw();
- this.legendRight.redraw();
- };
+/***/ },
+/* 22 */
+/***/ function(module, exports, __webpack_require__) {
- /**
- * Redraw the component, mandatory function
- * @return {boolean} Returns true if the component is resized
- */
- LineGraph.prototype.redraw = function() {
- var resized = false;
+ // first check if moment.js is already loaded in the browser window, if so,
+ // use this instance. Else, load via commonjs.
+ module.exports = (typeof window !== 'undefined') && window['moment'] || __webpack_require__(23);
- this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
- if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) {
- resized = true;
- }
- // check if this component is resized
- resized = this._isResized() || resized;
- // check whether zoomed (in that case we need to re-stack everything)
- var visibleInterval = this.body.range.end - this.body.range.start;
- var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
- this.lastVisibleInterval = visibleInterval;
- this.lastWidth = this.width;
- // calculate actual size and position
- this.width = this.dom.frame.offsetWidth;
+/***/ },
+/* 23 */
+/***/ function(module, exports, __webpack_require__) {
- // the svg element is three times as big as the width, this allows for fully dragging left and right
- // without reloading the graph. the controls for this are bound to events in the constructor
- if (resized == true) {
- this.svg.style.width = util.option.asSize(3*this.width);
- this.svg.style.left = util.option.asSize(-this.width);
- }
- if (zoomed == true || this.abortedGraphUpdate == true) {
- this._updateGraph();
- }
+ var __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(global, module) {//! moment.js
+ //! version : 2.7.0
+ //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+ //! license : MIT
+ //! momentjs.com
- this.legendLeft.redraw();
- this.legendRight.redraw();
+ (function (undefined) {
- return resized;
- };
+ /************************************
+ Constants
+ ************************************/
- /**
- * Update and redraw the graph.
- *
- */
- LineGraph.prototype._updateGraph = function () {
- // reset the svg elements
- DOMutil.prepareElements(this.svgElements);
- if (this.width != 0 && this.itemsData != null) {
- var group, i;
- var preprocessedGroupData = {};
- var processedGroupData = {};
- var groupRanges = {};
- var changeCalled = false;
+ var moment,
+ VERSION = "2.7.0",
+ // the global-scope this is NOT the global object in Node.js
+ globalScope = typeof global !== 'undefined' ? global : this,
+ oldGlobalMoment,
+ round = Math.round,
+ i,
- // getting group Ids
- var groupIds = [];
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- group = this.groups[groupId];
- if (group.visible == true && (this.options.groups.visibility[groupId] === undefined || this.options.groups.visibility[groupId] == true)) {
- groupIds.push(groupId);
- }
- }
- }
- if (groupIds.length > 0) {
- // this is the range of the SVG canvas
- var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width);
- var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
- var groupsData = {};
- // fill groups data
- this._getRelevantData(groupIds, groupsData, minDate, maxDate);
- // we transform the X coordinates to detect collisions
- for (i = 0; i < groupIds.length; i++) {
- preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]);
- }
- // now all needed data has been collected we start the processing.
- this._getYRanges(groupIds, preprocessedGroupData, groupRanges);
+ YEAR = 0,
+ MONTH = 1,
+ DATE = 2,
+ HOUR = 3,
+ MINUTE = 4,
+ SECOND = 5,
+ MILLISECOND = 6,
- // update the Y axis first, we use this data to draw at the correct Y points
- // changeCalled is required to clean the SVG on a change emit.
- changeCalled = this._updateYAxis(groupIds, groupRanges);
- if (changeCalled == true) {
- DOMutil.cleanupElements(this.svgElements);
- this.abortedGraphUpdate = true;
- this.body.emitter.emit("change");
- return;
- }
- this.abortedGraphUpdate = false;
+ // internal storage for language config files
+ languages = {},
- // With the yAxis scaled correctly, use this to get the Y values of the points.
- for (i = 0; i < groupIds.length; i++) {
- group = this.groups[groupIds[i]];
- processedGroupData[groupIds[i]] = this._convertYcoordinates(groupsData[groupIds[i]], group);
- }
+ // moment internal properties
+ momentProperties = {
+ _isAMomentObject: null,
+ _i : null,
+ _f : null,
+ _l : null,
+ _strict : null,
+ _tzm : null,
+ _isUTC : null,
+ _offset : null, // optional. Combine with _isUTC
+ _pf : null,
+ _lang : null // optional
+ },
+ // check for nodeJS
+ hasModule = (typeof module !== 'undefined' && module.exports),
- // draw the groups
- for (i = 0; i < groupIds.length; i++) {
- group = this.groups[groupIds[i]];
- if (group.options.style == 'line') {
- this._drawLineGraph(processedGroupData[groupIds[i]], group);
- }
- }
- this._drawBarGraphs(groupIds, processedGroupData);
- }
- }
-
- // cleanup unused svg elements
- DOMutil.cleanupElements(this.svgElements);
- };
+ // ASP.NET json date format regex
+ aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
+ aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
+ // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
+ // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
+ isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
- LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) {
- // first select and preprocess the data from the datasets.
- // the groups have their preselection of data, we now loop over this data to see
- // what data we need to draw. Sorted data is much faster.
- // more optimization is possible by doing the sampling before and using the binary search
- // to find the end date to determine the increment.
- var group, i, j, item;
- if (groupIds.length > 0) {
- for (i = 0; i < groupIds.length; i++) {
- group = this.groups[groupIds[i]];
- groupsData[groupIds[i]] = [];
- var dataContainer = groupsData[groupIds[i]];
- // optimization for sorted data
- if (group.options.sort == true) {
- var guess = Math.max(0, util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before'));
- for (j = guess; j < group.itemsData.length; j++) {
- item = group.itemsData[j];
- if (item !== undefined) {
- if (item.x > maxDate) {
- dataContainer.push(item);
- break;
- }
- else {
- dataContainer.push(item);
- }
- }
- }
- }
- else {
- for (j = 0; j < group.itemsData.length; j++) {
- item = group.itemsData[j];
- if (item !== undefined) {
- if (item.x > minDate && item.x < maxDate) {
- dataContainer.push(item);
- }
- }
- }
- }
- }
- }
+ // format tokens
+ formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
+ localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
- this._applySampling(groupIds, groupsData);
- };
+ // parsing token regexes
+ parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
+ parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
+ parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
+ parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
+ parseTokenDigits = /\d+/, // nonzero number of digits
+ parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic.
+ parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
+ parseTokenT = /T/i, // T (ISO separator)
+ parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
+ parseTokenOrdinal = /\d{1,2}/,
- LineGraph.prototype._applySampling = function (groupIds, groupsData) {
- var group;
- if (groupIds.length > 0) {
- for (var i = 0; i < groupIds.length; i++) {
- group = this.groups[groupIds[i]];
- if (group.options.sampling == true) {
- var dataContainer = groupsData[groupIds[i]];
- if (dataContainer.length > 0) {
- var increment = 1;
- var amountOfPoints = dataContainer.length;
+ //strict parsing regexes
+ parseTokenOneDigit = /\d/, // 0 - 9
+ parseTokenTwoDigits = /\d\d/, // 00 - 99
+ parseTokenThreeDigits = /\d{3}/, // 000 - 999
+ parseTokenFourDigits = /\d{4}/, // 0000 - 9999
+ parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
+ parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
- // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
- // of width changing of the yAxis.
- var xDistance = this.body.util.toGlobalScreen(dataContainer[dataContainer.length - 1].x) - this.body.util.toGlobalScreen(dataContainer[0].x);
- var pointsPerPixel = amountOfPoints / xDistance;
- increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1, Math.round(pointsPerPixel)));
+ // iso 8601 regex
+ // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00)
+ isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,
- var sampledData = [];
- for (var j = 0; j < amountOfPoints; j += increment) {
- sampledData.push(dataContainer[j]);
+ isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
- }
- groupsData[groupIds[i]] = sampledData;
- }
- }
- }
- }
- };
+ isoDates = [
+ ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
+ ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
+ ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
+ ['GGGG-[W]WW', /\d{4}-W\d{2}/],
+ ['YYYY-DDD', /\d{4}-\d{3}/]
+ ],
- LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) {
- var groupData, group, i,j;
- var barCombinedDataLeft = [];
- var barCombinedDataRight = [];
- var barCombinedData;
- if (groupIds.length > 0) {
- for (i = 0; i < groupIds.length; i++) {
- groupData = groupsData[groupIds[i]];
- if (groupData.length > 0) {
- group = this.groups[groupIds[i]];
- if (group.options.style == 'line' || group.options.barChart.handleOverlap != "stack") {
- var yMin = groupData[0].y;
- var yMax = groupData[0].y;
- for (j = 0; j < groupData.length; j++) {
- yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
- yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
- }
- groupRanges[groupIds[i]] = {min: yMin, max: yMax, yAxisOrientation: group.options.yAxisOrientation};
- }
- else if (group.options.style == 'bar') {
- if (group.options.yAxisOrientation == 'left') {
- barCombinedData = barCombinedDataLeft;
- }
- else {
- barCombinedData = barCombinedDataRight;
- }
+ // iso time formats and regexes
+ isoTimes = [
+ ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
+ ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
+ ['HH:mm', /(T| )\d\d:\d\d/],
+ ['HH', /(T| )\d\d/]
+ ],
- groupRanges[groupIds[i]] = {min: 0, max: 0, yAxisOrientation: group.options.yAxisOrientation, ignore: true};
+ // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
+ parseTimezoneChunker = /([\+\-]|\d\d)/gi,
- // combine data
- for (j = 0; j < groupData.length; j++) {
- barCombinedData.push({
- x: groupData[j].x,
- y: groupData[j].y,
- groupId: groupIds[i]
- });
- }
- }
- }
- }
+ // getter and setter names
+ proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
+ unitMillisecondFactors = {
+ 'Milliseconds' : 1,
+ 'Seconds' : 1e3,
+ 'Minutes' : 6e4,
+ 'Hours' : 36e5,
+ 'Days' : 864e5,
+ 'Months' : 2592e6,
+ 'Years' : 31536e6
+ },
- var intersections;
- if (barCombinedDataLeft.length > 0) {
- // sort by time and by group
- barCombinedDataLeft.sort(function (a, b) {
- if (a.x == b.x) {
- return a.groupId - b.groupId;
- } else {
- return a.x - b.x;
- }
- });
- intersections = {};
- this._getDataIntersections(intersections, barCombinedDataLeft);
- groupRanges["__barchartLeft"] = this._getStackedBarYRange(intersections, barCombinedDataLeft);
- groupRanges["__barchartLeft"].yAxisOrientation = "left";
- groupIds.push("__barchartLeft");
- }
- if (barCombinedDataRight.length > 0) {
- // sort by time and by group
- barCombinedDataRight.sort(function (a, b) {
- if (a.x == b.x) {
- return a.groupId - b.groupId;
- } else {
- return a.x - b.x;
- }
- });
- intersections = {};
- this._getDataIntersections(intersections, barCombinedDataRight);
- groupRanges["__barchartRight"] = this._getStackedBarYRange(intersections, barCombinedDataRight);
- groupRanges["__barchartRight"].yAxisOrientation = "right";
- groupIds.push("__barchartRight");
- }
- }
- };
+ unitAliases = {
+ ms : 'millisecond',
+ s : 'second',
+ m : 'minute',
+ h : 'hour',
+ d : 'day',
+ D : 'date',
+ w : 'week',
+ W : 'isoWeek',
+ M : 'month',
+ Q : 'quarter',
+ y : 'year',
+ DDD : 'dayOfYear',
+ e : 'weekday',
+ E : 'isoWeekday',
+ gg: 'weekYear',
+ GG: 'isoWeekYear'
+ },
- LineGraph.prototype._getStackedBarYRange = function (intersections, combinedData) {
- var key;
- var yMin = combinedData[0].y;
- var yMax = combinedData[0].y;
- for (var i = 0; i < combinedData.length; i++) {
- key = combinedData[i].x;
- if (intersections[key] === undefined) {
- yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin;
- yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax;
- }
- else {
- intersections[key].accumulated += combinedData[i].y;
- }
- }
- for (var xpos in intersections) {
- if (intersections.hasOwnProperty(xpos)) {
- yMin = yMin > intersections[xpos].accumulated ? intersections[xpos].accumulated : yMin;
- yMax = yMax < intersections[xpos].accumulated ? intersections[xpos].accumulated : yMax;
- }
- }
+ camelFunctions = {
+ dayofyear : 'dayOfYear',
+ isoweekday : 'isoWeekday',
+ isoweek : 'isoWeek',
+ weekyear : 'weekYear',
+ isoweekyear : 'isoWeekYear'
+ },
- return {min: yMin, max: yMax};
- };
+ // format function strings
+ formatFunctions = {},
+ // default relative time thresholds
+ relativeTimeThresholds = {
+ s: 45, //seconds to minutes
+ m: 45, //minutes to hours
+ h: 22, //hours to days
+ dd: 25, //days to month (month == 1)
+ dm: 45, //days to months (months > 1)
+ dy: 345 //days to year
+ },
- /**
- * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
- * @param {Array} groupIds
- * @param {Object} groupRanges
- * @private
- */
- LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
- var changeCalled = false;
- var yAxisLeftUsed = false;
- var yAxisRightUsed = false;
- var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
- // if groups are present
- if (groupIds.length > 0) {
- for (var i = 0; i < groupIds.length; i++) {
- if (groupRanges.hasOwnProperty(groupIds[i])) {
- if (groupRanges[groupIds[i]].ignore !== true) {
- minVal = groupRanges[groupIds[i]].min;
- maxVal = groupRanges[groupIds[i]].max;
+ // tokens to ordinalize and pad
+ ordinalizeTokens = 'DDD w W M D d'.split(' '),
+ paddedTokens = 'M D H h m s w W'.split(' '),
- if (groupRanges[groupIds[i]].yAxisOrientation == 'left') {
- yAxisLeftUsed = true;
- minLeft = minLeft > minVal ? minVal : minLeft;
- maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
- }
- else {
- yAxisRightUsed = true;
- minRight = minRight > minVal ? minVal : minRight;
- maxRight = maxRight < maxVal ? maxVal : maxRight;
- }
+ formatTokenFunctions = {
+ M : function () {
+ return this.month() + 1;
+ },
+ MMM : function (format) {
+ return this.lang().monthsShort(this, format);
+ },
+ MMMM : function (format) {
+ return this.lang().months(this, format);
+ },
+ D : function () {
+ return this.date();
+ },
+ DDD : function () {
+ return this.dayOfYear();
+ },
+ d : function () {
+ return this.day();
+ },
+ dd : function (format) {
+ return this.lang().weekdaysMin(this, format);
+ },
+ ddd : function (format) {
+ return this.lang().weekdaysShort(this, format);
+ },
+ dddd : function (format) {
+ return this.lang().weekdays(this, format);
+ },
+ w : function () {
+ return this.week();
+ },
+ W : function () {
+ return this.isoWeek();
+ },
+ YY : function () {
+ return leftZeroFill(this.year() % 100, 2);
+ },
+ YYYY : function () {
+ return leftZeroFill(this.year(), 4);
+ },
+ YYYYY : function () {
+ return leftZeroFill(this.year(), 5);
+ },
+ YYYYYY : function () {
+ var y = this.year(), sign = y >= 0 ? '+' : '-';
+ return sign + leftZeroFill(Math.abs(y), 6);
+ },
+ gg : function () {
+ return leftZeroFill(this.weekYear() % 100, 2);
+ },
+ gggg : function () {
+ return leftZeroFill(this.weekYear(), 4);
+ },
+ ggggg : function () {
+ return leftZeroFill(this.weekYear(), 5);
+ },
+ GG : function () {
+ return leftZeroFill(this.isoWeekYear() % 100, 2);
+ },
+ GGGG : function () {
+ return leftZeroFill(this.isoWeekYear(), 4);
+ },
+ GGGGG : function () {
+ return leftZeroFill(this.isoWeekYear(), 5);
+ },
+ e : function () {
+ return this.weekday();
+ },
+ E : function () {
+ return this.isoWeekday();
+ },
+ a : function () {
+ return this.lang().meridiem(this.hours(), this.minutes(), true);
+ },
+ A : function () {
+ return this.lang().meridiem(this.hours(), this.minutes(), false);
+ },
+ H : function () {
+ return this.hours();
+ },
+ h : function () {
+ return this.hours() % 12 || 12;
+ },
+ m : function () {
+ return this.minutes();
+ },
+ s : function () {
+ return this.seconds();
+ },
+ S : function () {
+ return toInt(this.milliseconds() / 100);
+ },
+ SS : function () {
+ return leftZeroFill(toInt(this.milliseconds() / 10), 2);
+ },
+ SSS : function () {
+ return leftZeroFill(this.milliseconds(), 3);
+ },
+ SSSS : function () {
+ return leftZeroFill(this.milliseconds(), 3);
+ },
+ Z : function () {
+ var a = -this.zone(),
+ b = "+";
+ if (a < 0) {
+ a = -a;
+ b = "-";
+ }
+ return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
+ },
+ ZZ : function () {
+ var a = -this.zone(),
+ b = "+";
+ if (a < 0) {
+ a = -a;
+ b = "-";
+ }
+ return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
+ },
+ z : function () {
+ return this.zoneAbbr();
+ },
+ zz : function () {
+ return this.zoneName();
+ },
+ X : function () {
+ return this.unix();
+ },
+ Q : function () {
+ return this.quarter();
+ }
+ },
+
+ lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
+
+ // Pick the first defined of two or three arguments. dfl comes from
+ // default.
+ function dfl(a, b, c) {
+ switch (arguments.length) {
+ case 2: return a != null ? a : b;
+ case 3: return a != null ? a : b != null ? b : c;
+ default: throw new Error("Implement me");
}
- }
}
- if (yAxisLeftUsed == true) {
- this.yAxisLeft.setRange(minLeft, maxLeft);
+ function defaultParsingFlags() {
+ // We need to deep clone this object, and es5 standard is not very
+ // helpful.
+ return {
+ empty : false,
+ unusedTokens : [],
+ unusedInput : [],
+ overflow : -2,
+ charsLeftOver : 0,
+ nullInput : false,
+ invalidMonth : null,
+ invalidFormat : false,
+ userInvalidated : false,
+ iso: false
+ };
}
- if (yAxisRightUsed == true) {
- this.yAxisRight.setRange(minRight, maxRight);
+
+ function deprecate(msg, fn) {
+ var firstTime = true;
+ function printMsg() {
+ if (moment.suppressDeprecationWarnings === false &&
+ typeof console !== 'undefined' && console.warn) {
+ console.warn("Deprecation warning: " + msg);
+ }
+ }
+ return extend(function () {
+ if (firstTime) {
+ printMsg();
+ firstTime = false;
+ }
+ return fn.apply(this, arguments);
+ }, fn);
}
- }
- changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled;
- changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled;
+ function padToken(func, count) {
+ return function (a) {
+ return leftZeroFill(func.call(this, a), count);
+ };
+ }
+ function ordinalizeToken(func, period) {
+ return function (a) {
+ return this.lang().ordinal(func.call(this, a), period);
+ };
+ }
- if (yAxisRightUsed == true && yAxisLeftUsed == true) {
- this.yAxisLeft.drawIcons = true;
- this.yAxisRight.drawIcons = true;
- }
- else {
- this.yAxisLeft.drawIcons = false;
- this.yAxisRight.drawIcons = false;
- }
+ while (ordinalizeTokens.length) {
+ i = ordinalizeTokens.pop();
+ formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
+ }
+ while (paddedTokens.length) {
+ i = paddedTokens.pop();
+ formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
+ }
+ formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
- this.yAxisRight.master = !yAxisLeftUsed;
- if (this.yAxisRight.master == false) {
- if (yAxisRightUsed == true) {this.yAxisLeft.lineOffset = this.yAxisRight.width;}
- else {this.yAxisLeft.lineOffset = 0;}
+ /************************************
+ Constructors
+ ************************************/
- changeCalled = this.yAxisLeft.redraw() || changeCalled;
- this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels;
- changeCalled = this.yAxisRight.redraw() || changeCalled;
- }
- else {
- changeCalled = this.yAxisRight.redraw() || changeCalled;
- }
+ function Language() {
- // clean the accumulated lists
- if (groupIds.indexOf("__barchartLeft") != -1) {
- groupIds.splice(groupIds.indexOf("__barchartLeft"),1);
- }
- if (groupIds.indexOf("__barchartRight") != -1) {
- groupIds.splice(groupIds.indexOf("__barchartRight"),1);
- }
-
- return changeCalled;
- };
+ }
- /**
- * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
- *
- * @param {boolean} axisUsed
- * @returns {boolean}
- * @private
- * @param axis
- */
- LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
- var changed = false;
- if (axisUsed == false) {
- if (axis.dom.frame.parentNode) {
- axis.hide();
- changed = true;
+ // Moment prototype object
+ function Moment(config) {
+ checkOverflow(config);
+ extend(this, config);
}
- }
- else {
- if (!axis.dom.frame.parentNode) {
- axis.show();
- changed = true;
+
+ // Duration Constructor
+ function Duration(duration) {
+ var normalizedInput = normalizeObjectUnits(duration),
+ years = normalizedInput.year || 0,
+ quarters = normalizedInput.quarter || 0,
+ months = normalizedInput.month || 0,
+ weeks = normalizedInput.week || 0,
+ days = normalizedInput.day || 0,
+ hours = normalizedInput.hour || 0,
+ minutes = normalizedInput.minute || 0,
+ seconds = normalizedInput.second || 0,
+ milliseconds = normalizedInput.millisecond || 0;
+
+ // representation for dateAddRemove
+ this._milliseconds = +milliseconds +
+ seconds * 1e3 + // 1000
+ minutes * 6e4 + // 1000 * 60
+ hours * 36e5; // 1000 * 60 * 60
+ // Because of dateAddRemove treats 24 hours as different from a
+ // day when working around DST, we need to store them separately
+ this._days = +days +
+ weeks * 7;
+ // It is impossible translate months into days without knowing
+ // which months you are are talking about, so we have to store
+ // it separately.
+ this._months = +months +
+ quarters * 3 +
+ years * 12;
+
+ this._data = {};
+
+ this._bubble();
}
- }
- return changed;
- };
+ /************************************
+ Helpers
+ ************************************/
- /**
- * draw a bar graph
- *
- * @param groupIds
- * @param processedGroupData
- */
- LineGraph.prototype._drawBarGraphs = function (groupIds, processedGroupData) {
- var combinedData = [];
- var intersections = {};
- var coreDistance;
- var key, drawData;
- var group;
- var i,j;
- var barPoints = 0;
- // combine all barchart data
- for (i = 0; i < groupIds.length; i++) {
- group = this.groups[groupIds[i]];
- if (group.options.style == 'bar') {
- if (group.visible == true && (this.options.groups.visibility[groupIds[i]] === undefined || this.options.groups.visibility[groupIds[i]] == true)) {
- for (j = 0; j < processedGroupData[groupIds[i]].length; j++) {
- combinedData.push({
- x: processedGroupData[groupIds[i]][j].x,
- y: processedGroupData[groupIds[i]][j].y,
- groupId: groupIds[i]
- });
- barPoints += 1;
+ function extend(a, b) {
+ for (var i in b) {
+ if (b.hasOwnProperty(i)) {
+ a[i] = b[i];
+ }
}
- }
+
+ if (b.hasOwnProperty("toString")) {
+ a.toString = b.toString;
+ }
+
+ if (b.hasOwnProperty("valueOf")) {
+ a.valueOf = b.valueOf;
+ }
+
+ return a;
}
- }
- if (barPoints == 0) {return;}
+ function cloneMoment(m) {
+ var result = {}, i;
+ for (i in m) {
+ if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) {
+ result[i] = m[i];
+ }
+ }
- // sort by time and by group
- combinedData.sort(function (a, b) {
- if (a.x == b.x) {
- return a.groupId - b.groupId;
- } else {
- return a.x - b.x;
+ return result;
}
- });
- // get intersections
- this._getDataIntersections(intersections, combinedData);
+ function absRound(number) {
+ if (number < 0) {
+ return Math.ceil(number);
+ } else {
+ return Math.floor(number);
+ }
+ }
- // plot barchart
- for (i = 0; i < combinedData.length; i++) {
- group = this.groups[combinedData[i].groupId];
- var minWidth = 0.1 * group.options.barChart.width;
+ // left zero fill a number
+ // see http://jsperf.com/left-zero-filling for performance comparison
+ function leftZeroFill(number, targetLength, forceSign) {
+ var output = '' + Math.abs(number),
+ sign = number >= 0;
- key = combinedData[i].x;
- var heightOffset = 0;
- if (intersections[key] === undefined) {
- if (i+1 < combinedData.length) {coreDistance = Math.abs(combinedData[i+1].x - key);}
- if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[i-1].x - key));}
- drawData = this._getSafeDrawData(coreDistance, group, minWidth);
+ while (output.length < targetLength) {
+ output = '0' + output;
+ }
+ return (sign ? (forceSign ? '+' : '') : '-') + output;
}
- else {
- var nextKey = i + (intersections[key].amount - intersections[key].resolved);
- var prevKey = i - (intersections[key].resolved + 1);
- if (nextKey < combinedData.length) {coreDistance = Math.abs(combinedData[nextKey].x - key);}
- if (prevKey > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[prevKey].x - key));}
- drawData = this._getSafeDrawData(coreDistance, group, minWidth);
- intersections[key].resolved += 1;
- if (group.options.barChart.handleOverlap == 'stack') {
- heightOffset = intersections[key].accumulated;
- intersections[key].accumulated += group.zeroPosition - combinedData[i].y;
- }
- else if (group.options.barChart.handleOverlap == 'sideBySide') {
- drawData.width = drawData.width / intersections[key].amount;
- drawData.offset += (intersections[key].resolved) * drawData.width - (0.5*drawData.width * (intersections[key].amount+1));
- if (group.options.barChart.align == 'left') {drawData.offset -= 0.5*drawData.width;}
- else if (group.options.barChart.align == 'right') {drawData.offset += 0.5*drawData.width;}
- }
+ // helper function for _.addTime and _.subtractTime
+ function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
+ var milliseconds = duration._milliseconds,
+ days = duration._days,
+ months = duration._months;
+ updateOffset = updateOffset == null ? true : updateOffset;
+
+ if (milliseconds) {
+ mom._d.setTime(+mom._d + milliseconds * isAdding);
+ }
+ if (days) {
+ rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
+ }
+ if (months) {
+ rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
+ }
+ if (updateOffset) {
+ moment.updateOffset(mom, days || months);
+ }
}
- DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', this.svgElements, this.svg);
- // draw points
- if (group.options.drawPoints.enabled == true) {
- DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, group, this.svgElements, this.svg);
+
+ // check if is an array
+ function isArray(input) {
+ return Object.prototype.toString.call(input) === '[object Array]';
}
- }
- };
- /**
- * Fill the intersections object with counters of how many datapoints share the same x coordinates
- * @param intersections
- * @param combinedData
- * @private
- */
- LineGraph.prototype._getDataIntersections = function (intersections, combinedData) {
- // get intersections
- var coreDistance;
- for (var i = 0; i < combinedData.length; i++) {
- if (i + 1 < combinedData.length) {
- coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x);
+ function isDate(input) {
+ return Object.prototype.toString.call(input) === '[object Date]' ||
+ input instanceof Date;
}
- if (i > 0) {
- coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x));
+
+ // compare two arrays, return the number of differences
+ function compareArrays(array1, array2, dontConvert) {
+ var len = Math.min(array1.length, array2.length),
+ lengthDiff = Math.abs(array1.length - array2.length),
+ diffs = 0,
+ i;
+ for (i = 0; i < len; i++) {
+ if ((dontConvert && array1[i] !== array2[i]) ||
+ (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
+ diffs++;
+ }
+ }
+ return diffs + lengthDiff;
}
- if (coreDistance == 0) {
- if (intersections[combinedData[i].x] === undefined) {
- intersections[combinedData[i].x] = {amount: 0, resolved: 0, accumulated: 0};
- }
- intersections[combinedData[i].x].amount += 1;
+
+ function normalizeUnits(units) {
+ if (units) {
+ var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
+ units = unitAliases[units] || camelFunctions[lowered] || lowered;
+ }
+ return units;
}
- }
- };
- /**
- * Get the width and offset for bargraphs based on the coredistance between datapoints
- *
- * @param coreDistance
- * @param group
- * @param minWidth
- * @returns {{width: Number, offset: Number}}
- * @private
- */
- LineGraph.prototype._getSafeDrawData = function (coreDistance, group, minWidth) {
- var width, offset;
- if (coreDistance < group.options.barChart.width && coreDistance > 0) {
- width = coreDistance < minWidth ? minWidth : coreDistance;
-
- offset = 0; // recalculate offset with the new width;
- if (group.options.barChart.align == 'left') {
- offset -= 0.5 * coreDistance;
- }
- else if (group.options.barChart.align == 'right') {
- offset += 0.5 * coreDistance;
- }
- }
- else {
- // default settings
- width = group.options.barChart.width;
- offset = 0;
- if (group.options.barChart.align == 'left') {
- offset -= 0.5 * group.options.barChart.width;
- }
- else if (group.options.barChart.align == 'right') {
- offset += 0.5 * group.options.barChart.width;
- }
- }
-
- return {width: width, offset: offset};
- };
+ function normalizeObjectUnits(inputObject) {
+ var normalizedInput = {},
+ normalizedProp,
+ prop;
+ for (prop in inputObject) {
+ if (inputObject.hasOwnProperty(prop)) {
+ normalizedProp = normalizeUnits(prop);
+ if (normalizedProp) {
+ normalizedInput[normalizedProp] = inputObject[prop];
+ }
+ }
+ }
- /**
- * draw a line graph
- *
- * @param dataset
- * @param group
- */
- LineGraph.prototype._drawLineGraph = function (dataset, group) {
- if (dataset != null) {
- if (dataset.length > 0) {
- var path, d;
- var svgHeight = Number(this.svg.style.height.replace("px",""));
- path = DOMutil.getSVGElement('path', this.svgElements, this.svg);
- path.setAttributeNS(null, "class", group.className);
+ return normalizedInput;
+ }
- // construct path from dataset
- if (group.options.catmullRom.enabled == true) {
- d = this._catmullRom(dataset, group);
- }
- else {
- d = this._linear(dataset);
- }
+ function makeList(field) {
+ var count, setter;
- // append with points for fill and finalize the path
- if (group.options.shaded.enabled == true) {
- var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg);
- var dFill;
- if (group.options.shaded.orientation == 'top') {
- dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0;
+ if (field.indexOf('week') === 0) {
+ count = 7;
+ setter = 'day';
+ }
+ else if (field.indexOf('month') === 0) {
+ count = 12;
+ setter = 'month';
}
else {
- dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight;
+ return;
}
- fillPath.setAttributeNS(null, "class", group.className + " fill");
- fillPath.setAttributeNS(null, "d", dFill);
- }
- // copy properties to path for drawing.
- path.setAttributeNS(null, "d", "M" + d);
- // draw points
- if (group.options.drawPoints.enabled == true) {
- this._drawPoints(dataset, group, this.svgElements, this.svg);
- }
- }
- }
- };
+ moment[field] = function (format, index) {
+ var i, getter,
+ method = moment.fn._lang[field],
+ results = [];
- /**
- * draw the data points
- *
- * @param {Array} dataset
- * @param {Object} JSONcontainer
- * @param {Object} svg | SVG DOM element
- * @param {GraphGroup} group
- * @param {Number} [offset]
- */
- LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) {
- if (offset === undefined) {offset = 0;}
- for (var i = 0; i < dataset.length; i++) {
- DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, JSONcontainer, svg);
- }
- };
+ if (typeof format === 'number') {
+ index = format;
+ format = undefined;
+ }
+ getter = function (i) {
+ var m = moment().utc().set(setter, i);
+ return method.call(moment.fn._lang, m, format || '');
+ };
+ if (index != null) {
+ return getter(index);
+ }
+ else {
+ for (i = 0; i < count; i++) {
+ results.push(getter(i));
+ }
+ return results;
+ }
+ };
+ }
- /**
- * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
- * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
- * the yAxis.
- *
- * @param datapoints
- * @returns {Array}
- * @private
- */
- LineGraph.prototype._convertXcoordinates = function (datapoints) {
- var extractedData = [];
- var xValue, yValue;
- var toScreen = this.body.util.toScreen;
+ function toInt(argumentForCoercion) {
+ var coercedNumber = +argumentForCoercion,
+ value = 0;
- for (var i = 0; i < datapoints.length; i++) {
- xValue = toScreen(datapoints[i].x) + this.width - 1;
- yValue = datapoints[i].y;
- extractedData.push({x: xValue, y: yValue});
- }
+ if (coercedNumber !== 0 && isFinite(coercedNumber)) {
+ if (coercedNumber >= 0) {
+ value = Math.floor(coercedNumber);
+ } else {
+ value = Math.ceil(coercedNumber);
+ }
+ }
- return extractedData;
- };
+ return value;
+ }
+ function daysInMonth(year, month) {
+ return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
+ }
+ function weeksInYear(year, dow, doy) {
+ return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
+ }
- /**
- * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
- * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
- * the yAxis.
- *
- * @param datapoints
- * @returns {Array}
- * @private
- */
- LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
- var extractedData = [];
- var xValue, yValue;
- var toScreen = this.body.util.toScreen;
- var axis = this.yAxisLeft;
- var svgHeight = Number(this.svg.style.height.replace("px",""));
- if (group.options.yAxisOrientation == 'right') {
- axis = this.yAxisRight;
- }
+ function daysInYear(year) {
+ return isLeapYear(year) ? 366 : 365;
+ }
- for (var i = 0; i < datapoints.length; i++) {
- xValue = toScreen(datapoints[i].x) + this.width - 1;
- yValue = Math.round(axis.convertValue(datapoints[i].y));
- extractedData.push({x: xValue, y: yValue});
- }
+ function isLeapYear(year) {
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+ }
- group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
+ function checkOverflow(m) {
+ var overflow;
+ if (m._a && m._pf.overflow === -2) {
+ overflow =
+ m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
+ m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
+ m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
+ m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
+ m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
+ m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
+ -1;
- return extractedData;
- };
+ if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
+ overflow = DATE;
+ }
- /**
- * This uses an uniform parametrization of the CatmullRom algorithm:
- * "On the Parameterization of Catmull-Rom Curves" by Cem Yuksel et al.
- * @param data
- * @returns {string}
- * @private
- */
- LineGraph.prototype._catmullRomUniform = function(data) {
- // catmull rom
- var p0, p1, p2, p3, bp1, bp2;
- var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
- var normalization = 1/6;
- var length = data.length;
- for (var i = 0; i < length - 1; i++) {
+ m._pf.overflow = overflow;
+ }
+ }
- p0 = (i == 0) ? data[0] : data[i-1];
- p1 = data[i];
- p2 = data[i+1];
- p3 = (i + 2 < length) ? data[i+2] : p2;
+ function isValid(m) {
+ if (m._isValid == null) {
+ m._isValid = !isNaN(m._d.getTime()) &&
+ m._pf.overflow < 0 &&
+ !m._pf.empty &&
+ !m._pf.invalidMonth &&
+ !m._pf.nullInput &&
+ !m._pf.invalidFormat &&
+ !m._pf.userInvalidated;
+ if (m._strict) {
+ m._isValid = m._isValid &&
+ m._pf.charsLeftOver === 0 &&
+ m._pf.unusedTokens.length === 0;
+ }
+ }
+ return m._isValid;
+ }
- // Catmull-Rom to Cubic Bezier conversion matrix
- // 0 1 0 0
- // -1/6 1 1/6 0
- // 0 1/6 1 -1/6
- // 0 0 1 0
-
- // bp0 = { x: p1.x, y: p1.y };
- bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
- bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
- // bp0 = { x: p2.x, y: p2.y };
-
- d += "C" +
- bp1.x + "," +
- bp1.y + " " +
- bp2.x + "," +
- bp2.y + " " +
- p2.x + "," +
- p2.y + " ";
- }
+ function normalizeLanguage(key) {
+ return key ? key.toLowerCase().replace('_', '-') : key;
+ }
- return d;
- };
+ // Return a moment from input, that is local/utc/zone equivalent to model.
+ function makeAs(input, model) {
+ return model._isUTC ? moment(input).zone(model._offset || 0) :
+ moment(input).local();
+ }
- /**
- * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
- * By default, the centripetal parameterization is used because this gives the nicest results.
- * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
- *
- * One optimization can be used to reuse distances since this is a sliding window approach.
- * @param data
- * @returns {string}
- * @private
- */
- LineGraph.prototype._catmullRom = function(data, group) {
- var alpha = group.options.catmullRom.alpha;
- if (alpha == 0 || alpha === undefined) {
- return this._catmullRomUniform(data);
- }
- else {
- var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
- var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
- var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
- var length = data.length;
- for (var i = 0; i < length - 1; i++) {
+ /************************************
+ Languages
+ ************************************/
- p0 = (i == 0) ? data[0] : data[i-1];
- p1 = data[i];
- p2 = data[i+1];
- p3 = (i + 2 < length) ? data[i+2] : p2;
- d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
- d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
- d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
+ extend(Language.prototype, {
- // Catmull-Rom to Cubic Bezier conversion matrix
- //
- // A = 2d1^2a + 3d1^a * d2^a + d3^2a
- // B = 2d3^2a + 3d3^a * d2^a + d2^2a
- //
- // [ 0 1 0 0 ]
- // [ -d2^2a/N A/N d1^2a/N 0 ]
- // [ 0 d3^2a/M B/M -d2^2a/M ]
- // [ 0 0 1 0 ]
+ set : function (config) {
+ var prop, i;
+ for (i in config) {
+ prop = config[i];
+ if (typeof prop === 'function') {
+ this[i] = prop;
+ } else {
+ this['_' + i] = prop;
+ }
+ }
+ },
- // [ 0 1 0 0 ]
- // [ -d2pow2a/N A/N d1pow2a/N 0 ]
- // [ 0 d3pow2a/M B/M -d2pow2a/M ]
- // [ 0 0 1 0 ]
+ _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
+ months : function (m) {
+ return this._months[m.month()];
+ },
- d3powA = Math.pow(d3, alpha);
- d3pow2A = Math.pow(d3,2*alpha);
- d2powA = Math.pow(d2, alpha);
- d2pow2A = Math.pow(d2,2*alpha);
- d1powA = Math.pow(d1, alpha);
- d1pow2A = Math.pow(d1,2*alpha);
+ _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
+ monthsShort : function (m) {
+ return this._monthsShort[m.month()];
+ },
- A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
- B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
- N = 3*d1powA * (d1powA + d2powA);
- if (N > 0) {N = 1 / N;}
- M = 3*d3powA * (d3powA + d2powA);
- if (M > 0) {M = 1 / M;}
+ monthsParse : function (monthName) {
+ var i, mom, regex;
- bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
- y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
+ if (!this._monthsParse) {
+ this._monthsParse = [];
+ }
- bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
- y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
+ for (i = 0; i < 12; i++) {
+ // make the regex if we don't have it already
+ if (!this._monthsParse[i]) {
+ mom = moment.utc([2000, i]);
+ regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
+ this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (this._monthsParse[i].test(monthName)) {
+ return i;
+ }
+ }
+ },
- if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
- if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
- d += "C" +
- bp1.x + "," +
- bp1.y + " " +
- bp2.x + "," +
- bp2.y + " " +
- p2.x + "," +
- p2.y + " ";
- }
+ _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
+ weekdays : function (m) {
+ return this._weekdays[m.day()];
+ },
- return d;
- }
- };
+ _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
+ weekdaysShort : function (m) {
+ return this._weekdaysShort[m.day()];
+ },
- /**
- * this generates the SVG path for a linear drawing between datapoints.
- * @param data
- * @returns {string}
- * @private
- */
- LineGraph.prototype._linear = function(data) {
- // linear
- var d = "";
- for (var i = 0; i < data.length; i++) {
- if (i == 0) {
- d += data[i].x + "," + data[i].y;
- }
- else {
- d += " " + data[i].x + "," + data[i].y;
- }
- }
- return d;
- };
+ _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
+ weekdaysMin : function (m) {
+ return this._weekdaysMin[m.day()];
+ },
- module.exports = LineGraph;
+ weekdaysParse : function (weekdayName) {
+ var i, mom, regex;
+ if (!this._weekdaysParse) {
+ this._weekdaysParse = [];
+ }
-/***/ },
-/* 27 */
-/***/ function(module, exports, __webpack_require__) {
+ for (i = 0; i < 7; i++) {
+ // make the regex if we don't have it already
+ if (!this._weekdaysParse[i]) {
+ mom = moment([2000, 1]).day(i);
+ regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
+ this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (this._weekdaysParse[i].test(weekdayName)) {
+ return i;
+ }
+ }
+ },
- var util = __webpack_require__(1);
- var Component = __webpack_require__(18);
- var TimeStep = __webpack_require__(17);
- var moment = __webpack_require__(40);
+ _longDateFormat : {
+ LT : "h:mm A",
+ L : "MM/DD/YYYY",
+ LL : "MMMM D YYYY",
+ LLL : "MMMM D YYYY LT",
+ LLLL : "dddd, MMMM D YYYY LT"
+ },
+ longDateFormat : function (key) {
+ var output = this._longDateFormat[key];
+ if (!output && this._longDateFormat[key.toUpperCase()]) {
+ output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
+ return val.slice(1);
+ });
+ this._longDateFormat[key] = output;
+ }
+ return output;
+ },
- /**
- * A horizontal time axis
- * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
- * @param {Object} [options] See TimeAxis.setOptions for the available
- * options.
- * @constructor TimeAxis
- * @extends Component
- */
- function TimeAxis (body, options) {
- this.dom = {
- foreground: null,
- majorLines: [],
- majorTexts: [],
- minorLines: [],
- minorTexts: [],
- redundant: {
- majorLines: [],
- majorTexts: [],
- minorLines: [],
- minorTexts: []
- }
- };
- this.props = {
- range: {
- start: 0,
- end: 0,
- minimumStep: 0
- },
- lineTop: 0
- };
+ isPM : function (input) {
+ // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
+ // Using charAt should be more compatible.
+ return ((input + '').toLowerCase().charAt(0) === 'p');
+ },
- this.defaultOptions = {
- orientation: 'bottom', // supported: 'top', 'bottom'
- // TODO: implement timeaxis orientations 'left' and 'right'
- showMinorLabels: true,
- showMajorLabels: true
- };
- this.options = util.extend({}, this.defaultOptions);
+ _meridiemParse : /[ap]\.?m?\.?/i,
+ meridiem : function (hours, minutes, isLower) {
+ if (hours > 11) {
+ return isLower ? 'pm' : 'PM';
+ } else {
+ return isLower ? 'am' : 'AM';
+ }
+ },
- this.body = body;
+ _calendar : {
+ sameDay : '[Today at] LT',
+ nextDay : '[Tomorrow at] LT',
+ nextWeek : 'dddd [at] LT',
+ lastDay : '[Yesterday at] LT',
+ lastWeek : '[Last] dddd [at] LT',
+ sameElse : 'L'
+ },
+ calendar : function (key, mom) {
+ var output = this._calendar[key];
+ return typeof output === 'function' ? output.apply(mom) : output;
+ },
- // create the HTML DOM
- this._create();
+ _relativeTime : {
+ future : "in %s",
+ past : "%s ago",
+ s : "a few seconds",
+ m : "a minute",
+ mm : "%d minutes",
+ h : "an hour",
+ hh : "%d hours",
+ d : "a day",
+ dd : "%d days",
+ M : "a month",
+ MM : "%d months",
+ y : "a year",
+ yy : "%d years"
+ },
+ relativeTime : function (number, withoutSuffix, string, isFuture) {
+ var output = this._relativeTime[string];
+ return (typeof output === 'function') ?
+ output(number, withoutSuffix, string, isFuture) :
+ output.replace(/%d/i, number);
+ },
+ pastFuture : function (diff, output) {
+ var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
+ return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
+ },
- this.setOptions(options);
- }
+ ordinal : function (number) {
+ return this._ordinal.replace("%d", number);
+ },
+ _ordinal : "%d",
- TimeAxis.prototype = new Component();
+ preparse : function (string) {
+ return string;
+ },
- /**
- * Set options for the TimeAxis.
- * Parameters will be merged in current options.
- * @param {Object} options Available options:
- * {string} [orientation]
- * {boolean} [showMinorLabels]
- * {boolean} [showMajorLabels]
- */
- TimeAxis.prototype.setOptions = function(options) {
- if (options) {
- // copy all options that we know
- util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels'], this.options, options);
+ postformat : function (string) {
+ return string;
+ },
- // apply locale to moment.js
- // TODO: not so nice, this is applied globally to moment.js
- if ('locale' in options) {
- if (typeof moment.locale === 'function') {
- // moment.js 2.8.1+
- moment.locale(options.locale);
- }
- else {
- moment.lang(options.locale);
- }
- }
- }
- };
+ week : function (mom) {
+ return weekOfYear(mom, this._week.dow, this._week.doy).week;
+ },
- /**
- * Create the HTML DOM for the TimeAxis
- */
- TimeAxis.prototype._create = function() {
- this.dom.foreground = document.createElement('div');
- this.dom.background = document.createElement('div');
+ _week : {
+ dow : 0, // Sunday is the first day of the week.
+ doy : 6 // The week that contains Jan 1st is the first week of the year.
+ },
- this.dom.foreground.className = 'timeaxis foreground';
- this.dom.background.className = 'timeaxis background';
- };
+ _invalidDate: 'Invalid date',
+ invalidDate: function () {
+ return this._invalidDate;
+ }
+ });
- /**
- * Destroy the TimeAxis
- */
- TimeAxis.prototype.destroy = function() {
- // remove from DOM
- if (this.dom.foreground.parentNode) {
- this.dom.foreground.parentNode.removeChild(this.dom.foreground);
- }
- if (this.dom.background.parentNode) {
- this.dom.background.parentNode.removeChild(this.dom.background);
- }
+ // Loads a language definition into the `languages` cache. The function
+ // takes a key and optionally values. If not in the browser and no values
+ // are provided, it will load the language file module. As a convenience,
+ // this function also returns the language values.
+ function loadLang(key, values) {
+ values.abbr = key;
+ if (!languages[key]) {
+ languages[key] = new Language();
+ }
+ languages[key].set(values);
+ return languages[key];
+ }
- this.body = null;
- };
+ // Remove a language from the `languages` cache. Mostly useful in tests.
+ function unloadLang(key) {
+ delete languages[key];
+ }
- /**
- * Repaint the component
- * @return {boolean} Returns true if the component is resized
- */
- TimeAxis.prototype.redraw = function () {
- var options = this.options,
- props = this.props,
- foreground = this.dom.foreground,
- background = this.dom.background;
+ // Determines which language definition to use and returns it.
+ //
+ // With no parameters, it will return the global language. If you
+ // pass in a language key, such as 'en', it will return the
+ // definition for 'en', so long as 'en' has already been loaded using
+ // moment.lang.
+ function getLangDefinition(key) {
+ var i = 0, j, lang, next, split,
+ get = function (k) {
+ if (!languages[k] && hasModule) {
+ try {
+ __webpack_require__(24)("./" + k);
+ } catch (e) { }
+ }
+ return languages[k];
+ };
- // determine the correct parent DOM element (depending on option orientation)
- var parent = (options.orientation == 'top') ? this.body.dom.top : this.body.dom.bottom;
- var parentChanged = (foreground.parentNode !== parent);
+ if (!key) {
+ return moment.fn._lang;
+ }
- // calculate character width and height
- this._calculateCharSize();
+ if (!isArray(key)) {
+ //short-circuit everything else
+ lang = get(key);
+ if (lang) {
+ return lang;
+ }
+ key = [key];
+ }
- // TODO: recalculate sizes only needed when parent is resized or options is changed
- var orientation = this.options.orientation,
- showMinorLabels = this.options.showMinorLabels,
- showMajorLabels = this.options.showMajorLabels;
+ //pick the language from the array
+ //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
+ //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
+ while (i < key.length) {
+ split = normalizeLanguage(key[i]).split('-');
+ j = split.length;
+ next = normalizeLanguage(key[i + 1]);
+ next = next ? next.split('-') : null;
+ while (j > 0) {
+ lang = get(split.slice(0, j).join('-'));
+ if (lang) {
+ return lang;
+ }
+ if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
+ //the next array item is better than a shallower substring of this one
+ break;
+ }
+ j--;
+ }
+ i++;
+ }
+ return moment.fn._lang;
+ }
- // determine the width and height of the elemens for the axis
- props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
- props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
- props.height = props.minorLabelHeight + props.majorLabelHeight;
- props.width = foreground.offsetWidth;
+ /************************************
+ Formatting
+ ************************************/
- props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight -
- (options.orientation == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height);
- props.minorLineWidth = 1; // TODO: really calculate width
- props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight;
- props.majorLineWidth = 1; // TODO: really calculate width
- // take foreground and background offline while updating (is almost twice as fast)
- var foregroundNextSibling = foreground.nextSibling;
- var backgroundNextSibling = background.nextSibling;
- foreground.parentNode && foreground.parentNode.removeChild(foreground);
- background.parentNode && background.parentNode.removeChild(background);
+ function removeFormattingTokens(input) {
+ if (input.match(/\[[\s\S]/)) {
+ return input.replace(/^\[|\]$/g, "");
+ }
+ return input.replace(/\\/g, "");
+ }
- foreground.style.height = this.props.height + 'px';
+ function makeFormatFunction(format) {
+ var array = format.match(formattingTokens), i, length;
- this._repaintLabels();
+ for (i = 0, length = array.length; i < length; i++) {
+ if (formatTokenFunctions[array[i]]) {
+ array[i] = formatTokenFunctions[array[i]];
+ } else {
+ array[i] = removeFormattingTokens(array[i]);
+ }
+ }
- // put DOM online again (at the same place)
- if (foregroundNextSibling) {
- parent.insertBefore(foreground, foregroundNextSibling);
- }
- else {
- parent.appendChild(foreground)
- }
- if (backgroundNextSibling) {
- this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling);
- }
- else {
- this.body.dom.backgroundVertical.appendChild(background)
- }
+ return function (mom) {
+ var output = "";
+ for (i = 0; i < length; i++) {
+ output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
+ }
+ return output;
+ };
+ }
- return this._isResized() || parentChanged;
- };
+ // format date using native date object
+ function formatMoment(m, format) {
- /**
- * Repaint major and minor text labels and vertical grid lines
- * @private
- */
- TimeAxis.prototype._repaintLabels = function () {
- var orientation = this.options.orientation;
+ if (!m.isValid()) {
+ return m.lang().invalidDate();
+ }
- // calculate range and step (step such that we have space for 7 characters per label)
- var start = util.convert(this.body.range.start, 'Number'),
- end = util.convert(this.body.range.end, 'Number'),
- minimumStep = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf()
- -this.body.util.toTime(0).valueOf();
- var step = new TimeStep(new Date(start), new Date(end), minimumStep);
- this.step = step;
+ format = expandFormat(format, m.lang());
- // Move all DOM elements to a "redundant" list, where they
- // can be picked for re-use, and clear the lists with lines and texts.
- // At the end of the function _repaintLabels, left over elements will be cleaned up
- var dom = this.dom;
- dom.redundant.majorLines = dom.majorLines;
- dom.redundant.majorTexts = dom.majorTexts;
- dom.redundant.minorLines = dom.minorLines;
- dom.redundant.minorTexts = dom.minorTexts;
- dom.majorLines = [];
- dom.majorTexts = [];
- dom.minorLines = [];
- dom.minorTexts = [];
+ if (!formatFunctions[format]) {
+ formatFunctions[format] = makeFormatFunction(format);
+ }
- step.first();
- var xFirstMajorLabel = undefined;
- var max = 0;
- while (step.hasNext() && max < 1000) {
- max++;
- var cur = step.getCurrent(),
- x = this.body.util.toScreen(cur),
- isMajor = step.isMajor();
+ return formatFunctions[format](m);
+ }
- // TODO: lines must have a width, such that we can create css backgrounds
+ function expandFormat(format, lang) {
+ var i = 5;
- if (this.options.showMinorLabels) {
- this._repaintMinorText(x, step.getLabelMinor(), orientation);
- }
+ function replaceLongDateFormatTokens(input) {
+ return lang.longDateFormat(input) || input;
+ }
- if (isMajor && this.options.showMajorLabels) {
- if (x > 0) {
- if (xFirstMajorLabel == undefined) {
- xFirstMajorLabel = x;
+ localFormattingTokens.lastIndex = 0;
+ while (i >= 0 && localFormattingTokens.test(format)) {
+ format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
+ localFormattingTokens.lastIndex = 0;
+ i -= 1;
}
- this._repaintMajorText(x, step.getLabelMajor(), orientation);
- }
- this._repaintMajorLine(x, orientation);
- }
- else {
- this._repaintMinorLine(x, orientation);
+
+ return format;
}
- step.next();
- }
- // create a major label on the left when needed
- if (this.options.showMajorLabels) {
- var leftTime = this.body.util.toTime(0),
- leftText = step.getLabelMajor(leftTime),
- widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
+ /************************************
+ Parsing
+ ************************************/
- if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
- this._repaintMajorText(0, leftText, orientation);
- }
- }
- // Cleanup leftover DOM elements from the redundant list
- util.forEach(this.dom.redundant, function (arr) {
- while (arr.length) {
- var elem = arr.pop();
- if (elem && elem.parentNode) {
- elem.parentNode.removeChild(elem);
- }
+ // get the regex to find the next token
+ function getParseRegexForToken(token, config) {
+ var a, strict = config._strict;
+ switch (token) {
+ case 'Q':
+ return parseTokenOneDigit;
+ case 'DDDD':
+ return parseTokenThreeDigits;
+ case 'YYYY':
+ case 'GGGG':
+ case 'gggg':
+ return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
+ case 'Y':
+ case 'G':
+ case 'g':
+ return parseTokenSignedNumber;
+ case 'YYYYYY':
+ case 'YYYYY':
+ case 'GGGGG':
+ case 'ggggg':
+ return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
+ case 'S':
+ if (strict) { return parseTokenOneDigit; }
+ /* falls through */
+ case 'SS':
+ if (strict) { return parseTokenTwoDigits; }
+ /* falls through */
+ case 'SSS':
+ if (strict) { return parseTokenThreeDigits; }
+ /* falls through */
+ case 'DDD':
+ return parseTokenOneToThreeDigits;
+ case 'MMM':
+ case 'MMMM':
+ case 'dd':
+ case 'ddd':
+ case 'dddd':
+ return parseTokenWord;
+ case 'a':
+ case 'A':
+ return getLangDefinition(config._l)._meridiemParse;
+ case 'X':
+ return parseTokenTimestampMs;
+ case 'Z':
+ case 'ZZ':
+ return parseTokenTimezone;
+ case 'T':
+ return parseTokenT;
+ case 'SSSS':
+ return parseTokenDigits;
+ case 'MM':
+ case 'DD':
+ case 'YY':
+ case 'GG':
+ case 'gg':
+ case 'HH':
+ case 'hh':
+ case 'mm':
+ case 'ss':
+ case 'ww':
+ case 'WW':
+ return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
+ case 'M':
+ case 'D':
+ case 'd':
+ case 'H':
+ case 'h':
+ case 'm':
+ case 's':
+ case 'w':
+ case 'W':
+ case 'e':
+ case 'E':
+ return parseTokenOneOrTwoDigits;
+ case 'Do':
+ return parseTokenOrdinal;
+ default :
+ a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
+ return a;
+ }
}
- });
- };
- /**
- * Create a minor label for the axis at position x
- * @param {Number} x
- * @param {String} text
- * @param {String} orientation "top" or "bottom" (default)
- * @private
- */
- TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
- // reuse redundant label
- var label = this.dom.redundant.minorTexts.shift();
+ function timezoneMinutesFromString(string) {
+ string = string || "";
+ var possibleTzMatches = (string.match(parseTokenTimezone) || []),
+ tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
+ parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
+ minutes = +(parts[1] * 60) + toInt(parts[2]);
- if (!label) {
- // create new label
- var content = document.createTextNode('');
- label = document.createElement('div');
- label.appendChild(content);
- label.className = 'text minor';
- this.dom.foreground.appendChild(label);
- }
- this.dom.minorTexts.push(label);
+ return parts[0] === '+' ? -minutes : minutes;
+ }
- label.childNodes[0].nodeValue = text;
+ // function to convert string input to date
+ function addTimeToArrayFromToken(token, input, config) {
+ var a, datePartArray = config._a;
- label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0';
- label.style.left = x + 'px';
- //label.title = title; // TODO: this is a heavy operation
- };
+ switch (token) {
+ // QUARTER
+ case 'Q':
+ if (input != null) {
+ datePartArray[MONTH] = (toInt(input) - 1) * 3;
+ }
+ break;
+ // MONTH
+ case 'M' : // fall through to MM
+ case 'MM' :
+ if (input != null) {
+ datePartArray[MONTH] = toInt(input) - 1;
+ }
+ break;
+ case 'MMM' : // fall through to MMMM
+ case 'MMMM' :
+ a = getLangDefinition(config._l).monthsParse(input);
+ // if we didn't find a month name, mark the date as invalid.
+ if (a != null) {
+ datePartArray[MONTH] = a;
+ } else {
+ config._pf.invalidMonth = input;
+ }
+ break;
+ // DAY OF MONTH
+ case 'D' : // fall through to DD
+ case 'DD' :
+ if (input != null) {
+ datePartArray[DATE] = toInt(input);
+ }
+ break;
+ case 'Do' :
+ if (input != null) {
+ datePartArray[DATE] = toInt(parseInt(input, 10));
+ }
+ break;
+ // DAY OF YEAR
+ case 'DDD' : // fall through to DDDD
+ case 'DDDD' :
+ if (input != null) {
+ config._dayOfYear = toInt(input);
+ }
- /**
- * Create a Major label for the axis at position x
- * @param {Number} x
- * @param {String} text
- * @param {String} orientation "top" or "bottom" (default)
- * @private
- */
- TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
- // reuse redundant label
- var label = this.dom.redundant.majorTexts.shift();
+ break;
+ // YEAR
+ case 'YY' :
+ datePartArray[YEAR] = moment.parseTwoDigitYear(input);
+ break;
+ case 'YYYY' :
+ case 'YYYYY' :
+ case 'YYYYYY' :
+ datePartArray[YEAR] = toInt(input);
+ break;
+ // AM / PM
+ case 'a' : // fall through to A
+ case 'A' :
+ config._isPm = getLangDefinition(config._l).isPM(input);
+ break;
+ // 24 HOUR
+ case 'H' : // fall through to hh
+ case 'HH' : // fall through to hh
+ case 'h' : // fall through to hh
+ case 'hh' :
+ datePartArray[HOUR] = toInt(input);
+ break;
+ // MINUTE
+ case 'm' : // fall through to mm
+ case 'mm' :
+ datePartArray[MINUTE] = toInt(input);
+ break;
+ // SECOND
+ case 's' : // fall through to ss
+ case 'ss' :
+ datePartArray[SECOND] = toInt(input);
+ break;
+ // MILLISECOND
+ case 'S' :
+ case 'SS' :
+ case 'SSS' :
+ case 'SSSS' :
+ datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
+ break;
+ // UNIX TIMESTAMP WITH MS
+ case 'X':
+ config._d = new Date(parseFloat(input) * 1000);
+ break;
+ // TIMEZONE
+ case 'Z' : // fall through to ZZ
+ case 'ZZ' :
+ config._useUTC = true;
+ config._tzm = timezoneMinutesFromString(input);
+ break;
+ // WEEKDAY - human
+ case 'dd':
+ case 'ddd':
+ case 'dddd':
+ a = getLangDefinition(config._l).weekdaysParse(input);
+ // if we didn't get a weekday name, mark the date as invalid
+ if (a != null) {
+ config._w = config._w || {};
+ config._w['d'] = a;
+ } else {
+ config._pf.invalidWeekday = input;
+ }
+ break;
+ // WEEK, WEEK DAY - numeric
+ case 'w':
+ case 'ww':
+ case 'W':
+ case 'WW':
+ case 'd':
+ case 'e':
+ case 'E':
+ token = token.substr(0, 1);
+ /* falls through */
+ case 'gggg':
+ case 'GGGG':
+ case 'GGGGG':
+ token = token.substr(0, 2);
+ if (input) {
+ config._w = config._w || {};
+ config._w[token] = toInt(input);
+ }
+ break;
+ case 'gg':
+ case 'GG':
+ config._w = config._w || {};
+ config._w[token] = moment.parseTwoDigitYear(input);
+ }
+ }
- if (!label) {
- // create label
- var content = document.createTextNode(text);
- label = document.createElement('div');
- label.className = 'text major';
- label.appendChild(content);
- this.dom.foreground.appendChild(label);
- }
- this.dom.majorTexts.push(label);
+ function dayOfYearFromWeekInfo(config) {
+ var w, weekYear, week, weekday, dow, doy, temp, lang;
- label.childNodes[0].nodeValue = text;
- //label.title = title; // TODO: this is a heavy operation
+ w = config._w;
+ if (w.GG != null || w.W != null || w.E != null) {
+ dow = 1;
+ doy = 4;
- label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px');
- label.style.left = x + 'px';
- };
+ // TODO: We need to take the current isoWeekYear, but that depends on
+ // how we interpret now (local, utc, fixed offset). So create
+ // a now version of current config (take local/utc/offset flags, and
+ // create now).
+ weekYear = dfl(w.GG, config._a[YEAR], weekOfYear(moment(), 1, 4).year);
+ week = dfl(w.W, 1);
+ weekday = dfl(w.E, 1);
+ } else {
+ lang = getLangDefinition(config._l);
+ dow = lang._week.dow;
+ doy = lang._week.doy;
- /**
- * Create a minor line for the axis at position x
- * @param {Number} x
- * @param {String} orientation "top" or "bottom" (default)
- * @private
- */
- TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
- // reuse redundant line
- var line = this.dom.redundant.minorLines.shift();
+ weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year);
+ week = dfl(w.w, 1);
- if (!line) {
- // create vertical line
- line = document.createElement('div');
- line.className = 'grid vertical minor';
- this.dom.background.appendChild(line);
- }
- this.dom.minorLines.push(line);
+ if (w.d != null) {
+ // weekday -- low day numbers are considered next week
+ weekday = w.d;
+ if (weekday < dow) {
+ ++week;
+ }
+ } else if (w.e != null) {
+ // local weekday -- counting starts from begining of week
+ weekday = w.e + dow;
+ } else {
+ // default to begining of week
+ weekday = dow;
+ }
+ }
+ temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow);
- var props = this.props;
- if (orientation == 'top') {
- line.style.top = props.majorLabelHeight + 'px';
- }
- else {
- line.style.top = this.body.domProps.top.height + 'px';
- }
- line.style.height = props.minorLineHeight + 'px';
- line.style.left = (x - props.minorLineWidth / 2) + 'px';
- };
+ config._a[YEAR] = temp.year;
+ config._dayOfYear = temp.dayOfYear;
+ }
- /**
- * Create a Major line for the axis at position x
- * @param {Number} x
- * @param {String} orientation "top" or "bottom" (default)
- * @private
- */
- TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
- // reuse redundant line
- var line = this.dom.redundant.majorLines.shift();
+ // convert an array to a date.
+ // the array should mirror the parameters below
+ // note: all values past the year are optional and will default to the lowest possible value.
+ // [year, month, day , hour, minute, second, millisecond]
+ function dateFromConfig(config) {
+ var i, date, input = [], currentDate, yearToUse;
- if (!line) {
- // create vertical line
- line = document.createElement('DIV');
- line.className = 'grid vertical major';
- this.dom.background.appendChild(line);
- }
- this.dom.majorLines.push(line);
+ if (config._d) {
+ return;
+ }
- var props = this.props;
- if (orientation == 'top') {
- line.style.top = '0';
- }
- else {
- line.style.top = this.body.domProps.top.height + 'px';
- }
- line.style.left = (x - props.majorLineWidth / 2) + 'px';
- line.style.height = props.majorLineHeight + 'px';
- };
+ currentDate = currentDateArray(config);
- /**
- * Determine the size of text on the axis (both major and minor axis).
- * The size is calculated only once and then cached in this.props.
- * @private
- */
- TimeAxis.prototype._calculateCharSize = function () {
- // Note: We calculate char size with every redraw. Size may change, for
- // example when any of the timelines parents had display:none for example.
+ //compute day of the year from weeks and weekdays
+ if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
+ dayOfYearFromWeekInfo(config);
+ }
- // determine the char width and height on the minor axis
- if (!this.dom.measureCharMinor) {
- this.dom.measureCharMinor = document.createElement('DIV');
- this.dom.measureCharMinor.className = 'text minor measure';
- this.dom.measureCharMinor.style.position = 'absolute';
+ //if the day of the year is set, figure out what it is
+ if (config._dayOfYear) {
+ yearToUse = dfl(config._a[YEAR], currentDate[YEAR]);
- this.dom.measureCharMinor.appendChild(document.createTextNode('0'));
- this.dom.foreground.appendChild(this.dom.measureCharMinor);
- }
- this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight;
- this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth;
+ if (config._dayOfYear > daysInYear(yearToUse)) {
+ config._pf._overflowDayOfYear = true;
+ }
- // determine the char width and height on the major axis
- if (!this.dom.measureCharMajor) {
- this.dom.measureCharMajor = document.createElement('DIV');
- this.dom.measureCharMajor.className = 'text minor measure';
- this.dom.measureCharMajor.style.position = 'absolute';
+ date = makeUTCDate(yearToUse, 0, config._dayOfYear);
+ config._a[MONTH] = date.getUTCMonth();
+ config._a[DATE] = date.getUTCDate();
+ }
- this.dom.measureCharMajor.appendChild(document.createTextNode('0'));
- this.dom.foreground.appendChild(this.dom.measureCharMajor);
- }
- this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight;
- this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth;
- };
+ // Default to current date.
+ // * if no year, month, day of month are given, default to today
+ // * if day of month is given, default month and year
+ // * if month is given, default only year
+ // * if year is given, don't default anything
+ for (i = 0; i < 3 && config._a[i] == null; ++i) {
+ config._a[i] = input[i] = currentDate[i];
+ }
- /**
- * 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
- */
- TimeAxis.prototype.snap = function(date) {
- return this.step.snap(date);
- };
-
- module.exports = TimeAxis;
-
-
-/***/ },
-/* 28 */
-/***/ function(module, exports, __webpack_require__) {
-
- var Hammer = __webpack_require__(41);
+ // Zero out whatever was not defaulted, including time
+ for (; i < 7; i++) {
+ config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
+ }
- /**
- * @constructor Item
- * @param {Object} data Object containing (optional) parameters type,
- * start, end, content, group, className.
- * @param {{toScreen: function, toTime: function}} conversion
- * Conversion functions from time to screen and vice versa
- * @param {Object} options Configuration options
- * // TODO: describe available options
- */
- function Item (data, conversion, options) {
- this.id = null;
- this.parent = null;
- this.data = data;
- this.dom = null;
- this.conversion = conversion || {};
- this.options = options || {};
+ config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
+ // Apply timezone offset from input. The actual zone can be changed
+ // with parseZone.
+ if (config._tzm != null) {
+ config._d.setUTCMinutes(config._d.getUTCMinutes() + config._tzm);
+ }
+ }
- this.selected = false;
- this.displayed = false;
- this.dirty = true;
+ function dateFromObject(config) {
+ var normalizedInput;
- this.top = null;
- this.left = null;
- this.width = null;
- this.height = null;
- }
+ if (config._d) {
+ return;
+ }
- /**
- * Select current item
- */
- Item.prototype.select = function() {
- this.selected = true;
- if (this.displayed) this.redraw();
- };
+ normalizedInput = normalizeObjectUnits(config._i);
+ config._a = [
+ normalizedInput.year,
+ normalizedInput.month,
+ normalizedInput.day,
+ normalizedInput.hour,
+ normalizedInput.minute,
+ normalizedInput.second,
+ normalizedInput.millisecond
+ ];
- /**
- * Unselect current item
- */
- Item.prototype.unselect = function() {
- this.selected = false;
- if (this.displayed) this.redraw();
- };
+ dateFromConfig(config);
+ }
- /**
- * Set a parent for the item
- * @param {ItemSet | Group} parent
- */
- Item.prototype.setParent = function(parent) {
- if (this.displayed) {
- this.hide();
- this.parent = parent;
- if (this.parent) {
- this.show();
+ function currentDateArray(config) {
+ var now = new Date();
+ if (config._useUTC) {
+ return [
+ now.getUTCFullYear(),
+ now.getUTCMonth(),
+ now.getUTCDate()
+ ];
+ } else {
+ return [now.getFullYear(), now.getMonth(), now.getDate()];
+ }
}
- }
- else {
- this.parent = parent;
- }
- };
- /**
- * Check whether this item is visible inside given range
- * @returns {{start: Number, end: Number}} range with a timestamp for start and end
- * @returns {boolean} True if visible
- */
- Item.prototype.isVisible = function(range) {
- // Should be implemented by Item implementations
- return false;
- };
+ // date from string and format string
+ function makeDateFromStringAndFormat(config) {
- /**
- * Show the Item in the DOM (when not already visible)
- * @return {Boolean} changed
- */
- Item.prototype.show = function() {
- return false;
- };
+ if (config._f === moment.ISO_8601) {
+ parseISO(config);
+ return;
+ }
- /**
- * Hide the Item from the DOM (when visible)
- * @return {Boolean} changed
- */
- Item.prototype.hide = function() {
- return false;
- };
+ config._a = [];
+ config._pf.empty = true;
- /**
- * Repaint the item
- */
- Item.prototype.redraw = function() {
- // should be implemented by the item
- };
+ // This array is used to make a Date, either with `new Date` or `Date.UTC`
+ var lang = getLangDefinition(config._l),
+ string = '' + config._i,
+ i, parsedInput, tokens, token, skipped,
+ stringLength = string.length,
+ totalParsedInputLength = 0;
- /**
- * Reposition the Item horizontally
- */
- Item.prototype.repositionX = function() {
- // should be implemented by the item
- };
+ tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
- /**
- * Reposition the Item vertically
- */
- Item.prototype.repositionY = function() {
- // should be implemented by the item
- };
+ for (i = 0; i < tokens.length; i++) {
+ token = tokens[i];
+ parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
+ if (parsedInput) {
+ skipped = string.substr(0, string.indexOf(parsedInput));
+ if (skipped.length > 0) {
+ config._pf.unusedInput.push(skipped);
+ }
+ string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
+ totalParsedInputLength += parsedInput.length;
+ }
+ // don't parse if it's not a known token
+ if (formatTokenFunctions[token]) {
+ if (parsedInput) {
+ config._pf.empty = false;
+ }
+ else {
+ config._pf.unusedTokens.push(token);
+ }
+ addTimeToArrayFromToken(token, parsedInput, config);
+ }
+ else if (config._strict && !parsedInput) {
+ config._pf.unusedTokens.push(token);
+ }
+ }
- /**
- * Repaint a delete button on the top right of the item when the item is selected
- * @param {HTMLElement} anchor
- * @protected
- */
- Item.prototype._repaintDeleteButton = function (anchor) {
- if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
- // create and show button
- var me = this;
+ // add remaining unparsed input length to the string
+ config._pf.charsLeftOver = stringLength - totalParsedInputLength;
+ if (string.length > 0) {
+ config._pf.unusedInput.push(string);
+ }
- var deleteButton = document.createElement('div');
- deleteButton.className = 'delete';
- deleteButton.title = 'Delete this item';
+ // handle am pm
+ if (config._isPm && config._a[HOUR] < 12) {
+ config._a[HOUR] += 12;
+ }
+ // if is 12 am, change hours to 0
+ if (config._isPm === false && config._a[HOUR] === 12) {
+ config._a[HOUR] = 0;
+ }
- Hammer(deleteButton, {
- preventDefault: true
- }).on('tap', function (event) {
- me.parent.removeFromDataSet(me);
- event.stopPropagation();
- });
+ dateFromConfig(config);
+ checkOverflow(config);
+ }
- anchor.appendChild(deleteButton);
- this.dom.deleteButton = deleteButton;
- }
- else if (!this.selected && this.dom.deleteButton) {
- // remove button
- if (this.dom.deleteButton.parentNode) {
- this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
+ function unescapeFormat(s) {
+ return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
+ return p1 || p2 || p3 || p4;
+ });
}
- this.dom.deleteButton = null;
- }
- };
- module.exports = Item;
+ // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+ function regexpEscape(s) {
+ return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+ }
+ // date from string and array of format strings
+ function makeDateFromStringAndArray(config) {
+ var tempConfig,
+ bestMoment,
-/***/ },
-/* 29 */
-/***/ function(module, exports, __webpack_require__) {
+ scoreToBeat,
+ i,
+ currentScore;
- var Item = __webpack_require__(28);
+ if (config._f.length === 0) {
+ config._pf.invalidFormat = true;
+ config._d = new Date(NaN);
+ return;
+ }
- /**
- * @constructor ItemBox
- * @extends Item
- * @param {Object} data Object containing parameters start
- * content, className.
- * @param {{toScreen: function, toTime: function}} conversion
- * Conversion functions from time to screen and vice versa
- * @param {Object} [options] Configuration options
- * // TODO: describe available options
- */
- function ItemBox (data, conversion, options) {
- this.props = {
- dot: {
- width: 0,
- height: 0
- },
- line: {
- width: 0,
- height: 0
- }
- };
+ for (i = 0; i < config._f.length; i++) {
+ currentScore = 0;
+ tempConfig = extend({}, config);
+ tempConfig._pf = defaultParsingFlags();
+ tempConfig._f = config._f[i];
+ makeDateFromStringAndFormat(tempConfig);
- // validate data
- if (data) {
- if (data.start == undefined) {
- throw new Error('Property "start" missing in item ' + data);
- }
- }
+ if (!isValid(tempConfig)) {
+ continue;
+ }
- Item.call(this, data, conversion, options);
- }
+ // if there is any input that was not parsed add a penalty for that format
+ currentScore += tempConfig._pf.charsLeftOver;
- ItemBox.prototype = new Item (null, null, null);
+ //or tokens
+ currentScore += tempConfig._pf.unusedTokens.length * 10;
- /**
- * Check whether this item is visible inside given range
- * @returns {{start: Number, end: Number}} range with a timestamp for start and end
- * @returns {boolean} True if visible
- */
- ItemBox.prototype.isVisible = function(range) {
- // determine visibility
- // TODO: account for the real width of the item. Right now we just add 1/4 to the window
- var interval = (range.end - range.start) / 4;
- return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
- };
+ tempConfig._pf.score = currentScore;
- /**
- * Repaint the item
- */
- ItemBox.prototype.redraw = function() {
- var dom = this.dom;
- if (!dom) {
- // create DOM
- this.dom = {};
- dom = this.dom;
+ if (scoreToBeat == null || currentScore < scoreToBeat) {
+ scoreToBeat = currentScore;
+ bestMoment = tempConfig;
+ }
+ }
- // create main box
- dom.box = document.createElement('DIV');
+ extend(config, bestMoment || tempConfig);
+ }
- // contents box (inside the background box). used for making margins
- dom.content = document.createElement('DIV');
- dom.content.className = 'content';
- dom.box.appendChild(dom.content);
+ // date from iso format
+ function parseISO(config) {
+ var i, l,
+ string = config._i,
+ match = isoRegex.exec(string);
- // line to axis
- dom.line = document.createElement('DIV');
- dom.line.className = 'line';
+ if (match) {
+ config._pf.iso = true;
+ for (i = 0, l = isoDates.length; i < l; i++) {
+ if (isoDates[i][1].exec(string)) {
+ // match[5] should be "T" or undefined
+ config._f = isoDates[i][0] + (match[6] || " ");
+ break;
+ }
+ }
+ for (i = 0, l = isoTimes.length; i < l; i++) {
+ if (isoTimes[i][1].exec(string)) {
+ config._f += isoTimes[i][0];
+ break;
+ }
+ }
+ if (string.match(parseTokenTimezone)) {
+ config._f += "Z";
+ }
+ makeDateFromStringAndFormat(config);
+ } else {
+ config._isValid = false;
+ }
+ }
- // dot on axis
- dom.dot = document.createElement('DIV');
- dom.dot.className = 'dot';
+ // date from iso format or fallback
+ function makeDateFromString(config) {
+ parseISO(config);
+ if (config._isValid === false) {
+ delete config._isValid;
+ moment.createFromInputFallback(config);
+ }
+ }
- // attach this item as attribute
- dom.box['timeline-item'] = this;
- }
+ function makeDateFromInput(config) {
+ var input = config._i,
+ matched = aspNetJsonRegex.exec(input);
- // append DOM to parent DOM
- if (!this.parent) {
- throw new Error('Cannot redraw item: no parent attached');
- }
- if (!dom.box.parentNode) {
- var foreground = this.parent.dom.foreground;
- if (!foreground) throw new Error('Cannot redraw time axis: parent has no foreground container element');
- foreground.appendChild(dom.box);
- }
- if (!dom.line.parentNode) {
- var background = this.parent.dom.background;
- if (!background) throw new Error('Cannot redraw time axis: parent has no background container element');
- background.appendChild(dom.line);
- }
- if (!dom.dot.parentNode) {
- var axis = this.parent.dom.axis;
- if (!background) throw new Error('Cannot redraw time axis: parent has no axis container element');
- axis.appendChild(dom.dot);
- }
- this.displayed = true;
+ if (input === undefined) {
+ config._d = new Date();
+ } else if (matched) {
+ config._d = new Date(+matched[1]);
+ } else if (typeof input === 'string') {
+ makeDateFromString(config);
+ } else if (isArray(input)) {
+ config._a = input.slice(0);
+ dateFromConfig(config);
+ } else if (isDate(input)) {
+ config._d = new Date(+input);
+ } else if (typeof(input) === 'object') {
+ dateFromObject(config);
+ } else if (typeof(input) === 'number') {
+ // from milliseconds
+ config._d = new Date(input);
+ } else {
+ moment.createFromInputFallback(config);
+ }
+ }
- // update contents
- if (this.data.content != this.content) {
- this.content = this.data.content;
- if (this.content instanceof Element) {
- dom.content.innerHTML = '';
- dom.content.appendChild(this.content);
+ function makeDate(y, m, d, h, M, s, ms) {
+ //can't just apply() to create a date:
+ //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
+ var date = new Date(y, m, d, h, M, s, ms);
+
+ //the date constructor doesn't accept years < 1970
+ if (y < 1970) {
+ date.setFullYear(y);
+ }
+ return date;
}
- else if (this.data.content != undefined) {
- dom.content.innerHTML = this.content;
+
+ function makeUTCDate(y) {
+ var date = new Date(Date.UTC.apply(null, arguments));
+ if (y < 1970) {
+ date.setUTCFullYear(y);
+ }
+ return date;
}
- else {
- throw new Error('Property "content" missing in item ' + this.data.id);
+
+ function parseWeekday(input, language) {
+ if (typeof input === 'string') {
+ if (!isNaN(input)) {
+ input = parseInt(input, 10);
+ }
+ else {
+ input = language.weekdaysParse(input);
+ if (typeof input !== 'number') {
+ return null;
+ }
+ }
+ }
+ return input;
}
- this.dirty = true;
- }
+ /************************************
+ Relative Time
+ ************************************/
- // update title
- if (this.data.title != this.title) {
- dom.box.title = this.data.title;
- this.title = this.data.title;
- }
- // update class
- var className = (this.data.className? ' ' + this.data.className : '') +
- (this.selected ? ' selected' : '');
- if (this.className != className) {
- this.className = className;
- dom.box.className = 'item box' + className;
- dom.line.className = 'item line' + className;
- dom.dot.className = 'item dot' + className;
+ // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
+ function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
+ return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
+ }
- this.dirty = true;
- }
+ function relativeTime(milliseconds, withoutSuffix, lang) {
+ var seconds = round(Math.abs(milliseconds) / 1000),
+ minutes = round(seconds / 60),
+ hours = round(minutes / 60),
+ days = round(hours / 24),
+ years = round(days / 365),
+ args = seconds < relativeTimeThresholds.s && ['s', seconds] ||
+ minutes === 1 && ['m'] ||
+ minutes < relativeTimeThresholds.m && ['mm', minutes] ||
+ hours === 1 && ['h'] ||
+ hours < relativeTimeThresholds.h && ['hh', hours] ||
+ days === 1 && ['d'] ||
+ days <= relativeTimeThresholds.dd && ['dd', days] ||
+ days <= relativeTimeThresholds.dm && ['M'] ||
+ days < relativeTimeThresholds.dy && ['MM', round(days / 30)] ||
+ years === 1 && ['y'] || ['yy', years];
+ args[2] = withoutSuffix;
+ args[3] = milliseconds > 0;
+ args[4] = lang;
+ return substituteTimeAgo.apply({}, args);
+ }
- // recalculate size
- if (this.dirty) {
- this.props.dot.height = dom.dot.offsetHeight;
- this.props.dot.width = dom.dot.offsetWidth;
- this.props.line.width = dom.line.offsetWidth;
- this.width = dom.box.offsetWidth;
- this.height = dom.box.offsetHeight;
- this.dirty = false;
- }
+ /************************************
+ Week of Year
+ ************************************/
- this._repaintDeleteButton(dom.box);
- };
- /**
- * Show the item in the DOM (when not already displayed). The items DOM will
- * be created when needed.
- */
- ItemBox.prototype.show = function() {
- if (!this.displayed) {
- this.redraw();
- }
- };
+ // firstDayOfWeek 0 = sun, 6 = sat
+ // the day of the week that starts the week
+ // (usually sunday or monday)
+ // firstDayOfWeekOfYear 0 = sun, 6 = sat
+ // the first week is the week that contains the first
+ // of this day of the week
+ // (eg. ISO weeks use thursday (4))
+ function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
+ var end = firstDayOfWeekOfYear - firstDayOfWeek,
+ daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
+ adjustedMoment;
- /**
- * Hide the item from the DOM (when visible)
- */
- ItemBox.prototype.hide = function() {
- if (this.displayed) {
- var dom = this.dom;
- if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
- if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
- if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
+ if (daysToDayOfWeek > end) {
+ daysToDayOfWeek -= 7;
+ }
- this.top = null;
- this.left = null;
+ if (daysToDayOfWeek < end - 7) {
+ daysToDayOfWeek += 7;
+ }
- this.displayed = false;
- }
- };
+ adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
+ return {
+ week: Math.ceil(adjustedMoment.dayOfYear() / 7),
+ year: adjustedMoment.year()
+ };
+ }
- /**
- * Reposition the item horizontally
- * @Override
- */
- ItemBox.prototype.repositionX = function() {
- var start = this.conversion.toScreen(this.data.start),
- align = this.options.align,
- left,
- box = this.dom.box,
- line = this.dom.line,
- dot = this.dom.dot;
+ //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
+ function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
+ var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
- // calculate left position of the box
- if (align == 'right') {
- this.left = start - this.width;
- }
- else if (align == 'left') {
- this.left = start;
- }
- else {
- // default or 'center'
- this.left = start - this.width / 2;
- }
+ d = d === 0 ? 7 : d;
+ weekday = weekday != null ? weekday : firstDayOfWeek;
+ daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
+ dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
- // reposition box
- box.style.left = this.left + 'px';
+ return {
+ year: dayOfYear > 0 ? year : year - 1,
+ dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
+ };
+ }
- // reposition line
- line.style.left = (start - this.props.line.width / 2) + 'px';
+ /************************************
+ Top Level Functions
+ ************************************/
- // reposition dot
- dot.style.left = (start - this.props.dot.width / 2) + 'px';
- };
+ function makeMoment(config) {
+ var input = config._i,
+ format = config._f;
- /**
- * Reposition the item vertically
- * @Override
- */
- ItemBox.prototype.repositionY = function() {
- var orientation = this.options.orientation,
- box = this.dom.box,
- line = this.dom.line,
- dot = this.dom.dot;
+ if (input === null || (format === undefined && input === '')) {
+ return moment.invalid({nullInput: true});
+ }
- if (orientation == 'top') {
- box.style.top = (this.top || 0) + 'px';
+ if (typeof input === 'string') {
+ config._i = input = getLangDefinition().preparse(input);
+ }
- line.style.top = '0';
- line.style.height = (this.parent.top + this.top + 1) + 'px';
- line.style.bottom = '';
- }
- else { // orientation 'bottom'
- var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty
- var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top;
+ if (moment.isMoment(input)) {
+ config = cloneMoment(input);
- box.style.top = (this.parent.height - this.top - this.height || 0) + 'px';
- line.style.top = (itemSetHeight - lineHeight) + 'px';
- line.style.bottom = '0';
- }
+ config._d = new Date(+input._d);
+ } else if (format) {
+ if (isArray(format)) {
+ makeDateFromStringAndArray(config);
+ } else {
+ makeDateFromStringAndFormat(config);
+ }
+ } else {
+ makeDateFromInput(config);
+ }
- dot.style.top = (-this.props.dot.height / 2) + 'px';
- };
+ return new Moment(config);
+ }
- module.exports = ItemBox;
+ moment = function (input, format, lang, strict) {
+ var c;
+ if (typeof(lang) === "boolean") {
+ strict = lang;
+ lang = undefined;
+ }
+ // object construction must be done this way.
+ // https://github.com/moment/moment/issues/1423
+ c = {};
+ c._isAMomentObject = true;
+ c._i = input;
+ c._f = format;
+ c._l = lang;
+ c._strict = strict;
+ c._isUTC = false;
+ c._pf = defaultParsingFlags();
-/***/ },
-/* 30 */
-/***/ function(module, exports, __webpack_require__) {
+ return makeMoment(c);
+ };
- var Item = __webpack_require__(28);
+ moment.suppressDeprecationWarnings = false;
- /**
- * @constructor ItemPoint
- * @extends Item
- * @param {Object} data Object containing parameters start
- * content, className.
- * @param {{toScreen: function, toTime: function}} conversion
- * Conversion functions from time to screen and vice versa
- * @param {Object} [options] Configuration options
- * // TODO: describe available options
- */
- function ItemPoint (data, conversion, options) {
- this.props = {
- dot: {
- top: 0,
- width: 0,
- height: 0
- },
- content: {
- height: 0,
- marginLeft: 0
- }
- };
+ moment.createFromInputFallback = deprecate(
+ "moment construction falls back to js Date. This is " +
+ "discouraged and will be removed in upcoming major " +
+ "release. Please refer to " +
+ "https://github.com/moment/moment/issues/1407 for more info.",
+ function (config) {
+ config._d = new Date(config._i);
+ });
- // validate data
- if (data) {
- if (data.start == undefined) {
- throw new Error('Property "start" missing in item ' + data);
+ // Pick a moment m from moments so that m[fn](other) is true for all
+ // other. This relies on the function fn to be transitive.
+ //
+ // moments should either be an array of moment objects or an array, whose
+ // first element is an array of moment objects.
+ function pickBy(fn, moments) {
+ var res, i;
+ if (moments.length === 1 && isArray(moments[0])) {
+ moments = moments[0];
+ }
+ if (!moments.length) {
+ return moment();
+ }
+ res = moments[0];
+ for (i = 1; i < moments.length; ++i) {
+ if (moments[i][fn](res)) {
+ res = moments[i];
+ }
+ }
+ return res;
}
- }
- Item.call(this, data, conversion, options);
- }
+ moment.min = function () {
+ var args = [].slice.call(arguments, 0);
- ItemPoint.prototype = new Item (null, null, null);
+ return pickBy('isBefore', args);
+ };
- /**
- * Check whether this item is visible inside given range
- * @returns {{start: Number, end: Number}} range with a timestamp for start and end
- * @returns {boolean} True if visible
- */
- ItemPoint.prototype.isVisible = function(range) {
- // determine visibility
- // TODO: account for the real width of the item. Right now we just add 1/4 to the window
- var interval = (range.end - range.start) / 4;
- return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
- };
+ moment.max = function () {
+ var args = [].slice.call(arguments, 0);
- /**
- * Repaint the item
- */
- ItemPoint.prototype.redraw = function() {
- var dom = this.dom;
- if (!dom) {
- // create DOM
- this.dom = {};
- dom = this.dom;
+ return pickBy('isAfter', args);
+ };
- // background box
- dom.point = document.createElement('div');
- // className is updated in redraw()
+ // creating with utc
+ moment.utc = function (input, format, lang, strict) {
+ var c;
- // contents box, right from the dot
- dom.content = document.createElement('div');
- dom.content.className = 'content';
- dom.point.appendChild(dom.content);
+ if (typeof(lang) === "boolean") {
+ strict = lang;
+ lang = undefined;
+ }
+ // object construction must be done this way.
+ // https://github.com/moment/moment/issues/1423
+ c = {};
+ c._isAMomentObject = true;
+ c._useUTC = true;
+ c._isUTC = true;
+ c._l = lang;
+ c._i = input;
+ c._f = format;
+ c._strict = strict;
+ c._pf = defaultParsingFlags();
- // dot at start
- dom.dot = document.createElement('div');
- dom.point.appendChild(dom.dot);
+ return makeMoment(c).utc();
+ };
- // attach this item as attribute
- dom.point['timeline-item'] = this;
- }
+ // creating with unix timestamp (in seconds)
+ moment.unix = function (input) {
+ return moment(input * 1000);
+ };
- // append DOM to parent DOM
- if (!this.parent) {
- throw new Error('Cannot redraw item: no parent attached');
- }
- if (!dom.point.parentNode) {
- var foreground = this.parent.dom.foreground;
- if (!foreground) {
- throw new Error('Cannot redraw time axis: parent has no foreground container element');
- }
- foreground.appendChild(dom.point);
- }
- this.displayed = true;
+ // duration
+ moment.duration = function (input, key) {
+ var duration = input,
+ // matching against regexp is expensive, do it on demand
+ match = null,
+ sign,
+ ret,
+ parseIso;
- // update contents
- if (this.data.content != this.content) {
- this.content = this.data.content;
- if (this.content instanceof Element) {
- dom.content.innerHTML = '';
- dom.content.appendChild(this.content);
- }
- else if (this.data.content != undefined) {
- dom.content.innerHTML = this.content;
- }
- else {
- throw new Error('Property "content" missing in item ' + this.data.id);
- }
+ if (moment.isDuration(input)) {
+ duration = {
+ ms: input._milliseconds,
+ d: input._days,
+ M: input._months
+ };
+ } else if (typeof input === 'number') {
+ duration = {};
+ if (key) {
+ duration[key] = input;
+ } else {
+ duration.milliseconds = input;
+ }
+ } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
+ sign = (match[1] === "-") ? -1 : 1;
+ duration = {
+ y: 0,
+ d: toInt(match[DATE]) * sign,
+ h: toInt(match[HOUR]) * sign,
+ m: toInt(match[MINUTE]) * sign,
+ s: toInt(match[SECOND]) * sign,
+ ms: toInt(match[MILLISECOND]) * sign
+ };
+ } else if (!!(match = isoDurationRegex.exec(input))) {
+ sign = (match[1] === "-") ? -1 : 1;
+ parseIso = function (inp) {
+ // We'd normally use ~~inp for this, but unfortunately it also
+ // converts floats to ints.
+ // inp may be undefined, so careful calling replace on it.
+ var res = inp && parseFloat(inp.replace(',', '.'));
+ // apply sign while we're at it
+ return (isNaN(res) ? 0 : res) * sign;
+ };
+ duration = {
+ y: parseIso(match[2]),
+ M: parseIso(match[3]),
+ d: parseIso(match[4]),
+ h: parseIso(match[5]),
+ m: parseIso(match[6]),
+ s: parseIso(match[7]),
+ w: parseIso(match[8])
+ };
+ }
- this.dirty = true;
- }
+ ret = new Duration(duration);
- // update title
- if (this.data.title != this.title) {
- dom.point.title = this.data.title;
- this.title = this.data.title;
- }
+ if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
+ ret._lang = input._lang;
+ }
- // update class
- var className = (this.data.className? ' ' + this.data.className : '') +
- (this.selected ? ' selected' : '');
- if (this.className != className) {
- this.className = className;
- dom.point.className = 'item point' + className;
- dom.dot.className = 'item dot' + className;
+ return ret;
+ };
- this.dirty = true;
- }
+ // version number
+ moment.version = VERSION;
- // recalculate size
- if (this.dirty) {
- this.width = dom.point.offsetWidth;
- this.height = dom.point.offsetHeight;
- this.props.dot.width = dom.dot.offsetWidth;
- this.props.dot.height = dom.dot.offsetHeight;
- this.props.content.height = dom.content.offsetHeight;
+ // default format
+ moment.defaultFormat = isoFormat;
- // resize contents
- dom.content.style.marginLeft = 2 * this.props.dot.width + 'px';
- //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
+ // constant that refers to the ISO standard
+ moment.ISO_8601 = function () {};
- dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
- dom.dot.style.left = (this.props.dot.width / 2) + 'px';
+ // Plugins that add properties should also add the key here (null value),
+ // so we can properly clone ourselves.
+ moment.momentProperties = momentProperties;
- this.dirty = false;
- }
+ // This function will be called whenever a moment is mutated.
+ // It is intended to keep the offset in sync with the timezone.
+ moment.updateOffset = function () {};
- this._repaintDeleteButton(dom.point);
- };
+ // This function allows you to set a threshold for relative time strings
+ moment.relativeTimeThreshold = function(threshold, limit) {
+ if (relativeTimeThresholds[threshold] === undefined) {
+ return false;
+ }
+ relativeTimeThresholds[threshold] = limit;
+ return true;
+ };
- /**
- * Show the item in the DOM (when not already visible). The items DOM will
- * be created when needed.
- */
- ItemPoint.prototype.show = function() {
- if (!this.displayed) {
- this.redraw();
- }
- };
+ // This function will load languages and then set the global language. If
+ // no arguments are passed in, it will simply return the current global
+ // language key.
+ moment.lang = function (key, values) {
+ var r;
+ if (!key) {
+ return moment.fn._lang._abbr;
+ }
+ if (values) {
+ loadLang(normalizeLanguage(key), values);
+ } else if (values === null) {
+ unloadLang(key);
+ key = 'en';
+ } else if (!languages[key]) {
+ getLangDefinition(key);
+ }
+ r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
+ return r._abbr;
+ };
- /**
- * Hide the item from the DOM (when visible)
- */
- ItemPoint.prototype.hide = function() {
- if (this.displayed) {
- if (this.dom.point.parentNode) {
- this.dom.point.parentNode.removeChild(this.dom.point);
- }
+ // returns language data
+ moment.langData = function (key) {
+ if (key && key._lang && key._lang._abbr) {
+ key = key._lang._abbr;
+ }
+ return getLangDefinition(key);
+ };
- this.top = null;
- this.left = null;
+ // compare moment object
+ moment.isMoment = function (obj) {
+ return obj instanceof Moment ||
+ (obj != null && obj.hasOwnProperty('_isAMomentObject'));
+ };
- this.displayed = false;
- }
- };
+ // for typechecking Duration objects
+ moment.isDuration = function (obj) {
+ return obj instanceof Duration;
+ };
- /**
- * Reposition the item horizontally
- * @Override
- */
- ItemPoint.prototype.repositionX = function() {
- var start = this.conversion.toScreen(this.data.start);
+ for (i = lists.length - 1; i >= 0; --i) {
+ makeList(lists[i]);
+ }
- this.left = start - this.props.dot.width;
+ moment.normalizeUnits = function (units) {
+ return normalizeUnits(units);
+ };
- // reposition point
- this.dom.point.style.left = this.left + 'px';
- };
+ moment.invalid = function (flags) {
+ var m = moment.utc(NaN);
+ if (flags != null) {
+ extend(m._pf, flags);
+ }
+ else {
+ m._pf.userInvalidated = true;
+ }
- /**
- * Reposition the item vertically
- * @Override
- */
- ItemPoint.prototype.repositionY = function() {
- var orientation = this.options.orientation,
- point = this.dom.point;
-
- if (orientation == 'top') {
- point.style.top = this.top + 'px';
- }
- else {
- point.style.top = (this.parent.height - this.top - this.height) + 'px';
- }
- };
-
- module.exports = ItemPoint;
-
-
-/***/ },
-/* 31 */
-/***/ function(module, exports, __webpack_require__) {
-
- var Hammer = __webpack_require__(41);
- var Item = __webpack_require__(28);
+ return m;
+ };
- /**
- * @constructor ItemRange
- * @extends Item
- * @param {Object} data Object containing parameters start, end
- * content, className.
- * @param {{toScreen: function, toTime: function}} conversion
- * Conversion functions from time to screen and vice versa
- * @param {Object} [options] Configuration options
- * // TODO: describe options
- */
- function ItemRange (data, conversion, options) {
- this.props = {
- content: {
- width: 0
- }
- };
- this.overflow = false; // if contents can overflow (css styling), this flag is set to true
+ moment.parseZone = function () {
+ return moment.apply(null, arguments).parseZone();
+ };
- // validate data
- if (data) {
- if (data.start == undefined) {
- throw new Error('Property "start" missing in item ' + data.id);
- }
- if (data.end == undefined) {
- throw new Error('Property "end" missing in item ' + data.id);
- }
- }
+ moment.parseTwoDigitYear = function (input) {
+ return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
+ };
- Item.call(this, data, conversion, options);
- }
+ /************************************
+ Moment Prototype
+ ************************************/
- ItemRange.prototype = new Item (null, null, null);
- ItemRange.prototype.baseClassName = 'item range';
+ extend(moment.fn = Moment.prototype, {
- /**
- * Check whether this item is visible inside given range
- * @returns {{start: Number, end: Number}} range with a timestamp for start and end
- * @returns {boolean} True if visible
- */
- ItemRange.prototype.isVisible = function(range) {
- // determine visibility
- return (this.data.start < range.end) && (this.data.end > range.start);
- };
+ clone : function () {
+ return moment(this);
+ },
- /**
- * Repaint the item
- */
- ItemRange.prototype.redraw = function() {
- var dom = this.dom;
- if (!dom) {
- // create DOM
- this.dom = {};
- dom = this.dom;
+ valueOf : function () {
+ return +this._d + ((this._offset || 0) * 60000);
+ },
- // background box
- dom.box = document.createElement('div');
- // className is updated in redraw()
+ unix : function () {
+ return Math.floor(+this / 1000);
+ },
- // contents box
- dom.content = document.createElement('div');
- dom.content.className = 'content';
- dom.box.appendChild(dom.content);
+ toString : function () {
+ return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
+ },
- // attach this item as attribute
- dom.box['timeline-item'] = this;
- }
+ toDate : function () {
+ return this._offset ? new Date(+this) : this._d;
+ },
- // append DOM to parent DOM
- if (!this.parent) {
- throw new Error('Cannot redraw item: no parent attached');
- }
- if (!dom.box.parentNode) {
- var foreground = this.parent.dom.foreground;
- if (!foreground) {
- throw new Error('Cannot redraw time axis: parent has no foreground container element');
- }
- foreground.appendChild(dom.box);
- }
- this.displayed = true;
+ toISOString : function () {
+ var m = moment(this).utc();
+ if (0 < m.year() && m.year() <= 9999) {
+ return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+ } else {
+ return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+ }
+ },
- // update contents
- if (this.data.content != this.content) {
- this.content = this.data.content;
- if (this.content instanceof Element) {
- dom.content.innerHTML = '';
- dom.content.appendChild(this.content);
- }
- else if (this.data.content != undefined) {
- dom.content.innerHTML = this.content;
- }
- else {
- throw new Error('Property "content" missing in item ' + this.data.id);
- }
+ toArray : function () {
+ var m = this;
+ return [
+ m.year(),
+ m.month(),
+ m.date(),
+ m.hours(),
+ m.minutes(),
+ m.seconds(),
+ m.milliseconds()
+ ];
+ },
- this.dirty = true;
- }
+ isValid : function () {
+ return isValid(this);
+ },
- // update title
- if (this.data.title != this.title) {
- dom.box.title = this.data.title;
- this.title = this.data.title;
- }
+ isDSTShifted : function () {
- // update class
- var className = (this.data.className ? (' ' + this.data.className) : '') +
- (this.selected ? ' selected' : '');
- if (this.className != className) {
- this.className = className;
- dom.box.className = this.baseClassName + className;
+ if (this._a) {
+ return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
+ }
- this.dirty = true;
- }
+ return false;
+ },
- // recalculate size
- if (this.dirty) {
- // determine from css whether this box has overflow
- this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden';
+ parsingFlags : function () {
+ return extend({}, this._pf);
+ },
- this.props.content.width = this.dom.content.offsetWidth;
- this.height = this.dom.box.offsetHeight;
+ invalidAt: function () {
+ return this._pf.overflow;
+ },
- this.dirty = false;
- }
+ utc : function () {
+ return this.zone(0);
+ },
- this._repaintDeleteButton(dom.box);
- this._repaintDragLeft();
- this._repaintDragRight();
- };
+ local : function () {
+ this.zone(0);
+ this._isUTC = false;
+ return this;
+ },
- /**
- * Show the item in the DOM (when not already visible). The items DOM will
- * be created when needed.
- */
- ItemRange.prototype.show = function() {
- if (!this.displayed) {
- this.redraw();
- }
- };
+ format : function (inputString) {
+ var output = formatMoment(this, inputString || moment.defaultFormat);
+ return this.lang().postformat(output);
+ },
- /**
- * Hide the item from the DOM (when visible)
- * @return {Boolean} changed
- */
- ItemRange.prototype.hide = function() {
- if (this.displayed) {
- var box = this.dom.box;
+ add : function (input, val) {
+ var dur;
+ // switch args to support add('s', 1) and add(1, 's')
+ if (typeof input === 'string' && typeof val === 'string') {
+ dur = moment.duration(isNaN(+val) ? +input : +val, isNaN(+val) ? val : input);
+ } else if (typeof input === 'string') {
+ dur = moment.duration(+val, input);
+ } else {
+ dur = moment.duration(input, val);
+ }
+ addOrSubtractDurationFromMoment(this, dur, 1);
+ return this;
+ },
- if (box.parentNode) {
- box.parentNode.removeChild(box);
- }
+ subtract : function (input, val) {
+ var dur;
+ // switch args to support subtract('s', 1) and subtract(1, 's')
+ if (typeof input === 'string' && typeof val === 'string') {
+ dur = moment.duration(isNaN(+val) ? +input : +val, isNaN(+val) ? val : input);
+ } else if (typeof input === 'string') {
+ dur = moment.duration(+val, input);
+ } else {
+ dur = moment.duration(input, val);
+ }
+ addOrSubtractDurationFromMoment(this, dur, -1);
+ return this;
+ },
- this.top = null;
- this.left = null;
+ diff : function (input, units, asFloat) {
+ var that = makeAs(input, this),
+ zoneDiff = (this.zone() - that.zone()) * 6e4,
+ diff, output;
- this.displayed = false;
- }
- };
+ units = normalizeUnits(units);
- /**
- * Reposition the item horizontally
- * @Override
- */
- ItemRange.prototype.repositionX = function() {
- var props = this.props,
- parentWidth = this.parent.width,
- start = this.conversion.toScreen(this.data.start),
- end = this.conversion.toScreen(this.data.end),
- padding = this.options.padding,
- contentLeft;
-
- // limit the width of the this, as browsers cannot draw very wide divs
- if (start < -parentWidth) {
- start = -parentWidth;
- }
- if (end > 2 * parentWidth) {
- end = 2 * parentWidth;
- }
- var boxWidth = Math.max(end - start, 1);
+ if (units === 'year' || units === 'month') {
+ // average number of days in the months in the given dates
+ diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
+ // difference in months
+ output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
+ // adjust by taking difference in days, average number of days
+ // and dst in the given months.
+ output += ((this - moment(this).startOf('month')) -
+ (that - moment(that).startOf('month'))) / diff;
+ // same as above but with zones, to negate all dst
+ output -= ((this.zone() - moment(this).startOf('month').zone()) -
+ (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
+ if (units === 'year') {
+ output = output / 12;
+ }
+ } else {
+ diff = (this - that);
+ output = units === 'second' ? diff / 1e3 : // 1000
+ units === 'minute' ? diff / 6e4 : // 1000 * 60
+ units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
+ units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
+ units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
+ diff;
+ }
+ return asFloat ? output : absRound(output);
+ },
- if (this.overflow) {
- // when range exceeds left of the window, position the contents at the left of the visible area
- contentLeft = Math.max(-start, 0);
+ from : function (time, withoutSuffix) {
+ return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
+ },
- this.left = start;
- this.width = boxWidth + this.props.content.width;
- // Note: The calculation of width is an optimistic calculation, giving
- // a width which will not change when moving the Timeline
- // So no restacking needed, which is nicer for the eye;
- }
- else { // no overflow
- // when range exceeds left of the window, position the contents at the left of the visible area
- if (start < 0) {
- contentLeft = Math.min(-start,
- (end - start - props.content.width - 2 * padding));
- // TODO: remove the need for options.padding. it's terrible.
- }
- else {
- contentLeft = 0;
- }
+ fromNow : function (withoutSuffix) {
+ return this.from(moment(), withoutSuffix);
+ },
- this.left = start;
- this.width = boxWidth;
- }
+ calendar : function (time) {
+ // We want to compare the start of today, vs this.
+ // Getting start-of-today depends on whether we're zone'd or not.
+ var now = time || moment(),
+ sod = makeAs(now, this).startOf('day'),
+ diff = this.diff(sod, 'days', true),
+ format = diff < -6 ? 'sameElse' :
+ diff < -1 ? 'lastWeek' :
+ diff < 0 ? 'lastDay' :
+ diff < 1 ? 'sameDay' :
+ diff < 2 ? 'nextDay' :
+ diff < 7 ? 'nextWeek' : 'sameElse';
+ return this.format(this.lang().calendar(format, this));
+ },
- this.dom.box.style.left = this.left + 'px';
- this.dom.box.style.width = boxWidth + 'px';
- this.dom.content.style.left = contentLeft + 'px';
- };
+ isLeapYear : function () {
+ return isLeapYear(this.year());
+ },
- /**
- * Reposition the item vertically
- * @Override
- */
- ItemRange.prototype.repositionY = function() {
- var orientation = this.options.orientation,
- box = this.dom.box;
+ isDST : function () {
+ return (this.zone() < this.clone().month(0).zone() ||
+ this.zone() < this.clone().month(5).zone());
+ },
- if (orientation == 'top') {
- box.style.top = this.top + 'px';
- }
- else {
- box.style.top = (this.parent.height - this.top - this.height) + 'px';
- }
- };
+ day : function (input) {
+ var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
+ if (input != null) {
+ input = parseWeekday(input, this.lang());
+ return this.add({ d : input - day });
+ } else {
+ return day;
+ }
+ },
- /**
- * Repaint a drag area on the left side of the range when the range is selected
- * @protected
- */
- ItemRange.prototype._repaintDragLeft = function () {
- if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
- // create and show drag area
- var dragLeft = document.createElement('div');
- dragLeft.className = 'drag-left';
- dragLeft.dragLeftItem = this;
+ month : makeAccessor('Month', true),
- // TODO: this should be redundant?
- Hammer(dragLeft, {
- preventDefault: true
- }).on('drag', function () {
- //console.log('drag left')
- });
+ startOf: function (units) {
+ units = normalizeUnits(units);
+ // the following switch intentionally omits break keywords
+ // to utilize falling through the cases.
+ switch (units) {
+ case 'year':
+ this.month(0);
+ /* falls through */
+ case 'quarter':
+ case 'month':
+ this.date(1);
+ /* falls through */
+ case 'week':
+ case 'isoWeek':
+ case 'day':
+ this.hours(0);
+ /* falls through */
+ case 'hour':
+ this.minutes(0);
+ /* falls through */
+ case 'minute':
+ this.seconds(0);
+ /* falls through */
+ case 'second':
+ this.milliseconds(0);
+ /* falls through */
+ }
- this.dom.box.appendChild(dragLeft);
- this.dom.dragLeft = dragLeft;
- }
- else if (!this.selected && this.dom.dragLeft) {
- // delete drag area
- if (this.dom.dragLeft.parentNode) {
- this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
- }
- this.dom.dragLeft = null;
- }
- };
+ // weeks are a special case
+ if (units === 'week') {
+ this.weekday(0);
+ } else if (units === 'isoWeek') {
+ this.isoWeekday(1);
+ }
- /**
- * Repaint a drag area on the right side of the range when the range is selected
- * @protected
- */
- ItemRange.prototype._repaintDragRight = function () {
- if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
- // create and show drag area
- var dragRight = document.createElement('div');
- dragRight.className = 'drag-right';
- dragRight.dragRightItem = this;
+ // quarters are also special
+ if (units === 'quarter') {
+ this.month(Math.floor(this.month() / 3) * 3);
+ }
- // TODO: this should be redundant?
- Hammer(dragRight, {
- preventDefault: true
- }).on('drag', function () {
- //console.log('drag right')
- });
+ return this;
+ },
- this.dom.box.appendChild(dragRight);
- this.dom.dragRight = dragRight;
- }
- else if (!this.selected && this.dom.dragRight) {
- // delete drag area
- if (this.dom.dragRight.parentNode) {
- this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
- }
- this.dom.dragRight = null;
- }
- };
+ endOf: function (units) {
+ units = normalizeUnits(units);
+ return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
+ },
- module.exports = ItemRange;
+ isAfter: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) > +moment(input).startOf(units);
+ },
+ isBefore: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) < +moment(input).startOf(units);
+ },
-/***/ },
-/* 32 */
-/***/ function(module, exports, __webpack_require__) {
+ isSame: function (input, units) {
+ units = units || 'ms';
+ return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
+ },
- var Emitter = __webpack_require__(49);
- var Hammer = __webpack_require__(41);
- var mousetrap = __webpack_require__(50);
- var util = __webpack_require__(1);
- var hammerUtil = __webpack_require__(42);
- var DataSet = __webpack_require__(3);
- var DataView = __webpack_require__(4);
- var dotparser = __webpack_require__(38);
- var gephiParser = __webpack_require__(39);
- var Groups = __webpack_require__(34);
- var Images = __webpack_require__(35);
- var Node = __webpack_require__(36);
- var Edge = __webpack_require__(33);
- var Popup = __webpack_require__(37);
- var MixinLoader = __webpack_require__(47);
- var Activator = __webpack_require__(48);
- var locales = __webpack_require__(45);
+ min: deprecate(
+ "moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",
+ function (other) {
+ other = moment.apply(null, arguments);
+ return other < this ? this : other;
+ }
+ ),
- // Load custom shapes into CanvasRenderingContext2D
- __webpack_require__(46);
+ max: deprecate(
+ "moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",
+ function (other) {
+ other = moment.apply(null, arguments);
+ return other > this ? this : other;
+ }
+ ),
- /**
- * @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');
- }
+ // keepTime = true means only change the timezone, without affecting
+ // the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
+ // It is possible that 5:31:26 doesn't exist int zone +0200, so we
+ // adjust the time as needed, to be valid.
+ //
+ // Keeping the time actually adds/subtracts (one hour)
+ // from the actual represented time. That is why we call updateOffset
+ // a second time. In case it wants us to change the offset again
+ // _changeInProgress == true case, then we have to adjust, because
+ // there is no such time in the given timezone.
+ zone : function (input, keepTime) {
+ var offset = this._offset || 0;
+ if (input != null) {
+ if (typeof input === "string") {
+ input = timezoneMinutesFromString(input);
+ }
+ if (Math.abs(input) < 16) {
+ input = input * 60;
+ }
+ this._offset = input;
+ this._isUTC = true;
+ if (offset !== input) {
+ if (!keepTime || this._changeInProgress) {
+ addOrSubtractDurationFromMoment(this,
+ moment.duration(offset - input, 'm'), 1, false);
+ } else if (!this._changeInProgress) {
+ this._changeInProgress = true;
+ moment.updateOffset(this, true);
+ this._changeInProgress = null;
+ }
+ }
+ } else {
+ return this._isUTC ? offset : this._d.getTimezoneOffset();
+ }
+ return this;
+ },
- this._initializeMixinLoaders();
+ zoneAbbr : function () {
+ return this._isUTC ? "UTC" : "";
+ },
- // create variables and set default values
- this.containerElement = container;
+ zoneName : function () {
+ return this._isUTC ? "Coordinated Universal Time" : "";
+ },
- // render and calculation settings
- this.renderRefreshRate = 60; // hz (fps)
- this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
- this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
- this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
- this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation
+ parseZone : function () {
+ if (this._tzm) {
+ this.zone(this._tzm);
+ } else if (typeof this._i === 'string') {
+ this.zone(this._i);
+ }
+ return this;
+ },
- this.initializing = true;
+ hasAlignedHourOffset : function (input) {
+ if (!input) {
+ input = 0;
+ }
+ else {
+ input = moment(input).zone();
+ }
- this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
+ return (this.zone() - input) % 60 === 0;
+ },
- // set constant values
- this.defaultOptions = {
- nodes: {
- mass: 1,
- radiusMin: 10,
- radiusMax: 30,
- radius: 10,
- shape: 'ellipse',
- image: undefined,
- widthMin: 16, // px
- widthMax: 64, // px
- fixed: false,
- fontColor: 'black',
- fontSize: 14, // px
- fontFace: 'verdana',
- level: -1,
- color: {
- border: '#2B7CE9',
- background: '#97C2FC',
- highlight: {
- border: '#2B7CE9',
- background: '#D2E5FF'
+ daysInMonth : function () {
+ return daysInMonth(this.year(), this.month());
},
- hover: {
- border: '#2B7CE9',
- background: '#D2E5FF'
- }
- },
- borderColor: '#2B7CE9',
- backgroundColor: '#97C2FC',
- highlightColor: '#D2E5FF',
- group: undefined,
- borderWidth: 1
- },
- 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,
- theta: 1 / 0.6, // 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
- },
- freezeForStabilization: false,
- smoothCurves: {
- enabled: true,
- dynamic: true,
- type: "continuous",
- roundness: 0.5
- },
- dynamicSmoothCurves: true,
- maxVelocity: 30,
- minVelocity: 0.1, // px/s
- stabilize: true, // stabilize before displaying the network
- stabilizationIterations: 1000, // maximum number of iteration to stabilize
- 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.hoverObj = {nodes:{},edges:{}};
- this.controlNodesActive = false;
+ dayOfYear : function (input) {
+ var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
+ return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
+ },
- // 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;
+ quarter : function (input) {
+ return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
+ },
- // 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();
+ weekYear : function (input) {
+ var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
+ return input == null ? year : this.add("y", (input - year));
+ },
- // apply options
- this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
- this._setScale(1);
- this.setOptions(options);
+ isoWeekYear : function (input) {
+ var year = weekOfYear(this, 1, 4).year;
+ return input == null ? year : this.add("y", (input - year));
+ },
- // other vars
- this.freezeSimulation = false;// freeze the simulation
- this.cachedFunctions = {};
+ week : function (input) {
+ var week = this.lang().week(this);
+ return input == null ? week : this.add("d", (input - week) * 7);
+ },
- // 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
+ isoWeek : function (input) {
+ var week = weekOfYear(this, 1, 4).week;
+ return input == null ? week : this.add("d", (input - week) * 7);
+ },
- // 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
+ weekday : function (input) {
+ var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
+ return input == null ? weekday : this.add("d", input - weekday);
+ },
- // datasets or dataviews
- this.nodesData = null; // A DataSet or DataView
- this.edgesData = null; // A DataSet or DataView
+ isoWeekday : function (input) {
+ // behaves the same as moment#day except
+ // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
+ // as a setter, sunday should belong to the previous week.
+ return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
+ },
- // 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);
- 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();
- }
- };
+ isoWeeksInYear : function () {
+ return weeksInYear(this.year(), 1, 4);
+ },
- // properties for the animation
- this.moving = true;
- this.timer = undefined; // Scheduling function. Is definded in this.start();
+ weeksInYear : function () {
+ var weekInfo = this._lang._week;
+ return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
+ },
- // 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);
+ get : function (units) {
+ units = normalizeUnits(units);
+ return this[units]();
+ },
- // 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(true,this.constants.clustering.enabled);
- }
- }
+ set : function (units, value) {
+ units = normalizeUnits(units);
+ if (typeof this[units] === 'function') {
+ this[units](value);
+ }
+ return this;
+ },
- // if clustering is disabled, the simulation will have started in the setData function
- if (this.constants.clustering.enabled) {
- this.startWithClustering();
- }
- }
+ // If passed a language key, it will set the language for this
+ // instance. Otherwise, it will return the language configuration
+ // variables for this instance.
+ lang : function (key) {
+ if (key === undefined) {
+ return this._lang;
+ } else {
+ this._lang = getLangDefinition(key);
+ return this;
+ }
+ }
+ });
- // Extend Network with an Emitter mixin
- Emitter(Network.prototype);
+ function rawMonthSetter(mom, value) {
+ var dayOfMonth;
- /**
- * 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' );
+ // TODO: Move this out of here!
+ if (typeof value === 'string') {
+ value = mom.lang().monthsParse(value);
+ // TODO: Another silent failure?
+ if (typeof value !== 'number') {
+ return mom;
+ }
+ }
- // 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);
+ dayOfMonth = Math.min(mom.date(),
+ daysInMonth(mom.year(), value));
+ mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
+ return mom;
}
- }
-
- return null;
- };
+ function rawGetter(mom, unit) {
+ return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
+ }
- /**
- * 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.x)) {minX = node.x;}
- if (maxX < (node.x)) {maxX = node.x;}
- if (minY > (node.y)) {minY = node.y;}
- if (maxY < (node.y)) {maxY = node.y;}
+ function rawSetter(mom, unit, value) {
+ if (unit === 'Month') {
+ return rawMonthSetter(mom, value);
+ } else {
+ return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
+ }
}
- }
- 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};
- };
+ function makeAccessor(unit, keepTime) {
+ return function (value) {
+ if (value != null) {
+ rawSetter(this, unit, value);
+ moment.updateOffset(this, keepTime);
+ return this;
+ } else {
+ return rawGetter(this, unit);
+ }
+ };
+ }
- /**
- * @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))};
- };
+ moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
+ moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
+ moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
+ // Setting the hour should keep the time, because the user explicitly
+ // specified which hour he wants. So trying to maintain the same hour (in
+ // a new timezone) makes sense. Adding/subtracting hours does not follow
+ // this rule.
+ moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
+ // moment.fn.month is defined separately
+ moment.fn.date = makeAccessor('Date', true);
+ moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true));
+ moment.fn.year = makeAccessor('FullYear', true);
+ moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true));
+ // add plural methods
+ moment.fn.days = moment.fn.day;
+ moment.fn.months = moment.fn.month;
+ moment.fn.weeks = moment.fn.week;
+ moment.fn.isoWeeks = moment.fn.isoWeek;
+ moment.fn.quarters = moment.fn.quarter;
- /**
- * center the network
- *
- * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
- */
- Network.prototype._centerNetwork = function(range) {
- var center = this._findCenter(range);
-
- center.x *= this.scale;
- center.y *= this.scale;
- center.x -= 0.5 * this.frame.canvas.clientWidth;
- center.y -= 0.5 * this.frame.canvas.clientHeight;
+ // add aliased format methods
+ moment.fn.toJSON = moment.fn.toISOString;
- this._setTranslation(-center.x,-center.y); // set at 0,0
- };
+ /************************************
+ Duration Prototype
+ ************************************/
- /**
- * 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(initialZoom, disableStart) {
- if (initialZoom === undefined) {
- initialZoom = false;
- }
- if (disableStart === undefined) {
- disableStart = false;
- }
+ extend(moment.duration.fn = Duration.prototype, {
- var range = this._getRange();
- var zoomLevel;
+ _bubble : function () {
+ var milliseconds = this._milliseconds,
+ days = this._days,
+ months = this._months,
+ data = this._data,
+ seconds, minutes, hours, years;
- 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.
- }
- }
+ // The following code bubbles up values, see the tests for
+ // examples of what that means.
+ data.milliseconds = milliseconds % 1000;
- // 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.minX) + Math.abs(range.maxX)) * 1.1;
- var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
+ seconds = absRound(milliseconds / 1000);
+ data.seconds = seconds % 60;
- var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
- var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
+ minutes = absRound(seconds / 60);
+ data.minutes = minutes % 60;
- zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
- }
+ hours = absRound(minutes / 60);
+ data.hours = hours % 24;
- if (zoomLevel > 1.0) {
- zoomLevel = 1.0;
- }
+ days += absRound(hours / 24);
+ data.days = days % 30;
+ months += absRound(days / 30);
+ data.months = months % 12;
- this._setScale(zoomLevel);
- this._centerNetwork(range);
- if (disableStart == false) {
- this.moving = true;
- this.start();
- }
- };
+ years = absRound(months / 12);
+ data.years = years;
+ },
+ weeks : function () {
+ return absRound(this.days() / 7);
+ },
- /**
- * 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);
- }
- }
- };
+ valueOf : function () {
+ return this._milliseconds +
+ this._days * 864e5 +
+ (this._months % 12) * 2592e6 +
+ toInt(this._months / 12) * 31536e6;
+ },
+ humanize : function (withSuffix) {
+ var difference = +this,
+ output = relativeTime(difference, !withSuffix, this.lang());
- /**
- * 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;
- }
+ if (withSuffix) {
+ output = this.lang().pastFuture(difference, output);
+ }
- 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.');
- }
+ return this.lang().postformat(output);
+ },
- // set options
- this.setOptions(data && data.options);
+ add : function (input, val) {
+ // supports only 2.0-style add(1, 's') or add(moment)
+ var dur = moment.duration(input, val);
- // 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._milliseconds += dur._milliseconds;
+ this._days += dur._days;
+ this._months += dur._months;
- this._putDataInSector();
- if (!disableStart) {
- // find a stable position or start animating to a stable position
- if (this.constants.stabilize) {
- var me = this;
- setTimeout(function() {me._stabilize(); me.start();},0)
- }
- else {
- this.start();
- }
- }
- };
+ this._bubble();
- /**
- * Set options
- * @param {Object} options
- */
- Network.prototype.setOptions = function (options) {
- if (options) {
- var prop;
+ return this;
+ },
- var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','clustering','navigation','keyboard','dataManipulation',
- 'onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse'
- ];
- util.selectiveNotDeepExtend(fields,this.constants, options);
- util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes);
- util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges);
+ subtract : function (input, val) {
+ var dur = moment.duration(input, val);
- if (options.physics) {
- util.mergeOptions(this.constants.physics, options.physics,'barnesHut');
- util.mergeOptions(this.constants.physics, options.physics,'repulsion');
+ this._milliseconds -= dur._milliseconds;
+ this._days -= dur._days;
+ this._months -= dur._months;
- 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];
- }
- }
- }
- }
+ this._bubble();
- 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;}
+ return this;
+ },
- 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');
+ get : function (units) {
+ units = normalizeUnits(units);
+ return this[units.toLowerCase() + 's']();
+ },
+ as : function (units) {
+ units = normalizeUnits(units);
+ return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
+ },
- if (options.dataManipulation) {
- this.editMode = this.constants.dataManipulation.initiallyVisible;
- }
+ lang : moment.fn.lang,
+ toIsoString : function () {
+ // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
+ var years = Math.abs(this.years()),
+ months = Math.abs(this.months()),
+ days = Math.abs(this.days()),
+ hours = Math.abs(this.hours()),
+ minutes = Math.abs(this.minutes()),
+ seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
- // 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;}
- }
- }
+ if (!this.asSeconds()) {
+ // this is the same as C#'s (Noda) and python (isodate)...
+ // but not other JS (goog.date)
+ return 'P0D';
+ }
- 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;}
+ return (this.asSeconds() < 0 ? '-' : '') +
+ 'P' +
+ (years ? years + 'Y' : '') +
+ (months ? months + 'M' : '') +
+ (days ? days + 'D' : '') +
+ ((hours || minutes || seconds) ? 'T' : '') +
+ (hours ? hours + 'H' : '') +
+ (minutes ? minutes + 'M' : '') +
+ (seconds ? seconds + 'S' : '');
}
- }
- }
+ });
- 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;
- }
+ function makeDurationGetter(name) {
+ moment.duration.fn[name] = function () {
+ return this._data[name];
+ };
}
- if (options.groups) {
- for (var groupname in options.groups) {
- if (options.groups.hasOwnProperty(groupname)) {
- var group = options.groups[groupname];
- this.groups.add(groupname, group);
- }
- }
+
+ function makeDurationAsGetter(name, factor) {
+ moment.duration.fn['as' + name] = function () {
+ return +this / factor;
+ };
}
- if (options.tooltip) {
- for (prop in options.tooltip) {
- if (options.tooltip.hasOwnProperty(prop)) {
- this.constants.tooltip[prop] = options.tooltip[prop];
+ for (i in unitMillisecondFactors) {
+ if (unitMillisecondFactors.hasOwnProperty(i)) {
+ makeDurationAsGetter(i, unitMillisecondFactors[i]);
+ makeDurationGetter(i.toLowerCase());
}
- }
- if (options.tooltip.color) {
- this.constants.tooltip.color = util.parseColor(options.tooltip.color);
- }
}
- if ('clickToUse' in options) {
- if (options.clickToUse) {
- this.activator = new Activator(this.frame);
- this.activator.on('change', this._createKeyBinds.bind(this));
- }
- else {
- if (this.activator) {
- this.activator.destroy();
- delete this.activator;
+ makeDurationAsGetter('Weeks', 6048e5);
+ moment.duration.fn.asMonths = function () {
+ return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
+ };
+
+
+ /************************************
+ Default Lang
+ ************************************/
+
+
+ // Set default language, other languages will inherit from English.
+ moment.lang('en', {
+ ordinal : function (number) {
+ var b = number % 10,
+ output = (toInt(number % 100 / 10) === 1) ? 'th' :
+ (b === 1) ? 'st' :
+ (b === 2) ? 'nd' :
+ (b === 3) ? 'rd' : 'th';
+ return number + output;
}
- }
- }
+ });
- if (options.labels) {
- throw new Error('Option "labels" is deprecated. Use options "locale" and "locales" instead.');
+ /* EMBED_LANGUAGES */
+
+ /************************************
+ Exposing Moment
+ ************************************/
+
+ function makeGlobal(shouldDeprecate) {
+ /*global ender:false */
+ if (typeof ender !== 'undefined') {
+ return;
+ }
+ oldGlobalMoment = globalScope.moment;
+ if (shouldDeprecate) {
+ globalScope.moment = deprecate(
+ "Accessing Moment through the global scope is " +
+ "deprecated, and will be removed in an upcoming " +
+ "release.",
+ moment);
+ } else {
+ globalScope.moment = moment;
+ }
}
- }
- // (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();
+ // CommonJS module is defined
+ if (hasModule) {
+ module.exports = moment;
+ } else if (true) {
+ !(__WEBPACK_AMD_DEFINE_RESULT__ = (function (require, exports, module) {
+ if (module.config && module.config() && module.config().noGlobal === true) {
+ // release the global variable
+ globalScope.moment = oldGlobalMoment;
+ }
+ return moment;
+ }.call(exports, __webpack_require__, exports, module)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ makeGlobal(true);
+ } else {
+ makeGlobal();
+ }
+ }).call(this);
+
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()), __webpack_require__(25)(module)))
- // bind keys. If disabled, this will not do anything;
- this._createKeyBinds();
- this.setSize(this.constants.width, this.constants.height);
- this.moving = true;
- this.start();
+/***/ },
+/* 24 */
+/***/ function(module, exports, __webpack_require__) {
+ var map = {};
+ function webpackContext(req) {
+ return __webpack_require__(webpackContextResolve(req));
};
+ function webpackContextResolve(req) {
+ return map[req] || (function() { throw new Error("Cannot find module '" + req + "'.") }());
+ };
+ webpackContext.keys = function webpackContextKeys() {
+ return Object.keys(map);
+ };
+ webpackContext.resolve = webpackContextResolve;
+ module.exports = webpackContext;
- /**
- * 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);
- }
-
- this.frame = document.createElement('div');
- this.frame.className = 'vis network-frame';
- this.frame.style.position = 'relative';
- this.frame.style.overflow = 'hidden';
- // create the network canvas (HTML canvas element)
- this.frame.canvas = document.createElement( 'canvas' );
- this.frame.canvas.style.position = 'relative';
- this.frame.appendChild(this.frame.canvas);
- if (!this.frame.canvas.getContext) {
- var noCanvas = document.createElement( 'DIV' );
- noCanvas.style.color = 'red';
- noCanvas.style.fontWeight = 'bold' ;
- noCanvas.style.padding = '10px';
- noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
- this.frame.canvas.appendChild(noCanvas);
- }
+/***/ },
+/* 25 */
+/***/ function(module, exports, __webpack_require__) {
- var me = this;
- this.drag = {};
- this.pinch = {};
- this.hammer = Hammer(this.frame.canvas, {
- prevent_default: true
- });
- this.hammer.on('tap', me._onTap.bind(me) );
- this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
- this.hammer.on('hold', me._onHold.bind(me) );
- this.hammer.on('pinch', me._onPinch.bind(me) );
- this.hammer.on('touch', me._onTouch.bind(me) );
- this.hammer.on('dragstart', me._onDragStart.bind(me) );
- this.hammer.on('drag', me._onDrag.bind(me) );
- this.hammer.on('dragend', me._onDragEnd.bind(me) );
- this.hammer.on('release', me._onRelease.bind(me) );
- this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
- this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
- this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
+ module.exports = function(module) {
+ if(!module.webpackPolyfill) {
+ module.deprecate = function() {};
+ module.paths = [];
+ // module.parent = undefined by default
+ module.children = [];
+ module.webpackPolyfill = 1;
+ }
+ return module;
+ }
- // add the frame to the container element
- this.containerElement.appendChild(this.frame);
- };
+/***/ },
+/* 26 */
+/***/ function(module, exports, __webpack_require__) {
+ var util = __webpack_require__(1);
+ var Component = __webpack_require__(20);
+ var moment = __webpack_require__(22);
+ var locales = __webpack_require__(64);
/**
- * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
- * @private
+ * A current time bar
+ * @param {{range: Range, dom: Object, domProps: Object}} body
+ * @param {Object} [options] Available parameters:
+ * {Boolean} [showCurrentTime]
+ * @constructor CurrentTime
+ * @extends Component
*/
- Network.prototype._createKeyBinds = function() {
- var me = this;
- this.mousetrap = mousetrap;
+ function CurrentTime (body, options) {
+ this.body = body;
- this.mousetrap.reset();
+ // default options
+ this.defaultOptions = {
+ showCurrentTime: true,
- if (this.constants.keyboard.enabled && this.isActive()) {
- this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
- this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
- this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
- this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
- this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
- this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
- this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
- this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
- this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
- this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
- this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
- this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
- this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
- this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
- this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
- this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
- this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
- this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
- this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
- this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
- }
+ locales: locales,
+ locale: 'en'
+ };
+ this.options = util.extend({}, this.defaultOptions);
+ this.offset = 0;
- if (this.constants.dataManipulation.enabled == true) {
- this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
- this.mousetrap.bind("del",this._deleteSelected.bind(me));
- }
- };
+ this._create();
+
+ this.setOptions(options);
+ }
+
+ CurrentTime.prototype = new Component();
/**
- * Get the pointer location from a touch location
- * @param {{pageX: Number, pageY: Number}} touch
- * @return {{x: Number, y: Number}} pointer
+ * Create the HTML DOM for the current time bar
* @private
*/
- Network.prototype._getPointer = function (touch) {
- return {
- x: touch.pageX - util.getAbsoluteLeft(this.frame.canvas),
- y: touch.pageY - util.getAbsoluteTop(this.frame.canvas)
- };
+ 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;
};
/**
- * On start of a touch gesture, store the pointer
- * @param event
- * @private
+ * Destroy the CurrentTime bar
*/
- Network.prototype._onTouch = function (event) {
- this.drag.pointer = this._getPointer(event.gesture.center);
- this.drag.pinched = false;
- this.pinch.scale = this._getScale();
+ CurrentTime.prototype.destroy = function () {
+ this.options.showCurrentTime = false;
+ this.redraw(); // will remove the bar from the DOM and stop refreshing
- this._handleTouch(this.drag.pointer);
+ this.body = null;
};
/**
- * handle drag start event
- * @private
+ * Set options for the component. Options will be merged in current options.
+ * @param {Object} options Available parameters:
+ * {boolean} [showCurrentTime]
*/
- Network.prototype._onDragStart = function () {
- this._handleDragStart();
+ CurrentTime.prototype.setOptions = function(options) {
+ if (options) {
+ // copy all options that we know
+ util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options);
+ }
};
-
/**
- * This function is called by _onDragStart.
- * It is separated out because we can then overload it for the datamanipulation system.
- *
- * @private
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
*/
- Network.prototype._handleDragStart = function() {
- var drag = this.drag;
- var node = this._getNodeAt(drag.pointer);
- // note: drag.pointer is set in _onTouch to get the initial touch location
-
- drag.dragging = true;
- drag.selection = [];
- drag.translation = this._getTranslation();
- drag.nodeId = null;
+ 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 (node != null) {
- drag.nodeId = node.id;
- // select the clicked node if not yet selected
- if (!node.isSelected()) {
- this._selectObject(node,false);
+ this.start();
}
- // create an array with the selected nodes and their original location and status
- for (var objectId in this.selectionObj.nodes) {
- if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
- var object = this.selectionObj.nodes[objectId];
- var s = {
- id: object.id,
- node: object,
-
- // store original x, y, xFixed and yFixed, make the node temporarily Fixed
- x: object.x,
- y: object.y,
- xFixed: object.xFixed,
- yFixed: object.yFixed
- };
+ var now = new Date(new Date().valueOf() + this.offset);
+ var x = this.body.util.toScreen(now);
- object.xFixed = true;
- object.yFixed = true;
+ 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);
- drag.selection.push(s);
- }
+ 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();
}
- };
-
- /**
- * handle drag event
- * @private
- */
- Network.prototype._onDrag = function (event) {
- this._handleOnDrag(event)
+ return false;
};
-
/**
- * This function is called by _onDrag.
- * It is separated out because we can then overload it for the datamanipulation system.
- *
- * @private
+ * Start auto refreshing the current time bar
*/
- Network.prototype._handleOnDrag = function(event) {
- if (this.drag.pinched) {
- return;
- }
-
- var pointer = this._getPointer(event.gesture.center);
-
+ CurrentTime.prototype.start = function() {
var me = this;
- var drag = this.drag;
- var selection = drag.selection;
- if (selection && selection.length && this.constants.dragNodes == true) {
- // calculate delta's and new location
- var deltaX = pointer.x - drag.pointer.x;
- var deltaY = pointer.y - drag.pointer.y;
-
- // update position of all selected nodes
- selection.forEach(function (s) {
- var node = s.node;
- if (!s.xFixed) {
- node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX);
- }
+ function update () {
+ me.stop();
- if (!s.yFixed) {
- node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY);
- }
- });
+ // 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 _animationStep if not yet running
- if (!this.moving) {
- this.moving = true;
- this.start();
- }
+ // start a timer to adjust for the new time
+ me.currentTimeTimer = setTimeout(update, interval);
}
- else {
- if (this.constants.dragNetwork == true) {
- // move the network
- var diffX = pointer.x - this.drag.pointer.x;
- var diffY = pointer.y - this.drag.pointer.y;
- this._setTranslation(
- this.drag.translation.x + diffX,
- this.drag.translation.y + diffY
- );
- this._redraw();
- // this.moving = true;
- // this.start();
- }
- }
+ update();
};
/**
- * handle drag start event
- * @private
+ * Stop auto refreshing the current time bar
*/
- Network.prototype._onDragEnd = function () {
- this.drag.dragging = false;
- var selection = this.drag.selection;
- if (selection && selection.length) {
- selection.forEach(function (s) {
- // restore original xFixed and yFixed
- s.node.xFixed = s.xFixed;
- s.node.yFixed = s.yFixed;
- });
- this.moving = true;
- this.start();
- }
- else {
- this._redraw();
+ CurrentTime.prototype.stop = function() {
+ if (this.currentTimeTimer !== undefined) {
+ clearTimeout(this.currentTimeTimer);
+ delete this.currentTimeTimer;
}
-
};
/**
- * handle tap/click event: select/unselect a node
- * @private
+ * 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.
*/
- Network.prototype._onTap = function (event) {
- var pointer = this._getPointer(event.gesture.center);
- this.pointerPosition = pointer;
- this._handleTap(pointer);
-
+ CurrentTime.prototype.setCurrentTime = function(time) {
+ var t = util.convert(time, 'Date').valueOf();
+ var now = new Date().valueOf();
+ this.offset = t - now;
+ this.redraw();
};
-
/**
- * handle doubletap event
- * @private
+ * Get the current time.
+ * @return {Date} Returns the current time.
*/
- Network.prototype._onDoubleTap = function (event) {
- var pointer = this._getPointer(event.gesture.center);
- this._handleDoubleTap(pointer);
+ CurrentTime.prototype.getCurrentTime = function() {
+ return new Date(new Date().valueOf() + this.offset);
};
+ module.exports = CurrentTime;
- /**
- * handle long tap event: multi select nodes
- * @private
- */
- Network.prototype._onHold = function (event) {
- var pointer = this._getPointer(event.gesture.center);
- this.pointerPosition = pointer;
- this._handleOnHold(pointer);
- };
+
+/***/ },
+/* 27 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Hammer = __webpack_require__(15);
+ var util = __webpack_require__(1);
+ var Component = __webpack_require__(20);
+ var moment = __webpack_require__(22);
+ var locales = __webpack_require__(64);
/**
- * handle the release of the screen
- *
- * @private
+ * A custom time bar
+ * @param {{range: Range, dom: Object}} body
+ * @param {Object} [options] Available parameters:
+ * {Boolean} [showCustomTime]
+ * @constructor CustomTime
+ * @extends Component
*/
- Network.prototype._onRelease = function (event) {
- var pointer = this._getPointer(event.gesture.center);
- this._handleOnRelease(pointer);
+
+ 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);
+ }
};
/**
- * Handle pinch event
- * @param event
+ * Create the DOM for the custom time
* @private
*/
- Network.prototype._onPinch = function (event) {
- var pointer = this._getPointer(event.gesture.center);
+ 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;
- this.drag.pinched = true;
- if (!('scale' in this.pinch)) {
- this.pinch.scale = 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);
- // TODO: enabled moving while pinching?
- var scale = this.pinch.scale * event.gesture.scale;
- this._zoom(scale, pointer)
+ // 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));
};
/**
- * Zoom the network in or out
- * @param {Number} scale a number around 1, and between 0.01 and 10
- * @param {{x: Number, y: Number}} pointer Position on screen
- * @return {Number} appliedScale scale is limited within the boundaries
- * @private
+ * Destroy the CustomTime bar
*/
- Network.prototype._zoom = function(scale, pointer) {
- if (this.constants.zoomable == true) {
- var scaleOld = this._getScale();
- if (scale < 0.00001) {
- scale = 0.00001;
- }
- if (scale > 10) {
- scale = 10;
- }
+ CustomTime.prototype.destroy = function () {
+ this.options.showCustomTime = false;
+ this.redraw(); // will remove the bar from the DOM
- var preScaleDragPointer = null;
- if (this.drag !== undefined) {
- if (this.drag.dragging == true) {
- preScaleDragPointer = this.DOMtoCanvas(this.drag.pointer);
+ 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.frame.canvas.clientHeight / 2
- var translation = this._getTranslation();
-
- var scaleFrac = scale / scaleOld;
- var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
- var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
- this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
- "y" : this._YconvertDOMtoCanvas(pointer.y)};
+ var x = this.body.util.toScreen(this.customTime);
- this._setScale(scale);
- this._setTranslation(tx, ty);
- this.updateClustersDefault();
+ var locale = this.options.locales[this.options.locale];
+ var title = locale.time + ': ' + moment(this.customTime).format('dddd, MMMM Do YYYY, H:mm:ss');
+ title = title.charAt(0).toUpperCase() + title.substring(1);
- if (preScaleDragPointer != null) {
- var postScaleDragPointer = this.canvasToDOM(preScaleDragPointer);
- this.drag.pointer.x = postScaleDragPointer.x;
- this.drag.pointer.y = postScaleDragPointer.y;
+ 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._redraw();
-
- if (scaleOld < scale) {
- this.emit("zoom", {direction:"+"});
- }
- else {
- this.emit("zoom", {direction:"-"});
- }
+ return false;
+ };
- return scale;
- }
+ /**
+ * Set custom time.
+ * @param {Date | number | string} time
+ */
+ CustomTime.prototype.setCustomTime = function(time) {
+ this.customTime = util.convert(time, 'Date');
+ this.redraw();
};
+ /**
+ * Retrieve the current custom time.
+ * @return {Date} customTime
+ */
+ CustomTime.prototype.getCustomTime = function() {
+ return new Date(this.customTime.valueOf());
+ };
/**
- * Event handler for mouse wheel event, used to zoom the timeline
- * See http://adomas.org/javascript-mouse-wheel/
- * https://github.com/EightMedia/hammer.js/issues/256
- * @param {MouseEvent} event
+ * Start moving horizontally
+ * @param {Event} event
* @private
*/
- Network.prototype._onMouseWheel = function(event) {
- // retrieve delta
- var delta = 0;
- if (event.wheelDelta) { /* IE/Opera. */
- delta = event.wheelDelta/120;
- } else if (event.detail) { /* Mozilla case. */
- // In Mozilla, sign of delta is different than in IE.
- // Also, delta is multiple of 3.
- delta = -event.detail/3;
- }
+ CustomTime.prototype._onDragStart = function(event) {
+ this.eventParams.dragging = true;
+ this.eventParams.customTime = this.customTime;
- // If delta is nonzero, handle it.
- // Basically, delta is now positive if wheel was scrolled up,
- // and negative, if wheel was scrolled down.
- if (delta) {
+ event.stopPropagation();
+ event.preventDefault();
+ };
- // calculate the new scale
- var scale = this._getScale();
- var zoom = delta / 10;
- if (delta < 0) {
- zoom = zoom / (1 - zoom);
- }
- scale *= (1 + zoom);
+ /**
+ * Perform moving operating.
+ * @param {Event} event
+ * @private
+ */
+ CustomTime.prototype._onDrag = function (event) {
+ if (!this.eventParams.dragging) return;
- // calculate the pointer location
- var gesture = hammerUtil.fakeGesture(this, event);
- var pointer = this._getPointer(gesture.center);
+ var deltaX = event.gesture.deltaX,
+ x = this.body.util.toScreen(this.eventParams.customTime) + deltaX,
+ time = this.body.util.toTime(x);
- // apply the new scale
- this._zoom(scale, pointer);
- }
+ this.setCustomTime(time);
- // Prevent default actions caused by mouse wheel.
+ // fire a timechange event
+ this.body.emitter.emit('timechange', {
+ time: new Date(this.customTime.valueOf())
+ });
+
+ event.stopPropagation();
event.preventDefault();
};
-
/**
- * Mouse move handler for checking whether the title moves over a node with a title.
- * @param {Event} event
+ * Stop moving operating.
+ * @param {event} event
* @private
*/
- Network.prototype._onMouseMoveTitle = function (event) {
- var gesture = hammerUtil.fakeGesture(this, event);
- var pointer = this._getPointer(gesture.center);
+ CustomTime.prototype._onDragEnd = function (event) {
+ if (!this.eventParams.dragging) return;
- // check if the previously selected node is still selected
- if (this.popupObj) {
- this._checkHidePopup(pointer);
- }
+ // fire a timechanged event
+ this.body.emitter.emit('timechanged', {
+ time: new Date(this.customTime.valueOf())
+ });
- // start a timeout that will check if the mouse is positioned above
- // an element
- var me = this;
- var checkShow = function() {
- me._checkShowPopup(pointer);
- };
- if (this.popupTimer) {
- clearInterval(this.popupTimer); // stop any running calculationTimer
- }
- if (!this.drag.dragging) {
- this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
- }
+ event.stopPropagation();
+ event.preventDefault();
+ };
+ module.exports = CustomTime;
- /**
- * Adding hover highlights
- */
- if (this.constants.hover == true) {
- // removing all hover highlights
- for (var edgeId in this.hoverObj.edges) {
- if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
- this.hoverObj.edges[edgeId].hover = false;
- delete this.hoverObj.edges[edgeId];
- }
- }
- // adding hover highlights
- var obj = this._getNodeAt(pointer);
- if (obj == null) {
- obj = this._getEdgeAt(pointer);
- }
- if (obj != null) {
- this._hoverObject(obj);
- }
+/***/ },
+/* 28 */
+/***/ function(module, exports, __webpack_require__) {
- // removing all node hover highlights except for the selected one.
- for (var nodeId in this.hoverObj.nodes) {
- if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
- if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) {
- this._blurObject(this.hoverObj.nodes[nodeId]);
- delete this.hoverObj.nodes[nodeId];
- }
- }
- }
- this.redraw();
- }
- };
+ var util = __webpack_require__(1);
+ var DOMutil = __webpack_require__(2);
+ var DataSet = __webpack_require__(3);
+ var DataView = __webpack_require__(4);
+ var Component = __webpack_require__(20);
+ var DataAxis = __webpack_require__(29);
+ var GraphGroup = __webpack_require__(31);
+ var Legend = __webpack_require__(32);
+
+ var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
/**
- * Check if there is an element on the given position in the network
- * (a node or edge). If so, and if this element has a title,
- * show a popup window with its title.
+ * This is the constructor of the LineGraph. It requires a Timeline body and options.
*
- * @param {{x:Number, y:Number}} pointer
- * @private
+ * @param body
+ * @param options
+ * @constructor
*/
- Network.prototype._checkShowPopup = function (pointer) {
- var obj = {
- left: this._XconvertDOMtoCanvas(pointer.x),
- top: this._YconvertDOMtoCanvas(pointer.y),
- right: this._XconvertDOMtoCanvas(pointer.x),
- bottom: this._YconvertDOMtoCanvas(pointer.y)
+ function LineGraph(body, options) {
+ this.id = util.randomUUID();
+ this.body = body;
+
+ this.defaultOptions = {
+ yAxisOrientation: 'left',
+ defaultGroup: 'default',
+ sort: true,
+ sampling: true,
+ graphHeight: '400px',
+ shaded: {
+ enabled: false,
+ orientation: 'bottom' // top, bottom
+ },
+ style: 'line', // line, bar
+ barChart: {
+ width: 50,
+ handleOverlap: 'overlap',
+ align: 'center' // left, center, right
+ },
+ catmullRom: {
+ enabled: true,
+ parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
+ alpha: 0.5
+ },
+ drawPoints: {
+ enabled: true,
+ size: 6,
+ style: 'square' // square, circle
+ },
+ dataAxis: {
+ showMinorLabels: true,
+ showMajorLabels: true,
+ icons: false,
+ width: '40px',
+ visible: true,
+ customRange: {
+ left: {min:undefined, max:undefined},
+ right: {min:undefined, max:undefined}
+ }
+ },
+ legend: {
+ enabled: false,
+ icons: true,
+ left: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,right
+ },
+ right: {
+ visible: true,
+ position: 'top-right' // top/bottom - left,right
+ }
+ },
+ groups: {
+ visibility: {}
+ }
};
- var id;
- var lastPopupNode = this.popupObj;
+ // options is shared by this ItemSet and all its items
+ this.options = util.extend({}, this.defaultOptions);
+ this.dom = {};
+ this.props = {};
+ this.hammer = null;
+ this.groups = {};
+ this.abortedGraphUpdate = false;
- if (this.popupObj == undefined) {
- // search the nodes for overlap, select the top one in case of multiple nodes
- var nodes = this.nodes;
- for (id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- var node = nodes[id];
- if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
- this.popupObj = node;
- break;
- }
- }
+ var me = this;
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
+
+ // listeners for the DataSet of the items
+ this.itemListeners = {
+ 'add': function (event, params, senderId) {
+ me._onAdd(params.items);
+ },
+ 'update': function (event, params, senderId) {
+ me._onUpdate(params.items);
+ },
+ 'remove': function (event, params, senderId) {
+ me._onRemove(params.items);
}
- }
+ };
- if (this.popupObj === undefined) {
- // search the edges for overlap
- var edges = this.edges;
- for (id in edges) {
- if (edges.hasOwnProperty(id)) {
- var edge = edges[id];
- if (edge.connected && (edge.getTitle() !== undefined) &&
- edge.isOverlappingWith(obj)) {
- this.popupObj = edge;
- break;
- }
- }
+ // listeners for the DataSet of the groups
+ this.groupListeners = {
+ 'add': function (event, params, senderId) {
+ me._onAddGroups(params.items);
+ },
+ 'update': function (event, params, senderId) {
+ me._onUpdateGroups(params.items);
+ },
+ 'remove': function (event, params, senderId) {
+ me._onRemoveGroups(params.items);
}
- }
+ };
- if (this.popupObj) {
- // show popup message window
- if (this.popupObj != lastPopupNode) {
- var me = this;
- if (!me.popup) {
- me.popup = new Popup(me.frame, me.constants.tooltip);
+ this.items = {}; // object with an Item for every data item
+ this.selection = []; // list with the ids of all selected nodes
+ this.lastStart = this.body.range.start;
+ this.touchParams = {}; // stores properties while dragging
+
+ this.svgElements = {};
+ this.setOptions(options);
+ this.groupsUsingDefaultStyles = [0];
+
+ this.body.emitter.on("rangechange",function() {
+ if (me.lastStart != 0) {
+ var offset = me.body.range.start - me.lastStart;
+ var range = me.body.range.end - me.body.range.start;
+ if (me.width != 0) {
+ var rangePerPixelInv = me.width/range;
+ var xOffset = offset * rangePerPixelInv;
+ me.svg.style.left = (-me.width - xOffset) + "px";
+ }
}
+ });
+ this.body.emitter.on("rangechanged", function() {
+ me.lastStart = me.body.range.start;
+ me.svg.style.left = util.option.asSize(-me.width);
+ me._updateGraph.apply(me);
+ });
- // adjust a small offset such that the mouse cursor is located in the
- // bottom left location of the popup, and you can easily move over the
- // popup area
- me.popup.setPosition(pointer.x - 3, pointer.y - 3);
- me.popup.setText(me.popupObj.getTitle());
- me.popup.show();
- }
- }
- else {
- if (this.popup) {
- this.popup.hide();
- }
- }
- };
+ // create the HTML DOM
+ this._create();
+ this.body.emitter.emit("change");
+ }
+ LineGraph.prototype = new Component();
/**
- * Check if the popup must be hided, which is the case when the mouse is no
- * longer hovering on the object
- * @param {{x:Number, y:Number}} pointer
- * @private
+ * Create the HTML DOM for the ItemSet
*/
- Network.prototype._checkHidePopup = function (pointer) {
- if (!this.popupObj || !this._getNodeAt(pointer) ) {
- this.popupObj = undefined;
- if (this.popup) {
- this.popup.hide();
- }
- }
- };
+ LineGraph.prototype._create = function(){
+ var frame = document.createElement('div');
+ frame.className = 'LineGraph';
+ this.dom.frame = frame;
+ // create svg element for graph drawing.
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = "relative";
+ this.svg.style.height = ('' + this.options.graphHeight).replace("px",'') + 'px';
+ this.svg.style.display = "block";
+ frame.appendChild(this.svg);
- /**
- * Set a new size for the network
- * @param {string} width Width in pixels or percentage (for example '800px'
- * or '50%')
- * @param {string} height Height in pixels or percentage (for example '400px'
- * or '30%')
- */
- Network.prototype.setSize = function(width, height) {
- this.frame.style.width = width;
- this.frame.style.height = height;
+ // data axis
+ this.options.dataAxis.orientation = 'left';
+ this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
- this.frame.canvas.style.width = '100%';
- this.frame.canvas.style.height = '100%';
+ this.options.dataAxis.orientation = 'right';
+ this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
+ delete this.options.dataAxis.orientation;
- this.frame.canvas.width = this.frame.canvas.clientWidth;
- this.frame.canvas.height = this.frame.canvas.clientHeight;
+ // legends
+ this.legendLeft = new Legend(this.body, this.options.legend, 'left', this.options.groups);
+ this.legendRight = new Legend(this.body, this.options.legend, 'right', this.options.groups);
- this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
+ this.show();
};
/**
- * Set a data set with nodes for the network
- * @param {Array | DataSet | DataView} nodes The data containing the nodes.
- * @private
+ * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
+ * @param options
*/
- Network.prototype._setNodes = function(nodes) {
- var oldNodesData = this.nodesData;
-
- if (nodes instanceof DataSet || nodes instanceof DataView) {
- this.nodesData = nodes;
- }
- else if (nodes instanceof Array) {
- this.nodesData = new DataSet();
- this.nodesData.add(nodes);
- }
- else if (!nodes) {
- this.nodesData = new DataSet();
- }
- else {
- throw new TypeError('Array or DataSet expected');
- }
-
- if (oldNodesData) {
- // unsubscribe from old dataset
- util.forEach(this.nodesListeners, function (callback, event) {
- oldNodesData.off(event, callback);
- });
- }
+ LineGraph.prototype.setOptions = function(options) {
+ if (options) {
+ var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort','groups'];
+ util.selectiveDeepExtend(fields, this.options, options);
+ util.mergeOptions(this.options, options,'catmullRom');
+ util.mergeOptions(this.options, options,'drawPoints');
+ util.mergeOptions(this.options, options,'shaded');
+ util.mergeOptions(this.options, options,'legend');
- // remove drawn nodes
- this.nodes = {};
+ if (options.catmullRom) {
+ if (typeof options.catmullRom == 'object') {
+ if (options.catmullRom.parametrization) {
+ if (options.catmullRom.parametrization == 'uniform') {
+ this.options.catmullRom.alpha = 0;
+ }
+ else if (options.catmullRom.parametrization == 'chordal') {
+ this.options.catmullRom.alpha = 1.0;
+ }
+ else {
+ this.options.catmullRom.parametrization = 'centripetal';
+ this.options.catmullRom.alpha = 0.5;
+ }
+ }
+ }
+ }
- if (this.nodesData) {
- // subscribe to new dataset
- var me = this;
- util.forEach(this.nodesListeners, function (callback, event) {
- me.nodesData.on(event, callback);
- });
+ if (this.yAxisLeft) {
+ if (options.dataAxis !== undefined) {
+ this.yAxisLeft.setOptions(this.options.dataAxis);
+ this.yAxisRight.setOptions(this.options.dataAxis);
+ }
+ }
- // draw all new nodes
- var ids = this.nodesData.getIds();
- this._addNodes(ids);
- }
- this._updateSelection();
- };
+ if (this.legendLeft) {
+ if (options.legend !== undefined) {
+ this.legendLeft.setOptions(this.options.legend);
+ this.legendRight.setOptions(this.options.legend);
+ }
+ }
- /**
- * Add nodes
- * @param {Number[] | String[]} ids
- * @private
- */
- Network.prototype._addNodes = function(ids) {
- var id;
- for (var i = 0, len = ids.length; i < len; i++) {
- id = ids[i];
- var data = this.nodesData.get(id);
- var node = new Node(data, this.images, this.groups, this.constants);
- this.nodes[id] = node; // note: this may replace an existing node
- if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
- var radius = 10 * 0.1*ids.length + 10;
- var angle = 2 * Math.PI * Math.random();
- if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
- if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
+ if (this.groups.hasOwnProperty(UNGROUPED)) {
+ this.groups[UNGROUPED].setOptions(options);
}
- this.moving = true;
}
- this._updateNodeIndexList();
- if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
- this._resetLevels();
- this._setupHierarchicalLayout();
+ if (this.dom.frame) {
+ this._updateGraph();
}
- this._updateCalculationNodes();
- this._reconnectEdges();
- this._updateValueRange(this.nodes);
- this.updateLabels();
};
/**
- * Update existing nodes, or create them when not yet existing
- * @param {Number[] | String[]} ids
- * @private
+ * Hide the component from the DOM
*/
- Network.prototype._updateNodes = function(ids) {
- var nodes = this.nodes,
- nodesData = this.nodesData;
- for (var i = 0, len = ids.length; i < len; i++) {
- var id = ids[i];
- var node = nodes[id];
- var data = nodesData.get(id);
- if (node) {
- // update node
- node.setProperties(data, this.constants);
- }
- else {
- // create node
- node = new Node(properties, this.images, this.groups, this.constants);
- nodes[id] = node;
- }
- }
- this.moving = true;
- if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
- this._resetLevels();
- this._setupHierarchicalLayout();
+ LineGraph.prototype.hide = function() {
+ // remove the frame containing the items
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
}
- this._updateNodeIndexList();
- this._reconnectEdges();
- this._updateValueRange(nodes);
};
/**
- * Remove existing nodes. If nodes do not exist, the method will just ignore it.
- * @param {Number[] | String[]} ids
- * @private
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
*/
- Network.prototype._removeNodes = function(ids) {
- var nodes = this.nodes;
- for (var i = 0, len = ids.length; i < len; i++) {
- var id = ids[i];
- delete nodes[id];
- }
- this._updateNodeIndexList();
- if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
- this._resetLevels();
- this._setupHierarchicalLayout();
+ LineGraph.prototype.show = function() {
+ // show frame containing the items
+ if (!this.dom.frame.parentNode) {
+ this.body.dom.center.appendChild(this.dom.frame);
}
- this._updateCalculationNodes();
- this._reconnectEdges();
- this._updateSelection();
- this._updateValueRange(nodes);
};
+
/**
- * Load edges by reading the data table
- * @param {Array | DataSet | DataView} edges The data containing the edges.
- * @private
- * @private
+ * Set items
+ * @param {vis.DataSet | null} items
*/
- Network.prototype._setEdges = function(edges) {
- var oldEdgesData = this.edgesData;
+ LineGraph.prototype.setItems = function(items) {
+ var me = this,
+ ids,
+ oldItemsData = this.itemsData;
- if (edges instanceof DataSet || edges instanceof DataView) {
- this.edgesData = edges;
- }
- else if (edges instanceof Array) {
- this.edgesData = new DataSet();
- this.edgesData.add(edges);
+ // replace the dataset
+ if (!items) {
+ this.itemsData = null;
}
- else if (!edges) {
- this.edgesData = new DataSet();
+ else if (items instanceof DataSet || items instanceof DataView) {
+ this.itemsData = items;
}
else {
- throw new TypeError('Array or DataSet expected');
+ throw new TypeError('Data must be an instance of DataSet or DataView');
}
- if (oldEdgesData) {
+ if (oldItemsData) {
// unsubscribe from old dataset
- util.forEach(this.edgesListeners, function (callback, event) {
- oldEdgesData.off(event, callback);
+ util.forEach(this.itemListeners, function (callback, event) {
+ oldItemsData.off(event, callback);
});
- }
- // remove drawn edges
- this.edges = {};
+ // remove all drawn items
+ ids = oldItemsData.getIds();
+ this._onRemove(ids);
+ }
- if (this.edgesData) {
+ if (this.itemsData) {
// subscribe to new dataset
- var me = this;
- util.forEach(this.edgesListeners, function (callback, event) {
- me.edgesData.on(event, callback);
+ var id = this.id;
+ util.forEach(this.itemListeners, function (callback, event) {
+ me.itemsData.on(event, callback, id);
});
- // draw all new nodes
- var ids = this.edgesData.getIds();
- this._addEdges(ids);
+ // add all new items
+ ids = this.itemsData.getIds();
+ this._onAdd(ids);
}
-
- this._reconnectEdges();
+ this._updateUngrouped();
+ this._updateGraph();
+ this.redraw();
};
/**
- * Add edges
- * @param {Number[] | String[]} ids
- * @private
+ * Set groups
+ * @param {vis.DataSet} groups
*/
- Network.prototype._addEdges = function (ids) {
- var edges = this.edges,
- edgesData = this.edgesData;
+ LineGraph.prototype.setGroups = function(groups) {
+ var me = this,
+ ids;
- for (var i = 0, len = ids.length; i < len; i++) {
- var id = ids[i];
+ // unsubscribe from current dataset
+ if (this.groupsData) {
+ util.forEach(this.groupListeners, function (callback, event) {
+ me.groupsData.unsubscribe(event, callback);
+ });
- var oldEdge = edges[id];
- if (oldEdge) {
- oldEdge.disconnect();
- }
+ // remove all drawn groups
+ ids = this.groupsData.getIds();
+ this.groupsData = null;
+ this._onRemoveGroups(ids); // note: this will cause a redraw
+ }
- var data = edgesData.get(id, {"showInternalIds" : true});
- edges[id] = new Edge(data, this, this.constants);
+ // replace the dataset
+ if (!groups) {
+ this.groupsData = null;
+ }
+ else if (groups instanceof DataSet || groups instanceof DataView) {
+ this.groupsData = groups;
+ }
+ else {
+ throw new TypeError('Data must be an instance of DataSet or DataView');
}
- this.moving = true;
- this._updateValueRange(edges);
- this._createBezierNodes();
- if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
- this._resetLevels();
- this._setupHierarchicalLayout();
+ if (this.groupsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.groupListeners, function (callback, event) {
+ me.groupsData.on(event, callback, id);
+ });
+
+ // draw all ms
+ ids = this.groupsData.getIds();
+ this._onAddGroups(ids);
}
- this._updateCalculationNodes();
+ this._onUpdate();
};
+
/**
- * Update existing edges, or create them when not yet existing
- * @param {Number[] | String[]} ids
+ * Update the datapoints
+ * @param [ids]
* @private
*/
- Network.prototype._updateEdges = function (ids) {
- var edges = this.edges,
- edgesData = this.edgesData;
- for (var i = 0, len = ids.length; i < len; i++) {
- var id = ids[i];
-
- var data = edgesData.get(id);
- var edge = edges[id];
- if (edge) {
- // update edge
- edge.disconnect();
- edge.setProperties(data, this.constants);
- edge.connect();
- }
- else {
- // create edge
- edge = new Edge(data, this, this.constants);
- this.edges[id] = edge;
- }
+ LineGraph.prototype._onUpdate = function(ids) {
+ this._updateUngrouped();
+ this._updateAllGroupData();
+ this._updateGraph();
+ this.redraw();
+ };
+ LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);};
+ LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);};
+ LineGraph.prototype._onUpdateGroups = function (groupIds) {
+ for (var i = 0; i < groupIds.length; i++) {
+ var group = this.groupsData.get(groupIds[i]);
+ this._updateGroup(group, groupIds[i]);
}
- this._createBezierNodes();
- if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
- this._resetLevels();
- this._setupHierarchicalLayout();
- }
- this.moving = true;
- this._updateValueRange(edges);
+ this._updateGraph();
+ this.redraw();
};
+ LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
- /**
- * Remove existing edges. Non existing ids will be ignored
- * @param {Number[] | String[]} ids
- * @private
- */
- Network.prototype._removeEdges = function (ids) {
- var edges = this.edges;
- for (var i = 0, len = ids.length; i < len; i++) {
- var id = ids[i];
- var edge = edges[id];
- if (edge) {
- if (edge.via != null) {
- delete this.sectors['support']['nodes'][edge.via.id];
+ LineGraph.prototype._onRemoveGroups = function (groupIds) {
+ for (var i = 0; i < groupIds.length; i++) {
+ if (!this.groups.hasOwnProperty(groupIds[i])) {
+ if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') {
+ this.yAxisRight.removeGroup(groupIds[i]);
+ this.legendRight.removeGroup(groupIds[i]);
+ this.legendRight.redraw();
}
- edge.disconnect();
- delete edges[id];
+ else {
+ this.yAxisLeft.removeGroup(groupIds[i]);
+ this.legendLeft.removeGroup(groupIds[i]);
+ this.legendLeft.redraw();
+ }
+ delete this.groups[groupIds[i]];
}
}
-
- this.moving = true;
- this._updateValueRange(edges);
- if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
- this._resetLevels();
- this._setupHierarchicalLayout();
- }
- this._updateCalculationNodes();
+ this._updateUngrouped();
+ this._updateGraph();
+ this.redraw();
};
/**
- * Reconnect all edges
+ * update a group object
+ *
+ * @param group
+ * @param groupId
* @private
*/
- Network.prototype._reconnectEdges = function() {
- var id,
- nodes = this.nodes,
- edges = this.edges;
- for (id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- nodes[id].edges = [];
+ LineGraph.prototype._updateGroup = function (group, groupId) {
+ if (!this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
+ if (this.groups[groupId].options.yAxisOrientation == 'right') {
+ this.yAxisRight.addGroup(groupId, this.groups[groupId]);
+ this.legendRight.addGroup(groupId, this.groups[groupId]);
+ }
+ else {
+ this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
+ this.legendLeft.addGroup(groupId, this.groups[groupId]);
}
}
-
- for (id in edges) {
- if (edges.hasOwnProperty(id)) {
- var edge = edges[id];
- edge.from = null;
- edge.to = null;
- edge.connect();
+ else {
+ this.groups[groupId].update(group);
+ if (this.groups[groupId].options.yAxisOrientation == 'right') {
+ this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
+ this.legendRight.updateGroup(groupId, this.groups[groupId]);
+ }
+ else {
+ this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
+ this.legendLeft.updateGroup(groupId, this.groups[groupId]);
}
}
+ this.legendLeft.redraw();
+ this.legendRight.redraw();
};
- /**
- * Update the values of all object in the given array according to the current
- * value range of the objects in the array.
- * @param {Object} obj An object containing a set of Edges or Nodes
- * The objects must have a method getValue() and
- * setValueRange(min, max).
- * @private
- */
- Network.prototype._updateValueRange = function(obj) {
- var id;
-
- // determine the range of the objects
- var valueMin = undefined;
- var valueMax = undefined;
- for (id in obj) {
- if (obj.hasOwnProperty(id)) {
- var value = obj[id].getValue();
- if (value !== undefined) {
- valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
- valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
+ LineGraph.prototype._updateAllGroupData = function () {
+ if (this.itemsData != null) {
+ var groupsContent = {};
+ var groupId;
+ for (groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ groupsContent[groupId] = [];
}
}
- }
-
- // adjust the range of all objects
- if (valueMin !== undefined && valueMax !== undefined) {
- for (id in obj) {
- if (obj.hasOwnProperty(id)) {
- obj[id].setValueRange(valueMin, valueMax);
+ for (var itemId in this.itemsData._data) {
+ if (this.itemsData._data.hasOwnProperty(itemId)) {
+ var item = this.itemsData._data[itemId];
+ item.x = util.convert(item.x,"Date");
+ groupsContent[item.group].push(item);
+ }
+ }
+ for (groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId].setItems(groupsContent[groupId]);
}
}
}
};
/**
- * Redraw the network with the current data
- * chart will be resized too.
+ * Create or delete the group holding all ungrouped items. This group is used when
+ * there are no groups specified. This anonymous group is called 'graph'.
+ * @protected
*/
- Network.prototype.redraw = function() {
- this.setSize(this.constants.width, this.constants.height);
- this._redraw();
- };
+ LineGraph.prototype._updateUngrouped = function() {
+ if (this.itemsData != null) {
+ // var t0 = new Date();
+ var group = {id: UNGROUPED, content: this.options.defaultGroup};
+ this._updateGroup(group, UNGROUPED);
+ var ungroupedCounter = 0;
+ if (this.itemsData) {
+ for (var itemId in this.itemsData._data) {
+ if (this.itemsData._data.hasOwnProperty(itemId)) {
+ var item = this.itemsData._data[itemId];
+ if (item != undefined) {
+ if (item.hasOwnProperty('group')) {
+ if (item.group === undefined) {
+ item.group = UNGROUPED;
+ }
+ }
+ else {
+ item.group = UNGROUPED;
+ }
+ ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter;
+ }
+ }
+ }
+ }
- /**
- * Redraw the network with the current data
- * @private
- */
- Network.prototype._redraw = function() {
- var ctx = this.frame.canvas.getContext('2d');
- // clear the canvas
- var w = this.frame.canvas.width;
- var h = this.frame.canvas.height;
- ctx.clearRect(0, 0, w, h);
-
- // set scaling and translation
- ctx.save();
- ctx.translate(this.translation.x, this.translation.y);
- ctx.scale(this.scale, this.scale);
-
- this.canvasTopLeft = {
- "x": this._XconvertDOMtoCanvas(0),
- "y": this._YconvertDOMtoCanvas(0)
- };
- this.canvasBottomRight = {
- "x": this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth),
- "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight)
- };
-
-
- this._doInAllSectors("_drawAllSectorNodes",ctx);
- if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) {
- this._doInAllSectors("_drawEdges",ctx);
- }
-
- if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) {
- this._doInAllSectors("_drawNodes",ctx,false);
+ if (ungroupedCounter == 0) {
+ delete this.groups[UNGROUPED];
+ this.legendLeft.removeGroup(UNGROUPED);
+ this.legendRight.removeGroup(UNGROUPED);
+ this.yAxisLeft.removeGroup(UNGROUPED);
+ this.yAxisRight.removeGroup(UNGROUPED);
+ }
}
-
- if (this.controlNodesActive == true) {
- this._doInAllSectors("_drawControlNodes",ctx);
+ else {
+ delete this.groups[UNGROUPED];
+ this.legendLeft.removeGroup(UNGROUPED);
+ this.legendRight.removeGroup(UNGROUPED);
+ this.yAxisLeft.removeGroup(UNGROUPED);
+ this.yAxisRight.removeGroup(UNGROUPED);
}
- // this._doInSupportSector("_drawNodes",ctx,true);
- // this._drawTree(ctx,"#F00F0F");
-
- // restore original scaling and translation
- ctx.restore();
+ this.legendLeft.redraw();
+ this.legendRight.redraw();
};
+
/**
- * Set the translation of the network
- * @param {Number} offsetX Horizontal offset
- * @param {Number} offsetY Vertical offset
- * @private
+ * Redraw the component, mandatory function
+ * @return {boolean} Returns true if the component is resized
*/
- Network.prototype._setTranslation = function(offsetX, offsetY) {
- if (this.translation === undefined) {
- this.translation = {
- x: 0,
- y: 0
- };
- }
+ LineGraph.prototype.redraw = function() {
+ var resized = false;
- if (offsetX !== undefined) {
- this.translation.x = offsetX;
- }
- if (offsetY !== undefined) {
- this.translation.y = offsetY;
+ this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
+ if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) {
+ resized = true;
}
+ // check if this component is resized
+ resized = this._isResized() || resized;
+ // check whether zoomed (in that case we need to re-stack everything)
+ var visibleInterval = this.body.range.end - this.body.range.start;
+ var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
+ this.lastVisibleInterval = visibleInterval;
+ this.lastWidth = this.width;
- this.emit('viewChanged');
- };
-
- /**
- * Get the translation of the network
- * @return {Object} translation An object with parameters x and y, both a number
- * @private
- */
- Network.prototype._getTranslation = function() {
- return {
- x: this.translation.x,
- y: this.translation.y
- };
- };
-
- /**
- * Scale the network
- * @param {Number} scale Scaling factor 1.0 is unscaled
- * @private
- */
- Network.prototype._setScale = function(scale) {
- this.scale = scale;
- };
+ // calculate actual size and position
+ this.width = this.dom.frame.offsetWidth;
- /**
- * Get the current scale of the network
- * @return {Number} scale Scaling factor 1.0 is unscaled
- * @private
- */
- Network.prototype._getScale = function() {
- return this.scale;
- };
+ // the svg element is three times as big as the width, this allows for fully dragging left and right
+ // without reloading the graph. the controls for this are bound to events in the constructor
+ if (resized == true) {
+ this.svg.style.width = util.option.asSize(3*this.width);
+ this.svg.style.left = util.option.asSize(-this.width);
+ }
+ if (zoomed == true || this.abortedGraphUpdate == true) {
+ this._updateGraph();
+ }
- /**
- * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
- * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
- * @param {number} x
- * @returns {number}
- * @private
- */
- Network.prototype._XconvertDOMtoCanvas = function(x) {
- return (x - this.translation.x) / this.scale;
- };
+ this.legendLeft.redraw();
+ this.legendRight.redraw();
- /**
- * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
- * the X coordinate in DOM-space (coordinate point in browser relative to the container div)
- * @param {number} x
- * @returns {number}
- * @private
- */
- Network.prototype._XconvertCanvasToDOM = function(x) {
- return x * this.scale + this.translation.x;
+ return resized;
};
/**
- * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to
- * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
- * @param {number} y
- * @returns {number}
- * @private
+ * Update and redraw the graph.
+ *
*/
- Network.prototype._YconvertDOMtoCanvas = function(y) {
- return (y - this.translation.y) / this.scale;
- };
+ LineGraph.prototype._updateGraph = function () {
+ // reset the svg elements
+ DOMutil.prepareElements(this.svgElements);
+ if (this.width != 0 && this.itemsData != null) {
+ var group, i;
+ var preprocessedGroupData = {};
+ var processedGroupData = {};
+ var groupRanges = {};
+ var changeCalled = false;
- /**
- * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
- * the Y coordinate in DOM-space (coordinate point in browser relative to the container div)
- * @param {number} y
- * @returns {number}
- * @private
- */
- Network.prototype._YconvertCanvasToDOM = function(y) {
- return y * this.scale + this.translation.y ;
- };
+ // getting group Ids
+ var groupIds = [];
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ group = this.groups[groupId];
+ if (group.visible == true && (this.options.groups.visibility[groupId] === undefined || this.options.groups.visibility[groupId] == true)) {
+ groupIds.push(groupId);
+ }
+ }
+ }
+ if (groupIds.length > 0) {
+ // this is the range of the SVG canvas
+ var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width);
+ var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
+ var groupsData = {};
+ // fill groups data
+ this._getRelevantData(groupIds, groupsData, minDate, maxDate);
+ // we transform the X coordinates to detect collisions
+ for (i = 0; i < groupIds.length; i++) {
+ preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]);
+ }
+ // now all needed data has been collected we start the processing.
+ this._getYRanges(groupIds, preprocessedGroupData, groupRanges);
+ // update the Y axis first, we use this data to draw at the correct Y points
+ // changeCalled is required to clean the SVG on a change emit.
+ changeCalled = this._updateYAxis(groupIds, groupRanges);
+ if (changeCalled == true) {
+ DOMutil.cleanupElements(this.svgElements);
+ this.abortedGraphUpdate = true;
+ this.body.emitter.emit("change");
+ return;
+ }
+ this.abortedGraphUpdate = false;
- /**
- *
- * @param {object} pos = {x: number, y: number}
- * @returns {{x: number, y: number}}
- * @constructor
- */
- Network.prototype.canvasToDOM = function(pos) {
- return {x:this._XconvertCanvasToDOM(pos.x),y:this._YconvertCanvasToDOM(pos.y)};
- }
+ // With the yAxis scaled correctly, use this to get the Y values of the points.
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ processedGroupData[groupIds[i]] = this._convertYcoordinates(groupsData[groupIds[i]], group);
+ }
- /**
- *
- * @param {object} pos = {x: number, y: number}
- * @returns {{x: number, y: number}}
- * @constructor
- */
- Network.prototype.DOMtoCanvas = function(pos) {
- return {x:this._XconvertDOMtoCanvas(pos.x),y:this._YconvertDOMtoCanvas(pos.y)};
- }
- /**
- * Redraw all nodes
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
- * @param {CanvasRenderingContext2D} ctx
- * @param {Boolean} [alwaysShow]
- * @private
- */
- Network.prototype._drawNodes = function(ctx,alwaysShow) {
- if (alwaysShow === undefined) {
- alwaysShow = false;
+ // draw the groups
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ if (group.options.style == 'line') {
+ this._drawLineGraph(processedGroupData[groupIds[i]], group);
+ }
+ }
+ this._drawBarGraphs(groupIds, processedGroupData);
+ }
}
- // first draw the unselected nodes
- var nodes = this.nodes;
- var selected = [];
+ // cleanup unused svg elements
+ DOMutil.cleanupElements(this.svgElements);
+ };
- for (var id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
- if (nodes[id].isSelected()) {
- selected.push(id);
+
+ LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) {
+ // first select and preprocess the data from the datasets.
+ // the groups have their preselection of data, we now loop over this data to see
+ // what data we need to draw. Sorted data is much faster.
+ // more optimization is possible by doing the sampling before and using the binary search
+ // to find the end date to determine the increment.
+ var group, i, j, item;
+ if (groupIds.length > 0) {
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ groupsData[groupIds[i]] = [];
+ var dataContainer = groupsData[groupIds[i]];
+ // optimization for sorted data
+ if (group.options.sort == true) {
+ var guess = Math.max(0, util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before'));
+ for (j = guess; j < group.itemsData.length; j++) {
+ item = group.itemsData[j];
+ if (item !== undefined) {
+ if (item.x > maxDate) {
+ dataContainer.push(item);
+ break;
+ }
+ else {
+ dataContainer.push(item);
+ }
+ }
+ }
}
else {
- if (nodes[id].inArea() || alwaysShow) {
- nodes[id].draw(ctx);
+ for (j = 0; j < group.itemsData.length; j++) {
+ item = group.itemsData[j];
+ if (item !== undefined) {
+ if (item.x > minDate && item.x < maxDate) {
+ dataContainer.push(item);
+ }
+ }
}
}
}
}
- // draw the selected nodes on top
- for (var s = 0, sMax = selected.length; s < sMax; s++) {
- if (nodes[selected[s]].inArea() || alwaysShow) {
- nodes[selected[s]].draw(ctx);
- }
- }
+ this._applySampling(groupIds, groupsData);
};
- /**
- * Redraw all edges
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
- * @param {CanvasRenderingContext2D} ctx
- * @private
- */
- Network.prototype._drawEdges = function(ctx) {
- var edges = this.edges;
- for (var id in edges) {
- if (edges.hasOwnProperty(id)) {
- var edge = edges[id];
- edge.setScale(this.scale);
- if (edge.connected) {
- edges[id].draw(ctx);
- }
- }
- }
- };
+ LineGraph.prototype._applySampling = function (groupIds, groupsData) {
+ var group;
+ if (groupIds.length > 0) {
+ for (var i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ if (group.options.sampling == true) {
+ var dataContainer = groupsData[groupIds[i]];
+ if (dataContainer.length > 0) {
+ var increment = 1;
+ var amountOfPoints = dataContainer.length;
- /**
- * Redraw all edges
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
- * @param {CanvasRenderingContext2D} ctx
- * @private
- */
- Network.prototype._drawControlNodes = function(ctx) {
- var edges = this.edges;
- for (var id in edges) {
- if (edges.hasOwnProperty(id)) {
- edges[id]._drawControlNodes(ctx);
+ // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
+ // of width changing of the yAxis.
+ var xDistance = this.body.util.toGlobalScreen(dataContainer[dataContainer.length - 1].x) - this.body.util.toGlobalScreen(dataContainer[0].x);
+ var pointsPerPixel = amountOfPoints / xDistance;
+ increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1, Math.round(pointsPerPixel)));
+
+ var sampledData = [];
+ for (var j = 0; j < amountOfPoints; j += increment) {
+ sampledData.push(dataContainer[j]);
+
+ }
+ groupsData[groupIds[i]] = sampledData;
+ }
+ }
}
}
};
- /**
- * Find a stable position for all nodes
- * @private
- */
- Network.prototype._stabilize = function() {
- if (this.constants.freezeForStabilization == true) {
- this._freezeDefinedNodes();
- }
+ LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) {
+ var groupData, group, i,j;
+ var barCombinedDataLeft = [];
+ var barCombinedDataRight = [];
+ var barCombinedData;
+ if (groupIds.length > 0) {
+ for (i = 0; i < groupIds.length; i++) {
+ groupData = groupsData[groupIds[i]];
+ if (groupData.length > 0) {
+ group = this.groups[groupIds[i]];
+ if (group.options.style == 'line' || group.options.barChart.handleOverlap != "stack") {
+ var yMin = groupData[0].y;
+ var yMax = groupData[0].y;
+ for (j = 0; j < groupData.length; j++) {
+ yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
+ yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
+ }
+ groupRanges[groupIds[i]] = {min: yMin, max: yMax, yAxisOrientation: group.options.yAxisOrientation};
+ }
+ else if (group.options.style == 'bar') {
+ if (group.options.yAxisOrientation == 'left') {
+ barCombinedData = barCombinedDataLeft;
+ }
+ else {
+ barCombinedData = barCombinedDataRight;
+ }
- // find stable position
- var count = 0;
- while (this.moving && count < this.constants.stabilizationIterations) {
- this._physicsTick();
- count++;
- }
- this.zoomExtent(false,true);
- if (this.constants.freezeForStabilization == true) {
- this._restoreFrozenNodes();
- }
- this.emit("stabilized",{iterations:count});
- };
+ groupRanges[groupIds[i]] = {min: 0, max: 0, yAxisOrientation: group.options.yAxisOrientation, ignore: true};
- /**
- * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
- * because only the supportnodes for the smoothCurves have to settle.
- *
- * @private
- */
- Network.prototype._freezeDefinedNodes = function() {
- var nodes = this.nodes;
- for (var id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- if (nodes[id].x != null && nodes[id].y != null) {
- nodes[id].fixedData.x = nodes[id].xFixed;
- nodes[id].fixedData.y = nodes[id].yFixed;
- nodes[id].xFixed = true;
- nodes[id].yFixed = true;
+ // combine data
+ for (j = 0; j < groupData.length; j++) {
+ barCombinedData.push({
+ x: groupData[j].x,
+ y: groupData[j].y,
+ groupId: groupIds[i]
+ });
+ }
+ }
}
}
- }
- };
- /**
- * Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
- *
- * @private
- */
- Network.prototype._restoreFrozenNodes = function() {
- var nodes = this.nodes;
- for (var id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- if (nodes[id].fixedData.x != null) {
- nodes[id].xFixed = nodes[id].fixedData.x;
- nodes[id].yFixed = nodes[id].fixedData.y;
- }
+ var intersections;
+ if (barCombinedDataLeft.length > 0) {
+ // sort by time and by group
+ barCombinedDataLeft.sort(function (a, b) {
+ if (a.x == b.x) {
+ return a.groupId - b.groupId;
+ } else {
+ return a.x - b.x;
+ }
+ });
+ intersections = {};
+ this._getDataIntersections(intersections, barCombinedDataLeft);
+ groupRanges["__barchartLeft"] = this._getStackedBarYRange(intersections, barCombinedDataLeft);
+ groupRanges["__barchartLeft"].yAxisOrientation = "left";
+ groupIds.push("__barchartLeft");
+ }
+ if (barCombinedDataRight.length > 0) {
+ // sort by time and by group
+ barCombinedDataRight.sort(function (a, b) {
+ if (a.x == b.x) {
+ return a.groupId - b.groupId;
+ } else {
+ return a.x - b.x;
+ }
+ });
+ intersections = {};
+ this._getDataIntersections(intersections, barCombinedDataRight);
+ groupRanges["__barchartRight"] = this._getStackedBarYRange(intersections, barCombinedDataRight);
+ groupRanges["__barchartRight"].yAxisOrientation = "right";
+ groupIds.push("__barchartRight");
}
}
};
-
- /**
- * Check if any of the nodes is still moving
- * @param {number} vmin the minimum velocity considered as 'moving'
- * @return {boolean} true if moving, false if non of the nodes is moving
- * @private
- */
- Network.prototype._isMoving = function(vmin) {
- var nodes = this.nodes;
- for (var id in nodes) {
- if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
- return true;
+ LineGraph.prototype._getStackedBarYRange = function (intersections, combinedData) {
+ var key;
+ var yMin = combinedData[0].y;
+ var yMax = combinedData[0].y;
+ for (var i = 0; i < combinedData.length; i++) {
+ key = combinedData[i].x;
+ if (intersections[key] === undefined) {
+ yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin;
+ yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax;
+ }
+ else {
+ intersections[key].accumulated += combinedData[i].y;
}
}
- return false;
+ for (var xpos in intersections) {
+ if (intersections.hasOwnProperty(xpos)) {
+ yMin = yMin > intersections[xpos].accumulated ? intersections[xpos].accumulated : yMin;
+ yMax = yMax < intersections[xpos].accumulated ? intersections[xpos].accumulated : yMax;
+ }
+ }
+
+ return {min: yMin, max: yMax};
};
/**
- * /**
- * Perform one discrete step for all nodes
- *
+ * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
+ * @param {Array} groupIds
+ * @param {Object} groupRanges
* @private
*/
- Network.prototype._discreteStepNodes = function(checkMovement) {
- var interval = this.physicsDiscreteStepsize;
- var nodes = this.nodes;
- var nodeId;
- var nodesPresent = false;
+ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
+ var changeCalled = false;
+ var yAxisLeftUsed = false;
+ var yAxisRightUsed = false;
+ var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
+ // if groups are present
+ if (groupIds.length > 0) {
+ for (var i = 0; i < groupIds.length; i++) {
+ if (groupRanges.hasOwnProperty(groupIds[i])) {
+ if (groupRanges[groupIds[i]].ignore !== true) {
+ minVal = groupRanges[groupIds[i]].min;
+ maxVal = groupRanges[groupIds[i]].max;
- if (this.constants.maxVelocity > 0) {
- for (nodeId in nodes) {
- if (nodes.hasOwnProperty(nodeId)) {
- nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
- nodesPresent = true;
+ if (groupRanges[groupIds[i]].yAxisOrientation == 'left') {
+ yAxisLeftUsed = true;
+ minLeft = minLeft > minVal ? minVal : minLeft;
+ maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
+ }
+ else {
+ yAxisRightUsed = true;
+ minRight = minRight > minVal ? minVal : minRight;
+ maxRight = maxRight < maxVal ? maxVal : maxRight;
+ }
+ }
}
}
+
+ if (yAxisLeftUsed == true) {
+ this.yAxisLeft.setRange(minLeft, maxLeft);
+ }
+ if (yAxisRightUsed == true) {
+ this.yAxisRight.setRange(minRight, maxRight);
+ }
+ }
+
+ changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled;
+ changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled;
+
+ if (yAxisRightUsed == true && yAxisLeftUsed == true) {
+ this.yAxisLeft.drawIcons = true;
+ this.yAxisRight.drawIcons = true;
}
else {
- for (nodeId in nodes) {
- if (nodes.hasOwnProperty(nodeId)) {
- nodes[nodeId].discreteStep(interval);
- nodesPresent = true;
- }
- }
+ this.yAxisLeft.drawIcons = false;
+ this.yAxisRight.drawIcons = false;
}
- if (nodesPresent == true && (checkMovement === undefined || checkMovement == true)) {
- var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
- if (vminCorrected > 0.5*this.constants.maxVelocity) {
- this.moving = true;
- }
- else {
- this.moving = this._isMoving(vminCorrected);
- if (this.moving == false) {
- this.emit("stabilized",{iterations:null});
- }
- this.moving = this.moving || this.configurePhysics;
+ this.yAxisRight.master = !yAxisLeftUsed;
- }
+ if (this.yAxisRight.master == false) {
+ if (yAxisRightUsed == true) {this.yAxisLeft.lineOffset = this.yAxisRight.width;}
+ else {this.yAxisLeft.lineOffset = 0;}
+
+ changeCalled = this.yAxisLeft.redraw() || changeCalled;
+ this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels;
+ changeCalled = this.yAxisRight.redraw() || changeCalled;
+ }
+ else {
+ changeCalled = this.yAxisRight.redraw() || changeCalled;
+ }
+
+ // clean the accumulated lists
+ if (groupIds.indexOf("__barchartLeft") != -1) {
+ groupIds.splice(groupIds.indexOf("__barchartLeft"),1);
}
+ if (groupIds.indexOf("__barchartRight") != -1) {
+ groupIds.splice(groupIds.indexOf("__barchartRight"),1);
+ }
+
+ return changeCalled;
};
/**
- * A single simulation step (or "tick") in the physics simulation
+ * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
*
+ * @param {boolean} axisUsed
+ * @returns {boolean}
* @private
+ * @param axis
*/
- Network.prototype._physicsTick = function() {
- if (!this.freezeSimulation) {
- if (this.moving == true) {
- this._doInAllActiveSectors("_initializeForceCalculation");
- this._doInAllActiveSectors("_discreteStepNodes");
- if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
- this._doInSupportSector("_discreteStepNodes", false);
- }
- this._findCenter(this._getRange())
+ LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
+ var changed = false;
+ if (axisUsed == false) {
+ if (axis.dom.frame.parentNode) {
+ axis.hide();
+ changed = true;
+ }
+ }
+ else {
+ if (!axis.dom.frame.parentNode) {
+ axis.show();
+ changed = true;
}
}
+ return changed;
};
/**
- * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
- * It reschedules itself at the beginning of the function
+ * draw a bar graph
*
- * @private
+ * @param groupIds
+ * @param processedGroupData
*/
- Network.prototype._animationStep = function() {
- // reset the timer so a new scheduled animation step can be set
- this.timer = undefined;
- // handle the keyboad movement
- this._handleNavigation();
-
- // this schedules a new animation step
- this.start();
+ LineGraph.prototype._drawBarGraphs = function (groupIds, processedGroupData) {
+ var combinedData = [];
+ var intersections = {};
+ var coreDistance;
+ var key, drawData;
+ var group;
+ var i,j;
+ var barPoints = 0;
- // start the physics simulation
- var calculationTime = Date.now();
- var maxSteps = 1;
- this._physicsTick();
- var timeRequired = Date.now() - calculationTime;
- while (timeRequired < 0.9*(this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
- this._physicsTick();
- timeRequired = Date.now() - calculationTime;
- maxSteps++;
+ // combine all barchart data
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ if (group.options.style == 'bar') {
+ if (group.visible == true && (this.options.groups.visibility[groupIds[i]] === undefined || this.options.groups.visibility[groupIds[i]] == true)) {
+ for (j = 0; j < processedGroupData[groupIds[i]].length; j++) {
+ combinedData.push({
+ x: processedGroupData[groupIds[i]][j].x,
+ y: processedGroupData[groupIds[i]][j].y,
+ groupId: groupIds[i]
+ });
+ barPoints += 1;
+ }
+ }
+ }
}
- // start the rendering process
- var renderTime = Date.now();
- this._redraw();
- this.renderTime = Date.now() - renderTime;
- };
+ if (barPoints == 0) {return;}
- if (typeof window !== 'undefined') {
- window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
- window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
- }
+ // sort by time and by group
+ combinedData.sort(function (a, b) {
+ if (a.x == b.x) {
+ return a.groupId - b.groupId;
+ } else {
+ return a.x - b.x;
+ }
+ });
- /**
- * Schedule a animation step with the refreshrate interval.
- */
- Network.prototype.start = function() {
- if (this.moving == true || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
- if (!this.timer) {
- var ua = navigator.userAgent.toLowerCase();
+ // get intersections
+ this._getDataIntersections(intersections, combinedData);
- var requiresTimeout = false;
- if (ua.indexOf('msie 9.0') != -1) { // IE 9
- requiresTimeout = true;
- }
- else if (ua.indexOf('safari') != -1) { // safari
- if (ua.indexOf('chrome') <= -1) {
- requiresTimeout = true;
- }
- }
+ // plot barchart
+ for (i = 0; i < combinedData.length; i++) {
+ group = this.groups[combinedData[i].groupId];
+ var minWidth = 0.1 * group.options.barChart.width;
- if (requiresTimeout == true) {
- this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
+ key = combinedData[i].x;
+ var heightOffset = 0;
+ if (intersections[key] === undefined) {
+ if (i+1 < combinedData.length) {coreDistance = Math.abs(combinedData[i+1].x - key);}
+ if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[i-1].x - key));}
+ drawData = this._getSafeDrawData(coreDistance, group, minWidth);
+ }
+ else {
+ var nextKey = i + (intersections[key].amount - intersections[key].resolved);
+ var prevKey = i - (intersections[key].resolved + 1);
+ if (nextKey < combinedData.length) {coreDistance = Math.abs(combinedData[nextKey].x - key);}
+ if (prevKey > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[prevKey].x - key));}
+ drawData = this._getSafeDrawData(coreDistance, group, minWidth);
+ intersections[key].resolved += 1;
+
+ if (group.options.barChart.handleOverlap == 'stack') {
+ heightOffset = intersections[key].accumulated;
+ intersections[key].accumulated += group.zeroPosition - combinedData[i].y;
}
- else{
- this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
+ else if (group.options.barChart.handleOverlap == 'sideBySide') {
+ drawData.width = drawData.width / intersections[key].amount;
+ drawData.offset += (intersections[key].resolved) * drawData.width - (0.5*drawData.width * (intersections[key].amount+1));
+ if (group.options.barChart.align == 'left') {drawData.offset -= 0.5*drawData.width;}
+ else if (group.options.barChart.align == 'right') {drawData.offset += 0.5*drawData.width;}
}
}
- }
- else {
- this._redraw();
+ DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', this.svgElements, this.svg);
+ // draw points
+ if (group.options.drawPoints.enabled == true) {
+ DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, group, this.svgElements, this.svg);
+ }
}
};
-
/**
- * Move the network according to the keyboard presses.
- *
+ * Fill the intersections object with counters of how many datapoints share the same x coordinates
+ * @param intersections
+ * @param combinedData
* @private
*/
- Network.prototype._handleNavigation = function() {
- if (this.xIncrement != 0 || this.yIncrement != 0) {
- var translation = this._getTranslation();
- this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
- }
- if (this.zoomIncrement != 0) {
- var center = {
- x: this.frame.canvas.clientWidth / 2,
- y: this.frame.canvas.clientHeight / 2
- };
- this._zoom(this.scale*(1 + this.zoomIncrement), center);
- }
- };
-
-
- /**
- * Freeze the _animationStep
- */
- Network.prototype.toggleFreeze = function() {
- if (this.freezeSimulation == false) {
- this.freezeSimulation = true;
- }
- else {
- this.freezeSimulation = false;
- this.start();
+ LineGraph.prototype._getDataIntersections = function (intersections, combinedData) {
+ // get intersections
+ var coreDistance;
+ for (var i = 0; i < combinedData.length; i++) {
+ if (i + 1 < combinedData.length) {
+ coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x);
+ }
+ if (i > 0) {
+ coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x));
+ }
+ if (coreDistance == 0) {
+ if (intersections[combinedData[i].x] === undefined) {
+ intersections[combinedData[i].x] = {amount: 0, resolved: 0, accumulated: 0};
+ }
+ intersections[combinedData[i].x].amount += 1;
+ }
}
};
-
/**
- * This function cleans the support nodes if they are not needed and adds them when they are.
+ * Get the width and offset for bargraphs based on the coredistance between datapoints
*
- * @param {boolean} [disableStart]
+ * @param coreDistance
+ * @param group
+ * @param minWidth
+ * @returns {{width: Number, offset: Number}}
* @private
*/
- Network.prototype._configureSmoothCurves = function(disableStart) {
- if (disableStart === undefined) {
- disableStart = true;
- }
- if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
- this._createBezierNodes();
- // cleanup unused support nodes
- for (var nodeId in this.sectors['support']['nodes']) {
- if (this.sectors['support']['nodes'].hasOwnProperty(nodeId)) {
- if (this.edges[this.sectors['support']['nodes'][nodeId].parentEdgeId] === undefined) {
- delete this.sectors['support']['nodes'][nodeId];
- }
- }
+ LineGraph.prototype._getSafeDrawData = function (coreDistance, group, minWidth) {
+ var width, offset;
+ if (coreDistance < group.options.barChart.width && coreDistance > 0) {
+ width = coreDistance < minWidth ? minWidth : coreDistance;
+
+ offset = 0; // recalculate offset with the new width;
+ if (group.options.barChart.align == 'left') {
+ offset -= 0.5 * coreDistance;
+ }
+ else if (group.options.barChart.align == 'right') {
+ offset += 0.5 * coreDistance;
}
}
else {
- // delete the support nodes
- this.sectors['support']['nodes'] = {};
- for (var edgeId in this.edges) {
- if (this.edges.hasOwnProperty(edgeId)) {
- this.edges[edgeId].via = null;
- }
+ // default settings
+ width = group.options.barChart.width;
+ offset = 0;
+ if (group.options.barChart.align == 'left') {
+ offset -= 0.5 * group.options.barChart.width;
+ }
+ else if (group.options.barChart.align == 'right') {
+ offset += 0.5 * group.options.barChart.width;
}
}
-
- this._updateCalculationNodes();
- if (!disableStart) {
- this.moving = true;
- this.start();
- }
+ return {width: width, offset: offset};
};
/**
- * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
- * are used for the force calculation.
+ * draw a line graph
*
- * @private
+ * @param dataset
+ * @param group
*/
- Network.prototype._createBezierNodes = function() {
- if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
- for (var edgeId in this.edges) {
- if (this.edges.hasOwnProperty(edgeId)) {
- var edge = this.edges[edgeId];
- if (edge.via == null) {
- var nodeId = "edgeId:".concat(edge.id);
- this.sectors['support']['nodes'][nodeId] = new Node(
- {id:nodeId,
- mass:1,
- shape:'circle',
- image:"",
- internalMultiplier:1
- },{},{},this.constants);
- edge.via = this.sectors['support']['nodes'][nodeId];
- edge.via.parentEdgeId = edge.id;
- edge.positionBezierNode();
+ LineGraph.prototype._drawLineGraph = function (dataset, group) {
+ if (dataset != null) {
+ if (dataset.length > 0) {
+ var path, d;
+ var svgHeight = Number(this.svg.style.height.replace("px",""));
+ path = DOMutil.getSVGElement('path', this.svgElements, this.svg);
+ path.setAttributeNS(null, "class", group.className);
+
+ // construct path from dataset
+ if (group.options.catmullRom.enabled == true) {
+ d = this._catmullRom(dataset, group);
+ }
+ else {
+ d = this._linear(dataset);
+ }
+
+ // append with points for fill and finalize the path
+ if (group.options.shaded.enabled == true) {
+ var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg);
+ var dFill;
+ if (group.options.shaded.orientation == 'top') {
+ dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0;
+ }
+ else {
+ dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight;
}
+ fillPath.setAttributeNS(null, "class", group.className + " fill");
+ fillPath.setAttributeNS(null, "d", dFill);
+ }
+ // copy properties to path for drawing.
+ path.setAttributeNS(null, "d", "M" + d);
+
+ // draw points
+ if (group.options.drawPoints.enabled == true) {
+ this._drawPoints(dataset, group, this.svgElements, this.svg);
}
}
}
};
/**
- * load the functions that load the mixins into the prototype.
+ * draw the data points
*
- * @private
+ * @param {Array} dataset
+ * @param {Object} JSONcontainer
+ * @param {Object} svg | SVG DOM element
+ * @param {GraphGroup} group
+ * @param {Number} [offset]
*/
- Network.prototype._initializeMixinLoaders = function () {
- for (var mixin in MixinLoader) {
- if (MixinLoader.hasOwnProperty(mixin)) {
- Network.prototype[mixin] = MixinLoader[mixin];
- }
+ LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) {
+ if (offset === undefined) {offset = 0;}
+ for (var i = 0; i < dataset.length; i++) {
+ DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, JSONcontainer, svg);
}
};
+
+
/**
- * Load the XY positions of the nodes into the dataset.
+ * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
+ * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
+ * the yAxis.
+ *
+ * @param datapoints
+ * @returns {Array}
+ * @private
*/
- Network.prototype.storePosition = function() {
- var dataArray = [];
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- var node = this.nodes[nodeId];
- var allowedToMoveX = !this.nodes.xFixed;
- var allowedToMoveY = !this.nodes.yFixed;
- if (this.nodesData._data[nodeId].x != Math.round(node.x) || this.nodesData._data[nodeId].y != Math.round(node.y)) {
- dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
- }
- }
+ LineGraph.prototype._convertXcoordinates = function (datapoints) {
+ var extractedData = [];
+ var xValue, yValue;
+ var toScreen = this.body.util.toScreen;
+
+ for (var i = 0; i < datapoints.length; i++) {
+ xValue = toScreen(datapoints[i].x) + this.width - 1;
+ yValue = datapoints[i].y;
+ extractedData.push({x: xValue, y: yValue});
}
- this.nodesData.update(dataArray);
+
+ return extractedData;
};
+
/**
- * Center a node in view.
+ * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
+ * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
+ * the yAxis.
*
- * @param {Number} nodeId
- * @param {Number} [zoomLevel]
+ * @param datapoints
+ * @returns {Array}
+ * @private
*/
- Network.prototype.focusOnNode = function (nodeId, zoomLevel) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- if (zoomLevel === undefined) {
- zoomLevel = this._getScale();
- }
- var nodePosition= {x: this.nodes[nodeId].x, y: this.nodes[nodeId].y};
-
- var requiredScale = zoomLevel;
- this._setScale(requiredScale);
+ LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
+ var extractedData = [];
+ var xValue, yValue;
+ var toScreen = this.body.util.toScreen;
+ var axis = this.yAxisLeft;
+ var svgHeight = Number(this.svg.style.height.replace("px",""));
+ if (group.options.yAxisOrientation == 'right') {
+ axis = this.yAxisRight;
+ }
- var canvasCenter = this.DOMtoCanvas({x:0.5 * this.frame.canvas.width,y:0.5 * this.frame.canvas.height});
- var translation = this._getTranslation();
+ for (var i = 0; i < datapoints.length; i++) {
+ xValue = toScreen(datapoints[i].x) + this.width - 1;
+ yValue = Math.round(axis.convertValue(datapoints[i].y));
+ extractedData.push({x: xValue, y: yValue});
+ }
- var distanceFromCenter = {x:canvasCenter.x - nodePosition.x,
- y:canvasCenter.y - nodePosition.y};
+ group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
- this._setTranslation(translation.x + requiredScale * distanceFromCenter.x,
- translation.y + requiredScale * distanceFromCenter.y);
- this.redraw();
- }
- else {
- console.log("This nodeId cannot be found.")
- }
+ return extractedData;
};
/**
- * Returns true when the Timeline is active.
- * @returns {boolean}
+ * This uses an uniform parametrization of the CatmullRom algorithm:
+ * "On the Parameterization of Catmull-Rom Curves" by Cem Yuksel et al.
+ * @param data
+ * @returns {string}
+ * @private
*/
- Network.prototype.isActive = function () {
- return !this.activator || this.activator.active;
- };
+ LineGraph.prototype._catmullRomUniform = function(data) {
+ // catmull rom
+ var p0, p1, p2, p3, bp1, bp2;
+ var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
+ var normalization = 1/6;
+ var length = data.length;
+ for (var i = 0; i < length - 1; i++) {
- module.exports = Network;
+ p0 = (i == 0) ? data[0] : data[i-1];
+ p1 = data[i];
+ p2 = data[i+1];
+ p3 = (i + 2 < length) ? data[i+2] : p2;
-/***/ },
-/* 33 */
-/***/ function(module, exports, __webpack_require__) {
+ // Catmull-Rom to Cubic Bezier conversion matrix
+ // 0 1 0 0
+ // -1/6 1 1/6 0
+ // 0 1/6 1 -1/6
+ // 0 0 1 0
- var util = __webpack_require__(1);
- var Node = __webpack_require__(36);
+ // bp0 = { x: p1.x, y: p1.y };
+ bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
+ bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
+ // bp0 = { x: p2.x, y: p2.y };
- /**
- * @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 Edge (properties, network, networkConstants) {
- if (!network) {
- throw "No network provided";
+ d += "C" +
+ bp1.x + "," +
+ bp1.y + " " +
+ bp2.x + "," +
+ bp2.y + " " +
+ p2.x + "," +
+ p2.y + " ";
}
- 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.from = null; // a node
- this.to = null; // a node
- this.via = null; // a temp node
-
- // 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;
- }
+ return d;
+ };
/**
- * Set or overwrite properties for the edge
- * @param {Object} properties an object with properties
- * @param {Object} constants and object with default, global properties
+ * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
+ * By default, the centripetal parameterization is used because this gives the nicest results.
+ * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
+ *
+ * One optimization can be used to reuse distances since this is a sliding window approach.
+ * @param data
+ * @returns {string}
+ * @private
*/
- Edge.prototype.setProperties = function(properties) {
- if (!properties) {
- return;
+ LineGraph.prototype._catmullRom = function(data, group) {
+ var alpha = group.options.catmullRom.alpha;
+ if (alpha == 0 || alpha === undefined) {
+ return this._catmullRomUniform(data);
}
+ else {
+ var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
+ var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
+ var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
+ var length = data.length;
+ for (var i = 0; i < length - 1; i++) {
- var fields = ['style','fontSize','fontFace','fontColor','fontFill','width',
- 'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash'
- ];
- util.selectiveDeepExtend(fields, this.options, properties);
-
- if (properties.from !== undefined) {this.fromId = properties.from;}
- if (properties.to !== undefined) {this.toId = properties.to;}
+ p0 = (i == 0) ? data[0] : data[i-1];
+ p1 = data[i];
+ p2 = data[i+1];
+ p3 = (i + 2 < length) ? data[i+2] : p2;
- if (properties.id !== undefined) {this.id = properties.id;}
- if (properties.label !== undefined) {this.label = properties.label;}
+ d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
+ d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
+ d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
- 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;}
+ // Catmull-Rom to Cubic Bezier conversion matrix
+ //
+ // A = 2d1^2a + 3d1^a * d2^a + d3^2a
+ // B = 2d3^2a + 3d3^a * d2^a + d2^2a
+ //
+ // [ 0 1 0 0 ]
+ // [ -d2^2a/N A/N d1^2a/N 0 ]
+ // [ 0 d3^2a/M B/M -d2^2a/M ]
+ // [ 0 0 1 0 ]
- // scale the arrow
- if (properties.arrowScaleFactor !== undefined) {this.options.arrowScaleFactor = properties.arrowScaleFactor;}
+ // [ 0 1 0 0 ]
+ // [ -d2pow2a/N A/N d1pow2a/N 0 ]
+ // [ 0 d3pow2a/M B/M -d2pow2a/M ]
+ // [ 0 0 1 0 ]
- if (properties.inheritColor !== undefined) {this.options.inheritColor = properties.inheritColor;}
+ d3powA = Math.pow(d3, alpha);
+ d3pow2A = Math.pow(d3,2*alpha);
+ d2powA = Math.pow(d2, alpha);
+ d2pow2A = Math.pow(d2,2*alpha);
+ d1powA = Math.pow(d1, alpha);
+ d1pow2A = Math.pow(d1,2*alpha);
- 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 = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
+ B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
+ N = 3*d1powA * (d1powA + d2powA);
+ if (N > 0) {N = 1 / N;}
+ M = 3*d3powA * (d3powA + d2powA);
+ if (M > 0) {M = 1 / M;}
- // A node is connected when it has a from and to node.
- this.connect();
+ bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
+ y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
- this.widthFixed = this.widthFixed || (properties.width !== undefined);
- this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
+ bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
+ y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
- this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
+ if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
+ if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
+ d += "C" +
+ bp1.x + "," +
+ bp1.y + " " +
+ bp2.x + "," +
+ bp2.y + " " +
+ p2.x + "," +
+ p2.y + " ";
+ }
- // 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 d;
}
};
/**
- * Connect an edge to its nodes
+ * this generates the SVG path for a linear drawing between datapoints.
+ * @param data
+ * @returns {string}
+ * @private
*/
- 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);
+ LineGraph.prototype._linear = function(data) {
+ // linear
+ var d = "";
+ for (var i = 0; i < data.length; i++) {
+ if (i == 0) {
+ d += data[i].x + "," + data[i].y;
}
- if (this.to) {
- this.to.detachEdge(this);
+ else {
+ d += " " + data[i].x + "," + data[i].y;
}
}
+ return d;
};
- /**
- * 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;
- }
+ module.exports = LineGraph;
- 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;
- };
+/***/ },
+/* 29 */
+/***/ function(module, exports, __webpack_require__) {
+ var util = __webpack_require__(1);
+ var DOMutil = __webpack_require__(2);
+ var Component = __webpack_require__(20);
+ var DataStep = __webpack_require__(30);
/**
- * Retrieve the value of the edge. Can be undefined
- * @return {Number} value
+ * A horizontal time axis
+ * @param {Object} [options] See DataAxis.setOptions for the available
+ * options.
+ * @constructor DataAxis
+ * @extends Component
+ * @param body
*/
- Edge.prototype.getValue = function() {
- return this.value;
+ function DataAxis (body, options, svg, linegraphOptions) {
+ this.id = util.randomUUID();
+ this.body = body;
+
+ this.defaultOptions = {
+ orientation: 'left', // supported: 'left', 'right'
+ showMinorLabels: true,
+ showMajorLabels: true,
+ icons: true,
+ majorLinesOffset: 7,
+ minorLinesOffset: 4,
+ labelOffsetX: 10,
+ labelOffsetY: 2,
+ iconWidth: 20,
+ width: '40px',
+ visible: true,
+ customRange: {
+ left: {min:undefined, max:undefined},
+ right: {min:undefined, max:undefined}
+ }
+ };
+
+ this.linegraphOptions = linegraphOptions;
+ this.linegraphSVG = svg;
+ this.props = {};
+ this.DOMelements = { // dynamic elements
+ lines: {},
+ labels: {}
+ };
+
+ this.dom = {};
+
+ this.range = {start:0, end:0};
+
+ this.options = util.extend({}, this.defaultOptions);
+ this.conversionFactor = 1;
+
+ this.setOptions(options);
+ this.width = Number(('' + this.options.width).replace("px",""));
+ this.minWidth = this.width;
+ this.height = this.linegraphSVG.offsetHeight;
+
+ this.stepPixels = 25;
+ this.stepPixelsForced = 25;
+ this.lineOffset = 0;
+ this.master = true;
+ this.svgElements = {};
+
+
+ this.groups = {};
+ this.amountOfGroups = 0;
+
+ // create the HTML DOM
+ this._create();
+ }
+
+ DataAxis.prototype = new Component();
+
+
+
+ DataAxis.prototype.addGroup = function(label, graphOptions) {
+ if (!this.groups.hasOwnProperty(label)) {
+ this.groups[label] = graphOptions;
+ }
+ this.amountOfGroups += 1;
};
- /**
- * 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;
+ DataAxis.prototype.updateGroup = function(label, graphOptions) {
+ this.groups[label] = graphOptions;
+ };
+
+ DataAxis.prototype.removeGroup = function(label) {
+ if (this.groups.hasOwnProperty(label)) {
+ delete this.groups[label];
+ this.amountOfGroups -= 1;
}
};
- /**
- * 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";
+
+ DataAxis.prototype.setOptions = function (options) {
+ if (options) {
+ var redraw = false;
+ if (this.options.orientation != options.orientation && options.orientation !== undefined) {
+ redraw = true;
+ }
+ var fields = [
+ 'orientation',
+ 'showMinorLabels',
+ 'showMajorLabels',
+ 'icons',
+ 'majorLinesOffset',
+ 'minorLinesOffset',
+ 'labelOffsetX',
+ 'labelOffsetY',
+ 'iconWidth',
+ 'width',
+ 'visible',
+ 'customRange'
+ ];
+ util.selectiveExtend(fields, this.options, options);
+
+ this.minWidth = Number(('' + this.options.width).replace("px",""));
+
+ if (redraw == true && this.dom.frame) {
+ this.hide();
+ this.show();
+ }
+ }
};
+
/**
- * 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
+ * Create the HTML DOM for the DataAxis
*/
- 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;
+ DataAxis.prototype._create = function() {
+ this.dom.frame = document.createElement('div');
+ this.dom.frame.style.width = this.options.width;
+ this.dom.frame.style.height = this.height;
- var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
+ this.dom.lineContainer = document.createElement('div');
+ this.dom.lineContainer.style.width = '100%';
+ this.dom.lineContainer.style.height = this.height;
- return (dist < distMax);
- }
- else {
- return false
- }
+ // create svg element for graph drawing.
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = "absolute";
+ this.svg.style.top = '0px';
+ this.svg.style.height = '100%';
+ this.svg.style.width = '100%';
+ this.svg.style.display = "block";
+ this.dom.frame.appendChild(this.svg);
};
- 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
- };
+ DataAxis.prototype._redrawGroupIcons = function () {
+ DOMutil.prepareElements(this.svgElements);
+
+ var x;
+ var iconWidth = this.options.iconWidth;
+ var iconHeight = 15;
+ var iconOffset = 4;
+ var y = iconOffset + 0.5 * iconHeight;
+
+ if (this.options.orientation == 'left') {
+ x = iconOffset;
}
- 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
- };
+ else {
+ x = this.width - iconWidth - iconOffset;
}
- if (this.selected == true) {return colorObj.highlight;}
- else if (this.hover == true) {return colorObj.hover;}
- else {return colorObj.color;}
- }
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
+ y += iconHeight + iconOffset;
+ }
+ }
+ }
+ DOMutil.cleanupElements(this.svgElements);
+ };
/**
- * 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 the HTML DOM for the DataAxis
*/
- 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;
+ DataAxis.prototype.show = function() {
+ if (!this.dom.frame.parentNode) {
+ if (this.options.orientation == 'left') {
+ this.body.dom.left.appendChild(this.dom.frame);
}
else {
- x = node.x + radius;
- y = node.y - node.height / 2;
+ this.body.dom.right.appendChild(this.dom.frame);
}
- this._circle(ctx, x, y, radius);
- point = this._pointOnCircle(x, y, radius, 0.5);
- this._label(ctx, this.label, point.x, point.y);
+ }
+
+ if (!this.dom.lineContainer.parentNode) {
+ this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer);
}
};
/**
- * Get the line width of the edge. Depends on width and whether one of the
- * connected nodes is selected.
- * @return {Number} width
- * @private
+ * Create the HTML DOM for the DataAxis
*/
- Edge.prototype._getLineWidth = function() {
- if (this.selected == true) {
- return Math.min(this.widthSelected, this.options.widthMax)*this.networkScaleInv;
+ DataAxis.prototype.hide = function() {
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
}
- else {
- if (this.hover == true) {
- return Math.min(this.options.hoverWidth, this.options.widthMax)*this.networkScaleInv;
- }
- else {
- return this.options.width*this.networkScaleInv;
- }
+
+ if (this.dom.lineContainer.parentNode) {
+ this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer);
}
};
- Edge.prototype._getViaCoordinates = function () {
- var xVia = null;
- var yVia = null;
- var factor = this.options.smoothCurves.roundness;
- var type = this.options.smoothCurves.type;
+ /**
+ * Set a range (start and end)
+ * @param end
+ * @param start
+ * @param end
+ */
+ DataAxis.prototype.setRange = function (start, end) {
+ this.range.start = start;
+ this.range.end = end;
+ };
- 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;
- }
- }
- 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;
+ /**
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
+ */
+ DataAxis.prototype.redraw = function () {
+ var changeCalled = false;
+ var activeGroups = 0;
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ activeGroups++;
}
- yVia = this.from.y;
}
}
- 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;
+ if (this.amountOfGroups == 0 || activeGroups == 0) {
+ this.hide();
}
- 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;
+ else {
+ this.show();
+ this.height = Number(this.linegraphSVG.style.height.replace("px",""));
+ // svg offsetheight did not work in firefox and explorer...
+
+ this.dom.lineContainer.style.height = this.height + 'px';
+ this.width = this.options.visible == true ? Number(('' + this.options.width).replace("px","")) : 0;
+
+ var props = this.props;
+ var frame = this.dom.frame;
+
+ // update classname
+ frame.className = 'dataaxis';
+
+ // calculate character width and height
+ this._calculateCharSize();
+
+ var orientation = this.options.orientation;
+ var showMinorLabels = this.options.showMinorLabels;
+ var showMajorLabels = this.options.showMajorLabels;
+
+ // determine the width and height of the elemens for the axis
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
+
+ props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset;
+ props.minorLineHeight = 1;
+ props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset;
+ props.majorLineHeight = 1;
+
+ // take frame offline while updating (is almost twice as fast)
+ if (orientation == 'left') {
+ frame.style.top = '0';
+ frame.style.left = '0';
+ frame.style.bottom = '';
+ frame.style.width = this.width + 'px';
+ frame.style.height = this.height + "px";
}
- }
- 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 { // right
+ frame.style.top = '';
+ frame.style.bottom = '0';
+ frame.style.left = '0';
+ frame.style.width = this.width + 'px';
+ frame.style.height = this.height + "px";
}
- 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;
- }
- }
+ changeCalled = this._redrawLabels();
+ if (this.options.icons == true) {
+ this._redrawGroupIcons();
}
}
-
-
- return {x:xVia, y:yVia};
- }
+ return changeCalled;
+ };
/**
- * Draw a line between two nodes
- * @param {CanvasRenderingContext2D} ctx
+ * Repaint major and minor text labels and vertical grid lines
* @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;
+ DataAxis.prototype._redrawLabels = function () {
+ DOMutil.prepareElements(this.DOMelements.lines);
+ DOMutil.prepareElements(this.DOMelements.labels);
+
+ var orientation = this.options['orientation'];
+
+ // calculate range and step (step such that we have space for 7 characters per label)
+ var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced;
+ var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight, this.options.customRange[this.options.orientation]);
+ this.step = step;
+ // get the distance in pixels for a step
+ // dead space is space that is "left over" after a step
+ var stepPixels = (this.dom.frame.offsetHeight - (step.deadSpace * (this.dom.frame.offsetHeight / step.marginRange))) / (((step.marginRange - step.deadSpace) / step.step));
+ this.stepPixels = stepPixels;
+
+ var amountOfSteps = this.height / stepPixels;
+ var stepDifference = 0;
+
+ if (this.master == false) {
+ stepPixels = this.stepPixelsForced;
+ stepDifference = Math.round((this.dom.frame.offsetHeight / stepPixels) - amountOfSteps);
+ for (var i = 0; i < 0.5 * stepDifference; i++) {
+ step.previous();
+ }
+ amountOfSteps = this.height / stepPixels;
+ }
+ else {
+ amountOfSteps += 0.25;
+ }
+
+
+ this.valueAtZero = step.marginEnd;
+ var marginStartPos = 0;
+
+ // do not draw the first label
+ var max = 1;
+
+ this.maxLabelSize = 0;
+ var y = 0;
+ while (max < Math.round(amountOfSteps)) {
+ step.next();
+ y = Math.round(max * stepPixels);
+ marginStartPos = max * stepPixels;
+ var isMajor = step.isMajor();
+
+ if (this.options['showMinorLabels'] && isMajor == false || this.master == false && this.options['showMinorLabels'] == true) {
+ this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis minor', this.props.minorCharHeight);
+ }
+
+ if (isMajor && this.options['showMajorLabels'] && this.master == true ||
+ this.options['showMinorLabels'] == false && this.master == false && isMajor == true) {
+ if (y >= 0) {
+ this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis major', this.props.majorCharHeight);
}
+ this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
}
else {
- ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
- ctx.stroke();
- return this.via;
+ this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth);
}
+
+ max++;
+ }
+
+ if (this.master == false) {
+ this.conversionFactor = y / (this.valueAtZero - step.current);
}
else {
- ctx.lineTo(this.to.x, this.to.y);
- ctx.stroke();
- return null;
+ this.conversionFactor = this.dom.frame.offsetHeight / step.marginRange;
+ }
+
+ var offset = this.options.icons == true ? this.options.iconWidth + this.options.labelOffsetX + 15 : this.options.labelOffsetX + 15;
+ // this will resize the yAxis to accomodate the labels.
+ if (this.maxLabelSize > (this.width - offset) && this.options.visible == true) {
+ this.width = this.maxLabelSize + offset;
+ this.options.width = this.width + "px";
+ DOMutil.cleanupElements(this.DOMelements.lines);
+ DOMutil.cleanupElements(this.DOMelements.labels);
+ this.redraw();
+ return true;
+ }
+ // this will resize the yAxis if it is too big for the labels.
+ else if (this.maxLabelSize < (this.width - offset) && this.options.visible == true && this.width > this.minWidth) {
+ this.width = Math.max(this.minWidth,this.maxLabelSize + offset);
+ this.options.width = this.width + "px";
+ DOMutil.cleanupElements(this.DOMelements.lines);
+ DOMutil.cleanupElements(this.DOMelements.labels);
+ this.redraw();
+ return true;
+ }
+ else {
+ DOMutil.cleanupElements(this.DOMelements.lines);
+ DOMutil.cleanupElements(this.DOMelements.labels);
+ return false;
}
};
- /**
- * Draw a line from a node to itself, a circle
- * @param {CanvasRenderingContext2D} ctx
- * @param {Number} x
- * @param {Number} y
- * @param {Number} radius
- * @private
- */
- 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();
+ DataAxis.prototype.convertValue = function (value) {
+ var invertedValue = this.valueAtZero - value;
+ var convertedValue = invertedValue * this.conversionFactor;
+ return convertedValue;
};
/**
- * Draw label with white background and with the middle at (x, y)
- * @param {CanvasRenderingContext2D} ctx
- * @param {String} text
- * @param {Number} x
- * @param {Number} y
+ * Create a label for the axis at position x
* @private
+ * @param y
+ * @param text
+ * @param orientation
+ * @param className
+ * @param characterHeight
*/
- Edge.prototype._label = function (ctx, text, x, y) {
- if (text) {
- // TODO: cache the calculated size
- ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
- this.options.fontSize + "px " + this.options.fontFace;
- ctx.fillStyle = this.options.fontFill;
- var width = ctx.measureText(text).width;
- var height = this.options.fontSize;
- var left = x - width / 2;
- var top = y - height / 2;
+ DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) {
+ // reuse redundant label
+ var label = DOMutil.getDOMElement('div',this.DOMelements.labels, this.dom.frame); //this.dom.redundant.labels.shift();
+ label.className = className;
+ label.innerHTML = text;
+ if (orientation == 'left') {
+ label.style.left = '-' + this.options.labelOffsetX + 'px';
+ label.style.textAlign = "right";
+ }
+ else {
+ label.style.right = '-' + this.options.labelOffsetX + 'px';
+ label.style.textAlign = "left";
+ }
- ctx.fillRect(left, top, width, height);
+ label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + 'px';
- // draw text
- ctx.fillStyle = this.options.fontColor || "black";
- ctx.textAlign = "left";
- ctx.textBaseline = "top";
- ctx.fillText(text, left, top);
+ text += '';
+
+ var largestWidth = Math.max(this.props.majorCharWidth,this.props.minorCharWidth);
+ if (this.maxLabelSize < text.length * largestWidth) {
+ this.maxLabelSize = text.length * largestWidth;
}
};
/**
- * 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
+ * Create a minor line for the axis at position y
+ * @param y
+ * @param orientation
+ * @param className
+ * @param offset
+ * @param width
*/
- Edge.prototype._drawDashLine = function(ctx) {
- // set style
- if (this.selected == true) {ctx.strokeStyle = this.options.color.highlight;}
- else if (this.hover == true) {ctx.strokeStyle = this.options.color.hover;}
- else {ctx.strokeStyle = this.options.color.color;}
-
- ctx.lineWidth = this._getLineWidth();
+ DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) {
+ if (this.master == true) {
+ var line = DOMutil.getDOMElement('div',this.DOMelements.lines, this.dom.lineContainer);//this.dom.redundant.lines.shift();
+ line.className = className;
+ line.innerHTML = '';
- 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];
+ if (orientation == 'left') {
+ line.style.left = (this.width - offset) + 'px';
}
else {
- pattern = [5,5];
+ line.style.right = (this.width - offset) + 'px';
}
- // set dash settings for chrome or firefox
- if (typeof ctx.setLineDash !== 'undefined') { //Chrome
- ctx.setLineDash(pattern);
- ctx.lineDashOffset = 0;
+ line.style.width = width + 'px';
+ line.style.top = y + 'px';
+ }
+ };
- } else { //Firefox
- ctx.mozDash = pattern;
- ctx.mozDashOffset = 0;
- }
- // draw the line
- via = this._line(ctx);
- // restore the dash settings.
- if (typeof ctx.setLineDash !== 'undefined') { //Chrome
- ctx.setLineDash([0]);
- ctx.lineDashOffset = 0;
- } else { //Firefox
- ctx.mozDash = [0];
- ctx.mozDashOffset = 0;
- }
- }
- 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();
- }
-
- // 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);
- }
- };
/**
- * Get a point on a line
- * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
- * @return {Object} point
+ * Determine the size of text on the axis (both major and minor axis).
+ * The size is calculated only once and then cached in this.props.
* @private
*/
- 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
+ DataAxis.prototype._calculateCharSize = function () {
+ // determine the char width and height on the minor axis
+ if (!('minorCharHeight' in this.props)) {
+ var textMinor = document.createTextNode('0');
+ var measureCharMinor = document.createElement('DIV');
+ measureCharMinor.className = 'yAxis minor measure';
+ measureCharMinor.appendChild(textMinor);
+ this.dom.frame.appendChild(measureCharMinor);
+
+ this.props.minorCharHeight = measureCharMinor.clientHeight;
+ this.props.minorCharWidth = measureCharMinor.clientWidth;
+
+ this.dom.frame.removeChild(measureCharMinor);
+ }
+
+ if (!('majorCharHeight' in this.props)) {
+ var textMajor = document.createTextNode('0');
+ var measureCharMajor = document.createElement('DIV');
+ measureCharMajor.className = 'yAxis major measure';
+ measureCharMajor.appendChild(textMajor);
+ this.dom.frame.appendChild(measureCharMajor);
+
+ this.props.majorCharHeight = measureCharMajor.clientHeight;
+ this.props.majorCharWidth = measureCharMajor.clientWidth;
+
+ this.dom.frame.removeChild(measureCharMajor);
}
};
/**
- * 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
+ * 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
*/
- 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)
- }
+ DataAxis.prototype.snap = function(date) {
+ return this.step.snap(date);
};
+ module.exports = DataAxis;
+
+
+/***/ },
+/* 30 */
+/***/ function(module, exports, __webpack_require__) {
+
/**
- * 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
+ * @constructor DataStep
+ * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
+ * end data point. 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 DataStep 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
*/
- Edge.prototype._drawArrowCenter = function(ctx) {
- var point;
- // set style
- if (this.selected == true) {ctx.strokeStyle = this.options.color.highlight; ctx.fillStyle = this.options.color.highlight;}
- else if (this.hover == true) {ctx.strokeStyle = this.options.color.hover; ctx.fillStyle = this.options.color.hover;}
- else {ctx.strokeStyle = this.options.color.color; ctx.fillStyle = this.options.color.color;}
- ctx.lineWidth = this._getLineWidth();
+ function DataStep(start, end, minimumStep, containerHeight, customRange) {
+ // variables
+ this.current = 0;
- if (this.from != this.to) {
- // draw line
- var via = this._line(ctx);
+ this.autoScale = true;
+ this.stepIndex = 0;
+ this.step = 1;
+ this.scale = 1;
- 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};
- }
- else {
- point = this._pointOnLine(0.5);
- }
+ this.marginStart;
+ this.marginEnd;
+ this.deadSpace = 0;
- ctx.arrow(point.x, point.y, angle, length);
- ctx.fill();
- ctx.stroke();
+ this.majorSteps = [1, 2, 5, 10];
+ this.minorSteps = [0.25, 0.5, 1, 2];
- // 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);
+ this.setRange(start, end, minimumStep, containerHeight, customRange);
+ }
- // 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);
- }
- }
- };
+ /**
+ * 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 {Number} [start] The start date and time.
+ * @param {Number} [end] The end date and time.
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
+ */
+ DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, customRange) {
+ this._start = customRange.min === undefined ? start : customRange.min;
+ this._end = customRange.max === undefined ? end : customRange.max;
+
+ if (start == end) {
+ this._start = start - 0.75;
+ this._end = end + 1;
+ }
+ if (this.autoScale) {
+ this.setMinimumStep(minimumStep, containerHeight);
+ }
+ this.setFirst(customRange);
+ };
/**
- * 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
+ * Automatically determine the scale that bests fits the provided minimum step
+ * @param {Number} [minimumStep] The minimum step size in milliseconds
*/
- Edge.prototype._drawArrow = function(ctx) {
- // set style
- if (this.selected == true) {ctx.strokeStyle = this.options.color.highlight; ctx.fillStyle = this.options.color.highlight;}
- else if (this.hover == true) {ctx.strokeStyle = this.options.color.hover; ctx.fillStyle = this.options.color.hover;}
- else {ctx.strokeStyle = this.options.color.color; ctx.fillStyle = this.options.color.color;}
-
- ctx.lineWidth = this._getLineWidth();
+ DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
+ // round to floor
+ var size = this._end - this._start;
+ var safeSize = size * 1.2;
+ var minimumStepValue = minimumStep * (safeSize / containerHeight);
+ var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
- 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);
+ var minorStepIdx = -1;
+ var magnitudefactor = Math.pow(10,orderOfMagnitude);
- 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 start = 0;
+ if (orderOfMagnitude < 0) {
+ start = orderOfMagnitude;
+ }
- var via;
- if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true ) {
- via = this.via;
+ var solutionFound = false;
+ for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
+ magnitudefactor = Math.pow(10,i);
+ for (var j = 0; j < this.minorSteps.length; j++) {
+ var stepSize = magnitudefactor * this.minorSteps[j];
+ if (stepSize >= minimumStepValue) {
+ solutionFound = true;
+ minorStepIdx = j;
+ break;
+ }
}
- else if (this.options.smoothCurves.enabled == true) {
- via = this._getViaCoordinates();
+ if (solutionFound == true) {
+ break;
}
+ }
+ this.stepIndex = minorStepIdx;
+ this.scale = magnitudefactor;
+ this.step = magnitudefactor * this.minorSteps[minorStepIdx];
+ };
- 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;
- }
- 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();
+ /**
+ * Round the current date to the first minor date value
+ * This must be executed once when the current date is set to start Date
+ */
+ DataStep.prototype.setFirst = function(customRange) {
+ if (customRange === undefined) {
+ customRange = {};
+ }
+ var niceStart = customRange.min === undefined ? this._start - (this.scale * 2 * this.minorSteps[this.stepIndex]) : customRange.min;
+ var niceEnd = customRange.max === undefined ? this._end + (this.scale * this.minorSteps[this.stepIndex]) : customRange.max;
- // 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();
+ this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max;
+ this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min;
+ this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart;
+ this.marginRange = this.marginEnd - this.marginStart;
- // 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);
- }
+ this.current = this.marginEnd;
+
+ };
+
+ DataStep.prototype.roundToMinor = function(value) {
+ var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
+ if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
+ return rounded + (this.scale * this.minorSteps[this.stepIndex]);
}
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 rounded;
+ }
+ }
- // 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();
- // draw label
- if (this.label) {
- point = this._pointOnCircle(x, y, radius, 0.5);
- this._label(ctx, this.label, point.x, point.y);
- }
+ /**
+ * Check if the there is a next step
+ * @return {boolean} true if the current date has not passed the end date
+ */
+ DataStep.prototype.hasNext = function () {
+ return (this.current >= this.marginStart);
+ };
+
+ /**
+ * Do the next step
+ */
+ DataStep.prototype.next = function() {
+ var prev = this.current;
+ this.current -= this.step;
+
+ // safety mechanism: if current time is still unchanged, move to the end
+ if (this.current == prev) {
+ this.current = this._end;
}
};
+ /**
+ * Do the next step
+ */
+ DataStep.prototype.previous = function() {
+ this.current += this.step;
+ this.marginEnd += this.step;
+ this.marginRange = this.marginEnd - this.marginStart;
+ };
+
/**
- * 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
+ * Get the current datetime
+ * @return {String} current The current date
*/
- Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
- 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;
- }
- return minDistance
- }
- else {
- return this._getDistanceToLine(x1,y1,x2,y2,x3,y3);
+ DataStep.prototype.getCurrent = function() {
+ var toPrecision = '' + Number(this.current).toPrecision(5);
+ for (var i = toPrecision.length-1; i > 0; i--) {
+ if (toPrecision[i] == "0") {
+ toPrecision = toPrecision.slice(0,i);
}
- }
- 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 if (toPrecision[i] == "." || toPrecision[i] == ",") {
+ toPrecision = toPrecision.slice(0,i);
+ break;
}
- else {
- x = node.x + radius;
- y = node.y - 0.5 * node.height;
+ else{
+ break;
}
- dx = x - x3;
- dy = y - y3;
- return Math.abs(Math.sqrt(dx*dx + dy*dy) - radius);
}
- };
- 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;
+ return toPrecision;
+ };
- if (u > 1) {
- u = 1;
- }
- else if (u < 0) {
- u = 0;
- }
- var x = x1 + u * px,
- y = y1 + u * py,
- dx = x - x3,
- dy = y - y3;
- //# 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
+ /**
+ * 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
+ */
+ DataStep.prototype.snap = function(date) {
- return Math.sqrt(dx*dx + dy*dy);
- }
+ };
/**
- * This allows the zoom level of the network to influence the rendering
- *
- * @param scale
+ * 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.
*/
- Edge.prototype.setScale = function(scale) {
- this.networkScaleInv = 1.0/scale;
+ DataStep.prototype.isMajor = function() {
+ return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
};
+ module.exports = DataStep;
- Edge.prototype.select = function() {
- this.selected = true;
- };
- Edge.prototype.unselect = function() {
- this.selected = false;
- };
+/***/ },
+/* 31 */
+/***/ function(module, exports, __webpack_require__) {
- 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);
- }
- };
+ var util = __webpack_require__(1);
+ var DOMutil = __webpack_require__(2);
/**
- * This function draws the control nodes for the manipulator. In order to enable this, only set the this.controlNodesEnabled to true.
- * @param ctx
+ * @constructor Group
+ * @param {Number | String} groupId
+ * @param {Object} data
+ * @param {ItemSet} itemSet
*/
- 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);
- }
+ function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
+ this.id = groupId;
+ var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom']
+ this.options = util.selectiveBridgeObject(fields,options);
+ this.usingDefaultStyle = group.className === undefined;
+ this.groupsUsingDefaultStyles = groupsUsingDefaultStyles;
+ this.zeroPosition = 0;
+ this.update(group);
+ if (this.usingDefaultStyle == true) {
+ this.groupsUsingDefaultStyles[0] += 1;
+ }
+ this.itemsData = [];
+ this.visible = group.visible === undefined ? true : group.visible;
+ }
- 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;
+ GraphGroup.prototype.setItems = function(items) {
+ if (items != null) {
+ this.itemsData = items;
+ if (this.options.sort == true) {
+ this.itemsData.sort(function (a,b) {return a.x - b.x;})
}
-
- this.controlNodes.from.draw(ctx);
- this.controlNodes.to.draw(ctx);
}
else {
- this.controlNodes = {from:null, to:null, positions:{}};
+ this.itemsData = [];
}
};
- /**
- * Enable control nodes.
- * @private
- */
- Edge.prototype._enableControlNodes = function() {
- this.controlNodesEnabled = true;
+ GraphGroup.prototype.setZeroPosition = function(pos) {
+ this.zeroPosition = pos;
};
- /**
- * disable control nodes
- * @private
- */
- Edge.prototype._disableControlNodes = function() {
- this.controlNodesEnabled = false;
- };
+ GraphGroup.prototype.setOptions = function(options) {
+ if (options !== undefined) {
+ var fields = ['sampling','style','sort','yAxisOrientation','barChart'];
+ util.selectiveDeepExtend(fields, this.options, options);
- /**
- * 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
- */
- 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));
+ util.mergeOptions(this.options, options,'catmullRom');
+ util.mergeOptions(this.options, options,'drawPoints');
+ util.mergeOptions(this.options, options,'shaded');
- 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;
+ if (options.catmullRom) {
+ if (typeof options.catmullRom == 'object') {
+ if (options.catmullRom.parametrization) {
+ if (options.catmullRom.parametrization == 'uniform') {
+ this.options.catmullRom.alpha = 0;
+ }
+ else if (options.catmullRom.parametrization == 'chordal') {
+ this.options.catmullRom.alpha = 1.0;
+ }
+ else {
+ this.options.catmullRom.parametrization = 'centripetal';
+ this.options.catmullRom.alpha = 0.5;
+ }
+ }
+ }
+ }
}
};
-
- /**
- * 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();
- }
- if (this.controlNodes.to.selected == true) {
- this.to = this.connectedNode;
- this.connectedNode = null;
- this.controlNodes.to.unselect();
- }
+ GraphGroup.prototype.update = function(group) {
+ this.group = group;
+ this.content = group.content || 'graph';
+ this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10;
+ this.visible = group.visible === undefined ? true : group.visible;
+ this.setOptions(group.options);
};
- /**
- * 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: *}}}
- */
- 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;
+ GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
+ var fillHeight = iconHeight * 0.5;
+ var path, fillPath;
- 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();
- }
+ var outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer);
+ outline.setAttributeNS(null, "x", x);
+ outline.setAttributeNS(null, "y", y - fillHeight);
+ outline.setAttributeNS(null, "width", iconWidth);
+ outline.setAttributeNS(null, "height", 2*fillHeight);
+ outline.setAttributeNS(null, "class", "outline");
- 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 (this.options.style == 'line') {
+ path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
+ path.setAttributeNS(null, "class", this.className);
+ path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+"");
+ if (this.options.shaded.enabled == true) {
+ fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
+ if (this.options.shaded.orientation == 'top') {
+ fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) +
+ "L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight));
+ }
+ else {
+ fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " +
+ "L"+x+"," + (y + fillHeight) + " " +
+ "L"+ (x + iconWidth) + "," + (y + fillHeight) +
+ "L"+ (x + iconWidth) + ","+y);
+ }
+ fillPath.setAttributeNS(null, "class", this.className + " iconFill");
+ }
- 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;
+ if (this.options.drawPoints.enabled == true) {
+ DOMutil.drawPoint(x + 0.5 * iconWidth,y, this, JSONcontainer, SVGcontainer);
+ }
}
else {
- xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
- yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
- }
+ var barWidth = Math.round(0.3 * iconWidth);
+ var bar1Height = Math.round(0.4 * iconHeight);
+ var bar2Height = Math.round(0.75 * iconHeight);
- return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}};
+ var offset = Math.round((iconWidth - (2 * barWidth))/3);
+
+ DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer);
+ DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer);
+ }
};
- module.exports = Edge;
+ /**
+ *
+ * @param iconWidth
+ * @param iconHeight
+ * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}}
+ */
+ GraphGroup.prototype.getLegend = function(iconWidth, iconHeight) {
+ var svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.drawIcon(0,0.5*iconHeight,[],svg,iconWidth,iconHeight);
+ return {icon: svg, label: this.content, orientation:this.options.yAxisOrientation};
+ }
+
+ module.exports = GraphGroup;
+
/***/ },
-/* 34 */
+/* 32 */
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
+ var DOMutil = __webpack_require__(2);
+ var Component = __webpack_require__(20);
/**
- * @class Groups
- * This class can store groups and properties specific for groups.
- */
- function Groups() {
- this.clear();
- this.defaultIndex = 0;
- }
-
-
- /**
- * default constants for group colors
+ * Legend for Graph2d
*/
- 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
- ];
-
+ function Legend(body, options, side, linegraphOptions) {
+ this.body = body;
+ this.defaultOptions = {
+ enabled: true,
+ icons: true,
+ iconSize: 20,
+ iconSpacing: 6,
+ left: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,center,right
+ },
+ right: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,center,right
+ }
+ }
+ this.side = side;
+ this.options = util.extend({},this.defaultOptions);
+ this.linegraphOptions = linegraphOptions;
- /**
- * Clear all groups
- */
- Groups.prototype.clear = function () {
+ this.svgElements = {};
+ this.dom = {};
this.groups = {};
- this.groups.length = function()
- {
- var i = 0;
- for ( var p in this ) {
- if (this.hasOwnProperty(p)) {
- i++;
- }
- }
- return i;
+ this.amountOfGroups = 0;
+ this._create();
+
+ this.setOptions(options);
+ }
+
+ Legend.prototype = new Component();
+
+
+ Legend.prototype.addGroup = function(label, graphOptions) {
+ if (!this.groups.hasOwnProperty(label)) {
+ this.groups[label] = graphOptions;
+ }
+ this.amountOfGroups += 1;
+ };
+
+ Legend.prototype.updateGroup = function(label, graphOptions) {
+ this.groups[label] = graphOptions;
+ };
+
+ Legend.prototype.removeGroup = function(label) {
+ if (this.groups.hasOwnProperty(label)) {
+ delete this.groups[label];
+ this.amountOfGroups -= 1;
}
};
+ Legend.prototype._create = function() {
+ this.dom.frame = document.createElement('div');
+ this.dom.frame.className = 'legend';
+ this.dom.frame.style.position = "absolute";
+ this.dom.frame.style.top = "10px";
+ this.dom.frame.style.display = "block";
+
+ this.dom.textArea = document.createElement('div');
+ this.dom.textArea.className = 'legendText';
+ this.dom.textArea.style.position = "relative";
+ this.dom.textArea.style.top = "0px";
+
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = 'absolute';
+ this.svg.style.top = 0 +'px';
+ this.svg.style.width = this.options.iconSize + 5 + 'px';
+
+ this.dom.frame.appendChild(this.svg);
+ this.dom.frame.appendChild(this.dom.textArea);
+ };
/**
- * 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
+ * Hide the component from the DOM
*/
- 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;
+ Legend.prototype.hide = function() {
+ // remove the frame containing the items
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
}
-
- return group;
};
/**
- * Add a custom group style
- * @param {String} groupname
- * @param {Object} style An object containing borderColor,
- * backgroundColor, etc.
- * @return {Object} group The created group object
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
*/
- Groups.prototype.add = function (groupname, style) {
- this.groups[groupname] = style;
- if (style.color) {
- style.color = util.parseColor(style.color);
+ Legend.prototype.show = function() {
+ // show frame containing the items
+ if (!this.dom.frame.parentNode) {
+ this.body.dom.center.appendChild(this.dom.frame);
}
- return style;
};
- module.exports = Groups;
+ Legend.prototype.setOptions = function(options) {
+ var fields = ['enabled','orientation','icons','left','right'];
+ util.selectiveDeepExtend(fields, this.options, options);
+ };
+ Legend.prototype.redraw = function() {
+ var activeGroups = 0;
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ activeGroups++;
+ }
+ }
+ }
-/***/ },
-/* 35 */
-/***/ function(module, exports, __webpack_require__) {
+ if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false || activeGroups == 0) {
+ this.hide();
+ }
+ else {
+ this.show();
+ if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') {
+ this.dom.frame.style.left = '4px';
+ this.dom.frame.style.textAlign = "left";
+ this.dom.textArea.style.textAlign = "left";
+ this.dom.textArea.style.left = (this.options.iconSize + 15) + 'px';
+ this.dom.textArea.style.right = '';
+ this.svg.style.left = 0 +'px';
+ this.svg.style.right = '';
+ }
+ else {
+ this.dom.frame.style.right = '4px';
+ this.dom.frame.style.textAlign = "right";
+ this.dom.textArea.style.textAlign = "right";
+ this.dom.textArea.style.right = (this.options.iconSize + 15) + 'px';
+ this.dom.textArea.style.left = '';
+ this.svg.style.right = 0 +'px';
+ this.svg.style.left = '';
+ }
- /**
- * @class Images
- * This class loads images and keeps them stored.
- */
- function Images() {
- this.images = {};
+ if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') {
+ this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
+ this.dom.frame.style.bottom = '';
+ }
+ else {
+ this.dom.frame.style.bottom = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
+ this.dom.frame.style.top = '';
+ }
- this.callback = undefined;
- }
+ if (this.options.icons == false) {
+ this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + 'px';
+ this.dom.textArea.style.right = '';
+ this.dom.textArea.style.left = '';
+ this.svg.style.width = '0px';
+ }
+ else {
+ this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + 'px'
+ this.drawLegendIcons();
+ }
- /**
- * Set an onload callback function. This will be called each time an image
- * is loaded
- * @param {function} callback
- */
- Images.prototype.setOnloadCallback = function(callback) {
- this.callback = callback;
+ var content = '';
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ content += this.groups[groupId].content + '
';
+ }
+ }
+ }
+ this.dom.textArea.innerHTML = content;
+ this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px';
+ }
};
- /**
- *
- * @param {string} url Url of the image
- * @return {Image} img The image object
- */
- Images.prototype.load = function(url) {
- 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);
+ Legend.prototype.drawLegendIcons = function() {
+ if (this.dom.frame.parentNode) {
+ DOMutil.prepareElements(this.svgElements);
+ var padding = window.getComputedStyle(this.dom.frame).paddingTop;
+ var iconOffset = Number(padding.replace('px',''));
+ var x = iconOffset;
+ var iconWidth = this.options.iconSize;
+ var iconHeight = 0.75 * this.options.iconSize;
+ var y = iconOffset + 0.5 * iconHeight + 3;
+
+ this.svg.style.width = iconWidth + 5 + iconOffset + 'px';
+
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
+ y += iconHeight + this.options.iconSpacing;
+ }
}
- };
- img.src = url;
- }
+ }
- return img;
+ DOMutil.cleanupElements(this.svgElements);
+ }
};
- module.exports = Images;
+ module.exports = Legend;
/***/ },
-/* 36 */
+/* 33 */
/***/ function(module, exports, __webpack_require__) {
- var util = __webpack_require__(1);
+ // Utility functions for ordering and stacking of items
+ var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors
/**
- * @class Node
- * A node. A node can be connected to other nodes via one or multiple edges.
- * @param {object} properties An object containing properties for the node. All
- * properties are optional, except for the id.
- * {number} id Id of the node. Required
- * {string} label Text label for the node
- * {number} x Horizontal position of the node
- * {number} y Vertical position of the node
- * {string} shape Node shape, available:
- * "database", "circle", "ellipse",
- * "box", "image", "text", "dot",
- * "star", "triangle", "triangleDown",
- * "square"
- * {string} image An image url
- * {string} title An title text, can be HTML
- * {anytype} group A group name or number
- * @param {Network.Images} imagelist A list with images. Only needed
- * when the node has an image
- * @param {Network.Groups} grouplist A list with groups. Needed for
- * retrieving group properties
- * @param {Object} constants An object with default values for
- * example for the color
- *
+ * Order items by their start data
+ * @param {Item[]} items
*/
- function Node(properties, imagelist, grouplist, networkConstants) {
- var constants = util.selectiveBridgeObject(['nodes'],networkConstants);
- this.options = constants.nodes;
+ exports.orderByStart = function(items) {
+ items.sort(function (a, b) {
+ return a.data.start - b.data.start;
+ });
+ };
- this.selected = false;
- this.hover = false;
+ /**
+ * Order items by their end date. If they have no end date, their start date
+ * is used.
+ * @param {Item[]} items
+ */
+ exports.orderByEnd = function(items) {
+ items.sort(function (a, b) {
+ var aTime = ('end' in a.data) ? a.data.end : a.data.start,
+ bTime = ('end' in b.data) ? b.data.end : b.data.start;
- this.edges = []; // all edges connected to this node
- this.dynamicEdges = [];
- this.reroutedEdges = {};
+ return aTime - bTime;
+ });
+ };
- this.fontDrawThreshold = 3;
+ /**
+ * Adjust vertical positions of the items such that they don't overlap each
+ * other.
+ * @param {Item[]} items
+ * All visible items
+ * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
+ * Margins between items and between items and the axis.
+ * @param {boolean} [force=false]
+ * If true, all items will be repositioned. If false (default), only
+ * items having a top===null will be re-stacked
+ */
+ exports.stack = function(items, margin, force) {
+ var i, iMax;
- // set defaults for the properties
- this.id = undefined;
- this.x = null;
- this.y = null;
- this.xFixed = false;
- this.yFixed = false;
- this.horizontalAlignLeft = true; // these are for the navigation controls
- this.verticalAlignTop = true; // these are for the navigation controls
- this.baseRadiusValue = networkConstants.nodes.radius;
- this.radiusFixed = false;
- this.level = -1;
- this.preassignedLevel = false;
+ if (force) {
+ // reset top position of all items
+ for (i = 0, iMax = items.length; i < iMax; i++) {
+ items[i].top = null;
+ }
+ }
+ // calculate new, non-overlapping positions
+ for (i = 0, iMax = items.length; i < iMax; i++) {
+ var item = items[i];
+ if (item.top === null) {
+ // initialize top position
+ item.top = margin.axis;
- this.imagelist = imagelist;
- this.grouplist = grouplist;
+ do {
+ // TODO: optimize checking for overlap. when there is a gap without items,
+ // you only need to check for items from the next item on, not from zero
+ var collidingItem = null;
+ for (var j = 0, jj = items.length; j < jj; j++) {
+ var other = items[j];
+ if (other.top !== null && other !== item && exports.collision(item, other, margin.item)) {
+ collidingItem = other;
+ break;
+ }
+ }
- // physics properties
- this.fx = 0.0; // external force x
- this.fy = 0.0; // external force y
- this.vx = 0.0; // velocity x
- this.vy = 0.0; // velocity y
- this.damping = networkConstants.physics.damping; // written every time gravity is calculated
- this.fixedData = {x:null,y:null};
+ if (collidingItem != null) {
+ // There is a collision. Reposition the items above the colliding element
+ item.top = collidingItem.top + collidingItem.height + margin.item.vertical;
+ }
+ } while (collidingItem);
+ }
+ }
+ };
+ /**
+ * Adjust vertical positions of the items without stacking them
+ * @param {Item[]} items
+ * All visible items
+ * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
+ * Margins between items and between items and the axis.
+ */
+ exports.nostack = function(items, margin) {
+ var i, iMax;
- this.setProperties(properties, constants);
+ // reset top position of all items
+ for (i = 0, iMax = items.length; i < iMax; i++) {
+ items[i].top = margin.axis;
+ }
+ };
- // creating the variables for clustering
- this.resetCluster();
- this.dynamicEdgesLength = 0;
- this.clusterSession = 0;
- this.clusterSizeWidthFactor = networkConstants.clustering.nodeScaling.width;
- this.clusterSizeHeightFactor = networkConstants.clustering.nodeScaling.height;
- this.clusterSizeRadiusFactor = networkConstants.clustering.nodeScaling.radius;
- this.maxNodeSizeIncrements = networkConstants.clustering.maxNodeSizeIncrements;
- this.growthIndicator = 0;
+ /**
+ * Test if the two provided items collide
+ * The items must have parameters left, width, top, and height.
+ * @param {Item} a The first item
+ * @param {Item} b The second item
+ * @param {{horizontal: number, vertical: number}} margin
+ * An object containing a horizontal and vertical
+ * minimum required margin.
+ * @return {boolean} true if a and b collide, else false
+ */
+ exports.collision = function(a, b, margin) {
+ return ((a.left - margin.horizontal + EPSILON) < (b.left + b.width) &&
+ (a.left + a.width + margin.horizontal - EPSILON) > b.left &&
+ (a.top - margin.vertical + EPSILON) < (b.top + b.height) &&
+ (a.top + a.height + margin.vertical - EPSILON) > b.top);
+ };
- // variables to tell the node about the network.
- this.networkScaleInv = 1;
- this.networkScale = 1;
- this.canvasTopLeft = {"x": -300, "y": -300};
- this.canvasBottomRight = {"x": 300, "y": 300};
- this.parentEdgeId = null;
+
+/***/ },
+/* 34 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Hammer = __webpack_require__(15);
+
+ /**
+ * @constructor Item
+ * @param {Object} data Object containing (optional) parameters type,
+ * start, end, content, group, className.
+ * @param {{toScreen: function, toTime: function}} conversion
+ * Conversion functions from time to screen and vice versa
+ * @param {Object} options Configuration options
+ * // TODO: describe available options
+ */
+ function Item (data, conversion, options) {
+ this.id = null;
+ this.parent = null;
+ this.data = data;
+ this.dom = null;
+ this.conversion = conversion || {};
+ this.options = options || {};
+
+ this.selected = false;
+ this.displayed = false;
+ this.dirty = true;
+
+ this.top = null;
+ this.left = null;
+ this.width = null;
+ this.height = null;
}
/**
- * (re)setting the clustering variables and objects
+ * Select current item
*/
- Node.prototype.resetCluster = function() {
- // clustering variables
- this.formationScale = undefined; // this is used to determine when to open the cluster
- this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
- this.containedNodes = {};
- this.containedEdges = {};
- this.clusterSessions = [];
+ Item.prototype.select = function() {
+ this.selected = true;
+ if (this.displayed) this.redraw();
};
/**
- * Attach a edge to the node
- * @param {Edge} edge
+ * Unselect current item
*/
- Node.prototype.attachEdge = function(edge) {
- if (this.edges.indexOf(edge) == -1) {
- this.edges.push(edge);
+ Item.prototype.unselect = function() {
+ this.selected = false;
+ if (this.displayed) this.redraw();
+ };
+
+ /**
+ * Set a parent for the item
+ * @param {ItemSet | Group} parent
+ */
+ Item.prototype.setParent = function(parent) {
+ if (this.displayed) {
+ this.hide();
+ this.parent = parent;
+ if (this.parent) {
+ this.show();
+ }
}
- if (this.dynamicEdges.indexOf(edge) == -1) {
- this.dynamicEdges.push(edge);
+ else {
+ this.parent = parent;
}
- this.dynamicEdgesLength = this.dynamicEdges.length;
};
/**
- * Detach a edge from the node
- * @param {Edge} edge
+ * Check whether this item is visible inside given range
+ * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @returns {boolean} True if visible
*/
- Node.prototype.detachEdge = function(edge) {
- var index = this.edges.indexOf(edge);
- if (index != -1) {
- this.edges.splice(index, 1);
- this.dynamicEdges.splice(index, 1);
- }
- this.dynamicEdgesLength = this.dynamicEdges.length;
+ Item.prototype.isVisible = function(range) {
+ // Should be implemented by Item implementations
+ return false;
};
+ /**
+ * Show the Item in the DOM (when not already visible)
+ * @return {Boolean} changed
+ */
+ Item.prototype.show = function() {
+ return false;
+ };
/**
- * Set or overwrite properties for the node
- * @param {Object} properties an object with properties
- * @param {Object} constants and object with default, global properties
+ * Hide the Item from the DOM (when visible)
+ * @return {Boolean} changed
*/
- Node.prototype.setProperties = function(properties, constants) {
- if (!properties) {
- return;
- }
-
- var fields = ['borderWidth','borderWidthSelected','shape','image','radius','fontColor',
- 'fontSize','fontFace','group','mass'
- ];
- util.selectiveDeepExtend(fields, this.options, properties);
-
- this.originalLabel = undefined;
- // basic properties
- if (properties.id !== undefined) {this.id = properties.id;}
- if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
- if (properties.title !== undefined) {this.title = properties.title;}
- if (properties.x !== undefined) {this.x = properties.x;}
- if (properties.y !== undefined) {this.y = properties.y;}
- if (properties.value !== undefined) {this.value = properties.value;}
- if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
-
- // navigation controls properties
- if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
- if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
- if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
-
- if (this.id === undefined) {
- throw "Node must have an id";
- }
-
- // copy group properties
- if (typeof this.options.group === 'number' || (typeof this.options.group === 'string' && this.options.group != '')) {
- var groupObj = this.grouplist.get(this.options.group);
- for (var prop in groupObj) {
- if (groupObj.hasOwnProperty(prop)) {
- this.options[prop] = groupObj[prop];
- }
- }
- }
-
-
- // individual shape properties
- if (properties.radius !== undefined) {this.baseRadiusValue = this.options.radius;}
- if (properties.color !== undefined) {this.options.color = util.parseColor(properties.color);}
-
- if (this.options.image!== undefined && this.options.image!= "") {
- if (this.imagelist) {
- this.imageObj = this.imagelist.load(this.options.image);
- }
- else {
- throw "No imagelist provided";
- }
- }
-
- this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX);
- this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY);
- this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
-
- if (this.options.shape == 'image') {
- this.options.radiusMin = constants.nodes.widthMin;
- this.options.radiusMax = constants.nodes.widthMax;
- }
-
-
- // choose draw method depending on the shape
- switch (this.options.shape) {
- case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
- case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
- case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
- case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
- // TODO: add diamond shape
- case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
- case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
- case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
- case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
- case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
- case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
- case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
- default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
- }
- // reset the size of the node, this can be changed
- this._reset();
- };
+ Item.prototype.hide = function() {
+ return false;
+ };
/**
- * select this node
+ * Repaint the item
*/
- Node.prototype.select = function() {
- this.selected = true;
- this._reset();
+ Item.prototype.redraw = function() {
+ // should be implemented by the item
};
/**
- * unselect this node
+ * Reposition the Item horizontally
*/
- Node.prototype.unselect = function() {
- this.selected = false;
- this._reset();
+ Item.prototype.repositionX = function() {
+ // should be implemented by the item
};
-
/**
- * Reset the calculated size of the node, forces it to recalculate its size
+ * Reposition the Item vertically
*/
- Node.prototype.clearSizeCache = function() {
- this._reset();
+ Item.prototype.repositionY = function() {
+ // should be implemented by the item
};
/**
- * Reset the calculated size of the node, forces it to recalculate its size
- * @private
+ * Repaint a delete button on the top right of the item when the item is selected
+ * @param {HTMLElement} anchor
+ * @protected
*/
- Node.prototype._reset = function() {
- this.width = undefined;
- this.height = undefined;
- };
+ Item.prototype._repaintDeleteButton = function (anchor) {
+ if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
+ // create and show button
+ var me = this;
- /**
- * get the title of this node.
- * @return {string} title The title of the node, or undefined when no title
- * has been set.
- */
- Node.prototype.getTitle = function() {
- return typeof this.title === "function" ? this.title() : this.title;
- };
+ var deleteButton = document.createElement('div');
+ deleteButton.className = 'delete';
+ deleteButton.title = 'Delete this item';
- /**
- * Calculate the distance to the border of the Node
- * @param {CanvasRenderingContext2D} ctx
- * @param {Number} angle Angle in radians
- * @returns {number} distance Distance to the border in pixels
- */
- Node.prototype.distanceToBorder = function (ctx, angle) {
- var borderWidth = 1;
+ Hammer(deleteButton, {
+ preventDefault: true
+ }).on('tap', function (event) {
+ me.parent.removeFromDataSet(me);
+ event.stopPropagation();
+ });
- if (!this.width) {
- this.resize(ctx);
+ anchor.appendChild(deleteButton);
+ this.dom.deleteButton = deleteButton;
}
+ else if (!this.selected && this.dom.deleteButton) {
+ // remove button
+ if (this.dom.deleteButton.parentNode) {
+ this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
+ }
+ this.dom.deleteButton = null;
+ }
+ };
- switch (this.options.shape) {
- case 'circle':
- case 'dot':
- return this.options.radius+ borderWidth;
-
- case 'ellipse':
- var a = this.width / 2;
- var b = this.height / 2;
- var w = (Math.sin(angle) * a);
- var h = (Math.cos(angle) * b);
- return a * b / Math.sqrt(w * w + h * h);
+ module.exports = Item;
- // TODO: implement distanceToBorder for database
- // TODO: implement distanceToBorder for triangle
- // TODO: implement distanceToBorder for triangleDown
- case 'box':
- case 'image':
- case 'text':
- default:
- if (this.width) {
- return Math.min(
- Math.abs(this.width / 2 / Math.cos(angle)),
- Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
- // TODO: reckon with border radius too in case of box
- }
- else {
- return 0;
- }
+/***/ },
+/* 35 */
+/***/ function(module, exports, __webpack_require__) {
- }
- // TODO: implement calculation of distance to border for all shapes
- };
+ var Item = __webpack_require__(34);
/**
- * Set forces acting on the node
- * @param {number} fx Force in horizontal direction
- * @param {number} fy Force in vertical direction
+ * @constructor ItemBox
+ * @extends Item
+ * @param {Object} data Object containing parameters start
+ * content, className.
+ * @param {{toScreen: function, toTime: function}} conversion
+ * Conversion functions from time to screen and vice versa
+ * @param {Object} [options] Configuration options
+ * // TODO: describe available options
*/
- Node.prototype._setForce = function(fx, fy) {
- this.fx = fx;
- this.fy = fy;
- };
+ function ItemBox (data, conversion, options) {
+ this.props = {
+ dot: {
+ width: 0,
+ height: 0
+ },
+ line: {
+ width: 0,
+ height: 0
+ }
+ };
+
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data);
+ }
+ }
+
+ Item.call(this, data, conversion, options);
+ }
+
+ ItemBox.prototype = new Item (null, null, null);
/**
- * Add forces acting on the node
- * @param {number} fx Force in horizontal direction
- * @param {number} fy Force in vertical direction
- * @private
+ * Check whether this item is visible inside given range
+ * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @returns {boolean} True if visible
*/
- Node.prototype._addForce = function(fx, fy) {
- this.fx += fx;
- this.fy += fy;
+ ItemBox.prototype.isVisible = function(range) {
+ // determine visibility
+ // TODO: account for the real width of the item. Right now we just add 1/4 to the window
+ var interval = (range.end - range.start) / 4;
+ return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
};
/**
- * Perform one discrete step for the node
- * @param {number} interval Time interval in seconds
+ * Repaint the item
*/
- Node.prototype.discreteStep = function(interval) {
- if (!this.xFixed) {
- var dx = this.damping * this.vx; // damping force
- var ax = (this.fx - dx) / this.options.mass; // acceleration
- this.vx += ax * interval; // velocity
- this.x += this.vx * interval; // position
- }
+ ItemBox.prototype.redraw = function() {
+ var dom = this.dom;
+ if (!dom) {
+ // create DOM
+ this.dom = {};
+ dom = this.dom;
- if (!this.yFixed) {
- var dy = this.damping * this.vy; // damping force
- var ay = (this.fy - dy) / this.options.mass; // acceleration
- this.vy += ay * interval; // velocity
- this.y += this.vy * interval; // position
+ // create main box
+ dom.box = document.createElement('DIV');
+
+ // contents box (inside the background box). used for making margins
+ dom.content = document.createElement('DIV');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+
+ // line to axis
+ dom.line = document.createElement('DIV');
+ dom.line.className = 'line';
+
+ // dot on axis
+ dom.dot = document.createElement('DIV');
+ dom.dot.className = 'dot';
+
+ // attach this item as attribute
+ dom.box['timeline-item'] = this;
}
- };
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot redraw item: no parent attached');
+ }
+ if (!dom.box.parentNode) {
+ var foreground = this.parent.dom.foreground;
+ if (!foreground) throw new Error('Cannot redraw time axis: parent has no foreground container element');
+ foreground.appendChild(dom.box);
+ }
+ if (!dom.line.parentNode) {
+ var background = this.parent.dom.background;
+ if (!background) throw new Error('Cannot redraw time axis: parent has no background container element');
+ background.appendChild(dom.line);
+ }
+ if (!dom.dot.parentNode) {
+ var axis = this.parent.dom.axis;
+ if (!background) throw new Error('Cannot redraw time axis: parent has no axis container element');
+ axis.appendChild(dom.dot);
+ }
+ this.displayed = true;
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
- /**
- * Perform one discrete step for the node
- * @param {number} interval Time interval in seconds
- * @param {number} maxVelocity The speed limit imposed on the velocity
- */
- Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
- if (!this.xFixed) {
- var dx = this.damping * this.vx; // damping force
- var ax = (this.fx - dx) / this.options.mass; // acceleration
- this.vx += ax * interval; // velocity
- this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
- this.x += this.vx * interval; // position
+ this.dirty = true;
}
- else {
- this.fx = 0;
+
+ // update title
+ if (this.data.title != this.title) {
+ dom.box.title = this.data.title;
+ this.title = this.data.title;
}
- if (!this.yFixed) {
- var dy = this.damping * this.vy; // damping force
- var ay = (this.fy - dy) / this.options.mass; // acceleration
- this.vy += ay * interval; // velocity
- this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
- this.y += this.vy * interval; // position
+ // update class
+ var className = (this.data.className? ' ' + this.data.className : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.box.className = 'item box' + className;
+ dom.line.className = 'item line' + className;
+ dom.dot.className = 'item dot' + className;
+
+ this.dirty = true;
}
- else {
- this.fy = 0;
+
+ // recalculate size
+ if (this.dirty) {
+ this.props.dot.height = dom.dot.offsetHeight;
+ this.props.dot.width = dom.dot.offsetWidth;
+ this.props.line.width = dom.line.offsetWidth;
+ this.width = dom.box.offsetWidth;
+ this.height = dom.box.offsetHeight;
+
+ this.dirty = false;
}
- };
- /**
- * Check if this node has a fixed x and y position
- * @return {boolean} true if fixed, false if not
- */
- Node.prototype.isFixed = function() {
- return (this.xFixed && this.yFixed);
+ this._repaintDeleteButton(dom.box);
};
/**
- * Check if this node is moving
- * @param {number} vmin the minimum velocity considered as "moving"
- * @return {boolean} true if moving, false if it has no velocity
+ * Show the item in the DOM (when not already displayed). The items DOM will
+ * be created when needed.
*/
- Node.prototype.isMoving = function(vmin) {
- var velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2));
- // this.velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2))
- return (velocity > vmin);
+ ItemBox.prototype.show = function() {
+ if (!this.displayed) {
+ this.redraw();
+ }
};
/**
- * check if this node is selecte
- * @return {boolean} selected True if node is selected, else false
+ * Hide the item from the DOM (when visible)
*/
- Node.prototype.isSelected = function() {
- return this.selected;
+ ItemBox.prototype.hide = function() {
+ if (this.displayed) {
+ var dom = this.dom;
+
+ if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
+ if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
+ if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
+
+ this.top = null;
+ this.left = null;
+
+ this.displayed = false;
+ }
};
/**
- * Retrieve the value of the node. Can be undefined
- * @return {Number} value
+ * Reposition the item horizontally
+ * @Override
*/
- Node.prototype.getValue = function() {
- return this.value;
+ ItemBox.prototype.repositionX = function() {
+ var start = this.conversion.toScreen(this.data.start),
+ align = this.options.align,
+ left,
+ box = this.dom.box,
+ line = this.dom.line,
+ dot = this.dom.dot;
+
+ // calculate left position of the box
+ if (align == 'right') {
+ this.left = start - this.width;
+ }
+ else if (align == 'left') {
+ this.left = start;
+ }
+ else {
+ // default or 'center'
+ this.left = start - this.width / 2;
+ }
+
+ // reposition box
+ box.style.left = this.left + 'px';
+
+ // reposition line
+ line.style.left = (start - this.props.line.width / 2) + 'px';
+
+ // reposition dot
+ dot.style.left = (start - this.props.dot.width / 2) + 'px';
};
/**
- * Calculate the distance from the nodes location to the given location (x,y)
- * @param {Number} x
- * @param {Number} y
- * @return {Number} value
+ * Reposition the item vertically
+ * @Override
*/
- Node.prototype.getDistance = function(x, y) {
- var dx = this.x - x,
- dy = this.y - y;
- return Math.sqrt(dx * dx + dy * dy);
+ ItemBox.prototype.repositionY = function() {
+ var orientation = this.options.orientation,
+ box = this.dom.box,
+ line = this.dom.line,
+ dot = this.dom.dot;
+
+ if (orientation == 'top') {
+ box.style.top = (this.top || 0) + 'px';
+
+ line.style.top = '0';
+ line.style.height = (this.parent.top + this.top + 1) + 'px';
+ line.style.bottom = '';
+ }
+ else { // orientation 'bottom'
+ var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty
+ var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top;
+
+ box.style.top = (this.parent.height - this.top - this.height || 0) + 'px';
+ line.style.top = (itemSetHeight - lineHeight) + 'px';
+ line.style.bottom = '0';
+ }
+
+ dot.style.top = (-this.props.dot.height / 2) + 'px';
};
+ module.exports = ItemBox;
+
+
+/***/ },
+/* 36 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Item = __webpack_require__(34);
/**
- * Adjust the value range of the node. The node will adjust it's radius
- * based on its value.
- * @param {Number} min
- * @param {Number} max
+ * @constructor ItemPoint
+ * @extends Item
+ * @param {Object} data Object containing parameters start
+ * content, className.
+ * @param {{toScreen: function, toTime: function}} conversion
+ * Conversion functions from time to screen and vice versa
+ * @param {Object} [options] Configuration options
+ * // TODO: describe available options
*/
- Node.prototype.setValueRange = function(min, max) {
- if (!this.radiusFixed && this.value !== undefined) {
- if (max == min) {
- this.options.radius= (this.options.radiusMin + this.options.radiusMax) / 2;
+ function ItemPoint (data, conversion, options) {
+ this.props = {
+ dot: {
+ top: 0,
+ width: 0,
+ height: 0
+ },
+ content: {
+ height: 0,
+ marginLeft: 0
}
- else {
- var scale = (this.options.radiusMax - this.options.radiusMin) / (max - min);
- this.options.radius= (this.value - min) * scale + this.options.radiusMin;
+ };
+
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data);
}
}
- this.baseRadiusValue = this.options.radius;
- };
+
+ Item.call(this, data, conversion, options);
+ }
+
+ ItemPoint.prototype = new Item (null, null, null);
/**
- * Draw this node in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
+ * Check whether this item is visible inside given range
+ * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @returns {boolean} True if visible
*/
- Node.prototype.draw = function(ctx) {
- throw "Draw method not initialized for node";
+ ItemPoint.prototype.isVisible = function(range) {
+ // determine visibility
+ // TODO: account for the real width of the item. Right now we just add 1/4 to the window
+ var interval = (range.end - range.start) / 4;
+ return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
};
/**
- * Recalculate the size of this node in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
+ * Repaint the item
*/
- Node.prototype.resize = function(ctx) {
- throw "Resize method not initialized for node";
- };
+ ItemPoint.prototype.redraw = function() {
+ var dom = this.dom;
+ if (!dom) {
+ // create DOM
+ this.dom = {};
+ dom = this.dom;
- /**
- * Check if this object is overlapping with the provided object
- * @param {Object} obj an object with parameters left, top, right, bottom
- * @return {boolean} True if location is located on node
- */
- Node.prototype.isOverlappingWith = function(obj) {
- return (this.left < obj.right &&
- this.left + this.width > obj.left &&
- this.top < obj.bottom &&
- this.top + this.height > obj.top);
- };
+ // background box
+ dom.point = document.createElement('div');
+ // className is updated in redraw()
- Node.prototype._resizeImage = function (ctx) {
- // TODO: pre calculate the image size
+ // contents box, right from the dot
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.point.appendChild(dom.content);
- if (!this.width || !this.height) { // undefined or 0
- var width, height;
- if (this.value) {
- this.options.radius= this.baseRadiusValue;
- var scale = this.imageObj.height / this.imageObj.width;
- if (scale !== undefined) {
- width = this.options.radius|| this.imageObj.width;
- height = this.options.radius* scale || this.imageObj.height;
- }
- else {
- width = 0;
- height = 0;
- }
+ // dot at start
+ dom.dot = document.createElement('div');
+ dom.point.appendChild(dom.dot);
+
+ // attach this item as attribute
+ dom.point['timeline-item'] = this;
+ }
+
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot redraw item: no parent attached');
+ }
+ if (!dom.point.parentNode) {
+ var foreground = this.parent.dom.foreground;
+ if (!foreground) {
+ throw new Error('Cannot redraw time axis: parent has no foreground container element');
+ }
+ foreground.appendChild(dom.point);
+ }
+ this.displayed = true;
+
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
}
else {
- width = this.imageObj.width;
- height = this.imageObj.height;
+ throw new Error('Property "content" missing in item ' + this.data.id);
}
- this.width = width;
- this.height = height;
- this.growthIndicator = 0;
- if (this.width > 0 && this.height > 0) {
- this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
- this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
- this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.width - width;
- }
+ this.dirty = true;
}
- };
+ // update title
+ if (this.data.title != this.title) {
+ dom.point.title = this.data.title;
+ this.title = this.data.title;
+ }
- Node.prototype._drawImage = function (ctx) {
- this._resizeImage(ctx);
+ // update class
+ var className = (this.data.className? ' ' + this.data.className : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.point.className = 'item point' + className;
+ dom.dot.className = 'item dot' + className;
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
+ this.dirty = true;
+ }
- var yLabel;
- if (this.imageObj.width != 0 ) {
- // draw the shade
- if (this.clusterSize > 1) {
- var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
- lineWidth *= this.networkScaleInv;
- lineWidth = Math.min(0.2 * this.width,lineWidth);
+ // recalculate size
+ if (this.dirty) {
+ this.width = dom.point.offsetWidth;
+ this.height = dom.point.offsetHeight;
+ this.props.dot.width = dom.dot.offsetWidth;
+ this.props.dot.height = dom.dot.offsetHeight;
+ this.props.content.height = dom.content.offsetHeight;
- ctx.globalAlpha = 0.5;
- ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
- }
+ // resize contents
+ dom.content.style.marginLeft = 2 * this.props.dot.width + 'px';
+ //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
- // draw the image
- ctx.globalAlpha = 1.0;
- ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
- yLabel = this.y + this.height / 2;
- }
- else {
- // image still loading... just draw the label for now
- yLabel = this.y;
+ dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
+ dom.dot.style.left = (this.props.dot.width / 2) + 'px';
+
+ this.dirty = false;
}
- this._label(ctx, this.label, this.x, yLabel, undefined, "top");
+ this._repaintDeleteButton(dom.point);
};
-
- Node.prototype._resizeBox = function (ctx) {
- if (!this.width) {
- var margin = 5;
- var textSize = this.getTextSize(ctx);
- this.width = textSize.width + 2 * margin;
- this.height = textSize.height + 2 * margin;
-
- this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
- this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
- this.growthIndicator = this.width - (textSize.width + 2 * margin);
- // this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
-
+ /**
+ * Show the item in the DOM (when not already visible). The items DOM will
+ * be created when needed.
+ */
+ ItemPoint.prototype.show = function() {
+ if (!this.displayed) {
+ this.redraw();
}
};
- Node.prototype._drawBox = function (ctx) {
- this._resizeBox(ctx);
-
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
-
- var clusterLineWidth = 2.5;
- var borderWidth = this.options.borderWidth;
- var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
-
- ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
+ /**
+ * Hide the item from the DOM (when visible)
+ */
+ ItemPoint.prototype.hide = function() {
+ if (this.displayed) {
+ if (this.dom.point.parentNode) {
+ this.dom.point.parentNode.removeChild(this.dom.point);
+ }
- // draw the outer border
- if (this.clusterSize > 1) {
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
+ this.top = null;
+ this.left = null;
- ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.options.radius);
- ctx.stroke();
+ this.displayed = false;
}
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
+ };
- ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.options.color.background;
+ /**
+ * Reposition the item horizontally
+ * @Override
+ */
+ ItemPoint.prototype.repositionX = function() {
+ var start = this.conversion.toScreen(this.data.start);
- ctx.roundRect(this.left, this.top, this.width, this.height, this.options.radius);
- ctx.fill();
- ctx.stroke();
+ this.left = start - this.props.dot.width;
- this._label(ctx, this.label, this.x, this.y);
+ // reposition point
+ this.dom.point.style.left = this.left + 'px';
};
+ /**
+ * Reposition the item vertically
+ * @Override
+ */
+ ItemPoint.prototype.repositionY = function() {
+ var orientation = this.options.orientation,
+ point = this.dom.point;
- Node.prototype._resizeDatabase = function (ctx) {
- if (!this.width) {
- var margin = 5;
- var textSize = this.getTextSize(ctx);
- var size = textSize.width + 2 * margin;
- this.width = size;
- this.height = size;
-
- // scaling used for clustering
- this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
- this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
- this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.width - size;
+ if (orientation == 'top') {
+ point.style.top = this.top + 'px';
+ }
+ else {
+ point.style.top = (this.parent.height - this.top - this.height) + 'px';
}
};
- Node.prototype._drawDatabase = function (ctx) {
- this._resizeDatabase(ctx);
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
+ module.exports = ItemPoint;
- var clusterLineWidth = 2.5;
- var borderWidth = this.options.borderWidth;
- var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
- ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
-
- // draw the outer border
- if (this.clusterSize > 1) {
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
+/***/ },
+/* 37 */
+/***/ function(module, exports, __webpack_require__) {
- ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth);
- ctx.stroke();
- }
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
+ var Hammer = __webpack_require__(15);
+ var Item = __webpack_require__(34);
- ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
- ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
- ctx.fill();
- ctx.stroke();
+ /**
+ * @constructor ItemRange
+ * @extends Item
+ * @param {Object} data Object containing parameters start, end
+ * content, className.
+ * @param {{toScreen: function, toTime: function}} conversion
+ * Conversion functions from time to screen and vice versa
+ * @param {Object} [options] Configuration options
+ * // TODO: describe options
+ */
+ function ItemRange (data, conversion, options) {
+ this.props = {
+ content: {
+ width: 0
+ }
+ };
+ this.overflow = false; // if contents can overflow (css styling), this flag is set to true
- this._label(ctx, this.label, this.x, this.y);
- };
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data.id);
+ }
+ if (data.end == undefined) {
+ throw new Error('Property "end" missing in item ' + data.id);
+ }
+ }
+ Item.call(this, data, conversion, options);
+ }
- Node.prototype._resizeCircle = function (ctx) {
- if (!this.width) {
- var margin = 5;
- var textSize = this.getTextSize(ctx);
- var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
- this.options.radius = diameter / 2;
+ ItemRange.prototype = new Item (null, null, null);
- this.width = diameter;
- this.height = diameter;
+ ItemRange.prototype.baseClassName = 'item range';
- // scaling used for clustering
- // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
- // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
- this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.options.radius- 0.5*diameter;
- }
+ /**
+ * Check whether this item is visible inside given range
+ * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @returns {boolean} True if visible
+ */
+ ItemRange.prototype.isVisible = function(range) {
+ // determine visibility
+ return (this.data.start < range.end) && (this.data.end > range.start);
};
- Node.prototype._drawCircle = function (ctx) {
- this._resizeCircle(ctx);
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
-
- var clusterLineWidth = 2.5;
- var borderWidth = this.options.borderWidth;
- var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
+ /**
+ * Repaint the item
+ */
+ ItemRange.prototype.redraw = function() {
+ var dom = this.dom;
+ if (!dom) {
+ // create DOM
+ this.dom = {};
+ dom = this.dom;
- ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
+ // background box
+ dom.box = document.createElement('div');
+ // className is updated in redraw()
- // draw the outer border
- if (this.clusterSize > 1) {
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
+ // contents box
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
- ctx.circle(this.x, this.y, this.options.radius+2*ctx.lineWidth);
- ctx.stroke();
+ // attach this item as attribute
+ dom.box['timeline-item'] = this;
}
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
-
- ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
- ctx.circle(this.x, this.y, this.options.radius);
- ctx.fill();
- ctx.stroke();
-
- this._label(ctx, this.label, this.x, this.y);
- };
-
- Node.prototype._resizeEllipse = function (ctx) {
- if (!this.width) {
- var textSize = this.getTextSize(ctx);
- this.width = textSize.width * 1.5;
- this.height = textSize.height * 2;
- if (this.width < this.height) {
- this.width = this.height;
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot redraw item: no parent attached');
+ }
+ if (!dom.box.parentNode) {
+ var foreground = this.parent.dom.foreground;
+ if (!foreground) {
+ throw new Error('Cannot redraw time axis: parent has no foreground container element');
}
- var defaultSize = this.width;
-
- // scaling used for clustering
- this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
- this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
- this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.width - defaultSize;
+ foreground.appendChild(dom.box);
}
- };
+ this.displayed = true;
- Node.prototype._drawEllipse = function (ctx) {
- this._resizeEllipse(ctx);
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
- var clusterLineWidth = 2.5;
- var borderWidth = this.options.borderWidth;
- var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
+ this.dirty = true;
+ }
- ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
+ // update title
+ if (this.data.title != this.title) {
+ dom.box.title = this.data.title;
+ this.title = this.data.title;
+ }
- // draw the outer border
- if (this.clusterSize > 1) {
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
+ // update class
+ var className = (this.data.className ? (' ' + this.data.className) : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.box.className = this.baseClassName + className;
- ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
- ctx.stroke();
+ this.dirty = true;
}
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
- ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
+ // recalculate size
+ if (this.dirty) {
+ // determine from css whether this box has overflow
+ this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden';
- ctx.ellipse(this.left, this.top, this.width, this.height);
- ctx.fill();
- ctx.stroke();
- this._label(ctx, this.label, this.x, this.y);
- };
+ this.props.content.width = this.dom.content.offsetWidth;
+ this.height = this.dom.box.offsetHeight;
- Node.prototype._drawDot = function (ctx) {
- this._drawShape(ctx, 'circle');
- };
+ this.dirty = false;
+ }
- Node.prototype._drawTriangle = function (ctx) {
- this._drawShape(ctx, 'triangle');
+ this._repaintDeleteButton(dom.box);
+ this._repaintDragLeft();
+ this._repaintDragRight();
};
- Node.prototype._drawTriangleDown = function (ctx) {
- this._drawShape(ctx, 'triangleDown');
+ /**
+ * Show the item in the DOM (when not already visible). The items DOM will
+ * be created when needed.
+ */
+ ItemRange.prototype.show = function() {
+ if (!this.displayed) {
+ this.redraw();
+ }
};
- Node.prototype._drawSquare = function (ctx) {
- this._drawShape(ctx, 'square');
- };
+ /**
+ * Hide the item from the DOM (when visible)
+ * @return {Boolean} changed
+ */
+ ItemRange.prototype.hide = function() {
+ if (this.displayed) {
+ var box = this.dom.box;
- Node.prototype._drawStar = function (ctx) {
- this._drawShape(ctx, 'star');
- };
+ if (box.parentNode) {
+ box.parentNode.removeChild(box);
+ }
- Node.prototype._resizeShape = function (ctx) {
- if (!this.width) {
- this.options.radius= this.baseRadiusValue;
- var size = 2 * this.options.radius;
- this.width = size;
- this.height = size;
+ this.top = null;
+ this.left = null;
- // scaling used for clustering
- this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
- this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
- this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.width - size;
+ this.displayed = false;
}
};
- Node.prototype._drawShape = function (ctx, shape) {
- this._resizeShape(ctx);
-
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
-
- var clusterLineWidth = 2.5;
- var borderWidth = this.options.borderWidth;
- var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
- var radiusMultiplier = 2;
-
- // choose draw method depending on the shape
- switch (shape) {
- case 'dot': radiusMultiplier = 2; break;
- case 'square': radiusMultiplier = 2; break;
- case 'triangle': radiusMultiplier = 3; break;
- case 'triangleDown': radiusMultiplier = 3; break;
- case 'star': radiusMultiplier = 4; break;
- }
-
- ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
- // draw the outer border
- if (this.clusterSize > 1) {
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
+ /**
+ * Reposition the item horizontally
+ * @Override
+ */
+ ItemRange.prototype.repositionX = function() {
+ var props = this.props,
+ parentWidth = this.parent.width,
+ start = this.conversion.toScreen(this.data.start),
+ end = this.conversion.toScreen(this.data.end),
+ padding = this.options.padding,
+ contentLeft;
- ctx[shape](this.x, this.y, this.options.radius+ radiusMultiplier * ctx.lineWidth);
- ctx.stroke();
+ // limit the width of the this, as browsers cannot draw very wide divs
+ if (start < -parentWidth) {
+ start = -parentWidth;
}
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
-
- ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
- ctx[shape](this.x, this.y, this.options.radius);
- ctx.fill();
- ctx.stroke();
-
- if (this.label) {
- this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top',true);
+ if (end > 2 * parentWidth) {
+ end = 2 * parentWidth;
}
- };
+ var boxWidth = Math.max(end - start, 1);
- Node.prototype._resizeText = function (ctx) {
- if (!this.width) {
- var margin = 5;
- var textSize = this.getTextSize(ctx);
- this.width = textSize.width + 2 * margin;
- this.height = textSize.height + 2 * margin;
+ if (this.overflow) {
+ // when range exceeds left of the window, position the contents at the left of the visible area
+ contentLeft = Math.max(-start, 0);
- // scaling used for clustering
- this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
- this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
- this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.width - (textSize.width + 2 * margin);
+ this.left = start;
+ this.width = boxWidth + this.props.content.width;
+ // Note: The calculation of width is an optimistic calculation, giving
+ // a width which will not change when moving the Timeline
+ // So no restacking needed, which is nicer for the eye;
}
- };
-
- Node.prototype._drawText = function (ctx) {
- this._resizeText(ctx);
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
-
- this._label(ctx, this.label, this.x, this.y);
- };
-
-
- Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) {
- if (text && Number(this.options.fontSize) * this.networkScale > this.fontDrawThreshold) {
- ctx.font = (this.selected ? "bold " : "") + this.options.fontSize + "px " + this.options.fontFace;
- ctx.fillStyle = this.options.fontColor || "black";
- ctx.textAlign = align || "center";
- ctx.textBaseline = baseline || "middle";
-
- var lines = text.split('\n');
- var lineCount = lines.length;
- var fontSize = (Number(this.options.fontSize) + 4);
- var yLine = y + (1 - lineCount) / 2 * fontSize;
- if (labelUnderNode == true) {
- yLine = y + (1 - lineCount) / (2 * fontSize);
- }
-
- for (var i = 0; i < lineCount; i++) {
- ctx.fillText(lines[i], x, yLine);
- yLine += fontSize;
+ else { // no overflow
+ // when range exceeds left of the window, position the contents at the left of the visible area
+ if (start < 0) {
+ contentLeft = Math.min(-start,
+ (end - start - props.content.width - 2 * padding));
+ // TODO: remove the need for options.padding. it's terrible.
}
- }
- };
-
-
- Node.prototype.getTextSize = function(ctx) {
- if (this.label !== undefined) {
- ctx.font = (this.selected ? "bold " : "") + this.options.fontSize + "px " + this.options.fontFace;
-
- var lines = this.label.split('\n'),
- height = (Number(this.options.fontSize) + 4) * lines.length,
- width = 0;
-
- for (var i = 0, iMax = lines.length; i < iMax; i++) {
- width = Math.max(width, ctx.measureText(lines[i]).width);
+ else {
+ contentLeft = 0;
}
- return {"width": width, "height": height};
- }
- else {
- return {"width": 0, "height": 0};
+ this.left = start;
+ this.width = boxWidth;
}
+
+ this.dom.box.style.left = this.left + 'px';
+ this.dom.box.style.width = boxWidth + 'px';
+ this.dom.content.style.left = contentLeft + 'px';
};
/**
- * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
- * there is a safety margin of 0.3 * width;
- *
- * @returns {boolean}
+ * Reposition the item vertically
+ * @Override
*/
- Node.prototype.inArea = function() {
- if (this.width !== undefined) {
- return (this.x + this.width *this.networkScaleInv >= this.canvasTopLeft.x &&
- this.x - this.width *this.networkScaleInv < this.canvasBottomRight.x &&
- this.y + this.height*this.networkScaleInv >= this.canvasTopLeft.y &&
- this.y - this.height*this.networkScaleInv < this.canvasBottomRight.y);
+ ItemRange.prototype.repositionY = function() {
+ var orientation = this.options.orientation,
+ box = this.dom.box;
+
+ if (orientation == 'top') {
+ box.style.top = this.top + 'px';
}
else {
- return true;
+ box.style.top = (this.parent.height - this.top - this.height) + 'px';
}
};
/**
- * checks if the core of the node is in the display area, this is used for opening clusters around zoom
- * @returns {boolean}
- */
- Node.prototype.inView = function() {
- return (this.x >= this.canvasTopLeft.x &&
- this.x < this.canvasBottomRight.x &&
- this.y >= this.canvasTopLeft.y &&
- this.y < this.canvasBottomRight.y);
- };
-
- /**
- * This allows the zoom level of the network to influence the rendering
- * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
- *
- * @param scale
- * @param canvasTopLeft
- * @param canvasBottomRight
+ * Repaint a drag area on the left side of the range when the range is selected
+ * @protected
*/
- Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
- this.networkScaleInv = 1.0/scale;
- this.networkScale = scale;
- this.canvasTopLeft = canvasTopLeft;
- this.canvasBottomRight = canvasBottomRight;
- };
+ ItemRange.prototype._repaintDragLeft = function () {
+ if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
+ // create and show drag area
+ var dragLeft = document.createElement('div');
+ dragLeft.className = 'drag-left';
+ dragLeft.dragLeftItem = this;
+ // TODO: this should be redundant?
+ Hammer(dragLeft, {
+ preventDefault: true
+ }).on('drag', function () {
+ //console.log('drag left')
+ });
- /**
- * This allows the zoom level of the network to influence the rendering
- *
- * @param scale
- */
- Node.prototype.setScale = function(scale) {
- this.networkScaleInv = 1.0/scale;
- this.networkScale = scale;
+ this.dom.box.appendChild(dragLeft);
+ this.dom.dragLeft = dragLeft;
+ }
+ else if (!this.selected && this.dom.dragLeft) {
+ // delete drag area
+ if (this.dom.dragLeft.parentNode) {
+ this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
+ }
+ this.dom.dragLeft = null;
+ }
};
-
-
/**
- * set the velocity at 0. Is called when this node is contained in another during clustering
+ * Repaint a drag area on the right side of the range when the range is selected
+ * @protected
*/
- Node.prototype.clearVelocity = function() {
- this.vx = 0;
- this.vy = 0;
- };
+ ItemRange.prototype._repaintDragRight = function () {
+ if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
+ // create and show drag area
+ var dragRight = document.createElement('div');
+ dragRight.className = 'drag-right';
+ dragRight.dragRightItem = this;
+ // TODO: this should be redundant?
+ Hammer(dragRight, {
+ preventDefault: true
+ }).on('drag', function () {
+ //console.log('drag right')
+ });
- /**
- * Basic preservation of (kinectic) energy
- *
- * @param massBeforeClustering
- */
- Node.prototype.updateVelocity = function(massBeforeClustering) {
- var energyBefore = this.vx * this.vx * massBeforeClustering;
- //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass);
- this.vx = Math.sqrt(energyBefore/this.options.mass);
- energyBefore = this.vy * this.vy * massBeforeClustering;
- //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass);
- this.vy = Math.sqrt(energyBefore/this.options.mass);
+ this.dom.box.appendChild(dragRight);
+ this.dom.dragRight = dragRight;
+ }
+ else if (!this.selected && this.dom.dragRight) {
+ // delete drag area
+ if (this.dom.dragRight.parentNode) {
+ this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
+ }
+ this.dom.dragRight = null;
+ }
};
- module.exports = Node;
+ module.exports = ItemRange;
/***/ },
-/* 37 */
+/* 38 */
/***/ function(module, exports, __webpack_require__) {
+ var util = __webpack_require__(1);
+ var stack = __webpack_require__(33);
+ var ItemRange = __webpack_require__(37);
+
/**
- * 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.
+ * @constructor Group
+ * @param {Number | String} groupId
+ * @param {Object} data
+ * @param {ItemSet} itemSet
*/
- function Popup(container, x, y, text, style) {
- if (container) {
- this.container = container;
- }
- else {
- this.container = document.body;
- }
+ function Group (groupId, data, itemSet) {
+ this.groupId = groupId;
- // 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'
- }
- }
+ this.itemSet = itemSet;
+
+ this.dom = {};
+ this.props = {
+ label: {
+ width: 0,
+ height: 0
}
- }
+ };
+ this.className = null;
- this.x = 0;
- this.y = 0;
- this.padding = 5;
+ this.items = {}; // items filtered by groupId of this group
+ this.visibleItems = []; // items currently visible in window
+ this.orderedItems = { // items sorted by start and by end
+ byStart: [],
+ byEnd: []
+ };
- if (x !== undefined && y !== undefined ) {
- this.setPosition(x, y);
- }
- if (text !== undefined) {
- this.setText(text);
- }
+ this._create();
- // 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);
+ this.setData(data);
}
/**
- * @param {number} x Horizontal position of the popup window
- * @param {number} y Vertical position of the popup window
+ * Create DOM elements for the group
+ * @private
*/
- Popup.prototype.setPosition = function(x, y) {
- this.x = parseInt(x);
- this.y = parseInt(y);
- };
+ Group.prototype._create = function() {
+ var label = document.createElement('div');
+ label.className = 'vlabel';
+ this.dom.label = label;
- /**
- * Set the text for the popup window. This can be HTML code
- * @param {string} text
- */
- Popup.prototype.setText = function(text) {
- this.frame.innerHTML = text;
+ var inner = document.createElement('div');
+ inner.className = 'inner';
+ label.appendChild(inner);
+ this.dom.inner = inner;
+
+ var foreground = document.createElement('div');
+ foreground.className = 'group';
+ foreground['timeline-group'] = this;
+ this.dom.foreground = foreground;
+
+ this.dom.background = document.createElement('div');
+ this.dom.background.className = 'group';
+
+ this.dom.axis = document.createElement('div');
+ this.dom.axis.className = 'group';
+
+ // create a hidden marker to detect when the Timelines container is attached
+ // to the DOM, or the style of a parent of the Timeline is changed from
+ // display:none is changed to visible.
+ this.dom.marker = document.createElement('div');
+ this.dom.marker.style.visibility = 'hidden';
+ this.dom.marker.innerHTML = '?';
+ this.dom.background.appendChild(this.dom.marker);
};
/**
- * Show the popup window
- * @param {boolean} show Optional. Show or hide the window
+ * Set the group data for this group
+ * @param {Object} data Group data, can contain properties content and className
*/
- Popup.prototype.show = function (show) {
- if (show === undefined) {
- show = true;
+ Group.prototype.setData = function(data) {
+ // update contents
+ var content = data && data.content;
+ if (content instanceof Element) {
+ this.dom.inner.appendChild(content);
+ }
+ else if (content !== undefined && content !== null) {
+ this.dom.inner.innerHTML = content;
+ }
+ else {
+ this.dom.inner.innerHTML = this.groupId || ''; // groupId can be null
}
- 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;
- }
- if (left < this.padding) {
- left = this.padding;
- }
+ // update title
+ this.dom.label.title = data && data.title || '';
- this.frame.style.left = left + "px";
- this.frame.style.top = top + "px";
- this.frame.style.visibility = "visible";
+ if (!this.dom.inner.firstChild) {
+ util.addClassName(this.dom.inner, 'hidden');
}
else {
- this.hide();
+ util.removeClassName(this.dom.inner, 'hidden');
+ }
+
+ // update className
+ var className = data && data.className || null;
+ if (className != this.className) {
+ if (this.className) {
+ util.removeClassName(this.dom.label, className);
+ util.removeClassName(this.dom.foreground, className);
+ util.removeClassName(this.dom.background, className);
+ util.removeClassName(this.dom.axis, className);
+ }
+ util.addClassName(this.dom.label, className);
+ util.addClassName(this.dom.foreground, className);
+ util.addClassName(this.dom.background, className);
+ util.addClassName(this.dom.axis, className);
}
};
/**
- * Hide the popup window
+ * Get the width of the group label
+ * @return {number} width
*/
- Popup.prototype.hide = function () {
- this.frame.style.visibility = "hidden";
+ Group.prototype.getLabelWidth = function() {
+ return this.props.label.width;
};
- module.exports = Popup;
-
-
-/***/ },
-/* 38 */
-/***/ 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
+ * Repaint this group
+ * @param {{start: number, end: number}} range
+ * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
+ * @param {boolean} [restack=false] Force restacking of all items
+ * @return {boolean} Returns true if the group is resized
*/
- function parseDOT (data) {
- dot = data;
- return parseGraph();
- }
+ Group.prototype.redraw = function(range, margin, restack) {
+ var resized = false;
- // token types enumeration
- var TOKENTYPE = {
- NULL : 0,
- DELIMITER : 1,
- IDENTIFIER: 2,
- UNKNOWN : 3
- };
+ this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
- // map with all delimiters
- var DELIMITERS = {
- '{': true,
- '}': true,
- '[': true,
- ']': true,
- ';': true,
- '=': true,
- ',': true,
+ // force recalculation of the height of the items when the marker height changed
+ // (due to the Timeline being attached to the DOM or changed from display:none to visible)
+ var markerHeight = this.dom.marker.clientHeight;
+ if (markerHeight != this.lastMarkerHeight) {
+ this.lastMarkerHeight = markerHeight;
- '->': true,
- '--': true
- };
+ util.forEach(this.items, function (item) {
+ item.dirty = true;
+ if (item.displayed) item.redraw();
+ });
- 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
+ restack = true;
+ }
- /**
- * 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);
- }
+ // reposition visible items vertically
+ if (this.itemSet.options.stack) { // TODO: ugly way to access options...
+ stack.stack(this.visibleItems, margin, restack);
+ }
+ else { // no stacking
+ stack.nostack(this.visibleItems, margin);
+ }
- /**
- * 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);
- }
+ // recalculate the height of the group
+ var height;
+ var visibleItems = this.visibleItems;
+ if (visibleItems.length) {
+ var min = visibleItems[0].top;
+ var max = visibleItems[0].top + visibleItems[0].height;
+ util.forEach(visibleItems, function (item) {
+ min = Math.min(min, item.top);
+ max = Math.max(max, (item.top + item.height));
+ });
+ if (min > margin.axis) {
+ // there is an empty gap between the lowest item and the axis
+ var offset = min - margin.axis;
+ max -= offset;
+ util.forEach(visibleItems, function (item) {
+ item.top -= offset;
+ });
+ }
+ height = max + margin.item.vertical / 2;
+ }
+ else {
+ height = margin.axis + margin.item.vertical;
+ }
+ height = Math.max(height, this.props.label.height);
- /**
- * Preview the next character from the dot file.
- * @return {String} cNext
- */
- function nextPreview() {
- return dot.charAt(index + 1);
- }
+ // calculate actual size and position
+ var foreground = this.dom.foreground;
+ this.top = foreground.offsetTop;
+ this.left = foreground.offsetLeft;
+ this.width = foreground.offsetWidth;
+ resized = util.updateProperty(this, 'height', height) || resized;
- /**
- * 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);
- }
+ // recalculate size of label
+ resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
+ resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
- /**
- * 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 = {};
- }
+ // apply new height
+ this.dom.background.style.height = height + 'px';
+ this.dom.foreground.style.height = height + 'px';
+ this.dom.label.style.height = height + 'px';
- if (b) {
- for (var name in b) {
- if (b.hasOwnProperty(name)) {
- a[name] = b[name];
- }
- }
+ // update vertical position of items after they are re-stacked and the height of the group is calculated
+ for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
+ var item = this.visibleItems[i];
+ item.repositionY();
}
- return a;
- }
+
+ return resized;
+ };
/**
- * 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
+ * Show this group: attach to the DOM
*/
- 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];
- }
- else {
- // this is the end point
- o[key] = value;
- }
+ Group.prototype.show = function() {
+ if (!this.dom.label.parentNode) {
+ this.itemSet.dom.labelSet.appendChild(this.dom.label);
}
- }
- /**
- * 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;
+ if (!this.dom.foreground.parentNode) {
+ this.itemSet.dom.foreground.appendChild(this.dom.foreground);
+ }
- // 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;
+ if (!this.dom.background.parentNode) {
+ this.itemSet.dom.background.appendChild(this.dom.background);
}
- // 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;
- }
- }
+ if (!this.dom.axis.parentNode) {
+ this.itemSet.dom.axis.appendChild(this.dom.axis);
}
+ };
- if (!current) {
- // this is a new node
- current = {
- id: node.id
- };
- if (graph.node) {
- // clone default attributes
- current.attr = merge(current.attr, graph.node);
- }
+ /**
+ * Hide this group: remove from the DOM
+ */
+ Group.prototype.hide = function() {
+ var label = this.dom.label;
+ if (label.parentNode) {
+ label.parentNode.removeChild(label);
}
- // add node to this (sub)graph and all its parent graphs
- for (i = graphs.length - 1; i >= 0; i--) {
- var g = graphs[i];
+ var foreground = this.dom.foreground;
+ if (foreground.parentNode) {
+ foreground.parentNode.removeChild(foreground);
+ }
- if (!g.nodes) {
- g.nodes = [];
- }
- if (g.nodes.indexOf(current) == -1) {
- g.nodes.push(current);
- }
+ var background = this.dom.background;
+ if (background.parentNode) {
+ background.parentNode.removeChild(background);
}
- // merge attributes
- if (node.attr) {
- current.attr = merge(current.attr, node.attr);
+ var axis = this.dom.axis;
+ if (axis.parentNode) {
+ axis.parentNode.removeChild(axis);
}
- }
+ };
/**
- * Add an edge to a graph object
- * @param {Object} graph
- * @param {Object} edge
+ * Add an item to the group
+ * @param {Item} item
*/
- 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
+ Group.prototype.add = function(item) {
+ this.items[item.id] = item;
+ item.setParent(this);
+
+ if (this.visibleItems.indexOf(item) == -1) {
+ var range = this.itemSet.body.range; // TODO: not nice accessing the range like this
+ this._checkIfVisible(item, this.visibleItems, range);
}
- }
+ };
/**
- * 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
+ * Remove an item from the group
+ * @param {Item} item
*/
- function createEdge(graph, from, to, type, attr) {
- var edge = {
- from: from,
- to: to,
- type: type
- };
+ Group.prototype.remove = function(item) {
+ delete this.items[item.id];
+ item.setParent(this.itemSet);
- if (graph.edge) {
- edge.attr = merge({}, graph.edge); // clone default attributes
- }
- edge.attr = merge(edge.attr || {}, attr); // merge attributes
+ // remove from visible items
+ var index = this.visibleItems.indexOf(item);
+ if (index != -1) this.visibleItems.splice(index, 1);
- return edge;
- }
+ // TODO: also remove from ordered items?
+ };
/**
- * Get next token in the current dot file.
- * The token and token type are available as token and tokenType
+ * Remove an item from the corresponding DataSet
+ * @param {Item} item
*/
- function getToken() {
- tokenType = TOKENTYPE.NULL;
- token = '';
+ Group.prototype.removeFromDataSet = function(item) {
+ this.itemSet.removeItem(item.id);
+ };
- // skip over whitespaces
- while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
- next();
- }
+ /**
+ * Reorder the items
+ */
+ Group.prototype.order = function() {
+ var array = util.toArray(this.items);
+ this.orderedItems.byStart = array;
+ this.orderedItems.byEnd = this._constructByEndArray(array);
- do {
- var isComment = false;
+ stack.orderByStart(this.orderedItems.byStart);
+ stack.orderByEnd(this.orderedItems.byEnd);
+ };
- // 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;
- }
+ /**
+ * Create an array containing all items being a range (having an end date)
+ * @param {Item[]} array
+ * @returns {ItemRange[]}
+ * @private
+ */
+ Group.prototype._constructByEndArray = function(array) {
+ var endArray = [];
- // skip over whitespaces
- while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
- next();
+ for (var i = 0; i < array.length; i++) {
+ if (array[i] instanceof ItemRange) {
+ endArray.push(array[i]);
}
}
- while (isComment);
+ return endArray;
+ };
- // check for end of dot file
- if (c == '') {
- // token is still empty
- tokenType = TOKENTYPE.DELIMITER;
- return;
- }
+ /**
+ * Update the visible items
+ * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
+ * @param {Item[]} visibleItems The previously visible items.
+ * @param {{start: number, end: number}} range Visible range
+ * @return {Item[]} visibleItems The new visible items.
+ * @private
+ */
+ Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) {
+ var initialPosByStart,
+ newVisibleItems = [],
+ i;
- // check for delimiters consisting of 2 characters
- var c2 = c + nextPreview();
- if (DELIMITERS[c2]) {
- tokenType = TOKENTYPE.DELIMITER;
- token = c2;
- next();
- next();
- return;
+ // first check if the items that were in view previously are still in view.
+ // this handles the case for the ItemRange that is both before and after the current one.
+ if (visibleItems.length > 0) {
+ for (i = 0; i < visibleItems.length; i++) {
+ this._checkIfVisible(visibleItems[i], newVisibleItems, range);
+ }
}
- // check for delimiters consisting of 1 character
- if (DELIMITERS[c]) {
- tokenType = TOKENTYPE.DELIMITER;
- token = c;
- next();
- return;
+ // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
+ if (newVisibleItems.length == 0) {
+ initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start');
+ }
+ else {
+ initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
}
- // 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();
+ // use visible search to find a visible ItemRange (only based on endTime)
+ var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end');
- while (isAlphaNumeric(c)) {
- token += c;
- next();
- }
- if (token == 'false') {
- token = false; // convert to boolean
- }
- else if (token == 'true') {
- token = true; // convert to boolean
+ // if we found a initial ID to use, trace it up and down until we meet an invisible item.
+ if (initialPosByStart != -1) {
+ for (i = initialPosByStart; i >= 0; i--) {
+ if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
}
- else if (!isNaN(Number(token))) {
- token = Number(token); // convert to number
+ for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
+ if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
}
- tokenType = TOKENTYPE.IDENTIFIER;
- return;
}
- // 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 we found a initial ID to use, trace it up and down until we meet an invisible item.
+ if (initialPosByEnd != -1) {
+ for (i = initialPosByEnd; i >= 0; i--) {
+ if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
}
- if (c != '"') {
- throw newSyntaxError('End of string " expected');
+ for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
+ if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
}
- 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) + '"');
- }
+ return newVisibleItems;
+ };
- /**
- * Parse a graph.
- * @returns {Object} graph
- */
- function parseGraph() {
- var graph = {};
- first();
- getToken();
- // optional strict keyword
- if (token == 'strict') {
- graph.strict = true;
- getToken();
+ /**
+ * this function checks if an item is invisible. If it is NOT we make it visible
+ * and add it to the global visible items. If it is, return true.
+ *
+ * @param {Item} item
+ * @param {Item[]} visibleItems
+ * @param {{start:number, end:number}} range
+ * @returns {boolean}
+ * @private
+ */
+ Group.prototype._checkIfInvisible = function(item, visibleItems, range) {
+ if (item.isVisible(range)) {
+ if (!item.displayed) item.show();
+ item.repositionX();
+ if (visibleItems.indexOf(item) == -1) {
+ visibleItems.push(item);
+ }
+ return false;
}
-
- // graph or digraph keyword
- if (token == 'graph' || token == 'digraph') {
- graph.type = token;
- getToken();
+ else {
+ if (item.displayed) item.hide();
+ return true;
}
+ };
- // optional graph id
- if (tokenType == TOKENTYPE.IDENTIFIER) {
- graph.id = token;
- getToken();
+ /**
+ * this function is very similar to the _checkIfInvisible() but it does not
+ * return booleans, hides the item if it should not be seen and always adds to
+ * the visibleItems.
+ * this one is for brute forcing and hiding.
+ *
+ * @param {Item} item
+ * @param {Array} visibleItems
+ * @param {{start:number, end:number}} range
+ * @private
+ */
+ Group.prototype._checkIfVisible = function(item, visibleItems, range) {
+ if (item.isVisible(range)) {
+ if (!item.displayed) item.show();
+ // reposition item horizontally
+ item.repositionX();
+ visibleItems.push(item);
}
-
- // open angle bracket
- if (token != '{') {
- throw newSyntaxError('Angle bracket { expected');
+ else {
+ if (item.displayed) item.hide();
}
- getToken();
+ };
- // statements
- parseStatements(graph);
+ module.exports = Group;
- // close angle bracket
- if (token != '}') {
- throw newSyntaxError('Angle bracket } expected');
- }
- getToken();
- // end of file
- if (token !== '') {
- throw newSyntaxError('End of file expected');
- }
- getToken();
+/***/ },
+/* 39 */
+/***/ function(module, exports, __webpack_require__) {
- // remove temporary default properties
- delete graph.node;
- delete graph.edge;
- delete graph.graph;
+ var Hammer = __webpack_require__(15);
+ var util = __webpack_require__(1);
+ var DataSet = __webpack_require__(3);
+ var DataView = __webpack_require__(4);
+ var Component = __webpack_require__(20);
+ var Group = __webpack_require__(38);
+ var ItemBox = __webpack_require__(35);
+ var ItemPoint = __webpack_require__(36);
+ var ItemRange = __webpack_require__(37);
- return graph;
- }
- /**
- * Parse a list with statements.
- * @param {Object} graph
- */
- function parseStatements (graph) {
- while (token !== '' && token != '}') {
- parseStatement(graph);
- if (token == ';') {
- getToken();
- }
- }
- }
+ var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
/**
- * 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
+ * An ItemSet holds a set of items and ranges which can be displayed in a
+ * range. The width is determined by the parent of the ItemSet, and the height
+ * is determined by the size of the items.
+ * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
+ * @param {Object} [options] See ItemSet.setOptions for the available options.
+ * @constructor ItemSet
+ * @extends Component
*/
- function parseStatement(graph) {
- // parse subgraph
- var subgraph = parseSubgraph(graph);
- if (subgraph) {
- // edge statements
- parseEdge(graph, subgraph);
+ function ItemSet(body, options) {
+ this.body = body;
- return;
- }
+ this.defaultOptions = {
+ type: null, // 'box', 'point', 'range'
+ orientation: 'bottom', // 'top' or 'bottom'
+ align: 'center', // alignment of box items
+ stack: true,
+ groupOrder: null,
- // parse an attribute statement
- var attr = parseAttributeStatement(graph);
- if (attr) {
- return;
- }
+ selectable: true,
+ editable: {
+ updateTime: false,
+ updateGroup: false,
+ add: false,
+ remove: false
+ },
- // parse node
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Identifier expected');
- }
- var id = token; // id can be a string or a number
- getToken();
+ onAdd: function (item, callback) {
+ callback(item);
+ },
+ onUpdate: function (item, callback) {
+ callback(item);
+ },
+ onMove: function (item, callback) {
+ callback(item);
+ },
+ onRemove: function (item, callback) {
+ callback(item);
+ },
- 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);
- }
- }
+ margin: {
+ item: {
+ horizontal: 10,
+ vertical: 10
+ },
+ axis: 20
+ },
+ padding: 5
+ };
- /**
- * Parse a subgraph
- * @param {Object} graph parent graph object
- * @return {Object | null} subgraph
- */
- function parseSubgraph (graph) {
- var subgraph = null;
+ // options is shared by this ItemSet and all its items
+ this.options = util.extend({}, this.defaultOptions);
- // optional subgraph keyword
- if (token == 'subgraph') {
- subgraph = {};
- subgraph.type = 'subgraph';
- getToken();
+ // options for getting items from the DataSet with the correct type
+ this.itemOptions = {
+ type: {start: 'Date', end: 'Date'}
+ };
- // optional graph id
- if (tokenType == TOKENTYPE.IDENTIFIER) {
- subgraph.id = token;
- getToken();
- }
- }
+ this.conversion = {
+ toScreen: body.util.toScreen,
+ toTime: body.util.toTime
+ };
+ this.dom = {};
+ this.props = {};
+ this.hammer = null;
- // open angle bracket
- if (token == '{') {
- getToken();
+ var me = this;
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
- if (!subgraph) {
- subgraph = {};
+ // listeners for the DataSet of the items
+ this.itemListeners = {
+ 'add': function (event, params, senderId) {
+ me._onAdd(params.items);
+ },
+ 'update': function (event, params, senderId) {
+ me._onUpdate(params.items);
+ },
+ 'remove': function (event, params, senderId) {
+ me._onRemove(params.items);
}
- subgraph.parent = graph;
- subgraph.node = graph.node;
- subgraph.edge = graph.edge;
- subgraph.graph = graph.graph;
-
- // statements
- parseStatements(subgraph);
+ };
- // close angle bracket
- if (token != '}') {
- throw newSyntaxError('Angle bracket } expected');
+ // listeners for the DataSet of the groups
+ this.groupListeners = {
+ 'add': function (event, params, senderId) {
+ me._onAddGroups(params.items);
+ },
+ 'update': function (event, params, senderId) {
+ me._onUpdateGroups(params.items);
+ },
+ 'remove': function (event, params, senderId) {
+ me._onRemoveGroups(params.items);
}
- 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 = [];
- }
- graph.subgraphs.push(subgraph);
- }
+ this.items = {}; // object with an Item for every data item
+ this.groups = {}; // Group object for every group
+ this.groupIds = [];
- return subgraph;
- }
+ this.selection = []; // list with the ids of all selected nodes
+ this.stackDirty = true; // if true, all items will be restacked on next redraw
- /**
- * 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.
- */
- function parseAttributeStatement (graph) {
- // attribute statements
- if (token == 'node') {
- getToken();
+ this.touchParams = {}; // stores properties while dragging
+ // create the HTML DOM
- // node attributes
- graph.node = parseAttributeList();
- return 'node';
- }
- else if (token == 'edge') {
- getToken();
+ this._create();
- // edge attributes
- graph.edge = parseAttributeList();
- return 'edge';
- }
- else if (token == 'graph') {
- getToken();
+ this.setOptions(options);
+ }
- // graph attributes
- graph.graph = parseAttributeList();
- return 'graph';
- }
+ ItemSet.prototype = new Component();
- return null;
- }
+ // available item types will be registered here
+ ItemSet.types = {
+ box: ItemBox,
+ range: ItemRange,
+ point: ItemPoint
+ };
/**
- * parse a node statement
- * @param {Object} graph
- * @param {String | Number} id
+ * Create the HTML DOM for the ItemSet
*/
- function parseNodeStatement(graph, id) {
- // node statement
- var node = {
- id: id
- };
- var attr = parseAttributeList();
- if (attr) {
- node.attr = attr;
- }
- addNode(graph, node);
+ ItemSet.prototype._create = function(){
+ var frame = document.createElement('div');
+ frame.className = 'itemset';
+ frame['timeline-itemset'] = this;
+ this.dom.frame = frame;
- // edge statements
- parseEdge(graph, id);
- }
+ // create background panel
+ var background = document.createElement('div');
+ background.className = 'background';
+ frame.appendChild(background);
+ this.dom.background = background;
- /**
- * Parse an edge or a series of edges
- * @param {Object} graph
- * @param {String | Number} from Id of the from node
- */
- function parseEdge(graph, from) {
- while (token == '->' || token == '--') {
- var to;
- var type = token;
- getToken();
+ // create foreground panel
+ var foreground = document.createElement('div');
+ foreground.className = 'foreground';
+ frame.appendChild(foreground);
+ this.dom.foreground = foreground;
- 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();
- }
+ // create axis panel
+ var axis = document.createElement('div');
+ axis.className = 'axis';
+ this.dom.axis = axis;
- // parse edge attributes
- var attr = parseAttributeList();
+ // create labelset
+ var labelSet = document.createElement('div');
+ labelSet.className = 'labelset';
+ this.dom.labelSet = labelSet;
- // create edge
- var edge = createEdge(graph, from, to, type, attr);
- addEdge(graph, edge);
+ // create ungrouped Group
+ this._updateUngrouped();
- from = to;
- }
- }
+ // attach event listeners
+ // Note: we bind to the centerContainer for the case where the height
+ // of the center container is larger than of the ItemSet, so we
+ // can click in the empty area to create a new item or deselect an item.
+ this.hammer = Hammer(this.body.dom.centerContainer, {
+ prevent_default: true
+ });
+
+ // drag items when selected
+ this.hammer.on('touch', this._onTouch.bind(this));
+ this.hammer.on('dragstart', this._onDragStart.bind(this));
+ this.hammer.on('drag', this._onDrag.bind(this));
+ this.hammer.on('dragend', this._onDragEnd.bind(this));
+
+ // single select (or unselect) when tapping an item
+ this.hammer.on('tap', this._onSelectItem.bind(this));
+
+ // multi select when holding mouse/touch, or on ctrl+click
+ this.hammer.on('hold', this._onMultiSelectItem.bind(this));
+
+ // add item on doubletap
+ this.hammer.on('doubletap', this._onAddItem.bind(this));
+
+ // attach to the DOM
+ this.show();
+ };
/**
- * Parse a set with attributes,
- * for example [label="1.000", shape=solid]
- * @return {Object | null} attr
+ * Set options for the ItemSet. Existing options will be extended/overwritten.
+ * @param {Object} [options] The following options are available:
+ * {String} type
+ * Default type for the items. Choose from 'box'
+ * (default), 'point', or 'range'. The default
+ * Style can be overwritten by individual items.
+ * {String} align
+ * Alignment for the items, only applicable for
+ * ItemBox. Choose 'center' (default), 'left', or
+ * 'right'.
+ * {String} orientation
+ * Orientation of the item set. Choose 'top' or
+ * 'bottom' (default).
+ * {Function} groupOrder
+ * A sorting function for ordering groups
+ * {Boolean} stack
+ * If true (deafult), items will be stacked on
+ * top of each other.
+ * {Number} margin.axis
+ * Margin between the axis and the items in pixels.
+ * Default is 20.
+ * {Number} margin.item.horizontal
+ * Horizontal margin between items in pixels.
+ * Default is 10.
+ * {Number} margin.item.vertical
+ * Vertical Margin between items in pixels.
+ * Default is 10.
+ * {Number} margin.item
+ * Margin between items in pixels in both horizontal
+ * and vertical direction. Default is 10.
+ * {Number} margin
+ * Set margin for both axis and items in pixels.
+ * {Number} padding
+ * Padding of the contents of an item in pixels.
+ * Must correspond with the items css. Default is 5.
+ * {Boolean} selectable
+ * If true (default), items can be selected.
+ * {Boolean} editable
+ * Set all editable options to true or false
+ * {Boolean} editable.updateTime
+ * Allow dragging an item to an other moment in time
+ * {Boolean} editable.updateGroup
+ * Allow dragging an item to an other group
+ * {Boolean} editable.add
+ * Allow creating new items on double tap
+ * {Boolean} editable.remove
+ * Allow removing items by clicking the delete button
+ * top right of a selected item.
+ * {Function(item: Item, callback: Function)} onAdd
+ * Callback function triggered when an item is about to be added:
+ * when the user double taps an empty space in the Timeline.
+ * {Function(item: Item, callback: Function)} onUpdate
+ * Callback function fired when an item is about to be updated.
+ * This function typically has to show a dialog where the user
+ * change the item. If not implemented, nothing happens.
+ * {Function(item: Item, callback: Function)} onMove
+ * Fired when an item has been moved. If not implemented,
+ * the move action will be accepted.
+ * {Function(item: Item, callback: Function)} onRemove
+ * Fired when an item is about to be deleted.
+ * If not implemented, the item will be always removed.
*/
- function parseAttributeList() {
- var attr = null;
+ ItemSet.prototype.setOptions = function(options) {
+ if (options) {
+ // copy all options that we know
+ var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder'];
+ util.selectiveExtend(fields, this.options, options);
- while (token == '[') {
- getToken();
- attr = {};
- while (token !== '' && token != ']') {
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Attribute name expected');
+ if ('margin' in options) {
+ if (typeof options.margin === 'number') {
+ this.options.margin.axis = options.margin;
+ this.options.margin.item.horizontal = options.margin;
+ this.options.margin.item.vertical = options.margin;
}
- var name = token;
-
- getToken();
- if (token != '=') {
- throw newSyntaxError('Equal sign = expected');
+ else if (typeof options.margin === 'object') {
+ util.selectiveExtend(['axis'], this.options.margin, options.margin);
+ if ('item' in options.margin) {
+ if (typeof options.margin.item === 'number') {
+ this.options.margin.item.horizontal = options.margin.item;
+ this.options.margin.item.vertical = options.margin.item;
+ }
+ else if (typeof options.margin.item === 'object') {
+ util.selectiveExtend(['horizontal', 'vertical'], this.options.margin.item, options.margin.item);
+ }
+ }
}
- getToken();
+ }
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Attribute value expected');
+ if ('editable' in options) {
+ if (typeof options.editable === 'boolean') {
+ this.options.editable.updateTime = options.editable;
+ this.options.editable.updateGroup = options.editable;
+ this.options.editable.add = options.editable;
+ this.options.editable.remove = options.editable;
}
- var value = token;
- setValue(attr, name, value); // name can be a path
-
- getToken();
- if (token ==',') {
- getToken();
+ else if (typeof options.editable === 'object') {
+ util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable);
}
}
- if (token != ']') {
- throw newSyntaxError('Bracket ] expected');
- }
- getToken();
- }
+ // callback functions
+ var addCallback = (function (name) {
+ if (name in options) {
+ var fn = options[name];
+ if (!(fn instanceof Function)) {
+ throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)');
+ }
+ this.options[name] = fn;
+ }
+ }).bind(this);
+ ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(addCallback);
- return attr;
- }
+ // force the itemSet to refresh: options like orientation and margins may be changed
+ this.markDirty();
+ }
+ };
/**
- * Create a syntax error with extra information on current token and index.
- * @param {String} message
- * @returns {SyntaxError} err
+ * Mark the ItemSet dirty so it will refresh everything with next redraw
*/
- function newSyntaxError(message) {
- return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
- }
+ ItemSet.prototype.markDirty = function() {
+ this.groupIds = [];
+ this.stackDirty = true;
+ };
/**
- * Chop off text after a maximum length
- * @param {String} text
- * @param {Number} maxLength
- * @returns {String}
+ * Destroy the ItemSet
*/
- function chop (text, maxLength) {
- return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
- }
+ ItemSet.prototype.destroy = function() {
+ this.hide();
+ this.setItems(null);
+ this.setGroups(null);
+
+ this.hammer = null;
+
+ this.body = null;
+ this.conversion = null;
+ };
/**
- * Execute a function fn for each pair of elements in two arrays
- * @param {Array | *} array1
- * @param {Array | *} array2
- * @param {function} fn
+ * Hide the component from the DOM
*/
- function forEach2(array1, array2, fn) {
- if (array1 instanceof Array) {
- array1.forEach(function (elem1) {
- if (array2 instanceof Array) {
- array2.forEach(function (elem2) {
- fn(elem1, elem2);
- });
- }
- else {
- fn(elem1, array2);
- }
- });
+ ItemSet.prototype.hide = function() {
+ // remove the frame containing the items
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
}
- else {
- if (array2 instanceof Array) {
- array2.forEach(function (elem2) {
- fn(array1, elem2);
- });
- }
- else {
- fn(array1, array2);
- }
+
+ // remove the axis with dots
+ if (this.dom.axis.parentNode) {
+ this.dom.axis.parentNode.removeChild(this.dom.axis);
}
- }
+
+ // remove the labelset containing all group labels
+ if (this.dom.labelSet.parentNode) {
+ this.dom.labelSet.parentNode.removeChild(this.dom.labelSet);
+ }
+ };
/**
- * 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
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
*/
- 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);
- });
+ ItemSet.prototype.show = function() {
+ // show frame containing the items
+ if (!this.dom.frame.parentNode) {
+ this.body.dom.center.appendChild(this.dom.frame);
}
- // copy the edges
- if (dotData.edges) {
- /**
- * Convert an edge in DOT format to an edge with VisGraph format
- * @param {Object} dotEdge
- * @returns {Object} graphEdge
- */
- function convertEdge(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
- }
- }
+ // show axis with dots
+ if (!this.dom.axis.parentNode) {
+ this.body.dom.top.appendChild(this.dom.axis);
+ }
- if (dotEdge.to instanceof Object) {
- to = dotEdge.to.nodes;
- }
- else {
- to = {
- id: dotEdge.to
- }
- }
+ // show labelset containing labels
+ if (!this.dom.labelSet.parentNode) {
+ this.body.dom.left.appendChild(this.dom.labelSet);
+ }
+ };
- if (dotEdge.from instanceof Object && dotEdge.from.edges) {
- dotEdge.from.edges.forEach(function (subEdge) {
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
- }
+ /**
+ * Set selected items by their id. Replaces the current selection
+ * Unknown id's are silently ignored.
+ * @param {string[] | string} [ids] An array with zero or more id's of the items to be
+ * selected, or a single item id. If ids is undefined
+ * or an empty array, all items will be unselected.
+ */
+ ItemSet.prototype.setSelection = function(ids) {
+ var i, ii, id, item;
- 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 (ids == undefined) ids = [];
+ if (!Array.isArray(ids)) ids = [ids];
- if (dotEdge.to instanceof Object && dotEdge.to.edges) {
- dotEdge.to.edges.forEach(function (subEdge) {
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
- }
- });
+ // unselect currently selected items
+ for (i = 0, ii = this.selection.length; i < ii; i++) {
+ id = this.selection[i];
+ item = this.items[id];
+ if (item) item.unselect();
}
- // copy the options
- if (dotData.attr) {
- graphData.options = dotData.attr;
+ // select items
+ this.selection = [];
+ for (i = 0, ii = ids.length; i < ii; i++) {
+ id = ids[i];
+ item = this.items[id];
+ if (item) {
+ this.selection.push(id);
+ item.select();
+ }
}
+ };
- return graphData;
- }
-
- // exports
- exports.parseDOT = parseDOT;
- exports.DOTToGraph = DOTToGraph;
+ /**
+ * Get the selected items by their id
+ * @return {Array} ids The ids of the selected items
+ */
+ ItemSet.prototype.getSelection = function() {
+ return this.selection.concat([]);
+ };
+ /**
+ * Get the id's of the currently visible items.
+ * @returns {Array} The ids of the visible items
+ */
+ ItemSet.prototype.getVisibleItems = function() {
+ var range = this.body.range.getRange();
+ var left = this.body.util.toScreen(range.start);
+ var right = this.body.util.toScreen(range.end);
-/***/ },
-/* 39 */
-/***/ function(module, exports, __webpack_require__) {
+ var ids = [];
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ var group = this.groups[groupId];
+ var rawVisibleItems = group.visibleItems;
-
- function parseGephi(gephiJSON, options) {
- var edges = [];
- var nodes = [];
- this.options = {
- edges: {
- inheritColor: true
- },
- nodes: {
- allowedToMove: false,
- parseColor: false
+ // filter the "raw" set with visibleItems into a set which is really
+ // visible by pixels
+ for (var i = 0; i < rawVisibleItems.length; i++) {
+ var item = rawVisibleItems[i];
+ // TODO: also check whether visible vertically
+ if ((item.left < right) && (item.left + item.width > left)) {
+ ids.push(item.id);
+ }
+ }
}
- };
-
- if (options !== undefined) {
- this.options.nodes['allowedToMove'] = options.allowedToMove | false;
- this.options.nodes['parseColor'] = options.parseColor | false;
- this.options.edges['inheritColor'] = options.inheritColor | true;
}
- 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);
- }
+ return ids;
+ };
- 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;
+ /**
+ * Deselect a selected item
+ * @param {String | Number} id
+ * @private
+ */
+ ItemSet.prototype._deselect = function(id) {
+ var selection = this.selection;
+ for (var i = 0, ii = selection.length; i < ii; i++) {
+ if (selection[i] == id) { // non-strict comparison!
+ selection.splice(i, 1);
+ break;
}
- 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);
}
+ };
- return {nodes:nodes, edges:edges};
- }
+ /**
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
+ */
+ ItemSet.prototype.redraw = function() {
+ var margin = this.options.margin,
+ range = this.body.range,
+ asSize = util.option.asSize,
+ options = this.options,
+ orientation = options.orientation,
+ resized = false,
+ frame = this.dom.frame,
+ editable = options.editable.updateTime || options.editable.updateGroup;
- exports.parseGephi = parseGephi;
+ // update class name
+ frame.className = 'itemset' + (editable ? ' editable' : '');
-/***/ },
-/* 40 */
-/***/ function(module, exports, __webpack_require__) {
+ // reorder the groups (if needed)
+ resized = this._orderGroups() || resized;
- // first check if moment.js is already loaded in the browser window, if so,
- // use this instance. Else, load via commonjs.
- module.exports = (typeof window !== 'undefined') && window['moment'] || __webpack_require__(51);
+ // check whether zoomed (in that case we need to re-stack everything)
+ // TODO: would be nicer to get this as a trigger from Range
+ var visibleInterval = range.end - range.start;
+ var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.props.width != this.props.lastWidth);
+ if (zoomed) this.stackDirty = true;
+ this.lastVisibleInterval = visibleInterval;
+ this.props.lastWidth = this.props.width;
+ // redraw all groups
+ var restack = this.stackDirty,
+ firstGroup = this._firstGroup(),
+ firstMargin = {
+ item: margin.item,
+ axis: margin.axis
+ },
+ nonFirstMargin = {
+ item: margin.item,
+ axis: margin.item.vertical / 2
+ },
+ height = 0,
+ minHeight = margin.axis + margin.item.vertical;
+ util.forEach(this.groups, function (group) {
+ var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin;
+ var groupResized = group.redraw(range, groupMargin, restack);
+ resized = groupResized || resized;
+ height += group.height;
+ });
+ height = Math.max(height, minHeight);
+ this.stackDirty = false;
-/***/ },
-/* 41 */
-/***/ function(module, exports, __webpack_require__) {
+ // update frame height
+ frame.style.height = asSize(height);
- // Only load hammer.js when in a browser environment
- // (loading hammer.js in a node.js environment gives errors)
- if (typeof window !== 'undefined') {
- module.exports = window['Hammer'] || __webpack_require__(52);
- }
- else {
- module.exports = function () {
- throw Error('hammer.js is only available in a browser, not in node.js.');
- }
- }
+ // calculate actual size and position
+ this.props.top = frame.offsetTop;
+ this.props.left = frame.offsetLeft;
+ this.props.width = frame.offsetWidth;
+ this.props.height = height;
+ // reposition axis
+ this.dom.axis.style.top = asSize((orientation == 'top') ?
+ (this.body.domProps.top.height + this.body.domProps.border.top) :
+ (this.body.domProps.top.height + this.body.domProps.centerContainer.height));
+ this.dom.axis.style.left = '0';
-/***/ },
-/* 42 */
-/***/ function(module, exports, __webpack_require__) {
+ // check if this component is resized
+ resized = this._isResized() || resized;
- var Hammer = __webpack_require__(41);
+ return resized;
+ };
/**
- * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
- * @param {Element} element
- * @param {Event} event
+ * Get the first group, aligned with the axis
+ * @return {Group | null} firstGroup
+ * @private
*/
- exports.fakeGesture = function(element, event) {
- var eventType = null;
+ ItemSet.prototype._firstGroup = function() {
+ var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1);
+ var firstGroupId = this.groupIds[firstGroupIndex];
+ var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED];
- // for hammer.js 1.0.5
- // var gesture = Hammer.event.collectEventData(this, eventType, event);
+ return firstGroup || null;
+ };
- // for hammer.js 1.0.6+
- var touches = Hammer.event.getTouchList(event, eventType);
- var gesture = Hammer.event.collectEventData(this, eventType, touches, event);
+ /**
+ * Create or delete the group holding all ungrouped items. This group is used when
+ * there are no groups specified.
+ * @protected
+ */
+ ItemSet.prototype._updateUngrouped = function() {
+ var ungrouped = this.groups[UNGROUPED];
- // on IE in standards mode, no touches are recognized by hammer.js,
- // resulting in NaN values for center.pageX and center.pageY
- if (isNaN(gesture.center.pageX)) {
- gesture.center.pageX = event.pageX;
+ if (this.groupsData) {
+ // remove the group holding all ungrouped items
+ if (ungrouped) {
+ ungrouped.hide();
+ delete this.groups[UNGROUPED];
+ }
}
- if (isNaN(gesture.center.pageY)) {
- gesture.center.pageY = event.pageY;
+ else {
+ // create a group holding all (unfiltered) items
+ if (!ungrouped) {
+ var id = null;
+ var data = null;
+ ungrouped = new Group(id, data, this);
+ this.groups[UNGROUPED] = ungrouped;
+
+ for (var itemId in this.items) {
+ if (this.items.hasOwnProperty(itemId)) {
+ ungrouped.add(this.items[itemId]);
+ }
+ }
+
+ ungrouped.show();
+ }
}
+ };
- return gesture;
+ /**
+ * Get the element for the labelset
+ * @return {HTMLElement} labelSet
+ */
+ ItemSet.prototype.getLabelSet = function() {
+ return this.dom.labelSet;
};
+ /**
+ * Set items
+ * @param {vis.DataSet | null} items
+ */
+ ItemSet.prototype.setItems = function(items) {
+ var me = this,
+ ids,
+ oldItemsData = this.itemsData;
-/***/ },
-/* 43 */
-/***/ function(module, exports, __webpack_require__) {
+ // replace the dataset
+ if (!items) {
+ this.itemsData = null;
+ }
+ else if (items instanceof DataSet || items instanceof DataView) {
+ this.itemsData = items;
+ }
+ else {
+ throw new TypeError('Data must be an instance of DataSet or DataView');
+ }
- // English
- exports['en'] = {
- current: 'current',
- time: 'time'
- };
- exports['en_EN'] = exports['en'];
- exports['en_US'] = exports['en'];
+ if (oldItemsData) {
+ // unsubscribe from old dataset
+ util.forEach(this.itemListeners, function (callback, event) {
+ oldItemsData.off(event, callback);
+ });
- // Dutch
- exports['nl'] = {
- custom: 'aangepaste',
- time: 'tijd'
- };
- exports['nl_NL'] = exports['nl'];
- exports['nl_BE'] = exports['nl'];
+ // remove all drawn items
+ ids = oldItemsData.getIds();
+ this._onRemove(ids);
+ }
+ if (this.itemsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.itemListeners, function (callback, event) {
+ me.itemsData.on(event, callback, id);
+ });
-/***/ },
-/* 44 */
-/***/ function(module, exports, __webpack_require__) {
+ // add all new items
+ ids = this.itemsData.getIds();
+ this._onAdd(ids);
- var Emitter = __webpack_require__(49);
- var Hammer = __webpack_require__(41);
- var util = __webpack_require__(1);
- var DataSet = __webpack_require__(3);
- var DataView = __webpack_require__(4);
- var Range = __webpack_require__(15);
- var TimeAxis = __webpack_require__(27);
- var CurrentTime = __webpack_require__(19);
- var CustomTime = __webpack_require__(20);
- var ItemSet = __webpack_require__(24);
- var Activator = __webpack_require__(48);
+ // update the group holding all ungrouped items
+ this._updateUngrouped();
+ }
+ };
/**
- * Create a timeline visualization
- * @param {HTMLElement} container
- * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
- * @param {Object} [options] See Core.setOptions for the available options.
- * @constructor
+ * Get the current items
+ * @returns {vis.DataSet | null}
*/
- function Core () {}
-
- // turn Core into an event emitter
- Emitter(Core.prototype);
+ ItemSet.prototype.getItems = function() {
+ return this.itemsData;
+ };
/**
- * Create the main DOM for the Core: a root panel containing left, right,
- * top, bottom, content, and background panel.
- * @param {Element} container The container element where the Core will
- * be attached.
- * @private
+ * Set groups
+ * @param {vis.DataSet} groups
*/
- Core.prototype._create = function (container) {
- this.dom = {};
+ ItemSet.prototype.setGroups = function(groups) {
+ var me = this,
+ ids;
- this.dom.root = document.createElement('div');
- this.dom.background = document.createElement('div');
- this.dom.backgroundVertical = document.createElement('div');
- this.dom.backgroundHorizontal = document.createElement('div');
- this.dom.centerContainer = document.createElement('div');
- this.dom.leftContainer = document.createElement('div');
- this.dom.rightContainer = document.createElement('div');
- this.dom.center = document.createElement('div');
- this.dom.left = document.createElement('div');
- this.dom.right = document.createElement('div');
- this.dom.top = document.createElement('div');
- this.dom.bottom = document.createElement('div');
- this.dom.shadowTop = document.createElement('div');
- this.dom.shadowBottom = document.createElement('div');
- this.dom.shadowTopLeft = document.createElement('div');
- this.dom.shadowBottomLeft = document.createElement('div');
- this.dom.shadowTopRight = document.createElement('div');
- this.dom.shadowBottomRight = document.createElement('div');
+ // unsubscribe from current dataset
+ if (this.groupsData) {
+ util.forEach(this.groupListeners, function (callback, event) {
+ me.groupsData.unsubscribe(event, callback);
+ });
- this.dom.root.className = 'vis timeline root';
- this.dom.background.className = 'vispanel background';
- this.dom.backgroundVertical.className = 'vispanel background vertical';
- this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
- this.dom.centerContainer.className = 'vispanel center';
- this.dom.leftContainer.className = 'vispanel left';
- this.dom.rightContainer.className = 'vispanel right';
- this.dom.top.className = 'vispanel top';
- this.dom.bottom.className = 'vispanel bottom';
- this.dom.left.className = 'content';
- this.dom.center.className = 'content';
- this.dom.right.className = 'content';
- this.dom.shadowTop.className = 'shadow top';
- this.dom.shadowBottom.className = 'shadow bottom';
- this.dom.shadowTopLeft.className = 'shadow top';
- this.dom.shadowBottomLeft.className = 'shadow bottom';
- this.dom.shadowTopRight.className = 'shadow top';
- this.dom.shadowBottomRight.className = 'shadow bottom';
+ // remove all drawn groups
+ ids = this.groupsData.getIds();
+ this.groupsData = null;
+ this._onRemoveGroups(ids); // note: this will cause a redraw
+ }
- this.dom.root.appendChild(this.dom.background);
- this.dom.root.appendChild(this.dom.backgroundVertical);
- this.dom.root.appendChild(this.dom.backgroundHorizontal);
- this.dom.root.appendChild(this.dom.centerContainer);
- this.dom.root.appendChild(this.dom.leftContainer);
- this.dom.root.appendChild(this.dom.rightContainer);
- this.dom.root.appendChild(this.dom.top);
- this.dom.root.appendChild(this.dom.bottom);
+ // replace the dataset
+ if (!groups) {
+ this.groupsData = null;
+ }
+ else if (groups instanceof DataSet || groups instanceof DataView) {
+ this.groupsData = groups;
+ }
+ else {
+ throw new TypeError('Data must be an instance of DataSet or DataView');
+ }
- this.dom.centerContainer.appendChild(this.dom.center);
- this.dom.leftContainer.appendChild(this.dom.left);
- this.dom.rightContainer.appendChild(this.dom.right);
+ if (this.groupsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.groupListeners, function (callback, event) {
+ me.groupsData.on(event, callback, id);
+ });
- this.dom.centerContainer.appendChild(this.dom.shadowTop);
- this.dom.centerContainer.appendChild(this.dom.shadowBottom);
- this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
- this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
- this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
- this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
+ // draw all ms
+ ids = this.groupsData.getIds();
+ this._onAddGroups(ids);
+ }
- this.on('rangechange', this.redraw.bind(this));
- this.on('change', this.redraw.bind(this));
- this.on('touch', this._onTouch.bind(this));
- this.on('pinch', this._onPinch.bind(this));
- this.on('dragstart', this._onDragStart.bind(this));
- this.on('drag', this._onDrag.bind(this));
+ // update the group holding all ungrouped items
+ this._updateUngrouped();
- // create event listeners for all interesting events, these events will be
- // emitted via emitter
- this.hammer = Hammer(this.dom.root, {
- prevent_default: true
- });
- this.listeners = {};
+ // update the order of all items in each group
+ this._order();
- var me = this;
- var events = [
- 'touch', 'pinch',
- 'tap', 'doubletap', 'hold',
- 'dragstart', 'drag', 'dragend',
- 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
- ];
- events.forEach(function (event) {
- var listener = function () {
- var args = [event].concat(Array.prototype.slice.call(arguments, 0));
- if (me.isActive()) {
- me.emit.apply(me, args);
- }
- };
- me.hammer.on(event, listener);
- me.listeners[event] = listener;
- });
+ this.body.emitter.emit('change');
+ };
- // size properties of each of the panels
- this.props = {
- root: {},
- background: {},
- centerContainer: {},
- leftContainer: {},
- rightContainer: {},
- center: {},
- left: {},
- right: {},
- top: {},
- bottom: {},
- border: {},
- scrollTop: 0,
- scrollTopMin: 0
- };
- this.touch = {}; // store state information needed for touch events
+ /**
+ * Get the current groups
+ * @returns {vis.DataSet | null} groups
+ */
+ ItemSet.prototype.getGroups = function() {
+ return this.groupsData;
+ };
- // attach the root panel to the provided container
- if (!container) throw new Error('No container provided');
- container.appendChild(this.dom.root);
+ /**
+ * Remove an item by its id
+ * @param {String | Number} id
+ */
+ ItemSet.prototype.removeItem = function(id) {
+ var item = this.itemsData.get(id),
+ dataset = this.itemsData.getDataSet();
+
+ if (item) {
+ // confirm deletion
+ this.options.onRemove(item, function (item) {
+ if (item) {
+ // remove by id here, it is possible that an item has no id defined
+ // itself, so better not delete by the item itself
+ dataset.remove(id);
+ }
+ });
+ }
};
/**
- * Set options. Options will be passed to all components loaded in the Timeline.
- * @param {Object} [options]
- * {String} orientation
- * Vertical orientation for the Timeline,
- * can be 'bottom' (default) or 'top'.
- * {String | Number} width
- * Width for the timeline, a number in pixels or
- * a css string like '1000px' or '75%'. '100%' by default.
- * {String | Number} height
- * Fixed height for the Timeline, a number in pixels or
- * a css string like '400px' or '75%'. If undefined,
- * The Timeline will automatically size such that
- * its contents fit.
- * {String | Number} minHeight
- * Minimum height for the Timeline, a number in pixels or
- * a css string like '400px' or '75%'.
- * {String | Number} maxHeight
- * Maximum height for the Timeline, a number in pixels or
- * a css string like '400px' or '75%'.
- * {Number | Date | String} start
- * Start date for the visible window
- * {Number | Date | String} end
- * End date for the visible window
+ * Handle updated items
+ * @param {Number[]} ids
+ * @protected
*/
- Core.prototype.setOptions = function (options) {
- if (options) {
- // copy the known options
- var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation', 'clickToUse'];
- util.selectiveExtend(fields, this.options, options);
+ ItemSet.prototype._onUpdate = function(ids) {
+ var me = this;
- if ('clickToUse' in options) {
- if (options.clickToUse) {
- this.activator = new Activator(this.dom.root);
+ ids.forEach(function (id) {
+ var itemData = me.itemsData.get(id, me.itemOptions),
+ item = me.items[id],
+ type = itemData.type || me.options.type || (itemData.end ? 'range' : 'box');
+
+ var constructor = ItemSet.types[type];
+
+ if (item) {
+ // update item
+ if (!constructor || !(item instanceof constructor)) {
+ // item type has changed, delete the item and recreate it
+ me._removeItem(item);
+ item = null;
}
else {
- if (this.activator) {
- this.activator.destroy();
- delete this.activator;
- }
+ me._updateItem(item, itemData);
}
}
- // enable/disable autoResize
- this._initAutoResize();
- }
-
- // propagate options to all components
- this.components.forEach(function (component) {
- component.setOptions(options);
+ if (!item) {
+ // create item
+ if (constructor) {
+ item = new constructor(itemData, me.conversion, me.options);
+ item.id = id; // TODO: not so nice setting id afterwards
+ me._addItem(item);
+ }
+ else if (type == 'rangeoverflow') {
+ // TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day
+ throw new TypeError('Item type "rangeoverflow" is deprecated. Use css styling instead: ' +
+ '.vis.timeline .item.range .content {overflow: visible;}');
+ }
+ else {
+ throw new TypeError('Unknown item type "' + type + '"');
+ }
+ }
});
- // TODO: remove deprecation error one day (deprecated since version 0.8.0)
- if (options && options.order) {
- throw new Error('Option order is deprecated. There is no replacement for this feature.');
- }
-
- // redraw everything
- this.redraw();
+ this._order();
+ this.stackDirty = true; // force re-stacking of all items next redraw
+ this.body.emitter.emit('change');
};
/**
- * Returns true when the Timeline is active.
- * @returns {boolean}
+ * Handle added items
+ * @param {Number[]} ids
+ * @protected
*/
- Core.prototype.isActive = function () {
- return !this.activator || this.activator.active;
- };
+ ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
/**
- * Destroy the Core, clean up all DOM elements and event listeners.
+ * Handle removed items
+ * @param {Number[]} ids
+ * @protected
*/
- Core.prototype.destroy = function () {
- // unbind datasets
- this.clear();
-
- // remove all event listeners
- this.off();
-
- // stop checking for changed size
- this._stopAutoResize();
-
- // remove from DOM
- if (this.dom.root.parentNode) {
- this.dom.root.parentNode.removeChild(this.dom.root);
- }
- this.dom = null;
-
- // remove Activator
- if (this.activator) {
- this.activator.destroy();
- delete this.activator;
- }
-
- // cleanup hammer touch events
- for (var event in this.listeners) {
- if (this.listeners.hasOwnProperty(event)) {
- delete this.listeners[event];
+ ItemSet.prototype._onRemove = function(ids) {
+ var count = 0;
+ var me = this;
+ ids.forEach(function (id) {
+ var item = me.items[id];
+ if (item) {
+ count++;
+ me._removeItem(item);
}
- }
- this.listeners = null;
- this.hammer = null;
-
- // give all components the opportunity to cleanup
- this.components.forEach(function (component) {
- component.destroy();
});
- this.body = null;
+ if (count) {
+ // update order
+ this._order();
+ this.stackDirty = true; // force re-stacking of all items next redraw
+ this.body.emitter.emit('change');
+ }
};
-
/**
- * Set a custom time bar
- * @param {Date} time
+ * Update the order of item in all groups
+ * @private
*/
- Core.prototype.setCustomTime = function (time) {
- if (!this.customTime) {
- throw new Error('Cannot get custom time: Custom time bar is not enabled');
- }
-
- this.customTime.setCustomTime(time);
+ ItemSet.prototype._order = function() {
+ // reorder the items in all groups
+ // TODO: optimization: only reorder groups affected by the changed items
+ util.forEach(this.groups, function (group) {
+ group.order();
+ });
};
/**
- * Retrieve the current custom time.
- * @return {Date} customTime
+ * Handle updated groups
+ * @param {Number[]} ids
+ * @private
*/
- Core.prototype.getCustomTime = function() {
- if (!this.customTime) {
- throw new Error('Cannot get custom time: Custom time bar is not enabled');
- }
-
- return this.customTime.getCustomTime();
+ ItemSet.prototype._onUpdateGroups = function(ids) {
+ this._onAddGroups(ids);
};
-
/**
- * Get the id's of the currently visible items.
- * @returns {Array} The ids of the visible items
+ * Handle changed groups
+ * @param {Number[]} ids
+ * @private
*/
- Core.prototype.getVisibleItems = function() {
- return this.itemSet && this.itemSet.getVisibleItems() || [];
- };
+ ItemSet.prototype._onAddGroups = function(ids) {
+ var me = this;
+ ids.forEach(function (id) {
+ var groupData = me.groupsData.get(id);
+ var group = me.groups[id];
+ if (!group) {
+ // check for reserved ids
+ if (id == UNGROUPED) {
+ throw new Error('Illegal group id. ' + id + ' is a reserved id.');
+ }
- /**
- * Clear the Core. By Default, items, groups and options are cleared.
- * Example usage:
- *
- * timeline.clear(); // clear items, groups, and options
- * timeline.clear({options: true}); // clear options only
- *
- * @param {Object} [what] Optionally specify what to clear. By default:
- * {items: true, groups: true, options: true}
- */
- Core.prototype.clear = function(what) {
- // clear items
- if (!what || what.items) {
- this.setItems(null);
- }
+ var groupOptions = Object.create(me.options);
+ util.extend(groupOptions, {
+ height: null
+ });
- // clear groups
- if (!what || what.groups) {
- this.setGroups(null);
- }
+ group = new Group(id, groupData, me);
+ me.groups[id] = group;
- // clear options of timeline and of each of the components
- if (!what || what.options) {
- this.components.forEach(function (component) {
- component.setOptions(component.defaultOptions);
- });
+ // add items with this groupId to the new group
+ for (var itemId in me.items) {
+ if (me.items.hasOwnProperty(itemId)) {
+ var item = me.items[itemId];
+ if (item.data.group == id) {
+ group.add(item);
+ }
+ }
+ }
- this.setOptions(this.defaultOptions); // this will also do a redraw
- }
+ group.order();
+ group.show();
+ }
+ else {
+ // update group
+ group.setData(groupData);
+ }
+ });
+
+ this.body.emitter.emit('change');
};
/**
- * Set Core window such that it fits all items
+ * Handle removed groups
+ * @param {Number[]} ids
+ * @private
*/
- Core.prototype.fit = function() {
- // apply the data range as range
- var dataRange = this.getItemRange();
+ ItemSet.prototype._onRemoveGroups = function(ids) {
+ var groups = this.groups;
+ ids.forEach(function (id) {
+ var group = groups[id];
- // add 5% space on both sides
- var start = dataRange.min;
- var end = dataRange.max;
- if (start != null && end != null) {
- var interval = (end.valueOf() - start.valueOf());
- if (interval <= 0) {
- // prevent an empty interval
- interval = 24 * 60 * 60 * 1000; // 1 day
+ if (group) {
+ group.hide();
+ delete groups[id];
}
- start = new Date(start.valueOf() - interval * 0.05);
- end = new Date(end.valueOf() + interval * 0.05);
- }
+ });
- // skip range set if there is no start and end date
- if (start === null && end === null) {
- return;
- }
+ this.markDirty();
- this.range.setRange(start, end);
+ this.body.emitter.emit('change');
};
-
/**
- * Set the visible window. Both parameters are optional, you can change only
- * start or only end. Syntax:
- *
- * TimeLine.setWindow(start, end)
- * TimeLine.setWindow(range)
- *
- * Where start and end can be a Date, number, or string, and range is an
- * object with properties start and end.
- *
- * @param {Date | Number | String | Object} [start] Start date of visible window
- * @param {Date | Number | String} [end] End date of visible window
+ * Reorder the groups if needed
+ * @return {boolean} changed
+ * @private
*/
- Core.prototype.setWindow = function(start, end) {
- if (arguments.length == 1) {
- var range = arguments[0];
- this.range.setRange(range.start, range.end);
+ ItemSet.prototype._orderGroups = function () {
+ if (this.groupsData) {
+ // reorder the groups
+ var groupIds = this.groupsData.getIds({
+ order: this.options.groupOrder
+ });
+
+ var changed = !util.equalArray(groupIds, this.groupIds);
+ if (changed) {
+ // hide all groups, removes them from the DOM
+ var groups = this.groups;
+ groupIds.forEach(function (groupId) {
+ groups[groupId].hide();
+ });
+
+ // show the groups again, attach them to the DOM in correct order
+ groupIds.forEach(function (groupId) {
+ groups[groupId].show();
+ });
+
+ this.groupIds = groupIds;
+ }
+
+ return changed;
}
else {
- this.range.setRange(start, end);
+ return false;
}
};
/**
- * Get the visible window
- * @return {{start: Date, end: Date}} Visible range
+ * Add a new item
+ * @param {Item} item
+ * @private
*/
- Core.prototype.getWindow = function() {
- var range = this.range.getRange();
- return {
- start: new Date(range.start),
- end: new Date(range.end)
- };
+ ItemSet.prototype._addItem = function(item) {
+ this.items[item.id] = item;
+
+ // add to group
+ var groupId = this.groupsData ? item.data.group : UNGROUPED;
+ var group = this.groups[groupId];
+ if (group) group.add(item);
};
/**
- * Force a redraw of the Core. Can be useful to manually redraw when
- * option autoResize=false
+ * Update an existing item
+ * @param {Item} item
+ * @param {Object} itemData
+ * @private
*/
- Core.prototype.redraw = function() {
- var resized = false,
- options = this.options,
- props = this.props,
- dom = this.dom;
-
- if (!dom) return; // when destroyed
+ ItemSet.prototype._updateItem = function(item, itemData) {
+ var oldGroupId = item.data.group;
- // update class names
- if (options.orientation == 'top') {
- util.addClassName(dom.root, 'top');
- util.removeClassName(dom.root, 'bottom');
+ item.data = itemData;
+ if (item.displayed) {
+ item.redraw();
}
- else {
- util.removeClassName(dom.root, 'top');
- util.addClassName(dom.root, 'bottom');
- }
-
- // update root width and height options
- dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
- dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
- dom.root.style.width = util.option.asSize(options.width, '');
-
- // calculate border widths
- props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
- props.border.right = props.border.left;
- props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
- props.border.bottom = props.border.top;
- var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
- var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
-
- // calculate the heights. If any of the side panels is empty, we set the height to
- // minus the border width, such that the border will be invisible
- props.center.height = dom.center.offsetHeight;
- props.left.height = dom.left.offsetHeight;
- props.right.height = dom.right.offsetHeight;
- props.top.height = dom.top.clientHeight || -props.border.top;
- props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
-
- // TODO: compensate borders when any of the panels is empty.
-
- // apply auto height
- // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
- var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
- var autoHeight = props.top.height + contentHeight + props.bottom.height +
- borderRootHeight + props.border.top + props.border.bottom;
- dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
-
- // calculate heights of the content panels
- props.root.height = dom.root.offsetHeight;
- props.background.height = props.root.height - borderRootHeight;
- var containerHeight = props.root.height - props.top.height - props.bottom.height -
- borderRootHeight;
- props.centerContainer.height = containerHeight;
- props.leftContainer.height = containerHeight;
- props.rightContainer.height = props.leftContainer.height;
-
- // calculate the widths of the panels
- props.root.width = dom.root.offsetWidth;
- props.background.width = props.root.width - borderRootWidth;
- props.left.width = dom.leftContainer.clientWidth || -props.border.left;
- props.leftContainer.width = props.left.width;
- props.right.width = dom.rightContainer.clientWidth || -props.border.right;
- props.rightContainer.width = props.right.width;
- var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
- props.center.width = centerWidth;
- props.centerContainer.width = centerWidth;
- props.top.width = centerWidth;
- props.bottom.width = centerWidth;
-
- // resize the panels
- dom.background.style.height = props.background.height + 'px';
- dom.backgroundVertical.style.height = props.background.height + 'px';
- dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px';
- dom.centerContainer.style.height = props.centerContainer.height + 'px';
- dom.leftContainer.style.height = props.leftContainer.height + 'px';
- dom.rightContainer.style.height = props.rightContainer.height + 'px';
-
- dom.background.style.width = props.background.width + 'px';
- dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
- dom.backgroundHorizontal.style.width = props.background.width + 'px';
- dom.centerContainer.style.width = props.center.width + 'px';
- dom.top.style.width = props.top.width + 'px';
- dom.bottom.style.width = props.bottom.width + 'px';
-
- // reposition the panels
- dom.background.style.left = '0';
- dom.background.style.top = '0';
- dom.backgroundVertical.style.left = props.left.width + 'px';
- dom.backgroundVertical.style.top = '0';
- dom.backgroundHorizontal.style.left = '0';
- dom.backgroundHorizontal.style.top = props.top.height + 'px';
- dom.centerContainer.style.left = props.left.width + 'px';
- dom.centerContainer.style.top = props.top.height + 'px';
- dom.leftContainer.style.left = '0';
- dom.leftContainer.style.top = props.top.height + 'px';
- dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
- dom.rightContainer.style.top = props.top.height + 'px';
- dom.top.style.left = props.left.width + 'px';
- dom.top.style.top = '0';
- dom.bottom.style.left = props.left.width + 'px';
- dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
-
- // update the scrollTop, feasible range for the offset can be changed
- // when the height of the Core or of the contents of the center changed
- this._updateScrollTop();
-
- // reposition the scrollable contents
- var offset = this.props.scrollTop;
- if (options.orientation == 'bottom') {
- offset += Math.max(this.props.centerContainer.height - this.props.center.height -
- this.props.border.top - this.props.border.bottom, 0);
- }
- dom.center.style.left = '0';
- dom.center.style.top = offset + 'px';
- dom.left.style.left = '0';
- dom.left.style.top = offset + 'px';
- dom.right.style.left = '0';
- dom.right.style.top = offset + 'px';
- // show shadows when vertical scrolling is available
- var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
- var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
- dom.shadowTop.style.visibility = visibilityTop;
- dom.shadowBottom.style.visibility = visibilityBottom;
- dom.shadowTopLeft.style.visibility = visibilityTop;
- dom.shadowBottomLeft.style.visibility = visibilityBottom;
- dom.shadowTopRight.style.visibility = visibilityTop;
- dom.shadowBottomRight.style.visibility = visibilityBottom;
+ // update group
+ if (oldGroupId != item.data.group) {
+ var oldGroup = this.groups[oldGroupId];
+ if (oldGroup) oldGroup.remove(item);
- // redraw all components
- this.components.forEach(function (component) {
- resized = component.redraw() || resized;
- });
- if (resized) {
- // keep repainting until all sizes are settled
- this.redraw();
+ var groupId = this.groupsData ? item.data.group : UNGROUPED;
+ var group = this.groups[groupId];
+ if (group) group.add(item);
}
};
- // TODO: deprecated since version 1.1.0, remove some day
- Core.prototype.repaint = function () {
- throw new Error('Function repaint is deprecated. Use redraw instead.');
- };
-
/**
- * Convert a position on screen (pixels) to a datetime
- * @param {int} x Position on the screen in pixels
- * @return {Date} time The datetime the corresponds with given position x
+ * Delete an item from the ItemSet: remove it from the DOM, from the map
+ * with items, and from the map with visible items, and from the selection
+ * @param {Item} item
* @private
*/
- // TODO: move this function to Range
- Core.prototype._toTime = function(x) {
- var conversion = this.range.conversion(this.props.center.width);
- return new Date(x / conversion.scale + conversion.offset);
- };
+ ItemSet.prototype._removeItem = function(item) {
+ // remove from DOM
+ item.hide();
+ // remove from items
+ delete this.items[item.id];
- /**
- * Convert a position on the global screen (pixels) to a datetime
- * @param {int} x Position on the screen in pixels
- * @return {Date} time The datetime the corresponds with given position x
- * @private
- */
- // TODO: move this function to Range
- Core.prototype._toGlobalTime = function(x) {
- var conversion = this.range.conversion(this.props.root.width);
- return new Date(x / conversion.scale + conversion.offset);
- };
+ // remove from selection
+ var index = this.selection.indexOf(item.id);
+ if (index != -1) this.selection.splice(index, 1);
- /**
- * Convert a datetime (Date object) into a position on the screen
- * @param {Date} time A date
- * @return {int} x The position on the screen in pixels which corresponds
- * with the given date.
- * @private
- */
- // TODO: move this function to Range
- Core.prototype._toScreen = function(time) {
- var conversion = this.range.conversion(this.props.center.width);
- return (time.valueOf() - conversion.offset) * conversion.scale;
+ // remove from group
+ var groupId = this.groupsData ? item.data.group : UNGROUPED;
+ var group = this.groups[groupId];
+ if (group) group.remove(item);
};
-
/**
- * Convert a datetime (Date object) into a position on the root
- * This is used to get the pixel density estimate for the screen, not the center panel
- * @param {Date} time A date
- * @return {int} x The position on root in pixels which corresponds
- * with the given date.
+ * Create an array containing all items being a range (having an end date)
+ * @param array
+ * @returns {Array}
* @private
*/
- // TODO: move this function to Range
- Core.prototype._toGlobalScreen = function(time) {
- var conversion = this.range.conversion(this.props.root.width);
- return (time.valueOf() - conversion.offset) * conversion.scale;
- };
+ ItemSet.prototype._constructByEndArray = function(array) {
+ var endArray = [];
+ for (var i = 0; i < array.length; i++) {
+ if (array[i] instanceof ItemRange) {
+ endArray.push(array[i]);
+ }
+ }
+ return endArray;
+ };
/**
- * Initialize watching when option autoResize is true
+ * Register the clicked item on touch, before dragStart is initiated.
+ *
+ * dragStart is initiated from a mousemove event, which can have left the item
+ * already resulting in an item == null
+ *
+ * @param {Event} event
* @private
*/
- Core.prototype._initAutoResize = function () {
- if (this.options.autoResize == true) {
- this._startAutoResize();
- }
- else {
- this._stopAutoResize();
- }
+ ItemSet.prototype._onTouch = function (event) {
+ // store the touched item, used in _onDragStart
+ this.touchParams.item = ItemSet.itemFromTarget(event);
};
/**
- * Watch for changes in the size of the container. On resize, the Panel will
- * automatically redraw itself.
+ * Start dragging the selected events
+ * @param {Event} event
* @private
*/
- Core.prototype._startAutoResize = function () {
- var me = this;
+ ItemSet.prototype._onDragStart = function (event) {
+ if (!this.options.editable.updateTime && !this.options.editable.updateGroup) {
+ return;
+ }
- this._stopAutoResize();
+ var item = this.touchParams.item || null,
+ me = this,
+ props;
- this._onResize = function() {
- if (me.options.autoResize != true) {
- // stop watching when the option autoResize is changed to false
- me._stopAutoResize();
- return;
- }
+ if (item && item.selected) {
+ var dragLeftItem = event.target.dragLeftItem;
+ var dragRightItem = event.target.dragRightItem;
- if (me.dom.root) {
- // check whether the frame is resized
- if ((me.dom.root.clientWidth != me.props.lastWidth) ||
- (me.dom.root.clientHeight != me.props.lastHeight)) {
- me.props.lastWidth = me.dom.root.clientWidth;
- me.props.lastHeight = me.dom.root.clientHeight;
+ if (dragLeftItem) {
+ props = {
+ item: dragLeftItem
+ };
- me.emit('change');
+ if (me.options.editable.updateTime) {
+ props.start = item.data.start.valueOf();
+ }
+ if (me.options.editable.updateGroup) {
+ if ('group' in item.data) props.group = item.data.group;
}
- }
- };
-
- // add event listener to window resize
- util.addEventListener(window, 'resize', this._onResize);
- this.watchTimer = setInterval(this._onResize, 1000);
- };
+ this.touchParams.itemProps = [props];
+ }
+ else if (dragRightItem) {
+ props = {
+ item: dragRightItem
+ };
- /**
- * Stop watching for a resize of the frame.
- * @private
- */
- Core.prototype._stopAutoResize = function () {
- if (this.watchTimer) {
- clearInterval(this.watchTimer);
- this.watchTimer = undefined;
- }
+ if (me.options.editable.updateTime) {
+ props.end = item.data.end.valueOf();
+ }
+ if (me.options.editable.updateGroup) {
+ if ('group' in item.data) props.group = item.data.group;
+ }
- // remove event listener on window.resize
- util.removeEventListener(window, 'resize', this._onResize);
- this._onResize = null;
- };
+ this.touchParams.itemProps = [props];
+ }
+ else {
+ this.touchParams.itemProps = this.getSelection().map(function (id) {
+ var item = me.items[id];
+ var props = {
+ item: item
+ };
- /**
- * Start moving the timeline vertically
- * @param {Event} event
- * @private
- */
- Core.prototype._onTouch = function (event) {
- this.touch.allowDragging = true;
+ if (me.options.editable.updateTime) {
+ if ('start' in item.data) props.start = item.data.start.valueOf();
+ if ('end' in item.data) props.end = item.data.end.valueOf();
+ }
+ if (me.options.editable.updateGroup) {
+ if ('group' in item.data) props.group = item.data.group;
+ }
+
+ return props;
+ });
+ }
+
+ event.stopPropagation();
+ }
};
/**
- * Start moving the timeline vertically
+ * Drag selected items
* @param {Event} event
* @private
*/
- Core.prototype._onPinch = function (event) {
- this.touch.allowDragging = false;
+ ItemSet.prototype._onDrag = function (event) {
+ if (this.touchParams.itemProps) {
+ var range = this.body.range,
+ snap = this.body.util.snap || null,
+ deltaX = event.gesture.deltaX,
+ scale = (this.props.width / (range.end - range.start)),
+ offset = deltaX / scale;
+
+ // move
+ this.touchParams.itemProps.forEach(function (props) {
+ if ('start' in props) {
+ var start = new Date(props.start + offset);
+ props.item.data.start = snap ? snap(start) : start;
+ }
+
+ if ('end' in props) {
+ var end = new Date(props.end + offset);
+ props.item.data.end = snap ? snap(end) : end;
+ }
+
+ if ('group' in props) {
+ // drag from one group to another
+ var group = ItemSet.groupFromTarget(event);
+ _moveToGroup(props.item, group);
+ }
+ });
+
+ // TODO: implement onMoving handler
+
+ this.stackDirty = true; // force re-stacking of all items next redraw
+ this.body.emitter.emit('change');
+
+ event.stopPropagation();
+ }
};
/**
- * Start moving the timeline vertically
- * @param {Event} event
+ * Move an item to another group
+ * @param {Item} item
+ * @param {Group} group
* @private
*/
- Core.prototype._onDragStart = function (event) {
- this.touch.initialScrollTop = this.props.scrollTop;
- };
+ function _moveToGroup (item, group) {
+ if (group && group.groupId != item.data.group) {
+ var oldGroup = item.parent;
+ oldGroup.remove(item);
+ oldGroup.order();
+ group.add(item);
+ group.order();
+
+ item.data.group = group.groupId;
+ }
+ }
/**
- * Move the timeline vertically
+ * End of dragging selected items
* @param {Event} event
* @private
*/
- Core.prototype._onDrag = function (event) {
- // refuse to drag when we where pinching to prevent the timeline make a jump
- // when releasing the fingers in opposite order from the touch screen
- if (!this.touch.allowDragging) return;
+ ItemSet.prototype._onDragEnd = function (event) {
+ if (this.touchParams.itemProps) {
+ // prepare a change set for the changed items
+ var changes = [],
+ me = this,
+ dataset = this.itemsData.getDataSet();
- var delta = event.gesture.deltaY;
+ var itemProps = this.touchParams.itemProps ;
+ this.touchParams.itemProps = null;
+ itemProps.forEach(function (props) {
+ var id = props.item.id,
+ itemData = me.itemsData.get(id, me.itemOptions);
- var oldScrollTop = this._getScrollTop();
- var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
+ var changed = false;
+ if ('start' in props.item.data) {
+ changed = (props.start != props.item.data.start.valueOf());
+ itemData.start = util.convert(props.item.data.start,
+ dataset._options.type && dataset._options.type.start || 'Date');
+ }
+ if ('end' in props.item.data) {
+ changed = changed || (props.end != props.item.data.end.valueOf());
+ itemData.end = util.convert(props.item.data.end,
+ dataset._options.type && dataset._options.type.end || 'Date');
+ }
+ if ('group' in props.item.data) {
+ changed = changed || (props.group != props.item.data.group);
+ itemData.group = props.item.data.group;
+ }
- if (newScrollTop != oldScrollTop) {
- this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
+ // only apply changes when start or end is actually changed
+ if (changed) {
+ me.options.onMove(itemData, function (itemData) {
+ if (itemData) {
+ // apply changes
+ itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined)
+ changes.push(itemData);
+ }
+ else {
+ // restore original values
+ if ('start' in props) props.item.data.start = props.start;
+ if ('end' in props) props.item.data.end = props.end;
+ if ('group' in props && props.item.data.group != props.group) {
+ var group = me.groups[props.group];
+ _moveToGroup(props.item, group);
+ }
+
+ me.stackDirty = true; // force re-stacking of all items next redraw
+ me.body.emitter.emit('change');
+ }
+ });
+ }
+ });
+
+ // apply the changes to the data (if there are changes)
+ if (changes.length) {
+ dataset.update(changes);
+ }
+
+ event.stopPropagation();
}
};
/**
- * Apply a scrollTop
- * @param {Number} scrollTop
- * @returns {Number} scrollTop Returns the applied scrollTop
+ * Handle selecting/deselecting an item when tapping it
+ * @param {Event} event
* @private
*/
- Core.prototype._setScrollTop = function (scrollTop) {
- this.props.scrollTop = scrollTop;
- this._updateScrollTop();
- return this.props.scrollTop;
- };
+ ItemSet.prototype._onSelectItem = function (event) {
+ if (!this.options.selectable) return;
- /**
- * Update the current scrollTop when the height of the containers has been changed
- * @returns {Number} scrollTop Returns the applied scrollTop
- * @private
- */
- Core.prototype._updateScrollTop = function () {
- // recalculate the scrollTopMin
- var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
- if (scrollTopMin != this.props.scrollTopMin) {
- // in case of bottom orientation, change the scrollTop such that the contents
- // do not move relative to the time axis at the bottom
- if (this.options.orientation == 'bottom') {
- this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
- }
- this.props.scrollTopMin = scrollTopMin;
+ var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
+ var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
+ if (ctrlKey || shiftKey) {
+ this._onMultiSelectItem(event);
+ return;
}
- // limit the scrollTop to the feasible scroll range
- if (this.props.scrollTop > 0) this.props.scrollTop = 0;
- if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
+ var oldSelection = this.getSelection();
- return this.props.scrollTop;
+ var item = ItemSet.itemFromTarget(event);
+ var selection = item ? [item.id] : [];
+ this.setSelection(selection);
+
+ var newSelection = this.getSelection();
+
+ // emit a select event,
+ // except when old selection is empty and new selection is still empty
+ if (newSelection.length > 0 || oldSelection.length > 0) {
+ this.body.emitter.emit('select', {
+ items: this.getSelection()
+ });
+ }
+
+ event.stopPropagation();
};
/**
- * Get the current scrollTop
- * @returns {number} scrollTop
+ * Handle creation and updates of an item on double tap
+ * @param event
* @private
*/
- Core.prototype._getScrollTop = function () {
- return this.props.scrollTop;
- };
+ ItemSet.prototype._onAddItem = function (event) {
+ if (!this.options.selectable) return;
+ if (!this.options.editable.add) return;
- module.exports = Core;
+ var me = this,
+ snap = this.body.util.snap || null,
+ item = ItemSet.itemFromTarget(event);
+ if (item) {
+ // update item
-/***/ },
-/* 45 */
-/***/ function(module, exports, __webpack_require__) {
+ // execute async handler to update the item (or cancel it)
+ var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
+ this.options.onUpdate(itemData, function (itemData) {
+ if (itemData) {
+ me.itemsData.update(itemData);
+ }
+ });
+ }
+ else {
+ // add item
+ var xAbs = util.getAbsoluteLeft(this.dom.frame);
+ var x = event.gesture.center.pageX - xAbs;
+ var start = this.body.util.toTime(x);
+ var newItem = {
+ start: snap ? snap(start) : start,
+ content: 'new item'
+ };
- // 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'];
+ // when default type is a range, add a default end date to the new item
+ if (this.options.type === 'range') {
+ var end = this.body.util.toTime(x + this.props.width / 5);
+ newItem.end = snap ? snap(end) : end;
+ }
- // 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'];
+ newItem[this.itemsData.fieldId] = util.randomUUID();
+ var group = ItemSet.groupFromTarget(event);
+ if (group) {
+ newItem.group = group.groupId;
+ }
-/***/ },
-/* 46 */
-/***/ function(module, exports, __webpack_require__) {
+ // execute async handler to customize (or cancel) adding an item
+ this.options.onAdd(newItem, function (item) {
+ if (item) {
+ me.itemsData.add(newItem);
+ // TODO: need to trigger a redraw?
+ }
+ });
+ }
+ };
/**
- * Canvas shapes used by Network
+ * Handle selecting/deselecting multiple items when holding an item
+ * @param {Event} event
+ * @private
*/
- if (typeof CanvasRenderingContext2D !== 'undefined') {
-
- /**
- * Draw a circle shape
- */
- CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
- this.beginPath();
- this.arc(x, y, r, 0, 2*Math.PI, false);
- };
+ ItemSet.prototype._onMultiSelectItem = function (event) {
+ if (!this.options.selectable) return;
- /**
- * 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);
- };
+ var selection,
+ item = ItemSet.itemFromTarget(event);
- /**
- * 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();
+ if (item) {
+ // multi select items
+ selection = this.getSelection(); // current selection
+ var index = selection.indexOf(item.id);
+ if (index == -1) {
+ // item is not yet selected -> select it
+ selection.push(item.id);
+ }
+ else {
+ // item is already selected -> deselect it
+ selection.splice(index, 1);
+ }
+ this.setSelection(selection);
- 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.body.emitter.emit('select', {
+ items: this.getSelection()
+ });
- 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();
- };
+ event.stopPropagation();
+ }
+ };
- /**
- * 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();
+ /**
+ * Find an item from an event target:
+ * searches for the attribute 'timeline-item' in the event target's element tree
+ * @param {Event} event
+ * @return {Item | null} item
+ */
+ ItemSet.itemFromTarget = function(event) {
+ var target = event.target;
+ while (target) {
+ if (target.hasOwnProperty('timeline-item')) {
+ return target['timeline-item'];
+ }
+ target = target.parentNode;
+ }
- 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
+ return null;
+ };
- 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();
- };
+ /**
+ * Find the Group from an event target:
+ * searches for the attribute 'timeline-group' in the event target's element tree
+ * @param {Event} event
+ * @return {Group | null} group
+ */
+ ItemSet.groupFromTarget = function(event) {
+ var target = event.target;
+ while (target) {
+ if (target.hasOwnProperty('timeline-group')) {
+ return target['timeline-group'];
+ }
+ target = target.parentNode;
+ }
- /**
- * 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();
+ return null;
+ };
- 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)
- );
+ /**
+ * Find the ItemSet from an event target:
+ * searches for the attribute 'timeline-itemset' in the event target's element tree
+ * @param {Event} event
+ * @return {ItemSet | null} item
+ */
+ ItemSet.itemSetFromTarget = function(event) {
+ var target = event.target;
+ while (target) {
+ if (target.hasOwnProperty('timeline-itemset')) {
+ return target['timeline-itemset'];
}
+ target = target.parentNode;
+ }
- this.closePath();
- };
+ return null;
+ };
- /**
- * 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
- }
+ module.exports = ItemSet;
/***/ },
-/* 47 */
+/* 40 */
/***/ function(module, exports, __webpack_require__) {
- var PhysicsMixin = __webpack_require__(59);
- var ClusterMixin = __webpack_require__(54);
- var SectorsMixin = __webpack_require__(55);
- var SelectionMixin = __webpack_require__(53);
- var ManipulationMixin = __webpack_require__(58);
- var NavigationMixin = __webpack_require__(56);
- var HierarchicalLayoutMixin = __webpack_require__(57);
-
- /**
- * Load a mixin into the network object
- *
- * @param {Object} sourceVariable | this object has to contain functions.
- * @private
- */
- exports._loadMixin = function (sourceVariable) {
- for (var mixinFunction in sourceVariable) {
- if (sourceVariable.hasOwnProperty(mixinFunction)) {
- this[mixinFunction] = sourceVariable[mixinFunction];
- }
- }
- };
-
-
- /**
- * removes a mixin from the network object.
- *
- * @param {Object} sourceVariable | this object has to contain functions.
- * @private
- */
- exports._clearMixin = function (sourceVariable) {
- for (var mixinFunction in sourceVariable) {
- if (sourceVariable.hasOwnProperty(mixinFunction)) {
- this[mixinFunction] = undefined;
- }
- }
- };
+ var Emitter = __webpack_require__(6);
+ var Hammer = __webpack_require__(15);
+ var mousetrap = __webpack_require__(41);
+ var util = __webpack_require__(1);
+ var hammerUtil = __webpack_require__(42);
+ var DataSet = __webpack_require__(3);
+ var DataView = __webpack_require__(4);
+ var dotparser = __webpack_require__(43);
+ var gephiParser = __webpack_require__(44);
+ var Groups = __webpack_require__(45);
+ var Images = __webpack_require__(46);
+ var Node = __webpack_require__(47);
+ var Edge = __webpack_require__(48);
+ var Popup = __webpack_require__(49);
+ var MixinLoader = __webpack_require__(50);
+ var Activator = __webpack_require__(61);
+ var locales = __webpack_require__(62);
+ // Load custom shapes into CanvasRenderingContext2D
+ __webpack_require__(63);
/**
- * Mixin the physics system and initialize the parameters required.
+ * @constructor Network
+ * Create a network visualization, displaying nodes and edges.
*
- * @private
+ * @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
*/
- exports._loadPhysicsSystem = function () {
- this._loadMixin(PhysicsMixin);
- this._loadSelectedForceSolver();
- if (this.constants.configurePhysics == true) {
- this._loadPhysicsConfiguration();
+ function Network (container, data, options) {
+ if (!(this instanceof Network)) {
+ throw new SyntaxError('Constructor must be called with the new operator');
}
- };
-
-
- /**
- * Mixin the cluster system and initialize the parameters required.
- *
- * @private
- */
- exports._loadClusterSystem = function () {
- this.clusterSession = 0;
- this.hubThreshold = 5;
- this._loadMixin(ClusterMixin);
- };
-
-
- /**
- * Mixin the sector system and initialize the parameters required
- *
- * @private
- */
- exports._loadSectorSystem = function () {
- this.sectors = {};
- this.activeSector = ["default"];
- this.sectors["active"] = {};
- this.sectors["active"]["default"] = {"nodes": {},
- "edges": {},
- "nodeIndices": [],
- "formationScale": 1.0,
- "drawingNode": undefined };
- this.sectors["frozen"] = {};
- this.sectors["support"] = {"nodes": {},
- "edges": {},
- "nodeIndices": [],
- "formationScale": 1.0,
- "drawingNode": undefined };
- this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
-
- this._loadMixin(SectorsMixin);
- };
-
-
- /**
- * Mixin the selection system and initialize the parameters required
- *
- * @private
- */
- exports._loadSelectionSystem = function () {
- this.selectionObj = {nodes: {}, edges: {}};
-
- this._loadMixin(SelectionMixin);
- };
+ this._initializeMixinLoaders();
+ // create variables and set default values
+ this.containerElement = container;
- /**
- * Mixin the navigationUI (User Interface) system and initialize the parameters required
- *
- * @private
- */
- exports._loadManipulationSystem = function () {
- // reset global variables -- these are used by the selection of nodes and edges.
- this.blockConnectingEdgeSelection = false;
- this.forceAppendSelection = false;
+ // render and calculation settings
+ this.renderRefreshRate = 60; // hz (fps)
+ this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
+ this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
+ this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
+ this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation
- if (this.constants.dataManipulation.enabled == true) {
- // load the manipulator HTML elements. All styling done in css.
- if (this.manipulationDiv === undefined) {
- this.manipulationDiv = document.createElement('div');
- this.manipulationDiv.className = 'network-manipulationDiv';
- this.manipulationDiv.id = 'network-manipulationDiv';
- if (this.editMode == true) {
- this.manipulationDiv.style.display = "block";
- }
- else {
- this.manipulationDiv.style.display = "none";
- }
- this.frame.appendChild(this.manipulationDiv);
- }
+ this.initializing = true;
- if (this.editModeDiv === undefined) {
- this.editModeDiv = document.createElement('div');
- this.editModeDiv.className = 'network-manipulation-editMode';
- this.editModeDiv.id = 'network-manipulation-editMode';
- if (this.editMode == true) {
- this.editModeDiv.style.display = "none";
- }
- else {
- this.editModeDiv.style.display = "block";
- }
- this.frame.appendChild(this.editModeDiv);
- }
+ this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
- if (this.closeDiv === undefined) {
- this.closeDiv = document.createElement('div');
- this.closeDiv.className = 'network-manipulation-closeDiv';
- this.closeDiv.id = 'network-manipulation-closeDiv';
- this.closeDiv.style.display = this.manipulationDiv.style.display;
- this.frame.appendChild(this.closeDiv);
- }
-
- // load the manipulation functions
- this._loadMixin(ManipulationMixin);
+ // set constant values
+ this.defaultOptions = {
+ nodes: {
+ mass: 1,
+ radiusMin: 10,
+ radiusMax: 30,
+ radius: 10,
+ shape: 'ellipse',
+ image: undefined,
+ widthMin: 16, // px
+ widthMax: 64, // px
+ fixed: false,
+ fontColor: 'black',
+ fontSize: 14, // px
+ fontFace: 'verdana',
+ level: -1,
+ color: {
+ border: '#2B7CE9',
+ background: '#97C2FC',
+ highlight: {
+ border: '#2B7CE9',
+ background: '#D2E5FF'
+ },
+ hover: {
+ border: '#2B7CE9',
+ background: '#D2E5FF'
+ }
+ },
+ borderColor: '#2B7CE9',
+ backgroundColor: '#97C2FC',
+ highlightColor: '#D2E5FF',
+ group: undefined,
+ borderWidth: 1
+ },
+ 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,
+ theta: 1 / 0.6, // 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
+ },
+ freezeForStabilization: false,
+ smoothCurves: {
+ enabled: true,
+ dynamic: true,
+ type: "continuous",
+ roundness: 0.5
+ },
+ dynamicSmoothCurves: true,
+ maxVelocity: 30,
+ minVelocity: 0.1, // px/s
+ stabilize: true, // stabilize before displaying the network
+ stabilizationIterations: 1000, // maximum number of iteration to stabilize
+ 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);
- // create the manipulator toolbar
- this._createManipulatorBar();
- }
- else {
- if (this.manipulationDiv !== undefined) {
- // removes all the bindings and overloads
- this._createManipulatorBar();
- // remove the manipulation divs
- this.containerElement.removeChild(this.manipulationDiv);
- this.containerElement.removeChild(this.editModeDiv);
- this.containerElement.removeChild(this.closeDiv);
+ this.hoverObj = {nodes:{},edges:{}};
+ this.controlNodesActive = false;
- this.manipulationDiv = undefined;
- this.editModeDiv = undefined;
- this.closeDiv = undefined;
- // remove the mixin functions
- this._clearMixin(ManipulationMixin);
- }
- }
- };
+ // 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;
- /**
- * Mixin the navigation (User Interface) system and initialize the parameters required
- *
- * @private
- */
- exports._loadNavigationControls = function () {
- this._loadMixin(NavigationMixin);
+ // 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();
- // the clean function removes the button divs, this is done to remove the bindings.
- this._cleanNavigation();
- if (this.constants.navigation.enabled == true) {
- this._loadNavigationElements();
- }
- };
+ // 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 = {};
- /**
- * Mixin the hierarchical layout system.
- *
- * @private
- */
- exports._loadHierarchySystem = function () {
- this._loadMixin(HierarchicalLayoutMixin);
- };
+ // 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
-/***/ },
-/* 48 */
-/***/ function(module, exports, __webpack_require__) {
-
- var mousetrap = __webpack_require__(50);
- var Emitter = __webpack_require__(49);
- var Hammer = __webpack_require__(41);
- var util = __webpack_require__(1);
-
- /**
- * Turn an element into an clickToUse element.
- * When not active, the element has a transparent overlay. When the overlay is
- * clicked, the mode is changed to active.
- * When active, the element is displayed with a blue border around it, and
- * the interactive contents of the element can be used. When clicked outside
- * the element, the elements mode is changed to inactive.
- * @param {Element} container
- * @constructor
- */
- function Activator(container) {
- this.active = false;
+ // datasets or dataviews
+ this.nodesData = null; // A DataSet or DataView
+ this.edgesData = null; // A DataSet or DataView
- this.dom = {
- container: container
+ // 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);
+ 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();
+ }
};
- this.dom.overlay = document.createElement('div');
- this.dom.overlay.className = 'overlay';
-
- this.dom.container.appendChild(this.dom.overlay);
-
- this.hammer = Hammer(this.dom.overlay, {prevent_default: false});
- this.hammer.on('tap', this._onTapOverlay.bind(this));
+ // properties for the animation
+ this.moving = true;
+ this.timer = undefined; // Scheduling function. Is definded in this.start();
- // block all touch events (except tap)
- var me = this;
- var events = [
- 'touch', 'pinch',
- 'doubletap', 'hold',
- 'dragstart', 'drag', 'dragend',
- 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
- ];
- events.forEach(function (event) {
- me.hammer.on(event, function (event) {
- event.stopPropagation();
- });
- });
+ // 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);
- // attach a tap event to the window, in order to deactivate when clicking outside the timeline
- this.windowHammer = Hammer(window, {prevent_default: false});
- this.windowHammer.on('tap', function (event) {
- // deactivate when clicked outside the container
- if (!_hasParent(event.target, container)) {
- me.deactivate();
+ // 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(true,this.constants.clustering.enabled);
}
- });
+ }
- // mousetrap listener only bounded when active)
- this.escListener = this.deactivate.bind(this);
+ // if clustering is disabled, the simulation will have started in the setData function
+ if (this.constants.clustering.enabled) {
+ this.startWithClustering();
+ }
}
- // turn into an event emitter
- Emitter(Activator.prototype);
-
- // The currently active activator
- Activator.current = null;
+ // Extend Network with an Emitter mixin
+ Emitter(Network.prototype);
/**
- * Destroy the activator. Cleans up all created DOM and event listeners
+ * 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
*/
- Activator.prototype.destroy = function () {
- this.deactivate();
-
- // remove dom
- this.dom.overlay.parentNode.removeChild(this.dom.overlay);
-
- // cleanup hammer instances
- this.hammer = null;
- this.windowHammer = null;
- // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory)
- };
+ Network.prototype._getScriptPath = function() {
+ var scripts = document.getElementsByTagName( 'script' );
- /**
- * Activate the element
- * Overlay is hidden, element is decorated with a blue shadow border
- */
- Activator.prototype.activate = function () {
- // we allow only one active activator at a time
- if (Activator.current) {
- Activator.current.deactivate();
+ // 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);
+ }
}
- Activator.current = this;
-
- this.active = true;
- this.dom.overlay.style.display = 'none';
- util.addClassName(this.dom.container, 'vis-active');
-
- this.emit('change');
- this.emit('activate');
-
- // ugly hack: bind ESC after emitting the events, as the Network rebinds all
- // keyboard events on a 'change' event
- mousetrap.bind('esc', this.escListener);
- };
-
- /**
- * Deactivate the element
- * Overlay is displayed on top of the element
- */
- Activator.prototype.deactivate = function () {
- this.active = false;
- this.dom.overlay.style.display = '';
- util.removeClassName(this.dom.container, 'vis-active');
- mousetrap.unbind('esc', this.escListener);
- this.emit('change');
- this.emit('deactivate');
+ return null;
};
- /**
- * Handle a tap event: activate the container
- * @param event
- * @private
- */
- Activator.prototype._onTapOverlay = function (event) {
- // activate the container
- this.activate();
- event.stopPropagation();
- };
/**
- * Test whether the element has the requested parent element somewhere in
- * its chain of parent nodes.
- * @param {HTMLElement} element
- * @param {HTMLElement} parent
- * @returns {boolean} Returns true when the parent is found somewhere in the
- * chain of parent nodes.
+ * Find the center position of the network
* @private
*/
- function _hasParent(element, parent) {
- while (element) {
- if (element === parent) {
- return true
+ 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.x)) {minX = node.x;}
+ if (maxX < (node.x)) {maxX = node.x;}
+ if (minY > (node.y)) {minY = node.y;}
+ if (maxY < (node.y)) {maxY = node.y;}
}
- element = element.parentNode;
}
- return false;
- }
-
- module.exports = Activator;
-
+ 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};
+ };
-/***/ },
-/* 49 */
-/***/ function(module, exports, __webpack_require__) {
-
/**
- * Expose `Emitter`.
+ * @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))};
+ };
- module.exports = Emitter;
/**
- * Initialize a new `Emitter`.
+ * center the network
*
- * @api public
+ * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
*/
+ Network.prototype._centerNetwork = function(range) {
+ var center = this._findCenter(range);
- function Emitter(obj) {
- if (obj) return mixin(obj);
+ center.x *= this.scale;
+ center.y *= this.scale;
+ center.x -= 0.5 * this.frame.canvas.clientWidth;
+ center.y -= 0.5 * this.frame.canvas.clientHeight;
+
+ this._setTranslation(-center.x,-center.y); // set at 0,0
};
+
/**
- * Mixin the emitter properties.
+ * This function zooms out to fit all data on screen based on amount of nodes
*
- * @param {Object} obj
- * @return {Object}
- * @api private
+ * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
+ * @param {Boolean} [disableStart] | If true, start is not called.
*/
-
- function mixin(obj) {
- for (var key in Emitter.prototype) {
- obj[key] = Emitter.prototype[key];
+ Network.prototype.zoomExtent = function(initialZoom, disableStart) {
+ if (initialZoom === undefined) {
+ initialZoom = false;
+ }
+ if (disableStart === undefined) {
+ disableStart = false;
}
- return obj;
- }
- /**
- * Listen on the given `event` with `fn`.
- *
- * @param {String} event
- * @param {Function} fn
- * @return {Emitter}
- * @api public
- */
-
- Emitter.prototype.on =
- Emitter.prototype.addEventListener = function(event, fn){
- this._callbacks = this._callbacks || {};
- (this._callbacks[event] = this._callbacks[event] || [])
- .push(fn);
- return this;
- };
-
- /**
- * Adds an `event` listener that will be invoked a single
- * time then automatically removed.
- *
- * @param {String} event
- * @param {Function} fn
- * @return {Emitter}
- * @api public
- */
+ var range = this._getRange();
+ var zoomLevel;
- Emitter.prototype.once = function(event, fn){
- var self = this;
- this._callbacks = this._callbacks || {};
+ 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.
+ }
+ }
- function on() {
- self.off(event, on);
- fn.apply(this, arguments);
+ // 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.minX) + Math.abs(range.maxX)) * 1.1;
+ var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
- on.fn = fn;
- this.on(event, on);
- return this;
- };
-
- /**
- * Remove the given callback for `event` or all
- * registered callbacks.
- *
- * @param {String} event
- * @param {Function} fn
- * @return {Emitter}
- * @api public
- */
+ var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
+ var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
- Emitter.prototype.off =
- Emitter.prototype.removeListener =
- Emitter.prototype.removeAllListeners =
- Emitter.prototype.removeEventListener = function(event, fn){
- this._callbacks = this._callbacks || {};
+ zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
+ }
- // all
- if (0 == arguments.length) {
- this._callbacks = {};
- return this;
+ if (zoomLevel > 1.0) {
+ zoomLevel = 1.0;
}
- // specific event
- var callbacks = this._callbacks[event];
- if (!callbacks) return this;
- // remove all handlers
- if (1 == arguments.length) {
- delete this._callbacks[event];
- return this;
+ this._setScale(zoomLevel);
+ this._centerNetwork(range);
+ if (disableStart == false) {
+ this.moving = true;
+ this.start();
}
+ };
- // remove specific handler
- var cb;
- for (var i = 0; i < callbacks.length; i++) {
- cb = callbacks[i];
- if (cb === fn || cb.fn === fn) {
- callbacks.splice(i, 1);
- break;
+
+ /**
+ * 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);
}
}
- return this;
};
+
/**
- * Emit `event` with the given args.
+ * Set nodes and edges, and optionally options as well.
*
- * @param {String} event
- * @param {Mixed} ...
- * @return {Emitter}
+ * @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;
+ }
- Emitter.prototype.emit = function(event){
- this._callbacks = this._callbacks || {};
- var args = [].slice.call(arguments, 1)
- , callbacks = this._callbacks[event];
+ 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 (callbacks) {
- callbacks = callbacks.slice(0);
- for (var i = 0, len = callbacks.length; i < len; ++i) {
- callbacks[i].apply(this, args);
+ // 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);
+ }
- return this;
+ this._putDataInSector();
+ if (!disableStart) {
+ // find a stable position or start animating to a stable position
+ if (this.constants.stabilize) {
+ var me = this;
+ setTimeout(function() {me._stabilize(); me.start();},0)
+ }
+ else {
+ this.start();
+ }
+ }
};
/**
- * Return array of callbacks for `event`.
- *
- * @param {String} event
- * @return {Array}
- * @api public
+ * Set options
+ * @param {Object} options
*/
+ Network.prototype.setOptions = function (options) {
+ if (options) {
+ var prop;
- Emitter.prototype.listeners = function(event){
- this._callbacks = this._callbacks || {};
- return this._callbacks[event] || [];
- };
+ var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','clustering','navigation','keyboard','dataManipulation',
+ 'onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse'
+ ];
+ util.selectiveNotDeepExtend(fields,this.constants, options);
+ util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes);
+ util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges);
- /**
- * Check if this emitter has `event` handlers.
- *
- * @param {String} event
- * @return {Boolean}
- * @api public
- */
+ if (options.physics) {
+ util.mergeOptions(this.constants.physics, options.physics,'barnesHut');
+ util.mergeOptions(this.constants.physics, options.physics,'repulsion');
- Emitter.prototype.hasListeners = function(event){
- return !! this.listeners(event).length;
- };
+ 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;}
-/***/ },
-/* 50 */
-/***/ function(module, exports, __webpack_require__) {
+ 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');
- /**
- * Copyright 2012 Craig Campbell
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * Mousetrap is a simple keyboard shortcut library for Javascript with
- * no external dependencies
- *
- * @version 1.1.2
- * @url craig.is/killing/mice
- */
- /**
- * mapping of special keycodes to their corresponding keys
- *
- * everything in this dictionary cannot use keypress events
- * so it has to be here to map to the correct keycodes for
- * keyup/keydown events
- *
- * @type {Object}
- */
- var _MAP = {
- 8: 'backspace',
- 9: 'tab',
- 13: 'enter',
- 16: 'shift',
- 17: 'ctrl',
- 18: 'alt',
- 20: 'capslock',
- 27: 'esc',
- 32: 'space',
- 33: 'pageup',
- 34: 'pagedown',
- 35: 'end',
- 36: 'home',
- 37: 'left',
- 38: 'up',
- 39: 'right',
- 40: 'down',
- 45: 'ins',
- 46: 'del',
- 91: 'meta',
- 93: 'meta',
- 224: 'meta'
- },
+ if (options.dataManipulation) {
+ this.editMode = this.constants.dataManipulation.initiallyVisible;
+ }
- /**
- * mapping for special characters so they can support
- *
- * this dictionary is only used incase you want to bind a
- * keyup or keydown event to one of these keys
- *
- * @type {Object}
- */
- _KEYCODE_MAP = {
- 106: '*',
- 107: '+',
- 109: '-',
- 110: '.',
- 111 : '/',
- 186: ';',
- 187: '=',
- 188: ',',
- 189: '-',
- 190: '.',
- 191: '/',
- 192: '`',
- 219: '[',
- 220: '\\',
- 221: ']',
- 222: '\''
- },
- /**
- * this is a mapping of keys that require shift on a US keypad
- * back to the non shift equivelents
- *
- * this is so you can use keyup events with these keys
- *
- * note that this will only work reliably on US keyboards
- *
- * @type {Object}
- */
- _SHIFT_MAP = {
- '~': '`',
- '!': '1',
- '@': '2',
- '#': '3',
- '$': '4',
- '%': '5',
- '^': '6',
- '&': '7',
- '*': '8',
- '(': '9',
- ')': '0',
- '_': '-',
- '+': '=',
- ':': ';',
- '\"': '\'',
- '<': ',',
- '>': '.',
- '?': '/',
- '|': '\\'
- },
+ // 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 is a list of special strings you can use to map
- * to modifier keys when you specify your keyboard shortcuts
- *
- * @type {Object}
- */
- _SPECIAL_ALIASES = {
- 'option': 'alt',
- 'command': 'meta',
- 'return': 'enter',
- 'escape': 'esc'
- },
+ 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;}
+ }
+ }
+ }
- /**
- * variable to store the flipped version of _MAP from above
- * needed to check if we should use keypress or not when no action
- * is specified
- *
- * @type {Object|undefined}
- */
- _REVERSE_MAP,
+ 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);
+ }
+ }
+ }
- /**
- * a list of all the callbacks setup via Mousetrap.bind()
- *
- * @type {Object}
- */
- _callbacks = {},
+ 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);
+ }
+ }
- /**
- * direct map of string combinations to callbacks used for trigger()
- *
- * @type {Object}
- */
- _direct_map = {},
+ if ('clickToUse' in options) {
+ if (options.clickToUse) {
+ this.activator = new Activator(this.frame);
+ this.activator.on('change', this._createKeyBinds.bind(this));
+ }
+ else {
+ if (this.activator) {
+ this.activator.destroy();
+ delete this.activator;
+ }
+ }
+ }
- /**
- * keeps track of what level each sequence is at since multiple
- * sequences can start out with the same sequence
- *
- * @type {Object}
- */
- _sequence_levels = {},
+ if (options.labels) {
+ throw new Error('Option "labels" is deprecated. Use options "locale" and "locales" instead.');
+ }
+ }
- /**
- * variable to store the setTimeout call
- *
- * @type {null|number}
- */
- _reset_timer,
+ // (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();
- /**
- * temporary state where we will ignore the next keyup
- *
- * @type {boolean|string}
- */
- _ignore_next_keyup = false,
- /**
- * are we currently inside of a sequence?
- * type of action ("keyup" or "keydown" or "keypress") or false
- *
- * @type {boolean|string}
- */
- _inside_sequence = false;
+ // bind keys. If disabled, this will not do anything;
+ this._createKeyBinds();
+ this.setSize(this.constants.width, this.constants.height);
+ this.moving = true;
+ this.start();
- /**
- * loop through the f keys, f1 to f19 and add them to the map
- * programatically
- */
- for (var i = 1; i < 20; ++i) {
- _MAP[111 + i] = 'f' + i;
+ };
+
+ /**
+ * 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);
}
- /**
- * loop through to map numbers on the numeric keypad
- */
- for (i = 0; i <= 9; ++i) {
- _MAP[i + 96] = i;
- }
-
- /**
- * cross browser add event method
- *
- * @param {Element|HTMLDocument} object
- * @param {string} type
- * @param {Function} callback
- * @returns void
- */
- function _addEvent(object, type, callback) {
- if (object.addEventListener) {
- return object.addEventListener(type, callback, false);
- }
+ this.frame = document.createElement('div');
+ this.frame.className = 'vis network-frame';
+ this.frame.style.position = 'relative';
+ this.frame.style.overflow = 'hidden';
- object.attachEvent('on' + type, callback);
+ // create the network canvas (HTML canvas element)
+ this.frame.canvas = document.createElement( 'canvas' );
+ this.frame.canvas.style.position = 'relative';
+ this.frame.appendChild(this.frame.canvas);
+ if (!this.frame.canvas.getContext) {
+ var noCanvas = document.createElement( 'DIV' );
+ noCanvas.style.color = 'red';
+ noCanvas.style.fontWeight = 'bold' ;
+ noCanvas.style.padding = '10px';
+ noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
+ this.frame.canvas.appendChild(noCanvas);
}
- /**
- * takes the event and returns the key character
- *
- * @param {Event} e
- * @return {string}
- */
- function _characterFromEvent(e) {
-
- // for keypress events we should return the character as is
- if (e.type == 'keypress') {
- return String.fromCharCode(e.which);
- }
+ var me = this;
+ this.drag = {};
+ this.pinch = {};
+ this.hammer = Hammer(this.frame.canvas, {
+ prevent_default: true
+ });
+ this.hammer.on('tap', me._onTap.bind(me) );
+ this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
+ this.hammer.on('hold', me._onHold.bind(me) );
+ this.hammer.on('pinch', me._onPinch.bind(me) );
+ this.hammer.on('touch', me._onTouch.bind(me) );
+ this.hammer.on('dragstart', me._onDragStart.bind(me) );
+ this.hammer.on('drag', me._onDrag.bind(me) );
+ this.hammer.on('dragend', me._onDragEnd.bind(me) );
+ this.hammer.on('release', me._onRelease.bind(me) );
+ this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
+ this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
+ this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
- // for non keypress events the special maps are needed
- if (_MAP[e.which]) {
- return _MAP[e.which];
- }
+ // add the frame to the container element
+ this.containerElement.appendChild(this.frame);
- if (_KEYCODE_MAP[e.which]) {
- return _KEYCODE_MAP[e.which];
- }
+ };
- // if it is not in the special map
- return String.fromCharCode(e.which).toLowerCase();
- }
- /**
- * should we stop this event before firing off callbacks
- *
- * @param {Event} e
- * @return {boolean}
- */
- function _stop(e) {
- var element = e.target || e.srcElement,
- tag_name = element.tagName;
+ /**
+ * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
+ * @private
+ */
+ Network.prototype._createKeyBinds = function() {
+ var me = this;
+ this.mousetrap = mousetrap;
- // if the element has the class "mousetrap" then no need to stop
- if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
- return false;
- }
+ this.mousetrap.reset();
- // stop for input, select, and textarea
- return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
+ if (this.constants.keyboard.enabled && this.isActive()) {
+ this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
+ this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
+ this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
+ this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
+ this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
+ this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
+ this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
+ this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
+ this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
+ this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
+ this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
+ this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
+ this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
+ this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
+ this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
+ this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
+ this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
+ this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
+ this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
+ this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
}
- /**
- * checks if two arrays are equal
- *
- * @param {Array} modifiers1
- * @param {Array} modifiers2
- * @returns {boolean}
- */
- function _modifiersMatch(modifiers1, modifiers2) {
- return modifiers1.sort().join(',') === modifiers2.sort().join(',');
+ if (this.constants.dataManipulation.enabled == true) {
+ this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
+ this.mousetrap.bind("del",this._deleteSelected.bind(me));
}
+ };
- /**
- * resets all sequence counters except for the ones passed in
- *
- * @param {Object} do_not_reset
- * @returns void
- */
- function _resetSequences(do_not_reset) {
- do_not_reset = do_not_reset || {};
+ /**
+ * Get the pointer location from a touch location
+ * @param {{pageX: Number, pageY: Number}} touch
+ * @return {{x: Number, y: Number}} pointer
+ * @private
+ */
+ Network.prototype._getPointer = function (touch) {
+ return {
+ x: touch.pageX - util.getAbsoluteLeft(this.frame.canvas),
+ y: touch.pageY - util.getAbsoluteTop(this.frame.canvas)
+ };
+ };
- var active_sequences = false,
- key;
+ /**
+ * On start of a touch gesture, store the pointer
+ * @param event
+ * @private
+ */
+ Network.prototype._onTouch = function (event) {
+ this.drag.pointer = this._getPointer(event.gesture.center);
+ this.drag.pinched = false;
+ this.pinch.scale = this._getScale();
- for (key in _sequence_levels) {
- if (do_not_reset[key]) {
- active_sequences = true;
- continue;
- }
- _sequence_levels[key] = 0;
- }
+ this._handleTouch(this.drag.pointer);
+ };
- if (!active_sequences) {
- _inside_sequence = false;
- }
- }
+ /**
+ * handle drag start event
+ * @private
+ */
+ Network.prototype._onDragStart = function () {
+ this._handleDragStart();
+ };
- /**
- * finds all callbacks that match based on the keycode, modifiers,
- * and action
- *
- * @param {string} character
- * @param {Array} modifiers
- * @param {string} action
- * @param {boolean=} remove - should we remove any matches
- * @param {string=} combination
- * @returns {Array}
- */
- function _getMatches(character, modifiers, action, remove, combination) {
- var i,
- callback,
- matches = [];
- // if there are no events related to this keycode
- if (!_callbacks[character]) {
- return [];
- }
+ /**
+ * This function is called by _onDragStart.
+ * It is separated out because we can then overload it for the datamanipulation system.
+ *
+ * @private
+ */
+ Network.prototype._handleDragStart = function() {
+ var drag = this.drag;
+ var node = this._getNodeAt(drag.pointer);
+ // note: drag.pointer is set in _onTouch to get the initial touch location
- // if a modifier key is coming up on its own we should allow it
- if (action == 'keyup' && _isModifier(character)) {
- modifiers = [character];
- }
+ drag.dragging = true;
+ drag.selection = [];
+ drag.translation = this._getTranslation();
+ drag.nodeId = null;
- // loop through all callbacks for the key that was pressed
- // and see if any of them match
- for (i = 0; i < _callbacks[character].length; ++i) {
- callback = _callbacks[character][i];
+ if (node != null) {
+ drag.nodeId = node.id;
+ // select the clicked node if not yet selected
+ if (!node.isSelected()) {
+ this._selectObject(node,false);
+ }
- // if this is a sequence but it is not at the right level
- // then move onto the next match
- if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
- continue;
- }
+ // create an array with the selected nodes and their original location and status
+ for (var objectId in this.selectionObj.nodes) {
+ if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
+ var object = this.selectionObj.nodes[objectId];
+ var s = {
+ id: object.id,
+ node: object,
- // if the action we are looking for doesn't match the action we got
- // then we should keep going
- if (action != callback.action) {
- continue;
- }
+ // store original x, y, xFixed and yFixed, make the node temporarily Fixed
+ x: object.x,
+ y: object.y,
+ xFixed: object.xFixed,
+ yFixed: object.yFixed
+ };
- // if this is a keypress event that means that we need to only
- // look at the character, otherwise check the modifiers as
- // well
- if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
-
- // remove is used so if you change your mind and call bind a
- // second time with a new function the first one is overwritten
- if (remove && callback.combo == combination) {
- _callbacks[character].splice(i, 1);
- }
+ object.xFixed = true;
+ object.yFixed = true;
- matches.push(callback);
- }
+ drag.selection.push(s);
}
-
- return matches;
+ }
}
+ };
- /**
- * takes a key event and figures out what the modifiers are
- *
- * @param {Event} e
- * @returns {Array}
- */
- function _eventModifiers(e) {
- var modifiers = [];
-
- if (e.shiftKey) {
- modifiers.push('shift');
- }
-
- if (e.altKey) {
- modifiers.push('alt');
- }
- if (e.ctrlKey) {
- modifiers.push('ctrl');
- }
+ /**
+ * handle drag event
+ * @private
+ */
+ Network.prototype._onDrag = function (event) {
+ this._handleOnDrag(event)
+ };
- if (e.metaKey) {
- modifiers.push('meta');
- }
- return modifiers;
+ /**
+ * This function is called by _onDrag.
+ * It is separated out because we can then overload it for the datamanipulation system.
+ *
+ * @private
+ */
+ Network.prototype._handleOnDrag = function(event) {
+ if (this.drag.pinched) {
+ return;
}
- /**
- * actually calls the callback function
- *
- * if your callback function returns false this will use the jquery
- * convention - prevent default and stop propogation on the event
- *
- * @param {Function} callback
- * @param {Event} e
- * @returns void
- */
- function _fireCallback(callback, e) {
- if (callback(e) === false) {
- if (e.preventDefault) {
- e.preventDefault();
- }
+ var pointer = this._getPointer(event.gesture.center);
- if (e.stopPropagation) {
- e.stopPropagation();
- }
+ var me = this;
+ var drag = this.drag;
+ var selection = drag.selection;
+ if (selection && selection.length && this.constants.dragNodes == true) {
+ // calculate delta's and new location
+ var deltaX = pointer.x - drag.pointer.x;
+ var deltaY = pointer.y - drag.pointer.y;
- e.returnValue = false;
- e.cancelBubble = true;
- }
- }
+ // update position of all selected nodes
+ selection.forEach(function (s) {
+ var node = s.node;
- /**
- * handles a character key event
- *
- * @param {string} character
- * @param {Event} e
- * @returns void
- */
- function _handleCharacter(character, e) {
+ if (!s.xFixed) {
+ node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX);
+ }
- // if this event should not happen stop here
- if (_stop(e)) {
- return;
+ if (!s.yFixed) {
+ node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY);
}
+ });
- var callbacks = _getMatches(character, _eventModifiers(e), e.type),
- i,
- do_not_reset = {},
- processed_sequence_callback = false;
- // loop through matching callbacks for this key event
- for (i = 0; i < callbacks.length; ++i) {
+ // start _animationStep if not yet running
+ if (!this.moving) {
+ this.moving = true;
+ this.start();
+ }
+ }
+ else {
+ if (this.constants.dragNetwork == true) {
+ // move the network
+ var diffX = pointer.x - this.drag.pointer.x;
+ var diffY = pointer.y - this.drag.pointer.y;
- // fire for all sequence callbacks
- // this is because if for example you have multiple sequences
- // bound such as "g i" and "g t" they both need to fire the
- // callback for matching g cause otherwise you can only ever
- // match the first one
- if (callbacks[i].seq) {
- processed_sequence_callback = true;
+ this._setTranslation(
+ this.drag.translation.x + diffX,
+ this.drag.translation.y + diffY
+ );
+ this._redraw();
+ // this.moving = true;
+ // this.start();
+ }
+ }
+ };
- // keep a list of which sequences were matches for later
- do_not_reset[callbacks[i].seq] = 1;
- _fireCallback(callbacks[i].callback, e);
- continue;
- }
+ /**
+ * handle drag start event
+ * @private
+ */
+ Network.prototype._onDragEnd = function () {
+ this.drag.dragging = false;
+ var selection = this.drag.selection;
+ if (selection && selection.length) {
+ selection.forEach(function (s) {
+ // restore original xFixed and yFixed
+ s.node.xFixed = s.xFixed;
+ s.node.yFixed = s.yFixed;
+ });
+ this.moving = true;
+ this.start();
+ }
+ else {
+ this._redraw();
+ }
- // if there were no sequence matches but we are still here
- // that means this is a regular match so we should fire that
- if (!processed_sequence_callback && !_inside_sequence) {
- _fireCallback(callbacks[i].callback, e);
- }
- }
+ };
- // if you are inside of a sequence and the key you are pressing
- // is not a modifier key then we should reset all sequences
- // that were not matched by this key event
- if (e.type == _inside_sequence && !_isModifier(character)) {
- _resetSequences(do_not_reset);
- }
- }
+ /**
+ * handle tap/click event: select/unselect a node
+ * @private
+ */
+ Network.prototype._onTap = function (event) {
+ var pointer = this._getPointer(event.gesture.center);
+ this.pointerPosition = pointer;
+ this._handleTap(pointer);
- /**
- * handles a keydown event
- *
- * @param {Event} e
- * @returns void
- */
- function _handleKey(e) {
+ };
- // normalize e.which for key events
- // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
- e.which = typeof e.which == "number" ? e.which : e.keyCode;
- var character = _characterFromEvent(e);
+ /**
+ * handle doubletap event
+ * @private
+ */
+ Network.prototype._onDoubleTap = function (event) {
+ var pointer = this._getPointer(event.gesture.center);
+ this._handleDoubleTap(pointer);
+ };
- // no character found then stop
- if (!character) {
- return;
- }
- if (e.type == 'keyup' && _ignore_next_keyup == character) {
- _ignore_next_keyup = false;
- return;
- }
+ /**
+ * handle long tap event: multi select nodes
+ * @private
+ */
+ Network.prototype._onHold = function (event) {
+ var pointer = this._getPointer(event.gesture.center);
+ this.pointerPosition = pointer;
+ this._handleOnHold(pointer);
+ };
- _handleCharacter(character, e);
- }
+ /**
+ * handle the release of the screen
+ *
+ * @private
+ */
+ Network.prototype._onRelease = function (event) {
+ var pointer = this._getPointer(event.gesture.center);
+ this._handleOnRelease(pointer);
+ };
- /**
- * determines if the keycode specified is a modifier key or not
- *
- * @param {string} key
- * @returns {boolean}
- */
- function _isModifier(key) {
- return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
+ /**
+ * Handle pinch event
+ * @param event
+ * @private
+ */
+ Network.prototype._onPinch = function (event) {
+ var pointer = this._getPointer(event.gesture.center);
+
+ this.drag.pinched = true;
+ if (!('scale' in this.pinch)) {
+ this.pinch.scale = 1;
}
- /**
- * called to set a 1 second timeout on the specified sequence
- *
- * this is so after each key press in the sequence you have 1 second
- * to press the next key before you have to start over
- *
- * @returns void
- */
- function _resetSequenceTimer() {
- clearTimeout(_reset_timer);
- _reset_timer = setTimeout(_resetSequences, 1000);
- }
-
- /**
- * reverses the map lookup so that we can look for specific keys
- * to see what can and can't use keypress
- *
- * @return {Object}
- */
- function _getReverseMap() {
- if (!_REVERSE_MAP) {
- _REVERSE_MAP = {};
- for (var key in _MAP) {
+ // TODO: enabled moving while pinching?
+ var scale = this.pinch.scale * event.gesture.scale;
+ this._zoom(scale, pointer)
+ };
- // pull out the numeric keypad from here cause keypress should
- // be able to detect the keys from the character
- if (key > 95 && key < 112) {
- continue;
- }
+ /**
+ * Zoom the network in or out
+ * @param {Number} scale a number around 1, and between 0.01 and 10
+ * @param {{x: Number, y: Number}} pointer Position on screen
+ * @return {Number} appliedScale scale is limited within the boundaries
+ * @private
+ */
+ Network.prototype._zoom = function(scale, pointer) {
+ if (this.constants.zoomable == true) {
+ var scaleOld = this._getScale();
+ if (scale < 0.00001) {
+ scale = 0.00001;
+ }
+ if (scale > 10) {
+ scale = 10;
+ }
- if (_MAP.hasOwnProperty(key)) {
- _REVERSE_MAP[_MAP[key]] = key;
- }
- }
+ var preScaleDragPointer = null;
+ if (this.drag !== undefined) {
+ if (this.drag.dragging == true) {
+ preScaleDragPointer = this.DOMtoCanvas(this.drag.pointer);
}
- return _REVERSE_MAP;
- }
+ }
+ // + this.frame.canvas.clientHeight / 2
+ var translation = this._getTranslation();
- /**
- * picks the best action based on the key combination
- *
- * @param {string} key - character for key
- * @param {Array} modifiers
- * @param {string=} action passed in
- */
- function _pickBestAction(key, modifiers, action) {
+ var scaleFrac = scale / scaleOld;
+ var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
+ var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
- // if no action was picked in we should try to pick the one
- // that we think would work best for this key
- if (!action) {
- action = _getReverseMap()[key] ? 'keydown' : 'keypress';
- }
+ this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
+ "y" : this._YconvertDOMtoCanvas(pointer.y)};
- // modifier keys don't work as expected with keypress,
- // switch to keydown
- if (action == 'keypress' && modifiers.length) {
- action = 'keydown';
- }
+ this._setScale(scale);
+ this._setTranslation(tx, ty);
+ this.updateClustersDefault();
- return action;
- }
+ if (preScaleDragPointer != null) {
+ var postScaleDragPointer = this.canvasToDOM(preScaleDragPointer);
+ this.drag.pointer.x = postScaleDragPointer.x;
+ this.drag.pointer.y = postScaleDragPointer.y;
+ }
- /**
- * binds a key sequence to an event
- *
- * @param {string} combo - combo specified in bind call
- * @param {Array} keys
- * @param {Function} callback
- * @param {string=} action
- * @returns void
- */
- function _bindSequence(combo, keys, callback, action) {
+ this._redraw();
- // start off by adding a sequence level record for this combination
- // and setting the level to 0
- _sequence_levels[combo] = 0;
+ if (scaleOld < scale) {
+ this.emit("zoom", {direction:"+"});
+ }
+ else {
+ this.emit("zoom", {direction:"-"});
+ }
- // if there is no action pick the best one for the first key
- // in the sequence
- if (!action) {
- action = _pickBestAction(keys[0], []);
- }
+ return scale;
+ }
+ };
- /**
- * callback to increase the sequence level for this sequence and reset
- * all other sequences that were active
- *
- * @param {Event} e
- * @returns void
- */
- var _increaseSequence = function(e) {
- _inside_sequence = action;
- ++_sequence_levels[combo];
- _resetSequenceTimer();
- },
- /**
- * wraps the specified callback inside of another function in order
- * to reset all sequence counters as soon as this sequence is done
- *
- * @param {Event} e
- * @returns void
- */
- _callbackAndReset = function(e) {
- _fireCallback(callback, e);
+ /**
+ * Event handler for mouse wheel event, used to zoom the timeline
+ * See http://adomas.org/javascript-mouse-wheel/
+ * https://github.com/EightMedia/hammer.js/issues/256
+ * @param {MouseEvent} event
+ * @private
+ */
+ Network.prototype._onMouseWheel = function(event) {
+ // retrieve delta
+ var delta = 0;
+ if (event.wheelDelta) { /* IE/Opera. */
+ delta = event.wheelDelta/120;
+ } else if (event.detail) { /* Mozilla case. */
+ // In Mozilla, sign of delta is different than in IE.
+ // Also, delta is multiple of 3.
+ delta = -event.detail/3;
+ }
- // we should ignore the next key up if the action is key down
- // or keypress. this is so if you finish a sequence and
- // release the key the final key will not trigger a keyup
- if (action !== 'keyup') {
- _ignore_next_keyup = _characterFromEvent(e);
- }
+ // If delta is nonzero, handle it.
+ // Basically, delta is now positive if wheel was scrolled up,
+ // and negative, if wheel was scrolled down.
+ if (delta) {
- // weird race condition if a sequence ends with the key
- // another sequence begins with
- setTimeout(_resetSequences, 10);
- },
- i;
+ // calculate the new scale
+ var scale = this._getScale();
+ var zoom = delta / 10;
+ if (delta < 0) {
+ zoom = zoom / (1 - zoom);
+ }
+ scale *= (1 + zoom);
- // loop through keys one at a time and bind the appropriate callback
- // function. for any key leading up to the final one it should
- // increase the sequence. after the final, it should reset all sequences
- for (i = 0; i < keys.length; ++i) {
- _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
- }
- }
+ // calculate the pointer location
+ var gesture = hammerUtil.fakeGesture(this, event);
+ var pointer = this._getPointer(gesture.center);
- /**
- * binds a single keyboard combination
- *
- * @param {string} combination
- * @param {Function} callback
- * @param {string=} action
- * @param {string=} sequence_name - name of sequence if part of sequence
- * @param {number=} level - what part of the sequence the command is
- * @returns void
- */
- function _bindSingle(combination, callback, action, sequence_name, level) {
+ // apply the new scale
+ this._zoom(scale, pointer);
+ }
- // make sure multiple spaces in a row become a single space
- combination = combination.replace(/\s+/g, ' ');
+ // Prevent default actions caused by mouse wheel.
+ event.preventDefault();
+ };
- var sequence = combination.split(' '),
- i,
- key,
- keys,
- modifiers = [];
- // if this pattern is a sequence of keys then run through this method
- // to reprocess each pattern one key at a time
- if (sequence.length > 1) {
- return _bindSequence(combination, sequence, callback, action);
- }
+ /**
+ * Mouse move handler for checking whether the title moves over a node with a title.
+ * @param {Event} event
+ * @private
+ */
+ Network.prototype._onMouseMoveTitle = function (event) {
+ var gesture = hammerUtil.fakeGesture(this, event);
+ var pointer = this._getPointer(gesture.center);
- // take the keys from this pattern and figure out what the actual
- // pattern is all about
- keys = combination === '+' ? ['+'] : combination.split('+');
+ // check if the previously selected node is still selected
+ if (this.popupObj) {
+ this._checkHidePopup(pointer);
+ }
- for (i = 0; i < keys.length; ++i) {
- key = keys[i];
+ // start a timeout that will check if the mouse is positioned above
+ // an element
+ var me = this;
+ var checkShow = function() {
+ me._checkShowPopup(pointer);
+ };
+ if (this.popupTimer) {
+ clearInterval(this.popupTimer); // stop any running calculationTimer
+ }
+ if (!this.drag.dragging) {
+ this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
+ }
- // normalize key names
- if (_SPECIAL_ALIASES[key]) {
- key = _SPECIAL_ALIASES[key];
- }
- // if this is not a keypress event then we should
- // be smart about using shift keys
- // this will only work for US keyboards however
- if (action && action != 'keypress' && _SHIFT_MAP[key]) {
- key = _SHIFT_MAP[key];
- modifiers.push('shift');
- }
-
- // if this key is a modifier then add it to the list of modifiers
- if (_isModifier(key)) {
- modifiers.push(key);
- }
+ /**
+ * Adding hover highlights
+ */
+ if (this.constants.hover == true) {
+ // removing all hover highlights
+ for (var edgeId in this.hoverObj.edges) {
+ if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
+ this.hoverObj.edges[edgeId].hover = false;
+ delete this.hoverObj.edges[edgeId];
}
+ }
- // depending on what the key combination is
- // we will try to pick the best event for it
- action = _pickBestAction(key, modifiers, action);
+ // adding hover highlights
+ var obj = this._getNodeAt(pointer);
+ if (obj == null) {
+ obj = this._getEdgeAt(pointer);
+ }
+ if (obj != null) {
+ this._hoverObject(obj);
+ }
- // make sure to initialize array if this is the first time
- // a callback is added for this key
- if (!_callbacks[key]) {
- _callbacks[key] = [];
+ // removing all node hover highlights except for the selected one.
+ for (var nodeId in this.hoverObj.nodes) {
+ if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
+ if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) {
+ this._blurObject(this.hoverObj.nodes[nodeId]);
+ delete this.hoverObj.nodes[nodeId];
+ }
}
+ }
+ this.redraw();
+ }
+ };
- // remove an existing match if there is one
- _getMatches(key, modifiers, action, !sequence_name, combination);
+ /**
+ * Check if there is an element on the given position in the network
+ * (a node or edge). If so, and if this element has a title,
+ * show a popup window with its title.
+ *
+ * @param {{x:Number, y:Number}} pointer
+ * @private
+ */
+ Network.prototype._checkShowPopup = function (pointer) {
+ var obj = {
+ left: this._XconvertDOMtoCanvas(pointer.x),
+ top: this._YconvertDOMtoCanvas(pointer.y),
+ right: this._XconvertDOMtoCanvas(pointer.x),
+ bottom: this._YconvertDOMtoCanvas(pointer.y)
+ };
- // add this call back to the array
- // if it is a sequence put it at the beginning
- // if not put it at the end
- //
- // this is important because the way these are processed expects
- // the sequence ones to come first
- _callbacks[key][sequence_name ? 'unshift' : 'push']({
- callback: callback,
- modifiers: modifiers,
- action: action,
- seq: sequence_name,
- level: level,
- combo: combination
- });
- }
+ var id;
+ var lastPopupNode = this.popupObj;
- /**
- * binds multiple combinations to the same callback
- *
- * @param {Array} combinations
- * @param {Function} callback
- * @param {string|undefined} action
- * @returns void
- */
- function _bindMultiple(combinations, callback, action) {
- for (var i = 0; i < combinations.length; ++i) {
- _bindSingle(combinations[i], callback, action);
+ if (this.popupObj == undefined) {
+ // search the nodes for overlap, select the top one in case of multiple nodes
+ var nodes = this.nodes;
+ for (id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ var node = nodes[id];
+ if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
+ this.popupObj = node;
+ break;
+ }
}
+ }
}
- // start!
- _addEvent(document, 'keypress', _handleKey);
- _addEvent(document, 'keydown', _handleKey);
- _addEvent(document, 'keyup', _handleKey);
+ if (this.popupObj === undefined) {
+ // search the edges for overlap
+ var edges = this.edges;
+ for (id in edges) {
+ if (edges.hasOwnProperty(id)) {
+ var edge = edges[id];
+ if (edge.connected && (edge.getTitle() !== undefined) &&
+ edge.isOverlappingWith(obj)) {
+ this.popupObj = edge;
+ break;
+ }
+ }
+ }
+ }
- var mousetrap = {
+ if (this.popupObj) {
+ // show popup message window
+ if (this.popupObj != lastPopupNode) {
+ var me = this;
+ if (!me.popup) {
+ me.popup = new Popup(me.frame, me.constants.tooltip);
+ }
- /**
- * binds an event to mousetrap
- *
- * can be a single key, a combination of keys separated with +,
- * a comma separated list of keys, an array of keys, or
- * a sequence of keys separated by spaces
- *
- * be sure to list the modifier keys first to make sure that the
- * correct key ends up getting bound (the last key in the pattern)
- *
- * @param {string|Array} keys
- * @param {Function} callback
- * @param {string=} action - 'keypress', 'keydown', or 'keyup'
- * @returns void
- */
- bind: function(keys, callback, action) {
- _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
- _direct_map[keys + ':' + action] = callback;
- return this;
- },
+ // adjust a small offset such that the mouse cursor is located in the
+ // bottom left location of the popup, and you can easily move over the
+ // popup area
+ me.popup.setPosition(pointer.x - 3, pointer.y - 3);
+ me.popup.setText(me.popupObj.getTitle());
+ me.popup.show();
+ }
+ }
+ else {
+ if (this.popup) {
+ this.popup.hide();
+ }
+ }
+ };
- /**
- * unbinds an event to mousetrap
- *
- * the unbinding sets the callback function of the specified key combo
- * to an empty function and deletes the corresponding key in the
- * _direct_map dict.
- *
- * the keycombo+action has to be exactly the same as
- * it was defined in the bind method
- *
- * TODO: actually remove this from the _callbacks dictionary instead
- * of binding an empty function
- *
- * @param {string|Array} keys
- * @param {string} action
- * @returns void
- */
- unbind: function(keys, action) {
- if (_direct_map[keys + ':' + action]) {
- delete _direct_map[keys + ':' + action];
- this.bind(keys, function() {}, action);
- }
- return this;
- },
- /**
- * triggers an event that has already been bound
- *
- * @param {string} keys
- * @param {string=} action
- * @returns void
- */
- trigger: function(keys, action) {
- _direct_map[keys + ':' + action]();
- return this;
- },
+ /**
+ * Check if the popup must be hided, which is the case when the mouse is no
+ * longer hovering on the object
+ * @param {{x:Number, y:Number}} pointer
+ * @private
+ */
+ Network.prototype._checkHidePopup = function (pointer) {
+ if (!this.popupObj || !this._getNodeAt(pointer) ) {
+ this.popupObj = undefined;
+ if (this.popup) {
+ this.popup.hide();
+ }
+ }
+ };
- /**
- * resets the library back to its initial state. this is useful
- * if you want to clear out the current keyboard shortcuts and bind
- * new ones - for example if you switch to another page
- *
- * @returns void
- */
- reset: function() {
- _callbacks = {};
- _direct_map = {};
- return this;
- }
- };
- module.exports = mousetrap;
+ /**
+ * Set a new size for the network
+ * @param {string} width Width in pixels or percentage (for example '800px'
+ * or '50%')
+ * @param {string} height Height in pixels or percentage (for example '400px'
+ * or '30%')
+ */
+ Network.prototype.setSize = function(width, height) {
+ this.frame.style.width = width;
+ this.frame.style.height = height;
+ this.frame.canvas.style.width = '100%';
+ this.frame.canvas.style.height = '100%';
+ this.frame.canvas.width = this.frame.canvas.clientWidth;
+ this.frame.canvas.height = this.frame.canvas.clientHeight;
-/***/ },
-/* 51 */
-/***/ function(module, exports, __webpack_require__) {
+ this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
+ };
- var __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(global, module) {//! moment.js
- //! version : 2.7.0
- //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
- //! license : MIT
- //! momentjs.com
+ /**
+ * Set a data set with nodes for the network
+ * @param {Array | DataSet | DataView} nodes The data containing the nodes.
+ * @private
+ */
+ Network.prototype._setNodes = function(nodes) {
+ var oldNodesData = this.nodesData;
- (function (undefined) {
+ if (nodes instanceof DataSet || nodes instanceof DataView) {
+ this.nodesData = nodes;
+ }
+ else if (nodes instanceof Array) {
+ this.nodesData = new DataSet();
+ this.nodesData.add(nodes);
+ }
+ else if (!nodes) {
+ this.nodesData = new DataSet();
+ }
+ else {
+ throw new TypeError('Array or DataSet expected');
+ }
- /************************************
- Constants
- ************************************/
+ if (oldNodesData) {
+ // unsubscribe from old dataset
+ util.forEach(this.nodesListeners, function (callback, event) {
+ oldNodesData.off(event, callback);
+ });
+ }
- var moment,
- VERSION = "2.7.0",
- // the global-scope this is NOT the global object in Node.js
- globalScope = typeof global !== 'undefined' ? global : this,
- oldGlobalMoment,
- round = Math.round,
- i,
+ // remove drawn nodes
+ this.nodes = {};
- YEAR = 0,
- MONTH = 1,
- DATE = 2,
- HOUR = 3,
- MINUTE = 4,
- SECOND = 5,
- MILLISECOND = 6,
+ if (this.nodesData) {
+ // subscribe to new dataset
+ var me = this;
+ util.forEach(this.nodesListeners, function (callback, event) {
+ me.nodesData.on(event, callback);
+ });
- // internal storage for language config files
- languages = {},
+ // draw all new nodes
+ var ids = this.nodesData.getIds();
+ this._addNodes(ids);
+ }
+ this._updateSelection();
+ };
- // moment internal properties
- momentProperties = {
- _isAMomentObject: null,
- _i : null,
- _f : null,
- _l : null,
- _strict : null,
- _tzm : null,
- _isUTC : null,
- _offset : null, // optional. Combine with _isUTC
- _pf : null,
- _lang : null // optional
- },
+ /**
+ * Add nodes
+ * @param {Number[] | String[]} ids
+ * @private
+ */
+ Network.prototype._addNodes = function(ids) {
+ var id;
+ for (var i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ var data = this.nodesData.get(id);
+ var node = new Node(data, this.images, this.groups, this.constants);
+ this.nodes[id] = node; // note: this may replace an existing node
+ if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
+ var radius = 10 * 0.1*ids.length + 10;
+ var angle = 2 * Math.PI * Math.random();
+ if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
+ if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
+ }
+ this.moving = true;
+ }
+ this._updateNodeIndexList();
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateCalculationNodes();
+ this._reconnectEdges();
+ this._updateValueRange(this.nodes);
+ this.updateLabels();
+ };
- // check for nodeJS
- hasModule = (typeof module !== 'undefined' && module.exports),
+ /**
+ * Update existing nodes, or create them when not yet existing
+ * @param {Number[] | String[]} ids
+ * @private
+ */
+ Network.prototype._updateNodes = function(ids) {
+ var nodes = this.nodes,
+ nodesData = this.nodesData;
+ for (var i = 0, len = ids.length; i < len; i++) {
+ var id = ids[i];
+ var node = nodes[id];
+ var data = nodesData.get(id);
+ if (node) {
+ // update node
+ node.setProperties(data, this.constants);
+ }
+ else {
+ // create node
+ node = new Node(properties, this.images, this.groups, this.constants);
+ nodes[id] = node;
+ }
+ }
+ this.moving = true;
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateNodeIndexList();
+ this._reconnectEdges();
+ this._updateValueRange(nodes);
+ };
- // ASP.NET json date format regex
- aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
- aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
+ /**
+ * Remove existing nodes. If nodes do not exist, the method will just ignore it.
+ * @param {Number[] | String[]} ids
+ * @private
+ */
+ Network.prototype._removeNodes = function(ids) {
+ var nodes = this.nodes;
+ for (var i = 0, len = ids.length; i < len; i++) {
+ var id = ids[i];
+ delete nodes[id];
+ }
+ this._updateNodeIndexList();
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateCalculationNodes();
+ this._reconnectEdges();
+ this._updateSelection();
+ this._updateValueRange(nodes);
+ };
- // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
- // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
- isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
+ /**
+ * Load edges by reading the data table
+ * @param {Array | DataSet | DataView} edges The data containing the edges.
+ * @private
+ * @private
+ */
+ Network.prototype._setEdges = function(edges) {
+ var oldEdgesData = this.edgesData;
- // format tokens
- formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
- localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
+ if (edges instanceof DataSet || edges instanceof DataView) {
+ this.edgesData = edges;
+ }
+ else if (edges instanceof Array) {
+ this.edgesData = new DataSet();
+ this.edgesData.add(edges);
+ }
+ else if (!edges) {
+ this.edgesData = new DataSet();
+ }
+ else {
+ throw new TypeError('Array or DataSet expected');
+ }
- // parsing token regexes
- parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
- parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
- parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
- parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
- parseTokenDigits = /\d+/, // nonzero number of digits
- parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic.
- parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
- parseTokenT = /T/i, // T (ISO separator)
- parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
- parseTokenOrdinal = /\d{1,2}/,
+ if (oldEdgesData) {
+ // unsubscribe from old dataset
+ util.forEach(this.edgesListeners, function (callback, event) {
+ oldEdgesData.off(event, callback);
+ });
+ }
- //strict parsing regexes
- parseTokenOneDigit = /\d/, // 0 - 9
- parseTokenTwoDigits = /\d\d/, // 00 - 99
- parseTokenThreeDigits = /\d{3}/, // 000 - 999
- parseTokenFourDigits = /\d{4}/, // 0000 - 9999
- parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
- parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
+ // remove drawn edges
+ this.edges = {};
- // iso 8601 regex
- // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00)
- isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,
+ if (this.edgesData) {
+ // subscribe to new dataset
+ var me = this;
+ util.forEach(this.edgesListeners, function (callback, event) {
+ me.edgesData.on(event, callback);
+ });
- isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
+ // draw all new nodes
+ var ids = this.edgesData.getIds();
+ this._addEdges(ids);
+ }
- isoDates = [
- ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
- ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
- ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
- ['GGGG-[W]WW', /\d{4}-W\d{2}/],
- ['YYYY-DDD', /\d{4}-\d{3}/]
- ],
+ this._reconnectEdges();
+ };
- // iso time formats and regexes
- isoTimes = [
- ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
- ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
- ['HH:mm', /(T| )\d\d:\d\d/],
- ['HH', /(T| )\d\d/]
- ],
+ /**
+ * Add edges
+ * @param {Number[] | String[]} ids
+ * @private
+ */
+ Network.prototype._addEdges = function (ids) {
+ var edges = this.edges,
+ edgesData = this.edgesData;
- // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
- parseTimezoneChunker = /([\+\-]|\d\d)/gi,
+ for (var i = 0, len = ids.length; i < len; i++) {
+ var id = ids[i];
- // getter and setter names
- proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
- unitMillisecondFactors = {
- 'Milliseconds' : 1,
- 'Seconds' : 1e3,
- 'Minutes' : 6e4,
- 'Hours' : 36e5,
- 'Days' : 864e5,
- 'Months' : 2592e6,
- 'Years' : 31536e6
- },
+ var oldEdge = edges[id];
+ if (oldEdge) {
+ oldEdge.disconnect();
+ }
+
+ var data = edgesData.get(id, {"showInternalIds" : true});
+ edges[id] = new Edge(data, this, this.constants);
+ }
+
+ this.moving = true;
+ this._updateValueRange(edges);
+ this._createBezierNodes();
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateCalculationNodes();
+ };
+
+ /**
+ * Update existing edges, or create them when not yet existing
+ * @param {Number[] | String[]} ids
+ * @private
+ */
+ Network.prototype._updateEdges = function (ids) {
+ var edges = this.edges,
+ edgesData = this.edgesData;
+ for (var i = 0, len = ids.length; i < len; i++) {
+ var id = ids[i];
+
+ var data = edgesData.get(id);
+ var edge = edges[id];
+ if (edge) {
+ // update edge
+ edge.disconnect();
+ edge.setProperties(data, this.constants);
+ edge.connect();
+ }
+ else {
+ // create edge
+ edge = new Edge(data, this, this.constants);
+ this.edges[id] = edge;
+ }
+ }
+
+ this._createBezierNodes();
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this.moving = true;
+ this._updateValueRange(edges);
+ };
+
+ /**
+ * Remove existing edges. Non existing ids will be ignored
+ * @param {Number[] | String[]} ids
+ * @private
+ */
+ Network.prototype._removeEdges = function (ids) {
+ var edges = this.edges;
+ for (var i = 0, len = ids.length; i < len; i++) {
+ var id = ids[i];
+ var edge = edges[id];
+ if (edge) {
+ if (edge.via != null) {
+ delete this.sectors['support']['nodes'][edge.via.id];
+ }
+ edge.disconnect();
+ delete edges[id];
+ }
+ }
+
+ this.moving = true;
+ this._updateValueRange(edges);
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateCalculationNodes();
+ };
+
+ /**
+ * Reconnect all edges
+ * @private
+ */
+ Network.prototype._reconnectEdges = function() {
+ var id,
+ nodes = this.nodes,
+ edges = this.edges;
+ for (id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ nodes[id].edges = [];
+ }
+ }
+
+ for (id in edges) {
+ if (edges.hasOwnProperty(id)) {
+ var edge = edges[id];
+ edge.from = null;
+ edge.to = null;
+ edge.connect();
+ }
+ }
+ };
+
+ /**
+ * Update the values of all object in the given array according to the current
+ * value range of the objects in the array.
+ * @param {Object} obj An object containing a set of Edges or Nodes
+ * The objects must have a method getValue() and
+ * setValueRange(min, max).
+ * @private
+ */
+ Network.prototype._updateValueRange = function(obj) {
+ var id;
+
+ // determine the range of the objects
+ var valueMin = undefined;
+ var valueMax = undefined;
+ for (id in obj) {
+ if (obj.hasOwnProperty(id)) {
+ var value = obj[id].getValue();
+ if (value !== undefined) {
+ valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
+ valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
+ }
+ }
+ }
+
+ // adjust the range of all objects
+ if (valueMin !== undefined && valueMax !== undefined) {
+ for (id in obj) {
+ if (obj.hasOwnProperty(id)) {
+ obj[id].setValueRange(valueMin, valueMax);
+ }
+ }
+ }
+ };
+
+ /**
+ * Redraw the network with the current data
+ * chart will be resized too.
+ */
+ Network.prototype.redraw = function() {
+ this.setSize(this.constants.width, this.constants.height);
+ this._redraw();
+ };
+
+ /**
+ * Redraw the network with the current data
+ * @private
+ */
+ Network.prototype._redraw = function() {
+ var ctx = this.frame.canvas.getContext('2d');
+ // clear the canvas
+ var w = this.frame.canvas.width;
+ var h = this.frame.canvas.height;
+ ctx.clearRect(0, 0, w, h);
+
+ // set scaling and translation
+ ctx.save();
+ ctx.translate(this.translation.x, this.translation.y);
+ ctx.scale(this.scale, this.scale);
+
+ this.canvasTopLeft = {
+ "x": this._XconvertDOMtoCanvas(0),
+ "y": this._YconvertDOMtoCanvas(0)
+ };
+ this.canvasBottomRight = {
+ "x": this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth),
+ "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight)
+ };
+
+
+ this._doInAllSectors("_drawAllSectorNodes",ctx);
+ if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) {
+ this._doInAllSectors("_drawEdges",ctx);
+ }
+
+ if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) {
+ this._doInAllSectors("_drawNodes",ctx,false);
+ }
+
+ if (this.controlNodesActive == true) {
+ this._doInAllSectors("_drawControlNodes",ctx);
+ }
+
+ // this._doInSupportSector("_drawNodes",ctx,true);
+ // this._drawTree(ctx,"#F00F0F");
+
+ // restore original scaling and translation
+ ctx.restore();
+ };
+
+ /**
+ * Set the translation of the network
+ * @param {Number} offsetX Horizontal offset
+ * @param {Number} offsetY Vertical offset
+ * @private
+ */
+ Network.prototype._setTranslation = function(offsetX, offsetY) {
+ if (this.translation === undefined) {
+ this.translation = {
+ x: 0,
+ y: 0
+ };
+ }
+
+ if (offsetX !== undefined) {
+ this.translation.x = offsetX;
+ }
+ if (offsetY !== undefined) {
+ this.translation.y = offsetY;
+ }
+
+ this.emit('viewChanged');
+ };
+
+ /**
+ * Get the translation of the network
+ * @return {Object} translation An object with parameters x and y, both a number
+ * @private
+ */
+ Network.prototype._getTranslation = function() {
+ return {
+ x: this.translation.x,
+ y: this.translation.y
+ };
+ };
+
+ /**
+ * Scale the network
+ * @param {Number} scale Scaling factor 1.0 is unscaled
+ * @private
+ */
+ Network.prototype._setScale = function(scale) {
+ this.scale = scale;
+ };
+
+ /**
+ * Get the current scale of the network
+ * @return {Number} scale Scaling factor 1.0 is unscaled
+ * @private
+ */
+ Network.prototype._getScale = function() {
+ return this.scale;
+ };
+
+ /**
+ * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
+ * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
+ * @param {number} x
+ * @returns {number}
+ * @private
+ */
+ Network.prototype._XconvertDOMtoCanvas = function(x) {
+ return (x - this.translation.x) / this.scale;
+ };
- unitAliases = {
- ms : 'millisecond',
- s : 'second',
- m : 'minute',
- h : 'hour',
- d : 'day',
- D : 'date',
- w : 'week',
- W : 'isoWeek',
- M : 'month',
- Q : 'quarter',
- y : 'year',
- DDD : 'dayOfYear',
- e : 'weekday',
- E : 'isoWeekday',
- gg: 'weekYear',
- GG: 'isoWeekYear'
- },
+ /**
+ * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
+ * the X coordinate in DOM-space (coordinate point in browser relative to the container div)
+ * @param {number} x
+ * @returns {number}
+ * @private
+ */
+ Network.prototype._XconvertCanvasToDOM = function(x) {
+ return x * this.scale + this.translation.x;
+ };
- camelFunctions = {
- dayofyear : 'dayOfYear',
- isoweekday : 'isoWeekday',
- isoweek : 'isoWeek',
- weekyear : 'weekYear',
- isoweekyear : 'isoWeekYear'
- },
+ /**
+ * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to
+ * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
+ * @param {number} y
+ * @returns {number}
+ * @private
+ */
+ Network.prototype._YconvertDOMtoCanvas = function(y) {
+ return (y - this.translation.y) / this.scale;
+ };
- // format function strings
- formatFunctions = {},
+ /**
+ * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
+ * the Y coordinate in DOM-space (coordinate point in browser relative to the container div)
+ * @param {number} y
+ * @returns {number}
+ * @private
+ */
+ Network.prototype._YconvertCanvasToDOM = function(y) {
+ return y * this.scale + this.translation.y ;
+ };
- // default relative time thresholds
- relativeTimeThresholds = {
- s: 45, //seconds to minutes
- m: 45, //minutes to hours
- h: 22, //hours to days
- dd: 25, //days to month (month == 1)
- dm: 45, //days to months (months > 1)
- dy: 345 //days to year
- },
- // tokens to ordinalize and pad
- ordinalizeTokens = 'DDD w W M D d'.split(' '),
- paddedTokens = 'M D H h m s w W'.split(' '),
+ /**
+ *
+ * @param {object} pos = {x: number, y: number}
+ * @returns {{x: number, y: number}}
+ * @constructor
+ */
+ Network.prototype.canvasToDOM = function(pos) {
+ return {x:this._XconvertCanvasToDOM(pos.x),y:this._YconvertCanvasToDOM(pos.y)};
+ }
- formatTokenFunctions = {
- M : function () {
- return this.month() + 1;
- },
- MMM : function (format) {
- return this.lang().monthsShort(this, format);
- },
- MMMM : function (format) {
- return this.lang().months(this, format);
- },
- D : function () {
- return this.date();
- },
- DDD : function () {
- return this.dayOfYear();
- },
- d : function () {
- return this.day();
- },
- dd : function (format) {
- return this.lang().weekdaysMin(this, format);
- },
- ddd : function (format) {
- return this.lang().weekdaysShort(this, format);
- },
- dddd : function (format) {
- return this.lang().weekdays(this, format);
- },
- w : function () {
- return this.week();
- },
- W : function () {
- return this.isoWeek();
- },
- YY : function () {
- return leftZeroFill(this.year() % 100, 2);
- },
- YYYY : function () {
- return leftZeroFill(this.year(), 4);
- },
- YYYYY : function () {
- return leftZeroFill(this.year(), 5);
- },
- YYYYYY : function () {
- var y = this.year(), sign = y >= 0 ? '+' : '-';
- return sign + leftZeroFill(Math.abs(y), 6);
- },
- gg : function () {
- return leftZeroFill(this.weekYear() % 100, 2);
- },
- gggg : function () {
- return leftZeroFill(this.weekYear(), 4);
- },
- ggggg : function () {
- return leftZeroFill(this.weekYear(), 5);
- },
- GG : function () {
- return leftZeroFill(this.isoWeekYear() % 100, 2);
- },
- GGGG : function () {
- return leftZeroFill(this.isoWeekYear(), 4);
- },
- GGGGG : function () {
- return leftZeroFill(this.isoWeekYear(), 5);
- },
- e : function () {
- return this.weekday();
- },
- E : function () {
- return this.isoWeekday();
- },
- a : function () {
- return this.lang().meridiem(this.hours(), this.minutes(), true);
- },
- A : function () {
- return this.lang().meridiem(this.hours(), this.minutes(), false);
- },
- H : function () {
- return this.hours();
- },
- h : function () {
- return this.hours() % 12 || 12;
- },
- m : function () {
- return this.minutes();
- },
- s : function () {
- return this.seconds();
- },
- S : function () {
- return toInt(this.milliseconds() / 100);
- },
- SS : function () {
- return leftZeroFill(toInt(this.milliseconds() / 10), 2);
- },
- SSS : function () {
- return leftZeroFill(this.milliseconds(), 3);
- },
- SSSS : function () {
- return leftZeroFill(this.milliseconds(), 3);
- },
- Z : function () {
- var a = -this.zone(),
- b = "+";
- if (a < 0) {
- a = -a;
- b = "-";
- }
- return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
- },
- ZZ : function () {
- var a = -this.zone(),
- b = "+";
- if (a < 0) {
- a = -a;
- b = "-";
- }
- return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
- },
- z : function () {
- return this.zoneAbbr();
- },
- zz : function () {
- return this.zoneName();
- },
- X : function () {
- return this.unix();
- },
- Q : function () {
- return this.quarter();
- }
- },
+ /**
+ *
+ * @param {object} pos = {x: number, y: number}
+ * @returns {{x: number, y: number}}
+ * @constructor
+ */
+ Network.prototype.DOMtoCanvas = function(pos) {
+ return {x:this._XconvertDOMtoCanvas(pos.x),y:this._YconvertDOMtoCanvas(pos.y)};
+ }
+
+ /**
+ * Redraw all nodes
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Boolean} [alwaysShow]
+ * @private
+ */
+ Network.prototype._drawNodes = function(ctx,alwaysShow) {
+ if (alwaysShow === undefined) {
+ alwaysShow = false;
+ }
+
+ // first draw the unselected nodes
+ var nodes = this.nodes;
+ var selected = [];
+
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
+ if (nodes[id].isSelected()) {
+ selected.push(id);
+ }
+ else {
+ if (nodes[id].inArea() || alwaysShow) {
+ nodes[id].draw(ctx);
+ }
+ }
+ }
+ }
+
+ // draw the selected nodes on top
+ for (var s = 0, sMax = selected.length; s < sMax; s++) {
+ if (nodes[selected[s]].inArea() || alwaysShow) {
+ nodes[selected[s]].draw(ctx);
+ }
+ }
+ };
- lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
+ /**
+ * Redraw all edges
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
+ */
+ Network.prototype._drawEdges = function(ctx) {
+ var edges = this.edges;
+ for (var id in edges) {
+ if (edges.hasOwnProperty(id)) {
+ var edge = edges[id];
+ edge.setScale(this.scale);
+ if (edge.connected) {
+ edges[id].draw(ctx);
+ }
+ }
+ }
+ };
- // Pick the first defined of two or three arguments. dfl comes from
- // default.
- function dfl(a, b, c) {
- switch (arguments.length) {
- case 2: return a != null ? a : b;
- case 3: return a != null ? a : b != null ? b : c;
- default: throw new Error("Implement me");
- }
+ /**
+ * Redraw all edges
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
+ */
+ Network.prototype._drawControlNodes = function(ctx) {
+ var edges = this.edges;
+ for (var id in edges) {
+ if (edges.hasOwnProperty(id)) {
+ edges[id]._drawControlNodes(ctx);
}
+ }
+ };
- function defaultParsingFlags() {
- // We need to deep clone this object, and es5 standard is not very
- // helpful.
- return {
- empty : false,
- unusedTokens : [],
- unusedInput : [],
- overflow : -2,
- charsLeftOver : 0,
- nullInput : false,
- invalidMonth : null,
- invalidFormat : false,
- userInvalidated : false,
- iso: false
- };
+ /**
+ * Find a stable position for all nodes
+ * @private
+ */
+ Network.prototype._stabilize = function() {
+ if (this.constants.freezeForStabilization == true) {
+ this._freezeDefinedNodes();
+ }
+
+ // find stable position
+ var count = 0;
+ while (this.moving && count < this.constants.stabilizationIterations) {
+ this._physicsTick();
+ count++;
+ }
+ this.zoomExtent(false,true);
+ if (this.constants.freezeForStabilization == true) {
+ this._restoreFrozenNodes();
+ }
+ this.emit("stabilized",{iterations:count});
+ };
+
+ /**
+ * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
+ * because only the supportnodes for the smoothCurves have to settle.
+ *
+ * @private
+ */
+ Network.prototype._freezeDefinedNodes = function() {
+ var nodes = this.nodes;
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ if (nodes[id].x != null && nodes[id].y != null) {
+ nodes[id].fixedData.x = nodes[id].xFixed;
+ nodes[id].fixedData.y = nodes[id].yFixed;
+ nodes[id].xFixed = true;
+ nodes[id].yFixed = true;
+ }
}
+ }
+ };
- function deprecate(msg, fn) {
- var firstTime = true;
- function printMsg() {
- if (moment.suppressDeprecationWarnings === false &&
- typeof console !== 'undefined' && console.warn) {
- console.warn("Deprecation warning: " + msg);
- }
- }
- return extend(function () {
- if (firstTime) {
- printMsg();
- firstTime = false;
- }
- return fn.apply(this, arguments);
- }, fn);
+ /**
+ * Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
+ *
+ * @private
+ */
+ Network.prototype._restoreFrozenNodes = function() {
+ var nodes = this.nodes;
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ if (nodes[id].fixedData.x != null) {
+ nodes[id].xFixed = nodes[id].fixedData.x;
+ nodes[id].yFixed = nodes[id].fixedData.y;
+ }
+ }
+ }
+ };
+
+
+ /**
+ * Check if any of the nodes is still moving
+ * @param {number} vmin the minimum velocity considered as 'moving'
+ * @return {boolean} true if moving, false if non of the nodes is moving
+ * @private
+ */
+ Network.prototype._isMoving = function(vmin) {
+ var nodes = this.nodes;
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
+ return true;
}
+ }
+ return false;
+ };
- function padToken(func, count) {
- return function (a) {
- return leftZeroFill(func.call(this, a), count);
- };
+
+ /**
+ * /**
+ * Perform one discrete step for all nodes
+ *
+ * @private
+ */
+ Network.prototype._discreteStepNodes = function(checkMovement) {
+ var interval = this.physicsDiscreteStepsize;
+ var nodes = this.nodes;
+ var nodeId;
+ var nodesPresent = false;
+
+ if (this.constants.maxVelocity > 0) {
+ for (nodeId in nodes) {
+ if (nodes.hasOwnProperty(nodeId)) {
+ nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
+ nodesPresent = true;
+ }
}
- function ordinalizeToken(func, period) {
- return function (a) {
- return this.lang().ordinal(func.call(this, a), period);
- };
+ }
+ else {
+ for (nodeId in nodes) {
+ if (nodes.hasOwnProperty(nodeId)) {
+ nodes[nodeId].discreteStep(interval);
+ nodesPresent = true;
+ }
}
+ }
- while (ordinalizeTokens.length) {
- i = ordinalizeTokens.pop();
- formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
+ if (nodesPresent == true && (checkMovement === undefined || checkMovement == true)) {
+ var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
+ if (vminCorrected > 0.5*this.constants.maxVelocity) {
+ this.moving = true;
}
- while (paddedTokens.length) {
- i = paddedTokens.pop();
- formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
+ else {
+ this.moving = this._isMoving(vminCorrected);
+ if (this.moving == false) {
+ this.emit("stabilized",{iterations:null});
+ }
+ this.moving = this.moving || this.configurePhysics;
+
}
- formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
+ }
+ };
+
+ /**
+ * A single simulation step (or "tick") in the physics simulation
+ *
+ * @private
+ */
+ Network.prototype._physicsTick = function() {
+ if (!this.freezeSimulation) {
+ if (this.moving == true) {
+ this._doInAllActiveSectors("_initializeForceCalculation");
+ this._doInAllActiveSectors("_discreteStepNodes");
+ if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
+ this._doInSupportSector("_discreteStepNodes", false);
+ }
+ this._findCenter(this._getRange())
+ }
+ }
+ };
+
+
+ /**
+ * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
+ * It reschedules itself at the beginning of the function
+ *
+ * @private
+ */
+ Network.prototype._animationStep = function() {
+ // reset the timer so a new scheduled animation step can be set
+ this.timer = undefined;
+ // handle the keyboad movement
+ this._handleNavigation();
+
+ // this schedules a new animation step
+ this.start();
+
+ // start the physics simulation
+ var calculationTime = Date.now();
+ var maxSteps = 1;
+ this._physicsTick();
+ var timeRequired = Date.now() - calculationTime;
+ while (timeRequired < 0.9*(this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
+ this._physicsTick();
+ timeRequired = Date.now() - calculationTime;
+ maxSteps++;
+ }
+ // start the rendering process
+ var renderTime = Date.now();
+ this._redraw();
+ this.renderTime = Date.now() - renderTime;
+
+ };
+ if (typeof window !== 'undefined') {
+ window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
+ window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
+ }
- /************************************
- Constructors
- ************************************/
+ /**
+ * Schedule a animation step with the refreshrate interval.
+ */
+ Network.prototype.start = function() {
+ if (this.moving == true || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
+ if (!this.timer) {
+ var ua = navigator.userAgent.toLowerCase();
- function Language() {
+ var requiresTimeout = false;
+ if (ua.indexOf('msie 9.0') != -1) { // IE 9
+ requiresTimeout = true;
+ }
+ else if (ua.indexOf('safari') != -1) { // safari
+ if (ua.indexOf('chrome') <= -1) {
+ requiresTimeout = true;
+ }
+ }
+ if (requiresTimeout == true) {
+ this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
+ }
+ else{
+ this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
+ }
}
+ }
+ else {
+ this._redraw();
+ }
+ };
- // Moment prototype object
- function Moment(config) {
- checkOverflow(config);
- extend(this, config);
- }
- // Duration Constructor
- function Duration(duration) {
- var normalizedInput = normalizeObjectUnits(duration),
- years = normalizedInput.year || 0,
- quarters = normalizedInput.quarter || 0,
- months = normalizedInput.month || 0,
- weeks = normalizedInput.week || 0,
- days = normalizedInput.day || 0,
- hours = normalizedInput.hour || 0,
- minutes = normalizedInput.minute || 0,
- seconds = normalizedInput.second || 0,
- milliseconds = normalizedInput.millisecond || 0;
+ /**
+ * Move the network according to the keyboard presses.
+ *
+ * @private
+ */
+ Network.prototype._handleNavigation = function() {
+ if (this.xIncrement != 0 || this.yIncrement != 0) {
+ var translation = this._getTranslation();
+ this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
+ }
+ if (this.zoomIncrement != 0) {
+ var center = {
+ x: this.frame.canvas.clientWidth / 2,
+ y: this.frame.canvas.clientHeight / 2
+ };
+ this._zoom(this.scale*(1 + this.zoomIncrement), center);
+ }
+ };
- // representation for dateAddRemove
- this._milliseconds = +milliseconds +
- seconds * 1e3 + // 1000
- minutes * 6e4 + // 1000 * 60
- hours * 36e5; // 1000 * 60 * 60
- // Because of dateAddRemove treats 24 hours as different from a
- // day when working around DST, we need to store them separately
- this._days = +days +
- weeks * 7;
- // It is impossible translate months into days without knowing
- // which months you are are talking about, so we have to store
- // it separately.
- this._months = +months +
- quarters * 3 +
- years * 12;
- this._data = {};
+ /**
+ * Freeze the _animationStep
+ */
+ Network.prototype.toggleFreeze = function() {
+ if (this.freezeSimulation == false) {
+ this.freezeSimulation = true;
+ }
+ else {
+ this.freezeSimulation = false;
+ this.start();
+ }
+ };
- this._bubble();
+
+ /**
+ * This function cleans the support nodes if they are not needed and adds them when they are.
+ *
+ * @param {boolean} [disableStart]
+ * @private
+ */
+ Network.prototype._configureSmoothCurves = function(disableStart) {
+ if (disableStart === undefined) {
+ disableStart = true;
+ }
+ if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
+ this._createBezierNodes();
+ // cleanup unused support nodes
+ for (var nodeId in this.sectors['support']['nodes']) {
+ if (this.sectors['support']['nodes'].hasOwnProperty(nodeId)) {
+ if (this.edges[this.sectors['support']['nodes'][nodeId].parentEdgeId] === undefined) {
+ delete this.sectors['support']['nodes'][nodeId];
+ }
+ }
+ }
+ }
+ else {
+ // delete the support nodes
+ this.sectors['support']['nodes'] = {};
+ for (var edgeId in this.edges) {
+ if (this.edges.hasOwnProperty(edgeId)) {
+ this.edges[edgeId].via = null;
+ }
}
+ }
- /************************************
- Helpers
- ************************************/
+ this._updateCalculationNodes();
+ if (!disableStart) {
+ this.moving = true;
+ this.start();
+ }
+ };
- function extend(a, b) {
- for (var i in b) {
- if (b.hasOwnProperty(i)) {
- a[i] = b[i];
- }
- }
- if (b.hasOwnProperty("toString")) {
- a.toString = b.toString;
+ /**
+ * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
+ * are used for the force calculation.
+ *
+ * @private
+ */
+ Network.prototype._createBezierNodes = function() {
+ if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
+ for (var edgeId in this.edges) {
+ if (this.edges.hasOwnProperty(edgeId)) {
+ var edge = this.edges[edgeId];
+ if (edge.via == null) {
+ var nodeId = "edgeId:".concat(edge.id);
+ this.sectors['support']['nodes'][nodeId] = new Node(
+ {id:nodeId,
+ mass:1,
+ shape:'circle',
+ image:"",
+ internalMultiplier:1
+ },{},{},this.constants);
+ edge.via = this.sectors['support']['nodes'][nodeId];
+ edge.via.parentEdgeId = edge.id;
+ edge.positionBezierNode();
}
+ }
+ }
+ }
+ };
- if (b.hasOwnProperty("valueOf")) {
- a.valueOf = b.valueOf;
- }
+ /**
+ * load the functions that load the mixins into the prototype.
+ *
+ * @private
+ */
+ Network.prototype._initializeMixinLoaders = function () {
+ for (var mixin in MixinLoader) {
+ if (MixinLoader.hasOwnProperty(mixin)) {
+ Network.prototype[mixin] = MixinLoader[mixin];
+ }
+ }
+ };
- return a;
+ /**
+ * Load the XY positions of the nodes into the dataset.
+ */
+ Network.prototype.storePosition = function() {
+ var dataArray = [];
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ var node = this.nodes[nodeId];
+ var allowedToMoveX = !this.nodes.xFixed;
+ var allowedToMoveY = !this.nodes.yFixed;
+ if (this.nodesData._data[nodeId].x != Math.round(node.x) || this.nodesData._data[nodeId].y != Math.round(node.y)) {
+ dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
+ }
}
+ }
+ this.nodesData.update(dataArray);
+ };
- function cloneMoment(m) {
- var result = {}, i;
- for (i in m) {
- if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) {
- result[i] = m[i];
- }
- }
- return result;
+ /**
+ * Center a node in view.
+ *
+ * @param {Number} nodeId
+ * @param {Number} [zoomLevel]
+ */
+ Network.prototype.focusOnNode = function (nodeId, zoomLevel) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ if (zoomLevel === undefined) {
+ zoomLevel = this._getScale();
}
+ var nodePosition= {x: this.nodes[nodeId].x, y: this.nodes[nodeId].y};
- function absRound(number) {
- if (number < 0) {
- return Math.ceil(number);
- } else {
- return Math.floor(number);
- }
- }
+ var requiredScale = zoomLevel;
+ this._setScale(requiredScale);
- // left zero fill a number
- // see http://jsperf.com/left-zero-filling for performance comparison
- function leftZeroFill(number, targetLength, forceSign) {
- var output = '' + Math.abs(number),
- sign = number >= 0;
+ var canvasCenter = this.DOMtoCanvas({x:0.5 * this.frame.canvas.width,y:0.5 * this.frame.canvas.height});
+ var translation = this._getTranslation();
+
+ var distanceFromCenter = {x:canvasCenter.x - nodePosition.x,
+ y:canvasCenter.y - nodePosition.y};
+
+ this._setTranslation(translation.x + requiredScale * distanceFromCenter.x,
+ translation.y + requiredScale * distanceFromCenter.y);
+ this.redraw();
+ }
+ else {
+ console.log("This nodeId cannot be found.")
+ }
+ };
+
+ /**
+ * Returns true when the Timeline is active.
+ * @returns {boolean}
+ */
+ Network.prototype.isActive = function () {
+ return !this.activator || this.activator.active;
+ };
- while (output.length < targetLength) {
- output = '0' + output;
- }
- return (sign ? (forceSign ? '+' : '') : '-') + output;
- }
+ module.exports = Network;
- // helper function for _.addTime and _.subtractTime
- function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
- var milliseconds = duration._milliseconds,
- days = duration._days,
- months = duration._months;
- updateOffset = updateOffset == null ? true : updateOffset;
- if (milliseconds) {
- mom._d.setTime(+mom._d + milliseconds * isAdding);
- }
- if (days) {
- rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
- }
- if (months) {
- rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
- }
- if (updateOffset) {
- moment.updateOffset(mom, days || months);
- }
- }
+/***/ },
+/* 41 */
+/***/ function(module, exports, __webpack_require__) {
- // check if is an array
- function isArray(input) {
- return Object.prototype.toString.call(input) === '[object Array]';
- }
+ /**
+ * Copyright 2012 Craig Campbell
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Mousetrap is a simple keyboard shortcut library for Javascript with
+ * no external dependencies
+ *
+ * @version 1.1.2
+ * @url craig.is/killing/mice
+ */
- function isDate(input) {
- return Object.prototype.toString.call(input) === '[object Date]' ||
- input instanceof Date;
- }
+ /**
+ * mapping of special keycodes to their corresponding keys
+ *
+ * everything in this dictionary cannot use keypress events
+ * so it has to be here to map to the correct keycodes for
+ * keyup/keydown events
+ *
+ * @type {Object}
+ */
+ var _MAP = {
+ 8: 'backspace',
+ 9: 'tab',
+ 13: 'enter',
+ 16: 'shift',
+ 17: 'ctrl',
+ 18: 'alt',
+ 20: 'capslock',
+ 27: 'esc',
+ 32: 'space',
+ 33: 'pageup',
+ 34: 'pagedown',
+ 35: 'end',
+ 36: 'home',
+ 37: 'left',
+ 38: 'up',
+ 39: 'right',
+ 40: 'down',
+ 45: 'ins',
+ 46: 'del',
+ 91: 'meta',
+ 93: 'meta',
+ 224: 'meta'
+ },
- // compare two arrays, return the number of differences
- function compareArrays(array1, array2, dontConvert) {
- var len = Math.min(array1.length, array2.length),
- lengthDiff = Math.abs(array1.length - array2.length),
- diffs = 0,
- i;
- for (i = 0; i < len; i++) {
- if ((dontConvert && array1[i] !== array2[i]) ||
- (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
- diffs++;
- }
- }
- return diffs + lengthDiff;
- }
+ /**
+ * mapping for special characters so they can support
+ *
+ * this dictionary is only used incase you want to bind a
+ * keyup or keydown event to one of these keys
+ *
+ * @type {Object}
+ */
+ _KEYCODE_MAP = {
+ 106: '*',
+ 107: '+',
+ 109: '-',
+ 110: '.',
+ 111 : '/',
+ 186: ';',
+ 187: '=',
+ 188: ',',
+ 189: '-',
+ 190: '.',
+ 191: '/',
+ 192: '`',
+ 219: '[',
+ 220: '\\',
+ 221: ']',
+ 222: '\''
+ },
- function normalizeUnits(units) {
- if (units) {
- var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
- units = unitAliases[units] || camelFunctions[lowered] || lowered;
- }
- return units;
- }
+ /**
+ * this is a mapping of keys that require shift on a US keypad
+ * back to the non shift equivelents
+ *
+ * this is so you can use keyup events with these keys
+ *
+ * note that this will only work reliably on US keyboards
+ *
+ * @type {Object}
+ */
+ _SHIFT_MAP = {
+ '~': '`',
+ '!': '1',
+ '@': '2',
+ '#': '3',
+ '$': '4',
+ '%': '5',
+ '^': '6',
+ '&': '7',
+ '*': '8',
+ '(': '9',
+ ')': '0',
+ '_': '-',
+ '+': '=',
+ ':': ';',
+ '\"': '\'',
+ '<': ',',
+ '>': '.',
+ '?': '/',
+ '|': '\\'
+ },
- function normalizeObjectUnits(inputObject) {
- var normalizedInput = {},
- normalizedProp,
- prop;
+ /**
+ * this is a list of special strings you can use to map
+ * to modifier keys when you specify your keyboard shortcuts
+ *
+ * @type {Object}
+ */
+ _SPECIAL_ALIASES = {
+ 'option': 'alt',
+ 'command': 'meta',
+ 'return': 'enter',
+ 'escape': 'esc'
+ },
- for (prop in inputObject) {
- if (inputObject.hasOwnProperty(prop)) {
- normalizedProp = normalizeUnits(prop);
- if (normalizedProp) {
- normalizedInput[normalizedProp] = inputObject[prop];
- }
- }
- }
+ /**
+ * variable to store the flipped version of _MAP from above
+ * needed to check if we should use keypress or not when no action
+ * is specified
+ *
+ * @type {Object|undefined}
+ */
+ _REVERSE_MAP,
- return normalizedInput;
- }
+ /**
+ * a list of all the callbacks setup via Mousetrap.bind()
+ *
+ * @type {Object}
+ */
+ _callbacks = {},
- function makeList(field) {
- var count, setter;
+ /**
+ * direct map of string combinations to callbacks used for trigger()
+ *
+ * @type {Object}
+ */
+ _direct_map = {},
- if (field.indexOf('week') === 0) {
- count = 7;
- setter = 'day';
- }
- else if (field.indexOf('month') === 0) {
- count = 12;
- setter = 'month';
- }
- else {
- return;
- }
+ /**
+ * keeps track of what level each sequence is at since multiple
+ * sequences can start out with the same sequence
+ *
+ * @type {Object}
+ */
+ _sequence_levels = {},
+
+ /**
+ * variable to store the setTimeout call
+ *
+ * @type {null|number}
+ */
+ _reset_timer,
+
+ /**
+ * temporary state where we will ignore the next keyup
+ *
+ * @type {boolean|string}
+ */
+ _ignore_next_keyup = false,
+
+ /**
+ * are we currently inside of a sequence?
+ * type of action ("keyup" or "keydown" or "keypress") or false
+ *
+ * @type {boolean|string}
+ */
+ _inside_sequence = false;
- moment[field] = function (format, index) {
- var i, getter,
- method = moment.fn._lang[field],
- results = [];
+ /**
+ * loop through the f keys, f1 to f19 and add them to the map
+ * programatically
+ */
+ for (var i = 1; i < 20; ++i) {
+ _MAP[111 + i] = 'f' + i;
+ }
- if (typeof format === 'number') {
- index = format;
- format = undefined;
- }
+ /**
+ * loop through to map numbers on the numeric keypad
+ */
+ for (i = 0; i <= 9; ++i) {
+ _MAP[i + 96] = i;
+ }
- getter = function (i) {
- var m = moment().utc().set(setter, i);
- return method.call(moment.fn._lang, m, format || '');
- };
+ /**
+ * cross browser add event method
+ *
+ * @param {Element|HTMLDocument} object
+ * @param {string} type
+ * @param {Function} callback
+ * @returns void
+ */
+ function _addEvent(object, type, callback) {
+ if (object.addEventListener) {
+ return object.addEventListener(type, callback, false);
+ }
- if (index != null) {
- return getter(index);
- }
- else {
- for (i = 0; i < count; i++) {
- results.push(getter(i));
- }
- return results;
- }
- };
- }
+ object.attachEvent('on' + type, callback);
+ }
- function toInt(argumentForCoercion) {
- var coercedNumber = +argumentForCoercion,
- value = 0;
+ /**
+ * takes the event and returns the key character
+ *
+ * @param {Event} e
+ * @return {string}
+ */
+ function _characterFromEvent(e) {
- if (coercedNumber !== 0 && isFinite(coercedNumber)) {
- if (coercedNumber >= 0) {
- value = Math.floor(coercedNumber);
- } else {
- value = Math.ceil(coercedNumber);
- }
- }
+ // for keypress events we should return the character as is
+ if (e.type == 'keypress') {
+ return String.fromCharCode(e.which);
+ }
- return value;
- }
+ // for non keypress events the special maps are needed
+ if (_MAP[e.which]) {
+ return _MAP[e.which];
+ }
- function daysInMonth(year, month) {
- return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
- }
+ if (_KEYCODE_MAP[e.which]) {
+ return _KEYCODE_MAP[e.which];
+ }
- function weeksInYear(year, dow, doy) {
- return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
- }
+ // if it is not in the special map
+ return String.fromCharCode(e.which).toLowerCase();
+ }
- function daysInYear(year) {
- return isLeapYear(year) ? 366 : 365;
- }
+ /**
+ * should we stop this event before firing off callbacks
+ *
+ * @param {Event} e
+ * @return {boolean}
+ */
+ function _stop(e) {
+ var element = e.target || e.srcElement,
+ tag_name = element.tagName;
- function isLeapYear(year) {
- return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
- }
+ // if the element has the class "mousetrap" then no need to stop
+ if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
+ return false;
+ }
- function checkOverflow(m) {
- var overflow;
- if (m._a && m._pf.overflow === -2) {
- overflow =
- m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
- m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
- m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
- m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
- m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
- m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
- -1;
+ // stop for input, select, and textarea
+ return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
+ }
- if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
- overflow = DATE;
- }
+ /**
+ * checks if two arrays are equal
+ *
+ * @param {Array} modifiers1
+ * @param {Array} modifiers2
+ * @returns {boolean}
+ */
+ function _modifiersMatch(modifiers1, modifiers2) {
+ return modifiers1.sort().join(',') === modifiers2.sort().join(',');
+ }
- m._pf.overflow = overflow;
- }
- }
+ /**
+ * resets all sequence counters except for the ones passed in
+ *
+ * @param {Object} do_not_reset
+ * @returns void
+ */
+ function _resetSequences(do_not_reset) {
+ do_not_reset = do_not_reset || {};
- function isValid(m) {
- if (m._isValid == null) {
- m._isValid = !isNaN(m._d.getTime()) &&
- m._pf.overflow < 0 &&
- !m._pf.empty &&
- !m._pf.invalidMonth &&
- !m._pf.nullInput &&
- !m._pf.invalidFormat &&
- !m._pf.userInvalidated;
+ var active_sequences = false,
+ key;
- if (m._strict) {
- m._isValid = m._isValid &&
- m._pf.charsLeftOver === 0 &&
- m._pf.unusedTokens.length === 0;
- }
- }
- return m._isValid;
- }
+ for (key in _sequence_levels) {
+ if (do_not_reset[key]) {
+ active_sequences = true;
+ continue;
+ }
+ _sequence_levels[key] = 0;
+ }
- function normalizeLanguage(key) {
- return key ? key.toLowerCase().replace('_', '-') : key;
- }
+ if (!active_sequences) {
+ _inside_sequence = false;
+ }
+ }
- // Return a moment from input, that is local/utc/zone equivalent to model.
- function makeAs(input, model) {
- return model._isUTC ? moment(input).zone(model._offset || 0) :
- moment(input).local();
- }
+ /**
+ * finds all callbacks that match based on the keycode, modifiers,
+ * and action
+ *
+ * @param {string} character
+ * @param {Array} modifiers
+ * @param {string} action
+ * @param {boolean=} remove - should we remove any matches
+ * @param {string=} combination
+ * @returns {Array}
+ */
+ function _getMatches(character, modifiers, action, remove, combination) {
+ var i,
+ callback,
+ matches = [];
- /************************************
- Languages
- ************************************/
+ // if there are no events related to this keycode
+ if (!_callbacks[character]) {
+ return [];
+ }
+ // if a modifier key is coming up on its own we should allow it
+ if (action == 'keyup' && _isModifier(character)) {
+ modifiers = [character];
+ }
- extend(Language.prototype, {
+ // loop through all callbacks for the key that was pressed
+ // and see if any of them match
+ for (i = 0; i < _callbacks[character].length; ++i) {
+ callback = _callbacks[character][i];
- set : function (config) {
- var prop, i;
- for (i in config) {
- prop = config[i];
- if (typeof prop === 'function') {
- this[i] = prop;
- } else {
- this['_' + i] = prop;
- }
- }
- },
+ // if this is a sequence but it is not at the right level
+ // then move onto the next match
+ if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
+ continue;
+ }
- _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
- months : function (m) {
- return this._months[m.month()];
- },
+ // if the action we are looking for doesn't match the action we got
+ // then we should keep going
+ if (action != callback.action) {
+ continue;
+ }
- _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
- monthsShort : function (m) {
- return this._monthsShort[m.month()];
- },
+ // if this is a keypress event that means that we need to only
+ // look at the character, otherwise check the modifiers as
+ // well
+ if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
+
+ // remove is used so if you change your mind and call bind a
+ // second time with a new function the first one is overwritten
+ if (remove && callback.combo == combination) {
+ _callbacks[character].splice(i, 1);
+ }
+
+ matches.push(callback);
+ }
+ }
- monthsParse : function (monthName) {
- var i, mom, regex;
+ return matches;
+ }
- if (!this._monthsParse) {
- this._monthsParse = [];
- }
+ /**
+ * takes a key event and figures out what the modifiers are
+ *
+ * @param {Event} e
+ * @returns {Array}
+ */
+ function _eventModifiers(e) {
+ var modifiers = [];
- for (i = 0; i < 12; i++) {
- // make the regex if we don't have it already
- if (!this._monthsParse[i]) {
- mom = moment.utc([2000, i]);
- regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
- this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
- }
- // test the regex
- if (this._monthsParse[i].test(monthName)) {
- return i;
- }
- }
- },
+ if (e.shiftKey) {
+ modifiers.push('shift');
+ }
- _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
- weekdays : function (m) {
- return this._weekdays[m.day()];
- },
+ if (e.altKey) {
+ modifiers.push('alt');
+ }
- _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
- weekdaysShort : function (m) {
- return this._weekdaysShort[m.day()];
- },
+ if (e.ctrlKey) {
+ modifiers.push('ctrl');
+ }
- _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
- weekdaysMin : function (m) {
- return this._weekdaysMin[m.day()];
- },
+ if (e.metaKey) {
+ modifiers.push('meta');
+ }
- weekdaysParse : function (weekdayName) {
- var i, mom, regex;
+ return modifiers;
+ }
- if (!this._weekdaysParse) {
- this._weekdaysParse = [];
- }
+ /**
+ * actually calls the callback function
+ *
+ * if your callback function returns false this will use the jquery
+ * convention - prevent default and stop propogation on the event
+ *
+ * @param {Function} callback
+ * @param {Event} e
+ * @returns void
+ */
+ function _fireCallback(callback, e) {
+ if (callback(e) === false) {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
- for (i = 0; i < 7; i++) {
- // make the regex if we don't have it already
- if (!this._weekdaysParse[i]) {
- mom = moment([2000, 1]).day(i);
- regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
- this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
- }
- // test the regex
- if (this._weekdaysParse[i].test(weekdayName)) {
- return i;
- }
- }
- },
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ }
- _longDateFormat : {
- LT : "h:mm A",
- L : "MM/DD/YYYY",
- LL : "MMMM D YYYY",
- LLL : "MMMM D YYYY LT",
- LLLL : "dddd, MMMM D YYYY LT"
- },
- longDateFormat : function (key) {
- var output = this._longDateFormat[key];
- if (!output && this._longDateFormat[key.toUpperCase()]) {
- output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
- return val.slice(1);
- });
- this._longDateFormat[key] = output;
- }
- return output;
- },
+ e.returnValue = false;
+ e.cancelBubble = true;
+ }
+ }
- isPM : function (input) {
- // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
- // Using charAt should be more compatible.
- return ((input + '').toLowerCase().charAt(0) === 'p');
- },
+ /**
+ * handles a character key event
+ *
+ * @param {string} character
+ * @param {Event} e
+ * @returns void
+ */
+ function _handleCharacter(character, e) {
- _meridiemParse : /[ap]\.?m?\.?/i,
- meridiem : function (hours, minutes, isLower) {
- if (hours > 11) {
- return isLower ? 'pm' : 'PM';
- } else {
- return isLower ? 'am' : 'AM';
- }
- },
+ // if this event should not happen stop here
+ if (_stop(e)) {
+ return;
+ }
- _calendar : {
- sameDay : '[Today at] LT',
- nextDay : '[Tomorrow at] LT',
- nextWeek : 'dddd [at] LT',
- lastDay : '[Yesterday at] LT',
- lastWeek : '[Last] dddd [at] LT',
- sameElse : 'L'
- },
- calendar : function (key, mom) {
- var output = this._calendar[key];
- return typeof output === 'function' ? output.apply(mom) : output;
- },
+ var callbacks = _getMatches(character, _eventModifiers(e), e.type),
+ i,
+ do_not_reset = {},
+ processed_sequence_callback = false;
- _relativeTime : {
- future : "in %s",
- past : "%s ago",
- s : "a few seconds",
- m : "a minute",
- mm : "%d minutes",
- h : "an hour",
- hh : "%d hours",
- d : "a day",
- dd : "%d days",
- M : "a month",
- MM : "%d months",
- y : "a year",
- yy : "%d years"
- },
- relativeTime : function (number, withoutSuffix, string, isFuture) {
- var output = this._relativeTime[string];
- return (typeof output === 'function') ?
- output(number, withoutSuffix, string, isFuture) :
- output.replace(/%d/i, number);
- },
- pastFuture : function (diff, output) {
- var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
- return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
- },
+ // loop through matching callbacks for this key event
+ for (i = 0; i < callbacks.length; ++i) {
- ordinal : function (number) {
- return this._ordinal.replace("%d", number);
- },
- _ordinal : "%d",
+ // fire for all sequence callbacks
+ // this is because if for example you have multiple sequences
+ // bound such as "g i" and "g t" they both need to fire the
+ // callback for matching g cause otherwise you can only ever
+ // match the first one
+ if (callbacks[i].seq) {
+ processed_sequence_callback = true;
- preparse : function (string) {
- return string;
- },
+ // keep a list of which sequences were matches for later
+ do_not_reset[callbacks[i].seq] = 1;
+ _fireCallback(callbacks[i].callback, e);
+ continue;
+ }
- postformat : function (string) {
- return string;
- },
+ // if there were no sequence matches but we are still here
+ // that means this is a regular match so we should fire that
+ if (!processed_sequence_callback && !_inside_sequence) {
+ _fireCallback(callbacks[i].callback, e);
+ }
+ }
- week : function (mom) {
- return weekOfYear(mom, this._week.dow, this._week.doy).week;
- },
+ // if you are inside of a sequence and the key you are pressing
+ // is not a modifier key then we should reset all sequences
+ // that were not matched by this key event
+ if (e.type == _inside_sequence && !_isModifier(character)) {
+ _resetSequences(do_not_reset);
+ }
+ }
- _week : {
- dow : 0, // Sunday is the first day of the week.
- doy : 6 // The week that contains Jan 1st is the first week of the year.
- },
+ /**
+ * handles a keydown event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _handleKey(e) {
- _invalidDate: 'Invalid date',
- invalidDate: function () {
- return this._invalidDate;
- }
- });
+ // normalize e.which for key events
+ // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
+ e.which = typeof e.which == "number" ? e.which : e.keyCode;
- // Loads a language definition into the `languages` cache. The function
- // takes a key and optionally values. If not in the browser and no values
- // are provided, it will load the language file module. As a convenience,
- // this function also returns the language values.
- function loadLang(key, values) {
- values.abbr = key;
- if (!languages[key]) {
- languages[key] = new Language();
- }
- languages[key].set(values);
- return languages[key];
- }
+ var character = _characterFromEvent(e);
+
+ // no character found then stop
+ if (!character) {
+ return;
+ }
- // Remove a language from the `languages` cache. Mostly useful in tests.
- function unloadLang(key) {
- delete languages[key];
- }
+ if (e.type == 'keyup' && _ignore_next_keyup == character) {
+ _ignore_next_keyup = false;
+ return;
+ }
- // Determines which language definition to use and returns it.
- //
- // With no parameters, it will return the global language. If you
- // pass in a language key, such as 'en', it will return the
- // definition for 'en', so long as 'en' has already been loaded using
- // moment.lang.
- function getLangDefinition(key) {
- var i = 0, j, lang, next, split,
- get = function (k) {
- if (!languages[k] && hasModule) {
- try {
- __webpack_require__(60)("./" + k);
- } catch (e) { }
- }
- return languages[k];
- };
+ _handleCharacter(character, e);
+ }
- if (!key) {
- return moment.fn._lang;
- }
+ /**
+ * determines if the keycode specified is a modifier key or not
+ *
+ * @param {string} key
+ * @returns {boolean}
+ */
+ function _isModifier(key) {
+ return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
+ }
- if (!isArray(key)) {
- //short-circuit everything else
- lang = get(key);
- if (lang) {
- return lang;
- }
- key = [key];
- }
+ /**
+ * called to set a 1 second timeout on the specified sequence
+ *
+ * this is so after each key press in the sequence you have 1 second
+ * to press the next key before you have to start over
+ *
+ * @returns void
+ */
+ function _resetSequenceTimer() {
+ clearTimeout(_reset_timer);
+ _reset_timer = setTimeout(_resetSequences, 1000);
+ }
- //pick the language from the array
- //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
- //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
- while (i < key.length) {
- split = normalizeLanguage(key[i]).split('-');
- j = split.length;
- next = normalizeLanguage(key[i + 1]);
- next = next ? next.split('-') : null;
- while (j > 0) {
- lang = get(split.slice(0, j).join('-'));
- if (lang) {
- return lang;
- }
- if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
- //the next array item is better than a shallower substring of this one
- break;
- }
- j--;
- }
- i++;
- }
- return moment.fn._lang;
- }
+ /**
+ * reverses the map lookup so that we can look for specific keys
+ * to see what can and can't use keypress
+ *
+ * @return {Object}
+ */
+ function _getReverseMap() {
+ if (!_REVERSE_MAP) {
+ _REVERSE_MAP = {};
+ for (var key in _MAP) {
- /************************************
- Formatting
- ************************************/
+ // pull out the numeric keypad from here cause keypress should
+ // be able to detect the keys from the character
+ if (key > 95 && key < 112) {
+ continue;
+ }
+ if (_MAP.hasOwnProperty(key)) {
+ _REVERSE_MAP[_MAP[key]] = key;
+ }
+ }
+ }
+ return _REVERSE_MAP;
+ }
- function removeFormattingTokens(input) {
- if (input.match(/\[[\s\S]/)) {
- return input.replace(/^\[|\]$/g, "");
- }
- return input.replace(/\\/g, "");
- }
+ /**
+ * picks the best action based on the key combination
+ *
+ * @param {string} key - character for key
+ * @param {Array} modifiers
+ * @param {string=} action passed in
+ */
+ function _pickBestAction(key, modifiers, action) {
- function makeFormatFunction(format) {
- var array = format.match(formattingTokens), i, length;
+ // if no action was picked in we should try to pick the one
+ // that we think would work best for this key
+ if (!action) {
+ action = _getReverseMap()[key] ? 'keydown' : 'keypress';
+ }
- for (i = 0, length = array.length; i < length; i++) {
- if (formatTokenFunctions[array[i]]) {
- array[i] = formatTokenFunctions[array[i]];
- } else {
- array[i] = removeFormattingTokens(array[i]);
- }
- }
+ // modifier keys don't work as expected with keypress,
+ // switch to keydown
+ if (action == 'keypress' && modifiers.length) {
+ action = 'keydown';
+ }
- return function (mom) {
- var output = "";
- for (i = 0; i < length; i++) {
- output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
- }
- return output;
- };
- }
+ return action;
+ }
- // format date using native date object
- function formatMoment(m, format) {
+ /**
+ * binds a key sequence to an event
+ *
+ * @param {string} combo - combo specified in bind call
+ * @param {Array} keys
+ * @param {Function} callback
+ * @param {string=} action
+ * @returns void
+ */
+ function _bindSequence(combo, keys, callback, action) {
- if (!m.isValid()) {
- return m.lang().invalidDate();
- }
+ // start off by adding a sequence level record for this combination
+ // and setting the level to 0
+ _sequence_levels[combo] = 0;
- format = expandFormat(format, m.lang());
+ // if there is no action pick the best one for the first key
+ // in the sequence
+ if (!action) {
+ action = _pickBestAction(keys[0], []);
+ }
- if (!formatFunctions[format]) {
- formatFunctions[format] = makeFormatFunction(format);
- }
+ /**
+ * callback to increase the sequence level for this sequence and reset
+ * all other sequences that were active
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ var _increaseSequence = function(e) {
+ _inside_sequence = action;
+ ++_sequence_levels[combo];
+ _resetSequenceTimer();
+ },
- return formatFunctions[format](m);
- }
+ /**
+ * wraps the specified callback inside of another function in order
+ * to reset all sequence counters as soon as this sequence is done
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ _callbackAndReset = function(e) {
+ _fireCallback(callback, e);
- function expandFormat(format, lang) {
- var i = 5;
+ // we should ignore the next key up if the action is key down
+ // or keypress. this is so if you finish a sequence and
+ // release the key the final key will not trigger a keyup
+ if (action !== 'keyup') {
+ _ignore_next_keyup = _characterFromEvent(e);
+ }
- function replaceLongDateFormatTokens(input) {
- return lang.longDateFormat(input) || input;
- }
+ // weird race condition if a sequence ends with the key
+ // another sequence begins with
+ setTimeout(_resetSequences, 10);
+ },
+ i;
- localFormattingTokens.lastIndex = 0;
- while (i >= 0 && localFormattingTokens.test(format)) {
- format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
- localFormattingTokens.lastIndex = 0;
- i -= 1;
- }
+ // loop through keys one at a time and bind the appropriate callback
+ // function. for any key leading up to the final one it should
+ // increase the sequence. after the final, it should reset all sequences
+ for (i = 0; i < keys.length; ++i) {
+ _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
+ }
+ }
- return format;
- }
+ /**
+ * binds a single keyboard combination
+ *
+ * @param {string} combination
+ * @param {Function} callback
+ * @param {string=} action
+ * @param {string=} sequence_name - name of sequence if part of sequence
+ * @param {number=} level - what part of the sequence the command is
+ * @returns void
+ */
+ function _bindSingle(combination, callback, action, sequence_name, level) {
+ // make sure multiple spaces in a row become a single space
+ combination = combination.replace(/\s+/g, ' ');
+
+ var sequence = combination.split(' '),
+ i,
+ key,
+ keys,
+ modifiers = [];
+
+ // if this pattern is a sequence of keys then run through this method
+ // to reprocess each pattern one key at a time
+ if (sequence.length > 1) {
+ return _bindSequence(combination, sequence, callback, action);
+ }
+
+ // take the keys from this pattern and figure out what the actual
+ // pattern is all about
+ keys = combination === '+' ? ['+'] : combination.split('+');
- /************************************
- Parsing
- ************************************/
+ for (i = 0; i < keys.length; ++i) {
+ key = keys[i];
+ // normalize key names
+ if (_SPECIAL_ALIASES[key]) {
+ key = _SPECIAL_ALIASES[key];
+ }
- // get the regex to find the next token
- function getParseRegexForToken(token, config) {
- var a, strict = config._strict;
- switch (token) {
- case 'Q':
- return parseTokenOneDigit;
- case 'DDDD':
- return parseTokenThreeDigits;
- case 'YYYY':
- case 'GGGG':
- case 'gggg':
- return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
- case 'Y':
- case 'G':
- case 'g':
- return parseTokenSignedNumber;
- case 'YYYYYY':
- case 'YYYYY':
- case 'GGGGG':
- case 'ggggg':
- return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
- case 'S':
- if (strict) { return parseTokenOneDigit; }
- /* falls through */
- case 'SS':
- if (strict) { return parseTokenTwoDigits; }
- /* falls through */
- case 'SSS':
- if (strict) { return parseTokenThreeDigits; }
- /* falls through */
- case 'DDD':
- return parseTokenOneToThreeDigits;
- case 'MMM':
- case 'MMMM':
- case 'dd':
- case 'ddd':
- case 'dddd':
- return parseTokenWord;
- case 'a':
- case 'A':
- return getLangDefinition(config._l)._meridiemParse;
- case 'X':
- return parseTokenTimestampMs;
- case 'Z':
- case 'ZZ':
- return parseTokenTimezone;
- case 'T':
- return parseTokenT;
- case 'SSSS':
- return parseTokenDigits;
- case 'MM':
- case 'DD':
- case 'YY':
- case 'GG':
- case 'gg':
- case 'HH':
- case 'hh':
- case 'mm':
- case 'ss':
- case 'ww':
- case 'WW':
- return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
- case 'M':
- case 'D':
- case 'd':
- case 'H':
- case 'h':
- case 'm':
- case 's':
- case 'w':
- case 'W':
- case 'e':
- case 'E':
- return parseTokenOneOrTwoDigits;
- case 'Do':
- return parseTokenOrdinal;
- default :
- a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
- return a;
- }
- }
+ // if this is not a keypress event then we should
+ // be smart about using shift keys
+ // this will only work for US keyboards however
+ if (action && action != 'keypress' && _SHIFT_MAP[key]) {
+ key = _SHIFT_MAP[key];
+ modifiers.push('shift');
+ }
- function timezoneMinutesFromString(string) {
- string = string || "";
- var possibleTzMatches = (string.match(parseTokenTimezone) || []),
- tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
- parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
- minutes = +(parts[1] * 60) + toInt(parts[2]);
+ // if this key is a modifier then add it to the list of modifiers
+ if (_isModifier(key)) {
+ modifiers.push(key);
+ }
+ }
- return parts[0] === '+' ? -minutes : minutes;
- }
+ // depending on what the key combination is
+ // we will try to pick the best event for it
+ action = _pickBestAction(key, modifiers, action);
- // function to convert string input to date
- function addTimeToArrayFromToken(token, input, config) {
- var a, datePartArray = config._a;
+ // make sure to initialize array if this is the first time
+ // a callback is added for this key
+ if (!_callbacks[key]) {
+ _callbacks[key] = [];
+ }
- switch (token) {
- // QUARTER
- case 'Q':
- if (input != null) {
- datePartArray[MONTH] = (toInt(input) - 1) * 3;
- }
- break;
- // MONTH
- case 'M' : // fall through to MM
- case 'MM' :
- if (input != null) {
- datePartArray[MONTH] = toInt(input) - 1;
- }
- break;
- case 'MMM' : // fall through to MMMM
- case 'MMMM' :
- a = getLangDefinition(config._l).monthsParse(input);
- // if we didn't find a month name, mark the date as invalid.
- if (a != null) {
- datePartArray[MONTH] = a;
- } else {
- config._pf.invalidMonth = input;
- }
- break;
- // DAY OF MONTH
- case 'D' : // fall through to DD
- case 'DD' :
- if (input != null) {
- datePartArray[DATE] = toInt(input);
- }
- break;
- case 'Do' :
- if (input != null) {
- datePartArray[DATE] = toInt(parseInt(input, 10));
- }
- break;
- // DAY OF YEAR
- case 'DDD' : // fall through to DDDD
- case 'DDDD' :
- if (input != null) {
- config._dayOfYear = toInt(input);
- }
+ // remove an existing match if there is one
+ _getMatches(key, modifiers, action, !sequence_name, combination);
- break;
- // YEAR
- case 'YY' :
- datePartArray[YEAR] = moment.parseTwoDigitYear(input);
- break;
- case 'YYYY' :
- case 'YYYYY' :
- case 'YYYYYY' :
- datePartArray[YEAR] = toInt(input);
- break;
- // AM / PM
- case 'a' : // fall through to A
- case 'A' :
- config._isPm = getLangDefinition(config._l).isPM(input);
- break;
- // 24 HOUR
- case 'H' : // fall through to hh
- case 'HH' : // fall through to hh
- case 'h' : // fall through to hh
- case 'hh' :
- datePartArray[HOUR] = toInt(input);
- break;
- // MINUTE
- case 'm' : // fall through to mm
- case 'mm' :
- datePartArray[MINUTE] = toInt(input);
- break;
- // SECOND
- case 's' : // fall through to ss
- case 'ss' :
- datePartArray[SECOND] = toInt(input);
- break;
- // MILLISECOND
- case 'S' :
- case 'SS' :
- case 'SSS' :
- case 'SSSS' :
- datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
- break;
- // UNIX TIMESTAMP WITH MS
- case 'X':
- config._d = new Date(parseFloat(input) * 1000);
- break;
- // TIMEZONE
- case 'Z' : // fall through to ZZ
- case 'ZZ' :
- config._useUTC = true;
- config._tzm = timezoneMinutesFromString(input);
- break;
- // WEEKDAY - human
- case 'dd':
- case 'ddd':
- case 'dddd':
- a = getLangDefinition(config._l).weekdaysParse(input);
- // if we didn't get a weekday name, mark the date as invalid
- if (a != null) {
- config._w = config._w || {};
- config._w['d'] = a;
- } else {
- config._pf.invalidWeekday = input;
- }
- break;
- // WEEK, WEEK DAY - numeric
- case 'w':
- case 'ww':
- case 'W':
- case 'WW':
- case 'd':
- case 'e':
- case 'E':
- token = token.substr(0, 1);
- /* falls through */
- case 'gggg':
- case 'GGGG':
- case 'GGGGG':
- token = token.substr(0, 2);
- if (input) {
- config._w = config._w || {};
- config._w[token] = toInt(input);
- }
- break;
- case 'gg':
- case 'GG':
- config._w = config._w || {};
- config._w[token] = moment.parseTwoDigitYear(input);
- }
- }
+ // add this call back to the array
+ // if it is a sequence put it at the beginning
+ // if not put it at the end
+ //
+ // this is important because the way these are processed expects
+ // the sequence ones to come first
+ _callbacks[key][sequence_name ? 'unshift' : 'push']({
+ callback: callback,
+ modifiers: modifiers,
+ action: action,
+ seq: sequence_name,
+ level: level,
+ combo: combination
+ });
+ }
- function dayOfYearFromWeekInfo(config) {
- var w, weekYear, week, weekday, dow, doy, temp, lang;
+ /**
+ * binds multiple combinations to the same callback
+ *
+ * @param {Array} combinations
+ * @param {Function} callback
+ * @param {string|undefined} action
+ * @returns void
+ */
+ function _bindMultiple(combinations, callback, action) {
+ for (var i = 0; i < combinations.length; ++i) {
+ _bindSingle(combinations[i], callback, action);
+ }
+ }
- w = config._w;
- if (w.GG != null || w.W != null || w.E != null) {
- dow = 1;
- doy = 4;
+ // start!
+ _addEvent(document, 'keypress', _handleKey);
+ _addEvent(document, 'keydown', _handleKey);
+ _addEvent(document, 'keyup', _handleKey);
- // TODO: We need to take the current isoWeekYear, but that depends on
- // how we interpret now (local, utc, fixed offset). So create
- // a now version of current config (take local/utc/offset flags, and
- // create now).
- weekYear = dfl(w.GG, config._a[YEAR], weekOfYear(moment(), 1, 4).year);
- week = dfl(w.W, 1);
- weekday = dfl(w.E, 1);
- } else {
- lang = getLangDefinition(config._l);
- dow = lang._week.dow;
- doy = lang._week.doy;
+ var mousetrap = {
- weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year);
- week = dfl(w.w, 1);
+ /**
+ * binds an event to mousetrap
+ *
+ * can be a single key, a combination of keys separated with +,
+ * a comma separated list of keys, an array of keys, or
+ * a sequence of keys separated by spaces
+ *
+ * be sure to list the modifier keys first to make sure that the
+ * correct key ends up getting bound (the last key in the pattern)
+ *
+ * @param {string|Array} keys
+ * @param {Function} callback
+ * @param {string=} action - 'keypress', 'keydown', or 'keyup'
+ * @returns void
+ */
+ bind: function(keys, callback, action) {
+ _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
+ _direct_map[keys + ':' + action] = callback;
+ return this;
+ },
- if (w.d != null) {
- // weekday -- low day numbers are considered next week
- weekday = w.d;
- if (weekday < dow) {
- ++week;
- }
- } else if (w.e != null) {
- // local weekday -- counting starts from begining of week
- weekday = w.e + dow;
- } else {
- // default to begining of week
- weekday = dow;
- }
- }
- temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow);
+ /**
+ * unbinds an event to mousetrap
+ *
+ * the unbinding sets the callback function of the specified key combo
+ * to an empty function and deletes the corresponding key in the
+ * _direct_map dict.
+ *
+ * the keycombo+action has to be exactly the same as
+ * it was defined in the bind method
+ *
+ * TODO: actually remove this from the _callbacks dictionary instead
+ * of binding an empty function
+ *
+ * @param {string|Array} keys
+ * @param {string} action
+ * @returns void
+ */
+ unbind: function(keys, action) {
+ if (_direct_map[keys + ':' + action]) {
+ delete _direct_map[keys + ':' + action];
+ this.bind(keys, function() {}, action);
+ }
+ return this;
+ },
- config._a[YEAR] = temp.year;
- config._dayOfYear = temp.dayOfYear;
- }
+ /**
+ * triggers an event that has already been bound
+ *
+ * @param {string} keys
+ * @param {string=} action
+ * @returns void
+ */
+ trigger: function(keys, action) {
+ _direct_map[keys + ':' + action]();
+ return this;
+ },
- // convert an array to a date.
- // the array should mirror the parameters below
- // note: all values past the year are optional and will default to the lowest possible value.
- // [year, month, day , hour, minute, second, millisecond]
- function dateFromConfig(config) {
- var i, date, input = [], currentDate, yearToUse;
+ /**
+ * resets the library back to its initial state. this is useful
+ * if you want to clear out the current keyboard shortcuts and bind
+ * new ones - for example if you switch to another page
+ *
+ * @returns void
+ */
+ reset: function() {
+ _callbacks = {};
+ _direct_map = {};
+ return this;
+ }
+ };
- if (config._d) {
- return;
- }
+ module.exports = mousetrap;
- currentDate = currentDateArray(config);
- //compute day of the year from weeks and weekdays
- if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
- dayOfYearFromWeekInfo(config);
- }
- //if the day of the year is set, figure out what it is
- if (config._dayOfYear) {
- yearToUse = dfl(config._a[YEAR], currentDate[YEAR]);
+/***/ },
+/* 42 */
+/***/ function(module, exports, __webpack_require__) {
- if (config._dayOfYear > daysInYear(yearToUse)) {
- config._pf._overflowDayOfYear = true;
- }
+ var Hammer = __webpack_require__(15);
- date = makeUTCDate(yearToUse, 0, config._dayOfYear);
- config._a[MONTH] = date.getUTCMonth();
- config._a[DATE] = date.getUTCDate();
- }
+ /**
+ * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
+ * @param {Element} element
+ * @param {Event} event
+ */
+ exports.fakeGesture = function(element, event) {
+ var eventType = null;
- // Default to current date.
- // * if no year, month, day of month are given, default to today
- // * if day of month is given, default month and year
- // * if month is given, default only year
- // * if year is given, don't default anything
- for (i = 0; i < 3 && config._a[i] == null; ++i) {
- config._a[i] = input[i] = currentDate[i];
- }
+ // for hammer.js 1.0.5
+ // var gesture = Hammer.event.collectEventData(this, eventType, event);
- // Zero out whatever was not defaulted, including time
- for (; i < 7; i++) {
- config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
- }
+ // for hammer.js 1.0.6+
+ var touches = Hammer.event.getTouchList(event, eventType);
+ var gesture = Hammer.event.collectEventData(this, eventType, touches, event);
- config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
- // Apply timezone offset from input. The actual zone can be changed
- // with parseZone.
- if (config._tzm != null) {
- config._d.setUTCMinutes(config._d.getUTCMinutes() + config._tzm);
- }
- }
+ // on IE in standards mode, no touches are recognized by hammer.js,
+ // resulting in NaN values for center.pageX and center.pageY
+ if (isNaN(gesture.center.pageX)) {
+ gesture.center.pageX = event.pageX;
+ }
+ if (isNaN(gesture.center.pageY)) {
+ gesture.center.pageY = event.pageY;
+ }
- function dateFromObject(config) {
- var normalizedInput;
+ return gesture;
+ };
- if (config._d) {
- return;
- }
- normalizedInput = normalizeObjectUnits(config._i);
- config._a = [
- normalizedInput.year,
- normalizedInput.month,
- normalizedInput.day,
- normalizedInput.hour,
- normalizedInput.minute,
- normalizedInput.second,
- normalizedInput.millisecond
- ];
+/***/ },
+/* 43 */
+/***/ 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
+ */
+ 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,
- dateFromConfig(config);
- }
+ '->': true,
+ '--': true
+ };
- function currentDateArray(config) {
- var now = new Date();
- if (config._useUTC) {
- return [
- now.getUTCFullYear(),
- now.getUTCMonth(),
- now.getUTCDate()
- ];
- } else {
- return [now.getFullYear(), now.getMonth(), now.getDate()];
- }
- }
+ 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
- // date from string and format string
- function makeDateFromStringAndFormat(config) {
+ /**
+ * 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 (config._f === moment.ISO_8601) {
- parseISO(config);
- return;
- }
+ /**
+ * 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);
+ }
- config._a = [];
- config._pf.empty = true;
+ /**
+ * Preview the next character from the dot file.
+ * @return {String} cNext
+ */
+ function nextPreview() {
+ return dot.charAt(index + 1);
+ }
- // This array is used to make a Date, either with `new Date` or `Date.UTC`
- var lang = getLangDefinition(config._l),
- string = '' + config._i,
- i, parsedInput, tokens, token, skipped,
- stringLength = string.length,
- totalParsedInputLength = 0;
+ /**
+ * 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);
+ }
- tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
+ /**
+ * 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 = {};
+ }
- for (i = 0; i < tokens.length; i++) {
- token = tokens[i];
- parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
- if (parsedInput) {
- skipped = string.substr(0, string.indexOf(parsedInput));
- if (skipped.length > 0) {
- config._pf.unusedInput.push(skipped);
- }
- string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
- totalParsedInputLength += parsedInput.length;
- }
- // don't parse if it's not a known token
- if (formatTokenFunctions[token]) {
- if (parsedInput) {
- config._pf.empty = false;
- }
- else {
- config._pf.unusedTokens.push(token);
- }
- addTimeToArrayFromToken(token, parsedInput, config);
- }
- else if (config._strict && !parsedInput) {
- config._pf.unusedTokens.push(token);
- }
- }
+ if (b) {
+ for (var name in b) {
+ if (b.hasOwnProperty(name)) {
+ a[name] = b[name];
+ }
+ }
+ }
+ return a;
+ }
- // add remaining unparsed input length to the string
- config._pf.charsLeftOver = stringLength - totalParsedInputLength;
- if (string.length > 0) {
- config._pf.unusedInput.push(string);
- }
+ /**
+ * 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
+ */
+ 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];
+ }
+ else {
+ // this is the end point
+ o[key] = value;
+ }
+ }
+ }
- // handle am pm
- if (config._isPm && config._a[HOUR] < 12) {
- config._a[HOUR] += 12;
- }
- // if is 12 am, change hours to 0
- if (config._isPm === false && config._a[HOUR] === 12) {
- config._a[HOUR] = 0;
- }
+ /**
+ * 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;
- dateFromConfig(config);
- checkOverflow(config);
+ // 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;
+ }
+
+ // 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;
+ }
}
+ }
- function unescapeFormat(s) {
- return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
- return p1 || p2 || p3 || p4;
- });
+ if (!current) {
+ // this is a new node
+ current = {
+ id: node.id
+ };
+ if (graph.node) {
+ // clone default attributes
+ current.attr = merge(current.attr, graph.node);
}
+ }
- // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
- function regexpEscape(s) {
- return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+ // add node to this (sub)graph and all its parent graphs
+ for (i = graphs.length - 1; i >= 0; i--) {
+ var g = graphs[i];
+
+ if (!g.nodes) {
+ g.nodes = [];
}
+ if (g.nodes.indexOf(current) == -1) {
+ g.nodes.push(current);
+ }
+ }
- // date from string and array of format strings
- function makeDateFromStringAndArray(config) {
- var tempConfig,
- bestMoment,
+ // merge attributes
+ if (node.attr) {
+ current.attr = merge(current.attr, node.attr);
+ }
+ }
- scoreToBeat,
- i,
- currentScore;
+ /**
+ * Add an edge to a graph object
+ * @param {Object} graph
+ * @param {Object} edge
+ */
+ 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
+ }
+ }
- if (config._f.length === 0) {
- config._pf.invalidFormat = true;
- config._d = new Date(NaN);
- return;
- }
+ /**
+ * 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
+ */
+ function createEdge(graph, from, to, type, attr) {
+ var edge = {
+ from: from,
+ to: to,
+ type: type
+ };
- for (i = 0; i < config._f.length; i++) {
- currentScore = 0;
- tempConfig = extend({}, config);
- tempConfig._pf = defaultParsingFlags();
- tempConfig._f = config._f[i];
- makeDateFromStringAndFormat(tempConfig);
+ if (graph.edge) {
+ edge.attr = merge({}, graph.edge); // clone default attributes
+ }
+ edge.attr = merge(edge.attr || {}, attr); // merge attributes
- if (!isValid(tempConfig)) {
- continue;
- }
+ return edge;
+ }
- // if there is any input that was not parsed add a penalty for that format
- currentScore += tempConfig._pf.charsLeftOver;
+ /**
+ * Get next token in the current dot file.
+ * The token and token type are available as token and tokenType
+ */
+ function getToken() {
+ tokenType = TOKENTYPE.NULL;
+ token = '';
- //or tokens
- currentScore += tempConfig._pf.unusedTokens.length * 10;
+ // skip over whitespaces
+ while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
+ next();
+ }
- tempConfig._pf.score = currentScore;
+ do {
+ var isComment = false;
- if (scoreToBeat == null || currentScore < scoreToBeat) {
- scoreToBeat = currentScore;
- bestMoment = tempConfig;
- }
+ // 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();
}
-
- extend(config, bestMoment || tempConfig);
+ isComment = true;
+ }
}
-
- // date from iso format
- function parseISO(config) {
- var i, l,
- string = config._i,
- match = isoRegex.exec(string);
-
- if (match) {
- config._pf.iso = true;
- for (i = 0, l = isoDates.length; i < l; i++) {
- if (isoDates[i][1].exec(string)) {
- // match[5] should be "T" or undefined
- config._f = isoDates[i][0] + (match[6] || " ");
- break;
- }
- }
- for (i = 0, l = isoTimes.length; i < l; i++) {
- if (isoTimes[i][1].exec(string)) {
- config._f += isoTimes[i][0];
- break;
- }
- }
- if (string.match(parseTokenTimezone)) {
- config._f += "Z";
- }
- makeDateFromStringAndFormat(config);
- } else {
- config._isValid = false;
+ 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;
}
- // date from iso format or fallback
- function makeDateFromString(config) {
- parseISO(config);
- if (config._isValid === false) {
- delete config._isValid;
- moment.createFromInputFallback(config);
- }
+ // skip over whitespaces
+ while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
+ next();
}
+ }
+ while (isComment);
- function makeDateFromInput(config) {
- var input = config._i,
- matched = aspNetJsonRegex.exec(input);
+ // check for end of dot file
+ if (c == '') {
+ // token is still empty
+ tokenType = TOKENTYPE.DELIMITER;
+ return;
+ }
- if (input === undefined) {
- config._d = new Date();
- } else if (matched) {
- config._d = new Date(+matched[1]);
- } else if (typeof input === 'string') {
- makeDateFromString(config);
- } else if (isArray(input)) {
- config._a = input.slice(0);
- dateFromConfig(config);
- } else if (isDate(input)) {
- config._d = new Date(+input);
- } else if (typeof(input) === 'object') {
- dateFromObject(config);
- } else if (typeof(input) === 'number') {
- // from milliseconds
- config._d = new Date(input);
- } else {
- moment.createFromInputFallback(config);
- }
- }
+ // check for delimiters consisting of 2 characters
+ var c2 = c + nextPreview();
+ if (DELIMITERS[c2]) {
+ tokenType = TOKENTYPE.DELIMITER;
+ token = c2;
+ next();
+ next();
+ return;
+ }
- function makeDate(y, m, d, h, M, s, ms) {
- //can't just apply() to create a date:
- //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
- var date = new Date(y, m, d, h, M, s, ms);
+ // check for delimiters consisting of 1 character
+ if (DELIMITERS[c]) {
+ tokenType = TOKENTYPE.DELIMITER;
+ token = c;
+ next();
+ return;
+ }
- //the date constructor doesn't accept years < 1970
- if (y < 1970) {
- date.setFullYear(y);
- }
- return date;
- }
+ // 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();
- function makeUTCDate(y) {
- var date = new Date(Date.UTC.apply(null, arguments));
- if (y < 1970) {
- date.setUTCFullYear(y);
- }
- return date;
+ while (isAlphaNumeric(c)) {
+ token += c;
+ next();
+ }
+ if (token == 'false') {
+ token = false; // convert to boolean
+ }
+ else if (token == 'true') {
+ token = true; // convert to boolean
+ }
+ else if (!isNaN(Number(token))) {
+ token = Number(token); // convert to number
}
+ tokenType = TOKENTYPE.IDENTIFIER;
+ return;
+ }
- function parseWeekday(input, language) {
- if (typeof input === 'string') {
- if (!isNaN(input)) {
- input = parseInt(input, 10);
- }
- else {
- input = language.weekdaysParse(input);
- if (typeof input !== 'number') {
- return null;
- }
- }
- }
- return input;
+ // 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');
}
+ 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
+ */
+ function parseGraph() {
+ var graph = {};
+
+ first();
+ getToken();
+
+ // optional strict keyword
+ if (token == 'strict') {
+ graph.strict = true;
+ getToken();
+ }
+
+ // graph or digraph keyword
+ if (token == 'graph' || token == 'digraph') {
+ graph.type = token;
+ getToken();
+ }
- /************************************
- Relative Time
- ************************************/
+ // optional graph id
+ if (tokenType == TOKENTYPE.IDENTIFIER) {
+ graph.id = token;
+ getToken();
+ }
+ // open angle bracket
+ if (token != '{') {
+ throw newSyntaxError('Angle bracket { expected');
+ }
+ getToken();
- // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
- function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
- return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
- }
+ // statements
+ parseStatements(graph);
- function relativeTime(milliseconds, withoutSuffix, lang) {
- var seconds = round(Math.abs(milliseconds) / 1000),
- minutes = round(seconds / 60),
- hours = round(minutes / 60),
- days = round(hours / 24),
- years = round(days / 365),
- args = seconds < relativeTimeThresholds.s && ['s', seconds] ||
- minutes === 1 && ['m'] ||
- minutes < relativeTimeThresholds.m && ['mm', minutes] ||
- hours === 1 && ['h'] ||
- hours < relativeTimeThresholds.h && ['hh', hours] ||
- days === 1 && ['d'] ||
- days <= relativeTimeThresholds.dd && ['dd', days] ||
- days <= relativeTimeThresholds.dm && ['M'] ||
- days < relativeTimeThresholds.dy && ['MM', round(days / 30)] ||
- years === 1 && ['y'] || ['yy', years];
- args[2] = withoutSuffix;
- args[3] = milliseconds > 0;
- args[4] = lang;
- return substituteTimeAgo.apply({}, args);
- }
+ // close angle bracket
+ if (token != '}') {
+ throw newSyntaxError('Angle bracket } expected');
+ }
+ getToken();
+ // end of file
+ if (token !== '') {
+ throw newSyntaxError('End of file expected');
+ }
+ getToken();
- /************************************
- Week of Year
- ************************************/
+ // remove temporary default properties
+ delete graph.node;
+ delete graph.edge;
+ delete graph.graph;
+ return graph;
+ }
- // firstDayOfWeek 0 = sun, 6 = sat
- // the day of the week that starts the week
- // (usually sunday or monday)
- // firstDayOfWeekOfYear 0 = sun, 6 = sat
- // the first week is the week that contains the first
- // of this day of the week
- // (eg. ISO weeks use thursday (4))
- function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
- var end = firstDayOfWeekOfYear - firstDayOfWeek,
- daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
- adjustedMoment;
+ /**
+ * Parse a list with statements.
+ * @param {Object} graph
+ */
+ function parseStatements (graph) {
+ while (token !== '' && token != '}') {
+ parseStatement(graph);
+ if (token == ';') {
+ getToken();
+ }
+ }
+ }
+ /**
+ * 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);
- if (daysToDayOfWeek > end) {
- daysToDayOfWeek -= 7;
- }
+ return;
+ }
- if (daysToDayOfWeek < end - 7) {
- daysToDayOfWeek += 7;
- }
+ // parse an attribute statement
+ var attr = parseAttributeStatement(graph);
+ if (attr) {
+ return;
+ }
- adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
- return {
- week: Math.ceil(adjustedMoment.dayOfYear() / 7),
- year: adjustedMoment.year()
- };
+ // 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);
+ }
+ }
- //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
- function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
- var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
+ /**
+ * Parse a subgraph
+ * @param {Object} graph parent graph object
+ * @return {Object | null} subgraph
+ */
+ function parseSubgraph (graph) {
+ var subgraph = null;
- d = d === 0 ? 7 : d;
- weekday = weekday != null ? weekday : firstDayOfWeek;
- daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
- dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
+ // optional subgraph keyword
+ if (token == 'subgraph') {
+ subgraph = {};
+ subgraph.type = 'subgraph';
+ getToken();
- return {
- year: dayOfYear > 0 ? year : year - 1,
- dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
- };
+ // optional graph id
+ if (tokenType == TOKENTYPE.IDENTIFIER) {
+ subgraph.id = token;
+ getToken();
}
+ }
- /************************************
- Top Level Functions
- ************************************/
-
- function makeMoment(config) {
- var input = config._i,
- format = config._f;
+ // open angle bracket
+ if (token == '{') {
+ getToken();
- if (input === null || (format === undefined && input === '')) {
- return moment.invalid({nullInput: true});
- }
+ if (!subgraph) {
+ subgraph = {};
+ }
+ subgraph.parent = graph;
+ subgraph.node = graph.node;
+ subgraph.edge = graph.edge;
+ subgraph.graph = graph.graph;
- if (typeof input === 'string') {
- config._i = input = getLangDefinition().preparse(input);
- }
+ // statements
+ parseStatements(subgraph);
- if (moment.isMoment(input)) {
- config = cloneMoment(input);
+ // close angle bracket
+ if (token != '}') {
+ throw newSyntaxError('Angle bracket } expected');
+ }
+ getToken();
- config._d = new Date(+input._d);
- } else if (format) {
- if (isArray(format)) {
- makeDateFromStringAndArray(config);
- } else {
- makeDateFromStringAndFormat(config);
- }
- } else {
- makeDateFromInput(config);
- }
+ // remove temporary default properties
+ delete subgraph.node;
+ delete subgraph.edge;
+ delete subgraph.graph;
+ delete subgraph.parent;
- return new Moment(config);
+ // register at the parent graph
+ if (!graph.subgraphs) {
+ graph.subgraphs = [];
}
+ graph.subgraphs.push(subgraph);
+ }
- moment = function (input, format, lang, strict) {
- var c;
+ return subgraph;
+ }
- if (typeof(lang) === "boolean") {
- strict = lang;
- lang = undefined;
- }
- // object construction must be done this way.
- // https://github.com/moment/moment/issues/1423
- c = {};
- c._isAMomentObject = true;
- c._i = input;
- c._f = format;
- c._l = lang;
- c._strict = strict;
- c._isUTC = false;
- c._pf = defaultParsingFlags();
+ /**
+ * 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.
+ */
+ 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();
- return makeMoment(c);
- };
+ // graph attributes
+ graph.graph = parseAttributeList();
+ return 'graph';
+ }
- moment.suppressDeprecationWarnings = false;
+ return null;
+ }
- moment.createFromInputFallback = deprecate(
- "moment construction falls back to js Date. This is " +
- "discouraged and will be removed in upcoming major " +
- "release. Please refer to " +
- "https://github.com/moment/moment/issues/1407 for more info.",
- function (config) {
- config._d = new Date(config._i);
- });
+ /**
+ * parse a node statement
+ * @param {Object} graph
+ * @param {String | Number} id
+ */
+ function parseNodeStatement(graph, id) {
+ // node statement
+ var node = {
+ id: id
+ };
+ var attr = parseAttributeList();
+ if (attr) {
+ node.attr = attr;
+ }
+ addNode(graph, node);
- // Pick a moment m from moments so that m[fn](other) is true for all
- // other. This relies on the function fn to be transitive.
- //
- // moments should either be an array of moment objects or an array, whose
- // first element is an array of moment objects.
- function pickBy(fn, moments) {
- var res, i;
- if (moments.length === 1 && isArray(moments[0])) {
- moments = moments[0];
- }
- if (!moments.length) {
- return moment();
- }
- res = moments[0];
- for (i = 1; i < moments.length; ++i) {
- if (moments[i][fn](res)) {
- res = moments[i];
- }
- }
- return res;
- }
+ // edge statements
+ parseEdge(graph, id);
+ }
- moment.min = function () {
- var args = [].slice.call(arguments, 0);
+ /**
+ * Parse an edge or a series of edges
+ * @param {Object} graph
+ * @param {String | Number} from Id of the from node
+ */
+ function parseEdge(graph, from) {
+ while (token == '->' || token == '--') {
+ var to;
+ var type = token;
+ getToken();
- return pickBy('isBefore', args);
- };
+ 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();
+ }
- moment.max = function () {
- var args = [].slice.call(arguments, 0);
+ // parse edge attributes
+ var attr = parseAttributeList();
- return pickBy('isAfter', args);
- };
+ // create edge
+ var edge = createEdge(graph, from, to, type, attr);
+ addEdge(graph, edge);
- // creating with utc
- moment.utc = function (input, format, lang, strict) {
- var c;
+ from = to;
+ }
+ }
- if (typeof(lang) === "boolean") {
- strict = lang;
- lang = undefined;
- }
- // object construction must be done this way.
- // https://github.com/moment/moment/issues/1423
- c = {};
- c._isAMomentObject = true;
- c._useUTC = true;
- c._isUTC = true;
- c._l = lang;
- c._i = input;
- c._f = format;
- c._strict = strict;
- c._pf = defaultParsingFlags();
+ /**
+ * Parse a set with attributes,
+ * for example [label="1.000", shape=solid]
+ * @return {Object | null} attr
+ */
+ function parseAttributeList() {
+ var attr = null;
- return makeMoment(c).utc();
- };
+ while (token == '[') {
+ getToken();
+ attr = {};
+ while (token !== '' && token != ']') {
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Attribute name expected');
+ }
+ var name = token;
- // creating with unix timestamp (in seconds)
- moment.unix = function (input) {
- return moment(input * 1000);
- };
+ getToken();
+ if (token != '=') {
+ throw newSyntaxError('Equal sign = expected');
+ }
+ getToken();
- // duration
- moment.duration = function (input, key) {
- var duration = input,
- // matching against regexp is expensive, do it on demand
- match = null,
- sign,
- ret,
- parseIso;
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Attribute value expected');
+ }
+ var value = token;
+ setValue(attr, name, value); // name can be a path
- if (moment.isDuration(input)) {
- duration = {
- ms: input._milliseconds,
- d: input._days,
- M: input._months
- };
- } else if (typeof input === 'number') {
- duration = {};
- if (key) {
- duration[key] = input;
- } else {
- duration.milliseconds = input;
- }
- } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
- sign = (match[1] === "-") ? -1 : 1;
- duration = {
- y: 0,
- d: toInt(match[DATE]) * sign,
- h: toInt(match[HOUR]) * sign,
- m: toInt(match[MINUTE]) * sign,
- s: toInt(match[SECOND]) * sign,
- ms: toInt(match[MILLISECOND]) * sign
- };
- } else if (!!(match = isoDurationRegex.exec(input))) {
- sign = (match[1] === "-") ? -1 : 1;
- parseIso = function (inp) {
- // We'd normally use ~~inp for this, but unfortunately it also
- // converts floats to ints.
- // inp may be undefined, so careful calling replace on it.
- var res = inp && parseFloat(inp.replace(',', '.'));
- // apply sign while we're at it
- return (isNaN(res) ? 0 : res) * sign;
- };
- duration = {
- y: parseIso(match[2]),
- M: parseIso(match[3]),
- d: parseIso(match[4]),
- h: parseIso(match[5]),
- m: parseIso(match[6]),
- s: parseIso(match[7]),
- w: parseIso(match[8])
- };
- }
+ getToken();
+ if (token ==',') {
+ getToken();
+ }
+ }
- ret = new Duration(duration);
+ if (token != ']') {
+ throw newSyntaxError('Bracket ] expected');
+ }
+ getToken();
+ }
- if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
- ret._lang = input._lang;
- }
+ return attr;
+ }
- return ret;
- };
+ /**
+ * 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 + ')');
+ }
- // version number
- moment.version = VERSION;
+ /**
+ * 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) + '...');
+ }
- // default format
- moment.defaultFormat = isoFormat;
+ /**
+ * 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 (array1 instanceof Array) {
+ array1.forEach(function (elem1) {
+ if (array2 instanceof Array) {
+ array2.forEach(function (elem2) {
+ fn(elem1, elem2);
+ });
+ }
+ else {
+ fn(elem1, array2);
+ }
+ });
+ }
+ else {
+ if (array2 instanceof Array) {
+ array2.forEach(function (elem2) {
+ fn(array1, elem2);
+ });
+ }
+ else {
+ fn(array1, array2);
+ }
+ }
+ }
- // constant that refers to the ISO standard
- moment.ISO_8601 = function () {};
+ /**
+ * 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: {}
+ };
- // Plugins that add properties should also add the key here (null value),
- // so we can properly clone ourselves.
- moment.momentProperties = momentProperties;
+ // 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);
+ });
+ }
- // This function will be called whenever a moment is mutated.
- // It is intended to keep the offset in sync with the timezone.
- moment.updateOffset = function () {};
+ // copy the edges
+ if (dotData.edges) {
+ /**
+ * Convert an edge in DOT format to an edge with VisGraph format
+ * @param {Object} dotEdge
+ * @returns {Object} graphEdge
+ */
+ function convertEdge(dotEdge) {
+ var graphEdge = {
+ from: dotEdge.from,
+ to: dotEdge.to
+ };
+ merge(graphEdge, dotEdge.attr);
+ graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
+ return graphEdge;
+ }
- // This function allows you to set a threshold for relative time strings
- moment.relativeTimeThreshold = function(threshold, limit) {
- if (relativeTimeThresholds[threshold] === undefined) {
- return false;
+ dotData.edges.forEach(function (dotEdge) {
+ var from, to;
+ if (dotEdge.from instanceof Object) {
+ from = dotEdge.from.nodes;
}
- relativeTimeThresholds[threshold] = limit;
- return true;
- };
-
- // This function will load languages and then set the global language. If
- // no arguments are passed in, it will simply return the current global
- // language key.
- moment.lang = function (key, values) {
- var r;
- if (!key) {
- return moment.fn._lang._abbr;
- }
- if (values) {
- loadLang(normalizeLanguage(key), values);
- } else if (values === null) {
- unloadLang(key);
- key = 'en';
- } else if (!languages[key]) {
- getLangDefinition(key);
+ else {
+ from = {
+ id: dotEdge.from
}
- r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
- return r._abbr;
- };
+ }
- // returns language data
- moment.langData = function (key) {
- if (key && key._lang && key._lang._abbr) {
- key = key._lang._abbr;
+ if (dotEdge.to instanceof Object) {
+ to = dotEdge.to.nodes;
+ }
+ else {
+ to = {
+ id: dotEdge.to
}
- return getLangDefinition(key);
- };
-
- // compare moment object
- moment.isMoment = function (obj) {
- return obj instanceof Moment ||
- (obj != null && obj.hasOwnProperty('_isAMomentObject'));
- };
-
- // for typechecking Duration objects
- moment.isDuration = function (obj) {
- return obj instanceof Duration;
- };
+ }
- for (i = lists.length - 1; i >= 0; --i) {
- makeList(lists[i]);
- }
+ if (dotEdge.from instanceof Object && dotEdge.from.edges) {
+ dotEdge.from.edges.forEach(function (subEdge) {
+ var graphEdge = convertEdge(subEdge);
+ graphData.edges.push(graphEdge);
+ });
+ }
- moment.normalizeUnits = function (units) {
- return normalizeUnits(units);
- };
+ 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);
+ });
- moment.invalid = function (flags) {
- var m = moment.utc(NaN);
- if (flags != null) {
- extend(m._pf, flags);
- }
- else {
- m._pf.userInvalidated = true;
- }
+ if (dotEdge.to instanceof Object && dotEdge.to.edges) {
+ dotEdge.to.edges.forEach(function (subEdge) {
+ var graphEdge = convertEdge(subEdge);
+ graphData.edges.push(graphEdge);
+ });
+ }
+ });
+ }
- return m;
- };
+ // copy the options
+ if (dotData.attr) {
+ graphData.options = dotData.attr;
+ }
- moment.parseZone = function () {
- return moment.apply(null, arguments).parseZone();
- };
+ return graphData;
+ }
- moment.parseTwoDigitYear = function (input) {
- return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
- };
+ // exports
+ exports.parseDOT = parseDOT;
+ exports.DOTToGraph = DOTToGraph;
- /************************************
- Moment Prototype
- ************************************/
+/***/ },
+/* 44 */
+/***/ function(module, exports, __webpack_require__) {
- extend(moment.fn = Moment.prototype, {
+
+ function parseGephi(gephiJSON, options) {
+ var edges = [];
+ var nodes = [];
+ this.options = {
+ edges: {
+ inheritColor: true
+ },
+ nodes: {
+ allowedToMove: false,
+ parseColor: false
+ }
+ };
- clone : function () {
- return moment(this);
- },
+ if (options !== undefined) {
+ this.options.nodes['allowedToMove'] = options.allowedToMove | false;
+ this.options.nodes['parseColor'] = options.parseColor | false;
+ this.options.edges['inheritColor'] = options.inheritColor | true;
+ }
- valueOf : function () {
- return +this._d + ((this._offset || 0) * 60000);
- },
+ 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);
+ }
- unix : function () {
- return Math.floor(+this / 1000);
- },
+ 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);
+ }
- toString : function () {
- return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
- },
+ return {nodes:nodes, edges:edges};
+ }
- toDate : function () {
- return this._offset ? new Date(+this) : this._d;
- },
+ exports.parseGephi = parseGephi;
- toISOString : function () {
- var m = moment(this).utc();
- if (0 < m.year() && m.year() <= 9999) {
- return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
- } else {
- return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
- }
- },
+/***/ },
+/* 45 */
+/***/ function(module, exports, __webpack_require__) {
- toArray : function () {
- var m = this;
- return [
- m.year(),
- m.month(),
- m.date(),
- m.hours(),
- m.minutes(),
- m.seconds(),
- m.milliseconds()
- ];
- },
+ var util = __webpack_require__(1);
- isValid : function () {
- return isValid(this);
- },
+ /**
+ * @class Groups
+ * This class can store groups and properties specific for groups.
+ */
+ function Groups() {
+ this.clear();
+ this.defaultIndex = 0;
+ }
- isDSTShifted : function () {
- if (this._a) {
- return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
- }
+ /**
+ * 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
+ ];
- return false;
- },
- parsingFlags : function () {
- return extend({}, this._pf);
- },
+ /**
+ * Clear all groups
+ */
+ Groups.prototype.clear = function () {
+ this.groups = {};
+ this.groups.length = function()
+ {
+ var i = 0;
+ for ( var p in this ) {
+ if (this.hasOwnProperty(p)) {
+ i++;
+ }
+ }
+ return i;
+ }
+ };
- invalidAt: function () {
- return this._pf.overflow;
- },
- utc : function () {
- return this.zone(0);
- },
+ /**
+ * 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
+ */
+ 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;
+ }
- local : function () {
- this.zone(0);
- this._isUTC = false;
- return this;
- },
+ return group;
+ };
- format : function (inputString) {
- var output = formatMoment(this, inputString || moment.defaultFormat);
- return this.lang().postformat(output);
- },
+ /**
+ * Add a custom group style
+ * @param {String} groupname
+ * @param {Object} style An object containing borderColor,
+ * backgroundColor, etc.
+ * @return {Object} group The created group object
+ */
+ Groups.prototype.add = function (groupname, style) {
+ this.groups[groupname] = style;
+ if (style.color) {
+ style.color = util.parseColor(style.color);
+ }
+ return style;
+ };
- add : function (input, val) {
- var dur;
- // switch args to support add('s', 1) and add(1, 's')
- if (typeof input === 'string' && typeof val === 'string') {
- dur = moment.duration(isNaN(+val) ? +input : +val, isNaN(+val) ? val : input);
- } else if (typeof input === 'string') {
- dur = moment.duration(+val, input);
- } else {
- dur = moment.duration(input, val);
- }
- addOrSubtractDurationFromMoment(this, dur, 1);
- return this;
- },
+ module.exports = Groups;
- subtract : function (input, val) {
- var dur;
- // switch args to support subtract('s', 1) and subtract(1, 's')
- if (typeof input === 'string' && typeof val === 'string') {
- dur = moment.duration(isNaN(+val) ? +input : +val, isNaN(+val) ? val : input);
- } else if (typeof input === 'string') {
- dur = moment.duration(+val, input);
- } else {
- dur = moment.duration(input, val);
- }
- addOrSubtractDurationFromMoment(this, dur, -1);
- return this;
- },
- diff : function (input, units, asFloat) {
- var that = makeAs(input, this),
- zoneDiff = (this.zone() - that.zone()) * 6e4,
- diff, output;
+/***/ },
+/* 46 */
+/***/ function(module, exports, __webpack_require__) {
- units = normalizeUnits(units);
+ /**
+ * @class Images
+ * This class loads images and keeps them stored.
+ */
+ function Images() {
+ this.images = {};
- if (units === 'year' || units === 'month') {
- // average number of days in the months in the given dates
- diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
- // difference in months
- output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
- // adjust by taking difference in days, average number of days
- // and dst in the given months.
- output += ((this - moment(this).startOf('month')) -
- (that - moment(that).startOf('month'))) / diff;
- // same as above but with zones, to negate all dst
- output -= ((this.zone() - moment(this).startOf('month').zone()) -
- (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
- if (units === 'year') {
- output = output / 12;
- }
- } else {
- diff = (this - that);
- output = units === 'second' ? diff / 1e3 : // 1000
- units === 'minute' ? diff / 6e4 : // 1000 * 60
- units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
- units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
- units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
- diff;
- }
- return asFloat ? output : absRound(output);
- },
+ this.callback = undefined;
+ }
- from : function (time, withoutSuffix) {
- return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
- },
+ /**
+ * Set an onload callback function. This will be called each time an image
+ * is loaded
+ * @param {function} callback
+ */
+ Images.prototype.setOnloadCallback = function(callback) {
+ this.callback = callback;
+ };
- fromNow : function (withoutSuffix) {
- return this.from(moment(), withoutSuffix);
- },
+ /**
+ *
+ * @param {string} url Url of the image
+ * @return {Image} img The image object
+ */
+ Images.prototype.load = function(url) {
+ 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.src = url;
+ }
- calendar : function (time) {
- // We want to compare the start of today, vs this.
- // Getting start-of-today depends on whether we're zone'd or not.
- var now = time || moment(),
- sod = makeAs(now, this).startOf('day'),
- diff = this.diff(sod, 'days', true),
- format = diff < -6 ? 'sameElse' :
- diff < -1 ? 'lastWeek' :
- diff < 0 ? 'lastDay' :
- diff < 1 ? 'sameDay' :
- diff < 2 ? 'nextDay' :
- diff < 7 ? 'nextWeek' : 'sameElse';
- return this.format(this.lang().calendar(format, this));
- },
+ return img;
+ };
- isLeapYear : function () {
- return isLeapYear(this.year());
- },
+ module.exports = Images;
- isDST : function () {
- return (this.zone() < this.clone().month(0).zone() ||
- this.zone() < this.clone().month(5).zone());
- },
- day : function (input) {
- var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
- if (input != null) {
- input = parseWeekday(input, this.lang());
- return this.add({ d : input - day });
- } else {
- return day;
- }
- },
+/***/ },
+/* 47 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var util = __webpack_require__(1);
+
+ /**
+ * @class Node
+ * A node. A node can be connected to other nodes via one or multiple edges.
+ * @param {object} properties An object containing properties for the node. All
+ * properties are optional, except for the id.
+ * {number} id Id of the node. Required
+ * {string} label Text label for the node
+ * {number} x Horizontal position of the node
+ * {number} y Vertical position of the node
+ * {string} shape Node shape, available:
+ * "database", "circle", "ellipse",
+ * "box", "image", "text", "dot",
+ * "star", "triangle", "triangleDown",
+ * "square"
+ * {string} image An image url
+ * {string} title An title text, can be HTML
+ * {anytype} group A group name or number
+ * @param {Network.Images} imagelist A list with images. Only needed
+ * when the node has an image
+ * @param {Network.Groups} grouplist A list with groups. Needed for
+ * retrieving group properties
+ * @param {Object} constants An object with default values for
+ * example for the color
+ *
+ */
+ function Node(properties, imagelist, grouplist, networkConstants) {
+ var constants = util.selectiveBridgeObject(['nodes'],networkConstants);
+ this.options = constants.nodes;
- month : makeAccessor('Month', true),
+ this.selected = false;
+ this.hover = false;
- startOf: function (units) {
- units = normalizeUnits(units);
- // the following switch intentionally omits break keywords
- // to utilize falling through the cases.
- switch (units) {
- case 'year':
- this.month(0);
- /* falls through */
- case 'quarter':
- case 'month':
- this.date(1);
- /* falls through */
- case 'week':
- case 'isoWeek':
- case 'day':
- this.hours(0);
- /* falls through */
- case 'hour':
- this.minutes(0);
- /* falls through */
- case 'minute':
- this.seconds(0);
- /* falls through */
- case 'second':
- this.milliseconds(0);
- /* falls through */
- }
+ this.edges = []; // all edges connected to this node
+ this.dynamicEdges = [];
+ this.reroutedEdges = {};
- // weeks are a special case
- if (units === 'week') {
- this.weekday(0);
- } else if (units === 'isoWeek') {
- this.isoWeekday(1);
- }
+ this.fontDrawThreshold = 3;
- // quarters are also special
- if (units === 'quarter') {
- this.month(Math.floor(this.month() / 3) * 3);
- }
+ // set defaults for the properties
+ this.id = undefined;
+ this.x = null;
+ this.y = null;
+ this.xFixed = false;
+ this.yFixed = false;
+ this.horizontalAlignLeft = true; // these are for the navigation controls
+ this.verticalAlignTop = true; // these are for the navigation controls
+ this.baseRadiusValue = networkConstants.nodes.radius;
+ this.radiusFixed = false;
+ this.level = -1;
+ this.preassignedLevel = false;
- return this;
- },
- endOf: function (units) {
- units = normalizeUnits(units);
- return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
- },
+ this.imagelist = imagelist;
+ this.grouplist = grouplist;
- isAfter: function (input, units) {
- units = typeof units !== 'undefined' ? units : 'millisecond';
- return +this.clone().startOf(units) > +moment(input).startOf(units);
- },
+ // physics properties
+ this.fx = 0.0; // external force x
+ this.fy = 0.0; // external force y
+ this.vx = 0.0; // velocity x
+ this.vy = 0.0; // velocity y
+ this.damping = networkConstants.physics.damping; // written every time gravity is calculated
+ this.fixedData = {x:null,y:null};
- isBefore: function (input, units) {
- units = typeof units !== 'undefined' ? units : 'millisecond';
- return +this.clone().startOf(units) < +moment(input).startOf(units);
- },
- isSame: function (input, units) {
- units = units || 'ms';
- return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
- },
+ this.setProperties(properties, constants);
- min: deprecate(
- "moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",
- function (other) {
- other = moment.apply(null, arguments);
- return other < this ? this : other;
- }
- ),
+ // creating the variables for clustering
+ this.resetCluster();
+ this.dynamicEdgesLength = 0;
+ this.clusterSession = 0;
+ this.clusterSizeWidthFactor = networkConstants.clustering.nodeScaling.width;
+ this.clusterSizeHeightFactor = networkConstants.clustering.nodeScaling.height;
+ this.clusterSizeRadiusFactor = networkConstants.clustering.nodeScaling.radius;
+ this.maxNodeSizeIncrements = networkConstants.clustering.maxNodeSizeIncrements;
+ this.growthIndicator = 0;
- max: deprecate(
- "moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",
- function (other) {
- other = moment.apply(null, arguments);
- return other > this ? this : other;
- }
- ),
+ // variables to tell the node about the network.
+ this.networkScaleInv = 1;
+ this.networkScale = 1;
+ this.canvasTopLeft = {"x": -300, "y": -300};
+ this.canvasBottomRight = {"x": 300, "y": 300};
+ this.parentEdgeId = null;
+ }
- // keepTime = true means only change the timezone, without affecting
- // the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
- // It is possible that 5:31:26 doesn't exist int zone +0200, so we
- // adjust the time as needed, to be valid.
- //
- // Keeping the time actually adds/subtracts (one hour)
- // from the actual represented time. That is why we call updateOffset
- // a second time. In case it wants us to change the offset again
- // _changeInProgress == true case, then we have to adjust, because
- // there is no such time in the given timezone.
- zone : function (input, keepTime) {
- var offset = this._offset || 0;
- if (input != null) {
- if (typeof input === "string") {
- input = timezoneMinutesFromString(input);
- }
- if (Math.abs(input) < 16) {
- input = input * 60;
- }
- this._offset = input;
- this._isUTC = true;
- if (offset !== input) {
- if (!keepTime || this._changeInProgress) {
- addOrSubtractDurationFromMoment(this,
- moment.duration(offset - input, 'm'), 1, false);
- } else if (!this._changeInProgress) {
- this._changeInProgress = true;
- moment.updateOffset(this, true);
- this._changeInProgress = null;
- }
- }
- } else {
- return this._isUTC ? offset : this._d.getTimezoneOffset();
- }
- return this;
- },
+ /**
+ * (re)setting the clustering variables and objects
+ */
+ Node.prototype.resetCluster = function() {
+ // clustering variables
+ this.formationScale = undefined; // this is used to determine when to open the cluster
+ this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
+ this.containedNodes = {};
+ this.containedEdges = {};
+ this.clusterSessions = [];
+ };
- zoneAbbr : function () {
- return this._isUTC ? "UTC" : "";
- },
+ /**
+ * Attach a edge to the node
+ * @param {Edge} edge
+ */
+ Node.prototype.attachEdge = function(edge) {
+ if (this.edges.indexOf(edge) == -1) {
+ this.edges.push(edge);
+ }
+ if (this.dynamicEdges.indexOf(edge) == -1) {
+ this.dynamicEdges.push(edge);
+ }
+ this.dynamicEdgesLength = this.dynamicEdges.length;
+ };
- zoneName : function () {
- return this._isUTC ? "Coordinated Universal Time" : "";
- },
+ /**
+ * Detach a edge from the node
+ * @param {Edge} edge
+ */
+ Node.prototype.detachEdge = function(edge) {
+ var index = this.edges.indexOf(edge);
+ if (index != -1) {
+ this.edges.splice(index, 1);
+ this.dynamicEdges.splice(index, 1);
+ }
+ this.dynamicEdgesLength = this.dynamicEdges.length;
+ };
- parseZone : function () {
- if (this._tzm) {
- this.zone(this._tzm);
- } else if (typeof this._i === 'string') {
- this.zone(this._i);
- }
- return this;
- },
- hasAlignedHourOffset : function (input) {
- if (!input) {
- input = 0;
- }
- else {
- input = moment(input).zone();
- }
+ /**
+ * Set or overwrite properties for the node
+ * @param {Object} properties an object with properties
+ * @param {Object} constants and object with default, global properties
+ */
+ Node.prototype.setProperties = function(properties, constants) {
+ if (!properties) {
+ return;
+ }
- return (this.zone() - input) % 60 === 0;
- },
+ var fields = ['borderWidth','borderWidthSelected','shape','image','radius','fontColor',
+ 'fontSize','fontFace','group','mass'
+ ];
+ util.selectiveDeepExtend(fields, this.options, properties);
- daysInMonth : function () {
- return daysInMonth(this.year(), this.month());
- },
+ this.originalLabel = undefined;
+ // basic properties
+ if (properties.id !== undefined) {this.id = properties.id;}
+ if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
+ if (properties.title !== undefined) {this.title = properties.title;}
+ if (properties.x !== undefined) {this.x = properties.x;}
+ if (properties.y !== undefined) {this.y = properties.y;}
+ if (properties.value !== undefined) {this.value = properties.value;}
+ if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
- dayOfYear : function (input) {
- var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
- return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
- },
+ // navigation controls properties
+ if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
+ if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
+ if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
- quarter : function (input) {
- return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
- },
+ if (this.id === undefined) {
+ throw "Node must have an id";
+ }
- weekYear : function (input) {
- var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
- return input == null ? year : this.add("y", (input - year));
- },
+ // copy group properties
+ if (typeof this.options.group === 'number' || (typeof this.options.group === 'string' && this.options.group != '')) {
+ var groupObj = this.grouplist.get(this.options.group);
+ for (var prop in groupObj) {
+ if (groupObj.hasOwnProperty(prop)) {
+ this.options[prop] = groupObj[prop];
+ }
+ }
+ }
- isoWeekYear : function (input) {
- var year = weekOfYear(this, 1, 4).year;
- return input == null ? year : this.add("y", (input - year));
- },
- week : function (input) {
- var week = this.lang().week(this);
- return input == null ? week : this.add("d", (input - week) * 7);
- },
+ // individual shape properties
+ if (properties.radius !== undefined) {this.baseRadiusValue = this.options.radius;}
+ if (properties.color !== undefined) {this.options.color = util.parseColor(properties.color);}
- isoWeek : function (input) {
- var week = weekOfYear(this, 1, 4).week;
- return input == null ? week : this.add("d", (input - week) * 7);
- },
+ if (this.options.image!== undefined && this.options.image!= "") {
+ if (this.imagelist) {
+ this.imageObj = this.imagelist.load(this.options.image);
+ }
+ else {
+ throw "No imagelist provided";
+ }
+ }
- weekday : function (input) {
- var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
- return input == null ? weekday : this.add("d", input - weekday);
- },
+ this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX);
+ this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY);
+ this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
- isoWeekday : function (input) {
- // behaves the same as moment#day except
- // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
- // as a setter, sunday should belong to the previous week.
- return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
- },
+ if (this.options.shape == 'image') {
+ this.options.radiusMin = constants.nodes.widthMin;
+ this.options.radiusMax = constants.nodes.widthMax;
+ }
- isoWeeksInYear : function () {
- return weeksInYear(this.year(), 1, 4);
- },
- weeksInYear : function () {
- var weekInfo = this._lang._week;
- return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
- },
+ // choose draw method depending on the shape
+ switch (this.options.shape) {
+ case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
+ case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
+ case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
+ case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
+ // TODO: add diamond shape
+ case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
+ case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
+ case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
+ case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
+ case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
+ case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
+ case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
+ default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
+ }
+ // reset the size of the node, this can be changed
+ this._reset();
+ };
- get : function (units) {
- units = normalizeUnits(units);
- return this[units]();
- },
+ /**
+ * select this node
+ */
+ Node.prototype.select = function() {
+ this.selected = true;
+ this._reset();
+ };
- set : function (units, value) {
- units = normalizeUnits(units);
- if (typeof this[units] === 'function') {
- this[units](value);
- }
- return this;
- },
+ /**
+ * unselect this node
+ */
+ Node.prototype.unselect = function() {
+ this.selected = false;
+ this._reset();
+ };
- // If passed a language key, it will set the language for this
- // instance. Otherwise, it will return the language configuration
- // variables for this instance.
- lang : function (key) {
- if (key === undefined) {
- return this._lang;
- } else {
- this._lang = getLangDefinition(key);
- return this;
- }
- }
- });
- function rawMonthSetter(mom, value) {
- var dayOfMonth;
+ /**
+ * Reset the calculated size of the node, forces it to recalculate its size
+ */
+ Node.prototype.clearSizeCache = function() {
+ this._reset();
+ };
- // TODO: Move this out of here!
- if (typeof value === 'string') {
- value = mom.lang().monthsParse(value);
- // TODO: Another silent failure?
- if (typeof value !== 'number') {
- return mom;
- }
- }
+ /**
+ * Reset the calculated size of the node, forces it to recalculate its size
+ * @private
+ */
+ Node.prototype._reset = function() {
+ this.width = undefined;
+ this.height = undefined;
+ };
- dayOfMonth = Math.min(mom.date(),
- daysInMonth(mom.year(), value));
- mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
- return mom;
- }
+ /**
+ * get the title of this node.
+ * @return {string} title The title of the node, or undefined when no title
+ * has been set.
+ */
+ Node.prototype.getTitle = function() {
+ return typeof this.title === "function" ? this.title() : this.title;
+ };
- function rawGetter(mom, unit) {
- return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
- }
+ /**
+ * Calculate the distance to the border of the Node
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Number} angle Angle in radians
+ * @returns {number} distance Distance to the border in pixels
+ */
+ Node.prototype.distanceToBorder = function (ctx, angle) {
+ var borderWidth = 1;
- function rawSetter(mom, unit, value) {
- if (unit === 'Month') {
- return rawMonthSetter(mom, value);
- } else {
- return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
- }
- }
+ if (!this.width) {
+ this.resize(ctx);
+ }
- function makeAccessor(unit, keepTime) {
- return function (value) {
- if (value != null) {
- rawSetter(this, unit, value);
- moment.updateOffset(this, keepTime);
- return this;
- } else {
- return rawGetter(this, unit);
- }
- };
- }
+ switch (this.options.shape) {
+ case 'circle':
+ case 'dot':
+ return this.options.radius+ borderWidth;
- moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
- moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
- moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
- // Setting the hour should keep the time, because the user explicitly
- // specified which hour he wants. So trying to maintain the same hour (in
- // a new timezone) makes sense. Adding/subtracting hours does not follow
- // this rule.
- moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
- // moment.fn.month is defined separately
- moment.fn.date = makeAccessor('Date', true);
- moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true));
- moment.fn.year = makeAccessor('FullYear', true);
- moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true));
+ case 'ellipse':
+ var a = this.width / 2;
+ var b = this.height / 2;
+ var w = (Math.sin(angle) * a);
+ var h = (Math.cos(angle) * b);
+ return a * b / Math.sqrt(w * w + h * h);
- // add plural methods
- moment.fn.days = moment.fn.day;
- moment.fn.months = moment.fn.month;
- moment.fn.weeks = moment.fn.week;
- moment.fn.isoWeeks = moment.fn.isoWeek;
- moment.fn.quarters = moment.fn.quarter;
+ // TODO: implement distanceToBorder for database
+ // TODO: implement distanceToBorder for triangle
+ // TODO: implement distanceToBorder for triangleDown
- // add aliased format methods
- moment.fn.toJSON = moment.fn.toISOString;
+ case 'box':
+ case 'image':
+ case 'text':
+ default:
+ if (this.width) {
+ return Math.min(
+ Math.abs(this.width / 2 / Math.cos(angle)),
+ Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
+ // TODO: reckon with border radius too in case of box
+ }
+ else {
+ return 0;
+ }
- /************************************
- Duration Prototype
- ************************************/
+ }
+ // TODO: implement calculation of distance to border for all shapes
+ };
+ /**
+ * Set forces acting on the node
+ * @param {number} fx Force in horizontal direction
+ * @param {number} fy Force in vertical direction
+ */
+ Node.prototype._setForce = function(fx, fy) {
+ this.fx = fx;
+ this.fy = fy;
+ };
- extend(moment.duration.fn = Duration.prototype, {
+ /**
+ * Add forces acting on the node
+ * @param {number} fx Force in horizontal direction
+ * @param {number} fy Force in vertical direction
+ * @private
+ */
+ Node.prototype._addForce = function(fx, fy) {
+ this.fx += fx;
+ this.fy += fy;
+ };
- _bubble : function () {
- var milliseconds = this._milliseconds,
- days = this._days,
- months = this._months,
- data = this._data,
- seconds, minutes, hours, years;
+ /**
+ * Perform one discrete step for the node
+ * @param {number} interval Time interval in seconds
+ */
+ Node.prototype.discreteStep = function(interval) {
+ if (!this.xFixed) {
+ var dx = this.damping * this.vx; // damping force
+ var ax = (this.fx - dx) / this.options.mass; // acceleration
+ this.vx += ax * interval; // velocity
+ this.x += this.vx * interval; // position
+ }
- // The following code bubbles up values, see the tests for
- // examples of what that means.
- data.milliseconds = milliseconds % 1000;
+ if (!this.yFixed) {
+ var dy = this.damping * this.vy; // damping force
+ var ay = (this.fy - dy) / this.options.mass; // acceleration
+ this.vy += ay * interval; // velocity
+ this.y += this.vy * interval; // position
+ }
+ };
- seconds = absRound(milliseconds / 1000);
- data.seconds = seconds % 60;
- minutes = absRound(seconds / 60);
- data.minutes = minutes % 60;
- hours = absRound(minutes / 60);
- data.hours = hours % 24;
+ /**
+ * Perform one discrete step for the node
+ * @param {number} interval Time interval in seconds
+ * @param {number} maxVelocity The speed limit imposed on the velocity
+ */
+ Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
+ if (!this.xFixed) {
+ var dx = this.damping * this.vx; // damping force
+ var ax = (this.fx - dx) / this.options.mass; // acceleration
+ this.vx += ax * interval; // velocity
+ this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
+ this.x += this.vx * interval; // position
+ }
+ else {
+ this.fx = 0;
+ }
- days += absRound(hours / 24);
- data.days = days % 30;
+ if (!this.yFixed) {
+ var dy = this.damping * this.vy; // damping force
+ var ay = (this.fy - dy) / this.options.mass; // acceleration
+ this.vy += ay * interval; // velocity
+ this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
+ this.y += this.vy * interval; // position
+ }
+ else {
+ this.fy = 0;
+ }
+ };
- months += absRound(days / 30);
- data.months = months % 12;
+ /**
+ * Check if this node has a fixed x and y position
+ * @return {boolean} true if fixed, false if not
+ */
+ Node.prototype.isFixed = function() {
+ return (this.xFixed && this.yFixed);
+ };
- years = absRound(months / 12);
- data.years = years;
- },
+ /**
+ * Check if this node is moving
+ * @param {number} vmin the minimum velocity considered as "moving"
+ * @return {boolean} true if moving, false if it has no velocity
+ */
+ Node.prototype.isMoving = function(vmin) {
+ var velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2));
+ // this.velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2))
+ return (velocity > vmin);
+ };
- weeks : function () {
- return absRound(this.days() / 7);
- },
+ /**
+ * check if this node is selecte
+ * @return {boolean} selected True if node is selected, else false
+ */
+ Node.prototype.isSelected = function() {
+ return this.selected;
+ };
- valueOf : function () {
- return this._milliseconds +
- this._days * 864e5 +
- (this._months % 12) * 2592e6 +
- toInt(this._months / 12) * 31536e6;
- },
+ /**
+ * Retrieve the value of the node. Can be undefined
+ * @return {Number} value
+ */
+ Node.prototype.getValue = function() {
+ return this.value;
+ };
- humanize : function (withSuffix) {
- var difference = +this,
- output = relativeTime(difference, !withSuffix, this.lang());
+ /**
+ * Calculate the distance from the nodes location to the given location (x,y)
+ * @param {Number} x
+ * @param {Number} y
+ * @return {Number} value
+ */
+ Node.prototype.getDistance = function(x, y) {
+ var dx = this.x - x,
+ dy = this.y - y;
+ return Math.sqrt(dx * dx + dy * dy);
+ };
- if (withSuffix) {
- output = this.lang().pastFuture(difference, output);
- }
- return this.lang().postformat(output);
- },
+ /**
+ * Adjust the value range of the node. The node will adjust it's radius
+ * based on its value.
+ * @param {Number} min
+ * @param {Number} max
+ */
+ Node.prototype.setValueRange = function(min, max) {
+ if (!this.radiusFixed && this.value !== undefined) {
+ if (max == min) {
+ this.options.radius= (this.options.radiusMin + this.options.radiusMax) / 2;
+ }
+ else {
+ var scale = (this.options.radiusMax - this.options.radiusMin) / (max - min);
+ this.options.radius= (this.value - min) * scale + this.options.radiusMin;
+ }
+ }
+ this.baseRadiusValue = this.options.radius;
+ };
- add : function (input, val) {
- // supports only 2.0-style add(1, 's') or add(moment)
- var dur = moment.duration(input, val);
+ /**
+ * Draw this node in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
+ */
+ Node.prototype.draw = function(ctx) {
+ throw "Draw method not initialized for node";
+ };
- this._milliseconds += dur._milliseconds;
- this._days += dur._days;
- this._months += dur._months;
+ /**
+ * Recalculate the size of this node in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
+ */
+ Node.prototype.resize = function(ctx) {
+ throw "Resize method not initialized for node";
+ };
- this._bubble();
+ /**
+ * Check if this object is overlapping with the provided object
+ * @param {Object} obj an object with parameters left, top, right, bottom
+ * @return {boolean} True if location is located on node
+ */
+ Node.prototype.isOverlappingWith = function(obj) {
+ return (this.left < obj.right &&
+ this.left + this.width > obj.left &&
+ this.top < obj.bottom &&
+ this.top + this.height > obj.top);
+ };
- return this;
- },
+ Node.prototype._resizeImage = function (ctx) {
+ // TODO: pre calculate the image size
- subtract : function (input, val) {
- var dur = moment.duration(input, val);
+ if (!this.width || !this.height) { // undefined or 0
+ var width, height;
+ if (this.value) {
+ this.options.radius= this.baseRadiusValue;
+ var scale = this.imageObj.height / this.imageObj.width;
+ if (scale !== undefined) {
+ width = this.options.radius|| this.imageObj.width;
+ height = this.options.radius* scale || this.imageObj.height;
+ }
+ else {
+ width = 0;
+ height = 0;
+ }
+ }
+ else {
+ width = this.imageObj.width;
+ height = this.imageObj.height;
+ }
+ this.width = width;
+ this.height = height;
- this._milliseconds -= dur._milliseconds;
- this._days -= dur._days;
- this._months -= dur._months;
+ this.growthIndicator = 0;
+ if (this.width > 0 && this.height > 0) {
+ this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
+ this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
+ this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
+ this.growthIndicator = this.width - width;
+ }
+ }
- this._bubble();
+ };
- return this;
- },
+ Node.prototype._drawImage = function (ctx) {
+ this._resizeImage(ctx);
- get : function (units) {
- units = normalizeUnits(units);
- return this[units.toLowerCase() + 's']();
- },
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
+
+ var yLabel;
+ if (this.imageObj.width != 0 ) {
+ // draw the shade
+ if (this.clusterSize > 1) {
+ var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
+ lineWidth *= this.networkScaleInv;
+ lineWidth = Math.min(0.2 * this.width,lineWidth);
- as : function (units) {
- units = normalizeUnits(units);
- return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
- },
+ ctx.globalAlpha = 0.5;
+ ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
+ }
- lang : moment.fn.lang,
+ // draw the image
+ ctx.globalAlpha = 1.0;
+ ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
+ yLabel = this.y + this.height / 2;
+ }
+ else {
+ // image still loading... just draw the label for now
+ yLabel = this.y;
+ }
- toIsoString : function () {
- // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
- var years = Math.abs(this.years()),
- months = Math.abs(this.months()),
- days = Math.abs(this.days()),
- hours = Math.abs(this.hours()),
- minutes = Math.abs(this.minutes()),
- seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
+ this._label(ctx, this.label, this.x, yLabel, undefined, "top");
+ };
- if (!this.asSeconds()) {
- // this is the same as C#'s (Noda) and python (isodate)...
- // but not other JS (goog.date)
- return 'P0D';
- }
- return (this.asSeconds() < 0 ? '-' : '') +
- 'P' +
- (years ? years + 'Y' : '') +
- (months ? months + 'M' : '') +
- (days ? days + 'D' : '') +
- ((hours || minutes || seconds) ? 'T' : '') +
- (hours ? hours + 'H' : '') +
- (minutes ? minutes + 'M' : '') +
- (seconds ? seconds + 'S' : '');
- }
- });
+ Node.prototype._resizeBox = function (ctx) {
+ if (!this.width) {
+ var margin = 5;
+ var textSize = this.getTextSize(ctx);
+ this.width = textSize.width + 2 * margin;
+ this.height = textSize.height + 2 * margin;
- function makeDurationGetter(name) {
- moment.duration.fn[name] = function () {
- return this._data[name];
- };
- }
+ this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
+ this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
+ this.growthIndicator = this.width - (textSize.width + 2 * margin);
+ // this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
- function makeDurationAsGetter(name, factor) {
- moment.duration.fn['as' + name] = function () {
- return +this / factor;
- };
- }
+ }
+ };
- for (i in unitMillisecondFactors) {
- if (unitMillisecondFactors.hasOwnProperty(i)) {
- makeDurationAsGetter(i, unitMillisecondFactors[i]);
- makeDurationGetter(i.toLowerCase());
- }
- }
+ Node.prototype._drawBox = function (ctx) {
+ this._resizeBox(ctx);
- makeDurationAsGetter('Weeks', 6048e5);
- moment.duration.fn.asMonths = function () {
- return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
- };
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
+ var clusterLineWidth = 2.5;
+ var borderWidth = this.options.borderWidth;
+ var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
- /************************************
- Default Lang
- ************************************/
+ ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
+ // draw the outer border
+ if (this.clusterSize > 1) {
+ ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth *= this.networkScaleInv;
+ ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
- // Set default language, other languages will inherit from English.
- moment.lang('en', {
- ordinal : function (number) {
- var b = number % 10,
- output = (toInt(number % 100 / 10) === 1) ? 'th' :
- (b === 1) ? 'st' :
- (b === 2) ? 'nd' :
- (b === 3) ? 'rd' : 'th';
- return number + output;
- }
- });
+ ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.options.radius);
+ ctx.stroke();
+ }
+ ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth *= this.networkScaleInv;
+ ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
- /* EMBED_LANGUAGES */
+ ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.options.color.background;
- /************************************
- Exposing Moment
- ************************************/
+ ctx.roundRect(this.left, this.top, this.width, this.height, this.options.radius);
+ ctx.fill();
+ ctx.stroke();
- function makeGlobal(shouldDeprecate) {
- /*global ender:false */
- if (typeof ender !== 'undefined') {
- return;
- }
- oldGlobalMoment = globalScope.moment;
- if (shouldDeprecate) {
- globalScope.moment = deprecate(
- "Accessing Moment through the global scope is " +
- "deprecated, and will be removed in an upcoming " +
- "release.",
- moment);
- } else {
- globalScope.moment = moment;
- }
- }
+ this._label(ctx, this.label, this.x, this.y);
+ };
- // CommonJS module is defined
- if (hasModule) {
- module.exports = moment;
- } else if (true) {
- !(__WEBPACK_AMD_DEFINE_RESULT__ = (function (require, exports, module) {
- if (module.config && module.config() && module.config().noGlobal === true) {
- // release the global variable
- globalScope.moment = oldGlobalMoment;
- }
- return moment;
- }.call(exports, __webpack_require__, exports, module)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
- makeGlobal(true);
- } else {
- makeGlobal();
- }
- }).call(this);
-
- /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()), __webpack_require__(64)(module)))
+ Node.prototype._resizeDatabase = function (ctx) {
+ if (!this.width) {
+ var margin = 5;
+ var textSize = this.getTextSize(ctx);
+ var size = textSize.width + 2 * margin;
+ this.width = size;
+ this.height = size;
-/***/ },
-/* 52 */
-/***/ function(module, exports, __webpack_require__) {
+ // scaling used for clustering
+ this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
+ this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
+ this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
+ this.growthIndicator = this.width - size;
+ }
+ };
- var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v1.1.3 - 2014-05-20
- * http://eightmedia.github.io/hammer.js
- *
- * Copyright (c) 2014 Jorik Tangelder
' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ 'Simulation Mode: ' +
+ ' ' +
+ 'Barnes Hut ' +
+ 'Repulsion ' +
+ 'Hierarchical ' +
+ '
'
+ 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);
+
+ 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");
+
+ 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");
+
+ 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;
}
- }
- };
+ var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
+ var graph_repositionNodes = document.getElementById("graph_repositionNodes");
+ var graph_generateOptions = document.getElementById("graph_generateOptions");
- /**
- * retrieve all nodes overlapping with given object
- * @param {Object} object An object with parameters left, top, right, bottom
- * @return {Number[]} An array with id's of the overlapping nodes
- * @private
- */
- exports._getAllEdgesOverlappingWith = function (object) {
- var overlappingEdges = [];
- this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
- return overlappingEdges;
- };
+ 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";
+ }
- /**
- * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
- * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
- *
- * @param pointer
- * @returns {null}
- * @private
- */
- exports._getEdgeAt = function(pointer) {
- var positionObject = this._pointerToPositionObject(pointer);
- var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
- if (overlappingEdges.length > 0) {
- return this.edges[overlappingEdges[overlappingEdges.length - 1]];
- }
- else {
- return null;
+ switchConfigurations.apply(this);
+
+ radioButton1.onchange = switchConfigurations.bind(this);
+ radioButton2.onchange = switchConfigurations.bind(this);
+ radioButton3.onchange = switchConfigurations.bind(this);
}
};
-
/**
- * Add object to the selection array.
+ * This overwrites the this.constants.
*
- * @param obj
+ * @param constantsVariableName
+ * @param value
* @private
*/
- exports._addToSelection = function(obj) {
- if (obj instanceof Node) {
- this.selectionObj.nodes[obj.id] = obj;
+ exports._overWriteGraphConstants = function (constantsVariableName, value) {
+ var nameArray = constantsVariableName.split("_");
+ if (nameArray.length == 1) {
+ this.constants[nameArray[0]] = value;
}
- else {
- this.selectionObj.edges[obj.id] = obj;
+ 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;
}
};
+
/**
- * Add object to the selection array.
- *
- * @param obj
- * @private
+ * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
*/
- exports._addToHover = function(obj) {
- if (obj instanceof Node) {
- this.hoverObj.nodes[obj.id] = obj;
- }
- else {
- this.hoverObj.edges[obj.id] = obj;
- }
- };
+ 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._configureSmoothCurves(false);
+ }
/**
- * Remove a single option from selection.
+ * this function is used to scramble the nodes
*
- * @param {Object} obj
- * @private
*/
- exports._removeFromSelection = function(obj) {
- if (obj instanceof Node) {
- delete this.selectionObj.nodes[obj.id];
+ 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 {
- delete this.selectionObj.edges[obj.id];
+ this.repositionNodes();
}
- };
+ this.moving = true;
+ this.start();
+ }
/**
- * Unselect all. The selectionObj is useful for this.
- *
- * @param {Boolean} [doNotTrigger] | ignore trigger
- * @private
+ * this is used to generate an options file from the playing with physics system.
*/
- exports._unselectAll = function(doNotTrigger) {
- if (doNotTrigger === undefined) {
- doNotTrigger = false;
+ 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 += '};'
+ }
}
- for(var nodeId in this.selectionObj.nodes) {
- if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- this.selectionObj.nodes[nodeId].unselect();
+ 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 += '};'
}
- for(var edgeId in this.selectionObj.edges) {
- if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
- this.selectionObj.edges[edgeId].unselect();
+ 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 += ", ";
+ }
+ }
+ 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.selectionObj = {nodes:{},edges:{}};
- if (doNotTrigger == false) {
- this.emit('select', this.getSelection());
- }
- };
+ this.optionsDiv.innerHTML = options;
+ }
/**
- * Unselect all clusters. The selectionObj is useful for this.
+ * this is used to switch between barnesHut, repulsion and hierarchical.
*
- * @param {Boolean} [doNotTrigger] | ignore trigger
- * @private
*/
- exports._unselectClusters = function(doNotTrigger) {
- if (doNotTrigger === undefined) {
- doNotTrigger = false;
+ 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";
+ }
}
-
- for (var nodeId in this.selectionObj.nodes) {
- if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
- this.selectionObj.nodes[nodeId].unselect();
- this._removeFromSelection(this.selectionObj.nodes[nodeId]);
- }
+ 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();
}
}
-
- if (doNotTrigger == false) {
- this.emit('select', this.getSelection());
+ 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();
+ }
/**
- * return the number of selected nodes
+ * this generates the ranges depending on the iniital values.
*
- * @returns {number}
- * @private
+ * @param id
+ * @param map
+ * @param constantsVariableName
*/
- exports._getSelectedNodeCount = function() {
- var count = 0;
- for (var nodeId in this.selectionObj.nodes) {
- if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- count += 1;
- }
+ function showValueOfRange (id,map,constantsVariableName) {
+ var valueId = id + "_value";
+ var rangeValue = document.getElementById(id).value;
+
+ if (map instanceof Array) {
+ document.getElementById(valueId).value = map[parseInt(rangeValue)];
+ this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
+ }
+ else {
+ document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
+ this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
}
- 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];
- }
+ if (constantsVariableName == "hierarchicalLayout_direction" ||
+ constantsVariableName == "hierarchicalLayout_levelSeparation" ||
+ constantsVariableName == "hierarchicalLayout_nodeSpacing") {
+ this._setupHierarchicalLayout();
}
- return null;
- };
+ this.moving = true;
+ this.start();
+ }
+
+
+/***/ },
+/* 52 */
+/***/ function(module, exports, __webpack_require__) {
/**
- * return the selected edge
+ * Calculate the forces the nodes apply on each other based on a repulsion field.
+ * This field is linearly approximated.
*
- * @returns {number}
* @private
*/
- exports._getSelectedEdge = function() {
- for (var edgeId in this.selectionObj.edges) {
- if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
- return this.selectionObj.edges[edgeId];
+ exports._calculateNodeForces = function () {
+ var dx, dy, angle, distance, fx, fy, combinedClusterSize,
+ repulsingForce, node1, node2, i, j;
+
+ 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 {
+ 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 / distance;
+
+ fx = dx * repulsingForce;
+ fy = dy * repulsingForce;
+
+ node1.fx -= fx;
+ node1.fy -= fy;
+ node2.fx += fx;
+ node2.fy += fy;
+ }
}
}
- return null;
};
+/***/ },
+/* 53 */
+/***/ function(module, exports, __webpack_require__) {
+
/**
- * return the number of selected edges
+ * Calculate the forces the nodes apply on eachother based on a repulsion field.
+ * This field is linearly approximated.
*
- * @returns {number}
* @private
*/
- exports._getSelectedEdgeCount = function() {
- var count = 0;
- for (var edgeId in this.selectionObj.edges) {
- if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
- count += 1;
+ exports._calculateNodeForces = function () {
+ var dx, dy, distance, fx, fy,
+ repulsingForce, node1, node2, i, j;
+
+ var nodes = this.calculationNodes;
+ var nodeIndices = this.calculationNodeIndices;
+
+ // repulsing forces between nodes
+ var nodeDistance = this.constants.physics.hierarchicalRepulsion.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]];
+
+ // 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;
+ }
}
}
- return count;
};
/**
- * return the number of selected objects.
+ * this function calculates the effects of the springs in the case of unsmooth curves.
*
- * @returns {number}
* @private
*/
- exports._getSelectedObjectCount = function() {
- var count = 0;
- for(var nodeId in this.selectionObj.nodes) {
- if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
- count += 1;
- }
+ 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;
}
- for(var edgeId in this.selectionObj.edges) {
- if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
- count += 1;
+
+
+ // 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;
+
+
+
+ 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;
+ }
+ }
+ }
}
}
- 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;
- }
+ // 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;
}
- for(var edgeId in this.selectionObj.edges) {
- if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
- return false;
- }
+
+ // 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;
}
- return true;
+ 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;
+ }
+
};
+/***/ },
+/* 54 */
+/***/ function(module, exports, __webpack_require__) {
/**
- * check if one of the selected nodes is a cluster.
+ * 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.
*
- * @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;
+ 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);
+
+ var barnesHutTree = this.barnesHutTree;
+
+ // 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);
}
}
}
- 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
+ * 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 {Node} node
+ * @param parentBranch
+ * @param 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);
- }
- };
+ 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);
- /**
- * 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);
+ // BarnesHut condition
+ // original condition : s/d < theta = passed === d/s > 1/theta = passed
+ // calcSize = 1/s --> d * 1/s > 1/theta = passed
+ if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
+ // 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 {
+ // 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;
+ }
+ }
+ }
}
};
-
-
-
/**
- * 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
+ * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
*
- * @param {Node || Edge} object
- * @param {Boolean} append
- * @param {Boolean} [doNotTrigger] | ignore trigger
+ * @param nodes
+ * @param nodeIndices
* @private
*/
- exports._selectObject = function(object, append, doNotTrigger, highlightEdges) {
- if (doNotTrigger === undefined) {
- doNotTrigger = false;
- }
- if (highlightEdges === undefined) {
- highlightEdges = true;
- }
+ exports._formBarnesHutTree = function(nodes,nodeIndices) {
+ var node;
+ var nodeCount = nodeIndices.length;
- if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
- this._unselectAll(true);
- }
+ var minX = Number.MAX_VALUE,
+ minY = Number.MAX_VALUE,
+ maxX =-Number.MAX_VALUE,
+ maxY =-Number.MAX_VALUE;
- if (object.selected == false) {
- object.select();
- this._addToSelection(object);
- if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) {
- this._selectConnectedEdges(object);
+ // 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 {
- object.unselect();
- this._removeFromSelection(object);
- }
-
- if (doNotTrigger == false) {
- this.emit('select', this.getSelection());
- }
- };
+ // 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
- /**
- * 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});
- }
- };
+ 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);
- /**
- * 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});
+ // 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);
}
}
- if (object instanceof Node) {
- this._hoverConnectedEdges(object);
- }
+
+ // make global
+ this.barnesHutTree = barnesHutTree
};
/**
- * 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
+ * this updates the mass of a branch. this is increased by adding a node.
*
- * @param {Object} pointer
+ * @param parentBranch
+ * @param node
* @private
*/
- exports._handleTouch = function(pointer) {
- };
+ 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;
- /**
- * 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();
- }
- }
- this.emit("click", this.getSelection());
- this._redraw();
- };
+ 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;
- /**
- * 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);
- }
- this.emit("doubleClick", this.getSelection());
};
/**
- * Handle the onHold selection part
+ * determine in which branch the node will be placed.
*
- * @param pointer
+ * @param parentBranch
+ * @param node
+ * @param skipMassUpdate
* @private
*/
- exports._handleOnHold = function(pointer) {
- var node = this._getNodeAt(pointer);
- if (node != null) {
- this._selectObject(node,true);
+ exports._placeInTree = function(parentBranch,node,skipMassUpdate) {
+ if (skipMassUpdate != true || skipMassUpdate === undefined) {
+ // update the mass of the branch.
+ this._updateBranchMass(parentBranch,node);
}
- else {
- var edge = this._getEdgeAt(pointer);
- if (edge != null) {
- this._selectObject(edge,true);
+
+ 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");
+ }
+ else { // in SW
+ this._placeInRegion(parentBranch,node,"SW");
+ }
+ }
+ else { // in NE or SE
+ if (parentBranch.children.NW.range.maxY > node.y) { // in NE
+ this._placeInRegion(parentBranch,node,"NE");
+ }
+ else { // in SE
+ this._placeInRegion(parentBranch,node,"SE");
}
}
- this._redraw();
};
/**
- * handle the onRelease event. These functions are here for the navigation controls module.
+ * actually place the node in a region (or branch)
*
- * @private
+ * @param parentBranch
+ * @param node
+ * @param region
+ * @private
*/
- exports._handleOnRelease = function(pointer) {
-
+ 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);
+ }
+ break;
+ case 4: // place in branch
+ this._placeInTree(parentBranch.children[region],node);
+ break;
+ }
};
-
- /**
- *
- * retrieve the currently selected objects
- * @return {{nodes: Array. ' +
+ 'Options: ' +
+ ' ' +
+ '' +
+ ' ' +
+ ' ' +
+ '
' +
- ' ' +
- ' ' +
- ' ' +
- ' ' +
- 'Simulation Mode: ' +
- ' ' +
- 'Barnes Hut ' +
- 'Repulsion ' +
- 'Hierarchical ' +
- '
'
- 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);
-
- 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");
-
- 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");
-
- 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");
+ 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();
+ }
+ }
+ };
- 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;
+ /**
+ * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
+ *
+ * @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();
+ });
}
- if (this.constants.hierarchicalLayout.enabled) {
- radioButton3.checked = true;
+ else {
+ 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');
+ }
+ };
- 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";
+
+
+ /**
+ * delete everything in the selection
+ *
+ * @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 {
- graph_toggleSmooth.style.background = "#FF8532";
+ alert(this.constants.locales[this.constants.locale]["deleteClusterError"]);
}
+ }
+ };
- switchConfigurations.apply(this);
+/***/ },
+/* 59 */
+/***/ function(module, exports, __webpack_require__) {
- radioButton1.onchange = switchConfigurations.bind(this);
- radioButton2.onchange = switchConfigurations.bind(this);
- radioButton3.onchange = switchConfigurations.bind(this);
+ var util = __webpack_require__(1);
+ var Hammer = __webpack_require__(15);
+
+ exports._cleanNavigation = function() {
+ // clean up previous navigation items
+ var wrapper = document.getElementById('network-navigation_wrapper');
+ if (wrapper && wrapper.parentNode) {
+ wrapper.parentNode.removeChild(wrapper);
}
+ document.onmouseup = null;
};
/**
- * This overwrites the this.constants.
+ * 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.
*
- * @param constantsVariableName
- * @param value
* @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;
- }
- else if (nameArray.length == 3) {
- this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
+ 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.navigationDivs['wrapper'].id = 'network-navigation_wrapper';
+ this.frame.appendChild(this.navigationDivs['wrapper']);
+
+ var me = this;
+ for (var i = 0; i < navigationDivs.length; i++) {
+ this.navigationDivs[navigationDivs[i]] = document.createElement('div');
+ this.navigationDivs[navigationDivs[i]].id = 'network-navigation_' + navigationDivs[i];
+ 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', me[navigationDivActions[i]].bind(me));
}
+ var hammer = Hammer(document, {prevent_default: false});
+ hammer.on('release', me._stopMovement.bind(me));
};
-
/**
- * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
+ * this stops all movement induced by the navigation buttons
+ *
+ * @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._stopMovement = function() {
+ this._xStopMoving();
+ this._yStopMoving();
+ this._stopZoom();
+ };
- this._configureSmoothCurves(false);
- }
/**
- * this function is used to scramble the nodes
+ * 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
*/
- 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();
- }
- this.moving = true;
- this.start();
- }
+ 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();
+ };
+
/**
- * this is used to generate an options file from the playing with physics system.
+ * move the screen down
+ * @private
*/
- 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 += ", "
- }
+ 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();
+ };
+
+
+ /**
+ * 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();
+ };
+
+
+ /**
+ * 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();
+ };
+
+
+ /**
+ * 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();
+ };
+
+
+ /**
+ * Stop zooming and unhighlight the zoom controls
+ * @private
+ */
+ exports._stopZoom = function(event) {
+ this.zoomIncrement = 0;
+ event && event.preventDefault();
+ };
+
+
+ /**
+ * Stop moving in the Y direction and unHighlight the up and down
+ * @private
+ */
+ exports._yStopMoving = function(event) {
+ this.yIncrement = 0;
+ event && event.preventDefault();
+ };
+
+
+ /**
+ * Stop moving in the X direction and unHighlight left and right.
+ * @private
+ */
+ exports._xStopMoving = function(event) {
+ this.xIncrement = 0;
+ event && event.preventDefault();
+ };
+
+
+/***/ },
+/* 60 */
+/***/ 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;
}
- 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;
+ }
+ };
+
+ /**
+ * This is the main function to layout the nodes in a hierarchical way.
+ * It checks if the node details are supplied correctly
+ *
+ * @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 *= -1;
}
- if (options != "No options are required, default values used.") {
- options += '};'
+ else {
+ this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
}
- }
- 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 += ", "
- }
+
+ if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "LR") {
+ if (this.constants.smoothCurves.enabled == true) {
+ this.constants.smoothCurves.type = "vertical";
}
- options += '}}'
}
- if (optionsSpecific.length == 0) {options += "}"}
- if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
- options += ", smoothCurves: " + this.constants.smoothCurves;
+ else {
+ if (this.constants.smoothCurves.enabled == true) {
+ this.constants.smoothCurves.type = "horizontal";
+ }
}
- 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 += ", ";
+ // 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;
}
}
- 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 += ", "
- }
+
+ // 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(true,this.constants.clustering.enabled);
+ if (!this.constants.clustering.enabled) {
+ this.start();
}
- options += '}'
}
else {
- options += "enabled:true}";
+ // setup the system to use hierarchical method.
+ this._changeConstants();
+
+ // define levels if undefined by the users. Based on hubsize
+ if (undefinedLevel == true) {
+ this._determineLevels(hubsize);
+ }
+ // check the distribution of the nodes per level.
+ var distribution = this._getDistribution();
+
+ // place the nodes on the canvas. This also stablilizes the system.
+ this._placeNodesByHierarchy(distribution);
+
+ // start the simulation.
+ this.start();
}
- options += '};'
}
+ };
- this.optionsDiv.innerHTML = options;
- }
-
/**
- * this is used to switch between barnesHut, repulsion and hierarchical.
+ * This function places the nodes on the canvas based on the hierarchial distribution.
*
+ * @param {Object} distribution | obtained by the function this._getDistribution()
+ * @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;
- }
- 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();
+ 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;
+
+ distribution[level].minPos += distribution[level].nodeSpacing;
+ }
+ }
+ else {
+ 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);
+ }
+ }
}
}
- 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();
- }
+
+ // stabilize the system after positioning. This function calls zoomExtent.
+ this._stabilize();
+ };
/**
- * this generates the ranges depending on the iniital values.
+ * This function get the distribution of levels based on hubsize
*
- * @param id
- * @param map
- * @param constantsVariableName
+ * @returns {Object}
+ * @private
*/
- function showValueOfRange (id,map,constantsVariableName) {
- var valueId = id + "_value";
- var rangeValue = document.getElementById(id).value;
+ exports._getDistribution = function() {
+ var distribution = {};
+ var nodeId, node, level;
- if (map instanceof Array) {
- document.getElementById(valueId).value = map[parseInt(rangeValue)];
- this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
- }
- else {
- document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
- this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
+ // 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;
+ }
}
- if (constantsVariableName == "hierarchicalLayout_direction" ||
- constantsVariableName == "hierarchicalLayout_levelSeparation" ||
- constantsVariableName == "hierarchicalLayout_nodeSpacing") {
- this._setupHierarchicalLayout();
+ // 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;
+ }
+ }
}
- this.moving = true;
- this.start();
- }
-
-/***/ },
-/* 60 */
-/***/ function(module, exports, __webpack_require__) {
+ // 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);
+ }
+ }
- var map = {};
- function webpackContext(req) {
- return __webpack_require__(webpackContextResolve(req));
- };
- function webpackContextResolve(req) {
- return map[req] || (function() { throw new Error("Cannot find module '" + req + "'.") }());
- };
- webpackContext.keys = function webpackContextKeys() {
- return Object.keys(map);
+ return distribution;
};
- webpackContext.resolve = webpackContextResolve;
- module.exports = webpackContext;
-
-/***/ },
-/* 61 */
-/***/ 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 function allocates nodes in levels based on the recursive branching from the largest hubs.
*
+ * @param hubsize
* @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;
-
- // 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 {
- 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 / distance;
+ exports._determineLevels = function(hubsize) {
+ var nodeId, node;
- fx = dx * repulsingForce;
- fy = dy * repulsingForce;
+ // determine hubs
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (node.edges.length == hubsize) {
+ node.level = 0;
+ }
+ }
+ }
- node1.fx -= fx;
- node1.fy -= fy;
- node2.fx += fx;
- node2.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);
}
}
}
};
-/***/ },
-/* 62 */
-/***/ function(module, exports, __webpack_require__) {
-
/**
- * Calculate the forces the nodes apply on eachother based on a repulsion field.
- * This field is linearly approximated.
+ * 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
*/
- exports._calculateNodeForces = function () {
- var dx, dy, distance, fx, fy,
- repulsingForce, node1, node2, i, j;
-
- var nodes = this.calculationNodes;
- var nodeIndices = this.calculationNodeIndices;
-
- // repulsing forces between nodes
- var nodeDistance = this.constants.physics.hierarchicalRepulsion.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]];
-
- // nodes only affect nodes on their level
- if (node1.level == node2.level) {
+ 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();
+ };
- dx = node2.x - node1.x;
- dy = node2.y - node1.y;
- distance = Math.sqrt(dx * dx + dy * dy);
+ /**
+ * 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 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;
+ }
+ else {
+ childNode = edges[i].to;
+ }
- 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;
+ // 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 {
+ if (childNode.yFixed && childNode.level > parentLevel) {
+ childNode.yFixed = false;
+ childNode.y = distribution[childNode.level].minPos;
+ nodeMoved = true;
+ }
+ }
- node1.fx -= fx;
- node1.fy -= fy;
- node2.fx += fx;
- node2.fy += fy;
+ 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);
}
}
}
@@ -30795,521 +30977,490 @@ return /******/ (function(modules) { // webpackBootstrap
/**
- * this function calculates the effects of the springs in the case of unsmooth curves.
+ * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
*
+ * @param level
+ * @param edges
+ * @param parentId
* @private
*/
- 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;
+ 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 (edges.length > 1) {
+ this._setLevel(level+1, childNode.edges, childNode.id);
+ }
+ }
+ }
+ };
- for (var i = 0; i < nodeIndices.length; i++) {
- var node1 = nodes[nodeIndices[i]];
- node1.springFx = 0;
- node1.springFy = 0;
+ /**
+ * Unfix nodes
+ *
+ * @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;
+ }
}
+ };
- // 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;
+/***/ },
+/* 61 */
+/***/ function(module, exports, __webpack_require__) {
- dx = (edge.from.x - edge.to.x);
- dy = (edge.from.y - edge.to.y);
- distance = Math.sqrt(dx * dx + dy * dy);
+ var mousetrap = __webpack_require__(41);
+ var Emitter = __webpack_require__(6);
+ var Hammer = __webpack_require__(15);
+ var util = __webpack_require__(1);
- if (distance == 0) {
- distance = 0.01;
- }
+ /**
+ * Turn an element into an clickToUse element.
+ * When not active, the element has a transparent overlay. When the overlay is
+ * clicked, the mode is changed to active.
+ * When active, the element is displayed with a blue border around it, and
+ * the interactive contents of the element can be used. When clicked outside
+ * the element, the elements mode is changed to inactive.
+ * @param {Element} container
+ * @constructor
+ */
+ function Activator(container) {
+ this.active = false;
- // 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.dom = {
+ container: container
+ };
- fx = dx * springForce;
- fy = dy * springForce;
+ this.dom.overlay = document.createElement('div');
+ this.dom.overlay.className = 'overlay';
+
+ this.dom.container.appendChild(this.dom.overlay);
+ this.hammer = Hammer(this.dom.overlay, {prevent_default: false});
+ this.hammer.on('tap', this._onTapOverlay.bind(this));
+ // block all touch events (except tap)
+ var me = this;
+ var events = [
+ 'touch', 'pinch',
+ 'doubletap', 'hold',
+ 'dragstart', 'drag', 'dragend',
+ 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
+ ];
+ events.forEach(function (event) {
+ me.hammer.on(event, function (event) {
+ event.stopPropagation();
+ });
+ });
- 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;
- }
- }
- }
+ // attach a tap event to the window, in order to deactivate when clicking outside the timeline
+ this.windowHammer = Hammer(window, {prevent_default: false});
+ this.windowHammer.on('tap', function (event) {
+ // deactivate when clicked outside the container
+ if (!_hasParent(event.target, container)) {
+ me.deactivate();
}
- }
+ });
- // 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));
+ // mousetrap listener only bounded when active)
+ this.escListener = this.deactivate.bind(this);
+ }
- node.fx += springFx;
- node.fy += springFy;
- }
+ // turn into an event emitter
+ Emitter(Activator.prototype);
- // 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;
+ // The currently active activator
+ Activator.current = null;
- for (i = 0; i < nodeIndices.length; i++) {
- var node = nodes[nodeIndices[i]];
- node.fx -= correctionFx;
- node.fy -= correctionFy;
- }
+ /**
+ * Destroy the activator. Cleans up all created DOM and event listeners
+ */
+ Activator.prototype.destroy = function () {
+ this.deactivate();
- };
+ // remove dom
+ this.dom.overlay.parentNode.removeChild(this.dom.overlay);
-/***/ },
-/* 63 */
-/***/ function(module, exports, __webpack_require__) {
+ // cleanup hammer instances
+ this.hammer = null;
+ this.windowHammer = null;
+ // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory)
+ };
/**
- * 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
+ * Activate the element
+ * Overlay is hidden, element is decorated with a blue shadow border
*/
- exports._calculateNodeForces = function() {
- if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
- var node;
- var nodes = this.calculationNodes;
- var nodeIndices = this.calculationNodeIndices;
- var nodeCount = nodeIndices.length;
+ Activator.prototype.activate = function () {
+ // we allow only one active activator at a time
+ if (Activator.current) {
+ Activator.current.deactivate();
+ }
+ Activator.current = this;
- this._formBarnesHutTree(nodes,nodeIndices);
+ this.active = true;
+ this.dom.overlay.style.display = 'none';
+ util.addClassName(this.dom.container, 'vis-active');
- var barnesHutTree = this.barnesHutTree;
+ this.emit('change');
+ this.emit('activate');
- // 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);
- }
- }
- }
+ // ugly hack: bind ESC after emitting the events, as the Network rebinds all
+ // keyboard events on a 'change' event
+ mousetrap.bind('esc', this.escListener);
};
-
/**
- * 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
+ * Deactivate the element
+ * Overlay is displayed on top of the element
*/
- 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);
+ Activator.prototype.deactivate = function () {
+ this.active = false;
+ this.dom.overlay.style.display = '';
+ util.removeClassName(this.dom.container, 'vis-active');
+ mousetrap.unbind('esc', this.escListener);
- // BarnesHut condition
- // original condition : s/d < theta = passed === d/s > 1/theta = passed
- // calcSize = 1/s --> d * 1/s > 1/theta = passed
- if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
- // 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 {
- // 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;
- }
- }
- }
- }
+ this.emit('change');
+ this.emit('deactivate');
};
/**
- * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
- *
- * @param nodes
- * @param nodeIndices
+ * Handle a tap event: activate the container
+ * @param event
* @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;
+ Activator.prototype._onTapOverlay = function (event) {
+ // activate the container
+ this.activate();
+ event.stopPropagation();
+ };
- // 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; }
+ /**
+ * Test whether the element has the requested parent element somewhere in
+ * its chain of parent nodes.
+ * @param {HTMLElement} element
+ * @param {HTMLElement} parent
+ * @returns {boolean} Returns true when the parent is found somewhere in the
+ * chain of parent nodes.
+ * @private
+ */
+ function _hasParent(element, parent) {
+ while (element) {
+ if (element === parent) {
+ return true
}
+ element = element.parentNode;
}
- // 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
+ return false;
+ }
+ module.exports = Activator;
- 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);
+/***/ },
+/* 62 */
+/***/ function(module, exports, __webpack_require__) {
- // 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);
- }
- }
+ // 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'];
- // make global
- this.barnesHutTree = barnesHutTree
+ // 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'];
+/***/ },
+/* 63 */
+/***/ function(module, exports, __webpack_require__) {
+
/**
- * this updates the mass of a branch. this is increased by adding a node.
- *
- * @param parentBranch
- * @param node
- * @private
+ * Canvas shapes used by Network
*/
- exports._updateBranchMass = function(parentBranch, node) {
- var totalMass = parentBranch.mass + node.options.mass;
- var totalMassInv = 1/totalMass;
+ if (typeof CanvasRenderingContext2D !== 'undefined') {
- parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
- parentBranch.centerOfMass.x *= totalMassInv;
+ /**
+ * Draw a circle shape
+ */
+ CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
+ this.beginPath();
+ this.arc(x, y, r, 0, 2*Math.PI, false);
+ };
- parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
- parentBranch.centerOfMass.y *= totalMassInv;
+ /**
+ * 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);
+ };
- parentBranch.mass = totalMass;
- var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
- parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
+ /**
+ * 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
+ 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();
+ };
- /**
- * determine in which branch the node will be placed.
- *
- * @param parentBranch
- * @param node
- * @param skipMassUpdate
- * @private
- */
- exports._placeInTree = function(parentBranch,node,skipMassUpdate) {
- if (skipMassUpdate != true || skipMassUpdate === undefined) {
- // update the mass of the branch.
- this._updateBranchMass(parentBranch,node);
- }
+ /**
+ * 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();
- 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");
- }
- else { // in SW
- this._placeInRegion(parentBranch,node,"SW");
- }
- }
- else { // in NE or SE
- if (parentBranch.children.NW.range.maxY > node.y) { // in NE
- this._placeInRegion(parentBranch,node,"NE");
- }
- else { // in SE
- this._placeInRegion(parentBranch,node,"SE");
+ 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();
+ };
+
+ /**
+ * 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();
+ };
- /**
- * 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();
- }
- else {
- this._splitBranch(parentBranch.children[region]);
- this._placeInTree(parentBranch.children[region],node);
- }
- break;
- case 4: // place in branch
- this._placeInTree(parentBranch.children[region],node);
- break;
- }
- };
+ /**
+ * 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 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
- */
- 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;
- }
- parentBranch.childrenCount = 4;
- parentBranch.children.data = null;
- this._insertRegion(parentBranch,"NW");
- this._insertRegion(parentBranch,"NE");
- this._insertRegion(parentBranch,"SW");
- this._insertRegion(parentBranch,"SE");
+ 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);
+ };
- if (containedNode != null) {
- this._placeInTree(parentBranch,containedNode);
- }
- };
- /**
- * 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;
- }
+ /**
+ * 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
- 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.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 function is for debugging purposed, it draws the tree.
- *
- * @param ctx
- * @param color
- * @private
- */
- exports._drawTree = function(ctx,color) {
- if (this.barnesHutTree !== undefined) {
+ this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
+ this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
- ctx.lineWidth = 1;
+ this.lineTo(xe, ymb);
- this._drawBranch(this.barnesHutTree.root,ctx,color);
- }
- };
+ this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
+ this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
+ this.lineTo(x, ym);
+ };
- /**
- * 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 (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();
+ /**
+ * 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);
- ctx.beginPath();
- ctx.moveTo(branch.range.maxX,branch.range.minY);
- ctx.lineTo(branch.range.maxX,branch.range.maxY);
- ctx.stroke();
+ // 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);
- ctx.beginPath();
- ctx.moveTo(branch.range.maxX,branch.range.maxY);
- ctx.lineTo(branch.range.minX,branch.range.maxY);
- ctx.stroke();
+ // 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);
- ctx.beginPath();
- ctx.moveTo(branch.range.minX,branch.range.maxY);
- ctx.lineTo(branch.range.minX,branch.range.minY);
- ctx.stroke();
+ // 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);
- /*
- if (branch.mass > 0) {
- ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
- ctx.stroke();
- }
+ 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
+ }
/***/ },
/* 64 */
/***/ function(module, exports, __webpack_require__) {
- module.exports = function(module) {
- if(!module.webpackPolyfill) {
- module.deprecate = function() {};
- module.paths = [];
- // module.parent = undefined by default
- module.children = [];
- module.webpackPolyfill = 1;
- }
- return module;
- }
+ // 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'];
/***/ }
diff --git a/docs/graph2d.html b/docs/graph2d.html
index b479b993..95d6bfd5 100644
--- a/docs/graph2d.html
+++ b/docs/graph2d.html
@@ -280,7 +280,7 @@ The options colored in green can also be used as options for the groups. All opt
' +
- 'Options: ' +
- ' ' +
- '' +
- ' ' +
- ' ' +
- ' yAxisOrientation
String
'left'
- This defines with which axis, left or right, the graph is coupled. Example 5 shows groups with different Y axis. If no groups are coupled
+ This defines with which axis, left or right, the graph is coupled. Example 5 shows groups with different Y axis. If no groups are coupled
with an axis, it will not be shown.
@@ -373,7 +373,7 @@ The options colored in green can also be used as options for the groups. All opt
catmullRom.parametrization
String
'centripetal'
- Define the type of parametrizaion. Example 7 shows the different methods. The options are 'centripetal' (best results), 'chordal' and 'uniform'. Uniform is the computationally cheapest variant.
+ Define the type of parametrizaion. Example 7 shows the different methods. The options are 'centripetal' (best results), 'chordal' and 'uniform'. Uniform is the computationally cheapest variant.
If catmullRom is disabled, linear interpolation is used.
@@ -435,7 +435,7 @@ The options colored in green can also be used as options for the groups. All opt
@@ -977,8 +977,8 @@ Graph2d comes with support for the following locales:
Object
You can use this to toggle the visibility of groups per graph2D instance. This is different from setting the visibility flag of the groups since
- this is not communicated across instances of graph2d. Take a look at Example 14
+ this is not communicated across instances of graph2d. Take a look at Example 14
for more explaination.
Additionally, Graph2d has 10 preset styles for graphs, which are cycled through when loading groups. These styles can be overwritten - as well, along with defining your own classes to style the graphs! Example 4 and - example 5 show the usage of custom styles. + as well, along with defining your own classes to style the graphs! Example 4 and + example 5 show the usage of custom styles.
The network can be navigated using shortcut keys. - The default state for the keyboard navigation is off. The predefined keys can be found in the example 20_navigation.html. + The default state for the keyboard navigation is off. The predefined keys can be found in the example 20_navigation.html.
@@ -2092,7 +2092,7 @@ var options: {The network can be used to display nodes in a hierarchical way. This can be determined automatically, based on the amount of edges connected to each node, or defined by the user. If the user wants to manually determine the hierarchy, each node has to be supplied with a level (from 0 being heighest to n). The automatic method - is shown in example 23 and the user-defined method is shown in example 24. + is shown in example 23 and the user-defined method is shown in example 24. This layout method does not support smooth curves or clustering. It automatically turns these features off.
diff --git a/examples/network/01_basic_usage.html b/examples/network/01_basic_usage.html index e03e75fe..0e01a341 100644 --- a/examples/network/01_basic_usage.html +++ b/examples/network/01_basic_usage.html @@ -22,20 +22,17 @@