Browse Source

Merge branch 'develop'

Conflicts:
	src/graph/Graph.js
v3_develop
Alex de Mulder 10 years ago
parent
commit
6bc96b0104
64 changed files with 4759 additions and 3765 deletions
  1. +25
    -0
      HISTORY.md
  2. +1
    -2
      Jakefile.js
  3. +1
    -1
      bower.json
  4. +127
    -58
      dist/vis.css
  5. +2208
    -1820
      dist/vis.js
  6. +1
    -1
      dist/vis.min.css
  7. +13
    -12
      dist/vis.min.js
  8. +5
    -5
      docs/dataset.html
  9. +21
    -1
      docs/graph.html
  10. BIN
      docs/img/vis_overview.odg
  11. BIN
      docs/img/vis_overview.png
  12. +16
    -10
      docs/index.html
  13. +38
    -4
      docs/timeline.html
  14. +9
    -2
      examples/timeline/01_basic.html
  15. +6
    -4
      examples/timeline/02_interactive.html
  16. +1
    -4
      examples/timeline/03_a_lot_of_data.html
  17. +2
    -2
      examples/timeline/04_html_data.html
  18. +1
    -7
      examples/timeline/06_event_listeners.html
  19. +1
    -1
      examples/timeline/07_custom_time_bar.html
  20. +2
    -2
      examples/timeline/10_limit_move_and_zoom.html
  21. +2
    -2
      examples/timeline/11_points.html
  22. +3
    -3
      examples/timeline/12_custom_styling.html
  23. +2
    -2
      examples/timeline/15_item_class_names.html
  24. +2
    -2
      examples/timeline/16_navigation_menu.html
  25. +120
    -0
      examples/timeline/17_data_serialization.html
  26. +1
    -0
      examples/timeline/index.html
  27. +2
    -2
      examples/timeline/requirejs/scripts/main.js
  28. +1
    -1
      package.json
  29. +106
    -130
      src/DataSet.js
  30. +35
    -37
      src/DataView.js
  31. +202
    -35
      src/graph/Edge.js
  32. +32
    -9
      src/graph/Graph.js
  33. +2
    -3
      src/graph/Node.js
  34. +143
    -0
      src/graph/graphMixins/ManipulationMixin.js
  35. +16
    -2
      src/graph/graphMixins/SelectionMixin.js
  36. +0
    -2
      src/module/exports.js
  37. +93
    -80
      src/timeline/Range.js
  38. +519
    -430
      src/timeline/Timeline.js
  39. +19
    -53
      src/timeline/component/Component.js
  40. +57
    -25
      src/timeline/component/CurrentTime.js
  41. +55
    -19
      src/timeline/component/CustomTime.js
  42. +47
    -50
      src/timeline/component/Group.js
  43. +402
    -206
      src/timeline/component/ItemSet.js
  44. +0
    -170
      src/timeline/component/Panel.js
  45. +0
    -176
      src/timeline/component/RootPanel.js
  46. +112
    -169
      src/timeline/component/TimeAxis.js
  47. +33
    -0
      src/timeline/component/css/animation.css
  48. +1
    -1
      src/timeline/component/css/currenttime.css
  49. +1
    -1
      src/timeline/component/css/customtime.css
  50. +1
    -11
      src/timeline/component/css/item.css
  51. +13
    -18
      src/timeline/component/css/itemset.css
  52. +8
    -6
      src/timeline/component/css/labelset.css
  53. +56
    -13
      src/timeline/component/css/panel.css
  54. +15
    -8
      src/timeline/component/css/timeaxis.css
  55. +16
    -15
      src/timeline/component/item/Item.js
  56. +30
    -30
      src/timeline/component/item/ItemBox.js
  57. +20
    -21
      src/timeline/component/item/ItemPoint.js
  58. +23
    -24
      src/timeline/component/item/ItemRange.js
  59. +10
    -10
      src/timeline/component/item/ItemRangeOverflow.js
  60. +5
    -5
      src/timeline/stack.js
  61. +61
    -35
      src/util.js
  62. +5
    -14
      test/dataset.js
  63. +6
    -6
      test/timeline.html
  64. +4
    -3
      test/timeline_groups.html

+ 25
- 0
HISTORY.md View File

@ -2,6 +2,30 @@
http://visjs.org
## not yet released, 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
### Timeline
@ -17,6 +41,7 @@ http://visjs.org
- Fixed error with zero nodes with hierarchical layout.
- Added focusOnNode function.
- Added hover option.
- Added dragNodes option. Renamed movebale to dragGraph option.
- Added hover events (hoverNode, blurNode).
### Graph3D

