vis-rails 0.0.6 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/lib/vis/rails/version.rb +1 -1
  3. data/vendor/assets/javascripts/vis.js +2 -9
  4. data/vendor/assets/vis/DataSet.js +17 -9
  5. data/vendor/assets/vis/graph/Edge.js +49 -24
  6. data/vendor/assets/vis/graph/Graph.js +268 -64
  7. data/vendor/assets/vis/graph/Groups.js +1 -1
  8. data/vendor/assets/vis/graph/Node.js +18 -67
  9. data/vendor/assets/vis/graph/Popup.js +40 -13
  10. data/vendor/assets/vis/graph/css/graph-navigation.css +18 -14
  11. data/vendor/assets/vis/graph/graphMixins/ClusterMixin.js +7 -5
  12. data/vendor/assets/vis/graph/graphMixins/HierarchicalLayoutMixin.js +20 -5
  13. data/vendor/assets/vis/graph/graphMixins/ManipulationMixin.js +33 -33
  14. data/vendor/assets/vis/graph/graphMixins/MixinLoader.js +30 -32
  15. data/vendor/assets/vis/graph/graphMixins/NavigationMixin.js +33 -1
  16. data/vendor/assets/vis/graph/graphMixins/SectorsMixin.js +2 -2
  17. data/vendor/assets/vis/graph/graphMixins/SelectionMixin.js +72 -60
  18. data/vendor/assets/vis/graph/graphMixins/physics/BarnesHut.js +43 -18
  19. data/vendor/assets/vis/graph/graphMixins/physics/HierarchialRepulsion.js +8 -8
  20. data/vendor/assets/vis/graph/graphMixins/physics/PhysicsMixin.js +309 -129
  21. data/vendor/assets/vis/graph/graphMixins/physics/Repulsion.js +10 -10
  22. data/vendor/assets/vis/module/exports.js +1 -2
  23. data/vendor/assets/vis/module/header.js +2 -2
  24. data/vendor/assets/vis/timeline/Range.js +53 -93
  25. data/vendor/assets/vis/timeline/Timeline.js +328 -224
  26. data/vendor/assets/vis/timeline/component/Component.js +17 -95
  27. data/vendor/assets/vis/timeline/component/CurrentTime.js +54 -59
  28. data/vendor/assets/vis/timeline/component/CustomTime.js +55 -83
  29. data/vendor/assets/vis/timeline/component/Group.js +398 -75
  30. data/vendor/assets/vis/timeline/component/ItemSet.js +662 -403
  31. data/vendor/assets/vis/timeline/component/Panel.js +118 -60
  32. data/vendor/assets/vis/timeline/component/RootPanel.js +80 -132
  33. data/vendor/assets/vis/timeline/component/TimeAxis.js +191 -277
  34. data/vendor/assets/vis/timeline/component/css/item.css +16 -23
  35. data/vendor/assets/vis/timeline/component/css/itemset.css +25 -4
  36. data/vendor/assets/vis/timeline/component/css/labelset.css +34 -0
  37. data/vendor/assets/vis/timeline/component/css/panel.css +15 -1
  38. data/vendor/assets/vis/timeline/component/css/timeaxis.css +8 -8
  39. data/vendor/assets/vis/timeline/component/item/Item.js +48 -26
  40. data/vendor/assets/vis/timeline/component/item/ItemBox.js +156 -230
  41. data/vendor/assets/vis/timeline/component/item/ItemPoint.js +118 -166
  42. data/vendor/assets/vis/timeline/component/item/ItemRange.js +135 -187
  43. data/vendor/assets/vis/timeline/component/item/ItemRangeOverflow.js +29 -92
  44. data/vendor/assets/vis/timeline/stack.js +112 -0
  45. data/vendor/assets/vis/util.js +136 -38
  46. metadata +4 -18
  47. data/vendor/assets/vis/.gitignore +0 -1
  48. data/vendor/assets/vis/EventBus.js +0 -89
  49. data/vendor/assets/vis/events.js +0 -116
  50. data/vendor/assets/vis/graph/ClusterMixin.js +0 -1019
  51. data/vendor/assets/vis/graph/NavigationMixin.js +0 -245
  52. data/vendor/assets/vis/graph/SectorsMixin.js +0 -547
  53. data/vendor/assets/vis/graph/SelectionMixin.js +0 -515
  54. data/vendor/assets/vis/graph/img/downarrow.png +0 -0
  55. data/vendor/assets/vis/graph/img/leftarrow.png +0 -0
  56. data/vendor/assets/vis/graph/img/rightarrow.png +0 -0
  57. data/vendor/assets/vis/graph/img/uparrow.png +0 -0
  58. data/vendor/assets/vis/timeline/Controller.js +0 -183
  59. data/vendor/assets/vis/timeline/Stack.js +0 -190
  60. data/vendor/assets/vis/timeline/component/ContentPanel.js +0 -113
  61. data/vendor/assets/vis/timeline/component/GroupSet.js +0 -580
  62. data/vendor/assets/vis/timeline/component/css/groupset.css +0 -59
