Browse Source

Implemented option `snap`

v3_develop
jos 9 years ago
parent
commit
1aea9fed30
11 changed files with 113 additions and 61 deletions
  1. +2
    -0
      HISTORY.md
  2. +11
    -0
      docs/timeline.html
  3. +54
    -0
      examples/timeline/33_custom_snapping.html
  4. +1
    -0
      examples/timeline/index.html
  5. +0
    -12
      lib/timeline/DataStep.js
  6. +1
    -2
      lib/timeline/Graph2d.js
  7. +22
    -18
      lib/timeline/TimeStep.js
  8. +7
    -2
      lib/timeline/Timeline.js
  9. +0
    -10
      lib/timeline/component/DataAxis.js
  10. +15
    -7
      lib/timeline/component/ItemSet.js
  11. +0
    -10
      lib/timeline/component/TimeAxis.js

+ 2
- 0
HISTORY.md View File

@ -30,6 +30,8 @@ http://visjs.org
### Timeline ### Timeline
- `Timeline.redraw()` now also recalculates the size of items. - `Timeline.redraw()` now also recalculates the size of items.
- Implemented option `snap: function` to customize snapping to nice dates
when dragging items.
- Implemented option `timeAxis: {scale: string, step: number}` to set a - Implemented option `timeAxis: {scale: string, step: number}` to set a
fixed scale. fixed scale.
- Fixed width of range items not always being maintained when moving due to - Fixed width of range items not always being maintained when moving due to

+ 11
- 0
docs/timeline.html View File

