Browse Source

Merge branch 'develop' into graph2d

Conflicts:
	dist/vis.css
	dist/vis.js
	dist/vis.min.css
	dist/vis.min.js
	src/DataSet.js
	src/timeline/Timeline.js
css_transitions
Alex de Mulder 10 years ago
parent
commit
4a2f6136e1
50 changed files with 1270 additions and 380 deletions
  1. +34
    -1
      HISTORY.md
  2. +1
    -0
      Jakefile.js
  3. +1
    -1
      bower.json
  4. +5
    -5
      docs/dataset.html
  5. +38
    -6
      docs/graph.html
  6. +9
    -2
      docs/index.html
  7. +41
    -7
      docs/timeline.html
  8. +9
    -2
      examples/timeline/01_basic.html
  9. +6
    -4
      examples/timeline/02_interactive.html
  10. +1
    -4
      examples/timeline/03_a_lot_of_data.html
  11. +2
    -2
      examples/timeline/04_html_data.html
  12. +1
    -7
      examples/timeline/06_event_listeners.html
  13. +1
    -1
      examples/timeline/07_custom_time_bar.html
  14. +2
    -2
      examples/timeline/10_limit_move_and_zoom.html
  15. +2
    -2
      examples/timeline/11_points.html
  16. +2
    -2
      examples/timeline/12_custom_styling.html
  17. +2
    -2
      examples/timeline/15_item_class_names.html
  18. +2
    -2
      examples/timeline/16_navigation_menu.html
  19. +120
    -0
      examples/timeline/17_data_serialization.html
  20. +53
    -0
      examples/timeline/18_range_overflow.html
  21. +2
    -0
      examples/timeline/index.html
  22. +2
    -2
      examples/timeline/requirejs/scripts/main.js
  23. +1
    -1
      package.json
  24. +9
    -19
      src/DataSet.js
  25. +202
    -35
      src/graph/Edge.js
  26. +26
    -2
      src/graph/Graph.js
  27. +1
    -2
      src/graph/Node.js
  28. +143
    -0
      src/graph/graphMixins/ManipulationMixin.js
  29. +78
    -4
      src/graph/graphMixins/SelectionMixin.js
  30. +4
    -0
      src/graph3d/Graph3d.js
  31. +45
    -40
      src/timeline/Range.js
  32. +206
    -30
      src/timeline/Timeline.js
  33. +7
    -0
      src/timeline/component/Component.js
  34. +11
    -1
      src/timeline/component/CurrentTime.js
  35. +13
    -0
      src/timeline/component/CustomTime.js
  36. +16
    -3
      src/timeline/component/Group.js
  37. +40
    -23
      src/timeline/component/ItemSet.js
  38. +17
    -16
      src/timeline/component/TimeAxis.js
  39. +33
    -0
      src/timeline/component/css/animation.css
  40. +4
    -18
      src/timeline/component/css/item.css
  41. +2
    -23
      src/timeline/component/css/itemset.css
  42. +20
    -0
      src/timeline/component/css/panel.css
  43. +7
    -8
      src/timeline/component/item/ItemBox.js
  44. +1
    -3
      src/timeline/component/item/ItemPoint.js
  45. +30
    -14
      src/timeline/component/item/ItemRange.js
  46. +0
    -57
      src/timeline/component/item/ItemRangeOverflow.js
  47. +2
    -3
      src/util.js
  48. +5
    -14
      test/dataset.js
  49. +7
    -7
      test/timeline.html
  50. +4
    -3
      test/timeline_groups.html

+ 34
- 1
HISTORY.md View File

@ -2,17 +2,50 @@
http://visjs.org
## 2014-06-06, version 1.1.1
## not yet released, version 2.1.0
### Timeline
- Fixed auto detected item type being preferred over the global item `type`.
- Throws an error when constructing without new keyword.
- Removed the 'rangeoverflow' item type. Instead, one can use a regular range
and change css styling of the item contents to:
.vis.timeline .item.range .content {
overflow: visible;
}
### Graph
- Throws an error when constructing without new keyword.
### Graph3d
- Throws an error when constructing without new keyword.
## 2014-06-19, version 2.0.0
### Timeline
- Implemented function `destroy` to neatly cleanup a Timeline.
- Implemented support for dragging the timeline contents vertically.
- Implemented options `zoomable` and `moveable`.
- Changed default value of option `showCurrentTime` to true.
- Internal refactoring and simplification of the code.
- Fixed property `className` of groups not being applied to related contents and
background elements, and not being updated once applied.
### Graph
- Reduced the timestep a little for smoother animations.
- Fixed dataManipulation.initiallyVisible functionality (thanks theGrue).
- Forced typecast of fontSize to Number.
- Added editing of edges using the data manipulation toolkit.
### DataSet
- Renamed option `convert` to `type`.
## 2014-06-06, version 1.1.0

+ 1
- 0
Jakefile.js View File

