vendor/assets/vis/timeline/Timeline.js in vis-rails-0.0.6 vs vendor/assets/vis/timeline/Timeline.js in vis-rails-1.0.0
- old
+ new
@@ -4,16 +4,28 @@
* @param {vis.DataSet | Array | google.visualization.DataTable} [items]
* @param {Object} [options] See Timeline.setOptions for the available options.
* @constructor
*/
function Timeline (container, items, options) {
+ // validate arguments
+ if (!container) throw new Error('No container element provided');
+
var me = this;
var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
this.options = {
orientation: 'bottom',
+ direction: 'horizontal', // 'horizontal' or 'vertical'
autoResize: true,
- editable: false,
+ stack: true,
+
+ editable: {
+ updateTime: false,
+ updateGroup: false,
+ add: false,
+ remove: false
+ },
+
selectable: true,
snap: null, // will be specified after timeaxis is created
min: null,
max: null,
@@ -25,10 +37,18 @@
showMinorLabels: true,
showMajorLabels: true,
showCurrentTime: false,
showCustomTime: false,
+ type: 'box',
+ align: 'center',
+ margin: {
+ axis: 20,
+ item: 10
+ },
+ padding: 5,
+
onAdd: function (item, callback) {
callback(item);
},
onUpdate: function (item, callback) {
callback(item);
@@ -36,157 +56,245 @@
onMove: function (item, callback) {
callback(item);
},
onRemove: function (item, callback) {
callback(item);
- }
+ },
+
+ toScreen: me._toScreen.bind(me),
+ toTime: me._toTime.bind(me)
};
- // controller
- this.controller = new Controller();
-
// root panel
- if (!container) {
- throw new Error('No container element provided');
- }
- var rootOptions = Object.create(this.options);
- rootOptions.height = function () {
- // TODO: change to height
- if (me.options.height) {
- // fixed height
- return me.options.height;
+ var rootOptions = util.extend(Object.create(this.options), {
+ height: function () {
+ if (me.options.height) {
+ // fixed height
+ return me.options.height;
+ }
+ else {
+ // auto height
+ // TODO: implement a css based solution to automatically have the right hight
+ return (me.timeAxis.height + me.contentPanel.height) + 'px';
+ }
}
- else {
- // auto height
- return (me.timeaxis.height + me.content.height) + 'px';
- }
- };
+ });
this.rootPanel = new RootPanel(container, rootOptions);
- this.controller.add(this.rootPanel);
// single select (or unselect) when tapping an item
- this.controller.on('tap', this._onSelectItem.bind(this));
+ this.rootPanel.on('tap', this._onSelectItem.bind(this));
// multi select when holding mouse/touch, or on ctrl+click
- this.controller.on('hold', this._onMultiSelectItem.bind(this));
+ this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
// add item on doubletap
- this.controller.on('doubletap', this._onAddItem.bind(this));
+ this.rootPanel.on('doubletap', this._onAddItem.bind(this));
- // item panel
- var itemOptions = Object.create(this.options);
- itemOptions.left = function () {
- return me.labelPanel.width;
- };
- itemOptions.width = function () {
- return me.rootPanel.width - me.labelPanel.width;
- };
- itemOptions.top = null;
- itemOptions.height = null;
- this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
- this.controller.add(this.itemPanel);
-
- // label panel
- var labelOptions = Object.create(this.options);
- labelOptions.top = null;
- labelOptions.left = null;
- labelOptions.height = null;
- labelOptions.width = function () {
- if (me.content && typeof me.content.getLabelsWidth === 'function') {
- return me.content.getLabelsWidth();
+ // side panel
+ var sideOptions = util.extend(Object.create(this.options), {
+ top: function () {
+ return (sideOptions.orientation == 'top') ? '0' : '';
+ },
+ bottom: function () {
+ return (sideOptions.orientation == 'top') ? '' : '0';
+ },
+ left: '0',
+ right: null,
+ height: '100%',
+ width: function () {
+ if (me.itemSet) {
+ return me.itemSet.getLabelsWidth();
+ }
+ else {
+ return 0;
+ }
+ },
+ className: function () {
+ return 'side' + (me.groupsData ? '' : ' hidden');
}
- else {
- return 0;
- }
- };
- this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
- this.controller.add(this.labelPanel);
+ });
+ this.sidePanel = new Panel(sideOptions);
+ this.rootPanel.appendChild(this.sidePanel);
+ // main panel (contains time axis and itemsets)
+ var mainOptions = util.extend(Object.create(this.options), {
+ left: function () {
+ // we align left to enable a smooth resizing of the window
+ return me.sidePanel.width;
+ },
+ right: null,
+ height: '100%',
+ width: function () {
+ return me.rootPanel.width - me.sidePanel.width;
+ },
+ className: 'main'
+ });
+ this.mainPanel = new Panel(mainOptions);
+ this.rootPanel.appendChild(this.mainPanel);
+
// range
+ // TODO: move range inside rootPanel?
var rangeOptions = Object.create(this.options);
- this.range = new Range(rangeOptions);
+ this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
this.range.setRange(
now.clone().add('days', -3).valueOf(),
now.clone().add('days', 4).valueOf()
);
-
- this.range.subscribe(this.controller, this.rootPanel, 'move', 'horizontal');
- this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal');
this.range.on('rangechange', function (properties) {
- var force = true;
- me.controller.emit('rangechange', properties);
- me.controller.emit('request-reflow', force);
+ me.rootPanel.repaint();
+ me.emit('rangechange', properties);
});
this.range.on('rangechanged', function (properties) {
- var force = true;
- me.controller.emit('rangechanged', properties);
- me.controller.emit('request-reflow', force);
+ me.rootPanel.repaint();
+ me.emit('rangechanged', properties);
});
- // time axis
- var timeaxisOptions = Object.create(rootOptions);
- timeaxisOptions.range = this.range;
- timeaxisOptions.left = null;
- timeaxisOptions.top = null;
- timeaxisOptions.width = '100%';
- timeaxisOptions.height = null;
- this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
- this.timeaxis.setRange(this.range);
- this.controller.add(this.timeaxis);
- this.options.snap = this.timeaxis.snap.bind(this.timeaxis);
+ // panel with time axis
+ var timeAxisOptions = util.extend(Object.create(rootOptions), {
+ range: this.range,
+ left: null,
+ top: null,
+ width: null,
+ height: null
+ });
+ this.timeAxis = new TimeAxis(timeAxisOptions);
+ this.timeAxis.setRange(this.range);
+ this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
+ this.mainPanel.appendChild(this.timeAxis);
+ // content panel (contains itemset(s))
+ var contentOptions = util.extend(Object.create(this.options), {
+ top: function () {
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
+ },
+ bottom: function () {
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
+ },
+ left: null,
+ right: null,
+ height: null,
+ width: null,
+ className: 'content'
+ });
+ this.contentPanel = new Panel(contentOptions);
+ this.mainPanel.appendChild(this.contentPanel);
+
+ // content panel (contains the vertical lines of box items)
+ var backgroundOptions = util.extend(Object.create(this.options), {
+ top: function () {
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
+ },
+ bottom: function () {
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
+ },
+ left: null,
+ right: null,
+ height: function () {
+ return me.contentPanel.height;
+ },
+ width: null,
+ className: 'background'
+ });
+ this.backgroundPanel = new Panel(backgroundOptions);
+ this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel);
+
+ // panel with axis holding the dots of item boxes
+ var axisPanelOptions = util.extend(Object.create(rootOptions), {
+ left: 0,
+ top: function () {
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
+ },
+ bottom: function () {
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
+ },
+ width: '100%',
+ height: 0,
+ className: 'axis'
+ });
+ this.axisPanel = new Panel(axisPanelOptions);
+ this.mainPanel.appendChild(this.axisPanel);
+
+ // content panel (contains itemset(s))
+ var sideContentOptions = util.extend(Object.create(this.options), {
+ top: function () {
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
+ },
+ bottom: function () {
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
+ },
+ left: null,
+ right: null,
+ height: null,
+ width: null,
+ className: 'side-content'
+ });
+ this.sideContentPanel = new Panel(sideContentOptions);
+ this.sidePanel.appendChild(this.sideContentPanel);
+
// current time bar
- this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
- this.controller.add(this.currenttime);
+ // Note: time bar will be attached in this.setOptions when selected
+ this.currentTime = new CurrentTime(this.range, rootOptions);
// custom time bar
- this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
- this.controller.add(this.customtime);
+ // Note: time bar will be attached in this.setOptions when selected
+ this.customTime = new CustomTime(rootOptions);
+ this.customTime.on('timechange', function (time) {
+ me.emit('timechange', time);
+ });
+ this.customTime.on('timechanged', function (time) {
+ me.emit('timechanged', time);
+ });
- // create groupset
- this.setGroups(null);
+ // itemset containing items and groups
+ var itemOptions = util.extend(Object.create(this.options), {
+ left: null,
+ right: null,
+ top: null,
+ bottom: null,
+ width: null,
+ height: null
+ });
+ this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, this.sideContentPanel, itemOptions);
+ this.itemSet.setRange(this.range);
+ this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
+ this.contentPanel.appendChild(this.itemSet);
this.itemsData = null; // DataSet
this.groupsData = null; // DataSet
// apply options
if (options) {
this.setOptions(options);
}
- // create itemset and groupset
+ // create itemset
if (items) {
this.setItems(items);
}
}
-/**
- * Add an event listener to the timeline
- * @param {String} event Available events: select, rangechange, rangechanged,
- * timechange, timechanged
- * @param {function} callback
- */
-Timeline.prototype.on = function on (event, callback) {
- this.controller.on(event, callback);
-};
+// turn Timeline into an event emitter
+Emitter(Timeline.prototype);
/**
- * Add an event listener from the timeline
- * @param {String} event
- * @param {function} callback
- */
-Timeline.prototype.off = function off (event, callback) {
- this.controller.off(event, callback);
-};
-
-/**
* Set options
* @param {Object} options TODO: describe the available options
*/
Timeline.prototype.setOptions = function (options) {
util.extend(this.options, options);
+ if ('editable' in options) {
+ var isBoolean = typeof options.editable === 'boolean';
+
+ this.options.editable = {
+ updateTime: isBoolean ? options.editable : (options.editable.updateTime || false),
+ updateGroup: isBoolean ? options.editable : (options.editable.updateGroup || false),
+ add: isBoolean ? options.editable : (options.editable.add || false),
+ remove: isBoolean ? options.editable : (options.editable.remove || false)
+ };
+ }
+
// force update of range (apply new min/max etc.)
// both start and end are optional
this.range.setRange(options.start, options.end);
if ('editable' in options || 'selectable' in options) {
@@ -198,44 +306,78 @@
// remove selection
this.setSelection([]);
}
}
+ // force the itemSet to refresh: options like orientation and margins may be changed
+ this.itemSet.markDirty();
+
// validate the callback functions
var validateCallback = (function (fn) {
if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)');
}
}).bind(this);
['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
- this.controller.reflow();
- this.controller.repaint();
+ // add/remove the current time bar
+ if (this.options.showCurrentTime) {
+ if (!this.mainPanel.hasChild(this.currentTime)) {
+ this.mainPanel.appendChild(this.currentTime);
+ this.currentTime.start();
+ }
+ }
+ else {
+ if (this.mainPanel.hasChild(this.currentTime)) {
+ this.currentTime.stop();
+ this.mainPanel.removeChild(this.currentTime);
+ }
+ }
+
+ // add/remove the custom time bar
+ if (this.options.showCustomTime) {
+ if (!this.mainPanel.hasChild(this.customTime)) {
+ this.mainPanel.appendChild(this.customTime);
+ }
+ }
+ else {
+ if (this.mainPanel.hasChild(this.customTime)) {
+ this.mainPanel.removeChild(this.customTime);
+ }
+ }
+
+ // 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.');
+ }
+
+ // repaint everything
+ this.rootPanel.repaint();
};
/**
* Set a custom time bar
* @param {Date} time
*/
Timeline.prototype.setCustomTime = function (time) {
- if (!this.customtime) {
+ if (!this.customTime) {
throw new Error('Cannot get custom time: Custom time bar is not enabled');
}
- this.customtime.setCustomTime(time);
+ this.customTime.setCustomTime(time);
};
/**
* Retrieve the current custom time.
* @return {Date} customTime
*/
Timeline.prototype.getCustomTime = function() {
- if (!this.customtime) {
+ if (!this.customTime) {
throw new Error('Cannot get custom time: Custom time bar is not enabled');
}
- return this.customtime.getCustomTime();
+ return this.customTime.getCustomTime();
};
/**
* Set items
* @param {vis.DataSet | Array | google.visualization.DataTable | null} items
@@ -246,134 +388,85 @@
// convert to type DataSet when needed
var newDataSet;
if (!items) {
newDataSet = null;
}
- else if (items instanceof DataSet) {
+ else if (items instanceof DataSet || items instanceof DataView) {
newDataSet = items;
}
- if (!(items instanceof DataSet)) {
- newDataSet = new DataSet({
+ else {
+ // turn an array into a dataset
+ newDataSet = new DataSet(items, {
convert: {
start: 'Date',
end: 'Date'
}
});
- newDataSet.add(items);
}
// set items
this.itemsData = newDataSet;
- this.content.setItems(newDataSet);
+ this.itemSet.setItems(newDataSet);
if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
- // apply the data range as range
- var dataRange = this.getItemRange();
+ this.fit();
- // 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
- }
- start = new Date(start.valueOf() - interval * 0.05);
- end = new Date(end.valueOf() + interval * 0.05);
- }
+ var start = (this.options.start != undefined) ? util.convert(this.options.start, 'Date') : null;
+ var end = (this.options.end != undefined) ? util.convert(this.options.end, 'Date') : null;
- // override specified start and/or end date
- if (this.options.start != undefined) {
- start = util.convert(this.options.start, 'Date');
- }
- if (this.options.end != undefined) {
- end = util.convert(this.options.end, 'Date');
- }
-
- // apply range if there is a min or max available
- if (start != null || end != null) {
- this.range.setRange(start, end);
- }
+ this.setWindow(start, end);
}
};
/**
* Set groups
* @param {vis.DataSet | Array | google.visualization.DataTable} groups
*/
-Timeline.prototype.setGroups = function(groups) {
- var me = this;
- this.groupsData = groups;
+Timeline.prototype.setGroups = function setGroups(groups) {
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!groups) {
+ newDataSet = null;
+ }
+ else if (groups instanceof DataSet || groups instanceof DataView) {
+ newDataSet = groups;
+ }
+ else {
+ // turn an array into a dataset
+ newDataSet = new DataSet(groups);
+ }
- // switch content type between ItemSet or GroupSet when needed
- var Type = this.groupsData ? GroupSet : ItemSet;
- if (!(this.content instanceof Type)) {
- // remove old content set
- if (this.content) {
- this.content.hide();
- if (this.content.setItems) {
- this.content.setItems(); // disconnect from items
- }
- if (this.content.setGroups) {
- this.content.setGroups(); // disconnect from groups
- }
- this.controller.remove(this.content);
- }
+ this.groupsData = newDataSet;
+ this.itemSet.setGroups(newDataSet);
+};
- // create new content set
- var options = Object.create(this.options);
- util.extend(options, {
- top: function () {
- if (me.options.orientation == 'top') {
- return me.timeaxis.height;
- }
- else {
- return me.itemPanel.height - me.timeaxis.height - me.content.height;
- }
- },
- left: null,
- width: '100%',
- height: function () {
- if (me.options.height) {
- // fixed height
- return me.itemPanel.height - me.timeaxis.height;
- }
- else {
- // auto height
- return null;
- }
- },
- maxHeight: function () {
- // TODO: change maxHeight to be a css string like '100%' or '300px'
- if (me.options.maxHeight) {
- if (!util.isNumber(me.options.maxHeight)) {
- throw new TypeError('Number expected for property maxHeight');
- }
- return me.options.maxHeight - me.timeaxis.height;
- }
- else {
- return null;
- }
- },
- labelContainer: function () {
- return me.labelPanel.getContainer();
- }
- });
+/**
+ * Set Timeline window such that it fits all items
+ */
+Timeline.prototype.fit = function fit() {
+ // apply the data range as range
+ var dataRange = this.getItemRange();
- this.content = new Type(this.itemPanel, [this.timeaxis], options);
- if (this.content.setRange) {
- this.content.setRange(this.range);
+ // 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 (this.content.setItems) {
- this.content.setItems(this.itemsData);
- }
- if (this.content.setGroups) {
- this.content.setGroups(this.groupsData);
- }
- this.controller.add(this.content);
+ 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.range.setRange(start, end);
};
/**
* Get the data range of the item set.
* @returns {{min: Date, max: Date}} range A range with a start and end Date.
@@ -419,29 +512,42 @@
* @param {Array} [ids] An array with zero or more id's of the items to be
* selected. If ids is an empty array, all items will be
* unselected.
*/
Timeline.prototype.setSelection = function setSelection (ids) {
- if (this.content) this.content.setSelection(ids);
+ this.itemSet.setSelection(ids);
};
/**
* Get the selected items by their id
* @return {Array} ids The ids of the selected items
*/
Timeline.prototype.getSelection = function getSelection() {
- return this.content ? this.content.getSelection() : [];
+ return this.itemSet.getSelection();
};
/**
* Set the visible window. Both parameters are optional, you can change only
- * start or only end.
+ * 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} [start] Start date of visible window
* @param {Date | Number | String} [end] End date of visible window
*/
Timeline.prototype.setWindow = function setWindow(start, end) {
- this.range.setRange(start, end);
+ if (arguments.length == 1) {
+ var range = arguments[0];
+ this.range.setRange(range.start, range.end);
+ }
+ else {
+ this.range.setRange(start, end);
+ }
};
/**
* Get the visible window
* @return {{start: Date, end: Date}} Visible range
@@ -468,30 +574,36 @@
if (ctrlKey || shiftKey) {
this._onMultiSelectItem(event);
return;
}
- var item = ItemSet.itemFromTarget(event);
+ var oldSelection = this.getSelection();
+ var item = ItemSet.itemFromTarget(event);
var selection = item ? [item.id] : [];
this.setSelection(selection);
- this.controller.emit('select', {
- items: this.getSelection()
- });
+ var newSelection = this.getSelection();
+ // if selection is changed, emit a select event
+ if (!util.equalArray(oldSelection, newSelection)) {
+ this.emit('select', {
+ items: this.getSelection()
+ });
+ }
+
event.stopPropagation();
};
/**
* Handle creation and updates of an item on double tap
* @param event
* @private
*/
Timeline.prototype._onAddItem = function (event) {
if (!this.options.selectable) return;
- if (!this.options.editable) return;
+ if (!this.options.editable.add) return;
var me = this,
item = ItemSet.itemFromTarget(event);
if (item) {
@@ -505,38 +617,30 @@
}
});
}
else {
// add item
- var xAbs = vis.util.getAbsoluteLeft(this.rootPanel.frame);
+ var xAbs = vis.util.getAbsoluteLeft(this.contentPanel.frame);
var x = event.gesture.center.pageX - xAbs;
var newItem = {
- start: this.timeaxis.snap(this._toTime(x)),
+ start: this.timeAxis.snap(this._toTime(x)),
content: 'new item'
};
var id = util.randomUUID();
newItem[this.itemsData.fieldId] = id;
- var group = GroupSet.groupFromTarget(event);
+ var group = ItemSet.groupFromTarget(event);
if (group) {
newItem.group = group.groupId;
}
// execute async handler to customize (or cancel) adding an item
this.options.onAdd(newItem, function (item) {
if (item) {
me.itemsData.add(newItem);
-
- // select the created item after it is repainted
- me.controller.once('repaint', function () {
- me.setSelection([id]);
-
- me.controller.emit('select', {
- items: me.getSelection()
- });
- }.bind(me));
+ // TODO: need to trigger a repaint?
}
});
}
};
@@ -564,11 +668,11 @@
// item is already selected -> deselect it
selection.splice(index, 1);
}
this.setSelection(selection);
- this.controller.emit('select', {
+ this.emit('select', {
items: this.getSelection()
});
event.stopPropagation();
}
@@ -579,11 +683,11 @@
* @param {int} x Position on the screen in pixels
* @return {Date} time The datetime the corresponds with given position x
* @private
*/
Timeline.prototype._toTime = function _toTime(x) {
- var conversion = this.range.conversion(this.content.width);
+ var conversion = this.range.conversion(this.mainPanel.width);
return new Date(x / conversion.scale + conversion.offset);
};
/**
* Convert a datetime (Date object) into a position on the screen
@@ -591,8 +695,8 @@
* @return {int} x The position on the screen in pixels which corresponds
* with the given date.
* @private
*/
Timeline.prototype._toScreen = function _toScreen(time) {
- var conversion = this.range.conversion(this.content.width);
+ var conversion = this.range.conversion(this.mainPanel.width);
return (time.valueOf() - conversion.offset) * conversion.scale;
};