diff --git a/HISTORY.md b/HISTORY.md index e588235e..cf855fbf 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,93 @@ # vis.js history http://visjs.org + +## 2017-01-15, version 4.18.0 + +### General + +- Readme improvements (#2520) +- Babel updates and fixes (#2466, #2513, #2566) +- Removed dist folder from the develop-branch (#2497) +- updated and cleaned-up npm dependencies (#2518, #2406) +- FEAT: Added CodeClimate tests (#2411) +- FEAT: Added initial Travis-CI support: https://travis-ci.org/almende/vis (#2550) +- FIX #2500: Replace { bool } with { boolean: bool } (#2501, #2506, #2581) +- FIX #2445: Fix YUI Compressor incompatibilities (#2452) +- FIX #2402: make sure a given element isn’t undefined before accessing properties (#2403) +- FIX #2560: IE11 issue 'Symbol' is undefined with babel-polyfill (#2566) +- FIX #2490: Don't pass non-string values to Date.parse (#2534) + +### DataSet + +- FIX: Removed event oldData items (#2535) +- FIX #2528: Fixed deleting item with id 0 (#2530) + +### Network + +- FIX #1911: Fix missing blur edge event (#2554) +- FIX #2478: Fix tooltip issue causing exception when node becomes cluster (#2555) +- FEAT: Change styles if element is selected (#2446) +- FEAT #2306: Add example for network onLoad animation. (#2476) +- FEAT #1845: Adding example of cursor change (#2463) +- FEAT #1603 #1628 #1936 #2298 #2384: Font styles, width and height of network nodes (#2385) +- FEAT: Add pointer position to zoom event (#2377) +- FEAT #1653 #2342: label margins for box, circle, database, icon and text nodes. (#2343) +- FEAT #2233 #2068 #1756: Edit edge without endpoint dragging, and pass label in data (#2329) + +### Timeline / Graph2D + +- FIX: #2522 Right button while dragging item makes items uneditable (#2582) +- FIX #2538: Major axis labels displaying wrong value (#2551) +- FEAT #2516: Added followMouse & overflowMethod to tooltip options (#2544) +- FIX: Fixed tool-tip surviving after item deleted (#2545) +- FIX #2515: Fixed hover events for HTML elements (#2539) +- FIX: Timeline.setGroups for Array (#2529) +- FIX: Error in React example when adding a ranged item (#2521) +- FEAT #226 #2421 #2429: Added mouse events for the timeline (#2473) +- FEAT #497: new stackSubgroups option (#2519, #2527) +- FEAT #338: Added HTML tool-tip support (#2498) +- FIX #2511: readded throttleRedraw option; added DEPRECATED warning (#2514) +- FEAT #2300: Added nested groups (#2416) +- FEAT #2464: Add template support for minor/major labels (#2493) +- FIX #2379: Fix initial drag (#2474) +- FIX #2102: Fix error on click for graph2D when no data is provided (#2472) +- FIX #2469: Fix graph2D render issue (#2470) +- FIX #1126: Add visibleFrameTemplate option for higher item dom content (#2437) +- FIX #2467: Fix Range ctor with optional options parameter (#2468) +- FEAT #1746: Rolling mode (#2439, #2486) +- FIX #2422: Timeline onMove callback (#2427) +- FIX #2370: IE10 drag-and-drop support (#2426) +- FIX #1906: Pass through original hammer.js events (#2420) +- FIX #2327: Add support to fixed times drag and drop (#2372) +- FIX: \_origRedraw sometimes undefined (#2399) +- FIX #2367 #2328: Group editable bug (#2368) +- FIX #2336: Mouse wheel problem on custom time element (#2366) +- FIX #2307: Timeline async initial redraw bug (#2386) +- FIX #2312: Vertical scroll bug with groups and fixed height (#2363) +- FIX #2333: Scrollbar width on browser zoom (#2344) +- Fixed #2319: Bug in TimeStep.prototype.getClassName (#2335) +- FEAT #257: Added option to change the visibility of a group (#2315) +- FEAT: More editable control of timeline items (#2305) +- FIX #2273: Cannot scroll page when zoomKey is enabled (#2301) +- FIX #2295, 2263: Issues with vertical scroll and maxHeight (#2302) +- FIX #2285: onUpdate event (#2304) +- FIX: Timeline-docs: updated group.content description to show that it can be an element (#2296) +- FIX #2251: No axis after daylight saving (#2290) +- FEAT #2256: Timeline editable can override items (#2284) +- FEAT: Graph2d performance enhancement (#2281) + +### Graph3D + +- FEAT #2451: Allow pass the color of points in 'dot-color' mode of Graph3D (#2489) +- FEAT: Improvement for camera 3d moving (#2340) +- FEAT: Add ability to move graph3d by left mouse button while pressing ctrl key and rotate like before (#2357) +- FIX: Fixed label disappearing bug for large axis values in graph3d (#2348) +- FIX: Fixed Grpah3D-docs: Changed "an" to "and" in graph3D docs (#2313) +- FIX #2274: Graph3d disappears when setSize is called (#2293) +- FIX: Fixed typo in index.html of Graph3D (#2286) + + ## 2016-11-05, version 4.17.0 ### General diff --git a/README.md b/README.md index ab8faaea..b08e5997 100644 --- a/README.md +++ b/README.md @@ -265,10 +265,6 @@ var options = {}; var timeline = new Timeline(container, data, options); ``` -Install the application dependencies via npm: - - $ npm install vis moment - The application can be bundled and minified: $ browserify app.js -o dist/app-bundle.js -t babelify @@ -289,6 +285,39 @@ And loaded into a webpage: ``` +#### Example 4: Integrate vis.js components directly in your webpack build + +You can integrate e.g. the timeline component directly in you webpack build. +Therefor you just import the component-files from root direcory (starting with "index-"). + +```js +var visTimeline = require('vis/index-timeline-graph2d'); + +var container = document.getElementById('visualization'); +var data = new DataSet(); +var timeline = new Timeline(container, data, {}); +``` + +To get this to work you'll need to add the some babel-loader-setting: + +```js +module: { + loaders: [{ + test: /node_modules[\\\/]vis[\\\/].*\.js$/, + loader: 'babel', + query: { + cacheDirectory: true, + presets: ["es2015"], + plugins: [ + "transform-es3-property-literals", + "transform-es3-member-expression-literals", + "transform-runtime" + ] + } + }] +} +``` + ## Test To test the library, install the project dependencies once: diff --git a/docs/network/index.html b/docs/network/index.html index 2742e3b8..cc86669f 100644 --- a/docs/network/index.html +++ b/docs/network/index.html @@ -1401,7 +1401,7 @@ var options = { zoom Object - Fired when the user zooms in or out. The properties tell you which direction the zoom is in. The scale is a number greater than 0, which is the same that you get with network.getScale(), Passes an object with properties structured as: + Fired when the user zooms in or out. The properties tell you which direction the zoom is in. The scale is a number greater than 0, which is the same that you get with network.getScale(). When fired by clicking the zoom in or zoom out navigation buttons, the pointer property of the object passed will be null. Passes an object with properties structured as:
{
   direction: '+'/'-',
   scale: Number,
diff --git a/docs/timeline/index.html b/docs/timeline/index.html
index 2f905929..351bb919 100644
--- a/docs/timeline/index.html
+++ b/docs/timeline/index.html
@@ -1592,7 +1592,7 @@ timeline.off('select', onSelect);
         
       
       Fired after the user selects or deselects items by tapping or holding them.
-        When a use taps an already selected item, the select event is fired again.
+        When a user taps an already selected item, the select event is fired again.
         Not fired when the method setSelectionis executed.
       
     
diff --git a/examples/graph2d/08_performance.html b/examples/graph2d/08_performance.html
index 9a2ad2e1..75701fa5 100644
--- a/examples/graph2d/08_performance.html
+++ b/examples/graph2d/08_performance.html
@@ -147,4 +147,4 @@
     var graph2d = new vis.Graph2d(container, dataset, options);
 
 
-
\ No newline at end of file
+
diff --git a/examples/network/basicUsage.html b/examples/network/basicUsage.html
index 2b8b086c..f1f4e5f9 100644
--- a/examples/network/basicUsage.html
+++ b/examples/network/basicUsage.html
@@ -37,7 +37,8 @@
     {from: 1, to: 3},
     {from: 1, to: 2},
     {from: 2, to: 4},
-    {from: 2, to: 5}
+    {from: 2, to: 5},
+    {from: 3, to: 3}
   ]);
 
   // create a network
