Browse Source

Merge branch 'develop'

v3_develop
jos 10 years ago
parent
commit
a1ed72880b
68 changed files with 6837 additions and 6957 deletions
  1. +38
    -2
      HISTORY.md
  2. +2
    -4
      Jakefile.js
  3. +1
    -1
      bower.json
  4. +71
    -69
      dist/vis.css
  5. +2664
    -3131
      dist/vis.js
  6. +1
    -1
      dist/vis.min.css
  7. +11
    -11
      dist/vis.min.js
  8. +357
    -220
      docs/dataset.html
  9. +12
    -2
      docs/graph.html
  10. +70
    -8
      docs/timeline.html
  11. +6
    -6
      examples/timeline/01_basic.html
  12. +18
    -16
      examples/timeline/02_interactive.html
  13. +69
    -0
      examples/timeline/03_a_lot_of_data.html
  14. +0
    -68
      examples/timeline/03_much_data.html
  15. +1
    -1
      examples/timeline/05_groups.html
  16. +1
    -2
      examples/timeline/08_edit_items.html
  17. +66
    -0
      examples/timeline/09_order_groups.html
  18. +51
    -0
      examples/timeline/10_limit_move_and_zoom.html
  19. +57
    -0
      examples/timeline/11_points.html
  20. +88
    -0
      examples/timeline/12_custom_styling.html
  21. +88
    -0
      examples/timeline/13_past_and_future.html
  22. +109
    -0
      examples/timeline/14_a_lot_of_grouped_data.html
  23. +115
    -0
      examples/timeline/15_item_class_names.html
  24. +11
    -2
      examples/timeline/index.html
  25. +3
    -0
      misc/how_to_publish.md
  26. +1
    -1
      package.json
  27. +17
    -9
      src/DataSet.js
  28. +8
    -4
      src/graph/Edge.js
  29. +43
    -9
      src/graph/Graph.js
  30. +1
    -0
      src/graph/Node.js
  31. +4
    -2
      src/graph/graphMixins/ClusterMixin.js
  32. +3
    -3
      src/graph/graphMixins/HierarchicalLayoutMixin.js
  33. +0
    -2
      src/graph/graphMixins/ManipulationMixin.js
  34. +6
    -6
      src/graph/graphMixins/MixinLoader.js
  35. +1
    -1
      src/graph/graphMixins/SelectionMixin.js
  36. +23
    -0
      src/graph/graphMixins/physics/BarnesHut.js
  37. +29
    -1
      src/graph/graphMixins/physics/PhysicsMixin.js
  38. +1
    -2
      src/module/exports.js
  39. +0
    -183
      src/timeline/Controller.js
  40. +53
    -93
      src/timeline/Range.js
  41. +0
    -190
      src/timeline/Stack.js
  42. +328
    -224
      src/timeline/Timeline.js
  43. +17
    -95
      src/timeline/component/Component.js
  44. +0
    -113
      src/timeline/component/ContentPanel.js
  45. +54
    -59
      src/timeline/component/CurrentTime.js
  46. +55
    -83
      src/timeline/component/CustomTime.js
  47. +398
    -75
      src/timeline/component/Group.js
  48. +0
    -580
      src/timeline/component/GroupSet.js
  49. +662
    -403
      src/timeline/component/ItemSet.js
  50. +118
    -60
      src/timeline/component/Panel.js
  51. +80
    -132
      src/timeline/component/RootPanel.js
  52. +191
    -277
      src/timeline/component/TimeAxis.js
  53. +0
    -59
      src/timeline/component/css/groupset.css
  54. +16
    -24
      src/timeline/component/css/item.css
  55. +25
    -4
      src/timeline/component/css/itemset.css
  56. +34
    -0
      src/timeline/component/css/labelset.css
  57. +15
    -1
      src/timeline/component/css/panel.css
  58. +8
    -8
      src/timeline/component/css/timeaxis.css
  59. +48
    -26
      src/timeline/component/item/Item.js
  60. +156
    -230
      src/timeline/component/item/ItemBox.js
  61. +118
    -166
      src/timeline/component/item/ItemPoint.js
  62. +135
    -187
      src/timeline/component/item/ItemRange.js
  63. +29
    -92
      src/timeline/component/item/ItemRangeOverflow.js
  64. +112
    -0
      src/timeline/stack.js
  65. +42
    -3
      src/util.js
  66. +15
    -0
      test/dataset.js
  67. +41
    -4
      test/timeline.html
  68. +40
    -2
      test/timeline_groups.html

+ 38
- 2
HISTORY.md View File

@ -1,7 +1,43 @@
vis.js history
# vis.js history
http://visjs.org
## 2014-05-02, version 1.0.0
### Timeline
- Large refactoring of the Timeline, simplifying the code.
- Great performance improvements.
- Improved layout of box-items inside groups.
- Items can now be dragged from one group to another.
- Implemented option `stack` to enable/disable stacking of items.
- Implemented function `fit`, which sets the Timeline window such that it fits
all items.
- Option `editable` can now be used to enable/disable individual manipulation
actions (`add`, `updateTime`, `updateGroup`, `remove`).
- Function `setWindow` now accepts an object with properties `start` and `end`.
- Fixed option `autoResize` forcing a repaint of the Timeline with every check
rather than when the Timeline is actually resized.
- Fixed `select` event fired repeatedly when clicking an empty place on the
Timeline, deselecting selected items).
- Fixed initial visible window in case items exceed `zoomMax`. Thanks @Remper.
- Fixed an offset in newly created items when using groups.
- Fixed height of a group not reckoning with the height of the group label.
- Option `order` is now deprecated. This was needed for performance improvements.
- More examples added.
- Minor bug fixes.
### Graph
- added recalculate hierarchical layout to update node event.
- added arrowScaleFactor to scale the arrows on the edges.
### DataSet
- A DataSet can now be constructed with initial data, like
`new DataSet(data, options)`.
## 2014-04-18, version 0.7.4
### Graph
@ -141,7 +177,7 @@ http://visjs.org
- Moved the generated library to folder `./dist`
- Css stylesheet must be loaded explicitly now.
- Implemented options `showCurrentTime` and `showCustomTime`. Thanks fi0dor.
- Implemented options `showCurrentTime` and `showCustomTime`. Thanks @fi0dor.
- Implemented touch support for Timeline.
- Fixed broken Timeline options `min` and `max`.
- Fixed not being able to load vis.js in node.js.

+ 2
- 4
Jakefile.js View File

