diff --git a/HISTORY.md b/HISTORY.md index 5f65d033..029ef2a1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,17 +2,50 @@ http://visjs.org -## 2014-06-06, version 1.1.1 +## not yet released, version 2.1.0 ### Timeline +- Fixed auto detected item type being preferred over the global item `type`. +- Throws an error when constructing without new keyword. +- Removed the 'rangeoverflow' item type. Instead, one can use a regular range + and change css styling of the item contents to: + + .vis.timeline .item.range .content { + overflow: visible; + } + +### Graph + +- Throws an error when constructing without new keyword. + +### Graph3d + +- Throws an error when constructing without new keyword. + + +## 2014-06-19, version 2.0.0 + +### Timeline + +- Implemented function `destroy` to neatly cleanup a Timeline. +- Implemented support for dragging the timeline contents vertically. +- Implemented options `zoomable` and `moveable`. - Changed default value of option `showCurrentTime` to true. +- Internal refactoring and simplification of the code. +- Fixed property `className` of groups not being applied to related contents and + background elements, and not being updated once applied. ### Graph - Reduced the timestep a little for smoother animations. - Fixed dataManipulation.initiallyVisible functionality (thanks theGrue). - Forced typecast of fontSize to Number. +- Added editing of edges using the data manipulation toolkit. + +### DataSet + +- Renamed option `convert` to `type`. ## 2014-06-06, version 1.1.0 diff --git a/Jakefile.js b/Jakefile.js index 384b6ef9..d1bb2ec6 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -44,6 +44,7 @@ task('build', {async: true}, function () { './src/timeline/component/css/timeaxis.css', './src/timeline/component/css/currenttime.css', './src/timeline/component/css/customtime.css', + './src/timeline/component/css/animation.css', './src/timeline/component/css/dataaxis.css', './src/timeline/component/css/pathStyles.css', diff --git a/bower.json b/bower.json index 336ebee4..fca2518e 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "vis", - "version": "1.1.0", + "version": "2.0.1-SNAPSHOT", "description": "A dynamic, browser-based visualization library.", "homepage": "http://visjs.org/", "repository": { diff --git a/docs/dataset.html b/docs/dataset.html index 0828e37e..723affae 100644 --- a/docs/dataset.html +++ b/docs/dataset.html @@ -91,7 +91,7 @@ console.log('filtered items', items); // retrieve formatted items var items = data.get({ fields: ['id', 'date'], - convert: { + type: { date: 'ISODate' } }); @@ -149,7 +149,7 @@ var data = new vis.DataSet([data] [, options]) - convert + type Object.<String, String> none @@ -227,7 +227,7 @@ var data = new vis.DataSet([data] [, options]) Number[] Get ids of all items or of a filtered set of items. - Available options are described in section Data Selection, except that options fields and convert are not applicable in case of getIds. + Available options are described in section Data Selection, except that options fields and type are not applicable in case of getIds. @@ -649,7 +649,7 @@ DataSet.map(callback [, options]); - convert + type Object.<String, String> An object containing field names as key, and data types as value. @@ -700,7 +700,7 @@ data.add([ // retrieve formatted items var items = data.get({ fields: ['id', 'date', 'group'], // output the specified fields only - convert: { + type: { date: 'Date', // convert the date fields to Date objects group: 'String' // convert the group fields to Strings } diff --git a/docs/graph.html b/docs/graph.html index 20e96ed2..791c8d51 100644 --- a/docs/graph.html +++ b/docs/graph.html @@ -1593,6 +1593,16 @@ var options: { // all fields normally accepted by a node can be used. callback(newData); // call the callback with the new data to edit the node. } + onEditEdge: function(data,callback) { + /** data = {id: edgeID, + * from: nodeId1, + * to: nodeId2, + * }; + */ + var newData = {..}; // alter the data as you want, except for the ID. + // all fields normally accepted by an edge can be used. + callback(newData); // call the callback with the new data to edit the edge. + } onConnect: function(data,callback) { // data = {from: nodeId1, to: nodeId2}; var newData = {..}; // check or alter data as you see fit. @@ -1951,10 +1961,12 @@ var options: { link:"Add Link", del:"Delete selected", editNode:"Edit Node", + editEdge:"Edit Edge", back:"Back", addDescription:"Click in an empty space to place a new node.", linkDescription:"Click on a node and drag the edge to another node to connect them.", + editEdgeDescription:"Click on either one of the control points and drag them to another node to connect to it.". addError:"The function for add does not support two arguments (data,callback).", linkError:"The function for connect does not support two arguments @@ -2137,16 +2149,36 @@ var options: { + + selectNodes(selection, [highlightEdges]) + none + Select nodes. + selection is an array with ids of nodes to be selected. + The array selection can contain zero or multiple ids. + Example usage: graph.selectNodes([3, 5]); will select + nodes with id 3 and 5. The highlisghEdges boolean can be used to automatically select the edges connected to the node. + + + + selectEdges(selection) + none + Select Edges. + selection is an array with ids of edges to be selected. + The array selection can contain zero or multiple ids. + Example usage: graph.selectEdges([3, 5]); will select + edges with id 3 and 5. + + setSelection(selection) none - Select nodes. - selection is an array with ids of nodes to be selected. - The array selection can contain zero or multiple ids. - Example usage: graph.setSelection([3, 5]); will select - nodes with id 3 and 5. + Select nodes [deprecated]. + selection is an array with ids of nodes to be selected. + The array selection can contain zero or multiple ids. + Example usage: graph.setSelection([3, 5]); will select + nodes with id 3 and 5. - + setSize(width, height) diff --git a/docs/index.html b/docs/index.html index 7a83de95..1d6e3bd8 100644 --- a/docs/index.html +++ b/docs/index.html @@ -162,16 +162,23 @@ var timeline = new vis.Timeline(container, data, options); <div id="visualization"></div> <script type="text/javascript"> + // DOM element where the Timeline will be attached var container = document.getElementById('visualization'); - var data = [ + + // Create a DataSet (allows two way data-binding) + var data = new vis.DataSet([ {id: 1, content: 'item 1', start: '2013-04-20'}, {id: 2, content: 'item 2', start: '2013-04-14'}, {id: 3, content: 'item 3', start: '2013-04-18'}, {id: 4, content: 'item 4', start: '2013-04-16', end: '2013-04-19'}, {id: 5, content: 'item 5', start: '2013-04-25'}, {id: 6, content: 'item 6', start: '2013-04-27'} - ]; + ]); + + // Configuration for the Timeline var options = {}; + + // Create a Timeline var timeline = new vis.Timeline(container, data, options); </script> </body> diff --git a/docs/timeline.html b/docs/timeline.html index 43abb226..07da66f4 100644 --- a/docs/timeline.html +++ b/docs/timeline.html @@ -68,16 +68,23 @@ <div id="visualization"></div> <script type="text/javascript"> + // DOM element where the Timeline will be attached var container = document.getElementById('visualization'); - var items = [ + + // Create a DataSet (allows two way data-binding) + var items = new vis.DataSet([ {id: 1, content: 'item 1', start: '2013-04-20'}, {id: 2, content: 'item 2', start: '2013-04-14'}, {id: 3, content: 'item 3', start: '2013-04-18'}, {id: 4, content: 'item 4', start: '2013-04-16', end: '2013-04-19'}, {id: 5, content: 'item 5', start: '2013-04-25'}, {id: 6, content: 'item 6', start: '2013-04-27'} - ]; + ]); + + // Configuration for the Timeline var options = {}; + + // Create a Timeline var timeline = new vis.Timeline(container, items, options); </script> </body> @@ -195,8 +202,8 @@ var items = [ type String 'box' - The type of the item. Can be 'box' (default), 'point', 'range', or 'rangeoverflow'. - Types 'box' and 'point' need a start date, and types 'range' and 'rangeoverflow' need both a start and end date. Types 'range' and rangeoverflow are equal, except that overflowing text in 'range' is hidden, while visible in 'rangeoverflow'. + The type of the item. Can be 'box' (default), 'point', or 'range'. + Types 'box' and 'point' need a start date, and type 'range' needs both a start and end date. @@ -458,6 +465,16 @@ var options = { Specifies the minimum height for the Timeline. Can be a number in pixels or a string like "300px". + + moveable + Boolean + true + + Specifies whether the Timeline can be moved and zoomed by dragging the window. + See also option zoomable. + + + onAdd Function @@ -587,8 +604,8 @@ var options = { type String - 'box' - Specifies the default type for the timeline items. Choose from 'box', 'point', 'range', and 'rangeoverflow'. Note that individual items can override this default type. + none + Specifies the default type for the timeline items. Choose from 'box', 'point', and 'range'. Note that individual items can override this default type. If undefined, the Timeline will auto detect the type from the items data: if a start and end date is available, a 'range' will be created, and else, a 'box' is created. @@ -599,6 +616,16 @@ var options = { The width of the timeline in pixels or as a percentage. + + zoomable + Boolean + true + + Specifies whether the Timeline can be zoomed by pinching or scrolling in the window. + Only applicable when option moveable is set true. + + + zoomMax Number @@ -646,6 +673,13 @@ timeline.clear({options: true}); // clear options only + + destroy() + none + Destroy the Timeline. The timeline is removed from memory. all DOM elements and event listeners are cleaned up. + + + fit() none @@ -895,7 +929,7 @@ var options = {