diff --git a/examples/network/edgeStyles/colors.html b/examples/network/edgeStyles/colors.html
index 99d98b11..4f99a340 100644
--- a/examples/network/edgeStyles/colors.html
+++ b/examples/network/edgeStyles/colors.html
@@ -30,14 +30,14 @@
 
 
diff --git a/lib/DataSet.js b/lib/DataSet.js
index 790a39e9..8ebee256 100644
--- a/lib/DataSet.js
+++ b/lib/DataSet.js
@@ -730,7 +730,7 @@ DataSet.prototype.clear = function (senderId) {
   var i, len;
   var ids = Object.keys(this._data);
   var items = [];
-  
+
   for (i = 0, len = ids.length; i < len; i++) {
     items.push(this._data[ids[i]]);
   }
@@ -912,7 +912,7 @@ DataSet.prototype._getItem = function (id, types) {
   }
 
   if (!converted[this._fieldId]) {
-    converted[this._fieldId] = id;
+    converted[this._fieldId] = raw.id;
   }
 
   return converted;
diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js
index b068133a..2f4addfc 100644
--- a/lib/network/modules/LayoutEngine.js
+++ b/lib/network/modules/LayoutEngine.js
@@ -381,9 +381,11 @@ class LayoutEngine {
     // the main method to shift the trees
     let shiftTrees = () => {
       let treeSizes = getTreeSizes();
+      let shiftBy = 0;
       for (let i = 0; i < treeSizes.length - 1; i++) {
         let diff = treeSizes[i].max - treeSizes[i+1].min;
-        shiftTree(i + 1, diff + this.options.hierarchical.treeSpacing);
+        shiftBy += diff + this.options.hierarchical.treeSpacing;
+        shiftTree(i + 1, shiftBy);
       }
     };
 
@@ -1358,4 +1360,4 @@ class LayoutEngine {
 
 }
 
-export default LayoutEngine;
\ No newline at end of file
+export default LayoutEngine;
diff --git a/lib/network/modules/components/NavigationHandler.js b/lib/network/modules/components/NavigationHandler.js
index befe3063..9d044fdc 100644
--- a/lib/network/modules/components/NavigationHandler.js
+++ b/lib/network/modules/components/NavigationHandler.js
@@ -160,7 +160,8 @@ class NavigationHandler {
 
     this.body.view.scale = scale;
     this.body.view.translation = { x: tx, y: ty };
-    this.body.emitter.emit('zoom', { direction: '+', scale: this.body.view.scale, pointer: pointer });
+    this.body.emitter.emit('zoom', { direction: '+', scale: this.body.view.scale, pointer: null });
+
   }
   _zoomOut()  {
     var scaleOld = this.body.view.scale;
@@ -172,7 +173,7 @@ class NavigationHandler {
 
     this.body.view.scale = scale;
     this.body.view.translation = { x: tx, y: ty };
-    this.body.emitter.emit('zoom', { direction: '-', scale: this.body.view.scale, pointer: pointer });
+    this.body.emitter.emit('zoom', { direction: '-', scale: this.body.view.scale, pointer: null });
   }
 
 
@@ -226,4 +227,4 @@ class NavigationHandler {
 }
 
 
-export default NavigationHandler;
\ No newline at end of file
+export default NavigationHandler;
diff --git a/lib/network/modules/components/edges/util/EdgeBase.js b/lib/network/modules/components/edges/util/EdgeBase.js
index a646cd4f..f8f608ea 100644
--- a/lib/network/modules/components/edges/util/EdgeBase.js
+++ b/lib/network/modules/components/edges/util/EdgeBase.js
@@ -57,7 +57,7 @@ class EdgeBase {
       this._line(ctx, values, viaNode, fromPoint, toPoint);
     }
     else {
-      let [x,y,radius] = this._getCircleData(ctx, values);
+      let [x,y,radius] = this._getCircleData(ctx);
       this._circle(ctx, values, x, y, radius);
     }
   }
@@ -83,7 +83,7 @@ class EdgeBase {
         this._line(ctx, values, viaNode);
       }
       else {
-        let [x,y,radius] = this._getCircleData(ctx, values);
+        let [x,y,radius] = this._getCircleData(ctx);
         this._circle(ctx, values, x, y, radius);
       }
 
@@ -98,7 +98,7 @@ class EdgeBase {
         ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, pattern);
       }
       else {
-        let [x,y,radius] = this._getCircleData(ctx, values);
+        let [x,y,radius] = this._getCircleData(ctx);
         this._circle(ctx, values, x, y, radius);
       }
       // draw shadow if enabled
@@ -331,7 +331,7 @@ class EdgeBase {
       returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via)
     }
     else {
-      let [x,y,radius] = this._getCircleData(undefined, values);
+      let [x,y,radius] = this._getCircleData(undefined);
       let dx = x - x3;
       let dy = y - y3;
       returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius);