+ 1
- 2
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/graph/css/graph-manipulation.css',
'./src/graph/css/graph-navigation.css'
@ -68,8 +69,6 @@ task('build', {async: true}, function () {
'./src/timeline/TimeStep.js',
'./src/timeline/Range.js',
'./src/timeline/component/Component.js',
'./src/timeline/component/Panel.js',
'./src/timeline/component/RootPanel.js',
'./src/timeline/component/TimeAxis.js',
'./src/timeline/component/CurrentTime.js',
'./src/timeline/component/CustomTime.js',

+ 1
- 1
bower.json View File

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

+ 127
- 58
dist/vis.css View File

@ -2,34 +2,76 @@
}
.vis.timeline.rootpanel {
.vis.timeline.root {
position: relative;
border: 1px solid #bfbfbf;
overflow: hidden;
padding: 0;
margin: 0;
border: 1px solid #bfbfbf;
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 {
.vis.timeline .vispanel {
position: absolute;
overflow: hidden;
padding: 0;
margin: 0;
box-sizing: border-box;
}
.vis.timeline .vpanel.side {
border-right: 1px solid #bfbfbf;
.vis.timeline .vispanel.center,
.vis.timeline .vispanel.left,
.vis.timeline .vispanel.right,
.vis.timeline .vispanel.top,
.vis.timeline .vispanel.bottom {
border: 1px #bfbfbf;
}
.vis.timeline .vispanel.center,
.vis.timeline .vispanel.left,
.vis.timeline .vispanel.right {
border-top-style: solid;
border-bottom-style: solid;
overflow: hidden;
}
.vis.timeline .vispanel.center,
.vis.timeline .vispanel.top,
.vis.timeline .vispanel.bottom {
border-left-style: solid;
border-right-style: solid;
}
.vis.timeline .background {
overflow: hidden;
}
.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 .vpanel.side.hidden {
display: none;
.vis.timeline .vispanel .shadow.top {
top: -1px;
left: 0;
}
.vis.timeline .vispanel .shadow.bottom {
bottom: -1px;
left: 0;
}
.vis.timeline .labelset {
position: relative;
@ -50,14 +92,12 @@
box-sizing: border-box;
}
.vis.timeline.top .labelset .vlabel {
border-top: 1px solid #bfbfbf;
border-bottom: none;
.vis.timeline .labelset .vlabel {
border-bottom: 1px solid #bfbfbf;
}
.vis.timeline.bottom .labelset .vlabel {
border-top: none;
border-bottom: 1px solid #bfbfbf;
.vis.timeline .labelset .vlabel:last-child {
border-bottom: none;
}
.vis.timeline .labelset .vlabel .inner {
@ -65,6 +105,10 @@
padding: 5px;
}
.vis.timeline .labelset .vlabel .inner.hidden {
padding: 0;
}
.vis.timeline .itemset {
position: relative;
@ -72,38 +116,33 @@
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 .background {
}
.vis.timeline .foreground {
.vis.timeline .itemset .background,
.vis.timeline .itemset .foreground {
position: absolute;
width: 100%;
height: 100%;
}
.vis.timeline .axis {
overflow: visible;
position: absolute;
width: 100%;
height: 0;
left: 1px;
z-index: 1;
}
.vis.timeline .group {
.vis.timeline .foreground .group {
position: relative;
box-sizing: border-box;
border-bottom: 1px solid #bfbfbf;
}
.vis.timeline.top .group {
border-top: 1px solid #bfbfbf;
.vis.timeline .foreground .group:last-child {
border-bottom: none;
}
.vis.timeline.bottom .group {
border-top: none;
border-bottom: 1px solid #bfbfbf;
}
.vis.timeline .item {
position: absolute;
@ -113,11 +152,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 {
@ -126,7 +160,7 @@
z-index: 999;
}
.vis.timeline.editable .item.selected {
.vis.timeline .editable .item.selected {
cursor: move;
}
@ -176,11 +210,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 {
@ -223,7 +252,22 @@
}
.vis.timeline .timeaxis {
position: relative;
overflow: hidden;
}
.vis.timeline .timeaxis.foreground {
top: 0;
left: 0;
width: 100%;
}
.vis.timeline .timeaxis.background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.vis.timeline .timeaxis .text {
@ -248,14 +292,6 @@
border-right: 1px solid;
}
.vis.timeline .timeaxis .grid.horizontal {
position: absolute;
left: 0;
width: 100%;
height: 0;
border-bottom: 1px solid;
}
.vis.timeline .timeaxis .grid.minor {
border-color: #e5e5e5;
}
@ -267,14 +303,47 @@
.vis.timeline .currenttime {
background-color: #FF7F6E;
width: 2px;
z-index: 9;
z-index: 1;
}
.vis.timeline .customtime {
background-color: #6E94FF;
width: 2px;
cursor: move;
z-index: 9;
z-index: 1;
}
.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;
}
/**/
div.graph-manipulationDiv {
border-width:0px;
border-bottom: 1px;

+ 2208
- 1820
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


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


+ 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
}

+ 21
- 1
docs/graph.html View File

@ -857,13 +857,21 @@ var options = {
</td>
</tr>
<tr>
<td>moveable</td>
<td>dragGraph</td>
<td>Boolean</td>
<td>true</td>
<td>
Toggle if the graph can be dragged. This will not affect the dragging of nodes.
</td>
</tr>
<tr>
<td>dragNodes</td>
<td>Boolean</td>
<td>true</td>
<td>
Toggle if the nodes can be dragged. This will not affect the dragging of the graph.
</td>
</tr>
<tr>
<td><a href="#Navigation_controls">navigation</a></td>
@ -1585,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.
@ -1943,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

BIN
docs/img/vis_overview.odg View File


BIN
docs/img/vis_overview.png View File

Before After
Width: 936  |  Height: 1008  |  Size: 62 KiB Width: 1128  |  Height: 1008  |  Size: 65 KiB

+ 16
- 10
docs/index.html View File

@ -34,6 +34,13 @@
Vis.js contains of the following components:
</p>
<div style="text-align: center; float: right; padding-left: 30px;">
<a href="img/vis_overview.png" target="_blank">
<img src="img/vis_overview.png" style="width: 350px; "/><br>
(click for a larger view)
</a>
</div>
<ul>
<li>
<a href="dataset.html"><b>DataSet</b></a>.
@ -59,14 +66,6 @@
</li>
</ul>
<div style="text-align: center;">
<a href="img/vis_overview.png" target="_blank">
<img src="img/vis_overview.png" style="width: 250px; "/><br>
(click for a larger view)
</a>
</div>
<h2 id="Install">Install</h2>
<h3>npm</h3>
@ -163,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;

+ 38
- 4
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;
@ -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>
@ -533,7 +550,7 @@ var options = {
<tr>
<td>showCurrentTime</td>
<td>boolean</td>
<td>false</td>
<td>true</td>
<td>Show a vertical bar at the current time.</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

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

@ -7,7 +7,7 @@
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
<style type="text/css">
.vis.timeline.rootpanel {
.vis.timeline.root {
border: 2px solid purple;
font-family: purisa, 'comic sans', cursive;
font-size: 12pt;
@ -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>

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

@ -28,6 +28,7 @@
<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="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.0",
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",
"repository": {

+ 106
- 130
src/DataSet.js View File

@ -4,7 +4,7 @@
* Usage:
* var dataSet = new DataSet({
* fieldId: '_id',
* convert: {
* type: {
* // ...
* }
* });
@ -30,43 +30,46 @@
* @param {Object} [options] Available options:
* {String} fieldId Field name of the id in the
* items, 'id' by default.
* {Object.<String, String} convert
* {Object.<String, String} type
* A map with field names as key,
* and the field type as value.
* @constructor DataSet
*/
// TODO: add a DataSet constructor DataSet(data, 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
this.convert = {}; // field types by field name
this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function
this._options = options || {};
this._data = {}; // map with data indexed by id
this._fieldId = this._options.fieldId || 'id'; // name of the field containing id
this._type = {}; // internal field types (NOTE: this can differ from this._options.type)
if (this.options.convert) {
for (var field in this.options.convert) {
if (this.options.convert.hasOwnProperty(field)) {
var value = this.options.convert[field];
// all variants of a Date are internally stored as Date, so we can convert
// from everything to everything (also from ISODate to Number for example)
if (this._options.type) {
for (var field in this._options.type) {
if (this._options.type.hasOwnProperty(field)) {
var value = this._options.type[field];
if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
this.convert[field] = 'Date';
this._type[field] = 'Date';
}
else {
this.convert[field] = value;
this._type[field] = value;
}
}
}
}
this.subscribers = {}; // event subscribers
this.internalIds = {}; // internally generated id's
// TODO: deprecated since version 1.1.1 (or 2.0.0?)
if (this._options.convert) {
throw new Error('Option "convert" is deprecated. Use "type" instead.');
}
this._subscribers = {}; // event subscribers
// add initial data when provided
if (data) {
@ -83,11 +86,11 @@ function DataSet (data, options) {
* {Object | null} params
* {String | Number} senderId
*/
DataSet.prototype.on = function on (event, callback) {
var subscribers = this.subscribers[event];
DataSet.prototype.on = function(event, callback) {
var subscribers = this._subscribers[event];
if (!subscribers) {
subscribers = [];
this.subscribers[event] = subscribers;
this._subscribers[event] = subscribers;
}
subscribers.push({
@ -103,10 +106,10 @@ DataSet.prototype.subscribe = DataSet.prototype.on;
* @param {String} event
* @param {function} callback
*/
DataSet.prototype.off = function off(event, callback) {
var subscribers = this.subscribers[event];
DataSet.prototype.off = function(event, callback) {
var subscribers = this._subscribers[event];
if (subscribers) {
this.subscribers[event] = subscribers.filter(function (listener) {
this._subscribers[event] = subscribers.filter(function (listener) {
return (listener.callback != callback);
});
}
@ -128,11 +131,11 @@ DataSet.prototype._trigger = function (event, params, senderId) {
}
var subscribers = [];
if (event in this.subscribers) {
subscribers = subscribers.concat(this.subscribers[event]);
if (event in this._subscribers) {
subscribers = subscribers.concat(this._subscribers[event]);
}
if ('*' in this.subscribers) {
subscribers = subscribers.concat(this.subscribers['*']);
if ('*' in this._subscribers) {
subscribers = subscribers.concat(this._subscribers['*']);
}
for (var i = 0; i < subscribers.length; i++) {
@ -155,7 +158,7 @@ DataSet.prototype.add = function (data, senderId) {
id,
me = this;
if (data instanceof Array) {
if (Array.isArray(data)) {
// Array
for (var i = 0, len = data.length; i < len; i++) {
id = me._addItem(data[i]);
@ -202,11 +205,11 @@ DataSet.prototype.update = function (data, senderId) {
var addedIds = [],
updatedIds = [],
me = this,
fieldId = me.fieldId;
fieldId = me._fieldId;
var addOrUpdate = function (item) {
var id = item[fieldId];
if (me.data[id]) {
if (me._data[id]) {
// update item
id = me._updateItem(item);
updatedIds.push(id);
@ -218,7 +221,7 @@ DataSet.prototype.update = function (data, senderId) {
}
};
if (data instanceof Array) {
if (Array.isArray(data)) {
// Array
for (var i = 0, len = data.length; i < len; i++) {
addOrUpdate(data[i]);
@ -277,9 +280,9 @@ DataSet.prototype.update = function (data, senderId) {
* {Number | String} id The id of an item
* {Number[] | String{}} ids An array with ids of items
* {Object} options An Object with options. Available options:
* {String} [type] Type of data to be returned. Can
* be 'DataTable' or 'Array' (default)
* {Object.<String, String>} [convert]
* {String} [returnType] Type of data to be
* returned. Can be 'DataTable' or 'Array' (default)
* {Object.<String, String>} [type]
* {String[]} [fields] field names to be returned
* {function} [filter] filter items
* {String | function} [order] Order the items by
@ -292,7 +295,6 @@ DataSet.prototype.update = function (data, senderId) {
*/
DataSet.prototype.get = function (args) {
var me = this;
var globalShowInternalIds = this.showInternalIds;
// parse the arguments
var id, ids, options, data;
@ -316,42 +318,35 @@ DataSet.prototype.get = function (args) {
}
// determine the return type
var type;
if (options && options.type) {
type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
var returnType;
if (options && options.returnType) {
returnType = (options.returnType == 'DataTable') ? 'DataTable' : 'Array';
if (data && (type != util.getType(data))) {
if (data && (returnType != util.getType(data))) {
throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
'does not correspond with specified options.type (' + options.type + ')');
}
if (type == 'DataTable' && !util.isDataTable(data)) {
if (returnType == 'DataTable' && !util.isDataTable(data)) {
throw new Error('Parameter "data" must be a DataTable ' +
'when options.type is "DataTable"');
}
}
else if (data) {
type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
returnType = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
}
else {
type = 'Array';
}
// we allow the setting of this value for a single get request.
if (options != undefined) {
if (options.showInternalIds != undefined) {
this.showInternalIds = options.showInternalIds;
}
returnType = 'Array';
}
// build options
var convert = options && options.convert || this.options.convert;
var type = options && options.type || this._options.type;
var filter = options && options.filter;
var items = [], item, itemId, i, len;
// convert items
if (id != undefined) {
// return a single item
item = me._getItem(id, convert);
item = me._getItem(id, type);
if (filter && !filter(item)) {
item = null;
}
@ -359,7 +354,7 @@ DataSet.prototype.get = function (args) {
else if (ids != undefined) {
// return a subset of items
for (i = 0, len = ids.length; i < len; i++) {
item = me._getItem(ids[i], convert);
item = me._getItem(ids[i], type);
if (!filter || filter(item)) {
items.push(item);
}
@ -367,9 +362,9 @@ DataSet.prototype.get = function (args) {
}
else {
// return all items
for (itemId in this.data) {
if (this.data.hasOwnProperty(itemId)) {
item = me._getItem(itemId, convert);
for (itemId in this._data) {
if (this._data.hasOwnProperty(itemId)) {
item = me._getItem(itemId, type);
if (!filter || filter(item)) {
items.push(item);
}
@ -377,9 +372,6 @@ DataSet.prototype.get = function (args) {
}
}
// restore the global value of showInternalIds
this.showInternalIds = globalShowInternalIds;
// order the results
if (options && options.order && id == undefined) {
this._sort(items, options.order);
@ -399,7 +391,7 @@ DataSet.prototype.get = function (args) {
}
// return the results
if (type == 'DataTable') {
if (returnType == 'DataTable') {
var columns = this._getColumnNames(data);
if (id != undefined) {
// append a single item to the data table
@ -445,10 +437,10 @@ DataSet.prototype.get = function (args) {
* @return {Array} ids
*/
DataSet.prototype.getIds = function (options) {
var data = this.data,
var data = this._data,
filter = options && options.filter,
order = options && options.order,
convert = options && options.convert || this.options.convert,
type = options && options.type || this._options.type,
i,
len,
id,
@ -463,7 +455,7 @@ DataSet.prototype.getIds = function (options) {
items = [];
for (id in data) {
if (data.hasOwnProperty(id)) {
item = this._getItem(id, convert);
item = this._getItem(id, type);
if (filter(item)) {
items.push(item);
}
@ -473,16 +465,16 @@ DataSet.prototype.getIds = function (options) {
this._sort(items, order);
for (i = 0, len = items.length; i < len; i++) {
ids[i] = items[i][this.fieldId];
ids[i] = items[i][this._fieldId];
}
}
else {
// create unordered list
for (id in data) {
if (data.hasOwnProperty(id)) {
item = this._getItem(id, convert);
item = this._getItem(id, type);
if (filter(item)) {
ids.push(item[this.fieldId]);
ids.push(item[this._fieldId]);
}
}
}
@ -502,7 +494,7 @@ DataSet.prototype.getIds = function (options) {
this._sort(items, order);
for (i = 0, len = items.length; i < len; i++) {
ids[i] = items[i][this.fieldId];
ids[i] = items[i][this._fieldId];
}
}
else {
@ -510,7 +502,7 @@ DataSet.prototype.getIds = function (options) {
for (id in data) {
if (data.hasOwnProperty(id)) {
item = data[id];
ids.push(item[this.fieldId]);
ids.push(item[this._fieldId]);
}
}
}
@ -523,7 +515,7 @@ DataSet.prototype.getIds = function (options) {
* Execute a callback function for every item in the dataset.
* @param {function} callback
* @param {Object} [options] Available options:
* {Object.<String, String>} [convert]
* {Object.<String, String>} [type]
* {String[]} [fields] filter fields
* {function} [filter] filter items
* {String | function} [order] Order the items by
@ -531,8 +523,8 @@ DataSet.prototype.getIds = function (options) {
*/
DataSet.prototype.forEach = function (callback, options) {
var filter = options && options.filter,
convert = options && options.convert || this.options.convert,
data = this.data,
type = options && options.type || this._options.type,
data = this._data,
item,
id;
@ -542,7 +534,7 @@ DataSet.prototype.forEach = function (callback, options) {
for (var i = 0, len = items.length; i < len; i++) {
item = items[i];
id = item[this.fieldId];
id = item[this._fieldId];
callback(item, id);
}
}
@ -550,7 +542,7 @@ DataSet.prototype.forEach = function (callback, options) {
// unordered
for (id in data) {
if (data.hasOwnProperty(id)) {
item = this._getItem(id, convert);
item = this._getItem(id, type);
if (!filter || filter(item)) {
callback(item, id);
}
@ -563,7 +555,7 @@ DataSet.prototype.forEach = function (callback, options) {
* Map every item in the dataset.
* @param {function} callback
* @param {Object} [options] Available options:
* {Object.<String, String>} [convert]
* {Object.<String, String>} [type]
* {String[]} [fields] filter fields
* {function} [filter] filter items
* {String | function} [order] Order the items by
@ -572,15 +564,15 @@ DataSet.prototype.forEach = function (callback, options) {
*/
DataSet.prototype.map = function (callback, options) {
var filter = options && options.filter,
convert = options && options.convert || this.options.convert,
type = options && options.type || this._options.type,
mappedItems = [],
data = this.data,
data = this._data,
item;
// convert and filter items
for (var id in data) {
if (data.hasOwnProperty(id)) {
item = this._getItem(id, convert);
item = this._getItem(id, type);
if (!filter || filter(item)) {
mappedItems.push(callback(item, id));
}
@ -652,7 +644,7 @@ DataSet.prototype.remove = function (id, senderId) {
var removedIds = [],
i, len, removedId;
if (id instanceof Array) {
if (Array.isArray(id)) {
for (i = 0, len = id.length; i < len; i++) {
removedId = this._remove(id[i]);
if (removedId != null) {
@ -682,17 +674,15 @@ DataSet.prototype.remove = function (id, senderId) {
*/
DataSet.prototype._remove = function (id) {
if (util.isNumber(id) || util.isString(id)) {
if (this.data[id]) {
delete this.data[id];
delete this.internalIds[id];
if (this._data[id]) {
delete this._data[id];
return id;
}
}
else if (id instanceof Object) {
var itemId = id[this.fieldId];
if (itemId && this.data[itemId]) {
delete this.data[itemId];
delete this.internalIds[itemId];
var itemId = id[this._fieldId];
if (itemId && this._data[itemId]) {
delete this._data[itemId];
return itemId;
}
}
@ -705,10 +695,9 @@ DataSet.prototype._remove = function (id) {
* @return {Array} removedIds The ids of all removed items
*/
DataSet.prototype.clear = function (senderId) {
var ids = Object.keys(this.data);
var ids = Object.keys(this._data);
this.data = {};
this.internalIds = {};
this._data = {};
this._trigger('remove', {items: ids}, senderId);
@ -721,7 +710,7 @@ DataSet.prototype.clear = function (senderId) {
* @return {Object | null} item Item containing max value, or null if no items
*/
DataSet.prototype.max = function (field) {
var data = this.data,
var data = this._data,
max = null,
maxField = null;
@ -745,7 +734,7 @@ DataSet.prototype.max = function (field) {
* @return {Object | null} item Item containing max value, or null if no items
*/
DataSet.prototype.min = function (field) {
var data = this.data,
var data = this._data,
min = null,
minField = null;
@ -771,17 +760,18 @@ DataSet.prototype.min = function (field) {
* The returned array is unordered.
*/
DataSet.prototype.distinct = function (field) {
var data = this.data,
values = [],
fieldType = this.options.convert[field],
count = 0;
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 = util.convert(item[field], fieldType);
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,6 +784,12 @@ DataSet.prototype.distinct = function (field) {
}
}
if (fieldType) {
for (i = 0; i < values.length; i++) {
values[i] = util.convert(values[i], fieldType);
}
}
return values;
};
@ -804,11 +800,11 @@ DataSet.prototype.distinct = function (field) {
* @private
*/
DataSet.prototype._addItem = function (item) {
var id = item[this.fieldId];
var id = item[this._fieldId];
if (id != undefined) {
// check whether this id is already taken
if (this.data[id]) {
if (this._data[id]) {
// item already exists
throw new Error('Cannot add item: item with id ' + id + ' already exists');
}
@ -816,18 +812,17 @@ DataSet.prototype._addItem = function (item) {
else {
// generate an id
id = util.randomUUID();
item[this.fieldId] = id;
this.internalIds[id] = item;
item[this._fieldId] = id;
}
var d = {};
for (var field in item) {
if (item.hasOwnProperty(field)) {
var fieldType = this.convert[field]; // type may be undefined
var fieldType = this._type[field]; // type may be undefined
d[field] = util.convert(item[field], fieldType);
}
}
this.data[id] = d;
this._data[id] = d;
return id;
};
@ -835,31 +830,26 @@ DataSet.prototype._addItem = function (item) {
/**
* Get an item. Fields can be converted to a specific type
* @param {String} id
* @param {Object.<String, String>} [convert] field types to convert
* @param {Object.<String, String>} [types] field types to convert
* @return {Object | null} item
* @private
*/
DataSet.prototype._getItem = function (id, convert) {
DataSet.prototype._getItem = function (id, types) {
var field, value;
// get the item from the dataset
var raw = this.data[id];
var raw = this._data[id];
if (!raw) {
return null;
}
// convert the items field types
var converted = {},
fieldId = this.fieldId,
internalIds = this.internalIds;
if (convert) {
var converted = {};
if (types) {
for (field in raw) {
if (raw.hasOwnProperty(field)) {
value = raw[field];
// output all fields, except internal ids
if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
converted[field] = util.convert(value, convert[field]);
}
converted[field] = util.convert(value, types[field]);
}
}
}
@ -868,10 +858,7 @@ DataSet.prototype._getItem = function (id, convert) {
for (field in raw) {
if (raw.hasOwnProperty(field)) {
value = raw[field];
// output all fields, except internal ids
if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
converted[field] = value;
}
converted[field] = value;
}
}
}
@ -887,11 +874,11 @@ DataSet.prototype._getItem = function (id, convert) {
* @private
*/
DataSet.prototype._updateItem = function (item) {
var id = item[this.fieldId];
var id = item[this._fieldId];
if (id == undefined) {
throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
}
var d = this.data[id];
var d = this._data[id];
if (!d) {
// item doesn't exist
throw new Error('Cannot update item: no item with id ' + id + ' found');
@ -900,7 +887,7 @@ DataSet.prototype._updateItem = function (item) {
// merge with current item
for (var field in item) {
if (item.hasOwnProperty(field)) {
var fieldType = this.convert[field]; // type may be undefined
var fieldType = this._type[field]; // type may be undefined
d[field] = util.convert(item[field], fieldType);
}
}
@ -908,17 +895,6 @@ DataSet.prototype._updateItem = function (item) {
return id;
};
/**
* check if an id is an internal or external id
* @param id
* @returns {boolean}
* @private
*/
DataSet.prototype.isInternalId = function(id) {
return (id in this.internalIds);
};
/**
* Get an array with the column names of a Google DataTable
* @param {DataTable} dataTable

+ 35
- 37
src/DataView.js View File

@ -9,13 +9,11 @@
* @constructor DataView
*/
function DataView (data, options) {
this.id = util.randomUUID();
this.data = null;
this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
this.options = options || {};
this.fieldId = 'id'; // name of the field containing id
this.subscribers = {}; // event subscribers
this._data = null;
this._ids = {}; // ids of the items currently in memory (just contains a boolean true)
this._options = options || {};
this._fieldId = 'id'; // name of the field containing id
this._subscribers = {}; // event subscribers
var me = this;
this.listener = function () {
@ -33,44 +31,44 @@ function DataView (data, options) {
* @param {DataSet | DataView} data
*/
DataView.prototype.setData = function (data) {
var ids, dataItems, i, len;
var ids, i, len;
if (this.data) {
if (this._data) {
// unsubscribe from current dataset
if (this.data.unsubscribe) {
this.data.unsubscribe('*', this.listener);
if (this._data.unsubscribe) {
this._data.unsubscribe('*', this.listener);
}
// trigger a remove of all items in memory
ids = [];
for (var id in this.ids) {
if (this.ids.hasOwnProperty(id)) {
for (var id in this._ids) {
if (this._ids.hasOwnProperty(id)) {
ids.push(id);
}
}
this.ids = {};
this._ids = {};
this._trigger('remove', {items: ids});
}
this.data = data;
this._data = data;
if (this.data) {
if (this._data) {
// update fieldId
this.fieldId = this.options.fieldId ||
(this.data && this.data.options && this.data.options.fieldId) ||
this._fieldId = this._options.fieldId ||
(this._data && this._data.options && this._data.options.fieldId) ||
'id';
// trigger an add of all added items
ids = this.data.getIds({filter: this.options && this.options.filter});
ids = this._data.getIds({filter: this._options && this._options.filter});
for (i = 0, len = ids.length; i < len; i++) {
id = ids[i];
this.ids[id] = true;
this._ids[id] = true;
}
this._trigger('add', {items: ids});
// subscribe to new dataset
if (this.data.on) {
this.data.on('*', this.listener);
if (this._data.on) {
this._data.on('*', this.listener);
}
}
};
@ -128,12 +126,12 @@ DataView.prototype.get = function (args) {
}
// extend the options with the default options and provided options
var viewOptions = util.extend({}, this.options, options);
var viewOptions = util.extend({}, this._options, options);
// create a combined filter method when needed
if (this.options.filter && options && options.filter) {
if (this._options.filter && options && options.filter) {
viewOptions.filter = function (item) {
return me.options.filter(item) && options.filter(item);
return me._options.filter(item) && options.filter(item);
}
}
@ -145,7 +143,7 @@ DataView.prototype.get = function (args) {
getArguments.push(viewOptions);
getArguments.push(data);
return this.data && this.data.get.apply(this.data, getArguments);
return this._data && this._data.get.apply(this._data, getArguments);
};
/**
@ -159,8 +157,8 @@ DataView.prototype.get = function (args) {
DataView.prototype.getIds = function (options) {
var ids;
if (this.data) {
var defaultFilter = this.options.filter;
if (this._data) {
var defaultFilter = this._options.filter;
var filter;
if (options && options.filter) {
@ -177,7 +175,7 @@ DataView.prototype.getIds = function (options) {
filter = defaultFilter;
}
ids = this.data.getIds({
ids = this._data.getIds({
filter: filter,
order: options && options.order
});
@ -201,7 +199,7 @@ DataView.prototype.getIds = function (options) {
DataView.prototype._onEvent = function (event, params, senderId) {
var i, len, id, item,
ids = params && params.items,
data = this.data,
data = this._data,
added = [],
updated = [],
removed = [];
@ -214,7 +212,7 @@ DataView.prototype._onEvent = function (event, params, senderId) {
id = ids[i];
item = this.get(id);
if (item) {
this.ids[id] = true;
this._ids[id] = true;
added.push(id);
}
}
@ -229,17 +227,17 @@ DataView.prototype._onEvent = function (event, params, senderId) {
item = this.get(id);
if (item) {
if (this.ids[id]) {
if (this._ids[id]) {
updated.push(id);
}
else {
this.ids[id] = true;
this._ids[id] = true;
added.push(id);
}
}
else {
if (this.ids[id]) {
delete this.ids[id];
if (this._ids[id]) {
delete this._ids[id];
removed.push(id);
}
else {
@ -254,8 +252,8 @@ DataView.prototype._onEvent = function (event, params, senderId) {
// filter the ids of the removed items
for (i = 0, len = ids.length; i < len; i++) {
id = ids[i];
if (this.ids[id]) {
delete this.ids[id];
if (this._ids[id]) {
delete this._ids[id];
removed.push(id);
}
}

+ 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}};
}

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

@ -23,14 +23,14 @@ function Graph (container, data, options) {
this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
this.physicsDiscreteStepsize = 0.65; // discrete stepsize of the simulation
this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation
this.stabilize = true; // stabilize before displaying the graph
this.selectable = true;
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 +166,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).",
@ -186,12 +188,14 @@ function Graph (container, data, options) {
background: '#FFFFC6'
}
},
moveable: true,
dragGraph: true,
dragNodes: true,
zoomable: true,
hover: false
};
this.hoverObj = {nodes:{},edges:{}};
// Node variables
var graph = this;
this.groups = new Groups(); // object with groups
@ -224,7 +228,6 @@ function Graph (container, data, options) {
this._setScale(1);
this.setOptions(options);
// other vars
this.freezeSimulation = false;// freeze the simulation
this.cachedFunctions = {};
@ -528,7 +531,8 @@ Graph.prototype.setOptions = function (options) {
if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;}
if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;}
if (options.moveable !== undefined) {this.constants.moveable = options.moveable;}
if (options.dragGraph !== undefined) {this.constants.dragGraph = options.dragGraph;}
if (options.dragNodes !== undefined) {this.constants.dragNodes = options.dragNodes;}
if (options.zoomable !== undefined) {this.constants.zoomable = options.zoomable;}
if (options.hover !== undefined) {this.constants.hover = options.hover;}
@ -548,6 +552,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;
}
@ -642,7 +650,7 @@ Graph.prototype.setOptions = function (options) {
this.constants.dataManipulation[prop] = options.dataManipulation[prop];
}
}
this.editMode = this.constants.dataManipulation.initiallyVisible;
this.editMode = this.constants.dataManipulation.initiallyVisible;
}
else if (options.dataManipulation !== undefined) {
this.constants.dataManipulation.enabled = false;
@ -956,7 +964,7 @@ Graph.prototype._handleOnDrag = function(event) {
var me = this,
drag = this.drag,
selection = drag.selection;
if (selection && selection.length) {
if (selection && selection.length && this.constants.dragNodes == true) {
// calculate delta's and new location
var deltaX = pointer.x - drag.pointer.x,
deltaY = pointer.y - drag.pointer.y;
@ -981,7 +989,7 @@ Graph.prototype._handleOnDrag = function(event) {
}
}
else {
if (this.constants.moveable == true) {
if (this.constants.dragGraph == true) {
// move the graph
var diffX = pointer.x - this.drag.pointer.x;
var diffY = pointer.y - this.drag.pointer.y;
@ -1677,7 +1685,6 @@ Graph.prototype._updateValueRange = function(obj) {
*/
Graph.prototype.redraw = function() {
this.setSize(this.width, this.height);
this._redraw();
};
@ -1709,6 +1716,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");
@ -1893,6 +1901,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

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

@ -30,9 +30,9 @@ function Node(properties, imagelist, grouplist, constants) {
this.edges = []; // all edges connected to this node
this.dynamicEdges = [];
this.reroutedEdges = {};
this.group = constants.nodes.group;
this.fontSize = constants.nodes.fontSize;
this.group = constants.nodes.group;
this.fontSize = Number(constants.nodes.fontSize);
this.fontFace = constants.nodes.fontFace;
this.fontColor = constants.nodes.fontColor;
this.fontDrawThreshold = 3;
@ -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
*

+ 16
- 2
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
@ -458,7 +473,6 @@ var SelectionMixin = {
* @private
*/
_handleTouch : function(pointer) {
},

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

@ -20,8 +20,6 @@ var vis = {
},
Component: Component,
Panel: Panel,
RootPanel: RootPanel,
ItemSet: ItemSet,
TimeAxis: TimeAxis
},

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

@ -3,57 +3,81 @@
* 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 {RootPanel} root Root panel, used to subscribe to events
* @param {Panel} parent Parent panel, used to attach to the DOM
* @param {{dom: Object, domProps: Object, emitter: Emitter}} body
* @param {Object} [options] See description at Range.setOptions
*/
function Range(root, parent, options) {
this.id = util.randomUUID();
this.start = null; // Number
this.end = null; // Number
function Range(body, options) {
var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
this.start = now.clone().add('days', -3).valueOf(); // Number
this.end = now.clone().add('days', 4).valueOf(); // Number
this.body = body;
// default options
this.defaultOptions = {
start: null,
end: null,
direction: 'horizontal', // 'horizontal' or 'vertical'
moveable: true,
zoomable: true,
min: null,
max: null,
zoomMin: 10, // milliseconds
zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds
};
this.options = util.extend({}, this.defaultOptions);
this.root = root;
this.parent = parent;
this.options = options || {};
this.props = {
touch: {}
};
// 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));
this.body.emitter.on('dragstart', this._onDragStart.bind(this));
this.body.emitter.on('drag', this._onDrag.bind(this));
this.body.emitter.on('dragend', this._onDragEnd.bind(this));
// ignore dragging when holding
this.root.on('hold', this._onHold.bind(this));
this.body.emitter.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
this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this));
this.body.emitter.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.body.emitter.on('touch', this._onTouch.bind(this));
this.body.emitter.on('pinch', this._onPinch.bind(this));
this.setOptions(options);
}
// turn Range into an event emitter
Emitter(Range.prototype);
Range.prototype = new Component();
/**
* Set options for the range controller
* @param {Object} options Available options:
* {Number | Date | String} start Start date for the range
* {Number | Date | String} end End date for the range
* {Number} min Minimum value for start
* {Number} max Maximum value for end
* {Number} zoomMin Set a minimum value for
* (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) {
util.extend(this.options, options);
// re-apply range with new limitations
if (this.start !== null && this.end !== null) {
this.setRange(this.start, this.end);
if (options) {
// copy the options that we know
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
this.setRange(options.start, options.end);
}
}
};
@ -80,8 +104,8 @@ Range.prototype.setRange = function(start, end) {
start: new Date(this.start),
end: new Date(this.end)
};
this.emit('rangechange', params);
this.emit('rangechanged', params);
this.body.emitter.emit('rangechange', params);
this.body.emitter.emit('rangechanged', params);
}
};
@ -240,77 +264,75 @@ 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;
var frame = this.parent.frame;
if (frame) {
frame.style.cursor = 'move';
if (this.body.dom.root) {
this.body.dom.root.style.cursor = 'move';
}
};
/**
* 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),
width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
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.emit('rangechange', {
this.body.emitter.emit('rangechange', {
start: new Date(this.start),
end: new Date(this.end)
});
};
/**
* 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;
if (!this.props.touch.allowDragging) return;
// TODO: reckon with option movable
if (this.parent.frame) {
this.parent.frame.style.cursor = 'auto';
if (this.body.dom.root) {
this.body.dom.root.style.cursor = 'auto';
}
// fire a rangechanged event
this.emit('rangechanged', {
this.body.emitter.emit('rangechanged', {
start: new Date(this.start),
end: new Date(this.end)
});
@ -323,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;
@ -353,7 +376,7 @@ Range.prototype._onMouseWheel = function(event) {
// calculate center, the date to zoom around
var gesture = util.fakeGesture(this, event),
pointer = getPointer(gesture.center, this.parent.frame),
pointer = getPointer(gesture.center, this.body.dom.center),
pointerDate = this._pointerToDate(pointer);
this.zoom(scale, pointerDate);
@ -369,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;
};
/**
@ -387,7 +403,7 @@ Range.prototype._onTouch = function (event) {
* @private
*/
Range.prototype._onHold = function () {
touchParams.ignore = true;
this.props.touch.allowDragging = false;
};
/**
@ -396,25 +412,22 @@ Range.prototype._onHold = function () {
* @private
*/
Range.prototype._onPinch = function (event) {
var direction = this.options.direction;
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.parent.frame);
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),
center = getPointer(event.gesture.center, this.parent.frame),
date = this._pointerToDate(this.parent, center),
delta = date - initDate; // TODO: utilize delta
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);
@ -434,12 +447,12 @@ Range.prototype._pointerToDate = function (pointer) {
validateDirection(direction);
if (direction == 'horizontal') {
var width = this.parent.width;
var width = this.body.domProps.center.width;
conversion = this.conversion(width);
return pointer.x / conversion.scale + conversion.offset;
}
else {
var height = this.parent.height;
var height = this.body.domProps.center.height;
conversion = this.conversion(height);
return pointer.y / conversion.scale + conversion.offset;
}

+ 519
- 430
src/timeline/Timeline.js
File diff suppressed because it is too large
View File


+ 19
- 53
src/timeline/component/Component.js View File

@ -1,73 +1,38 @@
/**
* Prototype for visual components
* @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body]
* @param {Object} [options]
*/
function Component () {
this.id = null;
this.parent = null;
this.childs = null;
function Component (body, options) {
this.options = null;
this.top = 0;
this.left = 0;
this.width = 0;
this.height = 0;
this.props = null;
}
// Turn the Component into an event emitter
Emitter(Component.prototype);
/**
* Set parameters for the frame. Parameters will be merged in current parameter
* set.
* @param {Object} options Available parameters:
* {String | function} [className]
* {String | Number | function} [left]
* {String | Number | function} [top]
* {String | Number | function} [width]
* {String | Number | function} [height]
* Set options for the component. The new options will be merged into the
* current options.
* @param {Object} options
*/
Component.prototype.setOptions = function setOptions(options) {
Component.prototype.setOptions = function(options) {
if (options) {
util.extend(this.options, options);
this.repaint();
}
};
/**
* Get an option value by name
* The function will first check this.options object, and else will check
* this.defaultOptions.
* @param {String} name
* @return {*} value
*/
Component.prototype.getOption = function getOption(name) {
var value;
if (this.options) {
value = this.options[name];
}
if (value === undefined && this.defaultOptions) {
value = this.defaultOptions[name];
}
return value;
};
/**
* Get the frame element of the component, the outer HTML DOM element.
* @returns {HTMLElement | null} frame
* Repaint the component
* @return {boolean} Returns true if the component is resized
*/
Component.prototype.getFrame = function getFrame() {
Component.prototype.redraw = function() {
// should be implemented by the component
return null;
return false;
};
/**
* Repaint the component
* @return {boolean} Returns true if the component is resized
* Destroy the component. Cleanup DOM and event listeners
*/
Component.prototype.repaint = function repaint() {
Component.prototype.destroy = function() {
// should be implemented by the component
return false;
};
/**
@ -76,11 +41,12 @@ Component.prototype.repaint = function repaint() {
* @return {Boolean} Returns true if the component is resized
* @protected
*/
Component.prototype._isResized = function _isResized() {
var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height);
Component.prototype._isResized = function() {
var resized = (this.props._previousWidth !== this.props.width ||
this.props._previousHeight !== this.props.height);
this._previousWidth = this.width;
this._previousHeight = this.height;
this.props._previousWidth = this.props.width;
this.props._previousHeight = this.props.height;
return resized;
};

+ 57
- 25
src/timeline/component/CurrentTime.js View File

@ -1,33 +1,33 @@
/**
* A current time bar
* @param {Range} range
* @param {{range: Range, dom: Object, domProps: Object}} body
* @param {Object} [options] Available parameters:
* {Boolean} [showCurrentTime]
* @constructor CurrentTime
* @extends Component
*/
function CurrentTime (range, options) {
this.id = util.randomUUID();
function CurrentTime (body, options) {
this.body = body;
this.range = range;
this.options = options || {};
// default options
this.defaultOptions = {
showCurrentTime: false
showCurrentTime: true
};
this.options = util.extend({}, this.defaultOptions);
this._create();
this.setOptions(options);
}
CurrentTime.prototype = new Component();
CurrentTime.prototype.setOptions = Component.prototype.setOptions;
/**
* Create the HTML DOM for the current time bar
* @private
*/
CurrentTime.prototype._create = function _create () {
CurrentTime.prototype._create = function() {
var bar = document.createElement('div');
bar.className = 'currenttime';
bar.style.position = 'absolute';
@ -38,25 +38,57 @@ CurrentTime.prototype._create = function _create () {
};
/**
* Get the frame element of the current time bar
* @returns {HTMLElement} frame
* 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:
* {boolean} [showCurrentTime]
*/
CurrentTime.prototype.getFrame = function getFrame() {
return this.bar;
CurrentTime.prototype.setOptions = function(options) {
if (options) {
// copy all options that we know
util.selectiveExtend(['showCurrentTime'], this.options, options);
}
};
/**
* Repaint the component
* @return {boolean} Returns true if the component is resized
*/
CurrentTime.prototype.repaint = function repaint() {
var parent = this.parent;
var now = new Date();
var x = this.options.toScreen(now);
this.bar.style.left = x + 'px';
this.bar.title = 'Current time: ' + now;
CurrentTime.prototype.redraw = function() {
if (this.options.showCurrentTime) {
var parent = this.body.dom.backgroundVertical;
if (this.bar.parentNode != parent) {
// attach to the dom
if (this.bar.parentNode) {
this.bar.parentNode.removeChild(this.bar);
}
parent.appendChild(this.bar);
this.start();
}
var now = new Date();
var x = this.body.util.toScreen(now);
this.bar.style.left = x + 'px';
this.bar.title = 'Current time: ' + now;
}
else {
// remove the line from the DOM
if (this.bar.parentNode) {
this.bar.parentNode.removeChild(this.bar);
}
this.stop();
}
return false;
};
@ -64,19 +96,19 @@ CurrentTime.prototype.repaint = function repaint() {
/**
* Start auto refreshing the current time bar
*/
CurrentTime.prototype.start = function start() {
CurrentTime.prototype.start = function() {
var me = this;
function update () {
me.stop();
// determine interval to refresh
var scale = me.range.conversion(me.parent.width).scale;
var scale = me.body.range.conversion(me.body.domProps.center.width).scale;
var interval = 1 / scale / 10;
if (interval < 30) interval = 30;
if (interval > 1000) interval = 1000;
me.repaint();
me.redraw();
// start a timer to adjust for the new time
me.currentTimeTimer = setTimeout(update, interval);
@ -88,7 +120,7 @@ CurrentTime.prototype.start = function start() {
/**
* Stop auto refreshing the current time bar
*/
CurrentTime.prototype.stop = function stop() {
CurrentTime.prototype.stop = function() {
if (this.currentTimeTimer !== undefined) {
clearTimeout(this.currentTimeTimer);
delete this.currentTimeTimer;

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

@ -1,35 +1,49 @@
/**
* A custom time bar
* @param {{range: Range, dom: Object}} body
* @param {Object} [options] Available parameters:
* {Boolean} [showCustomTime]
* @constructor CustomTime
* @extends Component
*/
function CustomTime (options) {
this.id = util.randomUUID();
function CustomTime (body, options) {
this.body = body;
this.options = options || {};
// default options
this.defaultOptions = {
showCustomTime: false
};
this.options = util.extend({}, this.defaultOptions);
this.customTime = new Date();
this.eventParams = {}; // stores state parameters while dragging the bar
// create the DOM
this._create();
this.setOptions(options);
}
CustomTime.prototype = new Component();
CustomTime.prototype.setOptions = Component.prototype.setOptions;
/**
* Set options for the component. Options will be merged in current options.
* @param {Object} options Available parameters:
* {boolean} [showCustomTime]
*/
CustomTime.prototype.setOptions = function(options) {
if (options) {
// copy all options that we know
util.selectiveExtend(['showCustomTime'], this.options, options);
}
};
/**
* Create the DOM for the custom time
* @private
*/
CustomTime.prototype._create = function _create () {
CustomTime.prototype._create = function() {
var bar = document.createElement('div');
bar.className = 'customtime';
bar.style.position = 'absolute';
@ -55,22 +69,44 @@ CustomTime.prototype._create = function _create () {
};
/**
* Get the frame element of the custom time bar
* @returns {HTMLElement} frame
* Destroy the CustomTime bar
*/
CustomTime.prototype.getFrame = function getFrame() {
return this.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
*/
CustomTime.prototype.repaint = function () {
var x = this.options.toScreen(this.customTime);
this.bar.style.left = x + 'px';
this.bar.title = 'Time: ' + this.customTime;
CustomTime.prototype.redraw = function () {
if (this.options.showCustomTime) {
var parent = this.body.dom.backgroundVertical;
if (this.bar.parentNode != parent) {
// attach to the dom
if (this.bar.parentNode) {
this.bar.parentNode.removeChild(this.bar);
}
parent.appendChild(this.bar);
}
var x = this.body.util.toScreen(this.customTime);
this.bar.style.left = x + 'px';
this.bar.title = 'Time: ' + this.customTime;
}
else {
// remove the line from the DOM
if (this.bar.parentNode) {
this.bar.parentNode.removeChild(this.bar);
}
}
return false;
};
@ -81,7 +117,7 @@ CustomTime.prototype.repaint = function () {
*/
CustomTime.prototype.setCustomTime = function(time) {
this.customTime = new Date(time.valueOf());
this.repaint();
this.redraw();
};
/**
@ -114,13 +150,13 @@ CustomTime.prototype._onDrag = function (event) {
if (!this.eventParams.dragging) return;
var deltaX = event.gesture.deltaX,
x = this.options.toScreen(this.eventParams.customTime) + deltaX,
time = this.options.toTime(x);
x = this.body.util.toScreen(this.eventParams.customTime) + deltaX,
time = this.body.util.toTime(x);
this.setCustomTime(time);
// fire a timechange event
this.emit('timechange', {
this.body.emitter.emit('timechange', {
time: new Date(this.customTime.valueOf())
});
@ -137,7 +173,7 @@ CustomTime.prototype._onDragEnd = function (event) {
if (!this.eventParams.dragging) return;
// fire a timechanged event
this.emit('timechanged', {
this.body.emitter.emit('timechanged', {
time: new Date(this.customTime.valueOf())
});

+ 47
- 50
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
@ -65,7 +68,7 @@ Group.prototype._create = function() {
* Set the group data for this group
* @param {Object} data Group data, can contain properties content and className
*/
Group.prototype.setData = function setData(data) {
Group.prototype.setData = function(data) {
// update contents
var content = data && data.content;
if (content instanceof Element) {
@ -78,42 +81,34 @@ Group.prototype.setData = function setData(data) {
this.dom.inner.innerHTML = this.groupId;
}
if (!this.dom.inner.firstChild) {
util.addClassName(this.dom.inner, 'hidden');
}
else {
util.removeClassName(this.dom.inner, 'hidden');
}
// 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);
}
};
/**
* Get the foreground container element
* @return {HTMLElement} foreground
*/
Group.prototype.getForeground = function getForeground() {
return this.dom.foreground;
};
/**
* Get the background container element
* @return {HTMLElement} background
*/
Group.prototype.getBackground = function getBackground() {
return this.dom.background;
};
/**
* 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() {
Group.prototype.getLabelWidth = function() {
return this.props.label.width;
};
@ -125,7 +120,7 @@ Group.prototype.getLabelWidth = function getLabelWidth() {
* @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) {
Group.prototype.redraw = function(range, margin, restack) {
var resized = false;
this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
@ -138,7 +133,7 @@ Group.prototype.repaint = function repaint(range, margin, restack) {
util.forEach(this.items, function (item) {
item.dirty = true;
if (item.displayed) item.repaint();
if (item.displayed) item.redraw();
});
restack = true;
@ -151,10 +146,6 @@ Group.prototype.repaint = function repaint(range, margin, restack) {
else { // no stacking
stack.nostack(this.visibleItems, margin);
}
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;
@ -188,34 +179,40 @@ Group.prototype.repaint = function repaint(range, margin, restack) {
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
for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
var item = this.visibleItems[i];
item.repositionY();
}
return resized;
};
/**
* Show this group: attach to the DOM
*/
Group.prototype.show = function show() {
Group.prototype.show = function() {
if (!this.dom.label.parentNode) {
this.itemSet.getLabelSet().appendChild(this.dom.label);
this.itemSet.dom.labelSet.appendChild(this.dom.label);
}
if (!this.dom.foreground.parentNode) {
this.itemSet.getForeground().appendChild(this.dom.foreground);
this.itemSet.dom.foreground.appendChild(this.dom.foreground);
}
if (!this.dom.background.parentNode) {
this.itemSet.getBackground().appendChild(this.dom.background);
this.itemSet.dom.background.appendChild(this.dom.background);
}
if (!this.dom.axis.parentNode) {
this.itemSet.getAxis().appendChild(this.dom.axis);
this.itemSet.dom.axis.appendChild(this.dom.axis);
}
};
/**
* Hide this group: remove from the DOM
*/
Group.prototype.hide = function hide() {
Group.prototype.hide = function() {
var label = this.dom.label;
if (label.parentNode) {
label.parentNode.removeChild(label);
@ -241,12 +238,12 @@ Group.prototype.hide = function hide() {
* Add an item to the group
* @param {Item} item
*/
Group.prototype.add = function add(item) {
Group.prototype.add = function(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
var range = this.itemSet.body.range; // TODO: not nice accessing the range like this
this._checkIfVisible(item, this.visibleItems, range);
}
};
@ -255,7 +252,7 @@ Group.prototype.add = function add(item) {
* Remove an item from the group
* @param {Item} item
*/
Group.prototype.remove = function remove(item) {
Group.prototype.remove = function(item) {
delete this.items[item.id];
item.setParent(this.itemSet);
@ -270,14 +267,14 @@ Group.prototype.remove = function remove(item) {
* Remove an item from the corresponding DataSet
* @param {Item} item
*/
Group.prototype.removeFromDataSet = function removeFromDataSet(item) {
Group.prototype.removeFromDataSet = function(item) {
this.itemSet.removeItem(item.id);
};
/**
* Reorder the items
*/
Group.prototype.order = function order() {
Group.prototype.order = function() {
var array = util.toArray(this.items);
this.orderedItems.byStart = array;
this.orderedItems.byEnd = this._constructByEndArray(array);
@ -292,7 +289,7 @@ Group.prototype.order = function order() {
* @returns {ItemRange[]}
* @private
*/
Group.prototype._constructByEndArray = function _constructByEndArray(array) {
Group.prototype._constructByEndArray = function(array) {
var endArray = [];
for (var i = 0; i < array.length; i++) {
@ -311,7 +308,7 @@ Group.prototype._constructByEndArray = function _constructByEndArray(array) {
* @return {Item[]} visibleItems The new visible items.
* @private
*/
Group.prototype._updateVisibleItems = function _updateVisibleItems(orderedItems, visibleItems, range) {
Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) {
var initialPosByStart,
newVisibleItems = [],
i;
@ -374,7 +371,7 @@ Group.prototype._updateVisibleItems = function _updateVisibleItems(orderedItems,
* @returns {number}
* @private
*/
Group.prototype._binarySearch = function _binarySearch(orderedItems, range, byEnd) {
Group.prototype._binarySearch = function(orderedItems, range, byEnd) {
var array = [];
var byTime = byEnd ? 'end' : 'start';
if (byEnd == true) {array = orderedItems.byEnd; }
@ -435,7 +432,7 @@ Group.prototype._binarySearch = function _binarySearch(orderedItems, range, byEn
* @returns {boolean}
* @private
*/
Group.prototype._checkIfInvisible = function _checkIfInvisible(item, visibleItems, range) {
Group.prototype._checkIfInvisible = function(item, visibleItems, range) {
if (item.isVisible(range)) {
if (!item.displayed) item.show();
item.repositionX();
@ -460,7 +457,7 @@ Group.prototype._checkIfInvisible = function _checkIfInvisible(item, visibleItem
* @param {{start:number, end:number}} range
* @private
*/
Group.prototype._checkIfVisible = function _checkIfVisible(item, visibleItems, range) {
Group.prototype._checkIfVisible = function(item, visibleItems, range) {
if (item.isVisible(range)) {
if (!item.displayed) item.show();
// reposition item horizontally

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


+ 0
- 170
src/timeline/component/Panel.js View File

@ -1,170 +0,0 @@
/**
* A panel can contain components
* @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 Panel
* @extends Component
*/
function Panel(options) {
this.id = util.randomUUID();
this.parent = null;
this.childs = [];
this.options = options || {};
// create frame
this.frame = (typeof document !== 'undefined') ? document.createElement('div') : null;
}
Panel.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]
*/
Panel.prototype.setOptions = Component.prototype.setOptions;
/**
* Get the outer frame of the panel
* @returns {HTMLElement} frame
*/
Panel.prototype.getFrame = function () {
return this.frame;
};
/**
* Append a child to the panel
* @param {Component} child
*/
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 {
this.frame.appendChild(frame);
}
}
}
};
/**
* 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);
}
}
};
/**
* Test whether the panel contains given child
* @param {Component} child
*/
Panel.prototype.hasChild = function (child) {
var index = this.childs.indexOf(child);
return (index != -1);
};
/**
* Repaint the component
* @return {boolean} Returns true if the component was resized since previous repaint
*/
Panel.prototype.repaint = function () {
var asString = util.option.asString,
options = this.options,
frame = this.getFrame();
// 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, '');
// get actual size
this.top = this.frame.offsetTop;
this.left = this.frame.offsetLeft;
this.width = this.frame.offsetWidth;
this.height = this.frame.offsetHeight;
};

+ 0
- 176
src/timeline/component/RootPanel.js View File

@ -1,176 +0,0 @@
/**
* A root panel can hold components. The root panel must be initialized with
* a DOM element as container.
* @param {HTMLElement} container
* @param {Object} [options] Available parameters: see RootPanel.setOptions.
* @constructor RootPanel
* @extends Panel
*/
function RootPanel(container, options) {
this.id = util.randomUUID();
this.container = container;
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 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
];
events.forEach(function (event) {
var listener = function () {
var args = [event].concat(Array.prototype.slice.call(arguments, 0));
me.emit.apply(me, args);
};
me.hammer.on(event, listener);
me.listeners[event] = listener;
});
};
/**
* 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]
* {Boolean | function} [autoResize]
*/
RootPanel.prototype.setOptions = function setOptions(options) {
if (options) {
util.extend(this.options, options);
this.repaint();
this._initWatch();
}
};
/**
* Get the frame of the root panel
*/
RootPanel.prototype.getFrame = function getFrame() {
return this.frame;
};
/**
* Repaint the root panel
*/
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.frame.style.minHeight = util.option.asSize(this.options.minHeight, '');
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);
}
};
/**
* Initialize watching when option autoResize is true
* @private
*/
RootPanel.prototype._initWatch = function _initWatch() {
var autoResize = this.getOption('autoResize');
if (autoResize) {
this._watch();
}
else {
this._unwatch();
}
};
/**
* Watch for changes in the size of the frame. On resize, the Panel will
* automatically redraw itself.
* @private
*/
RootPanel.prototype._watch = function _watch() {
var me = this;
this._unwatch();
var checkSize = function checkSize() {
var autoResize = me.getOption('autoResize');
if (!autoResize) {
// stop watching when the option autoResize is changed to false
me._unwatch();
return;
}
if (me.frame) {
// check whether the frame is resized
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?
}
}
};
// TODO: automatically cleanup the event listener when the frame is deleted
util.addEventListener(window, 'resize', checkSize);
this.watchTimer = setInterval(checkSize, 1000);
};
/**
* Stop watching for a resize of the frame.
* @private
*/
RootPanel.prototype._unwatch = function _unwatch() {
if (this.watchTimer) {
clearInterval(this.watchTimer);
this.watchTimer = undefined;
}
// TODO: remove event listener on window.resize
};

+ 112
- 169
src/timeline/component/TimeAxis.js View File

@ -1,14 +1,14 @@
/**
* A horizontal time axis
* @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
* @param {Object} [options] See TimeAxis.setOptions for the available
* options.
* @constructor TimeAxis
* @extends Component
*/
function TimeAxis (options) {
this.id = util.randomUUID();
function TimeAxis (body, options) {
this.dom = {
foreground: null,
majorLines: [],
majorTexts: [],
minorLines: [],
@ -29,121 +29,124 @@ function TimeAxis (options) {
lineTop: 0
};
this.options = options || {};
this.defaultOptions = {
orientation: 'bottom', // supported: 'top', 'bottom'
// TODO: implement timeaxis orientations 'left' and 'right'
showMinorLabels: true,
showMajorLabels: true
};
this.options = util.extend({}, this.defaultOptions);
this.range = null;
this.body = body;
// create the HTML DOM
this._create();
this.setOptions(options);
}
TimeAxis.prototype = new Component();
// TODO: comment options
TimeAxis.prototype.setOptions = Component.prototype.setOptions;
/**
* Create the HTML DOM for the TimeAxis
* Set options for the TimeAxis.
* Parameters will be merged in current options.
* @param {Object} options Available options:
* {string} [orientation]
* {boolean} [showMinorLabels]
* {boolean} [showMajorLabels]
*/
TimeAxis.prototype._create = function _create() {
this.frame = document.createElement('div');
TimeAxis.prototype.setOptions = function(options) {
if (options) {
// copy all options that we know
util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels'], this.options, options);
}
};
/**
* Set a range (start and end)
* @param {Range | Object} range A Range or an object containing start and end.
* Create the HTML DOM for the TimeAxis
*/
TimeAxis.prototype.setRange = function (range) {
if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
throw new TypeError('Range must be an instance of Range, ' +
'or an object containing start and end.');
}
this.range = range;
TimeAxis.prototype._create = function() {
this.dom.foreground = document.createElement('div');
this.dom.background = document.createElement('div');
this.dom.foreground.className = 'timeaxis foreground';
this.dom.background.className = 'timeaxis background';
};
/**
* Get the outer frame of the time axis
* @return {HTMLElement} frame
* Destroy the TimeAxis
*/
TimeAxis.prototype.getFrame = function getFrame() {
return this.frame;
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
*/
TimeAxis.prototype.repaint = function () {
var asSize = util.option.asSize,
options = this.options,
TimeAxis.prototype.redraw = function () {
var options = this.options,
props = this.props,
frame = this.frame;
// update classname
frame.className = 'timeaxis'; // TODO: add className from options if defined
var parent = frame.parentNode;
if (parent) {
// calculate character width and height
this._calculateCharSize();
// 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');
// 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?
props.minorLineHeight = parentHeight + props.minorLabelHeight;
props.minorLineWidth = 1; // TODO: really calculate width
props.majorLineHeight = parentHeight + this.height;
props.majorLineWidth = 1; // TODO: really calculate width
// 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
if (beforeChild) {
parent.insertBefore(frame, beforeChild);
}
else {
parent.appendChild(frame)
}
foreground = this.dom.foreground,
background = this.dom.background;
// determine the correct parent DOM element (depending on option orientation)
var parent = (options.orientation == 'top') ? this.body.dom.top : this.body.dom.bottom;
var parentChanged = (foreground.parentNode !== parent);
// calculate character width and height
this._calculateCharSize();
// TODO: recalculate sizes only needed when parent is resized or options is changed
var orientation = this.options.orientation,
showMinorLabels = this.options.showMinorLabels,
showMajorLabels = this.options.showMajorLabels;
// determine the width and height of the elemens for the axis
props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
props.height = props.minorLabelHeight + props.majorLabelHeight;
props.width = foreground.offsetWidth;
props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight -
(options.orientation == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height);
props.minorLineWidth = 1; // TODO: really calculate width
props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight;
props.majorLineWidth = 1; // TODO: really calculate width
// take foreground and background offline while updating (is almost twice as fast)
var foregroundNextSibling = foreground.nextSibling;
var backgroundNextSibling = background.nextSibling;
foreground.parentNode && foreground.parentNode.removeChild(foreground);
background.parentNode && background.parentNode.removeChild(background);
foreground.style.height = this.props.height + 'px';
this._repaintLabels();
// put DOM online again (at the same place)
if (foregroundNextSibling) {
parent.insertBefore(foreground, foregroundNextSibling);
}
else {
parent.appendChild(foreground)
}
if (backgroundNextSibling) {
this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling);
}
else {
this.body.dom.backgroundVertical.appendChild(background)
}
return this._isResized();
return this._isResized() || parentChanged;
};
/**
@ -151,13 +154,13 @@ TimeAxis.prototype.repaint = function () {
* @private
*/
TimeAxis.prototype._repaintLabels = function () {
var orientation = this.getOption('orientation');
var orientation = this.options.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 start = util.convert(this.body.range.start, 'Number'),
end = util.convert(this.body.range.end, 'Number'),
minimumStep = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf()
-this.body.util.toTime(0).valueOf();
var step = new TimeStep(new Date(start), new Date(end), minimumStep);
this.step = step;
@ -180,16 +183,16 @@ TimeAxis.prototype._repaintLabels = function () {
while (step.hasNext() && max < 1000) {
max++;
var cur = step.getCurrent(),
x = this.options.toScreen(cur),
x = this.body.util.toScreen(cur),
isMajor = step.isMajor();
// TODO: lines must have a width, such that we can create css backgrounds
if (this.getOption('showMinorLabels')) {
if (this.options.showMinorLabels) {
this._repaintMinorText(x, step.getLabelMinor(), orientation);
}
if (isMajor && this.getOption('showMajorLabels')) {
if (isMajor && this.options.showMajorLabels) {
if (x > 0) {
if (xFirstMajorLabel == undefined) {
xFirstMajorLabel = x;
@ -206,8 +209,8 @@ TimeAxis.prototype._repaintLabels = function () {
}
// create a major label on the left when needed
if (this.getOption('showMajorLabels')) {
var leftTime = this.options.toTime(0),
if (this.options.showMajorLabels) {
var leftTime = this.body.util.toTime(0),
leftText = step.getLabelMajor(leftTime),
widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
@ -244,20 +247,13 @@ TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
label = document.createElement('div');
label.appendChild(content);
label.className = 'text minor';
this.frame.appendChild(label);
this.dom.foreground.appendChild(label);
}
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.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0';
label.style.left = x + 'px';
//label.title = title; // TODO: this is a heavy operation
};
@ -279,21 +275,14 @@ TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
label = document.createElement('div');
label.className = 'text major';
label.appendChild(content);
this.frame.appendChild(label);
this.dom.foreground.appendChild(label);
}
this.dom.majorTexts.push(label);
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';
};
@ -311,18 +300,16 @@ TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
// create vertical line
line = document.createElement('div');
line.className = 'grid vertical minor';
this.frame.appendChild(line);
this.dom.background.appendChild(line);
}
this.dom.minorLines.push(line);
var props = this.props;
if (orientation == 'top') {
line.style.top = this.props.majorLabelHeight + 'px';
line.style.bottom = '';
line.style.top = props.majorLabelHeight + 'px';
}
else {
line.style.top = '';
line.style.bottom = this.props.majorLabelHeight + 'px';
line.style.top = this.body.domProps.top.height + 'px';
}
line.style.height = props.minorLineHeight + 'px';
line.style.left = (x - props.minorLineWidth / 2) + 'px';
@ -342,72 +329,28 @@ TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
// create vertical line
line = document.createElement('DIV');
line.className = 'grid vertical major';
this.frame.appendChild(line);
this.dom.background.appendChild(line);
}
this.dom.majorLines.push(line);
var props = this.props;
if (orientation == 'top') {
line.style.top = '0px';
line.style.bottom = '';
line.style.top = '0';
}
else {
line.style.top = '';
line.style.bottom = '0px';
line.style.top = this.body.domProps.top.height + 'px';
}
line.style.left = (x - props.majorLineWidth / 2) + 'px';
line.style.height = props.majorLineHeight + 'px';
};
/**
* Repaint the horizontal line for the axis
* @private
*/
TimeAxis.prototype._repaintLine = function() {
var line = this.dom.line,
frame = this.frame,
orientation = this.getOption('orientation');
// line before all axis elements
if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
if (line) {
// put this line at the end of all childs
frame.removeChild(line);
frame.appendChild(line);
}
else {
// create the axis line
line = document.createElement('div');
line.className = 'grid horizontal major';
frame.appendChild(line);
this.dom.line = line;
}
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.parentNode) {
line.parentNode.removeChild(line);
delete this.dom.line;
}
}
};
/**
* 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._calculateCharSize = function () {
// Note: We calculate char size with every repaint. Size may change, for
// Note: We calculate char size with every redraw. Size may change, for
// example when any of the timelines parents had display:none for example.
// determine the char width and height on the minor axis
@ -417,7 +360,7 @@ TimeAxis.prototype._calculateCharSize = function () {
this.dom.measureCharMinor.style.position = 'absolute';
this.dom.measureCharMinor.appendChild(document.createTextNode('0'));
this.frame.appendChild(this.dom.measureCharMinor);
this.dom.foreground.appendChild(this.dom.measureCharMinor);
}
this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight;
this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth;
@ -429,7 +372,7 @@ TimeAxis.prototype._calculateCharSize = function () {
this.dom.measureCharMajor.style.position = 'absolute';
this.dom.measureCharMajor.appendChild(document.createTextNode('0'));
this.frame.appendChild(this.dom.measureCharMajor);
this.dom.foreground.appendChild(this.dom.measureCharMajor);
}
this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight;
this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth;
@ -441,6 +384,6 @@ TimeAxis.prototype._calculateCharSize = function () {
* @param {Date} date the date to be snapped.
* @return {Date} snappedDate
*/
TimeAxis.prototype.snap = function snap (date) {
TimeAxis.prototype.snap = function(date) {
return this.step.snap(date);
};

+ 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;
}
/**/

+ 1
- 1
src/timeline/component/css/currenttime.css View File

@ -1,5 +1,5 @@
.vis.timeline .currenttime {
background-color: #FF7F6E;
width: 2px;
z-index: 9;
z-index: 1;
}

+ 1
- 1
src/timeline/component/css/customtime.css View File

@ -2,5 +2,5 @@
background-color: #6E94FF;
width: 2px;
cursor: move;
z-index: 9;
z-index: 1;
}

+ 1
- 11
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 {
@ -20,7 +15,7 @@
z-index: 999;
}
.vis.timeline.editable .item.selected {
.vis.timeline .editable .item.selected {
cursor: move;
}
@ -70,11 +65,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 {

+ 13
- 18
src/timeline/component/css/itemset.css View File

@ -5,34 +5,29 @@
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 .background {
}
.vis.timeline .foreground {
.vis.timeline .itemset .background,
.vis.timeline .itemset .foreground {
position: absolute;
width: 100%;
height: 100%;
}
.vis.timeline .axis {
overflow: visible;
position: absolute;
width: 100%;
height: 0;
left: 1px;
z-index: 1;
}
.vis.timeline .group {
.vis.timeline .foreground .group {
position: relative;
box-sizing: border-box;
border-bottom: 1px solid #bfbfbf;
}
.vis.timeline.top .group {
border-top: 1px solid #bfbfbf;
.vis.timeline .foreground .group:last-child {
border-bottom: none;
}
.vis.timeline.bottom .group {
border-top: none;
border-bottom: 1px solid #bfbfbf;
}

+ 8
- 6
src/timeline/component/css/labelset.css View File

@ -18,17 +18,19 @@
box-sizing: border-box;
}
.vis.timeline.top .labelset .vlabel {
border-top: 1px solid #bfbfbf;
border-bottom: none;
.vis.timeline .labelset .vlabel {
border-bottom: 1px solid #bfbfbf;
}
.vis.timeline.bottom .labelset .vlabel {
border-top: none;
border-bottom: 1px solid #bfbfbf;
.vis.timeline .labelset .vlabel:last-child {
border-bottom: none;
}
.vis.timeline .labelset .vlabel .inner {
display: inline-block;
padding: 5px;
}
.vis.timeline .labelset .vlabel .inner.hidden {
padding: 0;
}

+ 56
- 13
src/timeline/component/css/panel.css View File

@ -1,28 +1,71 @@
.vis.timeline.rootpanel {
.vis.timeline.root {
position: relative;
border: 1px solid #bfbfbf;
overflow: hidden;
padding: 0;
margin: 0;
border: 1px solid #bfbfbf;
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 {
.vis.timeline .vispanel {
position: absolute;
overflow: hidden;
padding: 0;
margin: 0;
box-sizing: border-box;
}
.vis.timeline .vpanel.side {
border-right: 1px solid #bfbfbf;
.vis.timeline .vispanel.center,
.vis.timeline .vispanel.left,
.vis.timeline .vispanel.right,
.vis.timeline .vispanel.top,
.vis.timeline .vispanel.bottom {
border: 1px #bfbfbf;
}
.vis.timeline .vpanel.side.hidden {
display: none;
.vis.timeline .vispanel.center,
.vis.timeline .vispanel.left,
.vis.timeline .vispanel.right {
border-top-style: solid;
border-bottom-style: solid;
overflow: hidden;
}
.vis.timeline .vispanel.center,
.vis.timeline .vispanel.top,
.vis.timeline .vispanel.bottom {
border-left-style: solid;
border-right-style: solid;
}
.vis.timeline .background {
overflow: hidden;
}
.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;
}

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

@ -1,5 +1,20 @@
.vis.timeline .timeaxis {
position: relative;
overflow: hidden;
}
.vis.timeline .timeaxis.foreground {
top: 0;
left: 0;
width: 100%;
}
.vis.timeline .timeaxis.background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.vis.timeline .timeaxis .text {
@ -24,14 +39,6 @@
border-right: 1px solid;
}
.vis.timeline .timeaxis .grid.horizontal {
position: absolute;
left: 0;
width: 100%;
height: 0;
border-bottom: 1px solid;
}
.vis.timeline .timeaxis .grid.minor {
border-color: #e5e5e5;
}

+ 16
- 15
src/timeline/component/item/Item.js View File

@ -2,17 +2,18 @@
* @constructor Item
* @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
* @param {{toScreen: function, toTime: function}} conversion
* Conversion functions from time to screen and vice versa
* @param {Object} options Configuration options
* // TODO: describe available options
*/
function Item (data, options, defaultOptions) {
function Item (data, conversion, options) {
this.id = null;
this.parent = null;
this.data = data;
this.dom = null;
this.conversion = conversion || {};
this.options = options || {};
this.defaultOptions = defaultOptions || {};
this.selected = false;
this.displayed = false;
@ -27,24 +28,24 @@ function Item (data, options, defaultOptions) {
/**
* Select current item
*/
Item.prototype.select = function select() {
Item.prototype.select = function() {
this.selected = true;
if (this.displayed) this.repaint();
if (this.displayed) this.redraw();
};
/**
* Unselect current item
*/
Item.prototype.unselect = function unselect() {
Item.prototype.unselect = function() {
this.selected = false;
if (this.displayed) this.repaint();
if (this.displayed) this.redraw();
};
/**
* Set a parent for the item
* @param {ItemSet | Group} parent
*/
Item.prototype.setParent = function setParent(parent) {
Item.prototype.setParent = function(parent) {
if (this.displayed) {
this.hide();
this.parent = parent;
@ -62,7 +63,7 @@ Item.prototype.setParent = function setParent(parent) {
* @returns {{start: Number, end: Number}} range with a timestamp for start and end
* @returns {boolean} True if visible
*/
Item.prototype.isVisible = function isVisible (range) {
Item.prototype.isVisible = function(range) {
// Should be implemented by Item implementations
return false;
};
@ -71,7 +72,7 @@ Item.prototype.isVisible = function isVisible (range) {
* Show the Item in the DOM (when not already visible)
* @return {Boolean} changed
*/
Item.prototype.show = function show() {
Item.prototype.show = function() {
return false;
};
@ -79,28 +80,28 @@ Item.prototype.show = function show() {
* Hide the Item from the DOM (when visible)
* @return {Boolean} changed
*/
Item.prototype.hide = function hide() {
Item.prototype.hide = function() {
return false;
};
/**
* Repaint the item
*/
Item.prototype.repaint = function repaint() {
Item.prototype.redraw = function() {
// should be implemented by the item
};
/**
* Reposition the Item horizontally
*/
Item.prototype.repositionX = function repositionX() {
Item.prototype.repositionX = function() {
// should be implemented by the item
};
/**
* Reposition the Item vertically
*/
Item.prototype.repositionY = function repositionY() {
Item.prototype.repositionY = function() {
// should be implemented by the item
};

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

@ -3,11 +3,12 @@
* @extends Item
* @param {Object} data Object containing parameters start
* content, className.
* @param {Object} [options] Options to set initial property values
* @param {Object} [defaultOptions] default options
* @param {{toScreen: function, toTime: function}} conversion
* Conversion functions from time to screen and vice versa
* @param {Object} [options] Configuration options
* // TODO: describe available options
*/
function ItemBox (data, options, defaultOptions) {
function ItemBox (data, conversion, options) {
this.props = {
dot: {
width: 0,
@ -26,17 +27,17 @@ function ItemBox (data, options, defaultOptions) {
}
}
Item.call(this, data, options, defaultOptions);
Item.call(this, data, conversion, options);
}
ItemBox.prototype = new Item (null);
ItemBox.prototype = new Item (null, null, 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) {
ItemBox.prototype.isVisible = function(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;
@ -46,7 +47,7 @@ ItemBox.prototype.isVisible = function isVisible (range) {
/**
* Repaint the item
*/
ItemBox.prototype.repaint = function repaint() {
ItemBox.prototype.redraw = function() {
var dom = this.dom;
if (!dom) {
// create DOM
@ -75,21 +76,21 @@ ItemBox.prototype.repaint = function repaint() {
// append DOM to parent DOM
if (!this.parent) {
throw new Error('Cannot repaint item: no parent attached');
throw new Error('Cannot redraw 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');
var foreground = this.parent.dom.foreground;
if (!foreground) throw new Error('Cannot redraw 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');
var background = this.parent.dom.background;
if (!background) throw new Error('Cannot redraw 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');
var axis = this.parent.dom.axis;
if (!background) throw new Error('Cannot redraw time axis: parent has no axis container element');
axis.appendChild(dom.dot);
}
this.displayed = true;
@ -141,16 +142,16 @@ ItemBox.prototype.repaint = function repaint() {
* Show the item in the DOM (when not already displayed). The items DOM will
* be created when needed.
*/
ItemBox.prototype.show = function show() {
ItemBox.prototype.show = function() {
if (!this.displayed) {
this.repaint();
this.redraw();
}
};
/**
* Hide the item from the DOM (when visible)
*/
ItemBox.prototype.hide = function hide() {
ItemBox.prototype.hide = function() {
if (this.displayed) {
var dom = this.dom;
@ -169,9 +170,9 @@ ItemBox.prototype.hide = function hide() {
* Reposition the item horizontally
* @Override
*/
ItemBox.prototype.repositionX = function repositionX() {
var start = this.defaultOptions.toScreen(this.data.start),
align = this.options.align || this.defaultOptions.align,
ItemBox.prototype.repositionX = function() {
var start = this.conversion.toScreen(this.data.start),
align = this.options.align,
left,
box = this.dom.box,
line = this.dom.line,
@ -203,27 +204,26 @@ ItemBox.prototype.repositionX = function repositionX() {
* Reposition the item vertically
* @Override
*/
ItemBox.prototype.repositionY = function repositionY () {
var orientation = this.options.orientation || this.defaultOptions.orientation,
ItemBox.prototype.repositionY = function() {
var orientation = this.options.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 = '';
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';

+ 20
- 21
src/timeline/component/item/ItemPoint.js View File

@ -3,11 +3,12 @@
* @extends Item
* @param {Object} data Object containing parameters start
* content, className.
* @param {Object} [options] Options to set initial property values
* @param {Object} [defaultOptions] default options
* @param {{toScreen: function, toTime: function}} conversion
* Conversion functions from time to screen and vice versa
* @param {Object} [options] Configuration options
* // TODO: describe available options
*/
function ItemPoint (data, options, defaultOptions) {
function ItemPoint (data, conversion, options) {
this.props = {
dot: {
top: 0,
@ -27,17 +28,17 @@ function ItemPoint (data, options, defaultOptions) {
}
}
Item.call(this, data, options, defaultOptions);
Item.call(this, data, conversion, options);
}
ItemPoint.prototype = new Item (null);
ItemPoint.prototype = new Item (null, null, 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) {
ItemPoint.prototype.isVisible = function(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;
@ -47,7 +48,7 @@ ItemPoint.prototype.isVisible = function isVisible (range) {
/**
* Repaint the item
*/
ItemPoint.prototype.repaint = function repaint() {
ItemPoint.prototype.redraw = function() {
var dom = this.dom;
if (!dom) {
// create DOM
@ -56,7 +57,7 @@ ItemPoint.prototype.repaint = function repaint() {
// background box
dom.point = document.createElement('div');
// className is updated in repaint()
// className is updated in redraw()
// contents box, right from the dot
dom.content = document.createElement('div');
@ -73,12 +74,12 @@ ItemPoint.prototype.repaint = function repaint() {
// append DOM to parent DOM
if (!this.parent) {
throw new Error('Cannot repaint item: no parent attached');
throw new Error('Cannot redraw item: no parent attached');
}
if (!dom.point.parentNode) {
var foreground = this.parent.getForeground();
var foreground = this.parent.dom.foreground;
if (!foreground) {
throw new Error('Cannot repaint time axis: parent has no foreground container element');
throw new Error('Cannot redraw time axis: parent has no foreground container element');
}
foreground.appendChild(dom.point);
}
@ -137,16 +138,16 @@ ItemPoint.prototype.repaint = function repaint() {
* Show the item in the DOM (when not already visible). The items DOM will
* be created when needed.
*/
ItemPoint.prototype.show = function show() {
ItemPoint.prototype.show = function() {
if (!this.displayed) {
this.repaint();
this.redraw();
}
};
/**
* Hide the item from the DOM (when visible)
*/
ItemPoint.prototype.hide = function hide() {
ItemPoint.prototype.hide = function() {
if (this.displayed) {
if (this.dom.point.parentNode) {
this.dom.point.parentNode.removeChild(this.dom.point);
@ -163,8 +164,8 @@ ItemPoint.prototype.hide = function hide() {
* Reposition the item horizontally
* @Override
*/
ItemPoint.prototype.repositionX = function repositionX() {
var start = this.defaultOptions.toScreen(this.data.start);
ItemPoint.prototype.repositionX = function() {
var start = this.conversion.toScreen(this.data.start);
this.left = start - this.props.dot.width;
@ -176,16 +177,14 @@ ItemPoint.prototype.repositionX = function repositionX() {
* Reposition the item vertically
* @Override
*/
ItemPoint.prototype.repositionY = function repositionY () {
var orientation = this.options.orientation || this.defaultOptions.orientation,
ItemPoint.prototype.repositionY = function() {
var orientation = this.options.orientation,
point = this.dom.point;
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';
}
};

+ 23
- 24
src/timeline/component/item/ItemRange.js View File

@ -3,11 +3,12 @@
* @extends Item
* @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
* @param {{toScreen: function, toTime: function}} conversion
* Conversion functions from time to screen and vice versa
* @param {Object} [options] Configuration options
* // TODO: describe options
*/
function ItemRange (data, options, defaultOptions) {
function ItemRange (data, conversion, options) {
this.props = {
content: {
width: 0
@ -24,10 +25,10 @@ function ItemRange (data, options, defaultOptions) {
}
}
Item.call(this, data, options, defaultOptions);
Item.call(this, data, conversion, options);
}
ItemRange.prototype = new Item (null);
ItemRange.prototype = new Item (null, null, null);
ItemRange.prototype.baseClassName = 'item range';
@ -36,7 +37,7 @@ ItemRange.prototype.baseClassName = 'item 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) {
ItemRange.prototype.isVisible = function(range) {
// determine visibility
return (this.data.start < range.end) && (this.data.end > range.start);
};
@ -44,7 +45,7 @@ ItemRange.prototype.isVisible = function isVisible (range) {
/**
* Repaint the item
*/
ItemRange.prototype.repaint = function repaint() {
ItemRange.prototype.redraw = function() {
var dom = this.dom;
if (!dom) {
// create DOM
@ -53,7 +54,7 @@ ItemRange.prototype.repaint = function repaint() {
// background box
dom.box = document.createElement('div');
// className is updated in repaint()
// className is updated in redraw()
// contents box
dom.content = document.createElement('div');
@ -66,12 +67,12 @@ ItemRange.prototype.repaint = function repaint() {
// append DOM to parent DOM
if (!this.parent) {
throw new Error('Cannot repaint item: no parent attached');
throw new Error('Cannot redraw item: no parent attached');
}
if (!dom.box.parentNode) {
var foreground = this.parent.getForeground();
var foreground = this.parent.dom.foreground;
if (!foreground) {
throw new Error('Cannot repaint time axis: parent has no foreground container element');
throw new Error('Cannot redraw time axis: parent has no foreground container element');
}
foreground.appendChild(dom.box);
}
@ -121,9 +122,9 @@ ItemRange.prototype.repaint = function repaint() {
* Show the item in the DOM (when not already visible). The items DOM will
* be created when needed.
*/
ItemRange.prototype.show = function show() {
ItemRange.prototype.show = function() {
if (!this.displayed) {
this.repaint();
this.redraw();
}
};
@ -131,7 +132,7 @@ ItemRange.prototype.show = function show() {
* Hide the item from the DOM (when visible)
* @return {Boolean} changed
*/
ItemRange.prototype.hide = function hide() {
ItemRange.prototype.hide = function() {
if (this.displayed) {
var box = this.dom.box;
@ -150,12 +151,12 @@ ItemRange.prototype.hide = function hide() {
* Reposition the item horizontally
* @Override
*/
ItemRange.prototype.repositionX = function repositionX() {
ItemRange.prototype.repositionX = function() {
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,
start = this.conversion.toScreen(this.data.start),
end = this.conversion.toScreen(this.data.end),
padding = this.options.padding,
contentLeft;
// limit the width of the this, as browsers cannot draw very wide divs
@ -188,17 +189,15 @@ ItemRange.prototype.repositionX = function repositionX() {
* Reposition the item vertically
* @Override
*/
ItemRange.prototype.repositionY = function repositionY() {
var orientation = this.options.orientation || this.defaultOptions.orientation,
ItemRange.prototype.repositionY = function() {
var orientation = this.options.orientation,
box = this.dom.box;
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';
}
};

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

@ -3,11 +3,12 @@
* @extends ItemRange
* @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
* @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, options, defaultOptions) {
function ItemRangeOverflow (data, conversion, options) {
this.props = {
content: {
left: 0,
@ -15,10 +16,10 @@ function ItemRangeOverflow (data, options, defaultOptions) {
}
};
ItemRange.call(this, data, options, defaultOptions);
ItemRange.call(this, data, conversion, options);
}
ItemRangeOverflow.prototype = new ItemRange (null);
ItemRangeOverflow.prototype = new ItemRange (null, null, null);
ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow';
@ -26,11 +27,10 @@ ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow';
* Reposition the item horizontally
* @Override
*/
ItemRangeOverflow.prototype.repositionX = function repositionX() {
ItemRangeOverflow.prototype.repositionX = function() {
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,
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

+ 5
- 5
src/timeline/stack.js View File

@ -7,7 +7,7 @@ var stack = {};
* Order items by their start data
* @param {Item[]} items
*/
stack.orderByStart = function orderByStart(items) {
stack.orderByStart = function(items) {
items.sort(function (a, b) {
return a.data.start - b.data.start;
});
@ -18,7 +18,7 @@ stack.orderByStart = function orderByStart(items) {
* is used.
* @param {Item[]} items
*/
stack.orderByEnd = function orderByEnd(items) {
stack.orderByEnd = function(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;
@ -38,7 +38,7 @@ stack.orderByEnd = function orderByEnd(items) {
* 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) {
stack.stack = function(items, margin, force) {
var i, iMax;
if (force) {
@ -83,7 +83,7 @@ stack.stack = function _stack (items, margin, force) {
* @param {{item: number, axis: number}} margin
* Margins between items and between items and the axis.
*/
stack.nostack = function nostack (items, margin) {
stack.nostack = function(items, margin) {
var i, iMax;
// reset top position of all items
@ -104,7 +104,7 @@ stack.nostack = function nostack (items, margin) {
* the requested margin.
* @return {boolean} true if a and b collide, else false
*/
stack.collision = function collision (a, b, margin) {
stack.collision = function(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) &&

+ 61
- 35
src/util.js View File

@ -8,7 +8,7 @@ var util = {};
* @param {*} object
* @return {Boolean} isNumber
*/
util.isNumber = function isNumber(object) {
util.isNumber = function(object) {
return (object instanceof Number || typeof object == 'number');
};
@ -17,7 +17,7 @@ util.isNumber = function isNumber(object) {
* @param {*} object
* @return {Boolean} isString
*/
util.isString = function isString(object) {
util.isString = function(object) {
return (object instanceof String || typeof object == 'string');
};
@ -26,7 +26,7 @@ util.isString = function isString(object) {
* @param {Date | String} object
* @return {Boolean} isDate
*/
util.isDate = function isDate(object) {
util.isDate = function(object) {
if (object instanceof Date) {
return true;
}
@ -49,7 +49,7 @@ util.isDate = function isDate(object) {
* @param {*} object
* @return {Boolean} isDataTable
*/
util.isDataTable = function isDataTable(object) {
util.isDataTable = function(object) {
return (typeof (google) !== 'undefined') &&
(google.visualization) &&
(google.visualization.DataTable) &&
@ -61,7 +61,7 @@ util.isDataTable = function isDataTable(object) {
* source: http://stackoverflow.com/a/105074/1262753
* @return {String} uuid
*/
util.randomUUID = function randomUUID () {
util.randomUUID = function() {
var S4 = function () {
return Math.floor(
Math.random() * 0x10000 /* 65536 */
@ -88,7 +88,34 @@ util.extend = function (a, b) {
for (var i = 1, len = arguments.length; i < len; i++) {
var other = arguments[i];
for (var prop in other) {
if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
if (other.hasOwnProperty(prop)) {
a[prop] = other[prop];
}
}
}
return a;
};
/**
* Extend object a with selected properties of object b or a series of objects
* Only properties with defined values are copied
* @param {Array.<String>} props
* @param {Object} a
* @param {... Object} b
* @return {Object} a
*/
util.selectiveExtend = function (props, a, b) {
if (!Array.isArray(props)) {
throw new Error('Array with property names expected as first argument');
}
for (var i = 1, len = arguments.length; i < len; i++) {
var other = arguments[i];
for (var p = 0, pp = props.length; p < pp; p++) {
var prop = props[p];
if (other.hasOwnProperty(prop)) {
a[prop] = other[prop];
}
}
@ -103,7 +130,7 @@ util.extend = function (a, b) {
* @param {Object} b
* @returns {Object}
*/
util.deepExtend = function deepExtend (a, b) {
util.deepExtend = function(a, b) {
// TODO: add support for Arrays to deepExtend
if (Array.isArray(b)) {
throw new TypeError('Arrays are not supported by deepExtend');
@ -116,7 +143,7 @@ util.deepExtend = function deepExtend (a, b) {
a[prop] = {};
}
if (a[prop].constructor === Object) {
deepExtend(a[prop], b[prop]);
util.deepExtend(a[prop], b[prop]);
}
else {
a[prop] = b[prop];
@ -157,7 +184,7 @@ util.equalArray = function (a, b) {
* @return {*} object
* @throws Error
*/
util.convert = function convert(object, type) {
util.convert = function(object, type) {
var match;
if (object === undefined) {
@ -292,8 +319,7 @@ util.convert = function convert(object, type) {
}
default:
throw new Error('Cannot convert object of type ' + util.getType(object) +
' to type "' + type + '"');
throw new Error('Unknown type "' + type + '"');
}
};
@ -307,7 +333,7 @@ var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
* @param {*} object
* @return {String} type
*/
util.getType = function getType(object) {
util.getType = function(object) {
var type = typeof object;
if (type == 'object') {
@ -350,7 +376,7 @@ util.getType = function getType(object) {
* @return {number} left The absolute left position of this element
* in the browser page.
*/
util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
util.getAbsoluteLeft = function(elem) {
var doc = document.documentElement;
var body = document.body;
@ -370,7 +396,7 @@ util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
* @return {number} top The absolute top position of this element
* in the browser page.
*/
util.getAbsoluteTop = function getAbsoluteTop (elem) {
util.getAbsoluteTop = function(elem) {
var doc = document.documentElement;
var body = document.body;
@ -389,7 +415,7 @@ util.getAbsoluteTop = function getAbsoluteTop (elem) {
* @param {Event} event
* @return {Number} pageY
*/
util.getPageY = function getPageY (event) {
util.getPageY = function(event) {
if ('pageY' in event) {
return event.pageY;
}
@ -415,7 +441,7 @@ util.getPageY = function getPageY (event) {
* @param {Event} event
* @return {Number} pageX
*/
util.getPageX = function getPageX (event) {
util.getPageX = function(event) {
if ('pageY' in event) {
return event.pageX;
}
@ -441,7 +467,7 @@ util.getPageX = function getPageX (event) {
* @param {Element} elem
* @param {String} className
*/
util.addClassName = function addClassName(elem, className) {
util.addClassName = function(elem, className) {
var classes = elem.className.split(' ');
if (classes.indexOf(className) == -1) {
classes.push(className); // add the class to the array
@ -454,7 +480,7 @@ util.addClassName = function addClassName(elem, className) {
* @param {Element} elem
* @param {String} className
*/
util.removeClassName = function removeClassname(elem, className) {
util.removeClassName = function(elem, className) {
var classes = elem.className.split(' ');
var index = classes.indexOf(className);
if (index != -1) {
@ -472,7 +498,7 @@ util.removeClassName = function removeClassname(elem, className) {
* the object or array with three parameters:
* callback(value, index, object)
*/
util.forEach = function forEach (object, callback) {
util.forEach = function(object, callback) {
var i,
len;
if (object instanceof Array) {
@ -497,7 +523,7 @@ util.forEach = function forEach (object, callback) {
* @param {Object} object
* @param {Array} array
*/
util.toArray = function toArray(object) {
util.toArray = function(object) {
var array = [];
for (var prop in object) {
@ -514,7 +540,7 @@ util.toArray = function toArray(object) {
* @param {*} value
* @return {Boolean} changed
*/
util.updateProperty = function updateProperty (object, key, value) {
util.updateProperty = function(object, key, value) {
if (object[key] !== value) {
object[key] = value;
return true;
@ -532,7 +558,7 @@ util.updateProperty = function updateProperty (object, key, value) {
* @param {function} listener The callback function to be executed
* @param {boolean} [useCapture]
*/
util.addEventListener = function addEventListener(element, action, listener, useCapture) {
util.addEventListener = function(element, action, listener, useCapture) {
if (element.addEventListener) {
if (useCapture === undefined)
useCapture = false;
@ -554,7 +580,7 @@ util.addEventListener = function addEventListener(element, action, listener, use
* @param {function} listener The listener function
* @param {boolean} [useCapture]
*/
util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
util.removeEventListener = function(element, action, listener, useCapture) {
if (element.removeEventListener) {
// non-IE browsers
if (useCapture === undefined)
@ -577,7 +603,7 @@ util.removeEventListener = function removeEventListener(element, action, listene
* @param {Event} event
* @return {Element} target element
*/
util.getTarget = function getTarget(event) {
util.getTarget = function(event) {
// code from http://www.quirksmode.org/js/events_properties.html
if (!event) {
event = window.event;
@ -605,7 +631,7 @@ util.getTarget = function getTarget(event) {
* @param {Element} element
* @param {Event} event
*/
util.fakeGesture = function fakeGesture (element, event) {
util.fakeGesture = function(element, event) {
var eventType = null;
// for hammer.js 1.0.5
@ -721,7 +747,7 @@ util.option.asElement = function (value, defaultValue) {
util.GiveDec = function GiveDec(Hex) {
util.GiveDec = function(Hex) {
var Value;
if (Hex == "A")
@ -742,7 +768,7 @@ util.GiveDec = function GiveDec(Hex) {
return Value;
};
util.GiveHex = function GiveHex(Dec) {
util.GiveHex = function(Dec) {
var Value;
if(Dec == 10)
@ -846,7 +872,7 @@ util.parseColor = function(color) {
* @param {String} hex
* @returns {{r: *, g: *, b: *}}
*/
util.hexToRGB = function hexToRGB(hex) {
util.hexToRGB = function(hex) {
hex = hex.replace("#","").toUpperCase();
var a = util.GiveDec(hex.substring(0, 1));
@ -863,7 +889,7 @@ util.hexToRGB = function hexToRGB(hex) {
return {r:r,g:g,b:b};
};
util.RGBToHex = function RGBToHex(red,green,blue) {
util.RGBToHex = function(red,green,blue) {
var a = util.GiveHex(Math.floor(red / 16));
var b = util.GiveHex(red % 16);
var c = util.GiveHex(Math.floor(green / 16));
@ -885,7 +911,7 @@ util.RGBToHex = function RGBToHex(red,green,blue) {
* @returns {*}
* @constructor
*/
util.RGBToHSV = function RGBToHSV (red,green,blue) {
util.RGBToHSV = function(red,green,blue) {
red=red/255; green=green/255; blue=blue/255;
var minRGB = Math.min(red,Math.min(green,blue));
var maxRGB = Math.max(red,Math.max(green,blue));
@ -913,7 +939,7 @@ util.RGBToHSV = function RGBToHSV (red,green,blue) {
* @returns {{r: number, g: number, b: number}}
* @constructor
*/
util.HSVToRGB = function HSVToRGB(h, s, v) {
util.HSVToRGB = function(h, s, v) {
var r, g, b;
var i = Math.floor(h * 6);
@ -934,22 +960,22 @@ util.HSVToRGB = function HSVToRGB(h, s, v) {
return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
};
util.HSVToHex = function HSVToHex(h, s, v) {
util.HSVToHex = function(h, s, v) {
var rgb = util.HSVToRGB(h, s, v);
return util.RGBToHex(rgb.r, rgb.g, rgb.b);
};
util.hexToHSV = function hexToHSV(hex) {
util.hexToHSV = function(hex) {
var rgb = util.hexToRGB(hex);
return util.RGBToHSV(rgb.r, rgb.g, rgb.b);
};
util.isValidHex = function isValidHex(hex) {
util.isValidHex = function(hex) {
var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
return isOk;
};
util.copyObject = function copyObject(objectFrom, objectTo) {
util.copyObject = function(objectFrom, objectTo) {
for (var i in objectFrom) {
if (objectFrom.hasOwnProperty(i)) {
if (typeof objectFrom[i] == "object") {

+ 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([

+ 6
- 6
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'
});

+ 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