@@ -10,8 +10,8 @@ var repulsionMixin = {
10
10
  * This field is linearly approximated.
11
11
  *
12
12
  * @private
13
- */
14
- _calculateNodeForces : function() {
13
+ */
14
+ _calculateNodeForces: function () {
15
15
  var dx, dy, angle, distance, fx, fy, combinedClusterSize,
16
16
  repulsingForce, node1, node2, i, j;
17
17
 
@@ -19,8 +19,8 @@ var repulsionMixin = {
19
19
  var nodeIndices = this.calculationNodeIndices;
20
20
 
21
21
  // approximation constants
22
- var a_base = -2/3;
23
- var b = 4/3;
22
+ var a_base = -2 / 3;
23
+ var b = 4 / 3;
24
24
 
25
25
  // repulsing forces between nodes
26
26
  var nodeDistance = this.constants.physics.repulsion.nodeDistance;
@@ -28,9 +28,9 @@ var repulsionMixin = {
28
28
 
29
29
  // we loop from i over all but the last entree in the array
30
30
  // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
31
- for (i = 0; i < nodeIndices.length-1; i++) {
31
+ for (i = 0; i < nodeIndices.length - 1; i++) {
32
32
  node1 = nodes[nodeIndices[i]];
33
- for (j = i+1; j < nodeIndices.length; j++) {
33
+ for (j = i + 1; j < nodeIndices.length; j++) {
34
34
  node2 = nodes[nodeIndices[j]];
35
35
  combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
36
36
 
@@ -40,8 +40,8 @@ var repulsionMixin = {
40
40
 
41
41
  minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
42
42
  var a = a_base / minimumDistance;
43
- if (distance < 2*minimumDistance) {
44
- if (distance < 0.5*minimumDistance) {
43
+ if (distance < 2 * minimumDistance) {
44
+ if (distance < 0.5 * minimumDistance) {
45
45
  repulsingForce = 1.0;
46
46
  }
47
47
  else {
@@ -50,7 +50,7 @@ var repulsionMixin = {
50
50
 
51
51
  // amplify the repulsion for clusters.
52
52
  repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
53
- repulsingForce = repulsingForce/distance;
53
+ repulsingForce = repulsingForce / distance;
54
54
 
55
55
  fx = dx * repulsingForce;
56
56
  fy = dy * repulsingForce;
@@ -63,4 +63,4 @@ var repulsionMixin = {
63
63
  }
64
64
  }
65
65
  }
66
- }
66
+ };
@@ -4,11 +4,10 @@
4
4
  var vis = {
5
5
  util: util,
6
6
 
7
- Controller: Controller,
8
7
  DataSet: DataSet,
9
8
  DataView: DataView,
10
9
  Range: Range,
11
- Stack: Stack,
10
+ stack: stack,
12
11
  TimeStep: TimeStep,
13
12
 
14
13
  components: {
@@ -4,8 +4,8 @@
4
4
  *
5
5
  * A dynamic, browser-based visualization library.
6
6
  *
7
- * @version 0.6.1
8
- * @date 2014-03-06e
7
+ * @version @@version
8
+ * @date @@date
9
9
  *
10
10
  * @license
11
11
  * Copyright (C) 2011-2014 Almende B.V, http://almende.com
@@ -3,20 +3,39 @@
3
3
  * A Range controls a numeric range with a start and end value.
4
4
  * The Range adjusts the range based on mouse events or programmatic changes,
5
5
  * and triggers events when the range is changing or has been changed.
6
- * @param {Object} [options] See description at Range.setOptions
7
- * @extends Controller
6
+ * @param {RootPanel} root Root panel, used to subscribe to events
7
+ * @param {Panel} parent Parent panel, used to attach to the DOM
8
+ * @param {Object} [options] See description at Range.setOptions
8
9
  */
9
- function Range(options) {
10
+ function Range(root, parent, options) {
10
11
  this.id = util.randomUUID();
11
12
  this.start = null; // Number
12
13
  this.end = null; // Number
13
14
 
15
+ this.root = root;
16
+ this.parent = parent;
14
17
  this.options = options || {};
15
18
 
19
+ // drag listeners for dragging
20
+ this.root.on('dragstart', this._onDragStart.bind(this));
21
+ this.root.on('drag', this._onDrag.bind(this));
22
+ this.root.on('dragend', this._onDragEnd.bind(this));
23
+
24
+ // ignore dragging when holding
25
+ this.root.on('hold', this._onHold.bind(this));
26
+
27
+ // mouse wheel for zooming
28
+ this.root.on('mousewheel', this._onMouseWheel.bind(this));
29
+ this.root.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
30
+
31
+ // pinch to zoom
32
+ this.root.on('touch', this._onTouch.bind(this));
33
+ this.root.on('pinch', this._onPinch.bind(this));
34
+
16
35
  this.setOptions(options);
17
36
  }
18
37
 
19
- // extend the Range prototype with an event emitter mixin
38
+ // turn Range into an event emitter
20
39
  Emitter(Range.prototype);
21
40
 
22
41
  /**
@@ -49,59 +68,6 @@ function validateDirection (direction) {
49
68
  }
50
69
  }
51
70
 
52
- /**
53
- * Add listeners for mouse and touch events to the component
54
- * @param {Controller} controller
55
- * @param {Component} component Should be a rootpanel
56
- * @param {String} event Available events: 'move', 'zoom'
57
- * @param {String} direction Available directions: 'horizontal', 'vertical'
58
- */
59
- Range.prototype.subscribe = function (controller, component, event, direction) {
60
- var me = this;
61
-
62
- if (event == 'move') {
63
- // drag start listener
64
- controller.on('dragstart', function (event) {
65
- me._onDragStart(event, component);
66
- });
67
-
68
- // drag listener
69
- controller.on('drag', function (event) {
70
- me._onDrag(event, component, direction);
71
- });
72
-
73
- // drag end listener
74
- controller.on('dragend', function (event) {
75
- me._onDragEnd(event, component);
76
- });
77
-
78
- // ignore dragging when holding
79
- controller.on('hold', function (event) {
80
- me._onHold();
81
- });
82
- }
83
- else if (event == 'zoom') {
84
- // mouse wheel
85
- function mousewheel (event) {
86
- me._onMouseWheel(event, component, direction);
87
- }
88
- controller.on('mousewheel', mousewheel);
89
- controller.on('DOMMouseScroll', mousewheel); // For FF
90
-
91
- // pinch
92
- controller.on('touch', function (event) {
93
- me._onTouch(event);
94
- });
95
- controller.on('pinch', function (event) {
96
- me._onPinch(event, component, direction);
97
- });
98
- }
99
- else {
100
- throw new TypeError('Unknown event "' + event + '". ' +
101
- 'Choose "move" or "zoom".');
102
- }
103
- };
104
-
105
71
  /**
106
72
  * Set a new start and end range
107
73
  * @param {Number} [start]
@@ -111,8 +77,8 @@ Range.prototype.setRange = function(start, end) {
111
77
  var changed = this._applyRange(start, end);
112
78
  if (changed) {
113
79
  var params = {
114
- start: this.start,
115
- end: this.end
80
+ start: new Date(this.start),
81
+ end: new Date(this.end)
116
82
  };
117
83
  this.emit('rangechange', params);
118
84
  this.emit('rangechanged', params);
@@ -280,10 +246,9 @@ var touchParams = {};
280
246
  /**
281
247
  * Start dragging horizontally or vertically
282
248
  * @param {Event} event
283
- * @param {Object} component
284
249
  * @private
285
250
  */
286
- Range.prototype._onDragStart = function(event, component) {
251
+ Range.prototype._onDragStart = function(event) {
287
252
  // refuse to drag when we where pinching to prevent the timeline make a jump
288
253
  // when releasing the fingers in opposite order from the touch screen
289
254
  if (touchParams.ignore) return;
@@ -293,7 +258,7 @@ Range.prototype._onDragStart = function(event, component) {
293
258
  touchParams.start = this.start;
294
259
  touchParams.end = this.end;
295
260
 
296
- var frame = component.frame;
261
+ var frame = this.parent.frame;
297
262
  if (frame) {
298
263
  frame.style.cursor = 'move';
299
264
  }
@@ -302,11 +267,10 @@ Range.prototype._onDragStart = function(event, component) {
302
267
  /**
303
268
  * Perform dragging operating.
304
269
  * @param {Event} event
305
- * @param {Component} component
306
- * @param {String} direction 'horizontal' or 'vertical'
307
270
  * @private
308
271
  */
309
- Range.prototype._onDrag = function (event, component, direction) {
272
+ Range.prototype._onDrag = function (event) {
273
+ var direction = this.options.direction;
310
274
  validateDirection(direction);
311
275
 
312
276
  // TODO: reckon with option movable
@@ -318,38 +282,37 @@ Range.prototype._onDrag = function (event, component, direction) {
318
282
 
319
283
  var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
320
284
  interval = (touchParams.end - touchParams.start),
321
- width = (direction == 'horizontal') ? component.width : component.height,
285
+ width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
322
286
  diffRange = -delta / width * interval;
323
287
 
324
288
  this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
325
289
 
326
290
  this.emit('rangechange', {
327
- start: this.start,
328
- end: this.end
291
+ start: new Date(this.start),
292
+ end: new Date(this.end)
329
293
  });
330
294
  };
331
295
 
332
296
  /**
333
297
  * Stop dragging operating.
334
298
  * @param {event} event
335
- * @param {Component} component
336
299
  * @private
337
300
  */
338
- Range.prototype._onDragEnd = function (event, component) {
301
+ Range.prototype._onDragEnd = function (event) {
339
302
  // refuse to drag when we where pinching to prevent the timeline make a jump
340
303
  // when releasing the fingers in opposite order from the touch screen
341
304
  if (touchParams.ignore) return;
342
305
 
343
306
  // TODO: reckon with option movable
344
307
 
345
- if (component.frame) {
346
- component.frame.style.cursor = 'auto';
308
+ if (this.parent.frame) {
309
+ this.parent.frame.style.cursor = 'auto';
347
310
  }
348
311
 
349
312
  // fire a rangechanged event
350
313
  this.emit('rangechanged', {
351
- start: this.start,
352
- end: this.end
314
+ start: new Date(this.start),
315
+ end: new Date(this.end)
353
316
  });
354
317
  };
355
318
 
@@ -357,13 +320,9 @@ Range.prototype._onDragEnd = function (event, component) {
357
320
  * Event handler for mouse wheel event, used to zoom
358
321
  * Code from http://adomas.org/javascript-mouse-wheel/
359
322
  * @param {Event} event
360
- * @param {Component} component
361
- * @param {String} direction 'horizontal' or 'vertical'
362
323
  * @private
363
324
  */
364
- Range.prototype._onMouseWheel = function(event, component, direction) {
365
- validateDirection(direction);
366
-
325
+ Range.prototype._onMouseWheel = function(event) {
367
326
  // TODO: reckon with option zoomable
368
327
 
369
328
  // retrieve delta
@@ -394,8 +353,8 @@ Range.prototype._onMouseWheel = function(event, component, direction) {
394
353
 
395
354
  // calculate center, the date to zoom around
396
355
  var gesture = util.fakeGesture(this, event),
397
- pointer = getPointer(gesture.center, component.frame),
398
- pointerDate = this._pointerToDate(component, direction, pointer);
356
+ pointer = getPointer(gesture.center, this.parent.frame),
357
+ pointerDate = this._pointerToDate(pointer);
399
358
 
400
359
  this.zoom(scale, pointerDate);
401
360
  }
@@ -434,24 +393,23 @@ Range.prototype._onHold = function () {
434
393
  /**
435
394
  * Handle pinch event
436
395
  * @param {Event} event
437
- * @param {Component} component
438
- * @param {String} direction 'horizontal' or 'vertical'
439
396
  * @private
440
397
  */
441
- Range.prototype._onPinch = function (event, component, direction) {
398
+ Range.prototype._onPinch = function (event) {
399
+ var direction = this.options.direction;
442
400
  touchParams.ignore = true;
443
401
 
444
402
  // TODO: reckon with option zoomable
445
403
 
446
404
  if (event.gesture.touches.length > 1) {
447
405
  if (!touchParams.center) {
448
- touchParams.center = getPointer(event.gesture.center, component.frame);
406
+ touchParams.center = getPointer(event.gesture.center, this.parent.frame);
449
407
  }
450
408
 
451
409
  var scale = 1 / event.gesture.scale,
452
- initDate = this._pointerToDate(component, direction, touchParams.center),
453
- center = getPointer(event.gesture.center, component.frame),
454
- date = this._pointerToDate(component, direction, center),
410
+ initDate = this._pointerToDate(touchParams.center),
411
+ center = getPointer(event.gesture.center, this.parent.frame),
412
+ date = this._pointerToDate(this.parent, center),
455
413
  delta = date - initDate; // TODO: utilize delta
456
414
 
457
415
  // calculate new start and end
@@ -465,21 +423,23 @@ Range.prototype._onPinch = function (event, component, direction) {
465
423
 
466
424
  /**
467
425
  * Helper function to calculate the center date for zooming
468
- * @param {Component} component
469
426
  * @param {{x: Number, y: Number}} pointer
470
- * @param {String} direction 'horizontal' or 'vertical'
471
427
  * @return {number} date
472
428
  * @private
473
429
  */
474
- Range.prototype._pointerToDate = function (component, direction, pointer) {
430
+ Range.prototype._pointerToDate = function (pointer) {
475
431
  var conversion;
432
+ var direction = this.options.direction;
433
+
434
+ validateDirection(direction);
435
+
476
436
  if (direction == 'horizontal') {
477
- var width = component.width;
437
+ var width = this.parent.width;
478
438
  conversion = this.conversion(width);
479
439
  return pointer.x / conversion.scale + conversion.offset;
480
440
  }
481
441
  else {
482
- var height = component.height;
442
+ var height = this.parent.height;
483
443
  conversion = this.conversion(height);
484
444
  return pointer.y / conversion.scale + conversion.offset;
485
445
  }
@@ -6,12 +6,24 @@
6
6
  * @constructor
7
7
  */
8
8
  function Timeline (container, items, options) {
9
+ // validate arguments
10
+ if (!container) throw new Error('No container element provided');
11
+
9
12
  var me = this;
10
13
  var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
11
14
  this.options = {
12
15
  orientation: 'bottom',
16
+ direction: 'horizontal', // 'horizontal' or 'vertical'
13
17
  autoResize: true,
14
- editable: false,
18
+ stack: true,
19
+
20
+ editable: {
21
+ updateTime: false,
22
+ updateGroup: false,
23
+ add: false,
24
+ remove: false
25
+ },
26
+
15
27
  selectable: true,
16
28
  snap: null, // will be specified after timeaxis is created
17
29
 
@@ -27,6 +39,14 @@ function Timeline (container, items, options) {
27
39
  showCurrentTime: false,
28
40
  showCustomTime: false,
29
41
 
42
+ type: 'box',
43
+ align: 'center',
44
+ margin: {
45
+ axis: 20,
46
+ item: 10
47
+ },
48
+ padding: 5,
49
+
30
50
  onAdd: function (item, callback) {
31
51
  callback(item);
32
52
  },
@@ -38,112 +58,205 @@ function Timeline (container, items, options) {
38
58
  },
39
59
  onRemove: function (item, callback) {
40
60
  callback(item);
41
- }
42
- };
61
+ },
43
62
 
44
- // controller
45
- this.controller = new Controller();
63
+ toScreen: me._toScreen.bind(me),
64
+ toTime: me._toTime.bind(me)
65
+ };
46
66
 
47
67
  // root panel
48
- if (!container) {
49
- throw new Error('No container element provided');
50
- }
51
- var rootOptions = Object.create(this.options);
52
- rootOptions.height = function () {
53
- // TODO: change to height
54
- if (me.options.height) {
55
- // fixed height
56
- return me.options.height;
57
- }
58
- else {
59
- // auto height
60
- return (me.timeaxis.height + me.content.height) + 'px';
68
+ var rootOptions = util.extend(Object.create(this.options), {
69
+ height: function () {
70
+ if (me.options.height) {
71
+ // fixed height
72
+ return me.options.height;
73
+ }
74
+ else {
75
+ // auto height
76
+ // TODO: implement a css based solution to automatically have the right hight
77
+ return (me.timeAxis.height + me.contentPanel.height) + 'px';
78
+ }
61
79
  }
62
- };
80
+ });
63
81
  this.rootPanel = new RootPanel(container, rootOptions);
64
- this.controller.add(this.rootPanel);
65
82
 
66
83
  // single select (or unselect) when tapping an item
67
- this.controller.on('tap', this._onSelectItem.bind(this));
84
+ this.rootPanel.on('tap', this._onSelectItem.bind(this));
68
85
 
69
86
  // multi select when holding mouse/touch, or on ctrl+click
70
- this.controller.on('hold', this._onMultiSelectItem.bind(this));
87
+ this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
71
88
 
72
89
  // add item on doubletap
73
- this.controller.on('doubletap', this._onAddItem.bind(this));
90
+ this.rootPanel.on('doubletap', this._onAddItem.bind(this));
74
91
 
75
- // item panel
76
- var itemOptions = Object.create(this.options);
77
- itemOptions.left = function () {
78
- return me.labelPanel.width;
79
- };
80
- itemOptions.width = function () {
81
- return me.rootPanel.width - me.labelPanel.width;
82
- };
83
- itemOptions.top = null;
84
- itemOptions.height = null;
85
- this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
86
- this.controller.add(this.itemPanel);
87
-
88
- // label panel
89
- var labelOptions = Object.create(this.options);
90
- labelOptions.top = null;
91
- labelOptions.left = null;
92
- labelOptions.height = null;
93
- labelOptions.width = function () {
94
- if (me.content && typeof me.content.getLabelsWidth === 'function') {
95
- return me.content.getLabelsWidth();
96
- }
97
- else {
98
- return 0;
92
+ // side panel
93
+ var sideOptions = util.extend(Object.create(this.options), {
94
+ top: function () {
95
+ return (sideOptions.orientation == 'top') ? '0' : '';
96
+ },
97
+ bottom: function () {
98
+ return (sideOptions.orientation == 'top') ? '' : '0';
99
+ },
100
+ left: '0',
101
+ right: null,
102
+ height: '100%',
103
+ width: function () {
104
+ if (me.itemSet) {
105
+ return me.itemSet.getLabelsWidth();
106
+ }
107
+ else {
108
+ return 0;
109
+ }
110
+ },
111
+ className: function () {
112
+ return 'side' + (me.groupsData ? '' : ' hidden');
99
113
  }
100
- };
101
- this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
102
- this.controller.add(this.labelPanel);
114
+ });
115
+ this.sidePanel = new Panel(sideOptions);
116
+ this.rootPanel.appendChild(this.sidePanel);
117
+
118
+ // main panel (contains time axis and itemsets)
119
+ var mainOptions = util.extend(Object.create(this.options), {
120
+ left: function () {
121
+ // we align left to enable a smooth resizing of the window
122
+ return me.sidePanel.width;
123
+ },
124
+ right: null,
125
+ height: '100%',
126
+ width: function () {
127
+ return me.rootPanel.width - me.sidePanel.width;
128
+ },
129
+ className: 'main'
130
+ });
131
+ this.mainPanel = new Panel(mainOptions);
132
+ this.rootPanel.appendChild(this.mainPanel);
103
133
 
104
134
  // range
135
+ // TODO: move range inside rootPanel?
105
136
  var rangeOptions = Object.create(this.options);
106
- this.range = new Range(rangeOptions);
137
+ this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
107
138
  this.range.setRange(
108
139
  now.clone().add('days', -3).valueOf(),
109
140
  now.clone().add('days', 4).valueOf()
110
141
  );
111
-
112
- this.range.subscribe(this.controller, this.rootPanel, 'move', 'horizontal');
113
- this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal');
114
142
  this.range.on('rangechange', function (properties) {
115
- var force = true;
116
- me.controller.emit('rangechange', properties);
117
- me.controller.emit('request-reflow', force);
143
+ me.rootPanel.repaint();
144
+ me.emit('rangechange', properties);
118
145
  });
119
146
  this.range.on('rangechanged', function (properties) {
120
- var force = true;
121
- me.controller.emit('rangechanged', properties);
122
- me.controller.emit('request-reflow', force);
147
+ me.rootPanel.repaint();
148
+ me.emit('rangechanged', properties);
149
+ });
150
+
151
+ // panel with time axis
152
+ var timeAxisOptions = util.extend(Object.create(rootOptions), {
153
+ range: this.range,
154
+ left: null,
155
+ top: null,
156
+ width: null,
157
+ height: null
158
+ });
159
+ this.timeAxis = new TimeAxis(timeAxisOptions);
160
+ this.timeAxis.setRange(this.range);
161
+ this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
162
+ this.mainPanel.appendChild(this.timeAxis);
163
+
164
+ // content panel (contains itemset(s))
165
+ var contentOptions = util.extend(Object.create(this.options), {
166
+ top: function () {
167
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
168
+ },
169
+ bottom: function () {
170
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
171
+ },
172
+ left: null,
173
+ right: null,
174
+ height: null,
175
+ width: null,
176
+ className: 'content'
177
+ });
178
+ this.contentPanel = new Panel(contentOptions);
179
+ this.mainPanel.appendChild(this.contentPanel);
180
+
181
+ // content panel (contains the vertical lines of box items)
182
+ var backgroundOptions = util.extend(Object.create(this.options), {
183
+ top: function () {
184
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
185
+ },
186
+ bottom: function () {
187
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
188
+ },
189
+ left: null,
190
+ right: null,
191
+ height: function () {
192
+ return me.contentPanel.height;
193
+ },
194
+ width: null,
195
+ className: 'background'
196
+ });
197
+ this.backgroundPanel = new Panel(backgroundOptions);
198
+ this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel);
199
+
200
+ // panel with axis holding the dots of item boxes
201
+ var axisPanelOptions = util.extend(Object.create(rootOptions), {
202
+ left: 0,
203
+ top: function () {
204
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
205
+ },
206
+ bottom: function () {
207
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
208
+ },
209
+ width: '100%',
210
+ height: 0,
211
+ className: 'axis'
123
212
  });
213
+ this.axisPanel = new Panel(axisPanelOptions);
214
+ this.mainPanel.appendChild(this.axisPanel);
124
215
 
125
- // time axis
126
- var timeaxisOptions = Object.create(rootOptions);
127
- timeaxisOptions.range = this.range;
128
- timeaxisOptions.left = null;
129
- timeaxisOptions.top = null;
130
- timeaxisOptions.width = '100%';
131
- timeaxisOptions.height = null;
132
- this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
133
- this.timeaxis.setRange(this.range);
134
- this.controller.add(this.timeaxis);
135
- this.options.snap = this.timeaxis.snap.bind(this.timeaxis);
216
+ // content panel (contains itemset(s))
217
+ var sideContentOptions = util.extend(Object.create(this.options), {
218
+ top: function () {
219
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
220
+ },
221
+ bottom: function () {
222
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
223
+ },
224
+ left: null,
225
+ right: null,
226
+ height: null,
227
+ width: null,
228
+ className: 'side-content'
229
+ });
230
+ this.sideContentPanel = new Panel(sideContentOptions);
231
+ this.sidePanel.appendChild(this.sideContentPanel);
136
232
 
137
233
  // current time bar
138
- this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
139
- this.controller.add(this.currenttime);
234
+ // Note: time bar will be attached in this.setOptions when selected
235
+ this.currentTime = new CurrentTime(this.range, rootOptions);
140
236
 
141
237
  // custom time bar
142
- this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
143
- this.controller.add(this.customtime);
238
+ // Note: time bar will be attached in this.setOptions when selected
239
+ this.customTime = new CustomTime(rootOptions);
240
+ this.customTime.on('timechange', function (time) {
241
+ me.emit('timechange', time);
242
+ });
243
+ this.customTime.on('timechanged', function (time) {
244
+ me.emit('timechanged', time);
245
+ });
144
246
 
145
- // create groupset
146
- this.setGroups(null);
247
+ // itemset containing items and groups
248
+ var itemOptions = util.extend(Object.create(this.options), {
249
+ left: null,
250
+ right: null,
251
+ top: null,
252
+ bottom: null,
253
+ width: null,
254
+ height: null
255
+ });
256
+ this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, this.sideContentPanel, itemOptions);
257
+ this.itemSet.setRange(this.range);
258
+ this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
259
+ this.contentPanel.appendChild(this.itemSet);
147
260
 
148
261
  this.itemsData = null; // DataSet
149
262
  this.groupsData = null; // DataSet
@@ -153,30 +266,14 @@ function Timeline (container, items, options) {
153
266
  this.setOptions(options);
154
267
  }
155
268
 
156
- // create itemset and groupset
269
+ // create itemset
157
270
  if (items) {
158
271
  this.setItems(items);
159
272
  }
160
273
  }
161
274
 
162
- /**
163
- * Add an event listener to the timeline
164
- * @param {String} event Available events: select, rangechange, rangechanged,
165
- * timechange, timechanged
166
- * @param {function} callback
167
- */
168
- Timeline.prototype.on = function on (event, callback) {
169
- this.controller.on(event, callback);
170
- };
171
-
172
- /**
173
- * Add an event listener from the timeline
174
- * @param {String} event
175
- * @param {function} callback
176
- */
177
- Timeline.prototype.off = function off (event, callback) {
178
- this.controller.off(event, callback);
179
- };
275
+ // turn Timeline into an event emitter
276
+ Emitter(Timeline.prototype);
180
277
 
181
278
  /**
182
279
  * Set options
@@ -185,6 +282,17 @@ Timeline.prototype.off = function off (event, callback) {
185
282
  Timeline.prototype.setOptions = function (options) {
186
283
  util.extend(this.options, options);
187
284
 
285
+ if ('editable' in options) {
286
+ var isBoolean = typeof options.editable === 'boolean';
287
+
288
+ this.options.editable = {
289
+ updateTime: isBoolean ? options.editable : (options.editable.updateTime || false),
290
+ updateGroup: isBoolean ? options.editable : (options.editable.updateGroup || false),
291
+ add: isBoolean ? options.editable : (options.editable.add || false),
292
+ remove: isBoolean ? options.editable : (options.editable.remove || false)
293
+ };
294
+ }
295
+
188
296
  // force update of range (apply new min/max etc.)
189
297
  // both start and end are optional
190
298
  this.range.setRange(options.start, options.end);
@@ -200,6 +308,9 @@ Timeline.prototype.setOptions = function (options) {
200
308
  }
201
309
  }
202
310
 
311
+ // force the itemSet to refresh: options like orientation and margins may be changed
312
+ this.itemSet.markDirty();
313
+
203
314
  // validate the callback functions
204
315
  var validateCallback = (function (fn) {
205
316
  if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
@@ -208,8 +319,39 @@ Timeline.prototype.setOptions = function (options) {
208
319
  }).bind(this);
209
320
  ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
210
321
 
211
- this.controller.reflow();
212
- this.controller.repaint();
322
+ // add/remove the current time bar
323
+ if (this.options.showCurrentTime) {
324
+ if (!this.mainPanel.hasChild(this.currentTime)) {
325
+ this.mainPanel.appendChild(this.currentTime);
326
+ this.currentTime.start();
327
+ }
328
+ }
329
+ else {
330
+ if (this.mainPanel.hasChild(this.currentTime)) {
331
+ this.currentTime.stop();
332
+ this.mainPanel.removeChild(this.currentTime);
333
+ }
334
+ }
335
+
336
+ // add/remove the custom time bar
337
+ if (this.options.showCustomTime) {
338
+ if (!this.mainPanel.hasChild(this.customTime)) {
339
+ this.mainPanel.appendChild(this.customTime);
340
+ }
341
+ }
342
+ else {
343
+ if (this.mainPanel.hasChild(this.customTime)) {
344
+ this.mainPanel.removeChild(this.customTime);
345
+ }
346
+ }
347
+
348
+ // TODO: remove deprecation error one day (deprecated since version 0.8.0)
349
+ if (options && options.order) {
350
+ throw new Error('Option order is deprecated. There is no replacement for this feature.');
351
+ }
352
+
353
+ // repaint everything
354
+ this.rootPanel.repaint();
213
355
  };
214
356
 
215
357
  /**
@@ -217,11 +359,11 @@ Timeline.prototype.setOptions = function (options) {
217
359
  * @param {Date} time
218
360
  */
219
361
  Timeline.prototype.setCustomTime = function (time) {
220
- if (!this.customtime) {
362
+ if (!this.customTime) {
221
363
  throw new Error('Cannot get custom time: Custom time bar is not enabled');
222
364
  }
223
365
 
224
- this.customtime.setCustomTime(time);
366
+ this.customTime.setCustomTime(time);
225
367
  };
226
368
 
227
369
  /**
@@ -229,11 +371,11 @@ Timeline.prototype.setCustomTime = function (time) {
229
371
  * @return {Date} customTime
230
372
  */
231
373
  Timeline.prototype.getCustomTime = function() {
232
- if (!this.customtime) {
374
+ if (!this.customTime) {
233
375
  throw new Error('Cannot get custom time: Custom time bar is not enabled');
234
376
  }
235
377
 
236
- return this.customtime.getCustomTime();
378
+ return this.customTime.getCustomTime();
237
379
  };
238
380
 
239
381
  /**
@@ -248,52 +390,30 @@ Timeline.prototype.setItems = function(items) {
248
390
  if (!items) {
249
391
  newDataSet = null;
250
392
  }
251
- else if (items instanceof DataSet) {
393
+ else if (items instanceof DataSet || items instanceof DataView) {
252
394
  newDataSet = items;
253
395
  }
254
- if (!(items instanceof DataSet)) {
255
- newDataSet = new DataSet({
396
+ else {
397
+ // turn an array into a dataset
398
+ newDataSet = new DataSet(items, {
256
399
  convert: {
257
400
  start: 'Date',
258
401
  end: 'Date'
259
402
  }
260
403
  });
261
- newDataSet.add(items);
262
404
  }
263
405
 
264
406
  // set items
265
407
  this.itemsData = newDataSet;
266
- this.content.setItems(newDataSet);
408
+ this.itemSet.setItems(newDataSet);
267
409
 
268
410
  if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
269
- // apply the data range as range
270
- var dataRange = this.getItemRange();
271
-
272
- // add 5% space on both sides
273
- var start = dataRange.min;
274
- var end = dataRange.max;
275
- if (start != null && end != null) {
276
- var interval = (end.valueOf() - start.valueOf());
277
- if (interval <= 0) {
278
- // prevent an empty interval
279
- interval = 24 * 60 * 60 * 1000; // 1 day
280
- }
281
- start = new Date(start.valueOf() - interval * 0.05);
282
- end = new Date(end.valueOf() + interval * 0.05);
283
- }
411
+ this.fit();
284
412
 
285
- // override specified start and/or end date
286
- if (this.options.start != undefined) {
287
- start = util.convert(this.options.start, 'Date');
288
- }
289
- if (this.options.end != undefined) {
290
- end = util.convert(this.options.end, 'Date');
291
- }
413
+ var start = (this.options.start != undefined) ? util.convert(this.options.start, 'Date') : null;
414
+ var end = (this.options.end != undefined) ? util.convert(this.options.end, 'Date') : null;
292
415
 
293
- // apply range if there is a min or max available
294
- if (start != null || end != null) {
295
- this.range.setRange(start, end);
296
- }
416
+ this.setWindow(start, end);
297
417
  }
298
418
  };
299
419
 
@@ -301,77 +421,50 @@ Timeline.prototype.setItems = function(items) {
301
421
  * Set groups
302
422
  * @param {vis.DataSet | Array | google.visualization.DataTable} groups
303
423
  */
304
- Timeline.prototype.setGroups = function(groups) {
305
- var me = this;
306
- this.groupsData = groups;
307
-
308
- // switch content type between ItemSet or GroupSet when needed
309
- var Type = this.groupsData ? GroupSet : ItemSet;
310
- if (!(this.content instanceof Type)) {
311
- // remove old content set
312
- if (this.content) {
313
- this.content.hide();
314
- if (this.content.setItems) {
315
- this.content.setItems(); // disconnect from items
316
- }
317
- if (this.content.setGroups) {
318
- this.content.setGroups(); // disconnect from groups
319
- }
320
- this.controller.remove(this.content);
321
- }
424
+ Timeline.prototype.setGroups = function setGroups(groups) {
425
+ // convert to type DataSet when needed
426
+ var newDataSet;
427
+ if (!groups) {
428
+ newDataSet = null;
429
+ }
430
+ else if (groups instanceof DataSet || groups instanceof DataView) {
431
+ newDataSet = groups;
432
+ }
433
+ else {
434
+ // turn an array into a dataset
435
+ newDataSet = new DataSet(groups);
436
+ }
322
437
 
323
- // create new content set
324
- var options = Object.create(this.options);
325
- util.extend(options, {
326
- top: function () {
327
- if (me.options.orientation == 'top') {
328
- return me.timeaxis.height;
329
- }
330
- else {
331
- return me.itemPanel.height - me.timeaxis.height - me.content.height;
332
- }
333
- },
334
- left: null,
335
- width: '100%',
336
- height: function () {
337
- if (me.options.height) {
338
- // fixed height
339
- return me.itemPanel.height - me.timeaxis.height;
340
- }
341
- else {
342
- // auto height
343
- return null;
344
- }
345
- },
346
- maxHeight: function () {
347
- // TODO: change maxHeight to be a css string like '100%' or '300px'
348
- if (me.options.maxHeight) {
349
- if (!util.isNumber(me.options.maxHeight)) {
350
- throw new TypeError('Number expected for property maxHeight');
351
- }
352
- return me.options.maxHeight - me.timeaxis.height;
353
- }
354
- else {
355
- return null;
356
- }
357
- },
358
- labelContainer: function () {
359
- return me.labelPanel.getContainer();
360
- }
361
- });
438
+ this.groupsData = newDataSet;
439
+ this.itemSet.setGroups(newDataSet);
440
+ };
362
441
 
363
- this.content = new Type(this.itemPanel, [this.timeaxis], options);
364
- if (this.content.setRange) {
365
- this.content.setRange(this.range);
366
- }
367
- if (this.content.setItems) {
368
- this.content.setItems(this.itemsData);
369
- }
370
- if (this.content.setGroups) {
371
- this.content.setGroups(this.groupsData);
442
+ /**
443
+ * Set Timeline window such that it fits all items
444
+ */
445
+ Timeline.prototype.fit = function fit() {
446
+ // apply the data range as range
447
+ var dataRange = this.getItemRange();
448
+
449
+ // add 5% space on both sides
450
+ var start = dataRange.min;
451
+ var end = dataRange.max;
452
+ if (start != null && end != null) {
453
+ var interval = (end.valueOf() - start.valueOf());
454
+ if (interval <= 0) {
455
+ // prevent an empty interval
456
+ interval = 24 * 60 * 60 * 1000; // 1 day
372
457
  }
373
- this.controller.add(this.content);
458
+ start = new Date(start.valueOf() - interval * 0.05);
459
+ end = new Date(end.valueOf() + interval * 0.05);
460
+ }
461
+
462
+ // skip range set if there is no start and end date
463
+ if (start === null && end === null) {
464
+ return;
374
465
  }
466
+
467
+ this.range.setRange(start, end);
375
468
  };
376
469
 
377
470
  /**
@@ -421,7 +514,7 @@ Timeline.prototype.getItemRange = function getItemRange() {
421
514
  * unselected.
422
515
  */
423
516
  Timeline.prototype.setSelection = function setSelection (ids) {
424
- if (this.content) this.content.setSelection(ids);
517
+ this.itemSet.setSelection(ids);
425
518
  };
426
519
 
427
520
  /**
@@ -429,17 +522,30 @@ Timeline.prototype.setSelection = function setSelection (ids) {
429
522
  * @return {Array} ids The ids of the selected items
430
523
  */
431
524
  Timeline.prototype.getSelection = function getSelection() {
432
- return this.content ? this.content.getSelection() : [];
525
+ return this.itemSet.getSelection();
433
526
  };
434
527
 
435
528
  /**
436
529
  * Set the visible window. Both parameters are optional, you can change only
437
- * start or only end.
530
+ * start or only end. Syntax:
531
+ *
532
+ * TimeLine.setWindow(start, end)
533
+ * TimeLine.setWindow(range)
534
+ *
535
+ * Where start and end can be a Date, number, or string, and range is an
536
+ * object with properties start and end.
537
+ *
438
538
  * @param {Date | Number | String} [start] Start date of visible window
439
539
  * @param {Date | Number | String} [end] End date of visible window
440
540
  */
441
541
  Timeline.prototype.setWindow = function setWindow(start, end) {
442
- this.range.setRange(start, end);
542
+ if (arguments.length == 1) {
543
+ var range = arguments[0];
544
+ this.range.setRange(range.start, range.end);
545
+ }
546
+ else {
547
+ this.range.setRange(start, end);
548
+ }
443
549
  };
444
550
 
445
551
  /**
@@ -470,14 +576,20 @@ Timeline.prototype._onSelectItem = function (event) {
470
576
  return;
471
577
  }
472
578
 
473
- var item = ItemSet.itemFromTarget(event);
579
+ var oldSelection = this.getSelection();
474
580
 
581
+ var item = ItemSet.itemFromTarget(event);
475
582
  var selection = item ? [item.id] : [];
476
583
  this.setSelection(selection);
477
584
 
478
- this.controller.emit('select', {
479
- items: this.getSelection()
480
- });
585
+ var newSelection = this.getSelection();
586
+
587
+ // if selection is changed, emit a select event
588
+ if (!util.equalArray(oldSelection, newSelection)) {
589
+ this.emit('select', {
590
+ items: this.getSelection()
591
+ });
592
+ }
481
593
 
482
594
  event.stopPropagation();
483
595
  };
@@ -489,7 +601,7 @@ Timeline.prototype._onSelectItem = function (event) {
489
601
  */
490
602
  Timeline.prototype._onAddItem = function (event) {
491
603
  if (!this.options.selectable) return;
492
- if (!this.options.editable) return;
604
+ if (!this.options.editable.add) return;
493
605
 
494
606
  var me = this,
495
607
  item = ItemSet.itemFromTarget(event);
@@ -507,17 +619,17 @@ Timeline.prototype._onAddItem = function (event) {
507
619
  }
508
620
  else {
509
621
  // add item
510
- var xAbs = vis.util.getAbsoluteLeft(this.rootPanel.frame);
622
+ var xAbs = vis.util.getAbsoluteLeft(this.contentPanel.frame);
511
623
  var x = event.gesture.center.pageX - xAbs;
512
624
  var newItem = {
513
- start: this.timeaxis.snap(this._toTime(x)),
625
+ start: this.timeAxis.snap(this._toTime(x)),
514
626
  content: 'new item'
515
627
  };
516
628
 
517
629
  var id = util.randomUUID();
518
630
  newItem[this.itemsData.fieldId] = id;
519
631
 
520
- var group = GroupSet.groupFromTarget(event);
632
+ var group = ItemSet.groupFromTarget(event);
521
633
  if (group) {
522
634
  newItem.group = group.groupId;
523
635
  }
@@ -526,15 +638,7 @@ Timeline.prototype._onAddItem = function (event) {
526
638
  this.options.onAdd(newItem, function (item) {
527
639
  if (item) {
528
640
  me.itemsData.add(newItem);
529
-
530
- // select the created item after it is repainted
531
- me.controller.once('repaint', function () {
532
- me.setSelection([id]);
533
-
534
- me.controller.emit('select', {
535
- items: me.getSelection()
536
- });
537
- }.bind(me));
641
+ // TODO: need to trigger a repaint?
538
642
  }
539
643
  });
540
644
  }
@@ -566,7 +670,7 @@ Timeline.prototype._onMultiSelectItem = function (event) {
566
670
  }
567
671
  this.setSelection(selection);
568
672
 
569
- this.controller.emit('select', {
673
+ this.emit('select', {
570
674
  items: this.getSelection()
571
675
  });
572
676
 
@@ -581,7 +685,7 @@ Timeline.prototype._onMultiSelectItem = function (event) {
581
685
  * @private
582
686
  */
583
687
  Timeline.prototype._toTime = function _toTime(x) {
584
- var conversion = this.range.conversion(this.content.width);
688
+ var conversion = this.range.conversion(this.mainPanel.width);
585
689
  return new Date(x / conversion.scale + conversion.offset);
586
690
  };
587
691
 
@@ -593,6 +697,6 @@ Timeline.prototype._toTime = function _toTime(x) {
593
697
  * @private
594
698
  */
595
699
  Timeline.prototype._toScreen = function _toScreen(time) {
596
- var conversion = this.range.conversion(this.content.width);
700
+ var conversion = this.range.conversion(this.mainPanel.width);
597
701
  return (time.valueOf() - conversion.offset) * conversion.scale;
598
702
  };