diff --git a/examples/timeline/01_basic.html b/examples/timeline/01_basic.html index 5dc030e0..289555f7 100644 --- a/examples/timeline/01_basic.html +++ b/examples/timeline/01_basic.html @@ -16,16 +16,23 @@

diff --git a/examples/timeline/02_interactive.html b/examples/timeline/02_interactive.html index e2f5e417..e555b29c 100644 --- a/examples/timeline/02_interactive.html +++ b/examples/timeline/02_interactive.html @@ -21,12 +21,14 @@ diff --git a/examples/timeline/06_event_listeners.html b/examples/timeline/06_event_listeners.html index 02cb8e45..df240ce4 100644 --- a/examples/timeline/06_event_listeners.html +++ b/examples/timeline/06_event_listeners.html @@ -18,13 +18,7 @@
+ + + + +

Serialization and deserialization

+ +

This example shows how to serialize and deserialize JSON data, and load this in the Timeline via a DataSet. Serialization and deserialization is needed when loading or saving data from a server.

+ + + +
+ + +
+ +
+ + + + \ No newline at end of file diff --git a/examples/timeline/18_range_overflow.html b/examples/timeline/18_range_overflow.html new file mode 100644 index 00000000..8f9f7506 --- /dev/null +++ b/examples/timeline/18_range_overflow.html @@ -0,0 +1,53 @@ + + + + Timeline | Range overflow + + + + + + + + +

+ In case of ranges being spread over a wide range of time, it can be interesting to have the text contents of the ranges overflow the box. This can be achieved by changing the overflow property of the contents to visible with css: +

+
+.vis.timeline .item.range .content {
+  overflow: visible;
+}
+
+ +
+ + + + \ No newline at end of file diff --git a/examples/timeline/index.html b/examples/timeline/index.html index d94e82a6..79b67820 100644 --- a/examples/timeline/index.html +++ b/examples/timeline/index.html @@ -28,6 +28,8 @@

14_a_lot_of_grouped_data.html

15_item_class_names.html

16_navigation_menu.html

+

17_data_serialization.html

+

18_range_overflow.html

requirejs_example.html