diff --git a/lib/network/modules/components/nodes/shapes/Circle.js b/lib/network/modules/components/nodes/shapes/Circle.js
index d8a9b2a1..a8f6d3bc 100644
--- a/lib/network/modules/components/nodes/shapes/Circle.js
+++ b/lib/network/modules/components/nodes/shapes/Circle.js
@@ -35,7 +35,7 @@ class Circle extends CircleImageBase {
 
     this.updateBoundingBox(x,y);
     this.labelModule.draw(ctx, this.left + this.textSize.width / 2 + this.margin.left,
-                               this.top + this.textSize.height / 2 + this.margin.top, selected, hover);
+                               y, selected, hover);
   }
 
   updateBoundingBox(x,y) {
diff --git a/lib/network/modules/components/nodes/shapes/Diamond.js b/lib/network/modules/components/nodes/shapes/Diamond.js
index 182c984d..ecda7ea3 100644
--- a/lib/network/modules/components/nodes/shapes/Diamond.js
+++ b/lib/network/modules/components/nodes/shapes/Diamond.js
@@ -7,7 +7,7 @@ class Diamond extends ShapeBase {
     super(options, body, labelModule)
   }
 
-  resize(ctx, values, selected = this.selected, hover = this.hover) {
+  resize(ctx, selected = this.selected, hover = this.hover, values) {
     this._resizeShape(selected, hover, values);
   }
 
diff --git a/lib/network/modules/components/nodes/shapes/Dot.js b/lib/network/modules/components/nodes/shapes/Dot.js
index 701600fc..6d054619 100644
--- a/lib/network/modules/components/nodes/shapes/Dot.js
+++ b/lib/network/modules/components/nodes/shapes/Dot.js
@@ -7,7 +7,7 @@ class Dot extends ShapeBase {
     super(options, body, labelModule)
   }
 
-  resize(ctx, values, selected = this.selected, hover = this.hover) {
+  resize(ctx, selected = this.selected, hover = this.hover, values) {
     this._resizeShape(selected, hover, values);
   }
 
diff --git a/lib/network/modules/components/nodes/shapes/Image.js b/lib/network/modules/components/nodes/shapes/Image.js
index becbf4ab..431f6915 100644
--- a/lib/network/modules/components/nodes/shapes/Image.js
+++ b/lib/network/modules/components/nodes/shapes/Image.js
@@ -42,11 +42,11 @@ class Image extends CircleImageBase {
       ctx.save();
       // if borders are zero width, they will be drawn with width 1 by default. This prevents that
       if (borderWidth > 0) {
-        this.enableBorderDashes(ctx);
+        this.enableBorderDashes(ctx, values);
         //draw the border
         ctx.stroke();
         //disable dashed border for other elements
-        this.disableBorderDashes(ctx);
+        this.disableBorderDashes(ctx, values);
       }
       ctx.restore();
 
diff --git a/lib/network/modules/components/nodes/shapes/Star.js b/lib/network/modules/components/nodes/shapes/Star.js
index 490de5ac..b99bc4e4 100644
--- a/lib/network/modules/components/nodes/shapes/Star.js
+++ b/lib/network/modules/components/nodes/shapes/Star.js
@@ -7,7 +7,7 @@ class Star extends ShapeBase {
     super(options, body, labelModule)
   }
 
-  resize(ctx, values, selected, hover) {
+  resize(ctx, selected, hover, values) {
     this._resizeShape(selected, hover, values);
   }
 
diff --git a/lib/timeline/Range.js b/lib/timeline/Range.js
index e49951d4..e2ef0bbb 100644
--- a/lib/timeline/Range.js
+++ b/lib/timeline/Range.js
@@ -15,7 +15,7 @@ var DateUtil = require('./DateUtil');
 function Range(body, options) {
   var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
   var start = now.clone().add(-3, 'days').valueOf();
-  var end = now.clone().add(-3, 'days').valueOf(); 
+  var end = now.clone().add(3, 'days').valueOf(); 
 
   if(options === undefined) {
     this.start = start;
@@ -256,6 +256,13 @@ Range.prototype.setRange = function(start, end, animation, byUser, event) {
   }
 };
 
+/**
+ * Get the number of milliseconds per pixel.
+ */
+Range.prototype.getMillisecondsPerPixel = function() {
+  return (this.end - this.start) / this.body.dom.center.clientWidth;
+}
+
 /**
  * Stop an animation
  * @private
@@ -291,7 +298,7 @@ Range.prototype._applyRange = function(start, end) {
     throw new Error('Invalid end "' + end + '"');
   }
 
-  // prevent start < end
+  // prevent end < start
   if (newEnd < newStart) {
     newEnd = newStart;
   }
diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js
index 948f5758..9d93d792 100644
--- a/lib/timeline/component/ItemSet.js
+++ b/lib/timeline/component/ItemSet.js
@@ -256,6 +256,8 @@ ItemSet.prototype._create = function(){
   this.body.dom.centerContainer.addEventListener('mouseover', this._onMouseOver.bind(this));
   this.body.dom.centerContainer.addEventListener('mouseout', this._onMouseOut.bind(this));
   this.body.dom.centerContainer.addEventListener('mousemove', this._onMouseMove.bind(this));
+  // right-click on timeline 
+  this.body.dom.centerContainer.addEventListener('contextmenu', this._onDragEnd.bind(this));
 
   // attach to the DOM
   this.show();
@@ -1590,6 +1592,7 @@ ItemSet.prototype._moveToGroup = function(item, groupId) {
  * @private
  */
 ItemSet.prototype._onDragEnd = function (event) {
+  this.touchParams.itemIsDragging = false;
   if (this.touchParams.itemProps) {
     event.stopPropagation();
 
@@ -1597,7 +1600,6 @@ ItemSet.prototype._onDragEnd = function (event) {
     var dataset = this.itemsData.getDataSet();
     var itemProps = this.touchParams.itemProps ;
     this.touchParams.itemProps = null;
-    this.touchParams.itemIsDragging = false;
 
     itemProps.forEach(function (props) {
       var id = props.item.id;
diff --git a/lib/timeline/component/item/BoxItem.js b/lib/timeline/component/item/BoxItem.js
index 02e35937..377b2e06 100644
--- a/lib/timeline/component/item/BoxItem.js
+++ b/lib/timeline/component/item/BoxItem.js
@@ -37,15 +37,14 @@ BoxItem.prototype = new Item (null, null, null);
 
 /**
  * Check whether this item is visible inside given range
- * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @param {{start: Number, end: Number}} range with a timestamp for start and end
  * @returns {boolean} True if visible
  */
 BoxItem.prototype.isVisible = function(range) {
   // determine visibility
   var isVisible;
   var align = this.options.align;
-  var msPerPixel = (range.end - range.start) / range.body.dom.center.clientWidth;
-  var widthInMs = this.width * msPerPixel;
+  var widthInMs = this.width * range.getMillisecondsPerPixel();
 
   if (align == 'right') {
     isVisible = (this.data.start.getTime() > range.start ) && (this.data.start.getTime() - widthInMs < range.end);
diff --git a/lib/timeline/component/item/PointItem.js b/lib/timeline/component/item/PointItem.js
index c4c14c91..83c4e67b 100644
--- a/lib/timeline/component/item/PointItem.js
+++ b/lib/timeline/component/item/PointItem.js
@@ -38,13 +38,12 @@ PointItem.prototype = new Item (null, null, null);
 
 /**
  * Check whether this item is visible inside given range
- * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @param {{start: Number, end: Number}} range with a timestamp for start and end
  * @returns {boolean} True if visible
  */
 PointItem.prototype.isVisible = function(range) {
   // determine visibility
-  var msPerPixel = (range.end - range.start) / range.body.dom.center.clientWidth;
-  var widthInMs = this.width * msPerPixel;
+  var widthInMs = this.width * range.getMillisecondsPerPixel();
   
   return (this.data.start.getTime() + widthInMs > range.start ) && (this.data.start < range.end);
 };
diff --git a/lib/timeline/optionsGraph2d.js b/lib/timeline/optionsGraph2d.js
index 18dd1228..0caaf2d2 100644
--- a/lib/timeline/optionsGraph2d.js
+++ b/lib/timeline/optionsGraph2d.js
@@ -18,90 +18,90 @@ let any = 'any';
 
 let allOptions = {
   configure: {
-    enabled: {bool},
-    filter: {bool,'function': 'function'},
+    enabled: {'boolean': bool},
+    filter: {'boolean': bool,'function': 'function'},
     container: {dom},
-    __type__: {object,bool,'function': 'function'}
+    __type__: {object,'boolean': bool,'function': 'function'}
   },
 
   //globals :
   yAxisOrientation: {string:['left','right']},
   defaultGroup: {string},
-  sort: {bool},
-  sampling: {bool},
-  stack:{bool},
+  sort: {'boolean': bool},
+  sampling: {'boolean': bool},
+  stack:{'boolean': bool},
   graphHeight: {string, number},
   shaded: {
-    enabled: {bool},
+    enabled: {'boolean': bool},
     orientation: {string:['bottom','top','zero','group']}, // top, bottom, zero, group
     groupId: {object},
-    __type__: {bool,object}
+    __type__: {'boolean': bool,object}
   },
   style: {string:['line','bar','points']}, // line, bar
   barChart: {
     width: {number},
     minWidth: {number},
-    sideBySide: {bool},
+    sideBySide: {'boolean': bool},
     align: {string:['left','center','right']},
     __type__: {object}
   },
   interpolation: {
-    enabled: {bool},
+    enabled: {'boolean': bool},
     parametrization: {string:['centripetal', 'chordal','uniform']}, // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
     alpha: {number},
-    __type__: {object,bool}
+    __type__: {object,'boolean': bool}
   },
   drawPoints: {
-    enabled: {bool},
+    enabled: {'boolean': bool},
     onRender: { 'function': 'function' },
     size: {number},
     style: {string:['square','circle']}, // square, circle
-    __type__: {object,bool,'function': 'function'}
+    __type__: {object,'boolean': bool,'function': 'function'}
   },
   dataAxis: {
-    showMinorLabels: {bool},
-    showMajorLabels: {bool},
-    icons: {bool},
+    showMinorLabels: {'boolean': bool},
+    showMajorLabels: {'boolean': bool},
+    icons: {'boolean': bool},
     width: {string, number},
-    visible: {bool},
-    alignZeros: {bool},
+    visible: {'boolean': bool},
+    alignZeros: {'boolean': bool},
     left:{
-      range: {min:{number},max:{number},__type__: {object}},
+      range: {min:{number,'undefined': 'undefined'},max:{number,'undefined': 'undefined'},__type__: {object}},
       format: {'function': 'function'},
-      title: {text:{string,number},style:{string},__type__: {object}},
+      title: {text:{string,number,'undefined': 'undefined'},style:{string,'undefined': 'undefined'},__type__: {object}},
       __type__: {object}
     },
     right:{
-      range: {min:{number},max:{number},__type__: {object}},
+      range: {min:{number,'undefined': 'undefined'},max:{number,'undefined': 'undefined'},__type__: {object}},
       format: {'function': 'function'},
-      title: {text:{string,number},style:{string},__type__: {object}},
+      title: {text:{string,number,'undefined': 'undefined'},style:{string,'undefined': 'undefined'},__type__: {object}},
       __type__: {object}
     },
     __type__: {object}
   },
   legend: {
-    enabled: {bool},
-    icons: {bool},
+    enabled: {'boolean': bool},
+    icons: {'boolean': bool},
     left: {
-      visible: {bool},
+      visible: {'boolean': bool},
       position: {string:['top-right','bottom-right','top-left','bottom-left']},
       __type__: {object}
     },
     right: {
-      visible: {bool},
+      visible: {'boolean': bool},
       position: {string:['top-right','bottom-right','top-left','bottom-left']},
       __type__: {object}
     },
-    __type__: {object,bool}
+    __type__: {object,'boolean': bool}
   },
   groups: {
     visibility: {any},
     __type__: {object}
   },
 
-  autoResize: {bool},
+  autoResize: {'boolean': bool},
   throttleRedraw: {number}, // TODO: DEPRICATED see https://github.com/almende/vis/issues/2511
-  clickToUse: {bool},
+  clickToUse: {'boolean': bool},
   end: {number, date, string, moment},
   format: {
     minorLabels: {
@@ -146,12 +146,12 @@ let allOptions = {
   maxMinorChars: {number},
   min: {date, number, string, moment},
   minHeight: {number, string},
-  moveable: {bool},
-  multiselect: {bool},
+  moveable: {'boolean': bool},
+  multiselect: {'boolean': bool},
   orientation: {string},
-  showCurrentTime: {bool},
-  showMajorLabels: {bool},
-  showMinorLabels: {bool},
+  showCurrentTime: {'boolean': bool},
+  showMajorLabels: {'boolean': bool},
+  showMinorLabels: {'boolean': bool},
   start: {date, number, string, moment},
   timeAxis: {
     scale: {string,'undefined': 'undefined'},
@@ -159,7 +159,7 @@ let allOptions = {
     __type__: {object}
   },
   width: {string, number},
-  zoomable: {bool},
+  zoomable: {'boolean': bool},
   zoomKey: {string: ['ctrlKey', 'altKey', 'metaKey', '']},
   zoomMax: {number},
   zoomMin: {number},
diff --git a/package.json b/package.json
index cbbce13b..243889ca 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "vis",
-  "version": "4.17.0-SNAPSHOT",
+  "version": "4.18.0-SNAPSHOT",
   "description": "A dynamic, browser-based visualization library.",
   "homepage": "http://visjs.org/",
   "license": "(Apache-2.0 OR MIT)",
@@ -29,8 +29,14 @@
     "watch-dev": "gulp watch --bundle"
   },
   "dependencies": {
+		"babel-core": "^6.21.0",
+		"babel-loader": "^6.2.10",
     "babel-polyfill": "^6.20.0",
-    "babel-runtime": "^6.20.0",    
+		"babel-plugin-transform-es3-member-expression-literals": "^6.8.0",
+		"babel-plugin-transform-es3-property-literals": "^6.8.0",
+		"babel-plugin-transform-runtime": "^6.15.0",
+		"babel-preset-es2015": "^6.18.0",
+		"babel-runtime": "^6.20.0",
     "emitter-component": "^1.1.1",
     "moment": "^2.12.0",
     "propagating-hammerjs": "^1.4.6",
@@ -39,12 +45,6 @@
   },
   "devDependencies": {
     "async": "^2.0.0-rc.2",
-    "babel-core": "^6.21.0",
-    "babel-loader": "^6.2.10",
-    "babel-plugin-transform-es3-member-expression-literals": "^6.8.0",
-    "babel-plugin-transform-es3-property-literals": "^6.8.0",
-    "babel-plugin-transform-runtime": "^6.15.0",
-    "babel-preset-es2015": "^6.18.0",
     "babelify": "^7.3.0",
     "clean-css": "^3.4.10",
     "gulp": "^3.9.1",
@@ -52,7 +52,9 @@
     "gulp-concat": "^2.6.0",
     "gulp-rename": "^1.2.2",
     "gulp-util": "^3.0.7",
+    "jsdom": "9.9.1",
     "mocha": "^3.1.2",
+    "mocha-jsdom": "^1.1.0",
     "rimraf": "^2.5.2",
     "uglify-js": "^2.6.2",
     "uuid": "^3.0.1",
diff --git a/test/TimelineRange.test.js b/test/TimelineRange.test.js
new file mode 100644
index 00000000..ba79142d
--- /dev/null
+++ b/test/TimelineRange.test.js
@@ -0,0 +1,61 @@
+var assert = require('assert');
+var vis = require('../dist/vis');
+var jsdom = require('mocha-jsdom')
+var moment = vis.moment;
+var timeline = vis.timeline;
+var Range = timeline.Range;
+
+function buildSimpleBody() {
+  var body = {
+      dom: {
+        center: {
+          clientWidth: 1000
+        }
+      },
+      domProps: this.props,
+      emitter: {
+        on: function() {},
+        off: function() {},
+        emit: function() {}
+      },
+      hiddenDates: [],
+      util: {}
+    };
+    body.dom.rollingModeBtn = document.createElement('div');
+    return body;
+}
+
+describe('Timeline Range', function () {
+  
+  jsdom();
+
+  it('should have start default before now', function () {
+    var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0).valueOf();
+    var range = new Range(buildSimpleBody());
+    assert(range.start < now, "Default start is before now");
+  });
+
+  it('should have end default after now', function () {
+    var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0).valueOf();
+    var range = new Range(buildSimpleBody());
+    assert(range.end > now, "Default end is after now");
+  });
+
+  it('should support custom start and end dates', function () {
+    var range = new Range(buildSimpleBody());
+    range.setRange(new Date(2017, 0, 26, 13, 26, 3, 320), new Date(2017, 3, 11, 0, 23, 35, 0), false, false, null);
+    assert.equal(range.start, new Date(2017, 0, 26, 13, 26, 3, 320).valueOf(),  "start is as expected");
+    assert.equal(range.end, new Date(2017, 3, 11, 0, 23, 35, 0).valueOf(),  "end is as expected");
+  });
+
+  it('should calculate milliseconds per pixel', function () {
+    var range = new Range(buildSimpleBody());
+    assert(range.getMillisecondsPerPixel() > 0, "positive value for milliseconds per pixel");
+  });
+
+  it('should calculate 1 millisecond per pixel for simple range', function () {
+    var range = new Range(buildSimpleBody());
+    range.setRange(new Date(2017, 0, 26, 13, 26, 3, 320), new Date(2017, 0, 26, 13, 26, 4, 320), false, false, null);
+    assert.equal(range.getMillisecondsPerPixel(), 1, "one second over 1000 pixels");
+  });
+});
\ No newline at end of file