@ -38,7 +38,7 @@ task('build', {async: true}, function () {
src: [
'./src/timeline/component/css/timeline.css',
'./src/timeline/component/css/panel.css',
'./src/timeline/component/css/groupset.css',
'./src/timeline/component/css/labelset.css',
'./src/timeline/component/css/itemset.css',
'./src/timeline/component/css/item.css',
'./src/timeline/component/css/timeaxis.css',
@ -64,10 +64,9 @@ task('build', {async: true}, function () {
'./src/DataSet.js',
'./src/DataView.js',
'./src/timeline/stack.js',
'./src/timeline/TimeStep.js',
'./src/timeline/Stack.js',
'./src/timeline/Range.js',
'./src/timeline/Controller.js',
'./src/timeline/component/Component.js',
'./src/timeline/component/Panel.js',
'./src/timeline/component/RootPanel.js',
@ -77,7 +76,6 @@ task('build', {async: true}, function () {
'./src/timeline/component/ItemSet.js',
'./src/timeline/component/item/*.js',
'./src/timeline/component/Group.js',
'./src/timeline/component/GroupSet.js',
'./src/timeline/Timeline.js',
'./src/graph/dotparser.js',

+ 1
- 1
bower.json View File

@ -1,6 +1,6 @@
{
"name": "vis",
"version": "0.7.4",
"version": "1.0.0",
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",
"repository": {

+ 71
- 69
dist/vis.css View File

@ -7,81 +7,76 @@
overflow: hidden;
border: 1px solid #bfbfbf;
-moz-box-sizing: border-box;
box-sizing: border-box;
/* FIXME: there is an issue with the height of the items when panel height is animated
-webkit-transition: height 4s ease-in-out;
transition: height 4s ease-in-out;
/**/
}
.vis.timeline .vpanel {
position: absolute;
overflow: hidden;
}
.vis.timeline .groupset {
position: absolute;
padding: 0;
margin: 0;
box-sizing: border-box;
}
.vis.timeline .labels {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
.vis.timeline .vpanel.side {
border-right: 1px solid #bfbfbf;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.vis.timeline .labels .label-set {
position: absolute;
top: 0;
left: 0;
.vis.timeline .vpanel.side.hidden {
display: none;
}
.vis.timeline .labelset {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
border-top: none;
border-bottom: 1px solid #bfbfbf;
box-sizing: border-box;
}
.vis.timeline .labels .label-set .vlabel {
position: absolute;
.vis.timeline .labelset .vlabel {
position: relative;
left: 0;
top: 0;
width: 100%;
color: #4d4d4d;
box-sizing: border-box;
}
.vis.timeline.top .labels .label-set .vlabel,
.vis.timeline.top .groupset .itemset-axis {
.vis.timeline.top .labelset .vlabel {
border-top: 1px solid #bfbfbf;
border-bottom: none;
}
.vis.timeline.bottom .labels .label-set .vlabel,
.vis.timeline.bottom .groupset .itemset-axis {
.vis.timeline.bottom .labelset .vlabel {
border-top: none;
border-bottom: 1px solid #bfbfbf;
}
.vis.timeline .labels .label-set .vlabel .inner {
.vis.timeline .labelset .vlabel .inner {
display: inline-block;
padding: 5px;
}
.vis.timeline .itemset {
position: absolute;
position: relative;
padding: 0;
margin: 0;
overflow: hidden;
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 .background {
@ -90,8 +85,23 @@
.vis.timeline .foreground {
}
.vis.timeline .itemset-axis {
position: absolute;
.vis.timeline .axis {
overflow: visible;
}
.vis.timeline .group {
position: relative;
box-sizing: border-box;
}
.vis.timeline.top .group {
border-top: 1px solid #bfbfbf;
border-bottom: none;
}
.vis.timeline.bottom .group {
border-top: none;
border-bottom: 1px solid #bfbfbf;
}
@ -99,9 +109,15 @@
position: absolute;
color: #1A1A1A;
border-color: #97B0F8;
border-width: 1px;
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 {
@ -116,49 +132,30 @@
.vis.timeline .item.point.selected {
background-color: #FFF785;
z-index: 999;
}
.vis.timeline .item.point.selected .dot {
border-color: #FFC200;
}
.vis.timeline .item.cluster {
/* TODO: use another color or pattern? */
background: #97B0F8 url('img/cluster_bg.png');
color: white;
}
.vis.timeline .item.cluster.point {
border-color: #D5DDF6;
}
.vis.timeline .item.box {
text-align: center;
border-style: solid;
border-width: 1px;
border-radius: 5px;
-moz-border-radius: 5px; /* For Firefox 3.6 and older */
border-radius: 2px;
}
.vis.timeline .item.point {
background: none;
}
.vis.timeline .dot,
.vis.timeline .item.dot {
padding: 0;
border: 5px solid #97B0F8;
position: absolute;
border-radius: 5px;
-moz-border-radius: 5px; /* For Firefox 3.6 and older */
padding: 0;
border-width: 4px;
border-style: solid;
border-radius: 4px;
}
.vis.timeline .item.range,
.vis.timeline .item.rangeoverflow{
border-style: solid;
border-width: 1px;
border-radius: 2px;
-moz-border-radius: 2px; /* For Firefox 3.6 and older */
-moz-box-sizing: border-box;
box-sizing: border-box;
}
@ -179,6 +176,11 @@
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 {
@ -220,18 +222,18 @@
z-index: 10001; /* a little higher z-index than .drag-left */
}
.vis.timeline .axis {
position: relative;
.vis.timeline .timeaxis {
position: absolute;
}
.vis.timeline .axis .text {
.vis.timeline .timeaxis .text {
position: absolute;
color: #4d4d4d;
padding: 3px;
white-space: nowrap;
}
.vis.timeline .axis .text.measure {
.vis.timeline .timeaxis .text.measure {
position: absolute;
padding-left: 0;
padding-right: 0;
@ -240,13 +242,13 @@
visibility: hidden;
}
.vis.timeline .axis .grid.vertical {
.vis.timeline .timeaxis .grid.vertical {
position: absolute;
width: 0;
border-right: 1px solid;
}
.vis.timeline .axis .grid.horizontal {
.vis.timeline .timeaxis .grid.horizontal {
position: absolute;
left: 0;
width: 100%;
@ -254,11 +256,11 @@
border-bottom: 1px solid;
}
.vis.timeline .axis .grid.minor {
.vis.timeline .timeaxis .grid.minor {
border-color: #e5e5e5;
}
.vis.timeline .axis .grid.major {
.vis.timeline .timeaxis .grid.major {
border-color: #bfbfbf;
}

+ 2664
- 3131
dist/vis.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/vis.min.css
File diff suppressed because it is too large
View File


+ 11
- 11
dist/vis.min.js
File diff suppressed because it is too large
View File


+ 357
- 220
docs/dataset.html View File

@ -20,10 +20,10 @@
<li><a href="#Overview">Overview</a></li>
<li><a href="#Example">Example</a></li>
<li><a href="#Construction">Construction</a></li>
<li><a href="#Data_Manipulation">Data Manipulation</a></li>
<li><a href="#Data_Filtering">Data Filtering</a></li>
<li><a href="#Data_Formatting">Data Formatting</a></li>
<li><a href="#Methods">Methods</a></li>
<li><a href="#Subscriptions">Subscriptions</a></li>
<li><a href="#Data_Manipulation">Data Manipulation</a></li>
<li><a href="#Data_Selection">Data Selection</a></li>
<li><a href="#Data_Policy">Data Policy</a></li>
</ul>
@ -107,7 +107,7 @@ console.log('formatted items', items);
</p>
<pre class="prettyprint lang-js">
var data = new vis.DataSet(options)
var data = new vis.DataSet([data] [, options])
</pre>
<p>
@ -116,6 +116,11 @@ var data = new vis.DataSet(options)
<a href="#Data_Manipulation">Data Manipulation</a>.
</p>
<p>
The parameter <code>data</code>code> is optional and can be an Array or
Google DataTable with items.
</p>
<p>
The parameter <code>options</code> is optional and is an object which can
contain the following properties:
@ -160,6 +165,294 @@ var data = new vis.DataSet(options)
</table>
<h2 id="Methods">Methods</h2>
<p>DataSet contains the following methods.</p>
<table>
<colgroup>
<col width="200">
</colgroup>
<tr>
<th>Method</th>
<th>Return Type</th>
<th>Description</th>
</tr>
<tr>
<td>add(data [, senderId])</td>
<td>Number[]</td>
<td>Add data to the DataSet. Adding an item will fail when there already is an item with the same id. The function returns an array with the ids of the added items. See section <a href="#Data_Manipulation">Data Manipulation</a>.</td>
</tr>
<tr>
<td>clear([senderId])</td>
<td>Number[]</td>
<td>Clear all data from the DataSet. The function returns an array with the ids of the removed items.</td>
</tr>
<tr>
<td>distinct(field)</td>
<td>Array</td>
<td>Find all distinct values of a specified field. Returns an unordered array containing all distinct values. If data items do not contain the specified field are ignored.</td>
</tr>
<tr>
<td>forEach(callback [, options])</td>
<td>none</td>
<td>
Execute a callback function for every item in the dataset.
The available options are described in section <a href="#Data_Selection">Data Selection</a>.
</td>
</tr>
<tr>
<td>
get([options] [, data])<br>
get(id [,options] [, data])<br>
get(ids [, options] [, data])
</td>
<td>Object | Array | DataTable</td>
<td>
Get a single item, multiple items, or all items from the DataSet.
Usage examples can be found in section <a href="#Getting_Data">Getting Data</a>, and the available <code>options</code> are described in section <a href="#Data_Selection">Data Selection</a>. If parameter <code>data</code> isprovided, items will be appended to this array or table, which is required in case of Google DataTable.
</td>
</tr>
<tr>
<td>
getIds([options])
</td>
<td>Number[]</td>
<td>
Get ids of all items or of a filtered set of items.
Available <code>options</code> are described in section <a href="#Data_Selection">Data Selection</a>, except that options <code>fields</code> and <code>convert</code> are not applicable in case of <code>getIds</code>.
</td>
</tr>
<tr>
<td>map(callback [, options])</td>
<td>Array</td>
<td>
Map every item in the DataSet.
The available options are described in section <a href="#Data_Selection">Data Selection</a>.
</td>
</tr>
<tr>
<td>max(field)</td>
<td>Object | null</td>
<td>
Find the item with maximum value of specified field. Returns <code>null</code> if no item is found.
</td>
</tr>
<tr>
<td>min(field)</td>
<td>Object | null</td>
<td>
Find the item with minimum value of specified field. Returns <code>null</code> if no item is found.
</td>
</tr>
<tr>
<td>off(event, callback)</td>
<td>none</td>
<td>
Unsubscribe from an event, remove an event listener. See section <a href="#Subscriptions">Subscriptions</a>.
</td>
</tr>
<tr>
<td>on(event, callback)</td>
<td>none</td>
<td>
Subscribe to an event, add an event listener. See section <a href="#Subscriptions">Subscriptions</a>.
</td>
</tr>
<tr>
<td>
remove(id [, senderId])<br>
remove(ids [, senderId])
</td>
<td>Number[]</td>
<td>
Remove on ore multiple items by id or by the items themselves. Returns an array with the ids of the removed items. See section <a href="#Data_Manipulation">Data Manipulation</a>.
</td>
</tr>
<tr>
<td>
update(id [, senderId])<br>
update(ids [, senderId])
</td>
<td>Number[]</td>
<td>
Update on ore existing items. When an item doesn't exist, it will be created. Returns an array with the ids of the removed items. See section <a href="#Data_Manipulation">Data Manipulation</a>.
</td>
</tr>
</table>
<h2 id="Subscriptions">Subscriptions</h2>
<p>
One can subscribe on changes in a DataSet.
A subscription can be created using the method <code>on</code>,
and removed with <code>off</code>.
</p>
<pre class="prettyprint lang-js">
// create a DataSet
var data = new vis.DataSet();
// subscribe to any change in the DataSet
data.on('*', function (event, properties, senderId) {
console.log('event:', event, 'properties:', properties, 'senderId:', senderId);
});
// add an item
data.add({id: 1, text: 'item 1'}); // triggers an 'add' event
data.update({id: 1, text: 'item 1 (updated)'}); // triggers an 'update' event
data.remove(1); // triggers an 'remove' event
</pre>
<h3 id="On">On</h3>
<p>
Subscribe to an event.
</p>
Syntax:
<pre class="prettyprint lang-js">DataSet.on(event, callback)</pre>
Where:
<ul>
<li>
<code>event</code> is a String containing any of the events listed
in section <a href="#Events">Events</a>.
</li>
<li>
<code>callback</code> is a callback function which will be called
each time the event occurs. The callback function is described in
section <a href="#Callback">Callback</a>.
</li>
</ul>
<h3 id="Off">Off</h3>
<p>
Unsubscribe from an event.
</p>
Syntax:
<pre class="prettyprint lang-js">DataSet.off(event, callback)</pre>
Where <code>event</code> and <code>callback</code> correspond with the
parameters used to <a href="#On">subscribe</a> to the event.
<h3 id="Events">Events</h3>
<p>
The following events are available for subscription:
</p>
<table>
<tr>
<th>Event</th>
<th>Description</th>
</tr>
<tr>
<td>add</td>
<td>
The <code>add</code> event is triggered when an item
or a set of items is added, or when an item is updated while
not yet existing.
</td>
</tr>
<tr>
<td>update</td>
<td>
The <code>update</code> event is triggered when an existing item
or a set of existing items is updated.
</td>
</tr>
<tr>
<td>remove</td>
<td>
The <code>remove</code> event is triggered when an item
or a set of items is removed.
</td>
</tr>
<tr>
<td>*</td>
<td>
The <code>*</code> event is triggered when any of the events
<code>add</code>, <code>update</code>, and <code>remove</code>
occurs.
</td>
</tr>
</table>
<h3 id="Callback">Callback</h3>
<p>
The callback functions of subscribers are called with the following
parameters:
</p>
<pre class="prettyprint lang-js">
function (event, properties, senderId) {
// handle the event
});
</pre>
<p>
where the parameters are defined as
</p>
<table>
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td>event</td>
<td>String</td>
<td>
Any of the available events: <code>add</code>,
<code>update</code>, or <code>remove</code>.
</td>
</tr>
<tr>
<td>properties</td>
<td>Object&nbsp;|&nbsp;null</td>
<td>
Optional properties providing more information on the event.
In case of the events <code>add</code>,
<code>update</code>, and <code>remove</code>,
<code>properties</code> is always an object containing a property
items, which contains an array with the ids of the affected
items.
</td>
</tr>
<tr>
<td>senderId</td>
<td>String&nbsp;|&nbsp;Number</td>
<td>
An senderId, optionally provided by the application code
which triggered the event. If senderId is not provided, the
argument will be <code>null</code>.
</td>
</tr>
</table>
<h2 id="Data_Manipulation">Data Manipulation</h2>
<p>
@ -317,70 +610,18 @@ Syntax:
</p>
<h2 id="Data_Filtering">Data Filtering</h2>
<h2 id="Data_Selection">Data Selection</h2>
<p>
Data can be retrieved from the DataSet using the method <code>get</code>.
This method can return a single item or a list with items.
The DataSet contains functionality to format, filter, and sort data retrieved via the
methods <code>get</code>, <code>getIds</code>, <code>forEach</code>, and <code>map</code>. These methods have the following syntax:
</p>
<p>A single item can be retrieved by its id:</p>
<pre class="prettyprint lang-js">
var item1 = dataset.get(1);
</pre>
<p>A selection of items can be retrieved by providing an array with ids:</p>
<pre class="prettyprint lang-js">
var items = dataset.get([1, 3, 4]); // retrieve items 1, 3, and 4
</pre>
<p>All items can be retrieved by simply calling <code>get</code> without
specifying an id:</p>
<pre class="prettyprint lang-js">
var items = dataset.get(); // retrieve all items
</pre>
<p>
Items can be filtered on specific properties by providing a filter
function. A filter function is executed for each of the items in the
DataSet, and is called with the item as parameter. The function must
return a boolean. All items for which the filter function returns
true will be emitted.
</p>
<pre class="prettyprint lang-js">
// retrieve all items having a property group with value 2
var group2 = dataset.get({
filter: function (item) {
return (item.group == 2);
}
});
// retrieve all items having a property balance with a value above zero
var positiveBalance = dataset.get({
filter: function (item) {
return (item.balance > 0);
}
});
</pre>
<h2 id="Data_Formatting">Data Formatting</h2>
<p>
The DataSet contains functionality to format data retrieved via the
method <code>get</code>. The method <code>get</code> has the following
syntax:
</p>
<pre class="prettyprint lang-js">
var item = DataSet.get(id, options); // retrieve a single item
var items = DataSet.get(ids, options); // retrieve a selection of items
var items = DataSet.get(options); // retrieve all items or a filtered set
DataSet.get([id] [, options] [, data]);
DataSet.getIds([options]);
DataSet.forEach(callback [, options]);
DataSet.map(callback [, options]);
</pre>
<p>
@ -466,6 +707,59 @@ var items = data.get({
});
</pre>
<h3 id="Getting_Data">Getting Data</h3>
<p>
Data can be retrieved from the DataSet using the method <code>get</code>.
This method can return a single item or a list with items.
</p>
<p>A single item can be retrieved by its id:</p>
<pre class="prettyprint lang-js">
var item1 = dataset.get(1);
</pre>
<p>A selection of items can be retrieved by providing an array with ids:</p>
<pre class="prettyprint lang-js">
var items = dataset.get([1, 3, 4]); // retrieve items 1, 3, and 4
</pre>
<p>All items can be retrieved by simply calling <code>get</code> without
specifying an id:</p>
<pre class="prettyprint lang-js">
var items = dataset.get(); // retrieve all items
</pre>
<h3 id="Data_Filtering">Data Filtering</h3>
<p>
Items can be filtered on specific properties by providing a filter
function. A filter function is executed for each of the items in the
DataSet, and is called with the item as parameter. The function must
return a boolean. All items for which the filter function returns
true will be emitted.
</p>
<pre class="prettyprint lang-js">
// retrieve all items having a property group with value 2
var group2 = dataset.get({
filter: function (item) {
return (item.group == 2);
}
});
// retrieve all items having a property balance with a value above zero
var positiveBalance = dataset.get({
filter: function (item) {
return (item.balance > 0);
}
});
</pre>
<h3 id="Data_Types">Data Types</h3>
@ -541,163 +835,6 @@ var items = data.get({
</table>
<h2 id="Subscriptions">Subscriptions</h2>
<p>
One can subscribe on changes in a DataSet.
A subscription can be created using the method <code>on</code>,
and removed with <code>off</code>.
</p>
<pre class="prettyprint lang-js">
// create a DataSet
var data = new vis.DataSet();
// subscribe to any change in the DataSet
data.on('*', function (event, properties, senderId) {
console.log('event:', event, 'properties:', properties, 'senderId:', senderId);
});
// add an item
data.add({id: 1, text: 'item 1'}); // triggers an 'add' event
data.update({id: 1, text: 'item 1 (updated)'}); // triggers an 'update' event
data.remove(1); // triggers an 'remove' event
</pre>
<h3 id="On">On</h3>
<p>
Subscribe to an event.
</p>
Syntax:
<pre class="prettyprint lang-js">DataSet.on(event, callback)</pre>
Where:
<ul>
<li>
<code>event</code> is a String containing any of the events listed
in section <a href="#Events">Events</a>.
</li>
<li>
<code>callback</code> is a callback function which will be called
each time the event occurs. The callback function is described in
section <a href="#Callback">Callback</a>.
</li>
</ul>
<h3 id="Off">Off</h3>
<p>
Unsubscribe from an event.
</p>
Syntax:
<pre class="prettyprint lang-js">DataSet.off(event, callback)</pre>
Where <code>event</code> and <code>callback</code> correspond with the
parameters used to <a href="#On">subscribe</a> to the event.
<h3 id="Events">Events</h3>
<p>
The following events are available for subscription:
</p>
<table>
<tr>
<th>Event</th>
<th>Description</th>
</tr>
<tr>
<td>add</td>
<td>
The <code>add</code> event is triggered when an item
or a set of items is added, or when an item is updated while
not yet existing.
</td>
</tr>
<tr>
<td>update</td>
<td>
The <code>update</code> event is triggered when an existing item
or a set of existing items is updated.
</td>
</tr>
<tr>
<td>remove</td>
<td>
The <code>remove</code> event is triggered when an item
or a set of items is removed.
</td>
</tr>
<tr>
<td>*</td>
<td>
The <code>*</code> event is triggered when any of the events
<code>add</code>, <code>update</code>, and <code>remove</code>
occurs.
</td>
</tr>
</table>
<h3 id="Callback">Callback</h3>
<p>
The callback functions of subscribers are called with the following
parameters:
</p>
<pre class="prettyprint lang-js">
function (event, properties, senderId) {
// handle the event
});
</pre>
<p>
where the parameters are defined as
</p>
<table>
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td>event</td>
<td>String</td>
<td>
Any of the available events: <code>add</code>,
<code>update</code>, or <code>remove</code>.
</td>
</tr>
<tr>
<td>properties</td>
<td>Object&nbsp;|&nbsp;null</td>
<td>
Optional properties providing more information on the event.
In case of the events <code>add</code>,
<code>update</code>, and <code>remove</code>,
<code>properties</code> is always an object containing a property
items, which contains an array with the ids of the affected
items.
</td>
</tr>
<tr>
<td>senderId</td>
<td>String&nbsp;|&nbsp;Number</td>
<td>
An senderId, optionally provided by the application code
which triggered the event. If senderId is not provided, the
argument will be <code>null</code>.
</td>
</tr>
</table>
<h2 id="Data_Policy">Data Policy</h2>
<p>
All code and data is processed and rendered in the browser.

+ 12
- 2
docs/graph.html View File

@ -490,7 +490,12 @@ var edges = [
<th>Required</th>
<th>Description</th>
</tr>
<tr>
<td>arrowScaleFactor</td>
<td>Number</td>
<td>no</td>
<td>If you are using arrows, this will scale the arrow. Values < 1 give smaller arrows, > 1 larger arrows. Default: 1.</td>
</tr>
<tr>
<td>color</td>
<td>String | Object</td>
@ -1062,7 +1067,12 @@ var options = {
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td>arrowScaleFactor</td>
<td>Number</td>
<td>1</td>
<td>If you are using arrows, this will scale the arrow. Values < 1 give smaller arrows, > 1 larger arrows. Default: 1.</td>
</tr>
<tr>
<td>color</td>
<td>String | Object</td>

+ 70
- 8
docs/timeline.html View File

@ -347,12 +347,41 @@ var options = {
<tr>
<td>editable</td>
<td>Boolean</td>
<td>Boolean | Object</td>
<td>false</td>
<td>If true, the items on the timeline can be dragged. Only applicable when option <code>selectable</code> is <code>true</code>. See also the callbacks <code>onAdd</code>, <code>onUpdate</code>, <code>onMove</code>, and <code>onRemove</code>, described in detail in section <a href="#Editing_Items">Editing Items</a>.
<td>If true, the items in the timeline can be manipulated. Only applicable when option <code>selectable</code> is <code>true</code>. See also the callbacks <code>onAdd</code>, <code>onUpdate</code>, <code>onMove</code>, and <code>onRemove</code>. When <code>editable</code> is an object, one can enable or disable individual manipulation actions.
See section <a href="#Editing_Items">Editing Items</a> for a detailed explanation.
</td>
</tr>
<tr>
<td>editable.add</td>
<td>Boolean</td>
<td>false</td>
<td>If true, new items can be created by double tapping an empty space in the Timeline. See section <a href="#Editing_Items">Editing Items</a> for a detailed explanation.</td>
</tr>
<tr>
<td>editable.remove</td>
<td>Boolean</td>
<td>false</td>
<td>If true, items can be deleted by first selecting them, and then clicking the delete button on the top right of the item. See section <a href="#Editing_Items">Editing Items</a> for a detailed explanation.</td>
</tr>
<tr>
<td>editable.updateGroup</td>
<td>Boolean</td>
<td>false</td>
<td>If true, items can be dragged from one group to another. Only applicable when the Timeline has groups. See section <a href="#Editing_Items">Editing Items</a> for a detailed explanation.</td>
</tr>
<tr>
<td>editable.updateTime</td>
<td>Boolean</td>
<td>false</td>
<td>If true, items can be dragged to another moment in time. See section <a href="#Editing_Items">Editing Items</a> for a detailed explanation.</td>
</tr>
<tr>
<td>end</td>
<td>Date | Number | String</td>
@ -428,7 +457,7 @@ var options = {
<td>onAdd</td>
<td>Function</td>
<td>none</td>
<td>Callback function triggered when an item is about to be added: when the user double taps an empty space in the Timeline. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable</code> are set <code>true</code>.
<td>Callback function triggered when an item is about to be added: when the user double taps an empty space in the Timeline. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable.add</code> are set <code>true</code>.
</td>
</tr>
@ -436,7 +465,7 @@ var options = {
<td>onUpdate</td>
<td>Function</td>
<td>none</td>
<td>Callback function triggered when an item is about to be updated, when the user double taps an item in the Timeline. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable</code> are set <code>true</code>.
<td>Callback function triggered when an item is about to be updated, when the user double taps an item in the Timeline. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable.updateTime</code> or <code>editable.updateGroup</code> are set <code>true</code>.
</td>
</tr>
@ -444,7 +473,7 @@ var options = {
<td>onMove</td>
<td>Function</td>
<td>none</td>
<td>Callback function triggered when an item has been moved: after the user has dragged the item to an other position. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable</code> are set <code>true</code>.
<td>Callback function triggered when an item has been moved: after the user has dragged the item to an other position. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable.updateTime</code> or <code>editable.updateGroup</code> are set <code>true</code>.
</td>
</tr>
@ -452,10 +481,11 @@ var options = {
<td>onRemove</td>
<td>Function</td>
<td>none</td>
<td>Callback function triggered when an item is about to be removed: when the user tapped the delete button on the top right of a selected item. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable</code> are set <code>true</code>.
<td>Callback function triggered when an item is about to be removed: when the user tapped the delete button on the top right of a selected item. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable.remove</code> are set <code>true</code>.
</td>
</tr>
<!-- TODO: cleanup option order
<tr>
<td>order</td>
<td>Function</td>
@ -466,6 +496,7 @@ var options = {
`vis.components.items.Item`.
</td>
</tr>
-->
<tr>
<td>orientation</td>
@ -483,8 +514,7 @@ var options = {
<pre class="prettyprint lang-css">
.vis.timeline .item {
padding: 10px;
}
</pre>
}</pre>
</td>
</tr>
@ -534,6 +564,13 @@ var options = {
visible.</td>
</tr>
<tr>
<td>stack</td>
<td>Boolean</td>
<td>true</td>
<td>If true (default), items will be stacked on top of each other such that they do not overlap.</td>
</tr>
<tr>
<td>start</td>
<td>Date | Number | String</td>
@ -591,6 +628,13 @@ var options = {
<th>Description</th>
</tr>
<tr>
<td>fit()</td>
<td>none</td>
<td>Adjust the visible window such that it fits all items.
</td>
</tr>
<tr>
<td>getCustomTime()</td>
<td>Date</td>
@ -791,6 +835,24 @@ timeline.off('select', onSelect);
When the Timeline is configured to be editable (both options <code>selectable</code> and <code>editable</code> are <code>true</code>), the user can move items by dragging them, can create a new item by double tapping on an empty space, can update an item by double tapping it, and can delete a selected item by clicking the delete button on the top right.
</p>
<p>Option <code>editable</code> accepts a boolean or an object. When <code>editable</code> is a boolean, all manipulation actions will be either enabled or disabled. When <code>editable</code> is an object, one can enable individual manipulation actions:</p>
<pre class="prettyprint lang-js">// enable or disable all manipulation actions
var options = {
editable: true // true or false
};
// enable or disable individual manipulation actions
var options = {
editable: {
add: true, // add new items by double tapping
updateTime: true, // drag items horizontally
updateGroup: true, // drag items from one group to another
remove: true // delete an item by tapping the delete button top right
}
};</pre>
<p>
One can specify callback functions to validate changes made by the user. There are a number of callback functions for this purpose:
</p>

+ 6
- 6
examples/timeline/01_basic.html View File

@ -18,12 +18,12 @@
<script type="text/javascript">
var container = document.getElementById('visualization');
var items = [
{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'}
{id: 1, content: 'item 1', start: '2014-04-20'},
{id: 2, content: 'item 2', start: '2014-04-14'},
{id: 3, content: 'item 3', start: '2014-04-18'},
{id: 4, content: 'item 4', start: '2014-04-16', end: '2014-04-19'},
{id: 5, content: 'item 5', start: '2014-04-25'},
{id: 6, content: 'item 6', start: '2014-04-27', type: 'point'}
];
var options = {};
var timeline = new vis.Timeline(container, items, options);

examples/timeline/02_dataset.html → examples/timeline/02_interactive.html View File

@ -1,36 +1,25 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Dataset example</title>
<title>Timeline | Interactive example</title>
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
height: 100%;
margin: 0;
padding: 0;
}
#visualization {
box-sizing: border-box;
width: 100%;
height: 100%;
}
</style>
<!-- note: moment.js must be loaded before vis.js, else vis.js uses its embedded version of moment.js -->
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.3.1/moment.min.js"></script>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<p>Drag items around, create new items, and remove items.</p>
<div id="visualization"></div>
<script>
var now = moment().minutes(0).seconds(0).milliseconds(0);
// create a dataset with items
var items = new vis.DataSet({
convert: {
@ -52,7 +41,20 @@
start: '2014-01-10',
end: '2014-02-10',
orientation: 'top',
height: '100%',
height: '300px',
editable: true,
/* alternatively, enable/disable individual actions:
editable: {
add: true,
updateTime: true,
updateGroup: true,
remove: true
},
*/
showCurrentTime: true
};

+ 69
- 0
examples/timeline/03_a_lot_of_data.html View File

@ -0,0 +1,69 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | a lot of data</title>
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
}
</style>
<!-- note: moment.js must be loaded before vis.js, else vis.js uses its embedded version of moment.js -->
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.3.1/moment.min.js"></script>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h1>
Test with a lot of data
</h1>
<p>
<label for="count">Number of items</label>
<input id="count" value="10000">
<input id="draw" type="button" value="draw">
</p>
<div id="visualization"></div>
<script>
// create a dataset with items
var now = moment().minutes(0).seconds(0).milliseconds(0);
var items = new vis.DataSet({
convert: {
start: 'Date',
end: 'Date'
}
});
// create data
function createData() {
var count = parseInt(document.getElementById('count').value) || 100;
var newData = [];
for (var i = 0; i < count; i++) {
newData.push({id: i, content: 'item ' + i, start: now.clone().add('days', i)});
}
items.clear();
items.add(newData);
}
createData();
document.getElementById('draw').onclick = createData;
var container = document.getElementById('visualization');
var options = {
editable: true,
start: now.clone().add('days', -3),
end: now.clone().add('days', 11),
zoomMin: 1000 * 60 * 60 * 24, // a day
zoomMax: 1000 * 60 * 60 * 24 * 30 * 3 // three months
//maxHeight: 300,
//height: '300px',
//orientation: 'top'
};
var timeline = new vis.Timeline(container, items, options);
</script>
</body>
</html>

+ 0
- 68
examples/timeline/03_much_data.html View File

@ -1,68 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | a lot of data</title>
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
}
</style>
<!-- note: moment.js must be loaded before vis.js, else vis.js uses its embedded version of moment.js -->
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.3.1/moment.min.js"></script>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h1>
Test with a lot of data
</h1>
<p>
<label for="count">Number of items</label>
<input id="count" value="100">
<input id="draw" type="button" value="draw">
</p>
<div id="visualization"></div>
<script>
// create a dataset with items
var now = moment().minutes(0).seconds(0).milliseconds(0);
var items = new vis.DataSet({
convert: {
start: 'Date',
end: 'Date'
}
});
// create data
function createData() {
var count = parseInt(document.getElementById('count').value) || 100;
var newData = [];
for (var i = 0; i < count; i++) {
newData.push({id: i, content: 'item ' + i, start: now.clone().add('days', i)});
}
items.clear();
items.add(newData);
}
createData();
document.getElementById('draw').onclick = createData;
var container = document.getElementById('visualization');
var options = {
start: now.clone().add('days', -3),
end: now.clone().add('days', 11),
zoomMin: 1000 * 60 * 60 * 24, // a day
zoomMax: 1000 * 60 * 60 * 24 * 30 * 3 // three months
//maxHeight: 300,
//height: '300px',
//orientation: 'top'
};
var timeline = new vis.Timeline(container, items, options);
</script>
</body>
</html>

+ 1
- 1
examples/timeline/05_groups.html View File

@ -60,7 +60,7 @@
// create visualization
var container = document.getElementById('visualization');
var options = {
groupOrder: 'content'
groupOrder: 'content' // groupOrder can be a property name or a sorting function
};
var timeline = new vis.Timeline(container);

+ 1
- 2
examples/timeline/08_edit_items.html View File

@ -18,8 +18,7 @@
<div id="log"></div>
<script type="text/javascript">
var items = new vis.DataSet();
items.add([
var items = new vis.DataSet([
{id: 1, content: 'item 1', start: new Date(2013, 3, 20)},
{id: 2, content: 'item 2', start: new Date(2013, 3, 14)},
{id: 3, content: 'item 3', start: new Date(2013, 3, 18)},

+ 66
- 0
examples/timeline/09_order_groups.html View File

@ -0,0 +1,66 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Order groups</title>
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
}
#visualization {
box-sizing: border-box;
width: 100%;
height: 300px;
}
</style>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<p>
This example demonstrate custom ordering of groups.
</p>
<div id="visualization"></div>
<script>
var groups = new vis.DataSet([
{id: 0, content: 'First', value: 1},
{id: 1, content: 'Third', value: 3},
{id: 2, content: 'Second', value: 2}
]);
// create a dataset with items
var items = new vis.DataSet([
{id: 0, group: 0, content: 'item 0', start: new Date(2014, 3, 17), end: new Date(2014, 3, 21)},
{id: 1, group: 0, content: 'item 1', start: new Date(2014, 3, 19), end: new Date(2014, 3, 20)},
{id: 2, group: 1, content: 'item 2', start: new Date(2014, 3, 16), end: new Date(2014, 3, 24)},
{id: 3, group: 1, content: 'item 3', start: new Date(2014, 3, 23), end: new Date(2014, 3, 24)},
{id: 4, group: 1, content: 'item 4', start: new Date(2014, 3, 22), end: new Date(2014, 3, 26)},
{id: 5, group: 2, content: 'item 5', start: new Date(2014, 3, 24), end: new Date(2014, 3, 27)}
]);
// create visualization
var container = document.getElementById('visualization');
var options = {
// option groupOrder can be a property name or a sort function
// the sort function must compare two groups and return a value
// > 0 when a > b
// < 0 when a < b
// 0 when a == b
groupOrder: function (a, b) {
return a.value - b.value;
},
editable: true
};
var timeline = new vis.Timeline(container);
timeline.setOptions(options);
timeline.setGroups(groups);
timeline.setItems(items);
</script>
</body>
</html>

+ 51
- 0
examples/timeline/10_limit_move_and_zoom.html View File

@ -0,0 +1,51 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Limit move and zoom</title>
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
}
</style>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<p>
The visible range is limited in this demo:
</p>
<ul>
<li>minimum visible date is limited to 2012-01-01 using option <code>min</code></li>
<li>maximum visible date is limited to 2013-01-01 (excluded) using option <code>max</code></li>
<li>visible zoom interval is limited to a minimum of 24 hours using option <code>zoomMin</code></li>
<li>visible zoom interval is limited to a maximum of about 3 months using option <code>zoomMax</code></li>
</ul>
<div id="visualization"></div>
<script>
// create some items
var items = [
{'start': new Date(2012, 4, 25), 'content': 'First'},
{'start': new Date(2012, 4, 26), 'content': 'Last'}
];
// create visualization
var container = document.getElementById('visualization');
var options = {
height: '300px',
min: new Date(2012, 0, 1), // lower limit of visible range
max: new Date(2013, 0, 1), // upper limit of visible range
zoomMin: 1000 * 60 * 60 * 24, // one day in milliseconds
zoomMax: 1000 * 60 * 60 * 24 * 31 * 3 // about three months in milliseconds
};
// create the timeline
var timeline = new vis.Timeline(container);
timeline.setOptions(options);
timeline.setItems(items);
</script>
</body>
</html>

+ 57
- 0
examples/timeline/11_points.html View File

@ -0,0 +1,57 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Points</title>
<style type="text/css">
body {
font: 10pt arial;
}
</style>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h1>World War II timeline</h1>
<p>Source: <a href="http://www.onwar.com/chrono/index.htm" target="_blank">http://www.onwar.com/chrono/index.htm</a></p>
<div id="mytimeline" style="background-color: #FAFAFA;"></div>
<div id="visualization"></div>
<script type="text/javascript">
var container = document.getElementById('visualization');
var items = [
{start: new Date(1939,8,1), content: 'German Invasion of Poland'},
{start: new Date(1940,4,10), content: 'Battle of France and the Low Countries'},
{start: new Date(1940,7,13), content: 'Battle of Britain - RAF vs. Luftwaffe'},
{start: new Date(1941,1,14), content: 'German Afrika Korps arrives in North Africa'},
{start: new Date(1941,5,22), content: 'Third Reich Invades the USSR'},
{start: new Date(1941,11,7), content: 'Japanese Attack Pearl Harbor'},
{start: new Date(1942,5,4), content: 'Battle of Midway in the Pacific'},
{start: new Date(1942,10,8), content: 'Americans open Second Front in North Africa'},
{start: new Date(1942,10,19),content: 'Battle of Stalingrad in Russia'},
{start: new Date(1943,6,5), content: 'Battle of Kursk - Last German Offensive on Eastern Front'},
{start: new Date(1943,6,10), content: 'Anglo-American Landings in Sicily'},
{start: new Date(1944,2,8), content: 'Japanese Attack British India'},
{start: new Date(1944,5,6), content: 'D-Day - Allied Invasion of Normandy'},
{start: new Date(1944,5,22), content: 'Destruction of Army Group Center in Byelorussia'},
{start: new Date(1944,7,1), content: 'The Warsaw Uprising in Occupied Poland'},
{start: new Date(1944,9,20), content: 'American Liberation of the Philippines'},
{start: new Date(1944,11,16),content: 'Battle of the Bulge in the Ardennes'},
{start: new Date(1944,1,19), content: 'American Landings on Iwo Jima'},
{start: new Date(1945,3,1), content: 'US Invasion of Okinawa'},
{start: new Date(1945,3,16), content: 'Battle of Berlin - End of the Third Reich'}
];
var options = {
// Set global item type. Type can also be specified for items individually
// Available types: 'box' (default), 'point', 'range', 'rangeoverflow'
type: 'point'
};
var timeline = new vis.Timeline(container, items, options);
</script>
</body>
</html>

+ 88
- 0
examples/timeline/12_custom_styling.html View File

@ -0,0 +1,88 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Custom styling</title>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
<style type="text/css">
.vis.timeline.rootpanel {
border: 2px solid purple;
font-family: purisa, 'comic sans', cursive;
font-size: 12pt;
background: #ffecea;
}
.vis.timeline .item {
border-color: #F991A3;
background-color: pink;
font-size: 15pt;
color: purple;
box-shadow: 5px 5px 20px rgba(128,128,128, 0.5);
}
.vis.timeline .item,
.vis.timeline .item.line {
border-width: 3px;
}
.vis.timeline .item.dot {
border-width: 10px;
border-radius: 10px;
}
.vis.timeline .item.selected {
border-color: green;
background-color: lightgreen;
}
.vis.timeline .timeaxis .text {
color: purple;
padding-top: 10px;
padding-left: 10px;
}
.vis.timeline .timeaxis .text.major {
font-weight: bold;
}
.vis.timeline .timeaxis .grid.minor {
border-width: 2px;
border-color: pink;
}
.vis.timeline .timeaxis .grid.major {
border-width: 2px;
border-color: #F991A3;
}
</style>
</head>
<body>
<div id="visualization"></div>
<script type="text/javascript">
var container = document.getElementById('visualization');
var items = [
{start: new Date(2010,7,23), content: '<div>Conversation</div><img src="img/community-users-icon.png" style="width:32px; height:32px;">', type: 'point'},
{start: new Date(2010,7,23,23,0,0), content: '<div>Mail from boss</div><img src="img/mail-icon.png" style="width:32px; height:32px;">'},
{start: new Date(2010,7,24,16,0,0), content: 'Report'},
{start: new Date(2010,7,26), end: new Date(2010,8,2), content: 'Traject A'},
{start: new Date(2010,7,28), content: '<div>Memo</div><img src="img/notes-edit-icon.png" style="width:48px; height:48px;">'},
{start: new Date(2010,7,29), content: '<div>Phone call</div><img src="img/Hardware-Mobile-Phone-icon.png" style="width:32px; height:32px;">'},
{start: new Date(2010,7,31), end: new Date(2010,8,3), content: 'Traject B'},
{start: new Date(2010,8,4,12,0,0), content: '<div>Report</div><img src="img/attachment-icon.png" style="width:32px; height:32px;">'}
];
var options = {
editable: true,
margin: {
item: 20,
axis: 40
}
};
var timeline = new vis.Timeline(container, items, options);
</script>
</body>
</html>

+ 88
- 0
examples/timeline/13_past_and_future.html View File

@ -0,0 +1,88 @@
<html>
<head>
<title>Timeline | Past and future</title>
<style type="text/css">
body {
font: 11pt verdana;
}
.vis.timeline .item.past {
filter: alpha(opacity=50);
opacity: 0.5;
}
</style>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<p style="width: 600px;">
When the custom time bar is shown, the user can drag this bar to a specific
time. The Timeline sends an event that the custom time is changed, after
which the contents of the timeline can be changed according to the specified
time in past or future.
</p>
<div id="customTime">&nbsp;</div>
<p></p>
<div id="mytimeline"></div>
<script>
// create a data set
var data = new vis.DataSet([
{
id: 1,
start: new Date((new Date()).getTime() - 60 * 1000),
end: new Date(),
content: 'Dynamic event'
}
]);
// specify options
var options = {
showCurrentTime: true,
showCustomTime: true
};
// create a timeline
var container = document.getElementById('mytimeline');
timeline = new vis.Timeline(container, data, options);
// add event listener
timeline.on('timechange', function (event) {
document.getElementById("customTime").innerHTML = "Custom Time: " + event.time;
var item = data.get(1);
if (event.time > item.start) {
item.end = new Date(event.time);
var now = new Date();
if (event.time < now) {
item.content = "Dynamic event (past)";
item.className = 'past';
}
else if (event.time > now) {
item.content = "Dynamic event (future)";
item.className = 'future';
}
else {
item.content = "Dynamic event (now)";
item.className = 'now';
}
data.update(item);
}
});
// set a custom range from -2 minute to +3 minutes current time
var start = new Date((new Date()).getTime() - 2 * 60 * 1000);
var end = new Date((new Date()).getTime() + 3 * 60 * 1000);
timeline.setWindow(start, end);
</script>
</body>
</html>

+ 109
- 0
examples/timeline/14_a_lot_of_grouped_data.html View File

@ -0,0 +1,109 @@
<html>
<head>
<title>Timeline | A lot of grouped data</title>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
<style type="text/css">
body {
color: #4D4D4D;
font: 10pt arial;
}
</style>
</head>
<body onresize="/*timeline.checkResize();*/">
<h1>Timeline grouping performance</h1>
<p>
Choose a number of items:
<a href="?count=100">100</a>,
<a href="?count=1000">1000</a>,
<a href="?count=10000">10000</a>,
<a href="?count=10000">100000</a>
<p>
<p>
Current number of items: <span id='count'>100</span>
</p>
<div id="mytimeline"></div>
<script>
/**
* Get URL parameter
* http://www.netlobo.com/url_query_string_javascript.html
*/
function gup( name ) {
name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
var regexS = "[\\?&]"+name+"=([^&#]*)";
var regex = new RegExp( regexS );
var results = regex.exec( window.location.href );
if( results == null )
return "";
else
return results[1];
}
// get selected item count from url parameter
var count = (Number(gup('count')) || 1000);
// create groups
var groups = new vis.DataSet([
{id: 1, content: 'Truck&nbsp;1'},
{id: 2, content: 'Truck&nbsp;2'},
{id: 3, content: 'Truck&nbsp;3'},
{id: 4, content: 'Truck&nbsp;4'}
]);
// create items
var items = new vis.DataSet();
var order = 1;
var truck = 1;
for (var j = 0; j < 4; j++) {
var date = new Date();
for (var i = 0; i < count/4; i++) {
date.setHours(date.getHours() + 4 * (Math.random() < 0.2));
var start = new Date(date);
date.setHours(date.getHours() + 2 + Math.floor(Math.random()*4));
var end = new Date(date);
items.add({
id: order,
group: truck,
start: start,
end: end,
content: 'Order ' + order
});
order++;
}
truck++;
}
// specify options
var options = {
stack: false,
start: new Date(),
end: new Date(1000*60*60*24 + (new Date()).valueOf()),
editable: true,
margin: {
item: 10, // minimal margin between items
axis: 5 // minimal margin between items and the axis
},
orientation: 'top'
};
// create a Timeline
var container = document.getElementById('mytimeline');
timeline = new vis.Timeline(container, null, options);
timeline.setGroups(groups);
timeline.setItems(items);
document.getElementById('count').innerHTML = count;
</script>
</body>
</html>

+ 115
- 0
examples/timeline/15_item_class_names.html View File

@ -0,0 +1,115 @@
<html>
<head>
<title>Timeline | Item class names</title>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
<style type="text/css">
body, input {
font: 12pt verdana;
}
/* custom styles for individual items, load this after vis.css */
.vis.timeline .item.green {
background-color: greenyellow;
border-color: green;
}
/* create a custom sized dot at the bottom of the red item */
.vis.timeline .item.red {
background-color: red;
border-color: darkred;
color: white;
font-family: monospace;
box-shadow: 0 0 10px gray;
}
.vis.timeline .item.dot.red {
border-radius: 10px;
border-width: 10px;
}
.vis.timeline .item.line.red {
border-width: 5px;
}
.vis.timeline .item.box.red {
border-radius: 0;
border-width: 2px;
font-size: 24pt;
font-weight: bold;
}
.vis.timeline .item.orange {
background-color: gold;
border-color: orange;
}
.vis.timeline .item.orange.selected {
/* custom colors for selected orange items */
background-color: orange;
border-color: orangered;
}
.vis.timeline .item.magenta {
background-color: magenta;
border-color: purple;
color: white;
}
/* our custom classes overrule the styles for selected events,
so lets define a new style for the selected events */
.vis.timeline .item.selected {
background-color: white;
border-color: black;
color: black;
box-shadow: 0 0 10px gray;
}
</style>
</head>
<body>
<p>This page demonstrates the Timeline with custom css classes for individual items.</p>
<div id="mytimeline"></div>
<script type="text/javascript">
// create data
var data = [
{
'start': new Date(2012,7,19),
'content': 'default'
},
{
'start': new Date(2012,7,23),
'content': 'green',
'className': 'green'
},
{
'start': new Date(2012,7,29),
'content': 'red',
'className': 'red'
},
{
'start': new Date(2012,7,27),
'end': new Date(2012,8,1),
'content': 'orange',
'className': 'orange'
},
{
'start': new Date(2012,8,2),
'content': 'magenta',
'className': 'magenta'
}
];
// specify options
var options = {
editable: true
};
// create the timeline
var container = document.getElementById('mytimeline');
timeline = new vis.Timeline(container, data, options);
</script>
</body>
</html>

+ 11
- 2
examples/timeline/index.html View File

@ -13,13 +13,22 @@
<h1>vis.js timeline examples</h1>
<p><a href="01_basic.html">01_basic.html</a></p>
<p><a href="02_dataset.html">02_dataset.html</a></p>
<p><a href="03_much_data.html">03_much_data.html</a></p>
<p><a href="02_interactive.html">02_dataset.html</a></p>
<p><a href="03_a_lot_of_data.html">03_a_lot_of_data.html</a></p>
<p><a href="04_html_data.html">04_html_data.html</a></p>
<p><a href="05_groups.html">05_groups.html</a></p>
<p><a href="06_event_listeners.html">06_event_listeners.html</a></p>
<p><a href="07_custom_time_bar.html">07_custom_time_bar.html</a></p>
<p><a href="08_edit_items.html">08_edit_items.html</a></p>
<p><a href="09_order_groups.html">09_order_groups.html</a></p>
<p><a href="10_limit_move_and_zoom.html">10_limit_range_and_zoom.html</a></p>
<p><a href="11_points.html">11_points.html</a></p>
<p><a href="12_custom_styling.html">12_custom_styling.html</a></p>
<p><a href="13_past_and_future.html">13_past_and_future.html</a></p>
<p><a href="14_a_lot_of_grouped_data.html">14_a_lot_of_grouped_data.html</a></p>
<p><a href="15_item_class_names.html">15_item_class_names.html</a></p>
<p><a href="requirejs/requirejs_example.html">requirejs_example.html</a></p>
</div>
</body>

+ 3
- 0
misc/how_to_publish.md View File

@ -70,6 +70,9 @@ This generates the vis.js library in the folder `./dist`.
- Move the created zip file `vis.zip` to the `download` folder in the
`github-pages` branch. TODO: this should be automated.
- Check if there are new or updated examples, and update the gallery screenshots
accordingly.
- Go to the `github-pages` branch and run the following script:
node updateversion.js

+ 1
- 1
package.json View File

@ -1,6 +1,6 @@
{
"name": "vis",
"version": "0.7.4",
"version": "1.0.0",
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",
"repository": {

+ 17
- 9
src/DataSet.js View File

@ -26,6 +26,7 @@
* - gives triggers upon changes in the data
* - can import/export data in various data formats
*
* @param {Array | DataTable} [data] Optional array with initial data
* @param {Object} [options] Available options:
* {String} fieldId Field name of the id in the
* items, 'id' by default.
@ -35,9 +36,15 @@
* @constructor DataSet
*/
// TODO: add a DataSet constructor DataSet(data, options)
function DataSet (options) {
function DataSet (data, options) {
this.id = util.randomUUID();
// correctly read optional arguments
if (data && !Array.isArray(data) && !util.isDataTable(data)) {
options = data;
data = null;
}
this.options = options || {};
this.data = {}; // map with data indexed by id
this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
@ -58,10 +65,13 @@ function DataSet (options) {
}
}
// event subscribers
this.subscribers = {};
this.subscribers = {}; // event subscribers
this.internalIds = {}; // internally generated id's
this.internalIds = {}; // internally generated id's
// add initial data when provided
if (data) {
this.add(data);
}
}
/**
@ -511,7 +521,6 @@ DataSet.prototype.getIds = function (options) {
/**
* Execute a callback function for every item in the dataset.
* The order of the items is not determined.
* @param {function} callback
* @param {Object} [options] Available options:
* {Object.<String, String>} [convert]
@ -757,9 +766,8 @@ DataSet.prototype.min = function (field) {
/**
* Find all distinct values of a specified field
* @param {String} field
* @return {Array} values Array containing all distinct values. If the data
* items do not contain the specified field, an array
* containing a single value undefined is returned.
* @return {Array} values Array containing all distinct values. If data items
* do not contain the specified field are ignored.
* The returned array is unordered.
*/
DataSet.prototype.distinct = function (field) {
@ -779,7 +787,7 @@ DataSet.prototype.distinct = function (field) {
break;
}
}
if (!exists) {
if (!exists && (value !== undefined)) {
values[count] = value;
count++;
}

+ 8
- 4
src/graph/Edge.js View File

@ -35,6 +35,7 @@ function Edge (properties, graph, constants) {
this.customLength = false;
this.selected = false;
this.smooth = constants.smoothCurves;
this.arrowScaleFactor = constants.edges.arrowScaleFactor;
this.from = null; // a node
this.to = null; // a node
@ -95,6 +96,9 @@ Edge.prototype.setProperties = function(properties, constants) {
if (properties.length !== undefined) {this.length = properties.length;
this.customLength = true;}
// scale the arrow
if (properties.arrowScaleFactor !== undefined) {this.arrowScaleFactor = properties.arrowScaleFactor;}
// Added to support dashed lines
// David Jordan
// 2012-08-08
@ -511,7 +515,7 @@ Edge.prototype._drawArrowCenter = function(ctx) {
this._line(ctx);
var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
var length = 10 + 5 * this.width; // TODO: make customizable?
var length = (10 + 5 * this.width) * this.arrowScaleFactor;
// draw an arrow halfway the line
if (this.smooth == true) {
var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
@ -551,7 +555,7 @@ Edge.prototype._drawArrowCenter = function(ctx) {
// draw all arrows
var angle = 0.2 * Math.PI;
var length = 10 + 5 * this.width; // TODO: make customizable?
var length = (10 + 5 * this.width) * this.arrowScaleFactor;
point = this._pointOnCircle(x, y, radius, 0.5);
ctx.arrow(point.x, point.y, angle, length);
ctx.fill();
@ -625,7 +629,7 @@ Edge.prototype._drawArrow = function(ctx) {
ctx.stroke();
// draw arrow at the end of the line
length = 10 + 5 * this.width;
length = (10 + 5 * this.width) * this.arrowScaleFactor;
ctx.arrow(xTo, yTo, angle, length);
ctx.fill();
ctx.stroke();
@ -676,7 +680,7 @@ Edge.prototype._drawArrow = function(ctx) {
ctx.stroke();
// draw all arrows
length = 10 + 5 * this.width; // TODO: make customizable?
var length = (10 + 5 * this.width) * this.arrowScaleFactor;
ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
ctx.fill();
ctx.stroke();

+ 43
- 9
src/graph/Graph.js View File

@ -73,6 +73,7 @@ function Graph (container, data, options) {
fontSize: 14, // px
fontFace: 'arial',
fontFill: 'white',
arrowScaleFactor: 1,
dash: {
length: 10,
gap: 5,
@ -371,6 +372,7 @@ Graph.prototype._centerGraph = function(range) {
* This function zooms out to fit all data on screen based on amount of nodes
*
* @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
* @param {Boolean} [disableStart] | If true, start is not called.
*/
Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
if (initialZoom === undefined) {
@ -627,6 +629,7 @@ Graph.prototype.setOptions = function (options) {
}
}
if (options.edges.color !== undefined) {
if (util.isString(options.edges.color)) {
this.constants.edges.color = {};
@ -956,7 +959,7 @@ Graph.prototype._handleOnDrag = function(event) {
this.drag.translation.x + diffX,
this.drag.translation.y + diffY);
this._redraw();
this.moved = true;
this.moving = true;
}
};
@ -1359,12 +1362,13 @@ Graph.prototype._updateNodes = function(ids) {
// create node
node = new Node(properties, this.images, this.groups, this.constants);
nodes[id] = node;
if (!node.isFixed()) {
this.moving = true;
}
}
}
this.moving = true;
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
this._resetLevels();
this._setupHierarchicalLayout();
}
this._updateNodeIndexList();
this._reconnectEdges();
this._updateValueRange(nodes);
@ -1809,7 +1813,12 @@ Graph.prototype._stabilize = function() {
this.emit("stabilized",{iterations:count});
};
/**
* When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
* because only the supportnodes for the smoothCurves have to settle.
*
* @private
*/
Graph.prototype._freezeDefinedNodes = function() {
var nodes = this.nodes;
for (var id in nodes) {
@ -1824,6 +1833,11 @@ Graph.prototype._freezeDefinedNodes = function() {
}
};
/**
* Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
*
* @private
*/
Graph.prototype._restoreFrozenNodes = function() {
var nodes = this.nodes;
for (var id in nodes) {
@ -1894,7 +1908,11 @@ Graph.prototype._discreteStepNodes = function() {
}
};
/**
* A single simulation step (or "tick") in the physics simulation
*
* @private
*/
Graph.prototype._physicsTick = function() {
if (!this.freezeSimulation) {
if (this.moving) {
@ -2013,7 +2031,12 @@ Graph.prototype.toggleFreeze = function() {
};
/**
* This function cleans the support nodes if they are not needed and adds them when they are.
*
* @param {boolean} [disableStart]
* @private
*/
Graph.prototype._configureSmoothCurves = function(disableStart) {
if (disableStart === undefined) {
disableStart = true;
@ -2039,6 +2062,13 @@ Graph.prototype._configureSmoothCurves = function(disableStart) {
}
};
/**
* Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
* are used for the force calculation.
*
* @private
*/
Graph.prototype._createBezierNodes = function() {
if (this.constants.smoothCurves == true) {
for (var edgeId in this.edges) {
@ -2063,7 +2093,11 @@ Graph.prototype._createBezierNodes = function() {
}
};
/**
* load the functions that load the mixins into the prototype.
*
* @private
*/
Graph.prototype._initializeMixinLoaders = function () {
for (var mixinFunction in graphMixinLoaders) {
if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {

+ 1
- 0
src/graph/Node.js View File

@ -356,6 +356,7 @@ Node.prototype.discreteStep = function(interval) {
/**
* Perform one discrete step for the node
* @param {number} interval Time interval in seconds
* @param {number} maxVelocity The speed limit imposed on the velocity
*/
Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
if (!this.xFixed) {

+ 4
- 2
src/graph/graphMixins/ClusterMixin.js View File

@ -137,6 +137,7 @@ var ClusterMixin = {
* @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
* @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
* @param {Boolean} force | enabled or disable forcing
* @param {Boolean} doNotStart | if true do not call start
*
*/
updateClusters : function(zoomDirection,recursive,force,doNotStart) {
@ -987,9 +988,10 @@ var ClusterMixin = {
var maxLevel = 0;
var minLevel = 1e9;
var clusterLevel = 0;
var nodeId;
// we loop over all nodes in the list
for (var nodeId in this.nodes) {
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
clusterLevel = this.nodes[nodeId].clusterSessions.length;
if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
@ -1001,7 +1003,7 @@ var ClusterMixin = {
var amountOfNodes = this.nodeIndices.length;
var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
// we loop over all nodes in the list
for (var nodeId in this.nodes) {
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
this._clusterToSmallestNeighbour(this.nodes[nodeId]);

+ 3
- 3
src/graph/graphMixins/HierarchicalLayoutMixin.js View File

@ -123,7 +123,7 @@ var HierarchicalLayoutMixin = {
*/
_getDistribution : function() {
var distribution = {};
var nodeId, node;
var nodeId, node, level;
// we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
// the fix of X is removed after the x value has been set.
@ -148,7 +148,7 @@ var HierarchicalLayoutMixin = {
// determine the largest amount of nodes of all levels
var maxCount = 0;
for (var level in distribution) {
for (level in distribution) {
if (distribution.hasOwnProperty(level)) {
if (maxCount < distribution[level].amount) {
maxCount = distribution[level].amount;
@ -157,7 +157,7 @@ var HierarchicalLayoutMixin = {
}
// set the initial position and spacing of each nodes accordingly
for (var level in distribution) {
for (level in distribution) {
if (distribution.hasOwnProperty(level)) {
distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
distribution[level].nodeSpacing /= (distribution[level].amount + 1);

+ 0
- 2
src/graph/graphMixins/ManipulationMixin.js View File

@ -288,8 +288,6 @@ var manipulationMixin = {
/**
* Adds a node on the specified location
*
* @param {Object} pointer
*/
_addNode : function() {
if (this._selectionIsEmpty() && this.editMode == true) {

+ 6
- 6
src/graph/graphMixins/MixinLoader.js View File

@ -67,15 +67,15 @@ var graphMixinLoaders = {
* @private
*/
_loadSectorSystem: function () {
this.sectors = { },
this.activeSector = ["default"];
this.sectors["active"] = { },
this.sectors["active"]["default"] = {"nodes": {},
this.sectors = {};
this.activeSector = ["default"];
this.sectors["active"] = {};
this.sectors["active"]["default"] = {"nodes": {},
"edges": {},
"nodeIndices": [],
"formationScale": 1.0,
"drawingNode": undefined };
this.sectors["frozen"] = {},
this.sectors["frozen"] = {};
this.sectors["support"] = {"nodes": {},
"edges": {},
"nodeIndices": [],
@ -108,7 +108,7 @@ var graphMixinLoaders = {
_loadManipulationSystem: function () {
// reset global variables -- these are used by the selection of nodes and edges.
this.blockConnectingEdgeSelection = false;
this.forceAppendSelection = false
this.forceAppendSelection = false;
if (this.constants.dataManipulation.enabled == true) {
// load the manipulator HTML elements. All styling done in css.

+ 1
- 1
src/graph/graphMixins/SelectionMixin.js View File

@ -174,7 +174,7 @@ var SelectionMixin = {
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
this.selectionObj.edges[edgeId].unselect();;
this.selectionObj.edges[edgeId].unselect();
}
}

+ 23
- 0
src/graph/graphMixins/physics/BarnesHut.js View File

@ -156,6 +156,13 @@ var barnesHutMixin = {
},
/**
* this updates the mass of a branch. this is increased by adding a node.
*
* @param parentBranch
* @param node
* @private
*/
_updateBranchMass : function(parentBranch, node) {
var totalMass = parentBranch.mass + node.mass;
var totalMassInv = 1/totalMass;
@ -173,6 +180,14 @@ var barnesHutMixin = {
},
/**
* determine in which branch the node will be placed.
*
* @param parentBranch
* @param node
* @param skipMassUpdate
* @private
*/
_placeInTree : function(parentBranch,node,skipMassUpdate) {
if (skipMassUpdate != true || skipMassUpdate === undefined) {
// update the mass of the branch.
@ -198,6 +213,14 @@ var barnesHutMixin = {
},
/**
* actually place the node in a region (or branch)
*
* @param parentBranch
* @param node
* @param region
* @private
*/
_placeInRegion : function(parentBranch,node,region) {
switch (parentBranch.children[region].childrenCount) {
case 0: // place node here

+ 29
- 1
src/graph/graphMixins/physics/PhysicsMixin.js View File

@ -464,6 +464,13 @@ var physicsMixin = {
}
},
/**
* This overwrites the this.constants.
*
* @param constantsVariableName
* @param value
* @private
*/
_overWriteGraphConstants: function (constantsVariableName, value) {
var nameArray = constantsVariableName.split("_");
if (nameArray.length == 1) {
@ -478,6 +485,9 @@ var physicsMixin = {
}
};
/**
* this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
*/
function graphToggleSmoothCurves () {
this.constants.smoothCurves = !this.constants.smoothCurves;
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
@ -487,6 +497,10 @@ function graphToggleSmoothCurves () {
this._configureSmoothCurves(false);
};
/**
* this function is used to scramble the nodes
*
*/
function graphRepositionNodes () {
for (var nodeId in this.calculationNodes) {
if (this.calculationNodes.hasOwnProperty(nodeId)) {
@ -504,6 +518,9 @@ function graphRepositionNodes () {
this.start();
};
/**
* this is used to generate an options file from the playing with physics system.
*/
function graphGenerateOptions () {
var options = "No options are required, default values used.";
var optionsSpecific = [];
@ -601,7 +618,10 @@ function graphGenerateOptions () {
};
/**
* this is used to switch between barnesHut, repulsion and hierarchical.
*
*/
function switchConfigurations () {
var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
@ -640,6 +660,14 @@ function switchConfigurations () {
}
/**
* this generates the ranges depending on the iniital values.
*
* @param id
* @param map
* @param constantsVariableName
*/
function showValueOfRange (id,map,constantsVariableName) {
var valueId = id + "_value";
var rangeValue = document.getElementById(id).value;

+ 1
- 2
src/module/exports.js View File

@ -4,11 +4,10 @@
var vis = {
util: util,
Controller: Controller,
DataSet: DataSet,
DataView: DataView,
Range: Range,
Stack: Stack,
stack: stack,
TimeStep: TimeStep,
components: {

+ 0
- 183
src/timeline/Controller.js View File

@ -1,183 +0,0 @@
/**
* @constructor Controller
*
* A Controller controls the reflows and repaints of all components,
* and is used as an event bus for all components.
*/
function Controller () {
var me = this;
this.id = util.randomUUID();
this.components = {};
/**
* Listen for a 'request-reflow' event. The controller will schedule a reflow
* @param {Boolean} [force] If true, an immediate reflow is forced. Default
* is false.
*/
var reflowTimer = null;
this.on('request-reflow', function requestReflow(force) {
if (force) {
me.reflow();
}
else {
if (!reflowTimer) {
reflowTimer = setTimeout(function () {
reflowTimer = null;
me.reflow();
}, 0);
}
}
});
/**
* Request a repaint. The controller will schedule a repaint
* @param {Boolean} [force] If true, an immediate repaint is forced. Default
* is false.
*/
var repaintTimer = null;
this.on('request-repaint', function requestRepaint(force) {
if (force) {
me.repaint();
}
else {
if (!repaintTimer) {
repaintTimer = setTimeout(function () {
repaintTimer = null;
me.repaint();
}, 0);
}
}
});
}
// Extend controller with Emitter mixin
Emitter(Controller.prototype);
/**
* Add a component to the controller
* @param {Component} component
*/
Controller.prototype.add = function add(component) {
// validate the component
if (component.id == undefined) {
throw new Error('Component has no field id');
}
if (!(component instanceof Component) && !(component instanceof Controller)) {
throw new TypeError('Component must be an instance of ' +
'prototype Component or Controller');
}
// add the component
component.setController(this);
this.components[component.id] = component;
};
/**
* Remove a component from the controller
* @param {Component | String} component
*/
Controller.prototype.remove = function remove(component) {
var id;
for (id in this.components) {
if (this.components.hasOwnProperty(id)) {
if (id == component || this.components[id] === component) {
break;
}
}
}
if (id) {
// unregister the controller (gives the component the ability to unregister
// event listeners and clean up other stuff)
this.components[id].setController(null);
delete this.components[id];
}
};
/**
* Repaint all components
*/
Controller.prototype.repaint = function repaint() {
var changed = false;
// cancel any running repaint request
if (this.repaintTimer) {
clearTimeout(this.repaintTimer);
this.repaintTimer = undefined;
}
var done = {};
function repaint(component, id) {
if (!(id in done)) {
// first repaint the components on which this component is dependent
if (component.depends) {
component.depends.forEach(function (dep) {
repaint(dep, dep.id);
});
}
if (component.parent) {
repaint(component.parent, component.parent.id);
}
// repaint the component itself and mark as done
changed = component.repaint() || changed;
done[id] = true;
}
}
util.forEach(this.components, repaint);
this.emit('repaint');
// immediately reflow when needed
if (changed) {
this.reflow();
}
// TODO: limit the number of nested reflows/repaints, prevent loop
};
/**
* Reflow all components
*/
Controller.prototype.reflow = function reflow() {
var resized = false;
// cancel any running repaint request
if (this.reflowTimer) {
clearTimeout(this.reflowTimer);
this.reflowTimer = undefined;
}
var done = {};
function reflow(component, id) {
if (!(id in done)) {
// first reflow the components on which this component is dependent
if (component.depends) {
component.depends.forEach(function (dep) {
reflow(dep, dep.id);
});
}
if (component.parent) {
reflow(component.parent, component.parent.id);
}
// reflow the component itself and mark as done
resized = component.reflow() || resized;
done[id] = true;
}
}
util.forEach(this.components, reflow);
this.emit('reflow');
// immediately repaint when needed
if (resized) {
this.repaint();
}
// TODO: limit the number of nested reflows/repaints, prevent loop
};

+ 53
- 93
src/timeline/Range.js View File

@ -3,20 +3,39 @@
* A Range controls a numeric range with a start and end value.
* The Range adjusts the range based on mouse events or programmatic changes,
* and triggers events when the range is changing or has been changed.
* @param {Object} [options] See description at Range.setOptions
* @extends Controller
* @param {RootPanel} root Root panel, used to subscribe to events
* @param {Panel} parent Parent panel, used to attach to the DOM
* @param {Object} [options] See description at Range.setOptions
*/
function Range(options) {
function Range(root, parent, options) {
this.id = util.randomUUID();
this.start = null; // Number
this.end = null; // Number
this.root = root;
this.parent = parent;
this.options = options || {};
// drag listeners for dragging
this.root.on('dragstart', this._onDragStart.bind(this));
this.root.on('drag', this._onDrag.bind(this));
this.root.on('dragend', this._onDragEnd.bind(this));
// ignore dragging when holding
this.root.on('hold', this._onHold.bind(this));
// mouse wheel for zooming
this.root.on('mousewheel', this._onMouseWheel.bind(this));
this.root.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
// pinch to zoom
this.root.on('touch', this._onTouch.bind(this));
this.root.on('pinch', this._onPinch.bind(this));
this.setOptions(options);
}
// extend the Range prototype with an event emitter mixin
// turn Range into an event emitter
Emitter(Range.prototype);
/**
@ -49,59 +68,6 @@ function validateDirection (direction) {
}
}
/**
* Add listeners for mouse and touch events to the component
* @param {Controller} controller
* @param {Component} component Should be a rootpanel
* @param {String} event Available events: 'move', 'zoom'
* @param {String} direction Available directions: 'horizontal', 'vertical'
*/
Range.prototype.subscribe = function (controller, component, event, direction) {
var me = this;
if (event == 'move') {
// drag start listener
controller.on('dragstart', function (event) {
me._onDragStart(event, component);
});
// drag listener
controller.on('drag', function (event) {
me._onDrag(event, component, direction);
});
// drag end listener
controller.on('dragend', function (event) {
me._onDragEnd(event, component);
});
// ignore dragging when holding
controller.on('hold', function (event) {
me._onHold();
});
}
else if (event == 'zoom') {
// mouse wheel
function mousewheel (event) {
me._onMouseWheel(event, component, direction);
}
controller.on('mousewheel', mousewheel);
controller.on('DOMMouseScroll', mousewheel); // For FF
// pinch
controller.on('touch', function (event) {
me._onTouch(event);
});
controller.on('pinch', function (event) {
me._onPinch(event, component, direction);
});
}
else {
throw new TypeError('Unknown event "' + event + '". ' +
'Choose "move" or "zoom".');
}
};
/**
* Set a new start and end range
* @param {Number} [start]
@ -111,8 +77,8 @@ Range.prototype.setRange = function(start, end) {
var changed = this._applyRange(start, end);
if (changed) {
var params = {
start: this.start,
end: this.end
start: new Date(this.start),
end: new Date(this.end)
};
this.emit('rangechange', params);
this.emit('rangechanged', params);
@ -280,10 +246,9 @@ var touchParams = {};
/**
* Start dragging horizontally or vertically
* @param {Event} event
* @param {Object} component
* @private
*/
Range.prototype._onDragStart = function(event, component) {
Range.prototype._onDragStart = 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 (touchParams.ignore) return;
@ -293,7 +258,7 @@ Range.prototype._onDragStart = function(event, component) {
touchParams.start = this.start;
touchParams.end = this.end;
var frame = component.frame;
var frame = this.parent.frame;
if (frame) {
frame.style.cursor = 'move';
}
@ -302,11 +267,10 @@ Range.prototype._onDragStart = function(event, component) {
/**
* Perform dragging operating.
* @param {Event} event
* @param {Component} component
* @param {String} direction 'horizontal' or 'vertical'
* @private
*/
Range.prototype._onDrag = function (event, component, direction) {
Range.prototype._onDrag = function (event) {
var direction = this.options.direction;
validateDirection(direction);
// TODO: reckon with option movable
@ -318,38 +282,37 @@ Range.prototype._onDrag = function (event, component, direction) {
var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
interval = (touchParams.end - touchParams.start),
width = (direction == 'horizontal') ? component.width : component.height,
width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
diffRange = -delta / width * interval;
this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
this.emit('rangechange', {
start: this.start,
end: this.end
start: new Date(this.start),
end: new Date(this.end)
});
};
/**
* Stop dragging operating.
* @param {event} event
* @param {Component} component
* @private
*/
Range.prototype._onDragEnd = function (event, component) {
Range.prototype._onDragEnd = 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 (touchParams.ignore) return;
// TODO: reckon with option movable
if (component.frame) {
component.frame.style.cursor = 'auto';
if (this.parent.frame) {
this.parent.frame.style.cursor = 'auto';
}
// fire a rangechanged event
this.emit('rangechanged', {
start: this.start,
end: this.end
start: new Date(this.start),
end: new Date(this.end)
});
};
@ -357,13 +320,9 @@ Range.prototype._onDragEnd = function (event, component) {
* Event handler for mouse wheel event, used to zoom
* Code from http://adomas.org/javascript-mouse-wheel/
* @param {Event} event
* @param {Component} component
* @param {String} direction 'horizontal' or 'vertical'
* @private
*/
Range.prototype._onMouseWheel = function(event, component, direction) {
validateDirection(direction);
Range.prototype._onMouseWheel = function(event) {
// TODO: reckon with option zoomable
// retrieve delta
@ -394,8 +353,8 @@ Range.prototype._onMouseWheel = function(event, component, direction) {
// calculate center, the date to zoom around
var gesture = util.fakeGesture(this, event),
pointer = getPointer(gesture.center, component.frame),
pointerDate = this._pointerToDate(component, direction, pointer);
pointer = getPointer(gesture.center, this.parent.frame),
pointerDate = this._pointerToDate(pointer);
this.zoom(scale, pointerDate);
}
@ -434,24 +393,23 @@ Range.prototype._onHold = function () {
/**
* Handle pinch event
* @param {Event} event
* @param {Component} component
* @param {String} direction 'horizontal' or 'vertical'
* @private
*/
Range.prototype._onPinch = function (event, component, direction) {
Range.prototype._onPinch = function (event) {
var direction = this.options.direction;
touchParams.ignore = true;
// TODO: reckon with option zoomable
if (event.gesture.touches.length > 1) {
if (!touchParams.center) {
touchParams.center = getPointer(event.gesture.center, component.frame);
touchParams.center = getPointer(event.gesture.center, this.parent.frame);
}
var scale = 1 / event.gesture.scale,
initDate = this._pointerToDate(component, direction, touchParams.center),
center = getPointer(event.gesture.center, component.frame),
date = this._pointerToDate(component, direction, center),
initDate = this._pointerToDate(touchParams.center),
center = getPointer(event.gesture.center, this.parent.frame),
date = this._pointerToDate(this.parent, center),
delta = date - initDate; // TODO: utilize delta
// calculate new start and end
@ -465,21 +423,23 @@ Range.prototype._onPinch = function (event, component, direction) {
/**
* Helper function to calculate the center date for zooming
* @param {Component} component
* @param {{x: Number, y: Number}} pointer
* @param {String} direction 'horizontal' or 'vertical'
* @return {number} date
* @private
*/
Range.prototype._pointerToDate = function (component, direction, pointer) {
Range.prototype._pointerToDate = function (pointer) {
var conversion;
var direction = this.options.direction;
validateDirection(direction);
if (direction == 'horizontal') {
var width = component.width;
var width = this.parent.width;
conversion = this.conversion(width);
return pointer.x / conversion.scale + conversion.offset;
}
else {
var height = component.height;
var height = this.parent.height;
conversion = this.conversion(height);
return pointer.y / conversion.scale + conversion.offset;
}

+ 0
- 190
src/timeline/Stack.js View File

@ -1,190 +0,0 @@
/**
* @constructor Stack
* Stacks items on top of each other.
* @param {ItemSet} itemset
* @param {Object} [options]
*/
function Stack (itemset, options) {
this.itemset = itemset;
this.options = options || {};
this.defaultOptions = {
order: function (a, b) {
//return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
// Order: ranges over non-ranges, ranged ordered by width, and
// lastly ordered by start.
if (a instanceof ItemRange) {
if (b instanceof ItemRange) {
var aInt = (a.data.end - a.data.start);
var bInt = (b.data.end - b.data.start);
return (aInt - bInt) || (a.data.start - b.data.start);
}
else {
return -1;
}
}
else {
if (b instanceof ItemRange) {
return 1;
}
else {
return (a.data.start - b.data.start);
}
}
},
margin: {
item: 10
}
};
this.ordered = []; // ordered items
}
/**
* Set options for the stack
* @param {Object} options Available options:
* {ItemSet} itemset
* {Number} margin
* {function} order Stacking order
*/
Stack.prototype.setOptions = function setOptions (options) {
util.extend(this.options, options);
// TODO: register on data changes at the connected itemset, and update the changed part only and immediately
};
/**
* Stack the items such that they don't overlap. The items will have a minimal
* distance equal to options.margin.item.
*/
Stack.prototype.update = function update() {
this._order();
this._stack();
};
/**
* Order the items. If a custom order function has been provided via the options,
* then this will be used.
* @private
*/
Stack.prototype._order = function _order () {
var items = this.itemset.items;
if (!items) {
throw new Error('Cannot stack items: ItemSet does not contain items');
}
// TODO: store the sorted items, to have less work later on
var ordered = [];
var index = 0;
// items is a map (no array)
util.forEach(items, function (item) {
if (item.visible) {
ordered[index] = item;
index++;
}
});
//if a customer stack order function exists, use it.
var order = this.options.order || this.defaultOptions.order;
if (!(typeof order === 'function')) {
throw new Error('Option order must be a function');
}
ordered.sort(order);
this.ordered = ordered;
};
/**
* Adjust vertical positions of the events such that they don't overlap each
* other.
* @private
*/
Stack.prototype._stack = function _stack () {
var i,
iMax,
ordered = this.ordered,
options = this.options,
orientation = options.orientation || this.defaultOptions.orientation,
axisOnTop = (orientation == 'top'),
margin;
if (options.margin && options.margin.item !== undefined) {
margin = options.margin.item;
}
else {
margin = this.defaultOptions.margin.item
}
// calculate new, non-overlapping positions
for (i = 0, iMax = ordered.length; i < iMax; i++) {
var item = ordered[i];
var collidingItem = null;
do {
// TODO: optimize checking for overlap. when there is a gap without items,
// you only need to check for items from the next item on, not from zero
collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
if (collidingItem != null) {
// There is a collision. Reposition the event above the colliding element
if (axisOnTop) {
item.top = collidingItem.top + collidingItem.height + margin;
}
else {
item.top = collidingItem.top - item.height - margin;
}
}
} while (collidingItem);
}
};
/**
* Check if the destiny position of given item overlaps with any
* of the other items from index itemStart to itemEnd.
* @param {Array} items Array with items
* @param {int} itemIndex Number of the item to be checked for overlap
* @param {int} itemStart First item to be checked.
* @param {int} itemEnd Last item to be checked.
* @return {Object | null} colliding item, or undefined when no collisions
* @param {Number} margin A minimum required margin.
* If margin is provided, the two items will be
* marked colliding when they overlap or
* when the margin between the two is smaller than
* the requested margin.
*/
Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
itemStart, itemEnd, margin) {
var collision = this.collision;
// we loop from end to start, as we suppose that the chance of a
// collision is larger for items at the end, so check these first.
var a = items[itemIndex];
for (var i = itemEnd; i >= itemStart; i--) {
var b = items[i];
if (collision(a, b, margin)) {
if (i != itemIndex) {
return b;
}
}
}
return null;
};
/**
* Test if the two provided items collide
* The items must have parameters left, width, top, and height.
* @param {Component} a The first item
* @param {Component} b The second item
* @param {Number} margin A minimum required margin.
* If margin is provided, the two items will be
* marked colliding when they overlap or
* when the margin between the two is smaller than
* the requested margin.
* @return {boolean} true if a and b collide, else false
*/
Stack.prototype.collision = function collision (a, b, margin) {
return ((a.left - margin) < (b.left + b.width) &&
(a.left + a.width + margin) > b.left &&
(a.top - margin) < (b.top + b.height) &&
(a.top + a.height + margin) > b.top);
};

+ 328
- 224
src/timeline/Timeline.js View File

@ -6,12 +6,24 @@
* @constructor
*/
function Timeline (container, items, options) {
// validate arguments
if (!container) throw new Error('No container element provided');
var me = this;
var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
this.options = {
orientation: 'bottom',
direction: 'horizontal', // 'horizontal' or 'vertical'
autoResize: true,
editable: false,
stack: true,
editable: {
updateTime: false,
updateGroup: false,
add: false,
remove: false
},
selectable: true,
snap: null, // will be specified after timeaxis is created
@ -27,6 +39,14 @@ function Timeline (container, items, options) {
showCurrentTime: false,
showCustomTime: false,
type: 'box',
align: 'center',
margin: {
axis: 20,
item: 10
},
padding: 5,
onAdd: function (item, callback) {
callback(item);
},
@ -38,112 +58,205 @@ function Timeline (container, items, options) {
},
onRemove: function (item, callback) {
callback(item);
}
};
},
// controller
this.controller = new Controller();
toScreen: me._toScreen.bind(me),
toTime: me._toTime.bind(me)
};
// root panel
if (!container) {
throw new Error('No container element provided');
}
var rootOptions = Object.create(this.options);
rootOptions.height = function () {
// TODO: change to height
if (me.options.height) {
// fixed height
return me.options.height;
}
else {
// auto height
return (me.timeaxis.height + me.content.height) + 'px';
var rootOptions = util.extend(Object.create(this.options), {
height: function () {
if (me.options.height) {
// fixed height
return me.options.height;
}
else {
// auto height
// TODO: implement a css based solution to automatically have the right hight
return (me.timeAxis.height + me.contentPanel.height) + 'px';
}
}
};
});
this.rootPanel = new RootPanel(container, rootOptions);
this.controller.add(this.rootPanel);
// single select (or unselect) when tapping an item
this.controller.on('tap', this._onSelectItem.bind(this));
this.rootPanel.on('tap', this._onSelectItem.bind(this));
// multi select when holding mouse/touch, or on ctrl+click
this.controller.on('hold', this._onMultiSelectItem.bind(this));
this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
// add item on doubletap
this.controller.on('doubletap', this._onAddItem.bind(this));
this.rootPanel.on('doubletap', this._onAddItem.bind(this));
// item panel
var itemOptions = Object.create(this.options);
itemOptions.left = function () {
return me.labelPanel.width;
};
itemOptions.width = function () {
return me.rootPanel.width - me.labelPanel.width;
};
itemOptions.top = null;
itemOptions.height = null;
this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
this.controller.add(this.itemPanel);
// label panel
var labelOptions = Object.create(this.options);
labelOptions.top = null;
labelOptions.left = null;
labelOptions.height = null;
labelOptions.width = function () {
if (me.content && typeof me.content.getLabelsWidth === 'function') {
return me.content.getLabelsWidth();
}
else {
return 0;
// side panel
var sideOptions = util.extend(Object.create(this.options), {
top: function () {
return (sideOptions.orientation == 'top') ? '0' : '';
},
bottom: function () {
return (sideOptions.orientation == 'top') ? '' : '0';
},
left: '0',
right: null,
height: '100%',
width: function () {
if (me.itemSet) {
return me.itemSet.getLabelsWidth();
}
else {
return 0;
}
},
className: function () {
return 'side' + (me.groupsData ? '' : ' hidden');
}
};
this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
this.controller.add(this.labelPanel);
});
this.sidePanel = new Panel(sideOptions);
this.rootPanel.appendChild(this.sidePanel);
// main panel (contains time axis and itemsets)
var mainOptions = util.extend(Object.create(this.options), {
left: function () {
// we align left to enable a smooth resizing of the window
return me.sidePanel.width;
},
right: null,
height: '100%',
width: function () {
return me.rootPanel.width - me.sidePanel.width;
},
className: 'main'
});
this.mainPanel = new Panel(mainOptions);
this.rootPanel.appendChild(this.mainPanel);
// range
// TODO: move range inside rootPanel?
var rangeOptions = Object.create(this.options);
this.range = new Range(rangeOptions);
this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
this.range.setRange(
now.clone().add('days', -3).valueOf(),
now.clone().add('days', 4).valueOf()
);
this.range.subscribe(this.controller, this.rootPanel, 'move', 'horizontal');
this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal');
this.range.on('rangechange', function (properties) {
var force = true;
me.controller.emit('rangechange', properties);
me.controller.emit('request-reflow', force);
me.rootPanel.repaint();
me.emit('rangechange', properties);
});
this.range.on('rangechanged', function (properties) {
var force = true;
me.controller.emit('rangechanged', properties);
me.controller.emit('request-reflow', force);
me.rootPanel.repaint();
me.emit('rangechanged', properties);
});
// panel with time axis
var timeAxisOptions = util.extend(Object.create(rootOptions), {
range: this.range,
left: null,
top: null,
width: null,
height: null
});
this.timeAxis = new TimeAxis(timeAxisOptions);
this.timeAxis.setRange(this.range);
this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
this.mainPanel.appendChild(this.timeAxis);
// content panel (contains itemset(s))
var contentOptions = util.extend(Object.create(this.options), {
top: function () {
return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
},
bottom: function () {
return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
},
left: null,
right: null,
height: null,
width: null,
className: 'content'
});
this.contentPanel = new Panel(contentOptions);
this.mainPanel.appendChild(this.contentPanel);
// content panel (contains the vertical lines of box items)
var backgroundOptions = util.extend(Object.create(this.options), {
top: function () {
return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
},
bottom: function () {
return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
},
left: null,
right: null,
height: function () {
return me.contentPanel.height;
},
width: null,
className: 'background'
});
this.backgroundPanel = new Panel(backgroundOptions);
this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel);
// panel with axis holding the dots of item boxes
var axisPanelOptions = util.extend(Object.create(rootOptions), {
left: 0,
top: function () {
return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
},
bottom: function () {
return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
},
width: '100%',
height: 0,
className: 'axis'
});
this.axisPanel = new Panel(axisPanelOptions);
this.mainPanel.appendChild(this.axisPanel);
// time axis
var timeaxisOptions = Object.create(rootOptions);
timeaxisOptions.range = this.range;
timeaxisOptions.left = null;
timeaxisOptions.top = null;
timeaxisOptions.width = '100%';
timeaxisOptions.height = null;
this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
this.timeaxis.setRange(this.range);
this.controller.add(this.timeaxis);
this.options.snap = this.timeaxis.snap.bind(this.timeaxis);
// content panel (contains itemset(s))
var sideContentOptions = util.extend(Object.create(this.options), {
top: function () {
return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
},
bottom: function () {
return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
},
left: null,
right: null,
height: null,
width: null,
className: 'side-content'
});
this.sideContentPanel = new Panel(sideContentOptions);
this.sidePanel.appendChild(this.sideContentPanel);
// current time bar
this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
this.controller.add(this.currenttime);
// Note: time bar will be attached in this.setOptions when selected
this.currentTime = new CurrentTime(this.range, rootOptions);
// custom time bar
this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
this.controller.add(this.customtime);
// Note: time bar will be attached in this.setOptions when selected
this.customTime = new CustomTime(rootOptions);
this.customTime.on('timechange', function (time) {
me.emit('timechange', time);
});
this.customTime.on('timechanged', function (time) {
me.emit('timechanged', time);
});
// create groupset
this.setGroups(null);
// itemset containing items and groups
var itemOptions = util.extend(Object.create(this.options), {
left: null,
right: null,
top: null,
bottom: null,
width: null,
height: null
});
this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, this.sideContentPanel, itemOptions);
this.itemSet.setRange(this.range);
this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
this.contentPanel.appendChild(this.itemSet);
this.itemsData = null; // DataSet
this.groupsData = null; // DataSet
@ -153,30 +266,14 @@ function Timeline (container, items, options) {
this.setOptions(options);
}
// create itemset and groupset
// create itemset
if (items) {
this.setItems(items);
}
}
/**
* Add an event listener to the timeline
* @param {String} event Available events: select, rangechange, rangechanged,
* timechange, timechanged
* @param {function} callback
*/
Timeline.prototype.on = function on (event, callback) {
this.controller.on(event, callback);
};
/**
* Add an event listener from the timeline
* @param {String} event
* @param {function} callback
*/
Timeline.prototype.off = function off (event, callback) {
this.controller.off(event, callback);
};
// turn Timeline into an event emitter
Emitter(Timeline.prototype);
/**
* Set options
@ -185,6 +282,17 @@ Timeline.prototype.off = function off (event, callback) {
Timeline.prototype.setOptions = function (options) {
util.extend(this.options, options);
if ('editable' in options) {
var isBoolean = typeof options.editable === 'boolean';
this.options.editable = {
updateTime: isBoolean ? options.editable : (options.editable.updateTime || false),
updateGroup: isBoolean ? options.editable : (options.editable.updateGroup || false),
add: isBoolean ? options.editable : (options.editable.add || false),
remove: isBoolean ? options.editable : (options.editable.remove || false)
};
}
// force update of range (apply new min/max etc.)
// both start and end are optional
this.range.setRange(options.start, options.end);
@ -200,6 +308,9 @@ Timeline.prototype.setOptions = function (options) {
}
}
// force the itemSet to refresh: options like orientation and margins may be changed
this.itemSet.markDirty();
// validate the callback functions
var validateCallback = (function (fn) {
if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
@ -208,8 +319,39 @@ Timeline.prototype.setOptions = function (options) {
}).bind(this);
['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
this.controller.reflow();
this.controller.repaint();
// add/remove the current time bar
if (this.options.showCurrentTime) {
if (!this.mainPanel.hasChild(this.currentTime)) {
this.mainPanel.appendChild(this.currentTime);
this.currentTime.start();
}
}
else {
if (this.mainPanel.hasChild(this.currentTime)) {
this.currentTime.stop();
this.mainPanel.removeChild(this.currentTime);
}
}
// add/remove the custom time bar
if (this.options.showCustomTime) {
if (!this.mainPanel.hasChild(this.customTime)) {
this.mainPanel.appendChild(this.customTime);
}
}
else {
if (this.mainPanel.hasChild(this.customTime)) {
this.mainPanel.removeChild(this.customTime);
}
}
// TODO: remove deprecation error one day (deprecated since version 0.8.0)
if (options && options.order) {
throw new Error('Option order is deprecated. There is no replacement for this feature.');
}
// repaint everything
this.rootPanel.repaint();
};
/**
@ -217,11 +359,11 @@ Timeline.prototype.setOptions = function (options) {
* @param {Date} time
*/
Timeline.prototype.setCustomTime = function (time) {
if (!this.customtime) {
if (!this.customTime) {
throw new Error('Cannot get custom time: Custom time bar is not enabled');
}
this.customtime.setCustomTime(time);
this.customTime.setCustomTime(time);
};
/**
@ -229,11 +371,11 @@ Timeline.prototype.setCustomTime = function (time) {
* @return {Date} customTime
*/
Timeline.prototype.getCustomTime = function() {
if (!this.customtime) {
if (!this.customTime) {
throw new Error('Cannot get custom time: Custom time bar is not enabled');
}
return this.customtime.getCustomTime();
return this.customTime.getCustomTime();
};
/**
@ -248,52 +390,30 @@ Timeline.prototype.setItems = function(items) {
if (!items) {
newDataSet = null;
}
else if (items instanceof DataSet) {
else if (items instanceof DataSet || items instanceof DataView) {
newDataSet = items;
}
if (!(items instanceof DataSet)) {
newDataSet = new DataSet({
else {
// turn an array into a dataset
newDataSet = new DataSet(items, {
convert: {
start: 'Date',
end: 'Date'
}
});
newDataSet.add(items);
}
// set items
this.itemsData = newDataSet;
this.content.setItems(newDataSet);
this.itemSet.setItems(newDataSet);
if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
// apply the data range as range
var dataRange = this.getItemRange();
// add 5% space on both sides
var start = dataRange.min;
var end = dataRange.max;
if (start != null && end != null) {
var interval = (end.valueOf() - start.valueOf());
if (interval <= 0) {
// prevent an empty interval
interval = 24 * 60 * 60 * 1000; // 1 day
}
start = new Date(start.valueOf() - interval * 0.05);
end = new Date(end.valueOf() + interval * 0.05);
}
this.fit();
// override specified start and/or end date
if (this.options.start != undefined) {
start = util.convert(this.options.start, 'Date');
}
if (this.options.end != undefined) {
end = util.convert(this.options.end, 'Date');
}
var start = (this.options.start != undefined) ? util.convert(this.options.start, 'Date') : null;
var end = (this.options.end != undefined) ? util.convert(this.options.end, 'Date') : null;
// apply range if there is a min or max available
if (start != null || end != null) {
this.range.setRange(start, end);
}
this.setWindow(start, end);
}
};
@ -301,77 +421,50 @@ Timeline.prototype.setItems = function(items) {
* Set groups
* @param {vis.DataSet | Array | google.visualization.DataTable} groups
*/
Timeline.prototype.setGroups = function(groups) {
var me = this;
this.groupsData = groups;
// switch content type between ItemSet or GroupSet when needed
var Type = this.groupsData ? GroupSet : ItemSet;
if (!(this.content instanceof Type)) {
// remove old content set
if (this.content) {
this.content.hide();
if (this.content.setItems) {
this.content.setItems(); // disconnect from items
}
if (this.content.setGroups) {
this.content.setGroups(); // disconnect from groups
}
this.controller.remove(this.content);
}
Timeline.prototype.setGroups = function setGroups(groups) {
// convert to type DataSet when needed
var newDataSet;
if (!groups) {
newDataSet = null;
}
else if (groups instanceof DataSet || groups instanceof DataView) {
newDataSet = groups;
}
else {
// turn an array into a dataset
newDataSet = new DataSet(groups);
}
// create new content set
var options = Object.create(this.options);
util.extend(options, {
top: function () {
if (me.options.orientation == 'top') {
return me.timeaxis.height;
}
else {
return me.itemPanel.height - me.timeaxis.height - me.content.height;
}
},
left: null,
width: '100%',
height: function () {
if (me.options.height) {
// fixed height
return me.itemPanel.height - me.timeaxis.height;
}
else {
// auto height
return null;
}
},
maxHeight: function () {
// TODO: change maxHeight to be a css string like '100%' or '300px'
if (me.options.maxHeight) {
if (!util.isNumber(me.options.maxHeight)) {
throw new TypeError('Number expected for property maxHeight');
}
return me.options.maxHeight - me.timeaxis.height;
}
else {
return null;
}
},
labelContainer: function () {
return me.labelPanel.getContainer();
}
});
this.groupsData = newDataSet;
this.itemSet.setGroups(newDataSet);
};
this.content = new Type(this.itemPanel, [this.timeaxis], options);
if (this.content.setRange) {
this.content.setRange(this.range);
}
if (this.content.setItems) {
this.content.setItems(this.itemsData);
}
if (this.content.setGroups) {
this.content.setGroups(this.groupsData);
/**
* Set Timeline window such that it fits all items
*/
Timeline.prototype.fit = function fit() {
// apply the data range as range
var dataRange = this.getItemRange();
// add 5% space on both sides
var start = dataRange.min;
var end = dataRange.max;
if (start != null && end != null) {
var interval = (end.valueOf() - start.valueOf());
if (interval <= 0) {
// prevent an empty interval
interval = 24 * 60 * 60 * 1000; // 1 day
}
this.controller.add(this.content);
start = new Date(start.valueOf() - interval * 0.05);
end = new Date(end.valueOf() + interval * 0.05);
}
// skip range set if there is no start and end date
if (start === null && end === null) {
return;
}
this.range.setRange(start, end);
};
/**
@ -421,7 +514,7 @@ Timeline.prototype.getItemRange = function getItemRange() {
* unselected.
*/
Timeline.prototype.setSelection = function setSelection (ids) {
if (this.content) this.content.setSelection(ids);
this.itemSet.setSelection(ids);
};
/**
@ -429,17 +522,30 @@ Timeline.prototype.setSelection = function setSelection (ids) {
* @return {Array} ids The ids of the selected items
*/
Timeline.prototype.getSelection = function getSelection() {
return this.content ? this.content.getSelection() : [];
return this.itemSet.getSelection();
};
/**
* Set the visible window. Both parameters are optional, you can change only
* start or only end.
* start or only end. Syntax:
*
* TimeLine.setWindow(start, end)
* TimeLine.setWindow(range)
*
* Where start and end can be a Date, number, or string, and range is an
* object with properties start and end.
*
* @param {Date | Number | String} [start] Start date of visible window
* @param {Date | Number | String} [end] End date of visible window
*/
Timeline.prototype.setWindow = function setWindow(start, end) {
this.range.setRange(start, end);
if (arguments.length == 1) {
var range = arguments[0];
this.range.setRange(range.start, range.end);
}
else {
this.range.setRange(start, end);
}
};
/**
@ -470,14 +576,20 @@ Timeline.prototype._onSelectItem = function (event) {
return;
}
var item = ItemSet.itemFromTarget(event);
var oldSelection = this.getSelection();
var item = ItemSet.itemFromTarget(event);
var selection = item ? [item.id] : [];
this.setSelection(selection);
this.controller.emit('select', {
items: this.getSelection()
});
var newSelection = this.getSelection();
// if selection is changed, emit a select event
if (!util.equalArray(oldSelection, newSelection)) {
this.emit('select', {
items: this.getSelection()
});
}
event.stopPropagation();
};
@ -489,7 +601,7 @@ Timeline.prototype._onSelectItem = function (event) {
*/
Timeline.prototype._onAddItem = function (event) {
if (!this.options.selectable) return;
if (!this.options.editable) return;
if (!this.options.editable.add) return;
var me = this,
item = ItemSet.itemFromTarget(event);
@ -507,17 +619,17 @@ Timeline.prototype._onAddItem = function (event) {
}
else {
// add item
var xAbs = vis.util.getAbsoluteLeft(this.rootPanel.frame);
var xAbs = vis.util.getAbsoluteLeft(this.contentPanel.frame);
var x = event.gesture.center.pageX - xAbs;
var newItem = {
start: this.timeaxis.snap(this._toTime(x)),
start: this.timeAxis.snap(this._toTime(x)),
content: 'new item'
};
var id = util.randomUUID();
newItem[this.itemsData.fieldId] = id;
var group = GroupSet.groupFromTarget(event);
var group = ItemSet.groupFromTarget(event);
if (group) {
newItem.group = group.groupId;
}
@ -526,15 +638,7 @@ Timeline.prototype._onAddItem = function (event) {
this.options.onAdd(newItem, function (item) {
if (item) {
me.itemsData.add(newItem);
// select the created item after it is repainted
me.controller.once('repaint', function () {
me.setSelection([id]);
me.controller.emit('select', {
items: me.getSelection()
});
}.bind(me));
// TODO: need to trigger a repaint?
}
});
}
@ -566,7 +670,7 @@ Timeline.prototype._onMultiSelectItem = function (event) {
}
this.setSelection(selection);
this.controller.emit('select', {
this.emit('select', {
items: this.getSelection()
});
@ -581,7 +685,7 @@ Timeline.prototype._onMultiSelectItem = function (event) {
* @private
*/
Timeline.prototype._toTime = function _toTime(x) {
var conversion = this.range.conversion(this.content.width);
var conversion = this.range.conversion(this.mainPanel.width);
return new Date(x / conversion.scale + conversion.offset);
};
@ -593,6 +697,6 @@ Timeline.prototype._toTime = function _toTime(x) {
* @private
*/
Timeline.prototype._toScreen = function _toScreen(time) {
var conversion = this.range.conversion(this.content.width);
var conversion = this.range.conversion(this.mainPanel.width);
return (time.valueOf() - conversion.offset) * conversion.scale;
};

+ 17
- 95
src/timeline/component/Component.js View File

@ -4,17 +4,18 @@
function Component () {
this.id = null;
this.parent = null;
this.depends = null;
this.controller = null;
this.childs = null;
this.options = null;
this.frame = null; // main DOM element
this.top = 0;
this.left = 0;
this.width = 0;
this.height = 0;
}
// Turn the Component into an event emitter
Emitter(Component.prototype);
/**
* Set parameters for the frame. Parameters will be merged in current parameter
* set.
@ -29,10 +30,7 @@ Component.prototype.setOptions = function setOptions(options) {
if (options) {
util.extend(this.options, options);
if (this.controller) {
this.requestRepaint();
this.requestReflow();
}
this.repaint();
}
};
@ -54,46 +52,18 @@ Component.prototype.getOption = function getOption(name) {
return value;
};
/**
* Set controller for this component, or remove current controller by passing
* null as parameter value.
* @param {Controller | null} controller
*/
Component.prototype.setController = function setController (controller) {
this.controller = controller || null;
};
/**
* Get controller of this component
* @return {Controller} controller
*/
Component.prototype.getController = function getController () {
return this.controller;
};
/**
* Get the container element of the component, which can be used by a child to
* add its own widgets. Not all components do have a container for childs, in
* that case null is returned.
* @returns {HTMLElement | null} container
*/
// TODO: get rid of the getContainer and getFrame methods, provide these via the options
Component.prototype.getContainer = function getContainer() {
// should be implemented by the component
return null;
};
/**
* Get the frame element of the component, the outer HTML DOM element.
* @returns {HTMLElement | null} frame
*/
Component.prototype.getFrame = function getFrame() {
return this.frame;
// should be implemented by the component
return null;
};
/**
* Repaint the component
* @return {Boolean} changed
* @return {boolean} Returns true if the component is resized
*/
Component.prototype.repaint = function repaint() {
// should be implemented by the component
@ -101,64 +71,16 @@ Component.prototype.repaint = function repaint() {
};
/**
* Reflow the component
* @return {Boolean} resized
* Test whether the component is resized since the last time _isResized() was
* called.
* @return {Boolean} Returns true if the component is resized
* @protected
*/
Component.prototype.reflow = function reflow() {
// should be implemented by the component
return false;
};
Component.prototype._isResized = function _isResized() {
var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height);
/**
* Hide the component from the DOM
* @return {Boolean} changed
*/
Component.prototype.hide = function hide() {
if (this.frame && this.frame.parentNode) {
this.frame.parentNode.removeChild(this.frame);
return true;
}
else {
return false;
}
};
/**
* Show the component in the DOM (when not already visible).
* A repaint will be executed when the component is not visible
* @return {Boolean} changed
*/
Component.prototype.show = function show() {
if (!this.frame || !this.frame.parentNode) {
return this.repaint();
}
else {
return false;
}
};
this._previousWidth = this.width;
this._previousHeight = this.height;
/**
* Request a repaint. The controller will schedule a repaint
*/
Component.prototype.requestRepaint = function requestRepaint() {
if (this.controller) {
this.controller.emit('request-repaint');
}
else {
throw new Error('Cannot request a repaint: no controller configured');
// TODO: just do a repaint when no parent is configured?
}
};
/**
* Request a reflow. The controller will schedule a reflow
*/
Component.prototype.requestReflow = function requestReflow() {
if (this.controller) {
this.controller.emit('request-reflow');
}
else {
throw new Error('Cannot request a reflow: no controller configured');
// TODO: just do a reflow when no parent is configured?
}
return resized;
};

+ 0
- 113
src/timeline/component/ContentPanel.js View File

@ -1,113 +0,0 @@
/**
* A content panel can contain a groupset or an itemset, and can handle
* vertical scrolling
* @param {Component} [parent]
* @param {Component[]} [depends] Components on which this components depends
* (except for the parent)
* @param {Object} [options] Available parameters:
* {String | Number | function} [left]
* {String | Number | function} [top]
* {String | Number | function} [width]
* {String | Number | function} [height]
* {String | function} [className]
* @constructor ContentPanel
* @extends Panel
*/
function ContentPanel(parent, depends, options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.options = options || {};
}
ContentPanel.prototype = new Component();
/**
* Set options. Will extend the current options.
* @param {Object} [options] Available parameters:
* {String | function} [className]
* {String | Number | function} [left]
* {String | Number | function} [top]
* {String | Number | function} [width]
* {String | Number | function} [height]
*/
ContentPanel.prototype.setOptions = Component.prototype.setOptions;
/**
* Get the container element of the panel, which can be used by a child to
* add its own widgets.
* @returns {HTMLElement} container
*/
ContentPanel.prototype.getContainer = function () {
return this.frame;
};
/**
* Repaint the component
* @return {Boolean} changed
*/
ContentPanel.prototype.repaint = function () {
var changed = 0,
update = util.updateProperty,
asSize = util.option.asSize,
options = this.options,
frame = this.frame;
if (!frame) {
frame = document.createElement('div');
frame.className = 'content-panel';
var className = options.className;
if (className) {
if (typeof className == 'function') {
util.addClassName(frame, String(className()));
}
else {
util.addClassName(frame, String(className));
}
}
this.frame = frame;
changed += 1;
}
if (!frame.parentNode) {
if (!this.parent) {
throw new Error('Cannot repaint panel: no parent attached');
}
var parentContainer = this.parent.getContainer();
if (!parentContainer) {
throw new Error('Cannot repaint panel: parent has no container element');
}
parentContainer.appendChild(frame);
changed += 1;
}
changed += update(frame.style, 'top', asSize(options.top, '0px'));
changed += update(frame.style, 'left', asSize(options.left, '0px'));
changed += update(frame.style, 'width', asSize(options.width, '100%'));
changed += update(frame.style, 'height', asSize(options.height, '100%'));
return (changed > 0);
};
/**
* Reflow the component
* @return {Boolean} resized
*/
ContentPanel.prototype.reflow = function () {
var changed = 0,
update = util.updateProperty,
frame = this.frame;
if (frame) {
changed += update(this, 'top', frame.offsetTop);
changed += update(this, 'left', frame.offsetLeft);
changed += update(this, 'width', frame.offsetWidth);
changed += update(this, 'height', frame.offsetHeight);
}
else {
changed += 1;
}
return (changed > 0);
};

+ 54
- 59
src/timeline/component/CurrentTime.js View File

@ -1,23 +1,22 @@
/**
* A current time bar
* @param {Component} parent
* @param {Component[]} [depends] Components on which this components depends
* (except for the parent)
* @param {Range} range
* @param {Object} [options] Available parameters:
* {Boolean} [showCurrentTime]
* @constructor CurrentTime
* @extends Component
*/
function CurrentTime (parent, depends, options) {
function CurrentTime (range, options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.range = range;
this.options = options || {};
this.defaultOptions = {
showCurrentTime: false
};
this._create();
}
CurrentTime.prototype = new Component();
@ -25,77 +24,73 @@ CurrentTime.prototype = new Component();
CurrentTime.prototype.setOptions = Component.prototype.setOptions;
/**
* Get the container element of the bar, which can be used by a child to
* add its own widgets.
* @returns {HTMLElement} container
* Create the HTML DOM for the current time bar
* @private
*/
CurrentTime.prototype.getContainer = function () {
return this.frame;
CurrentTime.prototype._create = function _create () {
var bar = document.createElement('div');
bar.className = 'currenttime';
bar.style.position = 'absolute';
bar.style.top = '0px';
bar.style.height = '100%';
this.bar = bar;
};
/**
* Get the frame element of the current time bar
* @returns {HTMLElement} frame
*/
CurrentTime.prototype.getFrame = function getFrame() {
return this.bar;
};
/**
* Repaint the component
* @return {Boolean} changed
* @return {boolean} Returns true if the component is resized
*/
CurrentTime.prototype.repaint = function () {
var bar = this.frame,
parent = this.parent,
parentContainer = parent.parent.getContainer();
CurrentTime.prototype.repaint = function repaint() {
var parent = this.parent;
if (!parent) {
throw new Error('Cannot repaint bar: no parent attached');
}
var now = new Date();
var x = this.options.toScreen(now);
if (!parentContainer) {
throw new Error('Cannot repaint bar: parent has no container element');
}
this.bar.style.left = x + 'px';
this.bar.title = 'Current time: ' + now;
if (!this.getOption('showCurrentTime')) {
if (bar) {
parentContainer.removeChild(bar);
delete this.frame;
}
return false;
};
return false;
}
/**
* Start auto refreshing the current time bar
*/
CurrentTime.prototype.start = function start() {
var me = this;
if (!bar) {
bar = document.createElement('div');
bar.className = 'currenttime';
bar.style.position = 'absolute';
bar.style.top = '0px';
bar.style.height = '100%';
function update () {
me.stop();
parentContainer.appendChild(bar);
this.frame = bar;
}
// determine interval to refresh
var scale = me.range.conversion(me.parent.width).scale;
var interval = 1 / scale / 10;
if (interval < 30) interval = 30;
if (interval > 1000) interval = 1000;
if (!parent.conversion) {
parent._updateConversion();
}
me.repaint();
var now = new Date();
var x = parent.toScreen(now);
// start a timer to adjust for the new time
me.currentTimeTimer = setTimeout(update, interval);
}
bar.style.left = x + 'px';
bar.title = 'Current time: ' + now;
update();
};
// start a timer to adjust for the new time
/**
* Stop auto refreshing the current time bar
*/
CurrentTime.prototype.stop = function stop() {
if (this.currentTimeTimer !== undefined) {
clearTimeout(this.currentTimeTimer);
delete this.currentTimeTimer;
}
var timeline = this;
var interval = 1 / parent.conversion.scale / 2;
if (interval < 30) {
interval = 30;
}
this.currentTimeTimer = setTimeout(function() {
timeline.repaint();
}, interval);
return false;
};

+ 55
- 83
src/timeline/component/CustomTime.js View File

@ -1,18 +1,13 @@
/**
* A custom time bar
* @param {Component} parent
* @param {Component[]} [depends] Components on which this components depends
* (except for the parent)
* @param {Object} [options] Available parameters:
* {Boolean} [showCustomTime]
* @constructor CustomTime
* @extends Component
*/
function CustomTime (parent, depends, options) {
function CustomTime (options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.options = options || {};
this.defaultOptions = {
@ -21,85 +16,61 @@ function CustomTime (parent, depends, options) {
this.customTime = new Date();
this.eventParams = {}; // stores state parameters while dragging the bar
// create the DOM
this._create();
}
CustomTime.prototype = new Component();
Emitter(CustomTime.prototype);
CustomTime.prototype.setOptions = Component.prototype.setOptions;
/**
* Get the container element of the bar, which can be used by a child to
* add its own widgets.
* @returns {HTMLElement} container
* Create the DOM for the custom time
* @private
*/
CustomTime.prototype._create = function _create () {
var bar = document.createElement('div');
bar.className = 'customtime';
bar.style.position = 'absolute';
bar.style.top = '0px';
bar.style.height = '100%';
this.bar = bar;
var drag = document.createElement('div');
drag.style.position = 'relative';
drag.style.top = '0px';
drag.style.left = '-10px';
drag.style.height = '100%';
drag.style.width = '20px';
bar.appendChild(drag);
// attach event listeners
this.hammer = Hammer(bar, {
prevent_default: true
});
this.hammer.on('dragstart', this._onDragStart.bind(this));
this.hammer.on('drag', this._onDrag.bind(this));
this.hammer.on('dragend', this._onDragEnd.bind(this));
};
/**
* Get the frame element of the custom time bar
* @returns {HTMLElement} frame
*/
CustomTime.prototype.getContainer = function () {
return this.frame;
CustomTime.prototype.getFrame = function getFrame() {
return this.bar;
};
/**
* Repaint the component
* @return {Boolean} changed
* @return {boolean} Returns true if the component is resized
*/
CustomTime.prototype.repaint = function () {
var bar = this.frame,
parent = this.parent;
if (!parent) {
throw new Error('Cannot repaint bar: no parent attached');
}
var parentContainer = parent.parent.getContainer();
if (!parentContainer) {
throw new Error('Cannot repaint bar: parent has no container element');
}
if (!this.getOption('showCustomTime')) {
if (bar) {
parentContainer.removeChild(bar);
delete this.frame;
}
return false;
}
if (!bar) {
bar = document.createElement('div');
bar.className = 'customtime';
bar.style.position = 'absolute';
bar.style.top = '0px';
bar.style.height = '100%';
parentContainer.appendChild(bar);
var drag = document.createElement('div');
drag.style.position = 'relative';
drag.style.top = '0px';
drag.style.left = '-10px';
drag.style.height = '100%';
drag.style.width = '20px';
bar.appendChild(drag);
this.frame = bar;
// attach event listeners
this.hammer = Hammer(bar, {
prevent_default: true
});
this.hammer.on('dragstart', this._onDragStart.bind(this));
this.hammer.on('drag', this._onDrag.bind(this));
this.hammer.on('dragend', this._onDragEnd.bind(this));
}
if (!parent.conversion) {
parent._updateConversion();
}
var x = parent.toScreen(this.customTime);
bar.style.left = x + 'px';
bar.title = 'Time: ' + this.customTime;
var x = this.options.toScreen(this.customTime);
this.bar.style.left = x + 'px';
this.bar.title = 'Time: ' + this.customTime;
return false;
};
@ -127,6 +98,7 @@ CustomTime.prototype.getCustomTime = function() {
* @private
*/
CustomTime.prototype._onDragStart = function(event) {
this.eventParams.dragging = true;
this.eventParams.customTime = this.customTime;
event.stopPropagation();
@ -139,18 +111,18 @@ CustomTime.prototype._onDragStart = function(event) {
* @private
*/
CustomTime.prototype._onDrag = function (event) {
if (!this.eventParams.dragging) return;
var deltaX = event.gesture.deltaX,
x = this.parent.toScreen(this.eventParams.customTime) + deltaX,
time = this.parent.toTime(x);
x = this.options.toScreen(this.eventParams.customTime) + deltaX,
time = this.options.toTime(x);
this.setCustomTime(time);
// fire a timechange event
if (this.controller) {
this.controller.emit('timechange', {
time: this.customTime
})
}
this.emit('timechange', {
time: new Date(this.customTime.valueOf())
});
event.stopPropagation();
event.preventDefault();
@ -162,12 +134,12 @@ CustomTime.prototype._onDrag = function (event) {
* @private
*/
CustomTime.prototype._onDragEnd = function (event) {
if (!this.eventParams.dragging) return;
// fire a timechanged event
if (this.controller) {
this.controller.emit('timechanged', {
time: this.customTime
})
}
this.emit('timechanged', {
time: new Date(this.customTime.valueOf())
});
event.stopPropagation();
event.preventDefault();

+ 398
- 75
src/timeline/component/Group.js View File

@ -1,20 +1,15 @@
/**
* @constructor Group
* @param {GroupSet} parent
* @param {Number | String} groupId
* @param {Object} [options] Options to set initial property values
* // TODO: describe available options
* @extends Component
* @param {Object} data
* @param {ItemSet} itemSet
*/
function Group (parent, groupId, options) {
this.id = util.randomUUID();
this.parent = parent;
function Group (groupId, data, itemSet) {
this.groupId = groupId;
this.itemset = null; // ItemSet
this.options = options || {};
this.options.top = 0;
this.itemSet = itemSet;
this.dom = {};
this.props = {
label: {
width: 0,
@ -22,108 +17,436 @@ function Group (parent, groupId, options) {
}
};
this.top = 0;
this.left = 0;
this.width = 0;
this.height = 0;
}
this.items = {}; // items filtered by groupId of this group
this.visibleItems = []; // items currently visible in window
this.orderedItems = { // items sorted by start and by end
byStart: [],
byEnd: []
};
Group.prototype = new Component();
this._create();
// TODO: comment
Group.prototype.setOptions = Component.prototype.setOptions;
this.setData(data);
}
/**
* Get the container element of the panel, which can be used by a child to
* add its own widgets.
* @returns {HTMLElement} container
* Create DOM elements for the group
* @private
*/
Group.prototype.getContainer = function () {
return this.parent.getContainer();
Group.prototype._create = function() {
var label = document.createElement('div');
label.className = 'vlabel';
this.dom.label = label;
var inner = document.createElement('div');
inner.className = 'inner';
label.appendChild(inner);
this.dom.inner = inner;
var foreground = document.createElement('div');
foreground.className = 'group';
foreground['timeline-group'] = this;
this.dom.foreground = foreground;
this.dom.background = document.createElement('div');
this.dom.axis = document.createElement('div');
};
/**
* Set item set for the group. The group will create a view on the itemset,
* filtered by the groups id.
* @param {DataSet | DataView} items
* Set the group data for this group
* @param {Object} data Group data, can contain properties content and className
*/
Group.prototype.setItems = function setItems(items) {
if (this.itemset) {
// remove current item set
this.itemset.hide();
this.itemset.setItems();
Group.prototype.setData = function setData(data) {
// update contents
var content = data && data.content;
if (content instanceof Element) {
this.dom.inner.appendChild(content);
}
else if (content != undefined) {
this.dom.inner.innerHTML = content;
}
else {
this.dom.inner.innerHTML = this.groupId;
}
this.parent.controller.remove(this.itemset);
this.itemset = null;
// update className
var className = data && data.className;
if (className) {
util.addClassName(this.dom.label, className);
}
};
if (items) {
var groupId = this.groupId;
/**
* Get the foreground container element
* @return {HTMLElement} foreground
*/
Group.prototype.getForeground = function getForeground() {
return this.dom.foreground;
};
var itemsetOptions = Object.create(this.options);
this.itemset = new ItemSet(this, null, itemsetOptions);
this.itemset.setRange(this.parent.range);
/**
* Get the background container element
* @return {HTMLElement} background
*/
Group.prototype.getBackground = function getBackground() {
return this.dom.background;
};
this.view = new DataView(items, {
filter: function (item) {
return item.group == groupId;
}
/**
* Get the axis container element
* @return {HTMLElement} axis
*/
Group.prototype.getAxis = function getAxis() {
return this.dom.axis;
};
/**
* Get the width of the group label
* @return {number} width
*/
Group.prototype.getLabelWidth = function getLabelWidth() {
return this.props.label.width;
};
/**
* Repaint this group
* @param {{start: number, end: number}} range
* @param {{item: number, axis: number}} margin
* @param {boolean} [restack=false] Force restacking of all items
* @return {boolean} Returns true if the group is resized
*/
Group.prototype.repaint = function repaint(range, margin, restack) {
var resized = false;
this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
// reposition visible items vertically
if (this.itemSet.options.stack) { // TODO: ugly way to access options...
stack.stack(this.visibleItems, margin, restack);
}
else { // no stacking
stack.nostack(this.visibleItems, margin);
}
this.stackDirty = false;
for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
var item = this.visibleItems[i];
item.repositionY();
}
// recalculate the height of the group
var height;
var visibleItems = this.visibleItems;
if (visibleItems.length) {
var min = visibleItems[0].top;
var max = visibleItems[0].top + visibleItems[0].height;
util.forEach(visibleItems, function (item) {
min = Math.min(min, item.top);
max = Math.max(max, (item.top + item.height));
});
this.itemset.setItems(this.view);
height = (max - min) + margin.axis + margin.item;
}
else {
height = margin.axis + margin.item;
}
height = Math.max(height, this.props.label.height);
// calculate actual size and position
var foreground = this.dom.foreground;
this.top = foreground.offsetTop;
this.left = foreground.offsetLeft;
this.width = foreground.offsetWidth;
resized = util.updateProperty(this, 'height', height) || resized;
// recalculate size of label
resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
// apply new height
foreground.style.height = height + 'px';
this.dom.label.style.height = height + 'px';
return resized;
};
this.parent.controller.add(this.itemset);
/**
* Show this group: attach to the DOM
*/
Group.prototype.show = function show() {
if (!this.dom.label.parentNode) {
this.itemSet.getLabelSet().appendChild(this.dom.label);
}
if (!this.dom.foreground.parentNode) {
this.itemSet.getForeground().appendChild(this.dom.foreground);
}
if (!this.dom.background.parentNode) {
this.itemSet.getBackground().appendChild(this.dom.background);
}
if (!this.dom.axis.parentNode) {
this.itemSet.getAxis().appendChild(this.dom.axis);
}
};
/**
* Set selected items by their id. Replaces the current selection.
* Unknown id's are silently ignored.
* @param {Array} [ids] An array with zero or more id's of the items to be
* selected. If ids is an empty array, all items will be
* unselected.
* Hide this group: remove from the DOM
*/
Group.prototype.setSelection = function setSelection(ids) {
if (this.itemset) this.itemset.setSelection(ids);
Group.prototype.hide = function hide() {
var label = this.dom.label;
if (label.parentNode) {
label.parentNode.removeChild(label);
}
var foreground = this.dom.foreground;
if (foreground.parentNode) {
foreground.parentNode.removeChild(foreground);
}
var background = this.dom.background;
if (background.parentNode) {
background.parentNode.removeChild(background);
}
var axis = this.dom.axis;
if (axis.parentNode) {
axis.parentNode.removeChild(axis);
}
};
/**
* Get the selected items by their id
* @return {Array} ids The ids of the selected items
* Add an item to the group
* @param {Item} item
*/
Group.prototype.getSelection = function getSelection() {
return this.itemset ? this.itemset.getSelection() : [];
Group.prototype.add = function add(item) {
this.items[item.id] = item;
item.setParent(this);
if (item instanceof ItemRange && this.visibleItems.indexOf(item) == -1) {
var range = this.itemSet.range; // TODO: not nice accessing the range like this
this._checkIfVisible(item, this.visibleItems, range);
}
};
/**
* Remove an item from the group
* @param {Item} item
*/
Group.prototype.remove = function remove(item) {
delete this.items[item.id];
item.setParent(this.itemSet);
// remove from visible items
var index = this.visibleItems.indexOf(item);
if (index != -1) this.visibleItems.splice(index, 1);
// TODO: also remove from ordered items?
};
/**
* Remove an item from the corresponding DataSet
* @param {Item} item
*/
Group.prototype.removeFromDataSet = function removeFromDataSet(item) {
this.itemSet.removeItem(item.id);
};
/**
* Reorder the items
*/
Group.prototype.order = function order() {
var array = util.toArray(this.items);
this.orderedItems.byStart = array;
this.orderedItems.byEnd = this._constructByEndArray(array);
stack.orderByStart(this.orderedItems.byStart);
stack.orderByEnd(this.orderedItems.byEnd);
};
/**
* Repaint the item
* @return {Boolean} changed
* Create an array containing all items being a range (having an end date)
* @param {Item[]} array
* @returns {ItemRange[]}
* @private
*/
Group.prototype.repaint = function repaint() {
return false;
Group.prototype._constructByEndArray = function _constructByEndArray(array) {
var endArray = [];
for (var i = 0; i < array.length; i++) {
if (array[i] instanceof ItemRange) {
endArray.push(array[i]);
}
}
return endArray;
};
/**
* Update the visible items
* @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
* @param {Item[]} visibleItems The previously visible items.
* @param {{start: number, end: number}} range Visible range
* @return {Item[]} visibleItems The new visible items.
* @private
*/
Group.prototype._updateVisibleItems = function _updateVisibleItems(orderedItems, visibleItems, range) {
var initialPosByStart,
newVisibleItems = [],
i;
// first check if the items that were in view previously are still in view.
// this handles the case for the ItemRange that is both before and after the current one.
if (visibleItems.length > 0) {
for (i = 0; i < visibleItems.length; i++) {
this._checkIfVisible(visibleItems[i], newVisibleItems, range);
}
}
// If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
if (newVisibleItems.length == 0) {
initialPosByStart = this._binarySearch(orderedItems, range, false);
}
else {
initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
}
// use visible search to find a visible ItemRange (only based on endTime)
var initialPosByEnd = this._binarySearch(orderedItems, range, true);
// if we found a initial ID to use, trace it up and down until we meet an invisible item.
if (initialPosByStart != -1) {
for (i = initialPosByStart; i >= 0; i--) {
if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
}
for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
}
}
// if we found a initial ID to use, trace it up and down until we meet an invisible item.
if (initialPosByEnd != -1) {
for (i = initialPosByEnd; i >= 0; i--) {
if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
}
for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
}
}
return newVisibleItems;
};
/**
* Reflow the item
* @return {Boolean} resized
* This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
* arrays. This is done by giving a boolean value true if you want to use the byEnd.
* This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check
* if the time we selected (start or end) is within the current range).
*
* The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is
* before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest,
* either the start OR end time has to be in the range.
*
* @param {{byStart: Item[], byEnd: Item[]}} orderedItems
* @param {{start: number, end: number}} range
* @param {Boolean} byEnd
* @returns {number}
* @private
*/
Group.prototype.reflow = function reflow() {
var changed = 0,
update = util.updateProperty;
Group.prototype._binarySearch = function _binarySearch(orderedItems, range, byEnd) {
var array = [];
var byTime = byEnd ? 'end' : 'start';
if (byEnd == true) {array = orderedItems.byEnd; }
else {array = orderedItems.byStart;}
var interval = range.end - range.start;
changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
var found = false;
var low = 0;
var high = array.length;
var guess = Math.floor(0.5*(high+low));
var newGuess;
// TODO: reckon with the height of the group label
if (high == 0) {guess = -1;}
else if (high == 1) {
if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
guess = 0;
}
else {
guess = -1;
}
}
else {
high -= 1;
while (found == false) {
if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
found = true;
}
else {
if (array[guess].data[byTime] < range.start - interval) { // it is too small --> increase low
low = Math.floor(0.5*(high+low));
}
else { // it is too big --> decrease high
high = Math.floor(0.5*(high+low));
}
newGuess = Math.floor(0.5*(high+low));
// not in list;
if (guess == newGuess) {
guess = -1;
found = true;
}
else {
guess = newGuess;
}
}
}
}
return guess;
};
if (this.label) {
var inner = this.label.firstChild;
changed += update(this.props.label, 'width', inner.clientWidth);
changed += update(this.props.label, 'height', inner.clientHeight);
/**
* this function checks if an item is invisible. If it is NOT we make it visible
* and add it to the global visible items. If it is, return true.
*
* @param {Item} item
* @param {Item[]} visibleItems
* @param {{start:number, end:number}} range
* @returns {boolean}
* @private
*/
Group.prototype._checkIfInvisible = function _checkIfInvisible(item, visibleItems, range) {
if (item.isVisible(range)) {
if (!item.displayed) item.show();
item.repositionX();
if (visibleItems.indexOf(item) == -1) {
visibleItems.push(item);
}
return false;
}
else {
changed += update(this.props.label, 'width', 0);
changed += update(this.props.label, 'height', 0);
return true;
}
};
return (changed > 0);
/**
* this function is very similar to the _checkIfInvisible() but it does not
* return booleans, hides the item if it should not be seen and always adds to
* the visibleItems.
* this one is for brute forcing and hiding.
*
* @param {Item} item
* @param {Array} visibleItems
* @param {{start:number, end:number}} range
* @private
*/
Group.prototype._checkIfVisible = function _checkIfVisible(item, visibleItems, range) {
if (item.isVisible(range)) {
if (!item.displayed) item.show();
// reposition item horizontally
item.repositionX();
visibleItems.push(item);
}
else {
if (item.displayed) item.hide();
}
};

+ 0
- 580
src/timeline/component/GroupSet.js View File

@ -1,580 +0,0 @@
/**
* An GroupSet holds a set of groups
* @param {Component} parent
* @param {Component[]} [depends] Components on which this components depends
* (except for the parent)
* @param {Object} [options] See GroupSet.setOptions for the available
* options.
* @constructor GroupSet
* @extends Panel
*/
function GroupSet(parent, depends, options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.options = options || {};
this.range = null; // Range or Object {start: number, end: number}
this.itemsData = null; // DataSet with items
this.groupsData = null; // DataSet with groups
this.groups = {}; // map with groups
this.dom = {};
this.props = {
labels: {
width: 0
}
};
// TODO: implement right orientation of the labels
// changes in groups are queued key/value map containing id/action
this.queue = {};
var me = this;
this.listeners = {
'add': function (event, params) {
me._onAdd(params.items);
},
'update': function (event, params) {
me._onUpdate(params.items);
},
'remove': function (event, params) {
me._onRemove(params.items);
}
};
}
GroupSet.prototype = new Panel();
/**
* Set options for the GroupSet. Existing options will be extended/overwritten.
* @param {Object} [options] The following options are available:
* {String | function} groupsOrder
* TODO: describe options
*/
GroupSet.prototype.setOptions = Component.prototype.setOptions;
GroupSet.prototype.setRange = function (range) {
// TODO: implement setRange
};
/**
* Set items
* @param {vis.DataSet | null} items
*/
GroupSet.prototype.setItems = function setItems(items) {
this.itemsData = items;
for (var id in this.groups) {
if (this.groups.hasOwnProperty(id)) {
var group = this.groups[id];
group.setItems(items);
}
}
};
/**
* Get items
* @return {vis.DataSet | null} items
*/
GroupSet.prototype.getItems = function getItems() {
return this.itemsData;
};
/**
* Set range (start and end).
* @param {Range | Object} range A Range or an object containing start and end.
*/
GroupSet.prototype.setRange = function setRange(range) {
this.range = range;
};
/**
* Set groups
* @param {vis.DataSet} groups
*/
GroupSet.prototype.setGroups = function setGroups(groups) {
var me = this,
ids;
// unsubscribe from current dataset
if (this.groupsData) {
util.forEach(this.listeners, function (callback, event) {
me.groupsData.unsubscribe(event, callback);
});
// remove all drawn groups
ids = this.groupsData.getIds();
this._onRemove(ids);
}
// replace the dataset
if (!groups) {
this.groupsData = null;
}
else if (groups instanceof DataSet) {
this.groupsData = groups;
}
else {
this.groupsData = new DataSet({
convert: {
start: 'Date',
end: 'Date'
}
});
this.groupsData.add(groups);
}
if (this.groupsData) {
// subscribe to new dataset
var id = this.id;
util.forEach(this.listeners, function (callback, event) {
me.groupsData.on(event, callback, id);
});
// draw all new groups
ids = this.groupsData.getIds();
this._onAdd(ids);
}
};
/**
* Get groups
* @return {vis.DataSet | null} groups
*/
GroupSet.prototype.getGroups = function getGroups() {
return this.groupsData;
};
/**
* Set selected items by their id. Replaces the current selection.
* Unknown id's are silently ignored.
* @param {Array} [ids] An array with zero or more id's of the items to be
* selected. If ids is an empty array, all items will be
* unselected.
*/
GroupSet.prototype.setSelection = function setSelection(ids) {
var selection = [],
groups = this.groups;
// iterate over each of the groups
for (var id in groups) {
if (groups.hasOwnProperty(id)) {
var group = groups[id];
group.setSelection(ids);
}
}
return selection;
};
/**
* Get the selected items by their id
* @return {Array} ids The ids of the selected items
*/
GroupSet.prototype.getSelection = function getSelection() {
var selection = [],
groups = this.groups;
// iterate over each of the groups
for (var id in groups) {
if (groups.hasOwnProperty(id)) {
var group = groups[id];
selection = selection.concat(group.getSelection());
}
}
return selection;
};
/**
* Repaint the component
* @return {Boolean} changed
*/
GroupSet.prototype.repaint = function repaint() {
var changed = 0,
i, id, group, label,
update = util.updateProperty,
asSize = util.option.asSize,
asElement = util.option.asElement,
options = this.options,
frame = this.dom.frame,
labels = this.dom.labels,
labelSet = this.dom.labelSet;
// create frame
if (!this.parent) {
throw new Error('Cannot repaint groupset: no parent attached');
}
var parentContainer = this.parent.getContainer();
if (!parentContainer) {
throw new Error('Cannot repaint groupset: parent has no container element');
}
if (!frame) {
frame = document.createElement('div');
frame.className = 'groupset';
frame['timeline-groupset'] = this;
this.dom.frame = frame;
var className = options.className;
if (className) {
util.addClassName(frame, util.option.asString(className));
}
changed += 1;
}
if (!frame.parentNode) {
parentContainer.appendChild(frame);
changed += 1;
}
// create labels
var labelContainer = asElement(options.labelContainer);
if (!labelContainer) {
throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
}
if (!labels) {
labels = document.createElement('div');
labels.className = 'labels';
this.dom.labels = labels;
}
if (!labelSet) {
labelSet = document.createElement('div');
labelSet.className = 'label-set';
labels.appendChild(labelSet);
this.dom.labelSet = labelSet;
}
if (!labels.parentNode || labels.parentNode != labelContainer) {
if (labels.parentNode) {
labels.parentNode.removeChild(labels.parentNode);
}
labelContainer.appendChild(labels);
}
// reposition frame
changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
changed += update(frame.style, 'top', asSize(options.top, '0px'));
changed += update(frame.style, 'left', asSize(options.left, '0px'));
changed += update(frame.style, 'width', asSize(options.width, '100%'));
// reposition labels
changed += update(labelSet.style, 'top', asSize(options.top, '0px'));
changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px'));
var me = this,
queue = this.queue,
groups = this.groups,
groupsData = this.groupsData;
// show/hide added/changed/removed groups
var ids = Object.keys(queue);
if (ids.length) {
ids.forEach(function (id) {
var action = queue[id];
var group = groups[id];
//noinspection FallthroughInSwitchStatementJS
switch (action) {
case 'add':
case 'update':
if (!group) {
var groupOptions = Object.create(me.options);
util.extend(groupOptions, {
height: null,
maxHeight: null
});
group = new Group(me, id, groupOptions);
group.setItems(me.itemsData); // attach items data
groups[id] = group;
me.controller.add(group);
}
// TODO: update group data
group.data = groupsData.get(id);
delete queue[id];
break;
case 'remove':
if (group) {
group.setItems(); // detach items data
delete groups[id];
me.controller.remove(group);
}
// update lists
delete queue[id];
break;
default:
console.log('Error: unknown action "' + action + '"');
}
});
// the groupset depends on each of the groups
//this.depends = this.groups; // TODO: gives a circular reference through the parent
// TODO: apply dependencies of the groupset
// update the top positions of the groups in the correct order
var orderedGroups = this.groupsData.getIds({
order: this.options.groupOrder
});
for (i = 0; i < orderedGroups.length; i++) {
(function (group, prevGroup) {
var top = 0;
if (prevGroup) {
top = function () {
// TODO: top must reckon with options.maxHeight
return prevGroup.top + prevGroup.height;
}
}
group.setOptions({
top: top
});
})(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
}
// (re)create the labels
while (labelSet.firstChild) {
labelSet.removeChild(labelSet.firstChild);
}
for (i = 0; i < orderedGroups.length; i++) {
id = orderedGroups[i];
label = this._createLabel(id);
labelSet.appendChild(label);
}
changed++;
}
// reposition the labels
// TODO: labels are not displayed correctly when orientation=='top'
// TODO: width of labelPanel is not immediately updated on a change in groups
for (id in groups) {
if (groups.hasOwnProperty(id)) {
group = groups[id];
label = group.label;
if (label) {
label.style.top = group.top + 'px';
label.style.height = group.height + 'px';
}
}
}
return (changed > 0);
};
/**
* Create a label for group with given id
* @param {Number} id
* @return {Element} label
* @private
*/
GroupSet.prototype._createLabel = function(id) {
var group = this.groups[id];
var label = document.createElement('div');
label.className = 'vlabel';
var inner = document.createElement('div');
inner.className = 'inner';
label.appendChild(inner);
var content = group.data && group.data.content;
if (content instanceof Element) {
inner.appendChild(content);
}
else if (content != undefined) {
inner.innerHTML = content;
}
var className = group.data && group.data.className;
if (className) {
util.addClassName(label, className);
}
group.label = label; // TODO: not so nice, parking labels in the group this way!!!
return label;
};
/**
* Get container element
* @return {HTMLElement} container
*/
GroupSet.prototype.getContainer = function getContainer() {
return this.dom.frame;
};
/**
* Get the width of the group labels
* @return {Number} width
*/
GroupSet.prototype.getLabelsWidth = function getContainer() {
return this.props.labels.width;
};
/**
* Reflow the component
* @return {Boolean} resized
*/
GroupSet.prototype.reflow = function reflow() {
var changed = 0,
id, group,
options = this.options,
update = util.updateProperty,
asNumber = util.option.asNumber,
asSize = util.option.asSize,
frame = this.dom.frame;
if (frame) {
var maxHeight = asNumber(options.maxHeight);
var fixedHeight = (asSize(options.height) != null);
var height;
if (fixedHeight) {
height = frame.offsetHeight;
}
else {
// height is not specified, calculate the sum of the height of all groups
height = 0;
for (id in this.groups) {
if (this.groups.hasOwnProperty(id)) {
group = this.groups[id];
height += group.height;
}
}
}
if (maxHeight != null) {
height = Math.min(height, maxHeight);
}
changed += update(this, 'height', height);
changed += update(this, 'top', frame.offsetTop);
changed += update(this, 'left', frame.offsetLeft);
changed += update(this, 'width', frame.offsetWidth);
}
// calculate the maximum width of the labels
var width = 0;
for (id in this.groups) {
if (this.groups.hasOwnProperty(id)) {
group = this.groups[id];
var labelWidth = group.props && group.props.label && group.props.label.width || 0;
width = Math.max(width, labelWidth);
}
}
changed += update(this.props.labels, 'width', width);
return (changed > 0);
};
/**
* Hide the component from the DOM
* @return {Boolean} changed
*/
GroupSet.prototype.hide = function hide() {
if (this.dom.frame && this.dom.frame.parentNode) {
this.dom.frame.parentNode.removeChild(this.dom.frame);
return true;
}
else {
return false;
}
};
/**
* Show the component in the DOM (when not already visible).
* A repaint will be executed when the component is not visible
* @return {Boolean} changed
*/
GroupSet.prototype.show = function show() {
if (!this.dom.frame || !this.dom.frame.parentNode) {
return this.repaint();
}
else {
return false;
}
};
/**
* Handle updated groups
* @param {Number[]} ids
* @private
*/
GroupSet.prototype._onUpdate = function _onUpdate(ids) {
this._toQueue(ids, 'update');
};
/**
* Handle changed groups
* @param {Number[]} ids
* @private
*/
GroupSet.prototype._onAdd = function _onAdd(ids) {
this._toQueue(ids, 'add');
};
/**
* Handle removed groups
* @param {Number[]} ids
* @private
*/
GroupSet.prototype._onRemove = function _onRemove(ids) {
this._toQueue(ids, 'remove');
};
/**
* Put groups in the queue to be added/updated/remove
* @param {Number[]} ids
* @param {String} action can be 'add', 'update', 'remove'
*/
GroupSet.prototype._toQueue = function _toQueue(ids, action) {
var queue = this.queue;
ids.forEach(function (id) {
queue[id] = action;
});
if (this.controller) {
//this.requestReflow();
this.requestRepaint();
}
};
/**
* Find the Group from an event target:
* searches for the attribute 'timeline-groupset' in the event target's element
* tree, then finds the right group in this groupset
* @param {Event} event
* @return {Group | null} group
*/
GroupSet.groupFromTarget = function groupFromTarget (event) {
var groupset,
target = event.target;
while (target) {
if (target.hasOwnProperty('timeline-groupset')) {
groupset = target['timeline-groupset'];
break;
}
target = target.parentNode;
}
if (groupset) {
for (var groupId in groupset.groups) {
if (groupset.groups.hasOwnProperty(groupId)) {
var group = groupset.groups[groupId];
if (group.itemset && ItemSet.itemSetFromTarget(event) == group.itemset) {
return group;
}
}
}
}
return null;
};

+ 662
- 403
src/timeline/component/ItemSet.js
File diff suppressed because it is too large
View File


+ 118
- 60
src/timeline/component/Panel.js View File

@ -1,8 +1,5 @@
/**
* A panel can contain components
* @param {Component} [parent]
* @param {Component[]} [depends] Components on which this components depends
* (except for the parent)
* @param {Object} [options] Available parameters:
* {String | Number | function} [left]
* {String | Number | function} [top]
@ -12,12 +9,15 @@
* @constructor Panel
* @extends Component
*/
function Panel(parent, depends, options) {
function Panel(options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.parent = null;
this.childs = [];
this.options = options || {};
// create frame
this.frame = (typeof document !== 'undefined') ? document.createElement('div') : null;
}
Panel.prototype = new Component();
@ -34,79 +34,137 @@ Panel.prototype = new Component();
Panel.prototype.setOptions = Component.prototype.setOptions;
/**
* Get the container element of the panel, which can be used by a child to
* add its own widgets.
* @returns {HTMLElement} container
* Get the outer frame of the panel
* @returns {HTMLElement} frame
*/
Panel.prototype.getContainer = function () {
Panel.prototype.getFrame = function () {
return this.frame;
};
/**
* Repaint the component
* @return {Boolean} changed
* Append a child to the panel
* @param {Component} child
*/
Panel.prototype.repaint = function () {
var changed = 0,
update = util.updateProperty,
asSize = util.option.asSize,
options = this.options,
frame = this.frame;
if (!frame) {
frame = document.createElement('div');
frame.className = 'vpanel';
var className = options.className;
if (className) {
if (typeof className == 'function') {
util.addClassName(frame, String(className()));
Panel.prototype.appendChild = function (child) {
this.childs.push(child);
child.parent = this;
// attach to the DOM
var frame = child.getFrame();
if (frame) {
if (frame.parentNode) {
frame.parentNode.removeChild(frame);
}
this.frame.appendChild(frame);
}
};
/**
* Insert a child to the panel
* @param {Component} child
* @param {Component} beforeChild
*/
Panel.prototype.insertBefore = function (child, beforeChild) {
var index = this.childs.indexOf(beforeChild);
if (index != -1) {
this.childs.splice(index, 0, child);
child.parent = this;
// attach to the DOM
var frame = child.getFrame();
if (frame) {
if (frame.parentNode) {
frame.parentNode.removeChild(frame);
}
var beforeFrame = beforeChild.getFrame();
if (beforeFrame) {
this.frame.insertBefore(frame, beforeFrame);
}
else {
util.addClassName(frame, String(className));
this.frame.appendChild(frame);
}
}
this.frame = frame;
changed += 1;
}
if (!frame.parentNode) {
if (!this.parent) {
throw new Error('Cannot repaint panel: no parent attached');
}
var parentContainer = this.parent.getContainer();
if (!parentContainer) {
throw new Error('Cannot repaint panel: parent has no container element');
};
/**
* Remove a child from the panel
* @param {Component} child
*/
Panel.prototype.removeChild = function (child) {
var index = this.childs.indexOf(child);
if (index != -1) {
this.childs.splice(index, 1);
child.parent = null;
// remove from the DOM
var frame = child.getFrame();
if (frame && frame.parentNode) {
this.frame.removeChild(frame);
}
parentContainer.appendChild(frame);
changed += 1;
}
};
changed += update(frame.style, 'top', asSize(options.top, '0px'));
changed += update(frame.style, 'left', asSize(options.left, '0px'));
changed += update(frame.style, 'width', asSize(options.width, '100%'));
changed += update(frame.style, 'height', asSize(options.height, '100%'));
return (changed > 0);
/**
* Test whether the panel contains given child
* @param {Component} child
*/
Panel.prototype.hasChild = function (child) {
var index = this.childs.indexOf(child);
return (index != -1);
};
/**
* Reflow the component
* @return {Boolean} resized
* Repaint the component
* @return {boolean} Returns true if the component was resized since previous repaint
*/
Panel.prototype.reflow = function () {
var changed = 0,
update = util.updateProperty,
frame = this.frame;
Panel.prototype.repaint = function () {
var asString = util.option.asString,
options = this.options,
frame = this.getFrame();
if (frame) {
changed += update(this, 'top', frame.offsetTop);
changed += update(this, 'left', frame.offsetLeft);
changed += update(this, 'width', frame.offsetWidth);
changed += update(this, 'height', frame.offsetHeight);
}
else {
changed += 1;
// update className
frame.className = 'vpanel' + (options.className ? (' ' + asString(options.className)) : '');
// repaint the child components
var childsResized = this._repaintChilds();
// update frame size
this._updateSize();
return this._isResized() || childsResized;
};
/**
* Repaint all childs of the panel
* @return {boolean} Returns true if the component is resized
* @private
*/
Panel.prototype._repaintChilds = function () {
var resized = false;
for (var i = 0, ii = this.childs.length; i < ii; i++) {
resized = this.childs[i].repaint() || resized;
}
return resized;
};
/**
* Apply the size from options to the panel, and recalculate it's actual size.
* @private
*/
Panel.prototype._updateSize = function () {
// apply size
this.frame.style.top = util.option.asSize(this.options.top);
this.frame.style.bottom = util.option.asSize(this.options.bottom);
this.frame.style.left = util.option.asSize(this.options.left);
this.frame.style.right = util.option.asSize(this.options.right);
this.frame.style.width = util.option.asSize(this.options.width, '100%');
this.frame.style.height = util.option.asSize(this.options.height, '');
return (changed > 0);
// get actual size
this.top = this.frame.offsetTop;
this.left = this.frame.offsetLeft;
this.width = this.frame.offsetWidth;
this.height = this.frame.offsetHeight;
};

+ 80
- 132
src/timeline/component/RootPanel.js View File

@ -10,32 +10,53 @@ function RootPanel(container, options) {
this.id = util.randomUUID();
this.container = container;
// create functions to be used as DOM event listeners
var me = this;
this.hammer = null;
this.options = options || {};
this.defaultOptions = {
autoResize: true
};
// create the HTML DOM
this._create();
// attach the root panel to the provided container
if (!this.container) throw new Error('Cannot repaint root panel: no container attached');
this.container.appendChild(this.getFrame());
this._initWatch();
}
RootPanel.prototype = new Panel();
// create listeners for all interesting events, these events will be emitted
// via the controller
/**
* Create the HTML DOM for the root panel
*/
RootPanel.prototype._create = function _create() {
// create frame
this.frame = document.createElement('div');
// create event listeners for all interesting events, these events will be
// emitted via emitter
this.hammer = Hammer(this.frame, {
prevent_default: true
});
this.listeners = {};
var me = this;
var events = [
'touch', 'pinch', 'tap', 'doubletap', 'hold',
'dragstart', 'drag', 'dragend',
'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
];
this.listeners = {};
events.forEach(function (event) {
me.listeners[event] = function () {
var listener = function () {
var args = [event].concat(Array.prototype.slice.call(arguments, 0));
me.controller.emit.apply(me.controller, args);
me.emit.apply(me, args);
};
me.hammer.on(event, listener);
me.listeners[event] = listener;
});
this.options = options || {};
this.defaultOptions = {
autoResize: true
};
}
RootPanel.prototype = new Panel();
};
/**
* Set options. Will extend the current options.
@ -47,80 +68,54 @@ RootPanel.prototype = new Panel();
* {String | Number | function} [height]
* {Boolean | function} [autoResize]
*/
RootPanel.prototype.setOptions = Component.prototype.setOptions;
/**
* Repaint the component
* @return {Boolean} changed
*/
RootPanel.prototype.repaint = function () {
var changed = 0,
update = util.updateProperty,
asSize = util.option.asSize,
options = this.options,
frame = this.frame;
RootPanel.prototype.setOptions = function setOptions(options) {
if (options) {
util.extend(this.options, options);
if (!frame) {
frame = document.createElement('div');
this.frame = frame;
this._registerListeners();
changed += 1;
}
if (!frame.parentNode) {
if (!this.container) {
throw new Error('Cannot repaint root panel: no container attached');
}
this.container.appendChild(frame);
changed += 1;
}
this.repaint();
frame.className = 'vis timeline rootpanel ' + options.orientation +
(options.editable ? ' editable' : '');
var className = options.className;
if (className) {
util.addClassName(frame, util.option.asString(className));
this._initWatch();
}
};
changed += update(frame.style, 'top', asSize(options.top, '0px'));
changed += update(frame.style, 'left', asSize(options.left, '0px'));
changed += update(frame.style, 'width', asSize(options.width, '100%'));
changed += update(frame.style, 'height', asSize(options.height, '100%'));
this._updateWatch();
return (changed > 0);
/**
* Get the frame of the root panel
*/
RootPanel.prototype.getFrame = function getFrame() {
return this.frame;
};
/**
* Reflow the component
* @return {Boolean} resized
* Repaint the root panel
*/
RootPanel.prototype.reflow = function () {
var changed = 0,
update = util.updateProperty,
frame = this.frame;
if (frame) {
changed += update(this, 'top', frame.offsetTop);
changed += update(this, 'left', frame.offsetLeft);
changed += update(this, 'width', frame.offsetWidth);
changed += update(this, 'height', frame.offsetHeight);
}
else {
changed += 1;
RootPanel.prototype.repaint = function repaint() {
// update class name
var options = this.options;
var editable = options.editable.updateTime || options.editable.updateGroup;
var className = 'vis timeline rootpanel ' + options.orientation + (editable ? ' editable' : '');
if (options.className) className += ' ' + util.option.asString(className);
this.frame.className = className;
// repaint the child components
var childsResized = this._repaintChilds();
// update frame size
this.frame.style.maxHeight = util.option.asSize(this.options.maxHeight, '');
this._updateSize();
// if the root panel or any of its childs is resized, repaint again,
// as other components may need to be resized accordingly
var resized = this._isResized() || childsResized;
if (resized) {
setTimeout(this.repaint.bind(this), 0);
}
return (changed > 0);
};
/**
* Update watching for resize, depending on the current option
* Initialize watching when option autoResize is true
* @private
*/
RootPanel.prototype._updateWatch = function () {
RootPanel.prototype._initWatch = function _initWatch() {
var autoResize = this.getOption('autoResize');
if (autoResize) {
this._watch();
@ -135,12 +130,12 @@ RootPanel.prototype._updateWatch = function () {
* automatically redraw itself.
* @private
*/
RootPanel.prototype._watch = function () {
RootPanel.prototype._watch = function _watch() {
var me = this;
this._unwatch();
var checkSize = function () {
var checkSize = function checkSize() {
var autoResize = me.getOption('autoResize');
if (!autoResize) {
// stop watching when the option autoResize is changed to false
@ -150,9 +145,12 @@ RootPanel.prototype._watch = function () {
if (me.frame) {
// check whether the frame is resized
if ((me.frame.clientWidth != me.width) ||
(me.frame.clientHeight != me.height)) {
me.requestReflow();
if ((me.frame.clientWidth != me.lastWidth) ||
(me.frame.clientHeight != me.lastHeight)) {
me.lastWidth = me.frame.clientWidth;
me.lastHeight = me.frame.clientHeight;
me.repaint();
// TODO: emit a resize event instead?
}
}
};
@ -167,7 +165,7 @@ RootPanel.prototype._watch = function () {
* Stop watching for a resize of the frame.
* @private
*/
RootPanel.prototype._unwatch = function () {
RootPanel.prototype._unwatch = function _unwatch() {
if (this.watchTimer) {
clearInterval(this.watchTimer);
this.watchTimer = undefined;
@ -175,53 +173,3 @@ RootPanel.prototype._unwatch = function () {
// TODO: remove event listener on window.resize
};
/**
* Set controller for this component, or remove current controller by passing
* null as parameter value.
* @param {Controller | null} controller
*/
RootPanel.prototype.setController = function setController (controller) {
this.controller = controller || null;
if (this.controller) {
this._registerListeners();
}
else {
this._unregisterListeners();
}
};
/**
* Register event emitters emitted by the rootpanel
* @private
*/
RootPanel.prototype._registerListeners = function () {
if (this.frame && this.controller && !this.hammer) {
this.hammer = Hammer(this.frame, {
prevent_default: true
});
for (var event in this.listeners) {
if (this.listeners.hasOwnProperty(event)) {
this.hammer.on(event, this.listeners[event]);
}
}
}
};
/**
* Unregister event emitters from the rootpanel
* @private
*/
RootPanel.prototype._unregisterListeners = function () {
if (this.hammer) {
for (var event in this.listeners) {
if (this.listeners.hasOwnProperty(event)) {
this.hammer.off(event, this.listeners[event]);
}
}
this.hammer = null;
}
};

+ 191
- 277
src/timeline/component/TimeAxis.js View File

@ -1,17 +1,12 @@
/**
* A horizontal time axis
* @param {Component} parent
* @param {Component[]} [depends] Components on which this components depends
* (except for the parent)
* @param {Object} [options] See TimeAxis.setOptions for the available
* options.
* @constructor TimeAxis
* @extends Component
*/
function TimeAxis (parent, depends, options) {
function TimeAxis (options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.dom = {
majorLines: [],
@ -42,8 +37,10 @@ function TimeAxis (parent, depends, options) {
showMajorLabels: true
};
this.conversion = null;
this.range = null;
// create the HTML DOM
this._create();
}
TimeAxis.prototype = new Component();
@ -51,6 +48,13 @@ TimeAxis.prototype = new Component();
// TODO: comment options
TimeAxis.prototype.setOptions = Component.prototype.setOptions;
/**
* Create the HTML DOM for the TimeAxis
*/
TimeAxis.prototype._create = function _create() {
this.frame = document.createElement('div');
};
/**
* Set a range (start and end)
* @param {Range | Object} range A Range or an object containing start and end.
@ -64,126 +68,70 @@ TimeAxis.prototype.setRange = function (range) {
};
/**
* Convert a position on screen (pixels) to a datetime
* @param {int} x Position on the screen in pixels
* @return {Date} time The datetime the corresponds with given position x
*/
TimeAxis.prototype.toTime = function(x) {
var conversion = this.conversion;
return new Date(x / conversion.scale + conversion.offset);
};
/**
* Convert a datetime (Date object) into a position on the screen
* @param {Date} time A date
* @return {int} x The position on the screen in pixels which corresponds
* with the given date.
* @private
* Get the outer frame of the time axis
* @return {HTMLElement} frame
*/
TimeAxis.prototype.toScreen = function(time) {
var conversion = this.conversion;
return (time.valueOf() - conversion.offset) * conversion.scale;
TimeAxis.prototype.getFrame = function getFrame() {
return this.frame;
};
/**
* Repaint the component
* @return {Boolean} changed
* @return {boolean} Returns true if the component is resized
*/
TimeAxis.prototype.repaint = function () {
var changed = 0,
update = util.updateProperty,
asSize = util.option.asSize,
var asSize = util.option.asSize,
options = this.options,
orientation = this.getOption('orientation'),
props = this.props,
step = this.step;
var frame = this.frame;
if (!frame) {
frame = document.createElement('div');
this.frame = frame;
changed += 1;
}
frame.className = 'axis';
// TODO: custom className?
if (!frame.parentNode) {
if (!this.parent) {
throw new Error('Cannot repaint time axis: no parent attached');
}
var parentContainer = this.parent.getContainer();
if (!parentContainer) {
throw new Error('Cannot repaint time axis: parent has no container element');
}
parentContainer.appendChild(frame);
frame = this.frame;
changed += 1;
}
// update classname
frame.className = 'timeaxis'; // TODO: add className from options if defined
var parent = frame.parentNode;
if (parent) {
var beforeChild = frame.nextSibling;
parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
(this.props.parentHeight - this.height) + 'px' :
'0px';
changed += update(frame.style, 'top', asSize(options.top, defaultTop));
changed += update(frame.style, 'left', asSize(options.left, '0px'));
changed += update(frame.style, 'width', asSize(options.width, '100%'));
changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
// get characters width and height
this._repaintMeasureChars();
if (this.step) {
this._repaintStart();
step.first();
var xFirstMajorLabel = undefined;
var max = 0;
while (step.hasNext() && max < 1000) {
max++;
var cur = step.getCurrent(),
x = this.toScreen(cur),
isMajor = step.isMajor();
// TODO: lines must have a width, such that we can create css backgrounds
if (this.getOption('showMinorLabels')) {
this._repaintMinorText(x, step.getLabelMinor());
}
if (isMajor && this.getOption('showMajorLabels')) {
if (x > 0) {
if (xFirstMajorLabel == undefined) {
xFirstMajorLabel = x;
}
this._repaintMajorText(x, step.getLabelMajor());
}
this._repaintMajorLine(x);
}
else {
this._repaintMinorLine(x);
}
// calculate character width and height
this._calculateCharSize();
step.next();
}
// TODO: recalculate sizes only needed when parent is resized or options is changed
var orientation = this.getOption('orientation'),
showMinorLabels = this.getOption('showMinorLabels'),
showMajorLabels = this.getOption('showMajorLabels');
// create a major label on the left when needed
if (this.getOption('showMajorLabels')) {
var leftTime = this.toTime(0),
leftText = step.getLabelMajor(leftTime),
widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
// determine the width and height of the elemens for the axis
var parentHeight = this.parent.height;
props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
this.height = props.minorLabelHeight + props.majorLabelHeight;
this.width = frame.offsetWidth; // TODO: only update the width when the frame is resized?
if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
this._repaintMajorText(0, leftText);
}
}
props.minorLineHeight = parentHeight + props.minorLabelHeight;
props.minorLineWidth = 1; // TODO: really calculate width
props.majorLineHeight = parentHeight + this.height;
props.majorLineWidth = 1; // TODO: really calculate width
this._repaintEnd();
// take frame offline while updating (is almost twice as fast)
var beforeChild = frame.nextSibling;
parent.removeChild(frame);
// TODO: top/bottom positioning should be determined by options set in the Timeline, not here
if (orientation == 'top') {
frame.style.top = '0';
frame.style.left = '0';
frame.style.bottom = '';
frame.style.width = asSize(options.width, '100%');
frame.style.height = this.height + 'px';
}
else { // bottom
frame.style.top = '';
frame.style.bottom = '0';
frame.style.left = '0';
frame.style.width = asSize(options.width, '100%');
frame.style.height = this.height + 'px';
}
this._repaintLabels();
this._repaintLine();
// put frame online again
@ -195,34 +143,80 @@ TimeAxis.prototype.repaint = function () {
}
}
return (changed > 0);
return this._isResized();
};
/**
* Start a repaint. Move all DOM elements to a redundant list, where they
* can be picked for re-use, or can be cleaned up in the end
* Repaint major and minor text labels and vertical grid lines
* @private
*/
TimeAxis.prototype._repaintStart = function () {
var dom = this.dom,
redundant = dom.redundant;
redundant.majorLines = dom.majorLines;
redundant.majorTexts = dom.majorTexts;
redundant.minorLines = dom.minorLines;
redundant.minorTexts = dom.minorTexts;
TimeAxis.prototype._repaintLabels = function () {
var orientation = this.getOption('orientation');
// calculate range and step (step such that we have space for 7 characters per label)
var start = util.convert(this.range.start, 'Number'),
end = util.convert(this.range.end, 'Number'),
minimumStep = this.options.toTime((this.props.minorCharWidth || 10) * 7).valueOf()
-this.options.toTime(0).valueOf();
var step = new TimeStep(new Date(start), new Date(end), minimumStep);
this.step = step;
// Move all DOM elements to a "redundant" list, where they
// can be picked for re-use, and clear the lists with lines and texts.
// At the end of the function _repaintLabels, left over elements will be cleaned up
var dom = this.dom;
dom.redundant.majorLines = dom.majorLines;
dom.redundant.majorTexts = dom.majorTexts;
dom.redundant.minorLines = dom.minorLines;
dom.redundant.minorTexts = dom.minorTexts;
dom.majorLines = [];
dom.majorTexts = [];
dom.minorLines = [];
dom.minorTexts = [];
};
/**
* End a repaint. Cleanup leftover DOM elements in the redundant list
* @private
*/
TimeAxis.prototype._repaintEnd = function () {
step.first();
var xFirstMajorLabel = undefined;
var max = 0;
while (step.hasNext() && max < 1000) {
max++;
var cur = step.getCurrent(),
x = this.options.toScreen(cur),
isMajor = step.isMajor();
// TODO: lines must have a width, such that we can create css backgrounds
if (this.getOption('showMinorLabels')) {
this._repaintMinorText(x, step.getLabelMinor(), orientation);
}
if (isMajor && this.getOption('showMajorLabels')) {
if (x > 0) {
if (xFirstMajorLabel == undefined) {
xFirstMajorLabel = x;
}
this._repaintMajorText(x, step.getLabelMajor(), orientation);
}
this._repaintMajorLine(x, orientation);
}
else {
this._repaintMinorLine(x, orientation);
}
step.next();
}
// create a major label on the left when needed
if (this.getOption('showMajorLabels')) {
var leftTime = this.options.toTime(0),
leftText = step.getLabelMajor(leftTime),
widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
this._repaintMajorText(0, leftText, orientation);
}
}
// Cleanup leftover DOM elements from the redundant list
util.forEach(this.dom.redundant, function (arr) {
while (arr.length) {
var elem = arr.pop();
@ -233,14 +227,14 @@ TimeAxis.prototype._repaintEnd = function () {
});
};
/**
* Create a minor label for the axis at position x
* @param {Number} x
* @param {String} text
* @param {String} orientation "top" or "bottom" (default)
* @private
*/
TimeAxis.prototype._repaintMinorText = function (x, text) {
TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
// reuse redundant label
var label = this.dom.redundant.minorTexts.shift();
@ -255,8 +249,16 @@ TimeAxis.prototype._repaintMinorText = function (x, text) {
this.dom.minorTexts.push(label);
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.left = x + 'px';
label.style.top = this.props.minorLabelTop + 'px';
//label.title = title; // TODO: this is a heavy operation
};
@ -264,9 +266,10 @@ TimeAxis.prototype._repaintMinorText = function (x, text) {
* Create a Major label for the axis at position x
* @param {Number} x
* @param {String} text
* @param {String} orientation "top" or "bottom" (default)
* @private
*/
TimeAxis.prototype._repaintMajorText = function (x, text) {
TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
// reuse redundant label
var label = this.dom.redundant.majorTexts.shift();
@ -281,17 +284,26 @@ TimeAxis.prototype._repaintMajorText = function (x, text) {
this.dom.majorTexts.push(label);
label.childNodes[0].nodeValue = text;
label.style.top = this.props.majorLabelTop + 'px';
label.style.left = x + 'px';
//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.left = x + 'px';
};
/**
* Create a minor line for the axis at position x
* @param {Number} x
* @param {String} orientation "top" or "bottom" (default)
* @private
*/
TimeAxis.prototype._repaintMinorLine = function (x) {
TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
// reuse redundant line
var line = this.dom.redundant.minorLines.shift();
@ -304,7 +316,14 @@ TimeAxis.prototype._repaintMinorLine = function (x) {
this.dom.minorLines.push(line);
var props = this.props;
line.style.top = props.minorLineTop + 'px';
if (orientation == 'top') {
line.style.top = this.props.majorLabelHeight + 'px';
line.style.bottom = '';
}
else {
line.style.top = '';
line.style.bottom = this.props.majorLabelHeight + 'px';
}
line.style.height = props.minorLineHeight + 'px';
line.style.left = (x - props.minorLineWidth / 2) + 'px';
};
@ -312,9 +331,10 @@ TimeAxis.prototype._repaintMinorLine = function (x) {
/**
* Create a Major line for the axis at position x
* @param {Number} x
* @param {String} orientation "top" or "bottom" (default)
* @private
*/
TimeAxis.prototype._repaintMajorLine = function (x) {
TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
// reuse redundant line
var line = this.dom.redundant.majorLines.shift();
@ -327,7 +347,14 @@ TimeAxis.prototype._repaintMajorLine = function (x) {
this.dom.majorLines.push(line);
var props = this.props;
line.style.top = props.majorLineTop + 'px';
if (orientation == 'top') {
line.style.top = '0px';
line.style.bottom = '';
}
else {
line.style.top = '';
line.style.bottom = '0px';
}
line.style.left = (x - props.majorLineWidth / 2) + 'px';
line.style.height = props.majorLineHeight + 'px';
};
@ -340,7 +367,7 @@ TimeAxis.prototype._repaintMajorLine = function (x) {
TimeAxis.prototype._repaintLine = function() {
var line = this.dom.line,
frame = this.frame,
options = this.options;
orientation = this.getOption('orientation');
// line before all axis elements
if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
@ -357,167 +384,54 @@ TimeAxis.prototype._repaintLine = function() {
this.dom.line = line;
}
line.style.top = this.props.lineTop + 'px';
if (orientation == 'top') {
line.style.top = this.height + 'px';
line.style.bottom = '';
}
else {
line.style.top = '';
line.style.bottom = this.height + 'px';
}
}
else {
if (line && line.parentElement) {
frame.removeChild(line.line);
if (line && line.parentNode) {
line.parentNode.removeChild(line);
delete this.dom.line;
}
}
};
/**
* Create characters used to determine the size of text on the axis
* Determine the size of text on the axis (both major and minor axis).
* The size is calculated only once and then cached in this.props.
* @private
*/
TimeAxis.prototype._repaintMeasureChars = function () {
// calculate the width and height of a single character
// this is used to calculate the step size, and also the positioning of the
// axis
var dom = this.dom,
text;
if (!dom.measureCharMinor) {
text = document.createTextNode('0');
TimeAxis.prototype._calculateCharSize = function () {
// determine the char width and height on the minor axis
if (!('minorCharHeight' in this.props)) {
var textMinor = document.createTextNode('0');
var measureCharMinor = document.createElement('DIV');
measureCharMinor.className = 'text minor measure';
measureCharMinor.appendChild(text);
measureCharMinor.appendChild(textMinor);
this.frame.appendChild(measureCharMinor);
dom.measureCharMinor = measureCharMinor;
this.props.minorCharHeight = measureCharMinor.clientHeight;
this.props.minorCharWidth = measureCharMinor.clientWidth;
this.frame.removeChild(measureCharMinor);
}
if (!dom.measureCharMajor) {
text = document.createTextNode('0');
if (!('majorCharHeight' in this.props)) {
var textMajor = document.createTextNode('0');
var measureCharMajor = document.createElement('DIV');
measureCharMajor.className = 'text major measure';
measureCharMajor.appendChild(text);
measureCharMajor.appendChild(textMajor);
this.frame.appendChild(measureCharMajor);
dom.measureCharMajor = measureCharMajor;
}
};
/**
* Reflow the component
* @return {Boolean} resized
*/
TimeAxis.prototype.reflow = function () {
var changed = 0,
update = util.updateProperty,
frame = this.frame,
range = this.range;
if (!range) {
throw new Error('Cannot repaint time axis: no range configured');
}
if (frame) {
changed += update(this, 'top', frame.offsetTop);
changed += update(this, 'left', frame.offsetLeft);
this.props.majorCharHeight = measureCharMajor.clientHeight;
this.props.majorCharWidth = measureCharMajor.clientWidth;
// calculate size of a character
var props = this.props,
showMinorLabels = this.getOption('showMinorLabels'),
showMajorLabels = this.getOption('showMajorLabels'),
measureCharMinor = this.dom.measureCharMinor,
measureCharMajor = this.dom.measureCharMajor;
if (measureCharMinor) {
props.minorCharHeight = measureCharMinor.clientHeight;
props.minorCharWidth = measureCharMinor.clientWidth;
}
if (measureCharMajor) {
props.majorCharHeight = measureCharMajor.clientHeight;
props.majorCharWidth = measureCharMajor.clientWidth;
}
var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
if (parentHeight != props.parentHeight) {
props.parentHeight = parentHeight;
changed += 1;
}
switch (this.getOption('orientation')) {
case 'bottom':
props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
props.minorLabelTop = 0;
props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
props.minorLineTop = -this.top;
props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
props.minorLineWidth = 1; // TODO: really calculate width
props.majorLineTop = -this.top;
props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
props.majorLineWidth = 1; // TODO: really calculate width
props.lineTop = 0;
break;
case 'top':
props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
props.majorLabelTop = 0;
props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
props.minorLineTop = props.minorLabelTop;
props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
props.minorLineWidth = 1; // TODO: really calculate width
props.majorLineTop = 0;
props.majorLineHeight = Math.max(parentHeight - this.top);
props.majorLineWidth = 1; // TODO: really calculate width
props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
break;
default:
throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
}
var height = props.minorLabelHeight + props.majorLabelHeight;
changed += update(this, 'width', frame.offsetWidth);
changed += update(this, 'height', height);
// calculate range and step
this._updateConversion();
var start = util.convert(range.start, 'Number'),
end = util.convert(range.end, 'Number'),
minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf()
-this.toTime(0).valueOf();
this.step = new TimeStep(new Date(start), new Date(end), minimumStep);
changed += update(props.range, 'start', start);
changed += update(props.range, 'end', end);
changed += update(props.range, 'minimumStep', minimumStep.valueOf());
}
return (changed > 0);
};
/**
* Calculate the scale and offset to convert a position on screen to the
* corresponding date and vice versa.
* After the method _updateConversion is executed once, the methods toTime
* and toScreen can be used.
* @private
*/
TimeAxis.prototype._updateConversion = function() {
var range = this.range;
if (!range) {
throw new Error('No range configured');
}
if (range.conversion) {
this.conversion = range.conversion(this.width);
}
else {
this.conversion = Range.conversion(range.start, range.end, this.width);
this.frame.removeChild(measureCharMajor);
}
};

+ 0
- 59
src/timeline/component/css/groupset.css View File

@ -1,59 +0,0 @@
.vis.timeline .groupset {
position: absolute;
padding: 0;
margin: 0;
}
.vis.timeline .labels {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
border-right: 1px solid #bfbfbf;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.vis.timeline .labels .label-set {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
border-top: none;
border-bottom: 1px solid #bfbfbf;
}
.vis.timeline .labels .label-set .vlabel {
position: absolute;
left: 0;
top: 0;
width: 100%;
color: #4d4d4d;
}
.vis.timeline.top .labels .label-set .vlabel,
.vis.timeline.top .groupset .itemset-axis {
border-top: 1px solid #bfbfbf;
border-bottom: none;
}
.vis.timeline.bottom .labels .label-set .vlabel,
.vis.timeline.bottom .groupset .itemset-axis {
border-top: none;
border-bottom: 1px solid #bfbfbf;
}
.vis.timeline .labels .label-set .vlabel .inner {
display: inline-block;
padding: 5px;
}

+ 16
- 24
src/timeline/component/css/item.css View File

@ -3,9 +3,15 @@
position: absolute;
color: #1A1A1A;
border-color: #97B0F8;
border-width: 1px;
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 {
@ -20,49 +26,30 @@
.vis.timeline .item.point.selected {
background-color: #FFF785;
z-index: 999;
}
.vis.timeline .item.point.selected .dot {
border-color: #FFC200;
}
.vis.timeline .item.cluster {
/* TODO: use another color or pattern? */
background: #97B0F8 url('img/cluster_bg.png');
color: white;
}
.vis.timeline .item.cluster.point {
border-color: #D5DDF6;
}
.vis.timeline .item.box {
text-align: center;
border-style: solid;
border-width: 1px;
border-radius: 5px;
-moz-border-radius: 5px; /* For Firefox 3.6 and older */
border-radius: 2px;
}
.vis.timeline .item.point {
background: none;
}
.vis.timeline .dot,
.vis.timeline .item.dot {
padding: 0;
border: 5px solid #97B0F8;
position: absolute;
border-radius: 5px;
-moz-border-radius: 5px; /* For Firefox 3.6 and older */
padding: 0;
border-width: 4px;
border-style: solid;
border-radius: 4px;
}
.vis.timeline .item.range,
.vis.timeline .item.rangeoverflow{
border-style: solid;
border-width: 1px;
border-radius: 2px;
-moz-border-radius: 2px; /* For Firefox 3.6 and older */
-moz-box-sizing: border-box;
box-sizing: border-box;
}
@ -83,6 +70,11 @@
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 {

+ 25
- 4
src/timeline/component/css/itemset.css View File

@ -1,9 +1,15 @@
.vis.timeline .itemset {
position: absolute;
position: relative;
padding: 0;
margin: 0;
overflow: hidden;
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 .background {
@ -12,6 +18,21 @@
.vis.timeline .foreground {
}
.vis.timeline .itemset-axis {
position: absolute;
.vis.timeline .axis {
overflow: visible;
}
.vis.timeline .group {
position: relative;
box-sizing: border-box;
}
.vis.timeline.top .group {
border-top: 1px solid #bfbfbf;
border-bottom: none;
}
.vis.timeline.bottom .group {
border-top: none;
border-bottom: 1px solid #bfbfbf;
}

+ 34
- 0
src/timeline/component/css/labelset.css View File

@ -0,0 +1,34 @@
.vis.timeline .labelset {
position: relative;
width: 100%;
overflow: hidden;
box-sizing: border-box;
}
.vis.timeline .labelset .vlabel {
position: relative;
left: 0;
top: 0;
width: 100%;
color: #4d4d4d;
box-sizing: border-box;
}
.vis.timeline.top .labelset .vlabel {
border-top: 1px solid #bfbfbf;
border-bottom: none;
}
.vis.timeline.bottom .labelset .vlabel {
border-top: none;
border-bottom: 1px solid #bfbfbf;
}
.vis.timeline .labelset .vlabel .inner {
display: inline-block;
padding: 5px;
}

+ 15
- 1
src/timeline/component/css/panel.css View File

@ -4,11 +4,25 @@
overflow: hidden;
border: 1px solid #bfbfbf;
-moz-box-sizing: border-box;
box-sizing: border-box;
/* FIXME: there is an issue with the height of the items when panel height is animated
-webkit-transition: height 4s ease-in-out;
transition: height 4s ease-in-out;
/**/
}
.vis.timeline .vpanel {
position: absolute;
overflow: hidden;
box-sizing: border-box;
}
.vis.timeline .vpanel.side {
border-right: 1px solid #bfbfbf;
}
.vis.timeline .vpanel.side.hidden {
display: none;
}

+ 8
- 8
src/timeline/component/css/timeaxis.css View File

@ -1,15 +1,15 @@
.vis.timeline .axis {
position: relative;
.vis.timeline .timeaxis {
position: absolute;
}
.vis.timeline .axis .text {
.vis.timeline .timeaxis .text {
position: absolute;
color: #4d4d4d;
padding: 3px;
white-space: nowrap;
}
.vis.timeline .axis .text.measure {
.vis.timeline .timeaxis .text.measure {
position: absolute;
padding-left: 0;
padding-right: 0;
@ -18,13 +18,13 @@
visibility: hidden;
}
.vis.timeline .axis .grid.vertical {
.vis.timeline .timeaxis .grid.vertical {
position: absolute;
width: 0;
border-right: 1px solid;
}
.vis.timeline .axis .grid.horizontal {
.vis.timeline .timeaxis .grid.horizontal {
position: absolute;
left: 0;
width: 100%;
@ -32,10 +32,10 @@
border-bottom: 1px solid;
}
.vis.timeline .axis .grid.minor {
.vis.timeline .timeaxis .grid.minor {
border-color: #e5e5e5;
}
.vis.timeline .axis .grid.major {
.vis.timeline .timeaxis .grid.major {
border-color: #bfbfbf;
}

+ 48
- 26
src/timeline/component/item/Item.js View File

@ -1,26 +1,27 @@
/**
* @constructor Item
* @param {ItemSet} parent
* @param {Object} data Object containing (optional) parameters type,
* start, end, content, group, className.
* @param {Object} [options] Options to set initial property values
* @param {Object} [defaultOptions] default options
* // TODO: describe available options
*/
function Item (parent, data, options, defaultOptions) {
this.parent = parent;
function Item (data, options, defaultOptions) {
this.id = null;
this.parent = null;
this.data = data;
this.dom = null;
this.options = options || {};
this.defaultOptions = defaultOptions || {};
this.selected = false;
this.visible = false;
this.top = 0;
this.left = 0;
this.width = 0;
this.height = 0;
this.offset = 0;
this.displayed = false;
this.dirty = true;
this.top = null;
this.left = null;
this.width = null;
this.height = null;
}
/**
@ -28,7 +29,7 @@ function Item (parent, data, options, defaultOptions) {
*/
Item.prototype.select = function select() {
this.selected = true;
if (this.visible) this.repaint();
if (this.displayed) this.repaint();
};
/**
@ -36,7 +37,34 @@ Item.prototype.select = function select() {
*/
Item.prototype.unselect = function unselect() {
this.selected = false;
if (this.visible) this.repaint();
if (this.displayed) this.repaint();
};
/**
* Set a parent for the item
* @param {ItemSet | Group} parent
*/
Item.prototype.setParent = function setParent(parent) {
if (this.displayed) {
this.hide();
this.parent = parent;
if (this.parent) {
this.show();
}
}
else {
this.parent = parent;
}
};
/**
* Check whether this item is visible inside given range
* @returns {{start: Number, end: Number}} range with a timestamp for start and end
* @returns {boolean} True if visible
*/
Item.prototype.isVisible = function isVisible (range) {
// Should be implemented by Item implementations
return false;
};
/**
@ -57,40 +85,34 @@ Item.prototype.hide = function hide() {
/**
* Repaint the item
* @return {Boolean} changed
*/
Item.prototype.repaint = function repaint() {
// should be implemented by the item
return false;
};
/**
* Reflow the item
* @return {Boolean} resized
* Reposition the Item horizontally
*/
Item.prototype.reflow = function reflow() {
Item.prototype.repositionX = function repositionX() {
// should be implemented by the item
return false;
};
/**
* Give the item a display offset in pixels
* @param {Number} offset Offset on screen in pixels
* Reposition the Item vertically
*/
Item.prototype.setOffset = function setOffset(offset) {
this.offset = offset;
Item.prototype.repositionY = function repositionY() {
// should be implemented by the item
};
/**
* Repaint a delete button on the top right of the item when the item is selected
* @param {HTMLElement} anchor
* @private
* @protected
*/
Item.prototype._repaintDeleteButton = function (anchor) {
if (this.selected && this.options.editable && !this.dom.deleteButton) {
if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
// create and show button
var parent = this.parent;
var id = this.id;
var me = this;
var deleteButton = document.createElement('div');
deleteButton.className = 'delete';
@ -99,7 +121,7 @@ Item.prototype._repaintDeleteButton = function (anchor) {
Hammer(deleteButton, {
preventDefault: true
}).on('tap', function (event) {
parent.removeItem(id);
me.parent.removeFromDataSet(me);
event.stopPropagation();
});

+ 156
- 230
src/timeline/component/item/ItemBox.js View File

@ -1,304 +1,230 @@
/**
* @constructor ItemBox
* @extends Item
* @param {ItemSet} parent
* @param {Object} data Object containing parameters start
* content, className.
* @param {Object} [options] Options to set initial property values
* @param {Object} [defaultOptions] default options
* // TODO: describe available options
*/
function ItemBox (parent, data, options, defaultOptions) {
function ItemBox (data, options, defaultOptions) {
this.props = {
dot: {
left: 0,
top: 0,
width: 0,
height: 0
},
line: {
top: 0,
left: 0,
width: 0,
height: 0
}
};
Item.call(this, parent, data, options, defaultOptions);
// validate data
if (data) {
if (data.start == undefined) {
throw new Error('Property "start" missing in item ' + data);
}
}
Item.call(this, data, options, defaultOptions);
}
ItemBox.prototype = new Item (null, null);
ItemBox.prototype = new Item (null);
/**
* Check whether this item is visible inside given range
* @returns {{start: Number, end: Number}} range with a timestamp for start and end
* @returns {boolean} True if visible
*/
ItemBox.prototype.isVisible = function isVisible (range) {
// determine visibility
// TODO: account for the real width of the item. Right now we just add 1/4 to the window
var interval = (range.end - range.start) / 4;
return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
};
/**
* Repaint the item
* @return {Boolean} changed
*/
ItemBox.prototype.repaint = function repaint() {
// TODO: make an efficient repaint
var changed = false;
var dom = this.dom;
if (!dom) {
this._create();
// create DOM
this.dom = {};
dom = this.dom;
changed = true;
// create main box
dom.box = document.createElement('DIV');
// contents box (inside the background box). used for making margins
dom.content = document.createElement('DIV');
dom.content.className = 'content';
dom.box.appendChild(dom.content);
// line to axis
dom.line = document.createElement('DIV');
dom.line.className = 'line';
// dot on axis
dom.dot = document.createElement('DIV');
dom.dot.className = 'dot';
// attach this item as attribute
dom.box['timeline-item'] = this;
}
if (dom) {
if (!this.parent) {
throw new Error('Cannot repaint item: no parent attached');
// append DOM to parent DOM
if (!this.parent) {
throw new Error('Cannot repaint item: no parent attached');
}
if (!dom.box.parentNode) {
var foreground = this.parent.getForeground();
if (!foreground) throw new Error('Cannot repaint time axis: parent has no foreground container element');
foreground.appendChild(dom.box);
}
if (!dom.line.parentNode) {
var background = this.parent.getBackground();
if (!background) throw new Error('Cannot repaint time axis: parent has no background container element');
background.appendChild(dom.line);
}
if (!dom.dot.parentNode) {
var axis = this.parent.getAxis();
if (!background) throw new Error('Cannot repaint time axis: parent has no axis container element');
axis.appendChild(dom.dot);
}
this.displayed = true;
// update contents
if (this.data.content != this.content) {
this.content = this.data.content;
if (this.content instanceof Element) {
dom.content.innerHTML = '';
dom.content.appendChild(this.content);
}
if (!dom.box.parentNode) {
var foreground = this.parent.getForeground();
if (!foreground) {
throw new Error('Cannot repaint time axis: ' +
'parent has no foreground container element');
}
foreground.appendChild(dom.box);
changed = true;
else if (this.data.content != undefined) {
dom.content.innerHTML = this.content;
}
if (!dom.line.parentNode) {
var background = this.parent.getBackground();
if (!background) {
throw new Error('Cannot repaint time axis: ' +
'parent has no background container element');
}
background.appendChild(dom.line);
changed = true;
else {
throw new Error('Property "content" missing in item ' + this.data.id);
}
if (!dom.dot.parentNode) {
var axis = this.parent.getAxis();
if (!background) {
throw new Error('Cannot repaint time axis: ' +
'parent has no axis container element');
}
axis.appendChild(dom.dot);
changed = true;
}
this.dirty = true;
}
this._repaintDeleteButton(dom.box);
// update contents
if (this.data.content != this.content) {
this.content = this.data.content;
if (this.content instanceof Element) {
dom.content.innerHTML = '';
dom.content.appendChild(this.content);
}
else if (this.data.content != undefined) {
dom.content.innerHTML = this.content;
}
else {
throw new Error('Property "content" missing in item ' + this.data.id);
}
changed = true;
}
// update class
var className = (this.data.className? ' ' + this.data.className : '') +
(this.selected ? ' selected' : '');
if (this.className != className) {
this.className = className;
dom.box.className = 'item box' + className;
dom.line.className = 'item line' + className;
dom.dot.className = 'item dot' + className;
// update class
var className = (this.data.className? ' ' + this.data.className : '') +
(this.selected ? ' selected' : '');
if (this.className != className) {
this.className = className;
dom.box.className = 'item box' + className;
dom.line.className = 'item line' + className;
dom.dot.className = 'item dot' + className;
changed = true;
}
this.dirty = true;
}
return changed;
// recalculate size
if (this.dirty) {
this.props.dot.height = dom.dot.offsetHeight;
this.props.dot.width = dom.dot.offsetWidth;
this.props.line.width = dom.line.offsetWidth;
this.width = dom.box.offsetWidth;
this.height = dom.box.offsetHeight;
this.dirty = false;
}
this._repaintDeleteButton(dom.box);
};
/**
* Show the item in the DOM (when not already visible). The items DOM will
* Show the item in the DOM (when not already displayed). The items DOM will
* be created when needed.
* @return {Boolean} changed
*/
ItemBox.prototype.show = function show() {
if (!this.dom || !this.dom.box.parentNode) {
return this.repaint();
}
else {
return false;
if (!this.displayed) {
this.repaint();
}
};
/**
* Hide the item from the DOM (when visible)
* @return {Boolean} changed
*/
ItemBox.prototype.hide = function hide() {
var changed = false,
dom = this.dom;
if (dom) {
if (dom.box.parentNode) {
dom.box.parentNode.removeChild(dom.box);
changed = true;
}
if (dom.line.parentNode) {
dom.line.parentNode.removeChild(dom.line);
}
if (dom.dot.parentNode) {
dom.dot.parentNode.removeChild(dom.dot);
}
if (this.displayed) {
var dom = this.dom;
if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
this.top = null;
this.left = null;
this.displayed = false;
}
return changed;
};
/**
* Reflow the item: calculate its actual size and position from the DOM
* @return {boolean} resized returns true if the axis is resized
* @override
* Reposition the item horizontally
* @Override
*/
ItemBox.prototype.reflow = function reflow() {
var changed = 0,
update,
dom,
props,
options,
margin,
start,
align,
orientation,
top,
ItemBox.prototype.repositionX = function repositionX() {
var start = this.defaultOptions.toScreen(this.data.start),
align = this.options.align || this.defaultOptions.align,
left,
data,
range;
box = this.dom.box,
line = this.dom.line,
dot = this.dom.dot;
if (this.data.start == undefined) {
throw new Error('Property "start" missing in item ' + this.data.id);
// calculate left position of the box
if (align == 'right') {
this.left = start - this.width;
}
data = this.data;
range = this.parent && this.parent.range;
if (data && range) {
// TODO: account for the width of the item
var interval = (range.end - range.start);
this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
else if (align == 'left') {
this.left = start;
}
else {
this.visible = false;
// default or 'center'
this.left = start - this.width / 2;
}
if (this.visible) {
dom = this.dom;
if (dom) {
update = util.updateProperty;
props = this.props;
options = this.options;
start = this.parent.toScreen(this.data.start) + this.offset;
align = options.align || this.defaultOptions.align;
margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
orientation = options.orientation || this.defaultOptions.orientation;
changed += update(props.dot, 'height', dom.dot.offsetHeight);
changed += update(props.dot, 'width', dom.dot.offsetWidth);
changed += update(props.line, 'width', dom.line.offsetWidth);
changed += update(props.line, 'height', dom.line.offsetHeight);
changed += update(props.line, 'top', dom.line.offsetTop);
changed += update(this, 'width', dom.box.offsetWidth);
changed += update(this, 'height', dom.box.offsetHeight);
if (align == 'right') {
left = start - this.width;
}
else if (align == 'left') {
left = start;
}
else {
// default or 'center'
left = start - this.width / 2;
}
changed += update(this, 'left', left);
changed += update(props.line, 'left', start - props.line.width / 2);
changed += update(props.dot, 'left', start - props.dot.width / 2);
changed += update(props.dot, 'top', -props.dot.height / 2);
if (orientation == 'top') {
top = margin;
changed += update(this, 'top', top);
}
else {
// default or 'bottom'
var parentHeight = this.parent.height;
top = parentHeight - this.height - margin;
changed += update(this, 'top', top);
}
}
else {
changed += 1;
}
}
return (changed > 0);
};
/**
* Create an items DOM
* @private
*/
ItemBox.prototype._create = function _create() {
var dom = this.dom;
if (!dom) {
this.dom = dom = {};
// reposition box
box.style.left = this.left + 'px';
// create the box
dom.box = document.createElement('DIV');
// className is updated in repaint()
// reposition line
line.style.left = (start - this.props.line.width / 2) + 'px';
// contents box (inside the background box). used for making margins
dom.content = document.createElement('DIV');
dom.content.className = 'content';
dom.box.appendChild(dom.content);
// line to axis
dom.line = document.createElement('DIV');
dom.line.className = 'line';
// dot on axis
dom.dot = document.createElement('DIV');
dom.dot.className = 'dot';
// attach this item as attribute
dom.box['timeline-item'] = this;
}
// reposition dot
dot.style.left = (start - this.props.dot.width / 2) + 'px';
};
/**
* Reposition the item, recalculate its left, top, and width, using the current
* range and size of the items itemset
* @override
* Reposition the item vertically
* @Override
*/
ItemBox.prototype.reposition = function reposition() {
var dom = this.dom,
props = this.props,
orientation = this.options.orientation || this.defaultOptions.orientation;
if (dom) {
var box = dom.box,
line = dom.line,
dot = dom.dot;
box.style.left = this.left + 'px';
box.style.top = this.top + 'px';
line.style.left = props.line.left + 'px';
if (orientation == 'top') {
line.style.top = 0 + 'px';
line.style.height = this.top + 'px';
}
else {
// orientation 'bottom'
line.style.top = (this.top + this.height) + 'px';
line.style.height = Math.max(this.parent.height - this.top - this.height +
this.props.dot.height / 2, 0) + 'px';
}
ItemBox.prototype.repositionY = function repositionY () {
var orientation = this.options.orientation || this.defaultOptions.orientation,
box = this.dom.box,
line = this.dom.line,
dot = this.dom.dot;
if (orientation == 'top') {
box.style.top = (this.top || 0) + 'px';
box.style.bottom = '';
line.style.top = '0';
line.style.bottom = '';
line.style.height = (this.parent.top + this.top + 1) + 'px';
}
else { // orientation 'bottom'
box.style.top = '';
box.style.bottom = (this.top || 0) + 'px';
dot.style.left = props.dot.left + 'px';
dot.style.top = props.dot.top + 'px';
line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px';
line.style.bottom = '0';
line.style.height = '';
}
dot.style.top = (-this.props.dot.height / 2) + 'px';
};

+ 118
- 166
src/timeline/component/item/ItemPoint.js View File

@ -1,14 +1,13 @@
/**
* @constructor ItemPoint
* @extends Item
* @param {ItemSet} parent
* @param {Object} data Object containing parameters start
* content, className.
* @param {Object} [options] Options to set initial property values
* @param {Object} [defaultOptions] default options
* // TODO: describe available options
*/
function ItemPoint (parent, data, options, defaultOptions) {
function ItemPoint (data, options, defaultOptions) {
this.props = {
dot: {
top: 0,
@ -21,219 +20,172 @@ function ItemPoint (parent, data, options, defaultOptions) {
}
};
Item.call(this, parent, data, options, defaultOptions);
// validate data
if (data) {
if (data.start == undefined) {
throw new Error('Property "start" missing in item ' + data);
}
}
Item.call(this, data, options, defaultOptions);
}
ItemPoint.prototype = new Item (null, null);
ItemPoint.prototype = new Item (null);
/**
* Check whether this item is visible inside given range
* @returns {{start: Number, end: Number}} range with a timestamp for start and end
* @returns {boolean} True if visible
*/
ItemPoint.prototype.isVisible = function isVisible (range) {
// determine visibility
// TODO: account for the real width of the item. Right now we just add 1/4 to the window
var interval = (range.end - range.start) / 4;
return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
};
/**
* Repaint the item
* @return {Boolean} changed
*/
ItemPoint.prototype.repaint = function repaint() {
// TODO: make an efficient repaint
var changed = false;
var dom = this.dom;
if (!dom) {
this._create();
// create DOM
this.dom = {};
dom = this.dom;
changed = true;
// background box
dom.point = document.createElement('div');
// className is updated in repaint()
// contents box, right from the dot
dom.content = document.createElement('div');
dom.content.className = 'content';
dom.point.appendChild(dom.content);
// dot at start
dom.dot = document.createElement('div');
dom.point.appendChild(dom.dot);
// attach this item as attribute
dom.point['timeline-item'] = this;
}
if (dom) {
if (!this.parent) {
throw new Error('Cannot repaint item: no parent attached');
}
// append DOM to parent DOM
if (!this.parent) {
throw new Error('Cannot repaint item: no parent attached');
}
if (!dom.point.parentNode) {
var foreground = this.parent.getForeground();
if (!foreground) {
throw new Error('Cannot repaint time axis: ' +
'parent has no foreground container element');
throw new Error('Cannot repaint time axis: parent has no foreground container element');
}
if (!dom.point.parentNode) {
foreground.appendChild(dom.point);
foreground.appendChild(dom.point);
changed = true;
foreground.appendChild(dom.point);
}
this.displayed = true;
// update contents
if (this.data.content != this.content) {
this.content = this.data.content;
if (this.content instanceof Element) {
dom.content.innerHTML = '';
dom.content.appendChild(this.content);
}
// update contents
if (this.data.content != this.content) {
this.content = this.data.content;
if (this.content instanceof Element) {
dom.content.innerHTML = '';
dom.content.appendChild(this.content);
}
else if (this.data.content != undefined) {
dom.content.innerHTML = this.content;
}
else {
throw new Error('Property "content" missing in item ' + this.data.id);
}
changed = true;
else if (this.data.content != undefined) {
dom.content.innerHTML = this.content;
}
else {
throw new Error('Property "content" missing in item ' + this.data.id);
}
this._repaintDeleteButton(dom.point);
this.dirty = true;
}
// update class
var className = (this.data.className? ' ' + this.data.className : '') +
(this.selected ? ' selected' : '');
if (this.className != className) {
this.className = className;
dom.point.className = 'item point' + className;
changed = true;
}
// update class
var className = (this.data.className? ' ' + this.data.className : '') +
(this.selected ? ' selected' : '');
if (this.className != className) {
this.className = className;
dom.point.className = 'item point' + className;
dom.dot.className = 'item dot' + className;
this.dirty = true;
}
// recalculate size
if (this.dirty) {
this.width = dom.point.offsetWidth;
this.height = dom.point.offsetHeight;
this.props.dot.width = dom.dot.offsetWidth;
this.props.dot.height = dom.dot.offsetHeight;
this.props.content.height = dom.content.offsetHeight;
// resize contents
dom.content.style.marginLeft = 2 * this.props.dot.width + 'px';
//dom.content.style.marginRight = ... + 'px'; // TODO: margin right
dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
dom.dot.style.left = (this.props.dot.width / 2) + 'px';
this.dirty = false;
}
return changed;
this._repaintDeleteButton(dom.point);
};
/**
* Show the item in the DOM (when not already visible). The items DOM will
* be created when needed.
* @return {Boolean} changed
*/
ItemPoint.prototype.show = function show() {
if (!this.dom || !this.dom.point.parentNode) {
return this.repaint();
}
else {
return false;
if (!this.displayed) {
this.repaint();
}
};
/**
* Hide the item from the DOM (when visible)
* @return {Boolean} changed
*/
ItemPoint.prototype.hide = function hide() {
var changed = false,
dom = this.dom;
if (dom) {
if (dom.point.parentNode) {
dom.point.parentNode.removeChild(dom.point);
changed = true;
if (this.displayed) {
if (this.dom.point.parentNode) {
this.dom.point.parentNode.removeChild(this.dom.point);
}
}
return changed;
};
/**
* Reflow the item: calculate its actual size from the DOM
* @return {boolean} resized returns true if the axis is resized
* @override
*/
ItemPoint.prototype.reflow = function reflow() {
var changed = 0,
update,
dom,
props,
options,
margin,
orientation,
start,
top,
data,
range;
if (this.data.start == undefined) {
throw new Error('Property "start" missing in item ' + this.data.id);
}
this.top = null;
this.left = null;
data = this.data;
range = this.parent && this.parent.range;
if (data && range) {
// TODO: account for the width of the item
var interval = (range.end - range.start);
this.visible = (data.start > range.start - interval) && (data.start < range.end);
}
else {
this.visible = false;
this.displayed = false;
}
if (this.visible) {
dom = this.dom;
if (dom) {
update = util.updateProperty;
props = this.props;
options = this.options;
orientation = options.orientation || this.defaultOptions.orientation;
margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
start = this.parent.toScreen(this.data.start) + this.offset;
changed += update(this, 'width', dom.point.offsetWidth);
changed += update(this, 'height', dom.point.offsetHeight);
changed += update(props.dot, 'width', dom.dot.offsetWidth);
changed += update(props.dot, 'height', dom.dot.offsetHeight);
changed += update(props.content, 'height', dom.content.offsetHeight);
if (orientation == 'top') {
top = margin;
}
else {
// default or 'bottom'
var parentHeight = this.parent.height;
top = Math.max(parentHeight - this.height - margin, 0);
}
changed += update(this, 'top', top);
changed += update(this, 'left', start - props.dot.width / 2);
changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
//changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
}
else {
changed += 1;
}
}
return (changed > 0);
};
/**
* Create an items DOM
* @private
* Reposition the item horizontally
* @Override
*/
ItemPoint.prototype._create = function _create() {
var dom = this.dom;
if (!dom) {
this.dom = dom = {};
ItemPoint.prototype.repositionX = function repositionX() {
var start = this.defaultOptions.toScreen(this.data.start);
// background box
dom.point = document.createElement('div');
// className is updated in repaint()
// contents box, right from the dot
dom.content = document.createElement('div');
dom.content.className = 'content';
dom.point.appendChild(dom.content);
this.left = start - this.props.dot.width;
// dot at start
dom.dot = document.createElement('div');
dom.dot.className = 'dot';
dom.point.appendChild(dom.dot);
// attach this item as attribute
dom.point['timeline-item'] = this;
}
// reposition point
this.dom.point.style.left = this.left + 'px';
};
/**
* Reposition the item, recalculate its left, top, and width, using the current
* range and size of the items itemset
* @override
* Reposition the item vertically
* @Override
*/
ItemPoint.prototype.reposition = function reposition() {
var dom = this.dom,
props = this.props;
ItemPoint.prototype.repositionY = function repositionY () {
var orientation = this.options.orientation || this.defaultOptions.orientation,
point = this.dom.point;
if (dom) {
dom.point.style.top = this.top + 'px';
dom.point.style.left = this.left + 'px';
dom.content.style.marginLeft = props.content.marginLeft + 'px';
//dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
dom.dot.style.top = props.dot.top + 'px';
if (orientation == 'top') {
point.style.top = this.top + 'px';
point.style.bottom = '';
}
else {
point.style.top = '';
point.style.bottom = this.top + 'px';
}
};

+ 135
- 187
src/timeline/component/item/ItemRange.js View File

@ -1,100 +1,129 @@
/**
* @constructor ItemRange
* @extends Item
* @param {ItemSet} parent
* @param {Object} data Object containing parameters start, end
* content, className.
* @param {Object} [options] Options to set initial property values
* @param {Object} [defaultOptions] default options
* // TODO: describe available options
*/
function ItemRange (parent, data, options, defaultOptions) {
function ItemRange (data, options, defaultOptions) {
this.props = {
content: {
left: 0,
width: 0
}
};
Item.call(this, parent, data, options, defaultOptions);
// validate data
if (data) {
if (data.start == undefined) {
throw new Error('Property "start" missing in item ' + data.id);
}
if (data.end == undefined) {
throw new Error('Property "end" missing in item ' + data.id);
}
}
Item.call(this, data, options, defaultOptions);
}
ItemRange.prototype = new Item (null, null);
ItemRange.prototype = new Item (null);
ItemRange.prototype.baseClassName = 'item range';
/**
* Check whether this item is visible inside given range
* @returns {{start: Number, end: Number}} range with a timestamp for start and end
* @returns {boolean} True if visible
*/
ItemRange.prototype.isVisible = function isVisible (range) {
// determine visibility
return (this.data.start < range.end) && (this.data.end > range.start);
};
/**
* Repaint the item
* @return {Boolean} changed
*/
ItemRange.prototype.repaint = function repaint() {
// TODO: make an efficient repaint
var changed = false;
var dom = this.dom;
if (!dom) {
this._create();
// create DOM
this.dom = {};
dom = this.dom;
changed = true;
// background box
dom.box = document.createElement('div');
// className is updated in repaint()
// contents box
dom.content = document.createElement('div');
dom.content.className = 'content';
dom.box.appendChild(dom.content);
// attach this item as attribute
dom.box['timeline-item'] = this;
}
if (dom) {
if (!this.parent) {
throw new Error('Cannot repaint item: no parent attached');
}
// append DOM to parent DOM
if (!this.parent) {
throw new Error('Cannot repaint item: no parent attached');
}
if (!dom.box.parentNode) {
var foreground = this.parent.getForeground();
if (!foreground) {
throw new Error('Cannot repaint time axis: ' +
'parent has no foreground container element');
throw new Error('Cannot repaint time axis: parent has no foreground container element');
}
if (!dom.box.parentNode) {
foreground.appendChild(dom.box);
changed = true;
foreground.appendChild(dom.box);
}
this.displayed = true;
// update contents
if (this.data.content != this.content) {
this.content = this.data.content;
if (this.content instanceof Element) {
dom.content.innerHTML = '';
dom.content.appendChild(this.content);
}
// update content
if (this.data.content != this.content) {
this.content = this.data.content;
if (this.content instanceof Element) {
dom.content.innerHTML = '';
dom.content.appendChild(this.content);
}
else if (this.data.content != undefined) {
dom.content.innerHTML = this.content;
}
else {
throw new Error('Property "content" missing in item ' + this.data.id);
}
changed = true;
else if (this.data.content != undefined) {
dom.content.innerHTML = this.content;
}
this._repaintDeleteButton(dom.box);
this._repaintDragLeft();
this._repaintDragRight();
// update class
var className = (this.data.className ? (' ' + this.data.className) : '') +
(this.selected ? ' selected' : '');
if (this.className != className) {
this.className = className;
dom.box.className = 'item range' + className;
changed = true;
else {
throw new Error('Property "content" missing in item ' + this.data.id);
}
this.dirty = true;
}
// update class
var className = (this.data.className ? (' ' + this.data.className) : '') +
(this.selected ? ' selected' : '');
if (this.className != className) {
this.className = className;
dom.box.className = this.baseClassName + className;
this.dirty = true;
}
return changed;
// recalculate size
if (this.dirty) {
this.props.content.width = this.dom.content.offsetWidth;
this.height = this.dom.box.offsetHeight;
this.dirty = false;
}
this._repaintDeleteButton(dom.box);
this._repaintDragLeft();
this._repaintDragRight();
};
/**
* Show the item in the DOM (when not already visible). The items DOM will
* be created when needed.
* @return {Boolean} changed
*/
ItemRange.prototype.show = function show() {
if (!this.dom || !this.dom.box.parentNode) {
return this.repaint();
}
else {
return false;
if (!this.displayed) {
this.repaint();
}
};
@ -103,163 +132,82 @@ ItemRange.prototype.show = function show() {
* @return {Boolean} changed
*/
ItemRange.prototype.hide = function hide() {
var changed = false,
dom = this.dom;
if (dom) {
if (dom.box.parentNode) {
dom.box.parentNode.removeChild(dom.box);
changed = true;
if (this.displayed) {
var box = this.dom.box;
if (box.parentNode) {
box.parentNode.removeChild(box);
}
this.top = null;
this.left = null;
this.displayed = false;
}
return changed;
};
/**
* Reflow the item: calculate its actual size from the DOM
* @return {boolean} resized returns true if the axis is resized
* @override
* Reposition the item horizontally
* @Override
*/
ItemRange.prototype.reflow = function reflow() {
var changed = 0,
dom,
props,
options,
margin,
padding,
parent,
start,
end,
data,
range,
update,
box,
parentWidth,
contentLeft,
orientation,
top;
if (this.data.start == undefined) {
throw new Error('Property "start" missing in item ' + this.data.id);
ItemRange.prototype.repositionX = function repositionX() {
var props = this.props,
parentWidth = this.parent.width,
start = this.defaultOptions.toScreen(this.data.start),
end = this.defaultOptions.toScreen(this.data.end),
padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
contentLeft;
// limit the width of the this, as browsers cannot draw very wide divs
if (start < -parentWidth) {
start = -parentWidth;
}
if (this.data.end == undefined) {
throw new Error('Property "end" missing in item ' + this.data.id);
if (end > 2 * parentWidth) {
end = 2 * parentWidth;
}
data = this.data;
range = this.parent && this.parent.range;
if (data && range) {
// TODO: account for the width of the item. Take some margin
this.visible = (data.start < range.end) && (data.end > range.start);
// 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 {
this.visible = false;
contentLeft = 0;
}
if (this.visible) {
dom = this.dom;
if (dom) {
props = this.props;
options = this.options;
parent = this.parent;
start = parent.toScreen(this.data.start) + this.offset;
end = parent.toScreen(this.data.end) + this.offset;
update = util.updateProperty;
box = dom.box;
parentWidth = parent.width;
orientation = options.orientation || this.defaultOptions.orientation;
margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
padding = options.padding || this.defaultOptions.padding;
changed += update(props.content, 'width', dom.content.offsetWidth);
changed += update(this, 'height', box.offsetHeight);
// 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
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;
}
changed += update(props.content, 'left', contentLeft);
if (orientation == 'top') {
top = margin;
changed += update(this, 'top', top);
}
else {
// default or 'bottom'
top = parent.height - this.height - margin;
changed += update(this, 'top', top);
}
changed += update(this, 'left', start);
changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
}
else {
changed += 1;
}
}
this.left = start;
this.width = Math.max(end - start, 1);
return (changed > 0);
this.dom.box.style.left = this.left + 'px';
this.dom.box.style.width = this.width + 'px';
this.dom.content.style.left = contentLeft + 'px';
};
/**
* Create an items DOM
* @private
* Reposition the item vertically
* @Override
*/
ItemRange.prototype._create = function _create() {
var dom = this.dom;
if (!dom) {
this.dom = dom = {};
// background box
dom.box = document.createElement('div');
// className is updated in repaint()
// contents box
dom.content = document.createElement('div');
dom.content.className = 'content';
dom.box.appendChild(dom.content);
ItemRange.prototype.repositionY = function repositionY() {
var orientation = this.options.orientation || this.defaultOptions.orientation,
box = this.dom.box;
// attach this item as attribute
dom.box['timeline-item'] = this;
if (orientation == 'top') {
box.style.top = this.top + 'px';
box.style.bottom = '';
}
};
/**
* Reposition the item, recalculate its left, top, and width, using the current
* range and size of the items itemset
* @override
*/
ItemRange.prototype.reposition = function reposition() {
var dom = this.dom,
props = this.props;
if (dom) {
dom.box.style.top = this.top + 'px';
dom.box.style.left = this.left + 'px';
dom.box.style.width = this.width + 'px';
dom.content.style.left = props.content.left + 'px';
else {
box.style.top = '';
box.style.bottom = this.top + 'px';
}
};
/**
* Repaint a drag area on the left side of the range when the range is selected
* @private
* @protected
*/
ItemRange.prototype._repaintDragLeft = function () {
if (this.selected && this.options.editable && !this.dom.dragLeft) {
if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
// create and show drag area
var dragLeft = document.createElement('div');
dragLeft.className = 'drag-left';
@ -286,10 +234,10 @@ ItemRange.prototype._repaintDragLeft = function () {
/**
* Repaint a drag area on the right side of the range when the range is selected
* @private
* @protected
*/
ItemRange.prototype._repaintDragRight = function () {
if (this.selected && this.options.editable && !this.dom.dragRight) {
if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
// create and show drag area
var dragRight = document.createElement('div');
dragRight.className = 'drag-right';

+ 29
- 92
src/timeline/component/item/ItemRangeOverflow.js View File

@ -1,14 +1,13 @@
/**
* @constructor ItemRangeOverflow
* @extends ItemRange
* @param {ItemSet} parent
* @param {Object} data Object containing parameters start, end
* content, className.
* @param {Object} [options] Options to set initial property values
* @param {Object} [defaultOptions] default options
* // TODO: describe available options
*/
function ItemRangeOverflow (parent, data, options, defaultOptions) {
function ItemRangeOverflow (data, options, defaultOptions) {
this.props = {
content: {
left: 0,
@ -16,104 +15,42 @@ function ItemRangeOverflow (parent, data, options, defaultOptions) {
}
};
// define a private property _width, which is the with of the range box
// adhering to the ranges start and end date. The property width has a
// getter which returns the max of border width and content width
this._width = 0;
Object.defineProperty(this, 'width', {
get: function () {
return (this.props.content && this._width < this.props.content.width) ?
this.props.content.width :
this._width;
},
set: function (width) {
this._width = width;
}
});
ItemRange.call(this, parent, data, options, defaultOptions);
ItemRange.call(this, data, options, defaultOptions);
}
ItemRangeOverflow.prototype = new ItemRange (null, null);
ItemRangeOverflow.prototype = new ItemRange (null);
ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow';
/**
* Repaint the item
* @return {Boolean} changed
* Reposition the item horizontally
* @Override
*/
ItemRangeOverflow.prototype.repaint = function repaint() {
// TODO: make an efficient repaint
var changed = false;
var dom = this.dom;
if (!dom) {
this._create();
dom = this.dom;
changed = true;
ItemRangeOverflow.prototype.repositionX = function repositionX() {
var parentWidth = this.parent.width,
start = this.defaultOptions.toScreen(this.data.start),
end = this.defaultOptions.toScreen(this.data.end),
padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
contentLeft;
// limit the width of the this, as browsers cannot draw very wide divs
if (start < -parentWidth) {
start = -parentWidth;
}
if (dom) {
if (!this.parent) {
throw new Error('Cannot repaint item: no parent attached');
}
var foreground = this.parent.getForeground();
if (!foreground) {
throw new Error('Cannot repaint time axis: ' +
'parent has no foreground container element');
}
if (!dom.box.parentNode) {
foreground.appendChild(dom.box);
changed = true;
}
// update content
if (this.data.content != this.content) {
this.content = this.data.content;
if (this.content instanceof Element) {
dom.content.innerHTML = '';
dom.content.appendChild(this.content);
}
else if (this.data.content != undefined) {
dom.content.innerHTML = this.content;
}
else {
throw new Error('Property "content" missing in item ' + this.id);
}
changed = true;
}
this._repaintDeleteButton(dom.box);
this._repaintDragLeft();
this._repaintDragRight();
// update class
var className = (this.data.className? ' ' + this.data.className : '') +
(this.selected ? ' selected' : '');
if (this.className != className) {
this.className = className;
dom.box.className = 'item rangeoverflow' + className;
changed = true;
}
if (end > 2 * parentWidth) {
end = 2 * parentWidth;
}
return changed;
};
// when range exceeds left of the window, position the contents at the left of the visible area
contentLeft = Math.max(-start, 0);
/**
* Reposition the item, recalculate its left, top, and width, using the current
* range and size of the items itemset
* @override
*/
ItemRangeOverflow.prototype.reposition = function reposition() {
var dom = this.dom,
props = this.props;
this.left = start;
var boxWidth = Math.max(end - start, 1);
this.width = (this.props.content.width < boxWidth) ?
boxWidth :
start + contentLeft + this.props.content.width;
if (dom) {
dom.box.style.top = this.top + 'px';
dom.box.style.left = this.left + 'px';
dom.box.style.width = this._width + 'px';
dom.content.style.left = props.content.left + 'px';
}
this.dom.box.style.left = this.left + 'px';
this.dom.box.style.width = boxWidth + 'px';
this.dom.content.style.left = contentLeft + 'px';
};

+ 112
- 0
src/timeline/stack.js View File

@ -0,0 +1,112 @@
/**
* Utility functions for ordering and stacking of items
*/
var stack = {};
/**
* Order items by their start data
* @param {Item[]} items
*/
stack.orderByStart = function orderByStart(items) {
items.sort(function (a, b) {
return a.data.start - b.data.start;
});
};
/**
* Order items by their end date. If they have no end date, their start date
* is used.
* @param {Item[]} items
*/
stack.orderByEnd = function orderByEnd(items) {
items.sort(function (a, b) {
var aTime = ('end' in a.data) ? a.data.end : a.data.start,
bTime = ('end' in b.data) ? b.data.end : b.data.start;
return aTime - bTime;
});
};
/**
* Adjust vertical positions of the items such that they don't overlap each
* other.
* @param {Item[]} items
* All visible items
* @param {{item: number, axis: number}} margin
* Margins between items and between items and the axis.
* @param {boolean} [force=false]
* If true, all items will be repositioned. If false (default), only
* items having a top===null will be re-stacked
*/
stack.stack = function _stack (items, margin, force) {
var i, iMax;
if (force) {
// reset top position of all items
for (i = 0, iMax = items.length; i < iMax; i++) {
items[i].top = null;
}
}
// calculate new, non-overlapping positions
for (i = 0, iMax = items.length; i < iMax; i++) {
var item = items[i];
if (item.top === null) {
// initialize top position
item.top = margin.axis;
do {
// TODO: optimize checking for overlap. when there is a gap without items,
// you only need to check for items from the next item on, not from zero
var collidingItem = null;
for (var j = 0, jj = items.length; j < jj; j++) {
var other = items[j];
if (other.top !== null && other !== item && stack.collision(item, other, margin.item)) {
collidingItem = other;
break;
}
}
if (collidingItem != null) {
// There is a collision. Reposition the items above the colliding element
item.top = collidingItem.top + collidingItem.height + margin.item;
}
} while (collidingItem);
}
}
};
/**
* Adjust vertical positions of the items without stacking them
* @param {Item[]} items
* All visible items
* @param {{item: number, axis: number}} margin
* Margins between items and between items and the axis.
*/
stack.nostack = function nostack (items, margin) {
var i, iMax;
// reset top position of all items
for (i = 0, iMax = items.length; i < iMax; i++) {
items[i].top = margin.axis;
}
};
/**
* Test if the two provided items collide
* The items must have parameters left, width, top, and height.
* @param {Item} a The first item
* @param {Item} b The second item
* @param {Number} margin A minimum required margin.
* If margin is provided, the two items will be
* marked colliding when they overlap or
* when the margin between the two is smaller than
* the requested margin.
* @return {boolean} true if a and b collide, else false
*/
stack.collision = function collision (a, b, margin) {
return ((a.left - margin) < (b.left + b.width) &&
(a.left + a.width + margin) > b.left &&
(a.top - margin) < (b.top + b.height) &&
(a.top + a.height + margin) > b.top);
};

+ 42
- 3
src/util.js View File

@ -97,6 +97,23 @@ util.extend = function (a, b) {
return a;
};
/**
* Test whether all elements in two arrays are equal.
* @param {Array} a
* @param {Array} b
* @return {boolean} Returns true if both arrays have the same length and same
* elements.
*/
util.equalArray = function (a, b) {
if (a.length != b.length) return false;
for (var i = 1, len = a.length; i < len; i++) {
if (a[i] != b[i]) return false;
}
return true;
};
/**
* Convert an object to another type
* @param {Boolean | Number | String | Date | Moment | Null | undefined} object
@ -440,6 +457,22 @@ util.forEach = function forEach (object, callback) {
}
};
/**
* Convert an object into an array: all objects properties are put into the
* array. The resulting array is unordered.
* @param {Object} object
* @param {Array} array
*/
util.toArray = function toArray(object) {
var array = [];
for (var prop in object) {
if (object.hasOwnProperty(prop)) array.push(object[prop]);
}
return array;
}
/**
* Update a property in an object
* @param {Object} object
@ -447,7 +480,7 @@ util.forEach = function forEach (object, callback) {
* @param {*} value
* @return {Boolean} changed
*/
util.updateProperty = function updateProp (object, key, value) {
util.updateProperty = function updateProperty (object, key, value) {
if (object[key] !== value) {
object[key] = value;
return true;
@ -655,6 +688,8 @@ util.option.asElement = function (value, defaultValue) {
util.GiveDec = function GiveDec(Hex) {
var Value;
if (Hex == "A")
Value = 10;
else if (Hex == "B")
@ -668,12 +703,15 @@ util.GiveDec = function GiveDec(Hex) {
else if (Hex == "F")
Value = 15;
else
Value = eval(Hex)
Value = eval(Hex);
return Value;
};
util.GiveHex = function GiveHex(Dec) {
if (Dec == 10)
var Value;
if(Dec == 10)
Value = "A";
else if (Dec == 11)
Value = "B";
@ -687,6 +725,7 @@ util.GiveHex = function GiveHex(Dec) {
Value = "F";
else
Value = "" + Dec;
return Value;
};

+ 15
- 0
test/dataset.js View File

@ -162,5 +162,20 @@ 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);
// create a dataset with initial data
var data = new DataSet([
{id: 1, content: 'Item 1', start: new Date(now.valueOf())},
{id: 2, content: 'Item 2', start: now.toISOString()}
]);
assert.deepEqual(data.getIds(), [1, 2]);
// create a dataset with initial data and options
var data = new DataSet([
{_id: 1, content: 'Item 1', start: new Date(now.valueOf())},
{_id: 2, content: 'Item 2', start: now.toISOString()}
], {fieldId: '_id'});
assert.deepEqual(data.getIds(), [1, 2]);
// TODO: extensively test DataSet
// TODO: test subscribing to events

+ 41
- 4
test/timeline.html View File

@ -35,11 +35,26 @@
});
};
</script>
<div>
<label for="currenttime"><input id="currenttime" type="checkbox" checked="true"> Show current time</label>
</div>
<script>
var currenttime = document.getElementById('currenttime');
currenttime.onchange = function () {
timeline.setOptions({
showCurrentTime: currenttime.checked
});
};
</script>
<br>
<div id="visualization"></div>
<script>
console.time('create dataset');
// create a dataset with items
var now = moment().minutes(0).seconds(0).milliseconds(0);
var items = new vis.DataSet({
@ -63,20 +78,42 @@
var container = document.getElementById('visualization');
var options = {
editable: true,
//orientation: 'top',
start: now.clone().add('days', -7),
end: now.clone().add('days', 7),
//maxHeight: 200,
height: 200,
//start: moment('2013-01-01'),
//end: moment('2013-12-31'),
//height: 200,
showCurrentTime: true,
showCustomTime: true,
//min: moment('2013-01-01'),
//max: moment('2013-12-31'),
zoomMin: 1000 * 60 * 60 * 24, // 1 day
//zoomMin: 1000 * 60 * 60 * 24, // 1 day
zoomMax: 1000 * 60 * 60 * 24 * 30 * 6 // 6 months
};
console.timeEnd('create dataset');
console.time('create timeline');
var timeline = new vis.Timeline(container, items, options);
console.timeEnd('create timeline');
timeline.on('select', function (selection) {
console.log('select', selection);
});
/*
timeline.on('rangechange', function (range) {
console.log('rangechange', range);
});
timeline.on('rangechanged', function (range) {
console.log('rangechanged', range);
});
*/
items.on('add', console.log.bind(console));
items.on('update', console.log.bind(console));
items.on('remove', console.log.bind(console));
</script>
</body>

+ 40
- 2
test/timeline_groups.html View File

@ -12,7 +12,6 @@
#visualization {
box-sizing: border-box;
width: 100%;
height: 300px;
}
</style>
@ -49,7 +48,7 @@
var itemCount = 20;
// create a data set with groups
var names = ['John', 'Alston', 'Lee', 'Grant'];
var names = ['John (0)', 'Alston (1)', 'Lee (2)', 'Grant (3)'];
var groups = new vis.DataSet();
for (var g = 0; g < groupCount; g++) {
groups.add({id: g, content: names[g]});
@ -73,14 +72,53 @@
// create visualization
var container = document.getElementById('visualization');
var options = {
editable: {
add: true,
remove: true,
updateTime: true,
updateGroup: true
},
//stack: false,
//height: 200,
groupOrder: 'content'
};
console.time('create timeline');
var timeline = new vis.Timeline(container);
console.timeEnd('create timeline');
console.time('set options');
timeline.setOptions(options);
console.timeEnd('set options');
console.time('set groups');
timeline.setGroups(groups);
console.timeEnd('set groups');
console.time('set items');
timeline.setItems(items);
console.timeEnd('set items');
timeline.on('select', function (selection) {
console.log('select', selection);
});
/*
timeline.on('rangechange', function (range) {
console.log('rangechange', range);
});
timeline.on('rangechanged', function (range) {
console.log('rangechanged', range);
});
*/
items.on('add', console.log.bind(console));
items.on('update', console.log.bind(console));
items.on('remove', console.log.bind(console));
groups.on('add', console.log.bind(console));
groups.on('update', console.log.bind(console));
groups.on('remove', console.log.bind(console));
</script>
</body>

Loading…
Cancel
Save