@ -742,6 +742,7 @@ var options = {
<code>showMinorLabels</code> are false, no horizontal axis will be <code>showMinorLabels</code> are false, no horizontal axis will be
visible.</td> visible.</td>
</tr> </tr>
<tr> <tr>
<td>stack</td> <td>stack</td>
<td>Boolean</td> <td>Boolean</td>
@ -749,6 +750,16 @@ var options = {
<td>If true (default), items will be stacked on top of each other such that they do not overlap.</td> <td>If true (default), items will be stacked on top of each other such that they do not overlap.</td>
</tr> </tr>
<tr>
<td>snap</td>
<td>function | null</td>
<td>function</td>
<td>When moving items on the Timeline, they will be snapped to nice dates like full hours or days, depending on the current scale. The <code>snap</code> function can be replaced with a custom function, or can be set to <code>null</code> to disable snapping. The signature of the snap function is:
<pre class="prettyprint lang-js">function snap(date: Date, scale: string, step: number) : Date | number</pre>
</td>
</tr>
<tr> <tr>
<td>start</td> <td>start</td>
<td>Date | Number | String</td> <td>Date | Number | String</td>

+ 54
- 0
examples/timeline/33_custom_snapping.html View File

@ -0,0 +1,54 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Custom snapping</title>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<p>
When moving the items in on the Timeline below, they will snap to full hours,
independent of being zoomed in or out.
</p>
<div id="visualization"></div>
<script type="text/javascript">
// DOM element where the Timeline will be attached
var container = document.getElementById('visualization');
// Create a DataSet (allows two way data-binding)
var items = new vis.DataSet([
{id: 1, content: 'A', start: '2015-02-09T04:00:00'},
{id: 2, content: 'B', start: '2015-02-09T14:00:00'},
{id: 3, content: 'C', start: '2015-02-09T16:00:00'},
{id: 4, content: 'D', start: '2015-02-09T17:00:00'},
{id: 5, content: 'E', start: '2015-02-10T03:00:00'}
]);
// Configuration for the Timeline
var options = {
editable: true,
// always snap to full hours, independent of the scale
snap: function (date, scale, step) {
var hour = 60 * 60 * 1000;
return Math.round(date / hour) * hour;
}
// to configure no snapping at all:
//
// snap: null
//
// or let the snap function return the date unchanged:
//
// snap: function (date, scale, step) {
// return date;
// }
};
// Create a Timeline
var timeline = new vis.Timeline(container, items, options);
</script>
</body>
</html>

+ 1
- 0
examples/timeline/index.html View File

@ -43,6 +43,7 @@
<p><a href="30_subgroups.html">30_subgroups.html</a></p> <p><a href="30_subgroups.html">30_subgroups.html</a></p>
<p><a href="31_background_areas_with_groups.html">31_background_areas_with_groups.html</a></p> <p><a href="31_background_areas_with_groups.html">31_background_areas_with_groups.html</a></p>
<p><a href="32_grid_styling.html">32_grid_styling.html</a></p> <p><a href="32_grid_styling.html">32_grid_styling.html</a></p>
<p><a href="33_custom_snapping.html">33_custom_snapping.html</a></p>
<p><a href="requirejs/requirejs_example.html">requirejs_example.html</a></p> <p><a href="requirejs/requirejs_example.html">requirejs_example.html</a></p>

+ 0
- 12
lib/timeline/DataStep.js View File

@ -251,18 +251,6 @@ DataStep.prototype.getCurrent = function(decimals) {
return toPrecision; return toPrecision;
}; };
/**
* 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) {
};
/** /**
* Check if the current value is a major value (for example when the step * 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) * is DAY, a major value is each first day of the MONTH)

+ 1
- 2
lib/timeline/Graph2d.js View File

@ -57,7 +57,6 @@ function Graph2d (container, items, groups, options) {
}, },
hiddenDates: [], hiddenDates: [],
util: { util: {
snap: null, // will be specified after TimeAxis is created
toScreen: me._toScreen.bind(me), toScreen: me._toScreen.bind(me),
toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
toTime: me._toTime.bind(me), toTime: me._toTime.bind(me),
@ -73,7 +72,7 @@ function Graph2d (container, items, groups, options) {
// time axis // time axis
this.timeAxis = new TimeAxis(this.body); this.timeAxis = new TimeAxis(this.body);
this.components.push(this.timeAxis); this.components.push(this.timeAxis);
this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
//this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
// current time bar // current time bar
this.currentTime = new CurrentTime(this.body); this.currentTime = new CurrentTime(this.body);

+ 22
- 18
lib/timeline/TimeStep.js View File

@ -321,15 +321,19 @@ TimeStep.prototype.setMinimumStep = function(minimumStep) {
/** /**
* Snap a date to a rounded value. * Snap a date to a rounded value.
* The snap intervals are dependent on the current scale and step. * The snap intervals are dependent on the current scale and step.
* @param {Date} date the date to be snapped.
* Static function
* @param {Date} date the date to be snapped.
* @param {string} scale Current scale, can be 'millisecond', 'second',
* 'minute', 'hour', 'weekday, 'day, 'month, 'year'.
* @param {number} step Current step (1, 2, 4, 5, ...
* @return {Date} snappedDate * @return {Date} snappedDate
*/ */
TimeStep.prototype.snap = function(date) {
TimeStep.snap = function(date, scale, step) {
var clone = new Date(date.valueOf()); var clone = new Date(date.valueOf());
if (this.scale == 'year') {
if (scale == 'year') {
var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
clone.setFullYear(Math.round(year / this.step) * this.step);
clone.setFullYear(Math.round(year / step) * step);
clone.setMonth(0); clone.setMonth(0);
clone.setDate(0); clone.setDate(0);
clone.setHours(0); clone.setHours(0);
@ -337,7 +341,7 @@ TimeStep.prototype.snap = function(date) {
clone.setSeconds(0); clone.setSeconds(0);
clone.setMilliseconds(0); clone.setMilliseconds(0);
} }
else if (this.scale == 'month') {
else if (scale == 'month') {
if (clone.getDate() > 15) { if (clone.getDate() > 15) {
clone.setDate(1); clone.setDate(1);
clone.setMonth(clone.getMonth() + 1); clone.setMonth(clone.getMonth() + 1);
@ -352,9 +356,9 @@ TimeStep.prototype.snap = function(date) {
clone.setSeconds(0); clone.setSeconds(0);
clone.setMilliseconds(0); clone.setMilliseconds(0);
} }
else if (this.scale == 'day') {
else if (scale == 'day') {
//noinspection FallthroughInSwitchStatementJS //noinspection FallthroughInSwitchStatementJS
switch (this.step) {
switch (step) {
case 5: case 5:
case 2: case 2:
clone.setHours(Math.round(clone.getHours() / 24) * 24); break; clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
@ -365,9 +369,9 @@ TimeStep.prototype.snap = function(date) {
clone.setSeconds(0); clone.setSeconds(0);
clone.setMilliseconds(0); clone.setMilliseconds(0);
} }
else if (this.scale == 'weekday') {
else if (scale == 'weekday') {
//noinspection FallthroughInSwitchStatementJS //noinspection FallthroughInSwitchStatementJS
switch (this.step) {
switch (step) {
case 5: case 5:
case 2: case 2:
clone.setHours(Math.round(clone.getHours() / 12) * 12); break; clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
@ -378,8 +382,8 @@ TimeStep.prototype.snap = function(date) {
clone.setSeconds(0); clone.setSeconds(0);
clone.setMilliseconds(0); clone.setMilliseconds(0);
} }
else if (this.scale == 'hour') {
switch (this.step) {
else if (scale == 'hour') {
switch (step) {
case 4: case 4:
clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break; clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
default: default:
@ -387,9 +391,9 @@ TimeStep.prototype.snap = function(date) {
} }
clone.setSeconds(0); clone.setSeconds(0);
clone.setMilliseconds(0); clone.setMilliseconds(0);
} else if (this.scale == 'minute') {
} else if (scale == 'minute') {
//noinspection FallthroughInSwitchStatementJS //noinspection FallthroughInSwitchStatementJS
switch (this.step) {
switch (step) {
case 15: case 15:
case 10: case 10:
clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
@ -402,9 +406,9 @@ TimeStep.prototype.snap = function(date) {
} }
clone.setMilliseconds(0); clone.setMilliseconds(0);
} }
else if (this.scale == 'second') {
else if (scale == 'second') {
//noinspection FallthroughInSwitchStatementJS //noinspection FallthroughInSwitchStatementJS
switch (this.step) {
switch (step) {
case 15: case 15:
case 10: case 10:
clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
@ -416,9 +420,9 @@ TimeStep.prototype.snap = function(date) {
clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break; clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
} }
} }
else if (this.scale == 'millisecond') {
var step = this.step > 5 ? this.step / 2 : 1;
clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
else if (scale == 'millisecond') {
var _step = step > 5 ? step / 2 : 1;
clone.setMilliseconds(Math.round(clone.getMilliseconds() / _step) * _step);
} }
return clone; return clone;

+ 7
- 2
lib/timeline/Timeline.js View File

@ -62,7 +62,13 @@ function Timeline (container, items, groups, options) {
}, },
hiddenDates: [], hiddenDates: [],
util: { util: {
snap: null, // will be specified after TimeAxis is created
getScale: function () {
return me.timeAxis.step.scale;
},
getStep: function () {
return me.timeAxis.step.step;
},
toScreen: me._toScreen.bind(me), toScreen: me._toScreen.bind(me),
toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
toTime: me._toTime.bind(me), toTime: me._toTime.bind(me),
@ -78,7 +84,6 @@ function Timeline (container, items, groups, options) {
// time axis // time axis
this.timeAxis = new TimeAxis(this.body); this.timeAxis = new TimeAxis(this.body);
this.components.push(this.timeAxis); this.components.push(this.timeAxis);
this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
// current time bar // current time bar
this.currentTime = new CurrentTime(this.body); this.currentTime = new CurrentTime(this.body);

+ 0
- 10
lib/timeline/component/DataAxis.js View File

@ -626,14 +626,4 @@ DataAxis.prototype._calculateCharSize = function () {
} }
}; };
/**
* 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
*/
DataAxis.prototype.snap = function(date) {
return this.step.snap(date);
};
module.exports = DataAxis; module.exports = DataAxis;

+ 15
- 7
lib/timeline/component/ItemSet.js View File

@ -2,6 +2,7 @@ var Hammer = require('../../module/hammer');
var util = require('../../util'); var util = require('../../util');
var DataSet = require('../../DataSet'); var DataSet = require('../../DataSet');
var DataView = require('../../DataView'); var DataView = require('../../DataView');
var TimeStep = require('../TimeStep');
var Component = require('./Component'); var Component = require('./Component');
var Group = require('./Group'); var Group = require('./Group');
var BackgroundGroup = require('./BackgroundGroup'); var BackgroundGroup = require('./BackgroundGroup');
@ -41,6 +42,8 @@ function ItemSet(body, options) {
remove: false remove: false
}, },
snap: TimeStep.snap,
onAdd: function (item, callback) { onAdd: function (item, callback) {
callback(item); callback(item);
}, },
@ -271,7 +274,7 @@ ItemSet.prototype._create = function(){
ItemSet.prototype.setOptions = function(options) { ItemSet.prototype.setOptions = function(options) {
if (options) { if (options) {
// copy all options that we know // copy all options that we know
var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder', 'dataAttributes', 'template','hide'];
var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder', 'dataAttributes', 'template','hide', 'snap'];
util.selectiveExtend(fields, this.options, options); util.selectiveExtend(fields, this.options, options);
if ('margin' in options) { if ('margin' in options) {
@ -1171,8 +1174,10 @@ ItemSet.prototype._onDrag = function (event) {
if (this.touchParams.itemProps) { if (this.touchParams.itemProps) {
var me = this; var me = this;
var snap = this.body.util.snap || null;
var snap = this.options.snap || null;
var xOffset = this.body.dom.root.offsetLeft + this.body.domProps.left.width; var xOffset = this.body.dom.root.offsetLeft + this.body.domProps.left.width;
var scale = this.body.util.getScale();
var step = this.body.util.getStep();
// move // move
this.touchParams.itemProps.forEach(function (props) { this.touchParams.itemProps.forEach(function (props) {
@ -1183,12 +1188,12 @@ ItemSet.prototype._onDrag = function (event) {
if ('start' in props) { if ('start' in props) {
var start = new Date(props.start + offset); var start = new Date(props.start + offset);
newProps.start = snap ? snap(start) : start;
newProps.start = snap ? snap(start, scale, step) : start;
} }
if ('end' in props) { if ('end' in props) {
var end = new Date(props.end + offset); var end = new Date(props.end + offset);
newProps.end = snap ? snap(end) : end;
newProps.end = snap ? snap(end, scale, step) : end;
} }
else if ('duration' in props) { else if ('duration' in props) {
newProps.end = new Date(newProps.start.valueOf() + props.duration); newProps.end = new Date(newProps.start.valueOf() + props.duration);
@ -1356,7 +1361,7 @@ ItemSet.prototype._onAddItem = function (event) {
if (!this.options.editable.add) return; if (!this.options.editable.add) return;
var me = this, var me = this,
snap = this.body.util.snap || null,
snap = this.options.snap || null,
item = ItemSet.itemFromTarget(event); item = ItemSet.itemFromTarget(event);
if (item) { if (item) {
@ -1375,15 +1380,18 @@ ItemSet.prototype._onAddItem = function (event) {
var xAbs = util.getAbsoluteLeft(this.dom.frame); var xAbs = util.getAbsoluteLeft(this.dom.frame);
var x = event.gesture.center.pageX - xAbs; var x = event.gesture.center.pageX - xAbs;
var start = this.body.util.toTime(x); var start = this.body.util.toTime(x);
var scale = this.body.util.getScale();
var step = this.body.util.getStep();
var newItem = { var newItem = {
start: snap ? snap(start) : start,
start: snap ? snap(start, scale, step) : start,
content: 'new item' content: 'new item'
}; };
// when default type is a range, add a default end date to the new item // when default type is a range, add a default end date to the new item
if (this.options.type === 'range') { if (this.options.type === 'range') {
var end = this.body.util.toTime(x + this.props.width / 5); var end = this.body.util.toTime(x + this.props.width / 5);
newItem.end = snap ? snap(end) : end;
newItem.end = snap ? snap(end, scale, step) : end;
} }
newItem[this.itemsData._fieldId] = util.randomUUID(); newItem[this.itemsData._fieldId] = util.randomUUID();

+ 0
- 10
lib/timeline/component/TimeAxis.js View File

@ -433,14 +433,4 @@ TimeAxis.prototype._calculateCharSize = function () {
this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth; this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth;
}; };
/**
* 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; module.exports = TimeAxis;

Loading…
Cancel
Save