@ -44,6 +44,7 @@ task('build', {async: true}, function () {
'./src/timeline/component/css/timeaxis.css',
'./src/timeline/component/css/currenttime.css',
'./src/timeline/component/css/customtime.css',
'./src/timeline/component/css/animation.css',
'./src/timeline/component/css/dataaxis.css',
'./src/timeline/component/css/pathStyles.css',

+ 1
- 1
bower.json View File

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

+ 5
- 5
docs/dataset.html View File

@ -91,7 +91,7 @@ console.log('filtered items', items);
// retrieve formatted items
var items = data.get({
fields: ['id', 'date'],
convert: {
type: {
date: 'ISODate'
}
});
@ -149,7 +149,7 @@ var data = new vis.DataSet([data] [, options])
</td>
</tr>
<tr>
<td>convert</td>
<td>type</td>
<td>Object.&lt;String,&nbsp;String&gt;</td>
<td>none</td>
<td>
@ -227,7 +227,7 @@ var data = new vis.DataSet([data] [, options])
<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>.
Available <code>options</code> are described in section <a href="#Data_Selection">Data Selection</a>, except that options <code>fields</code> and <code>type</code> are not applicable in case of <code>getIds</code>.
</td>
</tr>
@ -649,7 +649,7 @@ DataSet.map(callback [, options]);
</tr>
<tr>
<td>convert</td>
<td>type</td>
<td>Object.&lt;String,&nbsp;String&gt;</td>
<td>
An object containing field names as key, and data types as value.
@ -700,7 +700,7 @@ data.add([
// retrieve formatted items
var items = data.get({
fields: ['id', 'date', 'group'], // output the specified fields only
convert: {
type: {
date: 'Date', // convert the date fields to Date objects
group: 'String' // convert the group fields to Strings
}

+ 38
- 6
docs/graph.html View File

@ -1593,6 +1593,16 @@ var options: {
// all fields normally accepted by a node can be used.
callback(newData); // call the callback with the new data to edit the node.
}
onEditEdge: function(data,callback) {
/** data = {id: edgeID,
* from: nodeId1,
* to: nodeId2,
* };
*/
var newData = {..}; // alter the data as you want, except for the ID.
// all fields normally accepted by an edge can be used.
callback(newData); // call the callback with the new data to edit the edge.
}
onConnect: function(data,callback) {
// data = {from: nodeId1, to: nodeId2};
var newData = {..}; // check or alter data as you see fit.
@ -1951,10 +1961,12 @@ var options: {
link:"Add Link",
del:"Delete selected",
editNode:"Edit Node",
editEdge:"Edit Edge",
back:"Back",
addDescription:"Click in an empty space to place a new node.",
linkDescription:"Click on a node and drag the edge to another
node to connect them.",
editEdgeDescription:"Click on either one of the control points and drag them to another node to connect to it.".
addError:"The function for add does not support two arguments
(data,callback).",
linkError:"The function for connect does not support two arguments
@ -2137,16 +2149,36 @@ var options: {
</td>
</tr>
<tr>
<td>selectNodes(selection, [highlightEdges])</td>
<td>none</td>
<td>Select nodes.
<code>selection</code> is an array with ids of nodes to be selected.
The array <code>selection</code> can contain zero or multiple ids.
Example usage: <code>graph.selectNodes([3, 5]);</code> will select
nodes with id 3 and 5. The highlisghEdges boolean can be used to automatically select the edges connected to the node.
</td>
</tr>
<tr>
<td>selectEdges(selection)</td>
<td>none</td>
<td>Select Edges.
<code>selection</code> is an array with ids of edges to be selected.
The array <code>selection</code> can contain zero or multiple ids.
Example usage: <code>graph.selectEdges([3, 5]);</code> will select
edges with id 3 and 5.
</td>
</tr>
<tr>
<td>setSelection(selection)</td>
<td>none</td>
<td>Select nodes.
<code>selection</code> is an array with ids of nodes to be selected.
The array <code>selection</code> can contain zero or multiple ids.
Example usage: <code>graph.setSelection([3, 5]);</code> will select
nodes with id 3 and 5.
<td>Select nodes [deprecated].
<code>selection</code> is an array with ids of nodes to be selected.
The array <code>selection</code> can contain zero or multiple ids.
Example usage: <code>graph.setSelection([3, 5]);</code> will select
nodes with id 3 and 5.
</td>
</tr>
</tr>
<tr>
<td>setSize(width, height)</td>

+ 9
- 2
docs/index.html View File

@ -162,16 +162,23 @@ var timeline = new vis.Timeline(container, data, options);
&lt;div id="visualization"&gt;&lt;/div&gt;
&lt;script type="text/javascript"&gt;
// DOM element where the Timeline will be attached
var container = document.getElementById('visualization');
var data = [
// Create a DataSet (allows two way data-binding)
var data = new vis.DataSet([
{id: 1, content: 'item 1', start: '2013-04-20'},
{id: 2, content: 'item 2', start: '2013-04-14'},
{id: 3, content: 'item 3', start: '2013-04-18'},
{id: 4, content: 'item 4', start: '2013-04-16', end: '2013-04-19'},
{id: 5, content: 'item 5', start: '2013-04-25'},
{id: 6, content: 'item 6', start: '2013-04-27'}
];
]);
// Configuration for the Timeline
var options = {};
// Create a Timeline
var timeline = new vis.Timeline(container, data, options);
&lt;/script&gt;
&lt;/body&gt;

+ 41
- 7
docs/timeline.html View File

@ -68,16 +68,23 @@
&lt;div id="visualization"&gt;&lt;/div&gt;
&lt;script type="text/javascript"&gt;
// DOM element where the Timeline will be attached
var container = document.getElementById('visualization');
var items = [
// Create a DataSet (allows two way data-binding)
var items = new vis.DataSet([
{id: 1, content: 'item 1', start: '2013-04-20'},
{id: 2, content: 'item 2', start: '2013-04-14'},
{id: 3, content: 'item 3', start: '2013-04-18'},
{id: 4, content: 'item 4', start: '2013-04-16', end: '2013-04-19'},
{id: 5, content: 'item 5', start: '2013-04-25'},
{id: 6, content: 'item 6', start: '2013-04-27'}
];
]);
// Configuration for the Timeline
var options = {};
// Create a Timeline
var timeline = new vis.Timeline(container, items, options);
&lt;/script&gt;
&lt;/body&gt;
@ -195,8 +202,8 @@ var items = [
<td>type</td>
<td>String</td>
<td>'box'</td>
<td>The type of the item. Can be 'box' (default), 'point', 'range', or 'rangeoverflow'.
Types 'box' and 'point' need a start date, and types 'range' and 'rangeoverflow' need both a start and end date. Types 'range' and rangeoverflow are equal, except that overflowing text in 'range' is hidden, while visible in 'rangeoverflow'.
<td>The type of the item. Can be 'box' (default), 'point', or 'range'.
Types 'box' and 'point' need a start date, and type 'range' needs both a start and end date.
</td>
</tr>
<tr>
@ -458,6 +465,16 @@ var options = {
<td>Specifies the minimum height for the Timeline. Can be a number in pixels or a string like "300px".</td>
</tr>
<tr>
<td>moveable</td>
<td>Boolean</td>
<td>true</td>
<td>
Specifies whether the Timeline can be moved and zoomed by dragging the window.
See also option <code>zoomable</code>.
</td>
</tr>
<tr>
<td>onAdd</td>
<td>Function</td>
@ -587,8 +604,8 @@ var options = {
<tr>
<td>type</td>
<td>String</td>
<td>'box'</td>
<td>Specifies the default type for the timeline items. Choose from 'box', 'point', 'range', and 'rangeoverflow'. Note that individual items can override this default type.
<td>none</td>
<td>Specifies the default type for the timeline items. Choose from 'box', 'point', and 'range'. Note that individual items can override this default type. If undefined, the Timeline will auto detect the type from the items data: if a start and end date is available, a 'range' will be created, and else, a 'box' is created.
</td>
</tr>
@ -599,6 +616,16 @@ var options = {
<td>The width of the timeline in pixels or as a percentage.</td>
</tr>
<tr>
<td>zoomable</td>
<td>Boolean</td>
<td>true</td>
<td>
Specifies whether the Timeline can be zoomed by pinching or scrolling in the window.
Only applicable when option <code>moveable</code> is set <code>true</code>.
</td>
</tr>
<tr>
<td>zoomMax</td>
<td>Number</td>
@ -646,6 +673,13 @@ timeline.clear({options: true}); // clear options only
</td>
</tr>
<tr>
<td>destroy()</td>
<td>none</td>
<td>Destroy the Timeline. The timeline is removed from memory. all DOM elements and event listeners are cleaned up.
</td>
</tr>
<tr>
<td>fit()</td>
<td>none</td>
@ -895,7 +929,7 @@ var options = {
</p>
<ul>
<li><code>item</code>: the item being manipulated</li>
<li><code>callback</code>: a callback function which must be invoked to report back. The callback must be invoked as <code>callback(item | null)</code>. Here, <code>item</code> can contain changes to the passed item. When invoked as <code>callback(null)</code>, the action will be cancelled.</li>
<li><code>callback</code>: a callback function which must be invoked to report back. The callback must be invoked as <code>callback(item | null)</code>. Here, <code>item</code> can contain changes to the passed item. Parameter `item` typically contains fields `content`, `start`, and optionally `end`. The type of `start` and `end` is determined by the DataSet type configuration and is `Date` by default. When invoked as <code>callback(null)</code>, the action will be cancelled.</li>
</ul>
<p>

+ 9
- 2
examples/timeline/01_basic.html View File

@ -16,16 +16,23 @@
<div id="visualization"></div>
<script type="text/javascript">
// DOM element where the Timeline will be attached
var container = document.getElementById('visualization');
var items = [
// Create a DataSet (allows two way data-binding)
var items = new vis.DataSet([
{id: 1, content: 'item 1', start: '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'}
];
]);
// Configuration for the Timeline
var options = {};
// Create a Timeline
var timeline = new vis.Timeline(container, items, options);
</script>
</body>

+ 6
- 4
examples/timeline/02_interactive.html View File

@ -21,12 +21,14 @@
<script>
// create a dataset with items
// we specify the type of the fields `start` and `end` here to be strings
// containing an ISO date. The fields will be outputted as ISO dates
// automatically getting data from the DataSet via items.get().
var items = new vis.DataSet({
convert: {
start: 'Date',
end: 'Date'
}
type: { start: 'ISODate', end: 'ISODate' }
});
// add items to the DataSet
items.add([
{id: 1, content: 'item 1<br>start', start: '2014-01-23'},
{id: 2, content: 'item 2', start: '2014-01-18'},

+ 1
- 4
examples/timeline/03_a_lot_of_data.html View File

@ -31,10 +31,7 @@
// create a dataset with items
var now = moment().minutes(0).seconds(0).milliseconds(0);
var items = new vis.DataSet({
convert: {
start: 'Date',
end: 'Date'
}
type: {start: 'ISODate', end: 'ISODate' }
});
// create data

+ 2
- 2
examples/timeline/04_html_data.html View File

@ -58,7 +58,7 @@
// create data and a Timeline
var container = document.getElementById('visualization');
var items = [
var items = new vis.DataSet([
{id: 1, content: item1, start: '2013-04-20'},
{id: 2, content: item2, start: '2013-04-14'},
{id: 3, content: item3, start: '2013-04-18'},
@ -66,7 +66,7 @@
{id: 5, content: item5, start: '2013-04-25'},
{id: 6, content: item6, start: '2013-04-27'},
{id: 7, content: item7, start: '2013-04-21'}
];
]);
var options = {};
var timeline = new vis.Timeline(container, items, options);
</script>

+ 1
- 7
examples/timeline/06_event_listeners.html View File

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

+ 1
- 1
examples/timeline/07_custom_time_bar.html View File

@ -34,7 +34,7 @@
<script type="text/javascript">
var container = document.getElementById('visualization');
var items = [];
var items = new vis.DataSet();
var options = {
showCurrentTime: true,
showCustomTime: true,

+ 2
- 2
examples/timeline/10_limit_move_and_zoom.html View File

@ -28,10 +28,10 @@
<script>
// create some items
// note that months are zero-based in the JavaScript Date object, so month 4 is May
var items = [
var items = new vis.DataSet([
{'start': new Date(2012, 4, 25), 'content': 'First'},
{'start': new Date(2012, 4, 26), 'content': 'Last'}
];
]);
// create visualization
var container = document.getElementById('visualization');

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

@ -23,7 +23,7 @@
var container = document.getElementById('visualization');
// note that months are zero-based in the JavaScript Date object
var items = [
var items = new vis.DataSet([
{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'},
@ -44,7 +44,7 @@
{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

+ 2
- 2
examples/timeline/12_custom_styling.html View File

@ -67,7 +67,7 @@
var container = document.getElementById('visualization');
// note that months are zero-based in the JavaScript Date object
var items = [
var items = new vis.DataSet([
{start: new Date(2010,7,23), content: '<div>Conversation</div><img src="img/community-users-icon.png" style="width:32px; height:32px;">'},
{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'},
@ -76,7 +76,7 @@
{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,

+ 2
- 2
examples/timeline/15_item_class_names.html View File

@ -74,7 +74,7 @@
<script type="text/javascript">
// create data
// note that months are zero-based in the JavaScript Date object
var data = [
var data = new vis.DataSet([
{
'start': new Date(2012,7,19),
'content': 'default'
@ -100,7 +100,7 @@
'content': 'magenta',
'className': 'magenta'
}
];
]);
// specify options
var options = {

+ 2
- 2
examples/timeline/16_navigation_menu.html View File

@ -38,14 +38,14 @@
<script type="text/javascript">
// create a timeline with some data
var container = document.getElementById('visualization');
var items = [
var items = new vis.DataSet([
{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);

+ 120
- 0
examples/timeline/17_data_serialization.html View File

@ -0,0 +1,120 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Data serialization</title>
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
}
textarea {
width: 800px;
height: 200px;
}
.buttons {
margin: 20px 0;
}
.buttons input {
padding: 10px;
}
</style>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h1>Serialization and deserialization</h1>
<p>This example shows how to serialize and deserialize JSON data, and load this in the Timeline via a DataSet. Serialization and deserialization is needed when loading or saving data from a server.</p>
<textarea id="data">
[
{"id": 1, "content": "item 1<br>start", "start": "2014-01-23"},
{"id": 2, "content": "item 2", "start": "2014-01-18"},
{"id": 3, "content": "item 3", "start": "2014-01-21"},
{"id": 4, "content": "item 4", "start": "2014-01-19", "end": "2014-01-24"},
{"id": 5, "content": "item 5", "start": "2014-01-28", "type": "point"},
{"id": 6, "content": "item 6", "start": "2014-01-26"}
]
</textarea>
<div class="buttons">
<input type="button" id="load" value="&darr; Load" title="Load data from textarea into the Timeline">
<input type="button" id="save" value="&uarr; Save" title="Save data from the Timeline into the textarea">
</div>
<div id="visualization"></div>
<script>
var txtData = document.getElementById('data');
var btnLoad = document.getElementById('load');
var btnSave = document.getElementById('save');
// Create an empty DataSet.
// This DataSet is used for two way data binding with the Timeline.
var items = new vis.DataSet();
// create a timeline
var container = document.getElementById('visualization');
var options = {
editable: true
};
var timeline = new vis.Timeline(container, items, options);
function loadData () {
// get and deserialize the data
var data = JSON.parse(txtData.value);
// update the data in the DataSet
//
// Note: when retrieving updated data from a server instead of a complete
// new set of data, one can simply update the existing data like:
//
// items.update(data);
//
// Existing items will then be updated, and new items will be added.
items.clear();
items.add(data);
// adjust the timeline window such that we see the loaded data
timeline.fit();
}
btnLoad.onclick = loadData;
function saveData() {
// get the data from the DataSet
// Note that we specify the output type of the fields start and end
// as ISODate, which is safely serializable. Other serializable types
// are Number (unix timestamp) or ASPDate.
//
// Alternatively, it is possible to configure the DataSet to convert
// the output automatically to ISODates like:
//
// var options = {
// type: {start: 'ISODate', end: 'ISODate'}
// };
// var items = new vis.DataSet(options);
// // now items.get() will automatically convert start and end to ISO dates.
//
var data = items.get({
type: {
start: 'ISODate',
end: 'ISODate'
}
});
// serialize the data and put it in the textarea
txtData.value = JSON.stringify(data, null, 2);
}
btnSave.onclick = saveData;
// load the initial data
loadData();
</script>
</body>
</html>

+ 53
- 0
examples/timeline/18_range_overflow.html View File

@ -0,0 +1,53 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Range overflow</title>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
<style type="text/css">
body, html {
font-family: sans-serif;
}
.vis.timeline .item.range .content {
overflow: visible;
}
</style>
</head>
<body>
<p>
In case of ranges being spread over a wide range of time, it can be interesting to have the text contents of the ranges overflow the box. This can be achieved by changing the overflow property of the contents to visible with css:
</p>
<pre>
.vis.timeline .item.range .content {
overflow: visible;
}
</pre>
<div id="visualization"></div>
<script type="text/javascript">
// DOM element where the Timeline will be attached
var container = document.getElementById('visualization');
// Create a DataSet (allows two way data-binding)
var items = new vis.DataSet([
{id: 1, content: 'item 1 with overflowing text content', start: '2014-04-20', end: '2014-04-26'},
{id: 2, content: 'item 2 with overflowing text content', start: '2014-05-14', end: '2014-05-18'},
{id: 3, content: 'item 3 with overflowing text content', start: '2014-06-18', end: '2014-06-22'},
{id: 4, content: 'item 4 with overflowing text content', start: '2014-06-16', end: '2014-06-17'},
{id: 5, content: 'item 5 with overflowing text content', start: '2014-06-25', end: '2014-06-27'},
{id: 6, content: 'item 6 with overflowing text content', start: '2014-09-27', end: '2014-09-28'}
]);
// Configuration for the Timeline
var options = {};
// Create a Timeline
var timeline = new vis.Timeline(container, items, options);
</script>
</body>
</html>

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

@ -28,6 +28,8 @@
<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="16_navigation_menu.html">16_navigation_menu.html</a></p>
<p><a href="17_data_serialization.html">17_data_serialization.html</a></p>
<p><a href="18_range_overflow.html">18_range_overflow.html</a></p>
<p><a href="requirejs/requirejs_example.html">requirejs_example.html</a></p>

+ 2
- 2
examples/timeline/requirejs/scripts/main.js View File

@ -6,14 +6,14 @@ require.config({
require(['vis'], function (vis) {
var container = document.getElementById('visualization');
var data = [
var data = new vis.DataSet([
{id: 1, content: 'item 1', start: '2013-04-20'},
{id: 2, content: 'item 2', start: '2013-04-14'},
{id: 3, content: 'item 3', start: '2013-04-18'},
{id: 4, content: 'item 4', start: '2013-04-16', end: '2013-04-19'},
{id: 5, content: 'item 5', start: '2013-04-25'},
{id: 6, content: 'item 6', start: '2013-04-27'}
];
]);
var options = {};
var timeline = new vis.Timeline(container, data, options);
});

+ 1
- 1
package.json View File

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

+ 9
- 19
src/DataSet.js View File

@ -760,28 +760,18 @@ DataSet.prototype.min = function (field) {
* The returned array is unordered.
*/
DataSet.prototype.distinct = function (field) {
var data = this._data,
values = [],
fieldType = "",
count = 0;
// do not convert unless this is required.
var convert = false;
if (this._options) {
if (this._options.type) {
if (this._options.type.hasOwnProperty(field)) {
fieldType = this._options.type[field];
convert = true;
}
}
}
var data = this._data;
var values = [];
var fieldType = this._options.type && this._options.type[field] || null;
var count = 0;
var i;
for (var prop in data) {
if (data.hasOwnProperty(prop)) {
var item = data[prop];
var value = item[field];
var exists = false;
for (var i = 0; i < count; i++) {
for (i = 0; i < count; i++) {
if (values[i] == value) {
exists = true;
break;
@ -794,9 +784,9 @@ DataSet.prototype.distinct = function (field) {
}
}
if (convert == true) {
for (var i = 0; i < values.length; i++) {
values[i] = util.convert(values[i],fieldType);
if (fieldType) {
for (i = 0; i < values.length; i++) {
values[i] = util.convert(values[i], fieldType);
}
}

+ 202
- 35
src/graph/Edge.js View File

@ -62,6 +62,10 @@ function Edge (properties, graph, constants) {
this.lengthFixed = false;
this.setProperties(properties, constants);
this.controlNodesEnabled = false;
this.controlNodes = {from:null, to:null, positions:{}};
this.connectedNode = null;
}
/**
@ -721,44 +725,65 @@ Edge.prototype._drawArrow = function(ctx) {
* @private
*/
Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
if (this.smooth == true) {
var minDistance = 1e9;
var i,t,x,y,dx,dy;
for (i = 0; i < 10; i++) {
t = 0.1*i;
x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
dx = Math.abs(x3-x);
dy = Math.abs(y3-y);
minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
}
return minDistance
}
else {
var px = x2-x1,
py = y2-y1,
something = px*px + py*py,
u = ((x3 - x1) * px + (y3 - y1) * py) / something;
if (u > 1) {
u = 1;
}
else if (u < 0) {
u = 0;
if (this.from != this.to) {
if (this.smooth == true) {
var minDistance = 1e9;
var i,t,x,y,dx,dy;
for (i = 0; i < 10; i++) {
t = 0.1*i;
x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
dx = Math.abs(x3-x);
dy = Math.abs(y3-y);
minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
}
return minDistance
}
else {
var px = x2-x1,
py = y2-y1,
something = px*px + py*py,
u = ((x3 - x1) * px + (y3 - y1) * py) / something;
var x = x1 + u * px,
y = y1 + u * py,
dx = x - x3,
dy = y - y3;
if (u > 1) {
u = 1;
}
else if (u < 0) {
u = 0;
}
var x = x1 + u * px,
y = y1 + u * py,
dx = x - x3,
dy = y - y3;
//# Note: If the actual distance does not matter,
//# if you only want to compare what this function
//# returns to other results of this function, you
//# can just return the squared distance instead
//# (i.e. remove the sqrt) to gain a little performance
//# Note: If the actual distance does not matter,
//# if you only want to compare what this function
//# returns to other results of this function, you
//# can just return the squared distance instead
//# (i.e. remove the sqrt) to gain a little performance
return Math.sqrt(dx*dx + dy*dy);
return Math.sqrt(dx*dx + dy*dy);
}
}
else {
var x, y, dx, dy;
var radius = this.length / 4;
var node = this.from;
if (!node.width) {
node.resize(ctx);
}
if (node.width > node.height) {
x = node.x + node.width / 2;
y = node.y - radius;
}
else {
x = node.x + radius;
y = node.y - node.height / 2;
}
dx = x - x3;
dy = y - y3;
return Math.abs(Math.sqrt(dx*dx + dy*dy) - radius);
}
};
@ -787,4 +812,146 @@ Edge.prototype.positionBezierNode = function() {
this.via.x = 0.5 * (this.from.x + this.to.x);
this.via.y = 0.5 * (this.from.y + this.to.y);
}
};
};
/**
* This function draws the control nodes for the manipulator. In order to enable this, only set the this.controlNodesEnabled to true.
* @param ctx
*/
Edge.prototype._drawControlNodes = function(ctx) {
if (this.controlNodesEnabled == true) {
if (this.controlNodes.from === null && this.controlNodes.to === null) {
var nodeIdFrom = "edgeIdFrom:".concat(this.id);
var nodeIdTo = "edgeIdTo:".concat(this.id);
var constants = {
nodes:{group:'', radius:8},
physics:{damping:0},
clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}}
};
this.controlNodes.from = new Node(
{id:nodeIdFrom,
shape:'dot',
color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
},{},{},constants);
this.controlNodes.to = new Node(
{id:nodeIdTo,
shape:'dot',
color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
},{},{},constants);
}
if (this.controlNodes.from.selected == false && this.controlNodes.to.selected == false) {
this.controlNodes.positions = this.getControlNodePositions(ctx);
this.controlNodes.from.x = this.controlNodes.positions.from.x;
this.controlNodes.from.y = this.controlNodes.positions.from.y;
this.controlNodes.to.x = this.controlNodes.positions.to.x;
this.controlNodes.to.y = this.controlNodes.positions.to.y;
}
this.controlNodes.from.draw(ctx);
this.controlNodes.to.draw(ctx);
}
else {
this.controlNodes = {from:null, to:null, positions:{}};
}
}
/**
* Enable control nodes.
* @private
*/
Edge.prototype._enableControlNodes = function() {
this.controlNodesEnabled = true;
}
/**
* disable control nodes
* @private
*/
Edge.prototype._disableControlNodes = function() {
this.controlNodesEnabled = false;
}
/**
* This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null.
* @param x
* @param y
* @returns {null}
* @private
*/
Edge.prototype._getSelectedControlNode = function(x,y) {
var positions = this.controlNodes.positions;
var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2));
var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2));
if (fromDistance < 15) {
this.connectedNode = this.from;
this.from = this.controlNodes.from;
return this.controlNodes.from;
}
else if (toDistance < 15) {
this.connectedNode = this.to;
this.to = this.controlNodes.to;
return this.controlNodes.to;
}
else {
return null;
}
}
/**
* this resets the control nodes to their original position.
* @private
*/
Edge.prototype._restoreControlNodes = function() {
if (this.controlNodes.from.selected == true) {
this.from = this.connectedNode;
this.connectedNode = null;
this.controlNodes.from.unselect();
}
if (this.controlNodes.to.selected == true) {
this.to = this.connectedNode;
this.connectedNode = null;
this.controlNodes.to.unselect();
}
}
/**
* this calculates the position of the control nodes on the edges of the parent nodes.
*
* @param ctx
* @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
*/
Edge.prototype.getControlNodePositions = function(ctx) {
var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
var dx = (this.to.x - this.from.x);
var dy = (this.to.y - this.from.y);
var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
if (this.smooth == true) {
angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
dx = (this.to.x - this.via.x);
dy = (this.to.y - this.via.y);
edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
}
var toBorderDist = this.to.distanceToBorder(ctx, angle);
var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
var xTo,yTo;
if (this.smooth == true) {
xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
}
else {
xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
}
return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}};
}

+ 26
- 2
src/graph/Graph.js View File

@ -10,6 +10,9 @@
* @param {Object} options Options
*/
function Graph (container, data, options) {
if (!(this instanceof Graph)) {
throw new SyntaxError('Constructor must be called with the new operator');
}
this._initializeMixinLoaders();
@ -30,7 +33,7 @@ function Graph (container, data, options) {
this.initializing = true;
// these functions are triggered when the dataset is edited
this.triggerFunctions = {add:null,edit:null,connect:null,del:null};
this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
// set constant values
this.constants = {
@ -166,9 +169,11 @@ function Graph (container, data, options) {
link:"Add Link",
del:"Delete selected",
editNode:"Edit Node",
editEdge:"Edit Edge",
back:"Back",
addDescription:"Click in an empty space to place a new node.",
linkDescription:"Click on a node and drag the edge to another node to connect them.",
editEdgeDescription:"Click on the control points and drag them to a node to connect to it.",
addError:"The function for add does not support two arguments (data,callback).",
linkError:"The function for connect does not support two arguments (data,callback).",
editError:"The function for edit does not support two arguments (data, callback).",
@ -550,6 +555,10 @@ Graph.prototype.setOptions = function (options) {
this.triggerFunctions.edit = options.onEdit;
}
if (options.onEditEdge) {
this.triggerFunctions.editEdge = options.onEditEdge;
}
if (options.onConnect) {
this.triggerFunctions.connect = options.onConnect;
}
@ -1679,7 +1688,6 @@ Graph.prototype._updateValueRange = function(obj) {
*/
Graph.prototype.redraw = function() {
this.setSize(this.width, this.height);
this._redraw();
};
@ -1711,6 +1719,7 @@ Graph.prototype._redraw = function() {
this._doInAllSectors("_drawAllSectorNodes",ctx);
this._doInAllSectors("_drawEdges",ctx);
this._doInAllSectors("_drawNodes",ctx,false);
this._doInAllSectors("_drawControlNodes",ctx);
// this._doInSupportSector("_drawNodes",ctx,true);
// this._drawTree(ctx,"#F00F0F");
@ -1895,6 +1904,21 @@ Graph.prototype._drawEdges = function(ctx) {
}
};
/**
* Redraw all edges
* The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
* @param {CanvasRenderingContext2D} ctx
* @private
*/
Graph.prototype._drawControlNodes = function(ctx) {
var edges = this.edges;
for (var id in edges) {
if (edges.hasOwnProperty(id)) {
edges[id]._drawControlNodes(ctx);
}
}
};
/**
* Find a stable position for all nodes
* @private

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

@ -30,8 +30,8 @@ function Node(properties, imagelist, grouplist, constants) {
this.edges = []; // all edges connected to this node
this.dynamicEdges = [];
this.reroutedEdges = {};
this.group = constants.nodes.group;
this.group = constants.nodes.group;
this.fontSize = Number(constants.nodes.fontSize);
this.fontFace = constants.nodes.fontFace;
this.fontColor = constants.nodes.fontColor;
@ -812,7 +812,6 @@ Node.prototype._drawShape = function (ctx, shape) {
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
ctx[shape](this.x, this.y, this.radius);
ctx.fill();
ctx.stroke();

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

@ -65,6 +65,11 @@ var manipulationMixin = {
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
if (this.edgeBeingEdited !== undefined) {
this.edgeBeingEdited._disableControlNodes();
this.edgeBeingEdited = undefined;
this.selectedControlNode = null;
}
// restore overloaded functions
this._restoreOverloadedFunctions();
@ -93,6 +98,12 @@ var manipulationMixin = {
"<span class='graph-manipulationUI edit' id='graph-manipulate-editNode'>" +
"<span class='graph-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>";
}
else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
this.manipulationDiv.innerHTML += "" +
"<div class='graph-seperatorLine'></div>" +
"<span class='graph-manipulationUI edit' id='graph-manipulate-editEdge'>" +
"<span class='graph-manipulationLabel'>"+this.constants.labels['editEdge'] +"</span></span>";
}
if (this._selectionIsEmpty() == false) {
this.manipulationDiv.innerHTML += "" +
"<div class='graph-seperatorLine'></div>" +
@ -110,6 +121,10 @@ var manipulationMixin = {
var editButton = document.getElementById("graph-manipulate-editNode");
editButton.onclick = this._editNode.bind(this);
}
else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
var editButton = document.getElementById("graph-manipulate-editEdge");
editButton.onclick = this._createEditEdgeToolbar.bind(this);
}
if (this._selectionIsEmpty() == false) {
var deleteButton = document.getElementById("graph-manipulate-delete");
deleteButton.onclick = this._deleteSelected.bind(this);
@ -203,10 +218,106 @@ var manipulationMixin = {
// redraw to show the unselect
this._redraw();
},
/**
* create the toolbar to edit edges
*
* @private
*/
_createEditEdgeToolbar : function() {
// clear the toolbar
this._clearManipulatorBar();
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
this.edgeBeingEdited = this._getSelectedEdge();
this.edgeBeingEdited._enableControlNodes();
this.manipulationDiv.innerHTML = "" +
"<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
"<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
"<div class='graph-seperatorLine'></div>" +
"<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
"<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['editEdgeDescription'] + "</span></span>";
// bind the icon
var backButton = document.getElementById("graph-manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this);
// temporarily overload functions
this.cachedFunctions["_handleTouch"] = this._handleTouch;
this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
this.cachedFunctions["_handleTap"] = this._handleTap;
this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
this._handleTouch = this._selectControlNode;
this._handleTap = function () {};
this._handleOnDrag = this._controlNodeDrag;
this._handleDragStart = function () {}
this._handleOnRelease = this._releaseControlNode;
// redraw to show the unselect
this._redraw();
},
/**
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description
* to walk the user through the process.
*
* @private
*/
_selectControlNode : function(pointer) {
this.edgeBeingEdited.controlNodes.from.unselect();
this.edgeBeingEdited.controlNodes.to.unselect();
this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
if (this.selectedControlNode !== null) {
this.selectedControlNode.select();
this.freezeSimulation = true;
}
this._redraw();
},
/**
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description
* to walk the user through the process.
*
* @private
*/
_controlNodeDrag : function(event) {
var pointer = this._getPointer(event.gesture.center);
if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
}
this._redraw();
},
_releaseControlNode : function(pointer) {
var newNode = this._getNodeAt(pointer);
if (newNode != null) {
if (this.edgeBeingEdited.controlNodes.from.selected == true) {
this._editEdge(newNode.id, this.edgeBeingEdited.to.id);
this.edgeBeingEdited.controlNodes.from.unselect();
}
if (this.edgeBeingEdited.controlNodes.to.selected == true) {
this._editEdge(this.edgeBeingEdited.from.id, newNode.id);
this.edgeBeingEdited.controlNodes.to.unselect();
}
}
else {
this.edgeBeingEdited._restoreControlNodes();
}
this.freezeSimulation = false;
this._redraw();
},
/**
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description
* to walk the user through the process.
@ -351,6 +462,36 @@ var manipulationMixin = {
}
},
/**
* connect two nodes with a new edge.
*
* @private
*/
_editEdge : function(sourceNodeId,targetNodeId) {
if (this.editMode == true) {
var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
if (this.triggerFunctions.editEdge) {
if (this.triggerFunctions.editEdge.length == 2) {
var me = this;
this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
me.edgesData.update(finalizedData);
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels["linkError"]);
this.moving = true;
this.start();
}
}
else {
this.edgesData.update(defaultData);
this.moving = true;
this.start();
}
}
},
/**
* Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
@ -391,6 +532,8 @@ var manipulationMixin = {
},
/**
* delete everything in the selection
*

+ 78
- 4
src/graph/graphMixins/SelectionMixin.js View File

@ -241,7 +241,7 @@ var SelectionMixin = {
},
/**
* return the number of selected nodes
* return the selected node
*
* @returns {number}
* @private
@ -255,6 +255,21 @@ var SelectionMixin = {
return null;
},
/**
* return the selected edge
*
* @returns {number}
* @private
*/
_getSelectedEdge : function() {
for (var edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
return this.selectionObj.edges[edgeId];
}
}
return null;
},
/**
* return the number of selected edges
@ -387,10 +402,13 @@ var SelectionMixin = {
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
_selectObject : function(object, append, doNotTrigger) {
_selectObject : function(object, append, doNotTrigger, highlightEdges) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
if (highlightEdges === undefined) {
highlightEdges = true;
}
if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
this._unselectAll(true);
@ -399,7 +417,7 @@ var SelectionMixin = {
if (object.selected == false) {
object.select();
this._addToSelection(object);
if (object instanceof Node && this.blockConnectingEdgeSelection == false) {
if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) {
this._selectConnectedEdges(object);
}
}
@ -458,7 +476,6 @@ var SelectionMixin = {
* @private
*/
_handleTouch : function(pointer) {
},
@ -605,10 +622,67 @@ var SelectionMixin = {
}
this._selectObject(node,true,true);
}
console.log("setSelection is deprecated. Please use selectNodes instead.")
this.redraw();
},
/**
* select zero or more nodes with the option to highlight edges
* @param {Number[] | String[]} selection An array with the ids of the
* selected nodes.
* @param {boolean} [highlightEdges]
*/
selectNodes : function(selection, highlightEdges) {
var i, iMax, id;
if (!selection || (selection.length == undefined))
throw 'Selection must be an array with ids';
// first unselect any selected node
this._unselectAll(true);
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
var node = this.nodes[id];
if (!node) {
throw new RangeError('Node with id "' + id + '" not found');
}
this._selectObject(node,true,true,highlightEdges);
}
this.redraw();
},
/**
* select zero or more edges
* @param {Number[] | String[]} selection An array with the ids of the
* selected nodes.
*/
selectEdges : function(selection) {
var i, iMax, id;
if (!selection || (selection.length == undefined))
throw 'Selection must be an array with ids';
// first unselect any selected node
this._unselectAll(true);
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
var edge = this.edges[id];
if (!edge) {
throw new RangeError('Edge with id "' + id + '" not found');
}
this._selectObject(edge,true,true,highlightEdges);
}
this.redraw();
},
/**
* Validate the selection: remove ids of nodes which no longer exist
* @private

+ 4
- 0
src/graph3d/Graph3d.js View File

@ -10,6 +10,10 @@
* @param {Object} [options]
*/
function Graph3d(container, data, options) {
if (!(this instanceof Graph3d)) {
throw new SyntaxError('Constructor must be called with the new operator');
}
// create variables and set default values
this.containerElement = container;
this.width = '400px';

+ 45
- 40
src/timeline/Range.js View File

@ -18,6 +18,8 @@ function Range(body, options) {
start: null,
end: null,
direction: 'horizontal', // 'horizontal' or 'vertical'
moveable: true,
zoomable: true,
min: null,
max: null,
zoomMin: 10, // milliseconds
@ -25,6 +27,10 @@ function Range(body, options) {
};
this.options = util.extend({}, this.defaultOptions);
this.props = {
touch: {}
};
// drag listeners for dragging
this.body.emitter.on('dragstart', this._onDragStart.bind(this));
this.body.emitter.on('drag', this._onDrag.bind(this));
@ -57,11 +63,16 @@ Range.prototype = new Component();
* (end - start).
* {Number} zoomMax Set a maximum value for
* (end - start).
* {Boolean} moveable Enable moving of the range
* by dragging. True by default
* {Boolean} zoomable Enable zooming of the range
* by pinching/scrolling. True by default
*/
Range.prototype.setOptions = function (options) {
if (options) {
// copy the options that we know
util.selectiveExtend(['direction', 'min', 'max', 'zoomMin', 'zoomMax'], this.options, options);
var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable'];
util.selectiveExtend(fields, this.options, options);
if ('start' in options || 'end' in options) {
// apply a new range. both start and end are optional
@ -253,23 +264,21 @@ Range.conversion = function (start, end, width) {
}
};
// global (private) object to store drag params
var touchParams = {};
/**
* Start dragging horizontally or vertically
* @param {Event} event
* @private
*/
Range.prototype._onDragStart = function(event) {
// only allow dragging when configured as movable
if (!this.options.moveable) return;
// refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen
if (touchParams.ignore) return;
// TODO: reckon with option movable
if (!this.props.touch.allowDragging) return;
touchParams.start = this.start;
touchParams.end = this.end;
this.props.touch.start = this.start;
this.props.touch.end = this.end;
if (this.body.dom.root) {
this.body.dom.root.style.cursor = 'move';
@ -277,27 +286,27 @@ Range.prototype._onDragStart = function(event) {
};
/**
* Perform dragging operating.
* Perform dragging operation
* @param {Event} event
* @private
*/
Range.prototype._onDrag = function (event) {
// only allow dragging when configured as movable
if (!this.options.moveable) return;
var direction = this.options.direction;
validateDirection(direction);
// TODO: reckon with option movable
// refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen
if (touchParams.ignore) return;
if (!this.props.touch.allowDragging) return;
var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
interval = (touchParams.end - touchParams.start),
interval = (this.props.touch.end - this.props.touch.start),
width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height,
diffRange = -delta / width * interval;
this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange);
this.body.emitter.emit('rangechange', {
start: new Date(this.start),
@ -306,16 +315,17 @@ Range.prototype._onDrag = function (event) {
};
/**
* Stop dragging operating.
* Stop dragging operation
* @param {event} event
* @private
*/
Range.prototype._onDragEnd = function (event) {
// only allow dragging when configured as movable
if (!this.options.moveable) return;
// refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen
if (touchParams.ignore) return;
// TODO: reckon with option movable
if (!this.props.touch.allowDragging) return;
if (this.body.dom.root) {
this.body.dom.root.style.cursor = 'auto';
@ -335,7 +345,8 @@ Range.prototype._onDragEnd = function (event) {
* @private
*/
Range.prototype._onMouseWheel = function(event) {
// TODO: reckon with option zoomable
// only allow zooming when configured as zoomable and moveable
if (!(this.options.zoomable && this.options.moveable)) return;
// retrieve delta
var delta = 0;
@ -381,17 +392,10 @@ Range.prototype._onMouseWheel = function(event) {
* @private
*/
Range.prototype._onTouch = function (event) {
touchParams.start = this.start;
touchParams.end = this.end;
touchParams.ignore = false;
touchParams.center = null;
// don't move the range when dragging a selected event
// TODO: it's not so neat to have to know about the state of the ItemSet
var item = ItemSet.itemFromTarget(event);
if (item && item.selected && this.options.editable) {
touchParams.ignore = true;
}
this.props.touch.start = this.start;
this.props.touch.end = this.end;
this.props.touch.allowDragging = true;
this.props.touch.center = null;
};
/**
@ -399,7 +403,7 @@ Range.prototype._onTouch = function (event) {
* @private
*/
Range.prototype._onHold = function () {
touchParams.ignore = true;
this.props.touch.allowDragging = false;
};
/**
@ -408,21 +412,22 @@ Range.prototype._onHold = function () {
* @private
*/
Range.prototype._onPinch = function (event) {
touchParams.ignore = true;
// only allow zooming when configured as zoomable and moveable
if (!(this.options.zoomable && this.options.moveable)) return;
// TODO: reckon with option zoomable
this.props.touch.allowDragging = false;
if (event.gesture.touches.length > 1) {
if (!touchParams.center) {
touchParams.center = getPointer(event.gesture.center, this.body.dom.center);
if (!this.props.touch.center) {
this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center);
}
var scale = 1 / event.gesture.scale,
initDate = this._pointerToDate(touchParams.center);
initDate = this._pointerToDate(this.props.touch.center);
// calculate new start and end
var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
var newStart = parseInt(initDate + (this.props.touch.start - initDate) * scale);
var newEnd = parseInt(initDate + (this.props.touch.end - initDate) * scale);
// apply new range
this.setRange(newStart, newEnd);

+ 206
- 30
src/timeline/Timeline.js View File

@ -6,6 +6,10 @@
* @constructor
*/
function Timeline (container, items, options) {
if (!(this instanceof Timeline)) {
throw new SyntaxError('Constructor must be called with the new operator');
}
var me = this;
this.defaultOptions = {
start: null,
@ -13,12 +17,11 @@ function Timeline (container, items, options) {
autoResize: true,
orientation: 'bottom',
width: null,
height: null,
maxHeight: null,
minHeight: null
// TODO: implement options moveable and zoomable
};
this.options = util.deepExtend({}, this.defaultOptions);
@ -108,6 +111,12 @@ Timeline.prototype._create = function (container) {
this.dom.right = document.createElement('div');
this.dom.top = document.createElement('div');
this.dom.bottom = document.createElement('div');
this.dom.shadowTop = document.createElement('div');
this.dom.shadowBottom = document.createElement('div');
this.dom.shadowTopLeft = document.createElement('div');
this.dom.shadowBottomLeft = document.createElement('div');
this.dom.shadowTopRight = document.createElement('div');
this.dom.shadowBottomRight = document.createElement('div');
this.dom.background.className = 'vispanel background';
this.dom.backgroundVertical.className = 'vispanel background vertical';
@ -120,6 +129,12 @@ Timeline.prototype._create = function (container) {
this.dom.left.className = 'content';
this.dom.center.className = 'content';
this.dom.right.className = 'content';
this.dom.shadowTop.className = 'shadow top';
this.dom.shadowBottom.className = 'shadow bottom';
this.dom.shadowTopLeft.className = 'shadow top';
this.dom.shadowBottomLeft.className = 'shadow bottom';
this.dom.shadowTopRight.className = 'shadow top';
this.dom.shadowBottomRight.className = 'shadow bottom';
this.dom.root.appendChild(this.dom.background);
this.dom.root.appendChild(this.dom.backgroundVertical);
@ -134,8 +149,19 @@ Timeline.prototype._create = function (container) {
this.dom.leftContainer.appendChild(this.dom.left);
this.dom.rightContainer.appendChild(this.dom.right);
this.dom.centerContainer.appendChild(this.dom.shadowTop);
this.dom.centerContainer.appendChild(this.dom.shadowBottom);
this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
this.on('rangechange', this.redraw.bind(this));
this.on('change', this.redraw.bind(this));
this.on('touch', this._onTouch.bind(this));
this.on('pinch', this._onPinch.bind(this));
this.on('dragstart', this._onDragStart.bind(this));
this.on('drag', this._onDrag.bind(this));
// create event listeners for all interesting events, these events will be
// emitted via emitter
@ -146,8 +172,8 @@ Timeline.prototype._create = function (container) {
var me = this;
var events = [
'pinch',
//'tap', 'doubletap', 'hold', // TODO: catching the events here disables selecting an item
'touch', 'pinch',
'tap', 'doubletap', 'hold',
'dragstart', 'drag', 'dragend',
'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
];
@ -172,17 +198,59 @@ Timeline.prototype._create = function (container) {
right: {},
top: {},
bottom: {},
border: {}
border: {},
scrollTop: 0,
scrollTopMin: 0
};
this.touch = {}; // store state information needed for touch events
// attach the root panel to the provided container
if (!container) throw new Error('No container provided');
container.appendChild(this.dom.root);
};
/**
* Destroy the Timeline, clean up all DOM elements and event listeners.
*/
Timeline.prototype.destroy = function () {
// unbind datasets
this.clear();
// remove all event listeners
this.off();
// stop checking for changed size
this._stopAutoResize();
// remove from DOM
if (this.dom.root.parentNode) {
this.dom.root.parentNode.removeChild(this.dom.root);
}
this.dom = null;
// cleanup hammer touch events
for (var event in this.listeners) {
if (this.listeners.hasOwnProperty(event)) {
delete this.listeners[event];
}
}
this.listeners = null;
this.hammer = null;
// give all components the opportunity to cleanup
this.components.forEach(function (component) {
component.destroy();
});
this.body = null;
};
/**
* Set options. Options will be passed to all components loaded in the Timeline.
* @param {Object} [options]
* {String} orientation
* Vertical orientation for the Timeline,
* can be 'bottom' (default) or 'top'.
* {String | Number} width
* Width for the timeline, a number in pixels or
* a css string like '1000px' or '75%'. '100%' by default.
@ -205,7 +273,7 @@ Timeline.prototype._create = function (container) {
Timeline.prototype.setOptions = function (options) {
if (options) {
// copy the known options
var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end'];
var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
util.selectiveExtend(fields, this.options, options);
// enable/disable autoResize
@ -268,7 +336,7 @@ Timeline.prototype.setItems = function(items) {
else {
// turn an array into a dataset
newDataSet = new DataSet(items, {
convert: {
type: {
start: 'Date',
end: 'Date'
}
@ -385,20 +453,22 @@ Timeline.prototype.getItemRange = function() {
if (itemsData) {
// calculate the minimum value of the field 'start'
var minItem = itemsData.min('start');
min = minItem ? minItem.start.valueOf() : null;
min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
// Note: we convert first to Date and then to number because else
// a conversion from ISODate to Number will fail
// calculate maximum value of fields 'start' and 'end'
var maxStartItem = itemsData.max('start');
if (maxStartItem) {
max = maxStartItem.start.valueOf();
max = util.convert(maxStartItem.start, 'Date').valueOf();
}
var maxEndItem = itemsData.max('end');
if (maxEndItem) {
if (max == null) {
max = maxEndItem.end.valueOf();
max = util.convert(maxEndItem.end, 'Date').valueOf();
}
else {
max = Math.max(max, maxEndItem.end.valueOf());
max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
}
}
}
@ -473,6 +543,8 @@ Timeline.prototype.redraw = function() {
props = this.props,
dom = this.dom;
if (!dom) return; // when destroyed
// update class names
dom.root.className = 'vis timeline root ' + options.orientation;
@ -561,21 +633,31 @@ Timeline.prototype.redraw = function() {
dom.bottom.style.left = props.left.width + 'px';
dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
// update the scrollTop, feasible range for the offset can be changed
// when the height of the Timeline or of the contents of the center changed
this._updateScrollTop();
// reposition the scrollable contents
var offset;
if (options.orientation == 'top') {
offset = 0;
var offset = this.props.scrollTop;
if (options.orientation == 'bottom') {
offset += Math.max(this.props.centerContainer.height - this.props.center.height, 0);
}
else { // orientation == 'bottom'
// keep the items aligned to the axis at the bottom
offset = 0;// props.centerContainer.height - props.center.height;
}
dom.center.style.left = '0';
dom.center.style.top = offset+ 'px';
dom.left.style.left = '0';
dom.left.style.top = offset+ 'px';
dom.right.style.left = '0';
dom.right.style.top = offset+ 'px';
dom.center.style.left = '0';
dom.center.style.top = offset + 'px';
dom.left.style.left = '0';
dom.left.style.top = offset + 'px';
dom.right.style.left = '0';
dom.right.style.top = offset + 'px';
// show shadows when vertical scrolling is available
var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
dom.shadowTop.style.visibility = visibilityTop;
dom.shadowBottom.style.visibility = visibilityBottom;
dom.shadowTopLeft.style.visibility = visibilityTop;
dom.shadowBottomLeft.style.visibility = visibilityBottom;
dom.shadowTopRight.style.visibility = visibilityTop;
dom.shadowBottomRight.style.visibility = visibilityBottom;
// redraw all components
this.components.forEach(function (component) {
@ -640,7 +722,7 @@ Timeline.prototype._startAutoResize = function () {
this._stopAutoResize();
function checkSize() {
this._onResize = function() {
if (me.options.autoResize != true) {
// stop watching when the option autoResize is changed to false
me._stopAutoResize();
@ -657,12 +739,12 @@ Timeline.prototype._startAutoResize = function () {
me.emit('change');
}
}
}
};
// TODO: automatically cleanup the event listener when the frame is deleted
util.addEventListener(window, 'resize', checkSize);
// add event listener to window resize
util.addEventListener(window, 'resize', this._onResize);
this.watchTimer = setInterval(checkSize, 1000);
this.watchTimer = setInterval(this._onResize, 1000);
};
/**
@ -675,5 +757,99 @@ Timeline.prototype._stopAutoResize = function () {
this.watchTimer = undefined;
}
// TODO: remove event listener on window.resize
// remove event listener on window.resize
util.removeEventListener(window, 'resize', this._onResize);
this._onResize = null;
};
/**
* Start moving the timeline vertically
* @param {Event} event
* @private
*/
Timeline.prototype._onTouch = function (event) {
this.touch.allowDragging = true;
};
/**
* Start moving the timeline vertically
* @param {Event} event
* @private
*/
Timeline.prototype._onPinch = function (event) {
this.touch.allowDragging = false;
};
/**
* Start moving the timeline vertically
* @param {Event} event
* @private
*/
Timeline.prototype._onDragStart = function (event) {
this.touch.initialScrollTop = this.props.scrollTop;
};
/**
* Move the timeline vertically
* @param {Event} event
* @private
*/
Timeline.prototype._onDrag = function (event) {
// refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen
if (!this.touch.allowDragging) return;
var delta = event.gesture.deltaY;
var oldScrollTop = this._getScrollTop();
var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
if (newScrollTop != oldScrollTop) {
this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
}
};
/**
* Apply a scrollTop
* @param {Number} scrollTop
* @returns {Number} scrollTop Returns the applied scrollTop
* @private
*/
Timeline.prototype._setScrollTop = function (scrollTop) {
this.props.scrollTop = scrollTop;
this._updateScrollTop();
return this.props.scrollTop;
};
/**
* Update the current scrollTop when the height of the containers has been changed
* @returns {Number} scrollTop Returns the applied scrollTop
* @private
*/
Timeline.prototype._updateScrollTop = function () {
// recalculate the scrollTopMin
var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
if (scrollTopMin != this.props.scrollTopMin) {
// in case of bottom orientation, change the scrollTop such that the contents
// do not move relative to the time axis at the bottom
if (this.options.orientation == 'bottom') {
this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
}
this.props.scrollTopMin = scrollTopMin;
}
// limit the scrollTop to the feasible scroll range
if (this.props.scrollTop > 0) this.props.scrollTop = 0;
if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
return this.props.scrollTop;
};
/**
* Get the current scrollTop
* @returns {number} scrollTop
* @private
*/
Timeline.prototype._getScrollTop = function () {
return this.props.scrollTop;
};

+ 7
- 0
src/timeline/component/Component.js View File

@ -28,6 +28,13 @@ Component.prototype.redraw = function() {
return false;
};
/**
* Destroy the component. Cleanup DOM and event listeners
*/
Component.prototype.destroy = function() {
// should be implemented by the component
};
/**
* Test whether the component is resized since the last time _isResized() was
* called.

+ 11
- 1
src/timeline/component/CurrentTime.js View File

@ -37,6 +37,16 @@ CurrentTime.prototype._create = function() {
this.bar = bar;
};
/**
* Destroy the CurrentTime bar
*/
CurrentTime.prototype.destroy = function () {
this.options.showCurrentTime = false;
this.redraw(); // will remove the bar from the DOM and stop refreshing
this.body = null;
};
/**
* Set options for the component. Options will be merged in current options.
* @param {Object} options Available parameters:
@ -76,8 +86,8 @@ CurrentTime.prototype.redraw = function() {
// remove the line from the DOM
if (this.bar.parentNode) {
this.bar.parentNode.removeChild(this.bar);
this.stop();
}
this.stop();
}
return false;

+ 13
- 0
src/timeline/component/CustomTime.js View File

@ -68,6 +68,19 @@ CustomTime.prototype._create = function() {
this.hammer.on('dragend', this._onDragEnd.bind(this));
};
/**
* Destroy the CustomTime bar
*/
CustomTime.prototype.destroy = function () {
this.options.showCustomTime = false;
this.redraw(); // will remove the bar from the DOM
this.hammer.enable(false);
this.hammer = null;
this.body = null;
};
/**
* Repaint the component
* @return {boolean} Returns true if the component is resized

+ 16
- 3
src/timeline/component/Group.js View File

@ -16,6 +16,7 @@ function Group (groupId, data, itemSet) {
height: 0
}
};
this.className = null;
this.items = {}; // items filtered by groupId of this group
this.visibleItems = []; // items currently visible in window
@ -49,8 +50,10 @@ Group.prototype._create = function() {
this.dom.foreground = foreground;
this.dom.background = document.createElement('div');
this.dom.background.className = 'group';
this.dom.axis = document.createElement('div');
this.dom.axis.className = 'group';
// create a hidden marker to detect when the Timelines container is attached
// to the DOM, or the style of a parent of the Timeline is changed from
@ -86,9 +89,18 @@ Group.prototype.setData = function(data) {
}
// update className
var className = data && data.className;
if (className) {
var className = data && data.className || null;
if (className != this.className) {
if (this.className) {
util.removeClassName(this.dom.label, className);
util.removeClassName(this.dom.foreground, className);
util.removeClassName(this.dom.background, className);
util.removeClassName(this.dom.axis, className);
}
util.addClassName(this.dom.label, className);
util.addClassName(this.dom.foreground, className);
util.addClassName(this.dom.background, className);
util.addClassName(this.dom.axis, className);
}
};
@ -164,7 +176,8 @@ Group.prototype.redraw = function(range, margin, restack) {
resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
// apply new height
foreground.style.height = height + 'px';
this.dom.background.style.height = height + 'px';
this.dom.foreground.style.height = height + 'px';
this.dom.label.style.height = height + 'px';
// update vertical position of items after they are re-stacked and the height of the group is calculated

+ 40
- 23
src/timeline/component/ItemSet.js View File

@ -13,7 +13,7 @@ function ItemSet(body, options) {
this.body = body;
this.defaultOptions = {
type: 'box',
type: null, // 'box', 'point', 'range'
orientation: 'bottom', // 'top' or 'bottom'
align: 'center', // alignment of box items
stack: true,
@ -50,6 +50,11 @@ function ItemSet(body, options) {
// options is shared by this ItemSet and all its items
this.options = util.extend({}, this.defaultOptions);
// options for getting items from the DataSet with the correct type
this.itemOptions = {
type: {start: 'Date', end: 'Date'}
};
this.conversion = {
toScreen: body.util.toScreen,
toTime: body.util.toTime
@ -109,7 +114,6 @@ ItemSet.prototype = new Component();
ItemSet.types = {
box: ItemBox,
range: ItemRange,
rangeoverflow: ItemRangeOverflow,
point: ItemPoint
};
@ -148,8 +152,10 @@ ItemSet.prototype._create = function(){
this._updateUngrouped();
// attach event listeners
// TODO: use event listeners from the rootpanel to improve performance?
this.hammer = Hammer(frame, {
// Note: we bind to the centerContainer for the case where the height
// of the center container is larger than of the ItemSet, so we
// can click in the empty area to create a new item or deselect an item.
this.hammer = Hammer(this.body.dom.centerContainer, {
prevent_default: true
});
@ -281,6 +287,20 @@ ItemSet.prototype.markDirty = function() {
this.stackDirty = true;
};
/**
* Destroy the ItemSet
*/
ItemSet.prototype.destroy = function() {
this.hide();
this.setItems(null);
this.setGroups(null);
this.hammer = null;
this.body = null;
this.conversion = null;
};
/**
* Hide the component from the DOM
*/
@ -430,14 +450,8 @@ ItemSet.prototype.redraw = function() {
height = Math.max(height, minHeight);
this.stackDirty = false;
// reposition frame
frame.style.left = asSize(options.left, '');
frame.style.right = asSize(options.right, '');
frame.style.top = asSize((orientation == 'top') ? '0' : '');
frame.style.bottom = asSize((orientation == 'top') ? '' : '0');
frame.style.width = asSize(options.width, '100%');
// update frame height
frame.style.height = asSize(height);
//frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height
// calculate actual size and position
this.props.top = frame.offsetTop;
@ -656,12 +670,9 @@ ItemSet.prototype._onUpdate = function(ids) {
var me = this;
ids.forEach(function (id) {
var itemData = me.itemsData.get(id),
var itemData = me.itemsData.get(id, me.itemOptions),
item = me.items[id],
type = itemData.type ||
(itemData.start && itemData.end && 'range') ||
me.options.type ||
'box';
type = itemData.type || me.options.type || (itemData.end ? 'range' : 'box');
var constructor = ItemSet.types[type];
@ -684,6 +695,11 @@ ItemSet.prototype._onUpdate = function(ids) {
item.id = id; // TODO: not so nice setting id afterwards
me._addItem(item);
}
else if (type == 'rangeoverflow') {
// TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day
throw new TypeError('Item type "rangeoverflow" is deprecated. Use css styling instead: ' +
'.vis.timeline .item.range .content {overflow: visible;}');
}
else {
throw new TypeError('Unknown item type "' + type + '"');
}
@ -1076,16 +1092,18 @@ ItemSet.prototype._onDragEnd = function (event) {
this.touchParams.itemProps.forEach(function (props) {
var id = props.item.id,
itemData = me.itemsData.get(id);
itemData = me.itemsData.get(id, me.itemOptions);
var changed = false;
if ('start' in props.item.data) {
changed = (props.start != props.item.data.start.valueOf());
itemData.start = util.convert(props.item.data.start, dataset.convert['start']);
itemData.start = util.convert(props.item.data.start,
dataset._options.type && dataset._options.type.start || 'Date');
}
if ('end' in props.item.data) {
changed = changed || (props.end != props.item.data.end.valueOf());
itemData.end = util.convert(props.item.data.end, dataset.convert['end']);
itemData.end = util.convert(props.item.data.end,
dataset._options.type && dataset._options.type.end || 'Date');
}
if ('group' in props.item.data) {
changed = changed || (props.group != props.item.data.group);
@ -1097,7 +1115,7 @@ ItemSet.prototype._onDragEnd = function (event) {
me.options.onMove(itemData, function (itemData) {
if (itemData) {
// apply changes
itemData[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined)
changes.push(itemData);
}
else {
@ -1191,13 +1209,12 @@ ItemSet.prototype._onAddItem = function (event) {
};
// when default type is a range, add a default end date to the new item
if (this.options.type === 'range' || this.options.type == 'rangeoverflow') {
if (this.options.type === 'range') {
var end = this.body.util.toTime(x + this.props.width / 5);
newItem.end = snap ? snap(end) : end;
}
var id = util.randomUUID();
newItem[this.itemsData.fieldId] = id;
newItem[this.itemsData.fieldId] = util.randomUUID();
var group = ItemSet.groupFromTarget(event);
if (group) {

+ 17
- 16
src/timeline/component/TimeAxis.js View File

@ -73,6 +73,21 @@ TimeAxis.prototype._create = function() {
this.dom.background.className = 'timeaxis background';
};
/**
* Destroy the TimeAxis
*/
TimeAxis.prototype.destroy = function() {
// remove from DOM
if (this.dom.foreground.parentNode) {
this.dom.foreground.parentNode.removeChild(this.dom.foreground);
}
if (this.dom.background.parentNode) {
this.dom.background.parentNode.removeChild(this.dom.background);
}
this.body = null;
};
/**
* Repaint the component
* @return {boolean} Returns true if the component is resized
@ -238,14 +253,7 @@ TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
label.childNodes[0].nodeValue = text;
if (orientation == 'top') {
label.style.top = this.props.majorLabelHeight + 'px';
label.style.bottom = '';
}
else {
label.style.top = '';
label.style.bottom = this.props.majorLabelHeight + 'px';
}
label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0';
label.style.left = x + 'px';
//label.title = title; // TODO: this is a heavy operation
};
@ -274,14 +282,7 @@ TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
label.childNodes[0].nodeValue = text;
//label.title = title; // TODO: this is a heavy operation
if (orientation == 'top') {
label.style.top = '0px';
label.style.bottom = '';
}
else {
label.style.top = '';
label.style.bottom = '0px';
}
label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px');
label.style.left = x + 'px';
};

+ 33
- 0
src/timeline/component/css/animation.css View File

@ -0,0 +1,33 @@
.vis.timeline.root {
/*
-webkit-transition: height .4s ease-in-out;
transition: height .4s ease-in-out;
*/
}
.vis.timeline .vispanel {
/*
-webkit-transition: height .4s ease-in-out, top .4s ease-in-out;
transition: height .4s ease-in-out, top .4s ease-in-out;
*/
}
.vis.timeline .axis {
/*
-webkit-transition: top .4s ease-in-out;
transition: top .4s ease-in-out;
*/
}
/* TODO: get animation working nicely
.vis.timeline .item {
-webkit-transition: top .4s ease-in-out;
transition: top .4s ease-in-out;
}
.vis.timeline .item.line {
-webkit-transition: height .4s ease-in-out, top .4s ease-in-out;
transition: height .4s ease-in-out, top .4s ease-in-out;
}
/**/

+ 4
- 18
src/timeline/component/css/item.css View File

@ -7,11 +7,6 @@
background-color: #D5DDF6;
display: inline-block;
padding: 5px;
/* TODO: enable css transitions
-webkit-transition: top .4s ease-in-out, bottom .4s ease-in-out;
transition: top .4s ease-in-out, bottom .4s ease-in-out;
/**/
}
.vis.timeline .item.selected {
@ -46,15 +41,13 @@
border-radius: 4px;
}
.vis.timeline .item.range,
.vis.timeline .item.rangeoverflow{
.vis.timeline .item.range {
border-style: solid;
border-radius: 2px;
box-sizing: border-box;
}
.vis.timeline .item.range .content,
.vis.timeline .item.rangeoverflow .content {
.vis.timeline .item.range .content {
position: relative;
display: inline-block;
}
@ -70,11 +63,6 @@
width: 0;
border-left-width: 1px;
border-left-style: solid;
/* TODO: enable css transitions
-webkit-transition: height .4s ease-in-out, top .4s ease-in-out;
transition: height .4s ease-in-out, top .4s ease-in-out;
/**/
}
.vis.timeline .item .content {
@ -92,8 +80,7 @@
cursor: pointer;
}
.vis.timeline .item.range .drag-left,
.vis.timeline .item.rangeoverflow .drag-left {
.vis.timeline .item.range .drag-left {
position: absolute;
width: 24px;
height: 100%;
@ -104,8 +91,7 @@
z-index: 10000;
}
.vis.timeline .item.range .drag-right,
.vis.timeline .item.rangeoverflow .drag-right {
.vis.timeline .item.range .drag-right {
position: absolute;
width: 24px;
height: 100%;

+ 2
- 23
src/timeline/component/css/itemset.css View File

@ -5,11 +5,6 @@
margin: 0;
box-sizing: border-box;
/* FIXME: get transition working for rootpanel and itemset
-webkit-transition: height 4s ease-in-out;
transition: height 4s ease-in-out;
/**/
}
.vis.timeline .itemset .background,
@ -19,10 +14,6 @@
height: 100%;
}
.vis.timeline .itemset.foreground {
overflow: hidden;
}
.vis.timeline .axis {
position: absolute;
width: 100%;
@ -31,24 +22,12 @@
z-index: 1;
}
.vis.timeline .group {
.vis.timeline .foreground .group {
position: relative;
box-sizing: border-box;
border-bottom: 1px solid #bfbfbf;
}
.vis.timeline .group:last-child {
.vis.timeline .foreground .group:last-child {
border-bottom: none;
}
/*
.vis.timeline.top .group {
border-top: 1px solid #bfbfbf;
border-bottom: none;
}
.vis.timeline.bottom .group {
border-top: none;
border-bottom: 1px solid #bfbfbf;
}
*/

+ 20
- 0
src/timeline/component/css/panel.css View File

@ -49,3 +49,23 @@
.vis.timeline .vispanel > .content {
position: relative;
}
.vis.timeline .vispanel .shadow {
position: absolute;
width: 100%;
height: 1px;
box-shadow: 0 0 10px rgba(0,0,0,0.8);
/* TODO: find a nice way to ensure shadows are drawn on top of items
z-index: 1;
*/
}
.vis.timeline .vispanel .shadow.top {
top: -1px;
left: 0;
}
.vis.timeline .vispanel .shadow.bottom {
bottom: -1px;
left: 0;
}

+ 7
- 8
src/timeline/component/item/ItemBox.js View File

@ -211,20 +211,19 @@ ItemBox.prototype.repositionY = function() {
dot = this.dom.dot;
if (orientation == 'top') {
box.style.top = (this.top || 0) + 'px';
box.style.bottom = '';
box.style.top = (this.top || 0) + 'px';
line.style.top = '0';
line.style.bottom = '';
line.style.top = '0';
line.style.height = (this.parent.top + this.top + 1) + 'px';
line.style.bottom = '';
}
else { // orientation 'bottom'
box.style.top = '';
box.style.bottom = (this.top || 0) + 'px';
var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty
var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top;
line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px';
box.style.top = (this.parent.height - this.top - this.height || 0) + 'px';
line.style.top = (itemSetHeight - lineHeight) + 'px';
line.style.bottom = '0';
line.style.height = '';
}
dot.style.top = (-this.props.dot.height / 2) + 'px';

+ 1
- 3
src/timeline/component/item/ItemPoint.js View File

@ -183,10 +183,8 @@ ItemPoint.prototype.repositionY = function() {
if (orientation == 'top') {
point.style.top = this.top + 'px';
point.style.bottom = '';
}
else {
point.style.top = '';
point.style.bottom = this.top + 'px';
point.style.top = (this.parent.height - this.top - this.height) + 'px';
}
};

+ 30
- 14
src/timeline/component/item/ItemRange.js View File

@ -14,6 +14,7 @@ function ItemRange (data, conversion, options) {
width: 0
}
};
this.overflow = false; // if contents can overflow (css styling), this flag is set to true
// validate data
if (data) {
@ -107,6 +108,9 @@ ItemRange.prototype.redraw = function() {
// recalculate size
if (this.dirty) {
// determine from css whether this box has overflow
this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden';
this.props.content.width = this.dom.content.offsetWidth;
this.height = this.dom.box.offsetHeight;
@ -151,6 +155,7 @@ ItemRange.prototype.hide = function() {
* Reposition the item horizontally
* @Override
*/
// TODO: delete the old function
ItemRange.prototype.repositionX = function() {
var props = this.props,
parentWidth = this.parent.width,
@ -166,22 +171,35 @@ ItemRange.prototype.repositionX = function() {
if (end > 2 * parentWidth) {
end = 2 * parentWidth;
}
var boxWidth = Math.max(end - start, 1);
// when range exceeds left of the window, position the contents at the left of the visible area
if (start < 0) {
contentLeft = Math.min(-start,
(end - start - props.content.width - 2 * padding));
// TODO: remove the need for options.padding. it's terrible.
}
else {
contentLeft = 0;
if (this.overflow) {
// when range exceeds left of the window, position the contents at the left of the visible area
contentLeft = Math.max(-start, 0);
this.left = start;
this.width = boxWidth + this.props.content.width;
// Note: The calculation of width is an optimistic calculation, giving
// a width which will not change when moving the Timeline
// So no restacking needed, which is nicer for the eye;
}
else { // no overflow
// when range exceeds left of the window, position the contents at the left of the visible area
if (start < 0) {
contentLeft = Math.min(-start,
(end - start - props.content.width - 2 * padding));
// TODO: remove the need for options.padding. it's terrible.
}
else {
contentLeft = 0;
}
this.left = start;
this.width = Math.max(end - start, 1);
this.left = start;
this.width = boxWidth;
}
this.dom.box.style.left = this.left + 'px';
this.dom.box.style.width = this.width + 'px';
this.dom.box.style.width = boxWidth + 'px';
this.dom.content.style.left = contentLeft + 'px';
};
@ -195,11 +213,9 @@ ItemRange.prototype.repositionY = function() {
if (orientation == 'top') {
box.style.top = this.top + 'px';
box.style.bottom = '';
}
else {
box.style.top = '';
box.style.bottom = this.top + 'px';
box.style.top = (this.parent.height - this.top - this.height) + 'px';
}
};

+ 0
- 57
src/timeline/component/item/ItemRangeOverflow.js View File

@ -1,57 +0,0 @@
/**
* @constructor ItemRangeOverflow
* @extends ItemRange
* @param {Object} data Object containing parameters start, end
* content, className.
* @param {{toScreen: function, toTime: function}} conversion
* Conversion functions from time to screen and vice versa
* @param {Object} [options] Configuration options
* // TODO: describe options
*/
function ItemRangeOverflow (data, conversion, options) {
this.props = {
content: {
left: 0,
width: 0
}
};
ItemRange.call(this, data, conversion, options);
}
ItemRangeOverflow.prototype = new ItemRange (null, null, null);
ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow';
/**
* Reposition the item horizontally
* @Override
*/
ItemRangeOverflow.prototype.repositionX = function() {
var parentWidth = this.parent.width,
start = this.conversion.toScreen(this.data.start),
end = this.conversion.toScreen(this.data.end),
contentLeft;
// limit the width of the this, as browsers cannot draw very wide divs
if (start < -parentWidth) {
start = -parentWidth;
}
if (end > 2 * parentWidth) {
end = 2 * parentWidth;
}
// when range exceeds left of the window, position the contents at the left of the visible area
contentLeft = Math.max(-start, 0);
this.left = start;
var boxWidth = Math.max(end - start, 1);
this.width = boxWidth + this.props.content.width;
// Note: The calculation of width is an optimistic calculation, giving
// a width which will not change when moving the Timeline
// So no restacking needed, which is nicer for the eye
this.dom.box.style.left = this.left + 'px';
this.dom.box.style.width = boxWidth + 'px';
this.dom.content.style.left = contentLeft + 'px';
};

+ 2
- 3
src/util.js View File

@ -359,8 +359,7 @@ util.convert = function(object, type) {
}
default:
throw new Error('Cannot convert object of type ' + util.getType(object) +
' to type "' + type + '"');
throw new Error('Unknown type "' + type + '"');
}
};
@ -1055,4 +1054,4 @@ util._mergeOptions = function (mergeTarget, options, option) {
}
}
}
}
}

+ 5
- 14
test/dataset.js View File

@ -6,7 +6,7 @@ var assert = require('assert'),
var now = new Date();
var data = new DataSet({
convert: {
type: {
start: 'Date',
end: 'Date'
}
@ -31,6 +31,7 @@ items.forEach(function (item) {
var sort = function (a, b) {
return a.id > b.id;
};
assert.deepEqual(data.get({
fields: ['id', 'content']
}).sort(sort), [
@ -44,7 +45,7 @@ assert.deepEqual(data.get({
// convert dates
assert.deepEqual(data.get({
fields: ['id', 'start'],
convert: {start: 'Number'}
type: {start: 'Number'}
}).sort(sort), [
{id: 1, start: now.valueOf()},
{id: 2, start: now.valueOf()},
@ -56,7 +57,7 @@ assert.deepEqual(data.get({
// get a single item
assert.deepEqual(data.get(1, {
fields: ['id', 'start'],
convert: {start: 'ISODate'}
type: {start: 'ISODate'}
}), {
id: 1,
start: now.toISOString()
@ -150,17 +151,7 @@ data.clear();
data.add({content: 'Item 1'});
data.add({content: 'Item 2'});
assert.strictEqual(data.get()[0].id, undefined);
assert.deepEqual((data.get({"showInternalIds": true})[0].id == undefined),false);
assert.deepEqual(data.isInternalId(data.get({"showInternalIds": true})[0].id), true);
assert.deepEqual((data.get()[0].id == undefined), true);
// check if the global setting is applied correctly
var data = new DataSet({showInternalIds: true});
data.add({content: 'Item 1'});
assert.deepEqual((data.get()[0].id == undefined), false);
assert.deepEqual(data.isInternalId(data.get()[0].id), true);
assert.deepEqual((data.get({"showInternalIds": false})[0].id == undefined),true);
assert.notStrictEqual(data.get()[0].id, undefined);
// create a dataset with initial data
var data = new DataSet([

+ 7
- 7
test/timeline.html View File

@ -28,10 +28,10 @@
</select>
</div>
<script>
var orientation = document.getElementById('orientation');
orientation.onchange = function () {
var o = document.getElementById('orientation');
o.onchange = function () {
timeline.setOptions({
orientation: orientation.value
orientation: o.value
});
};
</script>
@ -58,9 +58,9 @@
// create a dataset with items
var now = moment().minutes(0).seconds(0).milliseconds(0);
var items = new vis.DataSet({
convert: {
start: 'Date',
end: 'Date'
type: {
start: 'ISODate',
end: 'ISODate'
},
fieldId: '_id'
});
@ -70,7 +70,7 @@
{_id: 2, content: 'item 2', start: now.clone().add('days', -2).toDate() },
{_id: 3, content: 'item 3', start: now.clone().add('days', 2).toDate()},
{
_id: 4, content: 'item 4',
_id: 4, content: 'item 4 ',
start: now.clone().add('days', 0).toDate(),
end: now.clone().add('days', 7).toDate()
},

+ 4
- 3
test/timeline_groups.html View File

@ -31,10 +31,10 @@
</select>
</div>
<script>
var orientation = document.getElementById('orientation');
orientation.onchange = function () {
var o = document.getElementById('orientation');
o.onchange = function () {
timeline.setOptions({
orientation: orientation.value
orientation: o.value
});
};
</script>
@ -72,6 +72,7 @@
// create visualization
var container = document.getElementById('visualization');
var options = {
//orientation: 'top',
editable: {
add: true,
remove: true,

Loading…
Cancel
Save