diff --git a/HISTORY.md b/HISTORY.md
index 40214d13..c3e656ae 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -15,6 +15,8 @@ http://visjs.org
on screen.
- Implemented an option `focus` for `setSelection(ids, options)`, to immediately
focus selected nodes.
+- Implemented animated range change for functions `fit`, `focus`, `setSelection`,
+ and `setWindow`.
### Network
diff --git a/docs/timeline.html b/docs/timeline.html
index aa0b89f3..93fc27fa 100644
--- a/docs/timeline.html
+++ b/docs/timeline.html
@@ -737,16 +737,23 @@ timeline.clear({options: true}); // clear options only
- fit() |
+ fit([options]) |
none |
Adjust the visible window such that it fits all items. See also function focus(id) .
+ Available options:
+
+ animate: boolean | number If true (default), the range is animated smoothly to the new window. If a number, the number is taken as duration for the animation. Default duration is 500 ms.
+
|
- focus(id | ids) |
+ focus(id | ids [, options]) |
none |
- Adjust the visible window such that the selected item (or multiple items) are centered on screen. See also function fit() .
+ | Adjust the visible window such that the selected item (or multiple items) are centered on screen. See also function fit() . Available options:
+
+ animate: boolean | number If true (default), the range is animated smoothly to the new window. If a number, the number is taken as duration for the animation. Default duration is 500 ms.
+
|
@@ -829,19 +836,24 @@ timeline.clear({options: true}); // clear options only
- setSelection([ids [, options]]) |
+ setSelection(id | ids [, options]) |
none |
Select one or multiple items by their id. The currently selected items will be unselected. To unselect all selected items, call `setSelection([])`. Available options:
- focus: boolean . If true, focus will be set to the selected item(s)
+ focus: boolean If true, focus will be set to the selected item(s)
+ animate: boolean | number If true (default), the range is animated smoothly to the new window. If a number, the number is taken as duration for the animation. Default duration is 500 ms. Only applicable when option focus is true.
|
- setWindow(start, end) |
+ setWindow(start, end [, options]) |
none |
- Set the current visible window. The parameters start and end can be a Date , Number , or String . If the parameter value of start or end is null, the parameter will be left unchanged. |
+ Set the current visible window. The parameters start and end can be a Date , Number , or String . If the parameter value of start or end is null, the parameter will be left unchanged. Available options:
+
+ animate: boolean | number If true (default), the range is animated smoothly to the new window. If a number, the number is taken as duration for the animation. Default duration is 500 ms.
+
+ |
diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js
index 7c33be50..2db38778 100644
--- a/lib/timeline/Core.js
+++ b/lib/timeline/Core.js
@@ -325,8 +325,14 @@ Core.prototype.clear = function(what) {
/**
* Set Core window such that it fits all items
+ * @param {Object} [options] Available options:
+ * `animate: boolean | number`
+ * If true (default), the range is animated
+ * smoothly to the new window.
+ * If a number, the number is taken as duration
+ * for the animation. Default duration is 500 ms.
*/
-Core.prototype.fit = function() {
+Core.prototype.fit = function(options) {
// apply the data range as range
var dataRange = this.getItemRange();
@@ -348,7 +354,8 @@ Core.prototype.fit = function() {
return;
}
- this.range.setRange(start, end);
+ var animate = (options && options.animate !== undefined) ? options.animate : true;
+ this.range.setRange(start, end, animate);
};
@@ -363,15 +370,22 @@ Core.prototype.fit = function() {
* 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
+ * @param {Date | Number | String} [end] End date of visible window
+ * @param {Object} [options] Available options:
+ * `animate: boolean | number`
+ * If true (default), the range is animated
+ * smoothly to the new window.
+ * If a number, the number is taken as duration
+ * for the animation. Default duration is 500 ms.
*/
-Core.prototype.setWindow = function(start, end) {
+Core.prototype.setWindow = function(start, end, options) {
+ var animate = (options && options.animate !== undefined) ? options.animate : true;
if (arguments.length == 1) {
var range = arguments[0];
- this.range.setRange(range.start, range.end);
+ this.range.setRange(range.start, range.end, animate);
}
else {
- this.range.setRange(start, end);
+ this.range.setRange(start, end, animate);
}
};
diff --git a/lib/timeline/Range.js b/lib/timeline/Range.js
index 20e9abff..98349baa 100644
--- a/lib/timeline/Range.js
+++ b/lib/timeline/Range.js
@@ -35,6 +35,7 @@ function Range(body, options) {
this.props = {
touch: {}
};
+ this.animateTimer = null;
// drag listeners for dragging
this.body.emitter.on('dragstart', this._onDragStart.bind(this));
@@ -76,7 +77,7 @@ Range.prototype = new Component();
Range.prototype.setOptions = function (options) {
if (options) {
// copy the options that we know
- var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable'];
+ var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', 'activate'];
util.selectiveExtend(fields, this.options, options);
if ('start' in options || 'end' in options) {
@@ -101,16 +102,69 @@ function validateDirection (direction) {
* Set a new start and end range
* @param {Number} [start]
* @param {Number} [end]
+ * @param {boolean | number} [animate=false] If true, the range is animated
+ * smoothly to the new window.
+ * If animate is a number, the
+ * number is taken as duration
+ * Default duration is 500 ms.
+ *
*/
-Range.prototype.setRange = function(start, end) {
- var changed = this._applyRange(start, end);
- if (changed) {
- var params = {
- start: new Date(this.start),
- end: new Date(this.end)
- };
- this.body.emitter.emit('rangechange', params);
- this.body.emitter.emit('rangechanged', params);
+Range.prototype.setRange = function(start, end, animate) {
+ this._cancelAnimation();
+
+ if (animate) {
+ var me = this;
+ var initStart = this.start;
+ var initEnd = this.end;
+ var duration = typeof animate === 'number' ? animate : 500;
+ var initTime = new Date().valueOf();
+ var anyChanged = false;
+ function next() {
+ if (!me.props.touch.dragging) {
+ var now = new Date().valueOf();
+ var time = now - initTime;
+ var s = util.easeInOutQuad(time, initStart, start, duration);
+ var e = util.easeInOutQuad(time, initEnd, end, duration);
+ changed = me._applyRange(s, e);
+ anyChanged = anyChanged || changed;
+ if (changed) {
+ me.body.emitter.emit('rangechange', {start: new Date(s), end: new Date(e)});
+ }
+
+ if (time <= duration) {
+ // animate with as high as possible frame rate, leave 20 ms in between
+ // each to prevent the browser from blocking
+ me.animateTimer = setTimeout(next, 20);
+ }
+ else {
+ // done
+ if (anyChanged) {
+ me.body.emitter.emit('rangechanged', {start: new Date(me.start), end: new Date(me.end)});
+ }
+ }
+ }
+ }
+
+ return next();
+ }
+ else {
+ var changed = this._applyRange(start, end);
+ if (changed) {
+ var params = {start: new Date(this.start), end: new Date(this.end)};
+ this.body.emitter.emit('rangechange', params);
+ this.body.emitter.emit('rangechanged', params);
+ }
+ }
+};
+
+/**
+ * Stop an animation
+ * @private
+ */
+Range.prototype._cancelAnimation = function () {
+ if (this.animateTimer) {
+ clearTimeout(this.animateTimer);
+ this.animateTimer = null;
}
};
@@ -284,6 +338,7 @@ Range.prototype._onDragStart = function(event) {
this.props.touch.start = this.start;
this.props.touch.end = this.end;
+ this.props.touch.dragging = true;
if (this.body.dom.root) {
this.body.dom.root.style.cursor = 'move';
@@ -327,6 +382,7 @@ Range.prototype._onDragEnd = function (event) {
// when releasing the fingers in opposite order from the touch screen
if (!this.props.touch.allowDragging) return;
+ this.props.touch.dragging = false;
if (this.body.dom.root) {
this.body.dom.root.style.cursor = 'auto';
}
diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js
index adb93774..a95c6f90 100644
--- a/lib/timeline/Timeline.js
+++ b/lib/timeline/Timeline.js
@@ -139,7 +139,7 @@ Timeline.prototype.setItems = function(items) {
var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
- this.setWindow(start, end);
+ this.setWindow(start, end, false);
}
};
@@ -172,14 +172,20 @@ Timeline.prototype.setGroups = function(groups) {
* selected. If ids is an empty array, all items will be
* unselected.
* @param {Object} [options] Available options:
- * `focus: boolean` If true, focus will be set
- * to the selected item(s)
+ * `focus: boolean`
+ * If true, focus will be set to the selected item(s)
+ * `animate: boolean | number`
+ * If true (default), the range is animated
+ * smoothly to the new window.
+ * If a number, the number is taken as duration
+ * for the animation. Default duration is 500 ms.
+ * Only applicable when option focus is true.
*/
Timeline.prototype.setSelection = function(ids, options) {
this.itemSet && this.itemSet.setSelection(ids);
if (options && options.focus) {
- this.focus(ids);
+ this.focus(ids, options);
}
};
@@ -195,8 +201,15 @@ Timeline.prototype.getSelection = function() {
* Adjust the visible window such that the selected item (or multiple items)
* are centered on screen.
* @param {String | String[]} id An item id or array with item ids
+ * @param {Object} [options] Available options:
+ * `animate: boolean | number`
+ * If true (default), the range is animated
+ * smoothly to the new window.
+ * If a number, the number is taken as duration
+ * for the animation. Default duration is 500 ms.
+ * Only applicable when option focus is true
*/
-Timeline.prototype.focus = function(id) {
+Timeline.prototype.focus = function(id, options) {
if (!this.itemsData || id == undefined) return;
var ids = Array.isArray(id) ? id : [id];
@@ -229,7 +242,8 @@ Timeline.prototype.focus = function(id) {
var middle = (start + end) / 2;
var interval = Math.max((this.range.end - this.range.start), (end - start) * 1.1);
- this.range.setRange(middle - interval / 2, middle + interval / 2);
+ var animate = (options && options.animate !== undefined) ? options.animate : true;
+ this.range.setRange(middle - interval / 2, middle + interval / 2, animate);
};
/**
diff --git a/lib/util.js b/lib/util.js
index f2a5fd4c..03fcbe2c 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -1243,4 +1243,21 @@ exports.binarySearchGeneric = function(orderedItems, target, field, sidePreferen
}
}
return guess;
+};
+
+/**
+ * Quadratic ease-in-out
+ * http://gizma.com/easing/
+ * @param {number} t Current time
+ * @param {number} start Start value
+ * @param {number} end End value
+ * @param {number} duration Duration
+ * @returns {number} Value corresponding with current time
+ */
+exports.easeInOutQuad = function (t, start, end, duration) {
+ var change = end - start;
+ t /= duration/2;
+ if (t < 1) return change/2*t*t + start;
+ t--;
+ return -change/2 * (t*(t-2) - 1) + start;
};
\ No newline at end of file