diff --git a/examples/timeline/requirejs/scripts/main.js b/examples/timeline/requirejs/scripts/main.js index ff6d5108..6549d024 100644 --- a/examples/timeline/requirejs/scripts/main.js +++ b/examples/timeline/requirejs/scripts/main.js @@ -6,14 +6,14 @@ require.config({ require(['vis'], function (vis) { var container = document.getElementById('visualization'); - var data = [ + var data = new vis.DataSet([ {id: 1, content: 'item 1', start: '2013-04-20'}, {id: 2, content: 'item 2', start: '2013-04-14'}, {id: 3, content: 'item 3', start: '2013-04-18'}, {id: 4, content: 'item 4', start: '2013-04-16', end: '2013-04-19'}, {id: 5, content: 'item 5', start: '2013-04-25'}, {id: 6, content: 'item 6', start: '2013-04-27'} - ]; + ]); var options = {}; var timeline = new vis.Timeline(container, data, options); }); diff --git a/package.json b/package.json index 5bc2cf62..ebca3246 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vis", - "version": "1.1.0", + "version": "2.0.1-SNAPSHOT", "description": "A dynamic, browser-based visualization library.", "homepage": "http://visjs.org/", "repository": { diff --git a/src/DataSet.js b/src/DataSet.js index cb1cf198..996622d3 100644 --- a/src/DataSet.js +++ b/src/DataSet.js @@ -760,28 +760,18 @@ DataSet.prototype.min = function (field) { * The returned array is unordered. */ DataSet.prototype.distinct = function (field) { - var data = this._data, - values = [], - fieldType = "", - count = 0; - - // do not convert unless this is required. - var convert = false; - if (this._options) { - if (this._options.type) { - if (this._options.type.hasOwnProperty(field)) { - fieldType = this._options.type[field]; - convert = true; - } - } - } + var data = this._data; + var values = []; + var fieldType = this._options.type && this._options.type[field] || null; + var count = 0; + var i; for (var prop in data) { if (data.hasOwnProperty(prop)) { var item = data[prop]; var value = item[field]; var exists = false; - for (var i = 0; i < count; i++) { + for (i = 0; i < count; i++) { if (values[i] == value) { exists = true; break; @@ -794,9 +784,9 @@ DataSet.prototype.distinct = function (field) { } } - if (convert == true) { - for (var i = 0; i < values.length; i++) { - values[i] = util.convert(values[i],fieldType); + if (fieldType) { + for (i = 0; i < values.length; i++) { + values[i] = util.convert(values[i], fieldType); } } diff --git a/src/graph/Edge.js b/src/graph/Edge.js index 3ca024ac..6bd94b03 100644 --- a/src/graph/Edge.js +++ b/src/graph/Edge.js @@ -62,6 +62,10 @@ function Edge (properties, graph, constants) { this.lengthFixed = false; this.setProperties(properties, constants); + + this.controlNodesEnabled = false; + this.controlNodes = {from:null, to:null, positions:{}}; + this.connectedNode = null; } /** @@ -721,44 +725,65 @@ Edge.prototype._drawArrow = function(ctx) { * @private */ Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point - if (this.smooth == true) { - var minDistance = 1e9; - var i,t,x,y,dx,dy; - for (i = 0; i < 10; i++) { - t = 0.1*i; - x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2; - y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2; - dx = Math.abs(x3-x); - dy = Math.abs(y3-y); - minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy)); - } - return minDistance - } - else { - var px = x2-x1, - py = y2-y1, - something = px*px + py*py, - u = ((x3 - x1) * px + (y3 - y1) * py) / something; - - if (u > 1) { - u = 1; - } - else if (u < 0) { - u = 0; + if (this.from != this.to) { + if (this.smooth == true) { + var minDistance = 1e9; + var i,t,x,y,dx,dy; + for (i = 0; i < 10; i++) { + t = 0.1*i; + x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2; + y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2; + dx = Math.abs(x3-x); + dy = Math.abs(y3-y); + minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy)); + } + return minDistance } + else { + var px = x2-x1, + py = y2-y1, + something = px*px + py*py, + u = ((x3 - x1) * px + (y3 - y1) * py) / something; - var x = x1 + u * px, - y = y1 + u * py, - dx = x - x3, - dy = y - y3; + 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 + //# 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 - return Math.sqrt(dx*dx + dy*dy); + return Math.sqrt(dx*dx + dy*dy); + } + } + else { + var x, y, dx, dy; + var radius = this.length / 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; + } + else { + x = node.x + radius; + y = node.y - node.height / 2; + } + dx = x - x3; + dy = y - y3; + return Math.abs(Math.sqrt(dx*dx + dy*dy) - radius); } }; @@ -787,4 +812,146 @@ Edge.prototype.positionBezierNode = function() { this.via.x = 0.5 * (this.from.x + this.to.x); this.via.y = 0.5 * (this.from.y + this.to.y); } -}; \ No newline at end of file +}; + +/** + * This function draws the control nodes for the manipulator. In order to enable this, only set the this.controlNodesEnabled to true. + * @param ctx + */ +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); + } + + 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; + } + + this.controlNodes.from.draw(ctx); + this.controlNodes.to.draw(ctx); + } + else { + this.controlNodes = {from:null, to:null, positions:{}}; + } +} + +/** + * Enable control nodes. + * @private + */ +Edge.prototype._enableControlNodes = function() { + this.controlNodesEnabled = true; +} + +/** + * disable control nodes + * @private + */ +Edge.prototype._disableControlNodes = function() { + this.controlNodesEnabled = false; +} + +/** + * 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)); + + 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; + } +} + + +/** + * 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(); + } +} + +/** + * 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; + + + if (this.smooth == true) { + angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x)); + dx = (this.to.x - this.via.x); + dy = (this.to.y - this.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.smooth == true) { + xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x; + yTo = (1 - toBorderPoint) * this.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; + } + + return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}}; +} \ No newline at end of file diff --git a/src/graph/Graph.js b/src/graph/Graph.js index b5cdceb2..084d2261 100644 --- a/src/graph/Graph.js +++ b/src/graph/Graph.js @@ -10,6 +10,9 @@ * @param {Object} options Options */ function Graph (container, data, options) { + if (!(this instanceof Graph)) { + throw new SyntaxError('Constructor must be called with the new operator'); + } this._initializeMixinLoaders(); @@ -30,7 +33,7 @@ function Graph (container, data, options) { this.initializing = true; // these functions are triggered when the dataset is edited - this.triggerFunctions = {add:null,edit:null,connect:null,del:null}; + this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null}; // set constant values this.constants = { @@ -166,9 +169,11 @@ function Graph (container, data, options) { link:"Add Link", del:"Delete selected", editNode:"Edit Node", + editEdge:"Edit Edge", back:"Back", addDescription:"Click in an empty space to place a new node.", linkDescription:"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.", addError:"The function for add does not support two arguments (data,callback).", linkError:"The function for connect does not support two arguments (data,callback).", editError:"The function for edit does not support two arguments (data, callback).", @@ -550,6 +555,10 @@ Graph.prototype.setOptions = function (options) { this.triggerFunctions.edit = options.onEdit; } + if (options.onEditEdge) { + this.triggerFunctions.editEdge = options.onEditEdge; + } + if (options.onConnect) { this.triggerFunctions.connect = options.onConnect; } @@ -1679,7 +1688,6 @@ Graph.prototype._updateValueRange = function(obj) { */ Graph.prototype.redraw = function() { this.setSize(this.width, this.height); - this._redraw(); }; @@ -1711,6 +1719,7 @@ Graph.prototype._redraw = function() { this._doInAllSectors("_drawAllSectorNodes",ctx); this._doInAllSectors("_drawEdges",ctx); this._doInAllSectors("_drawNodes",ctx,false); + this._doInAllSectors("_drawControlNodes",ctx); // this._doInSupportSector("_drawNodes",ctx,true); // this._drawTree(ctx,"#F00F0F"); @@ -1895,6 +1904,21 @@ Graph.prototype._drawEdges = function(ctx) { } }; +/** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @private + */ +Graph.prototype._drawControlNodes = function(ctx) { + var edges = this.edges; + for (var id in edges) { + if (edges.hasOwnProperty(id)) { + edges[id]._drawControlNodes(ctx); + } + } +}; + /** * Find a stable position for all nodes * @private diff --git a/src/graph/Node.js b/src/graph/Node.js index dede23be..b3a57295 100644 --- a/src/graph/Node.js +++ b/src/graph/Node.js @@ -30,8 +30,8 @@ function Node(properties, imagelist, grouplist, constants) { this.edges = []; // all edges connected to this node this.dynamicEdges = []; this.reroutedEdges = {}; - this.group = constants.nodes.group; + this.group = constants.nodes.group; this.fontSize = Number(constants.nodes.fontSize); this.fontFace = constants.nodes.fontFace; this.fontColor = constants.nodes.fontColor; @@ -812,7 +812,6 @@ Node.prototype._drawShape = function (ctx, shape) { ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background; - ctx[shape](this.x, this.y, this.radius); ctx.fill(); ctx.stroke(); diff --git a/src/graph/graphMixins/ManipulationMixin.js b/src/graph/graphMixins/ManipulationMixin.js index cee45a69..af4669fb 100644 --- a/src/graph/graphMixins/ManipulationMixin.js +++ b/src/graph/graphMixins/ManipulationMixin.js @@ -65,6 +65,11 @@ var manipulationMixin = { if (this.boundFunction) { this.off('select', this.boundFunction); } + if (this.edgeBeingEdited !== undefined) { + this.edgeBeingEdited._disableControlNodes(); + this.edgeBeingEdited = undefined; + this.selectedControlNode = null; + } // restore overloaded functions this._restoreOverloadedFunctions(); @@ -93,6 +98,12 @@ var manipulationMixin = { "" + ""+this.constants.labels['editNode'] +""; } + else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { + this.manipulationDiv.innerHTML += "" + + "
" + + "" + + ""+this.constants.labels['editEdge'] +""; + } if (this._selectionIsEmpty() == false) { this.manipulationDiv.innerHTML += "" + "
" + @@ -110,6 +121,10 @@ var manipulationMixin = { var editButton = document.getElementById("graph-manipulate-editNode"); editButton.onclick = this._editNode.bind(this); } + else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { + var editButton = document.getElementById("graph-manipulate-editEdge"); + editButton.onclick = this._createEditEdgeToolbar.bind(this); + } if (this._selectionIsEmpty() == false) { var deleteButton = document.getElementById("graph-manipulate-delete"); deleteButton.onclick = this._deleteSelected.bind(this); @@ -203,10 +218,106 @@ var manipulationMixin = { // redraw to show the unselect this._redraw(); + }, + /** + * create the toolbar to edit edges + * + * @private + */ + _createEditEdgeToolbar : function() { + // clear the toolbar + this._clearManipulatorBar(); + + if (this.boundFunction) { + this.off('select', this.boundFunction); + } + + this.edgeBeingEdited = this._getSelectedEdge(); + this.edgeBeingEdited._enableControlNodes(); + + this.manipulationDiv.innerHTML = "" + + "" + + "" + this.constants.labels['back'] + " " + + "
" + + "" + + "" + this.constants.labels['editEdgeDescription'] + ""; + + // bind the icon + var backButton = document.getElementById("graph-manipulate-back"); + backButton.onclick = this._createManipulatorBar.bind(this); + + // temporarily overload functions + this.cachedFunctions["_handleTouch"] = this._handleTouch; + this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; + this.cachedFunctions["_handleTap"] = this._handleTap; + this.cachedFunctions["_handleDragStart"] = this._handleDragStart; + this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; + this._handleTouch = this._selectControlNode; + this._handleTap = function () {}; + this._handleOnDrag = this._controlNodeDrag; + this._handleDragStart = function () {} + this._handleOnRelease = this._releaseControlNode; + + // redraw to show the unselect + this._redraw(); }, + + + + /** + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. + * + * @private + */ + _selectControlNode : function(pointer) { + this.edgeBeingEdited.controlNodes.from.unselect(); + this.edgeBeingEdited.controlNodes.to.unselect(); + this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y)); + if (this.selectedControlNode !== null) { + this.selectedControlNode.select(); + this.freezeSimulation = true; + } + this._redraw(); + }, + + /** + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. + * + * @private + */ + _controlNodeDrag : function(event) { + var pointer = this._getPointer(event.gesture.center); + if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { + this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); + this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); + } + this._redraw(); + }, + + _releaseControlNode : function(pointer) { + var newNode = this._getNodeAt(pointer); + if (newNode != null) { + if (this.edgeBeingEdited.controlNodes.from.selected == true) { + this._editEdge(newNode.id, this.edgeBeingEdited.to.id); + this.edgeBeingEdited.controlNodes.from.unselect(); + } + if (this.edgeBeingEdited.controlNodes.to.selected == true) { + this._editEdge(this.edgeBeingEdited.from.id, newNode.id); + this.edgeBeingEdited.controlNodes.to.unselect(); + } + } + else { + this.edgeBeingEdited._restoreControlNodes(); + } + this.freezeSimulation = false; + this._redraw(); + }, + /** * the function bound to the selection event. It checks if you want to connect a cluster and changes the description * to walk the user through the process. @@ -351,6 +462,36 @@ var manipulationMixin = { } }, + /** + * connect two nodes with a new edge. + * + * @private + */ + _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 { + alert(this.constants.labels["linkError"]); + this.moving = true; + this.start(); + } + } + else { + this.edgesData.update(defaultData); + this.moving = true; + this.start(); + } + } + }, /** * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. @@ -391,6 +532,8 @@ var manipulationMixin = { }, + + /** * delete everything in the selection * diff --git a/src/graph/graphMixins/SelectionMixin.js b/src/graph/graphMixins/SelectionMixin.js index e4aca7f6..8acb29c6 100644 --- a/src/graph/graphMixins/SelectionMixin.js +++ b/src/graph/graphMixins/SelectionMixin.js @@ -241,7 +241,7 @@ var SelectionMixin = { }, /** - * return the number of selected nodes + * return the selected node * * @returns {number} * @private @@ -255,6 +255,21 @@ var SelectionMixin = { return null; }, + /** + * return the selected edge + * + * @returns {number} + * @private + */ + _getSelectedEdge : function() { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return this.selectionObj.edges[edgeId]; + } + } + return null; + }, + /** * return the number of selected edges @@ -387,10 +402,13 @@ var SelectionMixin = { * @param {Boolean} [doNotTrigger] | ignore trigger * @private */ - _selectObject : function(object, append, doNotTrigger) { + _selectObject : function(object, append, doNotTrigger, highlightEdges) { if (doNotTrigger === undefined) { doNotTrigger = false; } + if (highlightEdges === undefined) { + highlightEdges = true; + } if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) { this._unselectAll(true); @@ -399,7 +417,7 @@ var SelectionMixin = { if (object.selected == false) { object.select(); this._addToSelection(object); - if (object instanceof Node && this.blockConnectingEdgeSelection == false) { + if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) { this._selectConnectedEdges(object); } } @@ -458,7 +476,6 @@ var SelectionMixin = { * @private */ _handleTouch : function(pointer) { - }, @@ -605,10 +622,67 @@ var SelectionMixin = { } this._selectObject(node,true,true); } + + console.log("setSelection is deprecated. Please use selectNodes instead.") + this.redraw(); }, + /** + * select zero or more nodes with the option to highlight edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + * @param {boolean} [highlightEdges] + */ + selectNodes : function(selection, highlightEdges) { + var i, iMax, id; + + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; + + // first unselect any selected node + this._unselectAll(true); + + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + + var node = this.nodes[id]; + if (!node) { + throw new RangeError('Node with id "' + id + '" not found'); + } + this._selectObject(node,true,true,highlightEdges); + } + this.redraw(); + }, + + + /** + * select zero or more edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ + selectEdges : function(selection) { + var i, iMax, id; + + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; + + // first unselect any selected node + this._unselectAll(true); + + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + + var edge = this.edges[id]; + if (!edge) { + throw new RangeError('Edge with id "' + id + '" not found'); + } + this._selectObject(edge,true,true,highlightEdges); + } + this.redraw(); + }, + /** * Validate the selection: remove ids of nodes which no longer exist * @private diff --git a/src/graph3d/Graph3d.js b/src/graph3d/Graph3d.js index 21e3f5d1..c246b380 100644 --- a/src/graph3d/Graph3d.js +++ b/src/graph3d/Graph3d.js @@ -10,6 +10,10 @@ * @param {Object} [options] */ function Graph3d(container, data, options) { + if (!(this instanceof Graph3d)) { + throw new SyntaxError('Constructor must be called with the new operator'); + } + // create variables and set default values this.containerElement = container; this.width = '400px'; diff --git a/src/timeline/Range.js b/src/timeline/Range.js index 4aa1c4fc..135d992a 100644 --- a/src/timeline/Range.js +++ b/src/timeline/Range.js @@ -18,6 +18,8 @@ function Range(body, options) { start: null, end: null, direction: 'horizontal', // 'horizontal' or 'vertical' + moveable: true, + zoomable: true, min: null, max: null, zoomMin: 10, // milliseconds @@ -25,6 +27,10 @@ function Range(body, options) { }; this.options = util.extend({}, this.defaultOptions); + this.props = { + touch: {} + }; + // drag listeners for dragging this.body.emitter.on('dragstart', this._onDragStart.bind(this)); this.body.emitter.on('drag', this._onDrag.bind(this)); @@ -57,11 +63,16 @@ Range.prototype = new Component(); * (end - start). * {Number} zoomMax Set a maximum value for * (end - start). + * {Boolean} moveable Enable moving of the range + * by dragging. True by default + * {Boolean} zoomable Enable zooming of the range + * by pinching/scrolling. True by default */ Range.prototype.setOptions = function (options) { if (options) { // copy the options that we know - util.selectiveExtend(['direction', 'min', 'max', 'zoomMin', 'zoomMax'], this.options, options); + var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable']; + util.selectiveExtend(fields, this.options, options); if ('start' in options || 'end' in options) { // apply a new range. both start and end are optional @@ -253,23 +264,21 @@ Range.conversion = function (start, end, width) { } }; -// global (private) object to store drag params -var touchParams = {}; - /** * Start dragging horizontally or vertically * @param {Event} event * @private */ Range.prototype._onDragStart = function(event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; + // 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 (touchParams.ignore) return; - - // TODO: reckon with option movable + if (!this.props.touch.allowDragging) return; - touchParams.start = this.start; - touchParams.end = this.end; + this.props.touch.start = this.start; + this.props.touch.end = this.end; if (this.body.dom.root) { this.body.dom.root.style.cursor = 'move'; @@ -277,27 +286,27 @@ Range.prototype._onDragStart = function(event) { }; /** - * Perform dragging operating. + * Perform dragging operation * @param {Event} event * @private */ Range.prototype._onDrag = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; + var direction = this.options.direction; validateDirection(direction); - // TODO: reckon with option movable - - // 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 (touchParams.ignore) return; + if (!this.props.touch.allowDragging) return; var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, - interval = (touchParams.end - touchParams.start), + interval = (this.props.touch.end - this.props.touch.start), width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height, diffRange = -delta / width * interval; - this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange); + this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange); this.body.emitter.emit('rangechange', { start: new Date(this.start), @@ -306,16 +315,17 @@ Range.prototype._onDrag = function (event) { }; /** - * Stop dragging operating. + * Stop dragging operation * @param {event} event * @private */ Range.prototype._onDragEnd = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; + // 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 (touchParams.ignore) return; - - // TODO: reckon with option movable + if (!this.props.touch.allowDragging) return; if (this.body.dom.root) { this.body.dom.root.style.cursor = 'auto'; @@ -335,7 +345,8 @@ Range.prototype._onDragEnd = function (event) { * @private */ Range.prototype._onMouseWheel = function(event) { - // TODO: reckon with option zoomable + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; // retrieve delta var delta = 0; @@ -381,17 +392,10 @@ Range.prototype._onMouseWheel = function(event) { * @private */ Range.prototype._onTouch = function (event) { - touchParams.start = this.start; - touchParams.end = this.end; - touchParams.ignore = false; - touchParams.center = null; - - // don't move the range when dragging a selected event - // TODO: it's not so neat to have to know about the state of the ItemSet - var item = ItemSet.itemFromTarget(event); - if (item && item.selected && this.options.editable) { - touchParams.ignore = true; - } + this.props.touch.start = this.start; + this.props.touch.end = this.end; + this.props.touch.allowDragging = true; + this.props.touch.center = null; }; /** @@ -399,7 +403,7 @@ Range.prototype._onTouch = function (event) { * @private */ Range.prototype._onHold = function () { - touchParams.ignore = true; + this.props.touch.allowDragging = false; }; /** @@ -408,21 +412,22 @@ Range.prototype._onHold = function () { * @private */ Range.prototype._onPinch = function (event) { - touchParams.ignore = true; + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - // TODO: reckon with option zoomable + this.props.touch.allowDragging = false; if (event.gesture.touches.length > 1) { - if (!touchParams.center) { - touchParams.center = getPointer(event.gesture.center, this.body.dom.center); + if (!this.props.touch.center) { + this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center); } var scale = 1 / event.gesture.scale, - initDate = this._pointerToDate(touchParams.center); + initDate = this._pointerToDate(this.props.touch.center); // calculate new start and end - var newStart = parseInt(initDate + (touchParams.start - initDate) * scale); - var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale); + var newStart = parseInt(initDate + (this.props.touch.start - initDate) * scale); + var newEnd = parseInt(initDate + (this.props.touch.end - initDate) * scale); // apply new range this.setRange(newStart, newEnd); diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 8993fe72..9706bc96 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -6,6 +6,10 @@ * @constructor */ function Timeline (container, items, options) { + if (!(this instanceof Timeline)) { + throw new SyntaxError('Constructor must be called with the new operator'); + } + var me = this; this.defaultOptions = { start: null, @@ -13,12 +17,11 @@ function Timeline (container, items, options) { autoResize: true, + orientation: 'bottom', width: null, height: null, maxHeight: null, minHeight: null - - // TODO: implement options moveable and zoomable }; this.options = util.deepExtend({}, this.defaultOptions); @@ -108,6 +111,12 @@ Timeline.prototype._create = function (container) { 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'); this.dom.background.className = 'vispanel background'; this.dom.backgroundVertical.className = 'vispanel background vertical'; @@ -120,6 +129,12 @@ Timeline.prototype._create = function (container) { 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'; this.dom.root.appendChild(this.dom.background); this.dom.root.appendChild(this.dom.backgroundVertical); @@ -134,8 +149,19 @@ Timeline.prototype._create = function (container) { this.dom.leftContainer.appendChild(this.dom.left); this.dom.rightContainer.appendChild(this.dom.right); + 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); + 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)); // create event listeners for all interesting events, these events will be // emitted via emitter @@ -146,8 +172,8 @@ Timeline.prototype._create = function (container) { var me = this; var events = [ - 'pinch', - //'tap', 'doubletap', 'hold', // TODO: catching the events here disables selecting an item + 'touch', 'pinch', + 'tap', 'doubletap', 'hold', 'dragstart', 'drag', 'dragend', 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox ]; @@ -172,17 +198,59 @@ Timeline.prototype._create = function (container) { right: {}, top: {}, bottom: {}, - border: {} + border: {}, + scrollTop: 0, + scrollTopMin: 0 }; + this.touch = {}; // store state information needed for touch events // attach the root panel to the provided container if (!container) throw new Error('No container provided'); container.appendChild(this.dom.root); }; +/** + * Destroy the Timeline, clean up all DOM elements and event listeners. + */ +Timeline.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; + + // cleanup hammer touch events + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + delete this.listeners[event]; + } + } + this.listeners = null; + this.hammer = null; + + // give all components the opportunity to cleanup + this.components.forEach(function (component) { + component.destroy(); + }); + + this.body = null; +}; + /** * 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. @@ -205,7 +273,7 @@ Timeline.prototype._create = function (container) { Timeline.prototype.setOptions = function (options) { if (options) { // copy the known options - var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end']; + var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation']; util.selectiveExtend(fields, this.options, options); // enable/disable autoResize @@ -268,7 +336,7 @@ Timeline.prototype.setItems = function(items) { else { // turn an array into a dataset newDataSet = new DataSet(items, { - convert: { + type: { start: 'Date', end: 'Date' } @@ -385,20 +453,22 @@ Timeline.prototype.getItemRange = function() { if (itemsData) { // calculate the minimum value of the field 'start' var minItem = itemsData.min('start'); - min = minItem ? minItem.start.valueOf() : null; + min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null; + // Note: we convert first to Date and then to number because else + // a conversion from ISODate to Number will fail // calculate maximum value of fields 'start' and 'end' var maxStartItem = itemsData.max('start'); if (maxStartItem) { - max = maxStartItem.start.valueOf(); + max = util.convert(maxStartItem.start, 'Date').valueOf(); } var maxEndItem = itemsData.max('end'); if (maxEndItem) { if (max == null) { - max = maxEndItem.end.valueOf(); + max = util.convert(maxEndItem.end, 'Date').valueOf(); } else { - max = Math.max(max, maxEndItem.end.valueOf()); + max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf()); } } } @@ -473,6 +543,8 @@ Timeline.prototype.redraw = function() { props = this.props, dom = this.dom; + if (!dom) return; // when destroyed + // update class names dom.root.className = 'vis timeline root ' + options.orientation; @@ -561,21 +633,31 @@ Timeline.prototype.redraw = function() { 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 Timeline or of the contents of the center changed + this._updateScrollTop(); + // reposition the scrollable contents - var offset; - if (options.orientation == 'top') { - offset = 0; + var offset = this.props.scrollTop; + if (options.orientation == 'bottom') { + offset += Math.max(this.props.centerContainer.height - this.props.center.height, 0); } - else { // orientation == 'bottom' - // keep the items aligned to the axis at the bottom - offset = 0;// props.centerContainer.height - props.center.height; - } - 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'; + 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; // redraw all components this.components.forEach(function (component) { @@ -640,7 +722,7 @@ Timeline.prototype._startAutoResize = function () { this._stopAutoResize(); - function checkSize() { + this._onResize = function() { if (me.options.autoResize != true) { // stop watching when the option autoResize is changed to false me._stopAutoResize(); @@ -657,12 +739,12 @@ Timeline.prototype._startAutoResize = function () { me.emit('change'); } } - } + }; - // TODO: automatically cleanup the event listener when the frame is deleted - util.addEventListener(window, 'resize', checkSize); + // add event listener to window resize + util.addEventListener(window, 'resize', this._onResize); - this.watchTimer = setInterval(checkSize, 1000); + this.watchTimer = setInterval(this._onResize, 1000); }; /** @@ -675,5 +757,99 @@ Timeline.prototype._stopAutoResize = function () { this.watchTimer = undefined; } - // TODO: remove event listener on window.resize + // remove event listener on window.resize + util.removeEventListener(window, 'resize', this._onResize); + this._onResize = null; +}; + +/** + * Start moving the timeline vertically + * @param {Event} event + * @private + */ +Timeline.prototype._onTouch = function (event) { + this.touch.allowDragging = true; +}; + +/** + * Start moving the timeline vertically + * @param {Event} event + * @private + */ +Timeline.prototype._onPinch = function (event) { + this.touch.allowDragging = false; +}; + +/** + * Start moving the timeline vertically + * @param {Event} event + * @private + */ +Timeline.prototype._onDragStart = function (event) { + this.touch.initialScrollTop = this.props.scrollTop; +}; + +/** + * Move the timeline vertically + * @param {Event} event + * @private + */ +Timeline.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; + + var delta = event.gesture.deltaY; + + var oldScrollTop = this._getScrollTop(); + var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); + + if (newScrollTop != oldScrollTop) { + this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already + } +}; + +/** + * Apply a scrollTop + * @param {Number} scrollTop + * @returns {Number} scrollTop Returns the applied scrollTop + * @private + */ +Timeline.prototype._setScrollTop = function (scrollTop) { + this.props.scrollTop = scrollTop; + this._updateScrollTop(); + return this.props.scrollTop; +}; + +/** + * Update the current scrollTop when the height of the containers has been changed + * @returns {Number} scrollTop Returns the applied scrollTop + * @private + */ +Timeline.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; + } + + // 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; + + return this.props.scrollTop; +}; + +/** + * Get the current scrollTop + * @returns {number} scrollTop + * @private + */ +Timeline.prototype._getScrollTop = function () { + return this.props.scrollTop; }; diff --git a/src/timeline/component/Component.js b/src/timeline/component/Component.js index d01a76bc..bddccf0d 100644 --- a/src/timeline/component/Component.js +++ b/src/timeline/component/Component.js @@ -28,6 +28,13 @@ Component.prototype.redraw = function() { return false; }; +/** + * Destroy the component. Cleanup DOM and event listeners + */ +Component.prototype.destroy = function() { + // should be implemented by the component +}; + /** * Test whether the component is resized since the last time _isResized() was * called. diff --git a/src/timeline/component/CurrentTime.js b/src/timeline/component/CurrentTime.js index ce08437f..3b067280 100644 --- a/src/timeline/component/CurrentTime.js +++ b/src/timeline/component/CurrentTime.js @@ -37,6 +37,16 @@ CurrentTime.prototype._create = function() { this.bar = bar; }; +/** + * Destroy the CurrentTime bar + */ +CurrentTime.prototype.destroy = function () { + this.options.showCurrentTime = false; + this.redraw(); // will remove the bar from the DOM and stop refreshing + + this.body = null; +}; + /** * Set options for the component. Options will be merged in current options. * @param {Object} options Available parameters: @@ -76,8 +86,8 @@ CurrentTime.prototype.redraw = function() { // remove the line from the DOM if (this.bar.parentNode) { this.bar.parentNode.removeChild(this.bar); - this.stop(); } + this.stop(); } return false; diff --git a/src/timeline/component/CustomTime.js b/src/timeline/component/CustomTime.js index 038d7be2..21c86e8b 100644 --- a/src/timeline/component/CustomTime.js +++ b/src/timeline/component/CustomTime.js @@ -68,6 +68,19 @@ CustomTime.prototype._create = function() { this.hammer.on('dragend', this._onDragEnd.bind(this)); }; +/** + * Destroy the CustomTime bar + */ +CustomTime.prototype.destroy = function () { + this.options.showCustomTime = false; + this.redraw(); // will remove the bar from the DOM + + this.hammer.enable(false); + this.hammer = null; + + this.body = null; +}; + /** * Repaint the component * @return {boolean} Returns true if the component is resized diff --git a/src/timeline/component/Group.js b/src/timeline/component/Group.js index 343ba185..7da12c84 100644 --- a/src/timeline/component/Group.js +++ b/src/timeline/component/Group.js @@ -16,6 +16,7 @@ function Group (groupId, data, itemSet) { height: 0 } }; + this.className = null; this.items = {}; // items filtered by groupId of this group this.visibleItems = []; // items currently visible in window @@ -49,8 +50,10 @@ Group.prototype._create = function() { 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 @@ -86,9 +89,18 @@ Group.prototype.setData = function(data) { } // update className - var className = data && data.className; - if (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); } }; @@ -164,7 +176,8 @@ Group.prototype.redraw = function(range, margin, restack) { resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized; // apply new height - foreground.style.height = height + 'px'; + this.dom.background.style.height = height + 'px'; + this.dom.foreground.style.height = height + 'px'; this.dom.label.style.height = height + 'px'; // update vertical position of items after they are re-stacked and the height of the group is calculated diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index 8e1f8274..2eee85ea 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -13,7 +13,7 @@ function ItemSet(body, options) { this.body = body; this.defaultOptions = { - type: 'box', + type: null, // 'box', 'point', 'range' orientation: 'bottom', // 'top' or 'bottom' align: 'center', // alignment of box items stack: true, @@ -50,6 +50,11 @@ function ItemSet(body, options) { // options is shared by this ItemSet and all its items this.options = util.extend({}, this.defaultOptions); + // options for getting items from the DataSet with the correct type + this.itemOptions = { + type: {start: 'Date', end: 'Date'} + }; + this.conversion = { toScreen: body.util.toScreen, toTime: body.util.toTime @@ -109,7 +114,6 @@ ItemSet.prototype = new Component(); ItemSet.types = { box: ItemBox, range: ItemRange, - rangeoverflow: ItemRangeOverflow, point: ItemPoint }; @@ -148,8 +152,10 @@ ItemSet.prototype._create = function(){ this._updateUngrouped(); // attach event listeners - // TODO: use event listeners from the rootpanel to improve performance? - this.hammer = Hammer(frame, { + // 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 }); @@ -281,6 +287,20 @@ ItemSet.prototype.markDirty = function() { this.stackDirty = true; }; +/** + * Destroy the ItemSet + */ +ItemSet.prototype.destroy = function() { + this.hide(); + this.setItems(null); + this.setGroups(null); + + this.hammer = null; + + this.body = null; + this.conversion = null; +}; + /** * Hide the component from the DOM */ @@ -430,14 +450,8 @@ ItemSet.prototype.redraw = function() { height = Math.max(height, minHeight); this.stackDirty = false; - // reposition frame - frame.style.left = asSize(options.left, ''); - frame.style.right = asSize(options.right, ''); - frame.style.top = asSize((orientation == 'top') ? '0' : ''); - frame.style.bottom = asSize((orientation == 'top') ? '' : '0'); - frame.style.width = asSize(options.width, '100%'); + // update frame height frame.style.height = asSize(height); - //frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height // calculate actual size and position this.props.top = frame.offsetTop; @@ -656,12 +670,9 @@ ItemSet.prototype._onUpdate = function(ids) { var me = this; ids.forEach(function (id) { - var itemData = me.itemsData.get(id), + var itemData = me.itemsData.get(id, me.itemOptions), item = me.items[id], - type = itemData.type || - (itemData.start && itemData.end && 'range') || - me.options.type || - 'box'; + type = itemData.type || me.options.type || (itemData.end ? 'range' : 'box'); var constructor = ItemSet.types[type]; @@ -684,6 +695,11 @@ ItemSet.prototype._onUpdate = function(ids) { 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 + '"'); } @@ -1076,16 +1092,18 @@ ItemSet.prototype._onDragEnd = function (event) { this.touchParams.itemProps.forEach(function (props) { var id = props.item.id, - itemData = me.itemsData.get(id); + itemData = me.itemsData.get(id, me.itemOptions); 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.convert['start']); + 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.convert['end']); + 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); @@ -1097,7 +1115,7 @@ ItemSet.prototype._onDragEnd = function (event) { me.options.onMove(itemData, function (itemData) { if (itemData) { // apply changes - itemData[dataset.fieldId] = id; // ensure the item contains its id (can be undefined) + itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined) changes.push(itemData); } else { @@ -1191,13 +1209,12 @@ ItemSet.prototype._onAddItem = function (event) { }; // when default type is a range, add a default end date to the new item - if (this.options.type === 'range' || this.options.type == 'rangeoverflow') { + if (this.options.type === 'range') { var end = this.body.util.toTime(x + this.props.width / 5); newItem.end = snap ? snap(end) : end; } - var id = util.randomUUID(); - newItem[this.itemsData.fieldId] = id; + newItem[this.itemsData.fieldId] = util.randomUUID(); var group = ItemSet.groupFromTarget(event); if (group) { diff --git a/src/timeline/component/TimeAxis.js b/src/timeline/component/TimeAxis.js index eadafa86..c244c0ce 100644 --- a/src/timeline/component/TimeAxis.js +++ b/src/timeline/component/TimeAxis.js @@ -73,6 +73,21 @@ TimeAxis.prototype._create = function() { this.dom.background.className = 'timeaxis background'; }; +/** + * 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); + } + + this.body = null; +}; + /** * Repaint the component * @return {boolean} Returns true if the component is resized @@ -238,14 +253,7 @@ TimeAxis.prototype._repaintMinorText = function (x, text, orientation) { label.childNodes[0].nodeValue = text; - if (orientation == 'top') { - label.style.top = this.props.majorLabelHeight + 'px'; - label.style.bottom = ''; - } - else { - label.style.top = ''; - label.style.bottom = this.props.majorLabelHeight + 'px'; - } + label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0'; label.style.left = x + 'px'; //label.title = title; // TODO: this is a heavy operation }; @@ -274,14 +282,7 @@ TimeAxis.prototype._repaintMajorText = function (x, text, orientation) { label.childNodes[0].nodeValue = text; //label.title = title; // TODO: this is a heavy operation - if (orientation == 'top') { - label.style.top = '0px'; - label.style.bottom = ''; - } - else { - label.style.top = ''; - label.style.bottom = '0px'; - } + label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px'); label.style.left = x + 'px'; }; diff --git a/src/timeline/component/css/animation.css b/src/timeline/component/css/animation.css new file mode 100644 index 00000000..0b03f57f --- /dev/null +++ b/src/timeline/component/css/animation.css @@ -0,0 +1,33 @@ +.vis.timeline.root { + /* + -webkit-transition: height .4s ease-in-out; + transition: height .4s ease-in-out; + */ +} + +.vis.timeline .vispanel { + /* + -webkit-transition: height .4s ease-in-out, top .4s ease-in-out; + transition: height .4s ease-in-out, top .4s ease-in-out; + */ +} + +.vis.timeline .axis { + /* + -webkit-transition: top .4s ease-in-out; + transition: top .4s ease-in-out; + */ +} + +/* TODO: get animation working nicely + +.vis.timeline .item { + -webkit-transition: top .4s ease-in-out; + transition: top .4s ease-in-out; +} + +.vis.timeline .item.line { + -webkit-transition: height .4s ease-in-out, top .4s ease-in-out; + transition: height .4s ease-in-out, top .4s ease-in-out; +} +/**/ \ No newline at end of file diff --git a/src/timeline/component/css/item.css b/src/timeline/component/css/item.css index 413112cf..fd248b81 100644 --- a/src/timeline/component/css/item.css +++ b/src/timeline/component/css/item.css @@ -7,11 +7,6 @@ background-color: #D5DDF6; display: inline-block; padding: 5px; - - /* TODO: enable css transitions - -webkit-transition: top .4s ease-in-out, bottom .4s ease-in-out; - transition: top .4s ease-in-out, bottom .4s ease-in-out; - /**/ } .vis.timeline .item.selected { @@ -46,15 +41,13 @@ border-radius: 4px; } -.vis.timeline .item.range, -.vis.timeline .item.rangeoverflow{ +.vis.timeline .item.range { border-style: solid; border-radius: 2px; box-sizing: border-box; } -.vis.timeline .item.range .content, -.vis.timeline .item.rangeoverflow .content { +.vis.timeline .item.range .content { position: relative; display: inline-block; } @@ -70,11 +63,6 @@ width: 0; border-left-width: 1px; border-left-style: solid; - - /* TODO: enable css transitions - -webkit-transition: height .4s ease-in-out, top .4s ease-in-out; - transition: height .4s ease-in-out, top .4s ease-in-out; - /**/ } .vis.timeline .item .content { @@ -92,8 +80,7 @@ cursor: pointer; } -.vis.timeline .item.range .drag-left, -.vis.timeline .item.rangeoverflow .drag-left { +.vis.timeline .item.range .drag-left { position: absolute; width: 24px; height: 100%; @@ -104,8 +91,7 @@ z-index: 10000; } -.vis.timeline .item.range .drag-right, -.vis.timeline .item.rangeoverflow .drag-right { +.vis.timeline .item.range .drag-right { position: absolute; width: 24px; height: 100%; diff --git a/src/timeline/component/css/itemset.css b/src/timeline/component/css/itemset.css index 3acb7a88..b8f76203 100644 --- a/src/timeline/component/css/itemset.css +++ b/src/timeline/component/css/itemset.css @@ -5,11 +5,6 @@ margin: 0; box-sizing: border-box; - - /* FIXME: get transition working for rootpanel and itemset - -webkit-transition: height 4s ease-in-out; - transition: height 4s ease-in-out; - /**/ } .vis.timeline .itemset .background, @@ -19,10 +14,6 @@ height: 100%; } -.vis.timeline .itemset.foreground { - overflow: hidden; -} - .vis.timeline .axis { position: absolute; width: 100%; @@ -31,24 +22,12 @@ z-index: 1; } -.vis.timeline .group { +.vis.timeline .foreground .group { position: relative; box-sizing: border-box; border-bottom: 1px solid #bfbfbf; } -.vis.timeline .group:last-child { +.vis.timeline .foreground .group:last-child { border-bottom: none; } - -/* -.vis.timeline.top .group { - border-top: 1px solid #bfbfbf; - border-bottom: none; -} - -.vis.timeline.bottom .group { - border-top: none; - border-bottom: 1px solid #bfbfbf; -} -*/ \ No newline at end of file diff --git a/src/timeline/component/css/panel.css b/src/timeline/component/css/panel.css index 0469e674..4c259f77 100644 --- a/src/timeline/component/css/panel.css +++ b/src/timeline/component/css/panel.css @@ -49,3 +49,23 @@ .vis.timeline .vispanel > .content { position: relative; } + +.vis.timeline .vispanel .shadow { + position: absolute; + width: 100%; + height: 1px; + box-shadow: 0 0 10px rgba(0,0,0,0.8); + /* TODO: find a nice way to ensure shadows are drawn on top of items + z-index: 1; + */ +} + +.vis.timeline .vispanel .shadow.top { + top: -1px; + left: 0; +} + +.vis.timeline .vispanel .shadow.bottom { + bottom: -1px; + left: 0; +} \ No newline at end of file diff --git a/src/timeline/component/item/ItemBox.js b/src/timeline/component/item/ItemBox.js index 149b0da2..6a067826 100644 --- a/src/timeline/component/item/ItemBox.js +++ b/src/timeline/component/item/ItemBox.js @@ -211,20 +211,19 @@ ItemBox.prototype.repositionY = function() { dot = this.dom.dot; if (orientation == 'top') { - box.style.top = (this.top || 0) + 'px'; - box.style.bottom = ''; + box.style.top = (this.top || 0) + 'px'; - line.style.top = '0'; - line.style.bottom = ''; + line.style.top = '0'; line.style.height = (this.parent.top + this.top + 1) + 'px'; + line.style.bottom = ''; } else { // orientation 'bottom' - box.style.top = ''; - box.style.bottom = (this.top || 0) + 'px'; + var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty + var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top; - line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px'; + box.style.top = (this.parent.height - this.top - this.height || 0) + 'px'; + line.style.top = (itemSetHeight - lineHeight) + 'px'; line.style.bottom = '0'; - line.style.height = ''; } dot.style.top = (-this.props.dot.height / 2) + 'px'; diff --git a/src/timeline/component/item/ItemPoint.js b/src/timeline/component/item/ItemPoint.js index 9d1664ea..ebb74c28 100644 --- a/src/timeline/component/item/ItemPoint.js +++ b/src/timeline/component/item/ItemPoint.js @@ -183,10 +183,8 @@ ItemPoint.prototype.repositionY = function() { if (orientation == 'top') { point.style.top = this.top + 'px'; - point.style.bottom = ''; } else { - point.style.top = ''; - point.style.bottom = this.top + 'px'; + point.style.top = (this.parent.height - this.top - this.height) + 'px'; } }; diff --git a/src/timeline/component/item/ItemRange.js b/src/timeline/component/item/ItemRange.js index 859245b6..98572557 100644 --- a/src/timeline/component/item/ItemRange.js +++ b/src/timeline/component/item/ItemRange.js @@ -14,6 +14,7 @@ function ItemRange (data, conversion, options) { width: 0 } }; + this.overflow = false; // if contents can overflow (css styling), this flag is set to true // validate data if (data) { @@ -107,6 +108,9 @@ ItemRange.prototype.redraw = function() { // recalculate size if (this.dirty) { + // determine from css whether this box has overflow + this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden'; + this.props.content.width = this.dom.content.offsetWidth; this.height = this.dom.box.offsetHeight; @@ -151,6 +155,7 @@ ItemRange.prototype.hide = function() { * Reposition the item horizontally * @Override */ +// TODO: delete the old function ItemRange.prototype.repositionX = function() { var props = this.props, parentWidth = this.parent.width, @@ -166,22 +171,35 @@ ItemRange.prototype.repositionX = function() { if (end > 2 * parentWidth) { end = 2 * parentWidth; } + var boxWidth = Math.max(end - start, 1); - // 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; + 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); + + 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; + } - this.left = start; - this.width = Math.max(end - start, 1); + this.left = start; + this.width = boxWidth; + } this.dom.box.style.left = this.left + 'px'; - this.dom.box.style.width = this.width + 'px'; + this.dom.box.style.width = boxWidth + 'px'; this.dom.content.style.left = contentLeft + 'px'; }; @@ -195,11 +213,9 @@ ItemRange.prototype.repositionY = function() { if (orientation == 'top') { box.style.top = this.top + 'px'; - box.style.bottom = ''; } else { - box.style.top = ''; - box.style.bottom = this.top + 'px'; + box.style.top = (this.parent.height - this.top - this.height) + 'px'; } }; diff --git a/src/timeline/component/item/ItemRangeOverflow.js b/src/timeline/component/item/ItemRangeOverflow.js deleted file mode 100644 index 3d5d474e..00000000 --- a/src/timeline/component/item/ItemRangeOverflow.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @constructor ItemRangeOverflow - * @extends ItemRange - * @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 ItemRangeOverflow (data, conversion, options) { - this.props = { - content: { - left: 0, - width: 0 - } - }; - - ItemRange.call(this, data, conversion, options); -} - -ItemRangeOverflow.prototype = new ItemRange (null, null, null); - -ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow'; - -/** - * Reposition the item horizontally - * @Override - */ -ItemRangeOverflow.prototype.repositionX = function() { - var parentWidth = this.parent.width, - start = this.conversion.toScreen(this.data.start), - end = this.conversion.toScreen(this.data.end), - 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; - } - - // when range exceeds left of the window, position the contents at the left of the visible area - contentLeft = Math.max(-start, 0); - - this.left = start; - var boxWidth = Math.max(end - start, 1); - 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 - - this.dom.box.style.left = this.left + 'px'; - this.dom.box.style.width = boxWidth + 'px'; - this.dom.content.style.left = contentLeft + 'px'; -}; diff --git a/src/util.js b/src/util.js index e1d7c324..51149ae1 100644 --- a/src/util.js +++ b/src/util.js @@ -359,8 +359,7 @@ util.convert = function(object, type) { } default: - throw new Error('Cannot convert object of type ' + util.getType(object) + - ' to type "' + type + '"'); + throw new Error('Unknown type "' + type + '"'); } }; @@ -1055,4 +1054,4 @@ util._mergeOptions = function (mergeTarget, options, option) { } } } -} \ No newline at end of file +} diff --git a/test/dataset.js b/test/dataset.js index 84fbe5ca..3e18923c 100644 --- a/test/dataset.js +++ b/test/dataset.js @@ -6,7 +6,7 @@ var assert = require('assert'), var now = new Date(); var data = new DataSet({ - convert: { + type: { start: 'Date', end: 'Date' } @@ -31,6 +31,7 @@ items.forEach(function (item) { var sort = function (a, b) { return a.id > b.id; }; + assert.deepEqual(data.get({ fields: ['id', 'content'] }).sort(sort), [ @@ -44,7 +45,7 @@ assert.deepEqual(data.get({ // convert dates assert.deepEqual(data.get({ fields: ['id', 'start'], - convert: {start: 'Number'} + type: {start: 'Number'} }).sort(sort), [ {id: 1, start: now.valueOf()}, {id: 2, start: now.valueOf()}, @@ -56,7 +57,7 @@ assert.deepEqual(data.get({ // get a single item assert.deepEqual(data.get(1, { fields: ['id', 'start'], - convert: {start: 'ISODate'} + type: {start: 'ISODate'} }), { id: 1, start: now.toISOString() @@ -150,17 +151,7 @@ data.clear(); data.add({content: 'Item 1'}); data.add({content: 'Item 2'}); -assert.strictEqual(data.get()[0].id, undefined); -assert.deepEqual((data.get({"showInternalIds": true})[0].id == undefined),false); -assert.deepEqual(data.isInternalId(data.get({"showInternalIds": true})[0].id), true); -assert.deepEqual((data.get()[0].id == undefined), true); - -// check if the global setting is applied correctly -var data = new DataSet({showInternalIds: true}); -data.add({content: 'Item 1'}); -assert.deepEqual((data.get()[0].id == undefined), false); -assert.deepEqual(data.isInternalId(data.get()[0].id), true); -assert.deepEqual((data.get({"showInternalIds": false})[0].id == undefined),true); +assert.notStrictEqual(data.get()[0].id, undefined); // create a dataset with initial data var data = new DataSet([ diff --git a/test/timeline.html b/test/timeline.html index 83e49b32..7486f353 100644 --- a/test/timeline.html +++ b/test/timeline.html @@ -28,10 +28,10 @@ @@ -58,9 +58,9 @@ // create a dataset with items var now = moment().minutes(0).seconds(0).milliseconds(0); var items = new vis.DataSet({ - convert: { - start: 'Date', - end: 'Date' + type: { + start: 'ISODate', + end: 'ISODate' }, fieldId: '_id' }); @@ -70,7 +70,7 @@ {_id: 2, content: 'item 2', start: now.clone().add('days', -2).toDate() }, {_id: 3, content: 'item 3', start: now.clone().add('days', 2).toDate()}, { - _id: 4, content: 'item 4', + _id: 4, content: 'item 4 ', start: now.clone().add('days', 0).toDate(), end: now.clone().add('days', 7).toDate() }, diff --git a/test/timeline_groups.html b/test/timeline_groups.html index 7e7b78f0..de2f5b16 100644 --- a/test/timeline_groups.html +++ b/test/timeline_groups.html @@ -31,10 +31,10 @@ @@ -72,6 +72,7 @@ // create visualization var container = document.getElementById('visualization'); var options = { + //orientation: 'top', editable: { add: true, remove: true,