vis-rails 0.0.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
  };