@ -0,0 +1,69 @@ | |||||
<!DOCTYPE HTML> | |||||
<html> | |||||
<head> | |||||
<title>Timeline | a lot of data</title> | |||||
<style> | |||||
body, html { | |||||
font-family: arial, sans-serif; | |||||
font-size: 11pt; | |||||
} | |||||
</style> | |||||
<!-- note: moment.js must be loaded before vis.js, else vis.js uses its embedded version of moment.js --> | |||||
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.3.1/moment.min.js"></script> | |||||
<script src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
</head> | |||||
<body> | |||||
<h1> | |||||
Test with a lot of data | |||||
</h1> | |||||
<p> | |||||
<label for="count">Number of items</label> | |||||
<input id="count" value="10000"> | |||||
<input id="draw" type="button" value="draw"> | |||||
</p> | |||||
<div id="visualization"></div> | |||||
<script> | |||||
// create a dataset with items | |||||
var now = moment().minutes(0).seconds(0).milliseconds(0); | |||||
var items = new vis.DataSet({ | |||||
convert: { | |||||
start: 'Date', | |||||
end: 'Date' | |||||
} | |||||
}); | |||||
// create data | |||||
function createData() { | |||||
var count = parseInt(document.getElementById('count').value) || 100; | |||||
var newData = []; | |||||
for (var i = 0; i < count; i++) { | |||||
newData.push({id: i, content: 'item ' + i, start: now.clone().add('days', i)}); | |||||
} | |||||
items.clear(); | |||||
items.add(newData); | |||||
} | |||||
createData(); | |||||
document.getElementById('draw').onclick = createData; | |||||
var container = document.getElementById('visualization'); | |||||
var options = { | |||||
editable: true, | |||||
start: now.clone().add('days', -3), | |||||
end: now.clone().add('days', 11), | |||||
zoomMin: 1000 * 60 * 60 * 24, // a day | |||||
zoomMax: 1000 * 60 * 60 * 24 * 30 * 3 // three months | |||||
//maxHeight: 300, | |||||
//height: '300px', | |||||
//orientation: 'top' | |||||
}; | |||||
var timeline = new vis.Timeline(container, items, options); | |||||
</script> | |||||
</body> | |||||
</html> |
@ -1,68 +0,0 @@ | |||||
<!DOCTYPE HTML> | |||||
<html> | |||||
<head> | |||||
<title>Timeline | a lot of data</title> | |||||
<style> | |||||
body, html { | |||||
font-family: arial, sans-serif; | |||||
font-size: 11pt; | |||||
} | |||||
</style> | |||||
<!-- note: moment.js must be loaded before vis.js, else vis.js uses its embedded version of moment.js --> | |||||
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.3.1/moment.min.js"></script> | |||||
<script src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
</head> | |||||
<body> | |||||
<h1> | |||||
Test with a lot of data | |||||
</h1> | |||||
<p> | |||||
<label for="count">Number of items</label> | |||||
<input id="count" value="100"> | |||||
<input id="draw" type="button" value="draw"> | |||||
</p> | |||||
<div id="visualization"></div> | |||||
<script> | |||||
// create a dataset with items | |||||
var now = moment().minutes(0).seconds(0).milliseconds(0); | |||||
var items = new vis.DataSet({ | |||||
convert: { | |||||
start: 'Date', | |||||
end: 'Date' | |||||
} | |||||
}); | |||||
// create data | |||||
function createData() { | |||||
var count = parseInt(document.getElementById('count').value) || 100; | |||||
var newData = []; | |||||
for (var i = 0; i < count; i++) { | |||||
newData.push({id: i, content: 'item ' + i, start: now.clone().add('days', i)}); | |||||
} | |||||
items.clear(); | |||||
items.add(newData); | |||||
} | |||||
createData(); | |||||
document.getElementById('draw').onclick = createData; | |||||
var container = document.getElementById('visualization'); | |||||
var options = { | |||||
start: now.clone().add('days', -3), | |||||
end: now.clone().add('days', 11), | |||||
zoomMin: 1000 * 60 * 60 * 24, // a day | |||||
zoomMax: 1000 * 60 * 60 * 24 * 30 * 3 // three months | |||||
//maxHeight: 300, | |||||
//height: '300px', | |||||
//orientation: 'top' | |||||
}; | |||||
var timeline = new vis.Timeline(container, items, options); | |||||
</script> | |||||
</body> | |||||
</html> |
@ -0,0 +1,66 @@ | |||||
<!DOCTYPE HTML> | |||||
<html> | |||||
<head> | |||||
<title>Timeline | Order groups</title> | |||||
<style> | |||||
body, html { | |||||
font-family: arial, sans-serif; | |||||
font-size: 11pt; | |||||
} | |||||
#visualization { | |||||
box-sizing: border-box; | |||||
width: 100%; | |||||
height: 300px; | |||||
} | |||||
</style> | |||||
<script src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
</head> | |||||
<body> | |||||
<p> | |||||
This example demonstrate custom ordering of groups. | |||||
</p> | |||||
<div id="visualization"></div> | |||||
<script> | |||||
var groups = new vis.DataSet([ | |||||
{id: 0, content: 'First', value: 1}, | |||||
{id: 1, content: 'Third', value: 3}, | |||||
{id: 2, content: 'Second', value: 2} | |||||
]); | |||||
// create a dataset with items | |||||
var items = new vis.DataSet([ | |||||
{id: 0, group: 0, content: 'item 0', start: new Date(2014, 3, 17), end: new Date(2014, 3, 21)}, | |||||
{id: 1, group: 0, content: 'item 1', start: new Date(2014, 3, 19), end: new Date(2014, 3, 20)}, | |||||
{id: 2, group: 1, content: 'item 2', start: new Date(2014, 3, 16), end: new Date(2014, 3, 24)}, | |||||
{id: 3, group: 1, content: 'item 3', start: new Date(2014, 3, 23), end: new Date(2014, 3, 24)}, | |||||
{id: 4, group: 1, content: 'item 4', start: new Date(2014, 3, 22), end: new Date(2014, 3, 26)}, | |||||
{id: 5, group: 2, content: 'item 5', start: new Date(2014, 3, 24), end: new Date(2014, 3, 27)} | |||||
]); | |||||
// create visualization | |||||
var container = document.getElementById('visualization'); | |||||
var options = { | |||||
// option groupOrder can be a property name or a sort function | |||||
// the sort function must compare two groups and return a value | |||||
// > 0 when a > b | |||||
// < 0 when a < b | |||||
// 0 when a == b | |||||
groupOrder: function (a, b) { | |||||
return a.value - b.value; | |||||
}, | |||||
editable: true | |||||
}; | |||||
var timeline = new vis.Timeline(container); | |||||
timeline.setOptions(options); | |||||
timeline.setGroups(groups); | |||||
timeline.setItems(items); | |||||
</script> | |||||
</body> | |||||
</html> |
@ -0,0 +1,51 @@ | |||||
<!DOCTYPE HTML> | |||||
<html> | |||||
<head> | |||||
<title>Timeline | Limit move and zoom</title> | |||||
<style> | |||||
body, html { | |||||
font-family: arial, sans-serif; | |||||
font-size: 11pt; | |||||
} | |||||
</style> | |||||
<script src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
</head> | |||||
<body> | |||||
<p> | |||||
The visible range is limited in this demo: | |||||
</p> | |||||
<ul> | |||||
<li>minimum visible date is limited to 2012-01-01 using option <code>min</code></li> | |||||
<li>maximum visible date is limited to 2013-01-01 (excluded) using option <code>max</code></li> | |||||
<li>visible zoom interval is limited to a minimum of 24 hours using option <code>zoomMin</code></li> | |||||
<li>visible zoom interval is limited to a maximum of about 3 months using option <code>zoomMax</code></li> | |||||
</ul> | |||||
<div id="visualization"></div> | |||||
<script> | |||||
// create some items | |||||
var items = [ | |||||
{'start': new Date(2012, 4, 25), 'content': 'First'}, | |||||
{'start': new Date(2012, 4, 26), 'content': 'Last'} | |||||
]; | |||||
// create visualization | |||||
var container = document.getElementById('visualization'); | |||||
var options = { | |||||
height: '300px', | |||||
min: new Date(2012, 0, 1), // lower limit of visible range | |||||
max: new Date(2013, 0, 1), // upper limit of visible range | |||||
zoomMin: 1000 * 60 * 60 * 24, // one day in milliseconds | |||||
zoomMax: 1000 * 60 * 60 * 24 * 31 * 3 // about three months in milliseconds | |||||
}; | |||||
// create the timeline | |||||
var timeline = new vis.Timeline(container); | |||||
timeline.setOptions(options); | |||||
timeline.setItems(items); | |||||
</script> | |||||
</body> | |||||
</html> |
@ -0,0 +1,57 @@ | |||||
<!DOCTYPE HTML> | |||||
<html> | |||||
<head> | |||||
<title>Timeline | Points</title> | |||||
<style type="text/css"> | |||||
body { | |||||
font: 10pt arial; | |||||
} | |||||
</style> | |||||
<script src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
</head> | |||||
<body> | |||||
<h1>World War II timeline</h1> | |||||
<p>Source: <a href="http://www.onwar.com/chrono/index.htm" target="_blank">http://www.onwar.com/chrono/index.htm</a></p> | |||||
<div id="mytimeline" style="background-color: #FAFAFA;"></div> | |||||
<div id="visualization"></div> | |||||
<script type="text/javascript"> | |||||
var container = document.getElementById('visualization'); | |||||
var items = [ | |||||
{start: new Date(1939,8,1), content: 'German Invasion of Poland'}, | |||||
{start: new Date(1940,4,10), content: 'Battle of France and the Low Countries'}, | |||||
{start: new Date(1940,7,13), content: 'Battle of Britain - RAF vs. Luftwaffe'}, | |||||
{start: new Date(1941,1,14), content: 'German Afrika Korps arrives in North Africa'}, | |||||
{start: new Date(1941,5,22), content: 'Third Reich Invades the USSR'}, | |||||
{start: new Date(1941,11,7), content: 'Japanese Attack Pearl Harbor'}, | |||||
{start: new Date(1942,5,4), content: 'Battle of Midway in the Pacific'}, | |||||
{start: new Date(1942,10,8), content: 'Americans open Second Front in North Africa'}, | |||||
{start: new Date(1942,10,19),content: 'Battle of Stalingrad in Russia'}, | |||||
{start: new Date(1943,6,5), content: 'Battle of Kursk - Last German Offensive on Eastern Front'}, | |||||
{start: new Date(1943,6,10), content: 'Anglo-American Landings in Sicily'}, | |||||
{start: new Date(1944,2,8), content: 'Japanese Attack British India'}, | |||||
{start: new Date(1944,5,6), content: 'D-Day - Allied Invasion of Normandy'}, | |||||
{start: new Date(1944,5,22), content: 'Destruction of Army Group Center in Byelorussia'}, | |||||
{start: new Date(1944,7,1), content: 'The Warsaw Uprising in Occupied Poland'}, | |||||
{start: new Date(1944,9,20), content: 'American Liberation of the Philippines'}, | |||||
{start: new Date(1944,11,16),content: 'Battle of the Bulge in the Ardennes'}, | |||||
{start: new Date(1944,1,19), content: 'American Landings on Iwo Jima'}, | |||||
{start: new Date(1945,3,1), content: 'US Invasion of Okinawa'}, | |||||
{start: new Date(1945,3,16), content: 'Battle of Berlin - End of the Third Reich'} | |||||
]; | |||||
var options = { | |||||
// Set global item type. Type can also be specified for items individually | |||||
// Available types: 'box' (default), 'point', 'range', 'rangeoverflow' | |||||
type: 'point' | |||||
}; | |||||
var timeline = new vis.Timeline(container, items, options); | |||||
</script> | |||||
</body> | |||||
</html> |
@ -0,0 +1,88 @@ | |||||
<!DOCTYPE HTML> | |||||
<html> | |||||
<head> | |||||
<title>Timeline | Custom styling</title> | |||||
<script src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
<style type="text/css"> | |||||
.vis.timeline.rootpanel { | |||||
border: 2px solid purple; | |||||
font-family: purisa, 'comic sans', cursive; | |||||
font-size: 12pt; | |||||
background: #ffecea; | |||||
} | |||||
.vis.timeline .item { | |||||
border-color: #F991A3; | |||||
background-color: pink; | |||||
font-size: 15pt; | |||||
color: purple; | |||||
box-shadow: 5px 5px 20px rgba(128,128,128, 0.5); | |||||
} | |||||
.vis.timeline .item, | |||||
.vis.timeline .item.line { | |||||
border-width: 3px; | |||||
} | |||||
.vis.timeline .item.dot { | |||||
border-width: 10px; | |||||
border-radius: 10px; | |||||
} | |||||
.vis.timeline .item.selected { | |||||
border-color: green; | |||||
background-color: lightgreen; | |||||
} | |||||
.vis.timeline .timeaxis .text { | |||||
color: purple; | |||||
padding-top: 10px; | |||||
padding-left: 10px; | |||||
} | |||||
.vis.timeline .timeaxis .text.major { | |||||
font-weight: bold; | |||||
} | |||||
.vis.timeline .timeaxis .grid.minor { | |||||
border-width: 2px; | |||||
border-color: pink; | |||||
} | |||||
.vis.timeline .timeaxis .grid.major { | |||||
border-width: 2px; | |||||
border-color: #F991A3; | |||||
} | |||||
</style> | |||||
</head> | |||||
<body> | |||||
<div id="visualization"></div> | |||||
<script type="text/javascript"> | |||||
var container = document.getElementById('visualization'); | |||||
var items = [ | |||||
{start: new Date(2010,7,23), content: '<div>Conversation</div><img src="img/community-users-icon.png" style="width:32px; height:32px;">', type: 'point'}, | |||||
{start: new Date(2010,7,23,23,0,0), content: '<div>Mail from boss</div><img src="img/mail-icon.png" style="width:32px; height:32px;">'}, | |||||
{start: new Date(2010,7,24,16,0,0), content: 'Report'}, | |||||
{start: new Date(2010,7,26), end: new Date(2010,8,2), content: 'Traject A'}, | |||||
{start: new Date(2010,7,28), content: '<div>Memo</div><img src="img/notes-edit-icon.png" style="width:48px; height:48px;">'}, | |||||
{start: new Date(2010,7,29), content: '<div>Phone call</div><img src="img/Hardware-Mobile-Phone-icon.png" style="width:32px; height:32px;">'}, | |||||
{start: new Date(2010,7,31), end: new Date(2010,8,3), content: 'Traject B'}, | |||||
{start: new Date(2010,8,4,12,0,0), content: '<div>Report</div><img src="img/attachment-icon.png" style="width:32px; height:32px;">'} | |||||
]; | |||||
var options = { | |||||
editable: true, | |||||
margin: { | |||||
item: 20, | |||||
axis: 40 | |||||
} | |||||
}; | |||||
var timeline = new vis.Timeline(container, items, options); | |||||
</script> | |||||
</body> | |||||
</html> |
@ -0,0 +1,88 @@ | |||||
<html> | |||||
<head> | |||||
<title>Timeline | Past and future</title> | |||||
<style type="text/css"> | |||||
body { | |||||
font: 11pt verdana; | |||||
} | |||||
.vis.timeline .item.past { | |||||
filter: alpha(opacity=50); | |||||
opacity: 0.5; | |||||
} | |||||
</style> | |||||
<script src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
</head> | |||||
<body> | |||||
<p style="width: 600px;"> | |||||
When the custom time bar is shown, the user can drag this bar to a specific | |||||
time. The Timeline sends an event that the custom time is changed, after | |||||
which the contents of the timeline can be changed according to the specified | |||||
time in past or future. | |||||
</p> | |||||
<div id="customTime"> </div> | |||||
<p></p> | |||||
<div id="mytimeline"></div> | |||||
<script> | |||||
// create a data set | |||||
var data = new vis.DataSet([ | |||||
{ | |||||
id: 1, | |||||
start: new Date((new Date()).getTime() - 60 * 1000), | |||||
end: new Date(), | |||||
content: 'Dynamic event' | |||||
} | |||||
]); | |||||
// specify options | |||||
var options = { | |||||
showCurrentTime: true, | |||||
showCustomTime: true | |||||
}; | |||||
// create a timeline | |||||
var container = document.getElementById('mytimeline'); | |||||
timeline = new vis.Timeline(container, data, options); | |||||
// add event listener | |||||
timeline.on('timechange', function (event) { | |||||
document.getElementById("customTime").innerHTML = "Custom Time: " + event.time; | |||||
var item = data.get(1); | |||||
if (event.time > item.start) { | |||||
item.end = new Date(event.time); | |||||
var now = new Date(); | |||||
if (event.time < now) { | |||||
item.content = "Dynamic event (past)"; | |||||
item.className = 'past'; | |||||
} | |||||
else if (event.time > now) { | |||||
item.content = "Dynamic event (future)"; | |||||
item.className = 'future'; | |||||
} | |||||
else { | |||||
item.content = "Dynamic event (now)"; | |||||
item.className = 'now'; | |||||
} | |||||
data.update(item); | |||||
} | |||||
}); | |||||
// set a custom range from -2 minute to +3 minutes current time | |||||
var start = new Date((new Date()).getTime() - 2 * 60 * 1000); | |||||
var end = new Date((new Date()).getTime() + 3 * 60 * 1000); | |||||
timeline.setWindow(start, end); | |||||
</script> | |||||
</body> | |||||
</html> |
@ -0,0 +1,109 @@ | |||||
<html> | |||||
<head> | |||||
<title>Timeline | A lot of grouped data</title> | |||||
<script src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
<style type="text/css"> | |||||
body { | |||||
color: #4D4D4D; | |||||
font: 10pt arial; | |||||
} | |||||
</style> | |||||
</head> | |||||
<body onresize="/*timeline.checkResize();*/"> | |||||
<h1>Timeline grouping performance</h1> | |||||
<p> | |||||
Choose a number of items: | |||||
<a href="?count=100">100</a>, | |||||
<a href="?count=1000">1000</a>, | |||||
<a href="?count=10000">10000</a>, | |||||
<a href="?count=10000">100000</a> | |||||
<p> | |||||
<p> | |||||
Current number of items: <span id='count'>100</span> | |||||
</p> | |||||
<div id="mytimeline"></div> | |||||
<script> | |||||
/** | |||||
* Get URL parameter | |||||
* http://www.netlobo.com/url_query_string_javascript.html | |||||
*/ | |||||
function gup( name ) { | |||||
name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]"); | |||||
var regexS = "[\\?&]"+name+"=([^&#]*)"; | |||||
var regex = new RegExp( regexS ); | |||||
var results = regex.exec( window.location.href ); | |||||
if( results == null ) | |||||
return ""; | |||||
else | |||||
return results[1]; | |||||
} | |||||
// get selected item count from url parameter | |||||
var count = (Number(gup('count')) || 1000); | |||||
// create groups | |||||
var groups = new vis.DataSet([ | |||||
{id: 1, content: 'Truck 1'}, | |||||
{id: 2, content: 'Truck 2'}, | |||||
{id: 3, content: 'Truck 3'}, | |||||
{id: 4, content: 'Truck 4'} | |||||
]); | |||||
// create items | |||||
var items = new vis.DataSet(); | |||||
var order = 1; | |||||
var truck = 1; | |||||
for (var j = 0; j < 4; j++) { | |||||
var date = new Date(); | |||||
for (var i = 0; i < count/4; i++) { | |||||
date.setHours(date.getHours() + 4 * (Math.random() < 0.2)); | |||||
var start = new Date(date); | |||||
date.setHours(date.getHours() + 2 + Math.floor(Math.random()*4)); | |||||
var end = new Date(date); | |||||
items.add({ | |||||
id: order, | |||||
group: truck, | |||||
start: start, | |||||
end: end, | |||||
content: 'Order ' + order | |||||
}); | |||||
order++; | |||||
} | |||||
truck++; | |||||
} | |||||
// specify options | |||||
var options = { | |||||
stack: false, | |||||
start: new Date(), | |||||
end: new Date(1000*60*60*24 + (new Date()).valueOf()), | |||||
editable: true, | |||||
margin: { | |||||
item: 10, // minimal margin between items | |||||
axis: 5 // minimal margin between items and the axis | |||||
}, | |||||
orientation: 'top' | |||||
}; | |||||
// create a Timeline | |||||
var container = document.getElementById('mytimeline'); | |||||
timeline = new vis.Timeline(container, null, options); | |||||
timeline.setGroups(groups); | |||||
timeline.setItems(items); | |||||
document.getElementById('count').innerHTML = count; | |||||
</script> | |||||
</body> | |||||
</html> |
@ -0,0 +1,115 @@ | |||||
<html> | |||||
<head> | |||||
<title>Timeline | Item class names</title> | |||||
<script src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
<style type="text/css"> | |||||
body, input { | |||||
font: 12pt verdana; | |||||
} | |||||
/* custom styles for individual items, load this after vis.css */ | |||||
.vis.timeline .item.green { | |||||
background-color: greenyellow; | |||||
border-color: green; | |||||
} | |||||
/* create a custom sized dot at the bottom of the red item */ | |||||
.vis.timeline .item.red { | |||||
background-color: red; | |||||
border-color: darkred; | |||||
color: white; | |||||
font-family: monospace; | |||||
box-shadow: 0 0 10px gray; | |||||
} | |||||
.vis.timeline .item.dot.red { | |||||
border-radius: 10px; | |||||
border-width: 10px; | |||||
} | |||||
.vis.timeline .item.line.red { | |||||
border-width: 5px; | |||||
} | |||||
.vis.timeline .item.box.red { | |||||
border-radius: 0; | |||||
border-width: 2px; | |||||
font-size: 24pt; | |||||
font-weight: bold; | |||||
} | |||||
.vis.timeline .item.orange { | |||||
background-color: gold; | |||||
border-color: orange; | |||||
} | |||||
.vis.timeline .item.orange.selected { | |||||
/* custom colors for selected orange items */ | |||||
background-color: orange; | |||||
border-color: orangered; | |||||
} | |||||
.vis.timeline .item.magenta { | |||||
background-color: magenta; | |||||
border-color: purple; | |||||
color: white; | |||||
} | |||||
/* our custom classes overrule the styles for selected events, | |||||
so lets define a new style for the selected events */ | |||||
.vis.timeline .item.selected { | |||||
background-color: white; | |||||
border-color: black; | |||||
color: black; | |||||
box-shadow: 0 0 10px gray; | |||||
} | |||||
</style> | |||||
</head> | |||||
<body> | |||||
<p>This page demonstrates the Timeline with custom css classes for individual items.</p> | |||||
<div id="mytimeline"></div> | |||||
<script type="text/javascript"> | |||||
// create data | |||||
var data = [ | |||||
{ | |||||
'start': new Date(2012,7,19), | |||||
'content': 'default' | |||||
}, | |||||
{ | |||||
'start': new Date(2012,7,23), | |||||
'content': 'green', | |||||
'className': 'green' | |||||
}, | |||||
{ | |||||
'start': new Date(2012,7,29), | |||||
'content': 'red', | |||||
'className': 'red' | |||||
}, | |||||
{ | |||||
'start': new Date(2012,7,27), | |||||
'end': new Date(2012,8,1), | |||||
'content': 'orange', | |||||
'className': 'orange' | |||||
}, | |||||
{ | |||||
'start': new Date(2012,8,2), | |||||
'content': 'magenta', | |||||
'className': 'magenta' | |||||
} | |||||
]; | |||||
// specify options | |||||
var options = { | |||||
editable: true | |||||
}; | |||||
// create the timeline | |||||
var container = document.getElementById('mytimeline'); | |||||
timeline = new vis.Timeline(container, data, options); | |||||
</script> | |||||
</body> | |||||
</html> |
@ -1,183 +0,0 @@ | |||||
/** | |||||
* @constructor Controller | |||||
* | |||||
* A Controller controls the reflows and repaints of all components, | |||||
* and is used as an event bus for all components. | |||||
*/ | |||||
function Controller () { | |||||
var me = this; | |||||
this.id = util.randomUUID(); | |||||
this.components = {}; | |||||
/** | |||||
* Listen for a 'request-reflow' event. The controller will schedule a reflow | |||||
* @param {Boolean} [force] If true, an immediate reflow is forced. Default | |||||
* is false. | |||||
*/ | |||||
var reflowTimer = null; | |||||
this.on('request-reflow', function requestReflow(force) { | |||||
if (force) { | |||||
me.reflow(); | |||||
} | |||||
else { | |||||
if (!reflowTimer) { | |||||
reflowTimer = setTimeout(function () { | |||||
reflowTimer = null; | |||||
me.reflow(); | |||||
}, 0); | |||||
} | |||||
} | |||||
}); | |||||
/** | |||||
* Request a repaint. The controller will schedule a repaint | |||||
* @param {Boolean} [force] If true, an immediate repaint is forced. Default | |||||
* is false. | |||||
*/ | |||||
var repaintTimer = null; | |||||
this.on('request-repaint', function requestRepaint(force) { | |||||
if (force) { | |||||
me.repaint(); | |||||
} | |||||
else { | |||||
if (!repaintTimer) { | |||||
repaintTimer = setTimeout(function () { | |||||
repaintTimer = null; | |||||
me.repaint(); | |||||
}, 0); | |||||
} | |||||
} | |||||
}); | |||||
} | |||||
// Extend controller with Emitter mixin | |||||
Emitter(Controller.prototype); | |||||
/** | |||||
* Add a component to the controller | |||||
* @param {Component} component | |||||
*/ | |||||
Controller.prototype.add = function add(component) { | |||||
// validate the component | |||||
if (component.id == undefined) { | |||||
throw new Error('Component has no field id'); | |||||
} | |||||
if (!(component instanceof Component) && !(component instanceof Controller)) { | |||||
throw new TypeError('Component must be an instance of ' + | |||||
'prototype Component or Controller'); | |||||
} | |||||
// add the component | |||||
component.setController(this); | |||||
this.components[component.id] = component; | |||||
}; | |||||
/** | |||||
* Remove a component from the controller | |||||
* @param {Component | String} component | |||||
*/ | |||||
Controller.prototype.remove = function remove(component) { | |||||
var id; | |||||
for (id in this.components) { | |||||
if (this.components.hasOwnProperty(id)) { | |||||
if (id == component || this.components[id] === component) { | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
if (id) { | |||||
// unregister the controller (gives the component the ability to unregister | |||||
// event listeners and clean up other stuff) | |||||
this.components[id].setController(null); | |||||
delete this.components[id]; | |||||
} | |||||
}; | |||||
/** | |||||
* Repaint all components | |||||
*/ | |||||
Controller.prototype.repaint = function repaint() { | |||||
var changed = false; | |||||
// cancel any running repaint request | |||||
if (this.repaintTimer) { | |||||
clearTimeout(this.repaintTimer); | |||||
this.repaintTimer = undefined; | |||||
} | |||||
var done = {}; | |||||
function repaint(component, id) { | |||||
if (!(id in done)) { | |||||
// first repaint the components on which this component is dependent | |||||
if (component.depends) { | |||||
component.depends.forEach(function (dep) { | |||||
repaint(dep, dep.id); | |||||
}); | |||||
} | |||||
if (component.parent) { | |||||
repaint(component.parent, component.parent.id); | |||||
} | |||||
// repaint the component itself and mark as done | |||||
changed = component.repaint() || changed; | |||||
done[id] = true; | |||||
} | |||||
} | |||||
util.forEach(this.components, repaint); | |||||
this.emit('repaint'); | |||||
// immediately reflow when needed | |||||
if (changed) { | |||||
this.reflow(); | |||||
} | |||||
// TODO: limit the number of nested reflows/repaints, prevent loop | |||||
}; | |||||
/** | |||||
* Reflow all components | |||||
*/ | |||||
Controller.prototype.reflow = function reflow() { | |||||
var resized = false; | |||||
// cancel any running repaint request | |||||
if (this.reflowTimer) { | |||||
clearTimeout(this.reflowTimer); | |||||
this.reflowTimer = undefined; | |||||
} | |||||
var done = {}; | |||||
function reflow(component, id) { | |||||
if (!(id in done)) { | |||||
// first reflow the components on which this component is dependent | |||||
if (component.depends) { | |||||
component.depends.forEach(function (dep) { | |||||
reflow(dep, dep.id); | |||||
}); | |||||
} | |||||
if (component.parent) { | |||||
reflow(component.parent, component.parent.id); | |||||
} | |||||
// reflow the component itself and mark as done | |||||
resized = component.reflow() || resized; | |||||
done[id] = true; | |||||
} | |||||
} | |||||
util.forEach(this.components, reflow); | |||||
this.emit('reflow'); | |||||
// immediately repaint when needed | |||||
if (resized) { | |||||
this.repaint(); | |||||
} | |||||
// TODO: limit the number of nested reflows/repaints, prevent loop | |||||
}; |
@ -1,190 +0,0 @@ | |||||
/** | |||||
* @constructor Stack | |||||
* Stacks items on top of each other. | |||||
* @param {ItemSet} itemset | |||||
* @param {Object} [options] | |||||
*/ | |||||
function Stack (itemset, options) { | |||||
this.itemset = itemset; | |||||
this.options = options || {}; | |||||
this.defaultOptions = { | |||||
order: function (a, b) { | |||||
//return (b.width - a.width) || (a.left - b.left); // TODO: cleanup | |||||
// Order: ranges over non-ranges, ranged ordered by width, and | |||||
// lastly ordered by start. | |||||
if (a instanceof ItemRange) { | |||||
if (b instanceof ItemRange) { | |||||
var aInt = (a.data.end - a.data.start); | |||||
var bInt = (b.data.end - b.data.start); | |||||
return (aInt - bInt) || (a.data.start - b.data.start); | |||||
} | |||||
else { | |||||
return -1; | |||||
} | |||||
} | |||||
else { | |||||
if (b instanceof ItemRange) { | |||||
return 1; | |||||
} | |||||
else { | |||||
return (a.data.start - b.data.start); | |||||
} | |||||
} | |||||
}, | |||||
margin: { | |||||
item: 10 | |||||
} | |||||
}; | |||||
this.ordered = []; // ordered items | |||||
} | |||||
/** | |||||
* Set options for the stack | |||||
* @param {Object} options Available options: | |||||
* {ItemSet} itemset | |||||
* {Number} margin | |||||
* {function} order Stacking order | |||||
*/ | |||||
Stack.prototype.setOptions = function setOptions (options) { | |||||
util.extend(this.options, options); | |||||
// TODO: register on data changes at the connected itemset, and update the changed part only and immediately | |||||
}; | |||||
/** | |||||
* Stack the items such that they don't overlap. The items will have a minimal | |||||
* distance equal to options.margin.item. | |||||
*/ | |||||
Stack.prototype.update = function update() { | |||||
this._order(); | |||||
this._stack(); | |||||
}; | |||||
/** | |||||
* Order the items. If a custom order function has been provided via the options, | |||||
* then this will be used. | |||||
* @private | |||||
*/ | |||||
Stack.prototype._order = function _order () { | |||||
var items = this.itemset.items; | |||||
if (!items) { | |||||
throw new Error('Cannot stack items: ItemSet does not contain items'); | |||||
} | |||||
// TODO: store the sorted items, to have less work later on | |||||
var ordered = []; | |||||
var index = 0; | |||||
// items is a map (no array) | |||||
util.forEach(items, function (item) { | |||||
if (item.visible) { | |||||
ordered[index] = item; | |||||
index++; | |||||
} | |||||
}); | |||||
//if a customer stack order function exists, use it. | |||||
var order = this.options.order || this.defaultOptions.order; | |||||
if (!(typeof order === 'function')) { | |||||
throw new Error('Option order must be a function'); | |||||
} | |||||
ordered.sort(order); | |||||
this.ordered = ordered; | |||||
}; | |||||
/** | |||||
* Adjust vertical positions of the events such that they don't overlap each | |||||
* other. | |||||
* @private | |||||
*/ | |||||
Stack.prototype._stack = function _stack () { | |||||
var i, | |||||
iMax, | |||||
ordered = this.ordered, | |||||
options = this.options, | |||||
orientation = options.orientation || this.defaultOptions.orientation, | |||||
axisOnTop = (orientation == 'top'), | |||||
margin; | |||||
if (options.margin && options.margin.item !== undefined) { | |||||
margin = options.margin.item; | |||||
} | |||||
else { | |||||
margin = this.defaultOptions.margin.item | |||||
} | |||||
// calculate new, non-overlapping positions | |||||
for (i = 0, iMax = ordered.length; i < iMax; i++) { | |||||
var item = ordered[i]; | |||||
var collidingItem = null; | |||||
do { | |||||
// TODO: optimize checking for overlap. when there is a gap without items, | |||||
// you only need to check for items from the next item on, not from zero | |||||
collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin); | |||||
if (collidingItem != null) { | |||||
// There is a collision. Reposition the event above the colliding element | |||||
if (axisOnTop) { | |||||
item.top = collidingItem.top + collidingItem.height + margin; | |||||
} | |||||
else { | |||||
item.top = collidingItem.top - item.height - margin; | |||||
} | |||||
} | |||||
} while (collidingItem); | |||||
} | |||||
}; | |||||
/** | |||||
* Check if the destiny position of given item overlaps with any | |||||
* of the other items from index itemStart to itemEnd. | |||||
* @param {Array} items Array with items | |||||
* @param {int} itemIndex Number of the item to be checked for overlap | |||||
* @param {int} itemStart First item to be checked. | |||||
* @param {int} itemEnd Last item to be checked. | |||||
* @return {Object | null} colliding item, or undefined when no collisions | |||||
* @param {Number} margin A minimum required margin. | |||||
* If margin is provided, the two items will be | |||||
* marked colliding when they overlap or | |||||
* when the margin between the two is smaller than | |||||
* the requested margin. | |||||
*/ | |||||
Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex, | |||||
itemStart, itemEnd, margin) { | |||||
var collision = this.collision; | |||||
// we loop from end to start, as we suppose that the chance of a | |||||
// collision is larger for items at the end, so check these first. | |||||
var a = items[itemIndex]; | |||||
for (var i = itemEnd; i >= itemStart; i--) { | |||||
var b = items[i]; | |||||
if (collision(a, b, margin)) { | |||||
if (i != itemIndex) { | |||||
return b; | |||||
} | |||||
} | |||||
} | |||||
return null; | |||||
}; | |||||
/** | |||||
* Test if the two provided items collide | |||||
* The items must have parameters left, width, top, and height. | |||||
* @param {Component} a The first item | |||||
* @param {Component} b The second item | |||||
* @param {Number} margin A minimum required margin. | |||||
* If margin is provided, the two items will be | |||||
* marked colliding when they overlap or | |||||
* when the margin between the two is smaller than | |||||
* the requested margin. | |||||
* @return {boolean} true if a and b collide, else false | |||||
*/ | |||||
Stack.prototype.collision = function collision (a, b, margin) { | |||||
return ((a.left - margin) < (b.left + b.width) && | |||||
(a.left + a.width + margin) > b.left && | |||||
(a.top - margin) < (b.top + b.height) && | |||||
(a.top + a.height + margin) > b.top); | |||||
}; |
@ -1,113 +0,0 @@ | |||||
/** | |||||
* A content panel can contain a groupset or an itemset, and can handle | |||||
* vertical scrolling | |||||
* @param {Component} [parent] | |||||
* @param {Component[]} [depends] Components on which this components depends | |||||
* (except for the parent) | |||||
* @param {Object} [options] Available parameters: | |||||
* {String | Number | function} [left] | |||||
* {String | Number | function} [top] | |||||
* {String | Number | function} [width] | |||||
* {String | Number | function} [height] | |||||
* {String | function} [className] | |||||
* @constructor ContentPanel | |||||
* @extends Panel | |||||
*/ | |||||
function ContentPanel(parent, depends, options) { | |||||
this.id = util.randomUUID(); | |||||
this.parent = parent; | |||||
this.depends = depends; | |||||
this.options = options || {}; | |||||
} | |||||
ContentPanel.prototype = new Component(); | |||||
/** | |||||
* Set options. Will extend the current options. | |||||
* @param {Object} [options] Available parameters: | |||||
* {String | function} [className] | |||||
* {String | Number | function} [left] | |||||
* {String | Number | function} [top] | |||||
* {String | Number | function} [width] | |||||
* {String | Number | function} [height] | |||||
*/ | |||||
ContentPanel.prototype.setOptions = Component.prototype.setOptions; | |||||
/** | |||||
* Get the container element of the panel, which can be used by a child to | |||||
* add its own widgets. | |||||
* @returns {HTMLElement} container | |||||
*/ | |||||
ContentPanel.prototype.getContainer = function () { | |||||
return this.frame; | |||||
}; | |||||
/** | |||||
* Repaint the component | |||||
* @return {Boolean} changed | |||||
*/ | |||||
ContentPanel.prototype.repaint = function () { | |||||
var changed = 0, | |||||
update = util.updateProperty, | |||||
asSize = util.option.asSize, | |||||
options = this.options, | |||||
frame = this.frame; | |||||
if (!frame) { | |||||
frame = document.createElement('div'); | |||||
frame.className = 'content-panel'; | |||||
var className = options.className; | |||||
if (className) { | |||||
if (typeof className == 'function') { | |||||
util.addClassName(frame, String(className())); | |||||
} | |||||
else { | |||||
util.addClassName(frame, String(className)); | |||||
} | |||||
} | |||||
this.frame = frame; | |||||
changed += 1; | |||||
} | |||||
if (!frame.parentNode) { | |||||
if (!this.parent) { | |||||
throw new Error('Cannot repaint panel: no parent attached'); | |||||
} | |||||
var parentContainer = this.parent.getContainer(); | |||||
if (!parentContainer) { | |||||
throw new Error('Cannot repaint panel: parent has no container element'); | |||||
} | |||||
parentContainer.appendChild(frame); | |||||
changed += 1; | |||||
} | |||||
changed += update(frame.style, 'top', asSize(options.top, '0px')); | |||||
changed += update(frame.style, 'left', asSize(options.left, '0px')); | |||||
changed += update(frame.style, 'width', asSize(options.width, '100%')); | |||||
changed += update(frame.style, 'height', asSize(options.height, '100%')); | |||||
return (changed > 0); | |||||
}; | |||||
/** | |||||
* Reflow the component | |||||
* @return {Boolean} resized | |||||
*/ | |||||
ContentPanel.prototype.reflow = function () { | |||||
var changed = 0, | |||||
update = util.updateProperty, | |||||
frame = this.frame; | |||||
if (frame) { | |||||
changed += update(this, 'top', frame.offsetTop); | |||||
changed += update(this, 'left', frame.offsetLeft); | |||||
changed += update(this, 'width', frame.offsetWidth); | |||||
changed += update(this, 'height', frame.offsetHeight); | |||||
} | |||||
else { | |||||
changed += 1; | |||||
} | |||||
return (changed > 0); | |||||
}; |
@ -1,580 +0,0 @@ | |||||
/** | |||||
* An GroupSet holds a set of groups | |||||
* @param {Component} parent | |||||
* @param {Component[]} [depends] Components on which this components depends | |||||
* (except for the parent) | |||||
* @param {Object} [options] See GroupSet.setOptions for the available | |||||
* options. | |||||
* @constructor GroupSet | |||||
* @extends Panel | |||||
*/ | |||||
function GroupSet(parent, depends, options) { | |||||
this.id = util.randomUUID(); | |||||
this.parent = parent; | |||||
this.depends = depends; | |||||
this.options = options || {}; | |||||
this.range = null; // Range or Object {start: number, end: number} | |||||
this.itemsData = null; // DataSet with items | |||||
this.groupsData = null; // DataSet with groups | |||||
this.groups = {}; // map with groups | |||||
this.dom = {}; | |||||
this.props = { | |||||
labels: { | |||||
width: 0 | |||||
} | |||||
}; | |||||
// TODO: implement right orientation of the labels | |||||
// changes in groups are queued key/value map containing id/action | |||||
this.queue = {}; | |||||
var me = this; | |||||
this.listeners = { | |||||
'add': function (event, params) { | |||||
me._onAdd(params.items); | |||||
}, | |||||
'update': function (event, params) { | |||||
me._onUpdate(params.items); | |||||
}, | |||||
'remove': function (event, params) { | |||||
me._onRemove(params.items); | |||||
} | |||||
}; | |||||
} | |||||
GroupSet.prototype = new Panel(); | |||||
/** | |||||
* Set options for the GroupSet. Existing options will be extended/overwritten. | |||||
* @param {Object} [options] The following options are available: | |||||
* {String | function} groupsOrder | |||||
* TODO: describe options | |||||
*/ | |||||
GroupSet.prototype.setOptions = Component.prototype.setOptions; | |||||
GroupSet.prototype.setRange = function (range) { | |||||
// TODO: implement setRange | |||||
}; | |||||
/** | |||||
* Set items | |||||
* @param {vis.DataSet | null} items | |||||
*/ | |||||
GroupSet.prototype.setItems = function setItems(items) { | |||||
this.itemsData = items; | |||||
for (var id in this.groups) { | |||||
if (this.groups.hasOwnProperty(id)) { | |||||
var group = this.groups[id]; | |||||
group.setItems(items); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* Get items | |||||
* @return {vis.DataSet | null} items | |||||
*/ | |||||
GroupSet.prototype.getItems = function getItems() { | |||||
return this.itemsData; | |||||
}; | |||||
/** | |||||
* Set range (start and end). | |||||
* @param {Range | Object} range A Range or an object containing start and end. | |||||
*/ | |||||
GroupSet.prototype.setRange = function setRange(range) { | |||||
this.range = range; | |||||
}; | |||||
/** | |||||
* Set groups | |||||
* @param {vis.DataSet} groups | |||||
*/ | |||||
GroupSet.prototype.setGroups = function setGroups(groups) { | |||||
var me = this, | |||||
ids; | |||||
// unsubscribe from current dataset | |||||
if (this.groupsData) { | |||||
util.forEach(this.listeners, function (callback, event) { | |||||
me.groupsData.unsubscribe(event, callback); | |||||
}); | |||||
// remove all drawn groups | |||||
ids = this.groupsData.getIds(); | |||||
this._onRemove(ids); | |||||
} | |||||
// replace the dataset | |||||
if (!groups) { | |||||
this.groupsData = null; | |||||
} | |||||
else if (groups instanceof DataSet) { | |||||
this.groupsData = groups; | |||||
} | |||||
else { | |||||
this.groupsData = new DataSet({ | |||||
convert: { | |||||
start: 'Date', | |||||
end: 'Date' | |||||
} | |||||
}); | |||||
this.groupsData.add(groups); | |||||
} | |||||
if (this.groupsData) { | |||||
// subscribe to new dataset | |||||
var id = this.id; | |||||
util.forEach(this.listeners, function (callback, event) { | |||||
me.groupsData.on(event, callback, id); | |||||
}); | |||||
// draw all new groups | |||||
ids = this.groupsData.getIds(); | |||||
this._onAdd(ids); | |||||
} | |||||
}; | |||||
/** | |||||
* Get groups | |||||
* @return {vis.DataSet | null} groups | |||||
*/ | |||||
GroupSet.prototype.getGroups = function getGroups() { | |||||
return this.groupsData; | |||||
}; | |||||
/** | |||||
* Set selected items by their id. Replaces the current selection. | |||||
* Unknown id's are silently ignored. | |||||
* @param {Array} [ids] An array with zero or more id's of the items to be | |||||
* selected. If ids is an empty array, all items will be | |||||
* unselected. | |||||
*/ | |||||
GroupSet.prototype.setSelection = function setSelection(ids) { | |||||
var selection = [], | |||||
groups = this.groups; | |||||
// iterate over each of the groups | |||||
for (var id in groups) { | |||||
if (groups.hasOwnProperty(id)) { | |||||
var group = groups[id]; | |||||
group.setSelection(ids); | |||||
} | |||||
} | |||||
return selection; | |||||
}; | |||||
/** | |||||
* Get the selected items by their id | |||||
* @return {Array} ids The ids of the selected items | |||||
*/ | |||||
GroupSet.prototype.getSelection = function getSelection() { | |||||
var selection = [], | |||||
groups = this.groups; | |||||
// iterate over each of the groups | |||||
for (var id in groups) { | |||||
if (groups.hasOwnProperty(id)) { | |||||
var group = groups[id]; | |||||
selection = selection.concat(group.getSelection()); | |||||
} | |||||
} | |||||
return selection; | |||||
}; | |||||
/** | |||||
* Repaint the component | |||||
* @return {Boolean} changed | |||||
*/ | |||||
GroupSet.prototype.repaint = function repaint() { | |||||
var changed = 0, | |||||
i, id, group, label, | |||||
update = util.updateProperty, | |||||
asSize = util.option.asSize, | |||||
asElement = util.option.asElement, | |||||
options = this.options, | |||||
frame = this.dom.frame, | |||||
labels = this.dom.labels, | |||||
labelSet = this.dom.labelSet; | |||||
// create frame | |||||
if (!this.parent) { | |||||
throw new Error('Cannot repaint groupset: no parent attached'); | |||||
} | |||||
var parentContainer = this.parent.getContainer(); | |||||
if (!parentContainer) { | |||||
throw new Error('Cannot repaint groupset: parent has no container element'); | |||||
} | |||||
if (!frame) { | |||||
frame = document.createElement('div'); | |||||
frame.className = 'groupset'; | |||||
frame['timeline-groupset'] = this; | |||||
this.dom.frame = frame; | |||||
var className = options.className; | |||||
if (className) { | |||||
util.addClassName(frame, util.option.asString(className)); | |||||
} | |||||
changed += 1; | |||||
} | |||||
if (!frame.parentNode) { | |||||
parentContainer.appendChild(frame); | |||||
changed += 1; | |||||
} | |||||
// create labels | |||||
var labelContainer = asElement(options.labelContainer); | |||||
if (!labelContainer) { | |||||
throw new Error('Cannot repaint groupset: option "labelContainer" not defined'); | |||||
} | |||||
if (!labels) { | |||||
labels = document.createElement('div'); | |||||
labels.className = 'labels'; | |||||
this.dom.labels = labels; | |||||
} | |||||
if (!labelSet) { | |||||
labelSet = document.createElement('div'); | |||||
labelSet.className = 'label-set'; | |||||
labels.appendChild(labelSet); | |||||
this.dom.labelSet = labelSet; | |||||
} | |||||
if (!labels.parentNode || labels.parentNode != labelContainer) { | |||||
if (labels.parentNode) { | |||||
labels.parentNode.removeChild(labels.parentNode); | |||||
} | |||||
labelContainer.appendChild(labels); | |||||
} | |||||
// reposition frame | |||||
changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); | |||||
changed += update(frame.style, 'top', asSize(options.top, '0px')); | |||||
changed += update(frame.style, 'left', asSize(options.left, '0px')); | |||||
changed += update(frame.style, 'width', asSize(options.width, '100%')); | |||||
// reposition labels | |||||
changed += update(labelSet.style, 'top', asSize(options.top, '0px')); | |||||
changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px')); | |||||
var me = this, | |||||
queue = this.queue, | |||||
groups = this.groups, | |||||
groupsData = this.groupsData; | |||||
// show/hide added/changed/removed groups | |||||
var ids = Object.keys(queue); | |||||
if (ids.length) { | |||||
ids.forEach(function (id) { | |||||
var action = queue[id]; | |||||
var group = groups[id]; | |||||
//noinspection FallthroughInSwitchStatementJS | |||||
switch (action) { | |||||
case 'add': | |||||
case 'update': | |||||
if (!group) { | |||||
var groupOptions = Object.create(me.options); | |||||
util.extend(groupOptions, { | |||||
height: null, | |||||
maxHeight: null | |||||
}); | |||||
group = new Group(me, id, groupOptions); | |||||
group.setItems(me.itemsData); // attach items data | |||||
groups[id] = group; | |||||
me.controller.add(group); | |||||
} | |||||
// TODO: update group data | |||||
group.data = groupsData.get(id); | |||||
delete queue[id]; | |||||
break; | |||||
case 'remove': | |||||
if (group) { | |||||
group.setItems(); // detach items data | |||||
delete groups[id]; | |||||
me.controller.remove(group); | |||||
} | |||||
// update lists | |||||
delete queue[id]; | |||||
break; | |||||
default: | |||||
console.log('Error: unknown action "' + action + '"'); | |||||
} | |||||
}); | |||||
// the groupset depends on each of the groups | |||||
//this.depends = this.groups; // TODO: gives a circular reference through the parent | |||||
// TODO: apply dependencies of the groupset | |||||
// update the top positions of the groups in the correct order | |||||
var orderedGroups = this.groupsData.getIds({ | |||||
order: this.options.groupOrder | |||||
}); | |||||
for (i = 0; i < orderedGroups.length; i++) { | |||||
(function (group, prevGroup) { | |||||
var top = 0; | |||||
if (prevGroup) { | |||||
top = function () { | |||||
// TODO: top must reckon with options.maxHeight | |||||
return prevGroup.top + prevGroup.height; | |||||
} | |||||
} | |||||
group.setOptions({ | |||||
top: top | |||||
}); | |||||
})(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]); | |||||
} | |||||
// (re)create the labels | |||||
while (labelSet.firstChild) { | |||||
labelSet.removeChild(labelSet.firstChild); | |||||
} | |||||
for (i = 0; i < orderedGroups.length; i++) { | |||||
id = orderedGroups[i]; | |||||
label = this._createLabel(id); | |||||
labelSet.appendChild(label); | |||||
} | |||||
changed++; | |||||
} | |||||
// reposition the labels | |||||
// TODO: labels are not displayed correctly when orientation=='top' | |||||
// TODO: width of labelPanel is not immediately updated on a change in groups | |||||
for (id in groups) { | |||||
if (groups.hasOwnProperty(id)) { | |||||
group = groups[id]; | |||||
label = group.label; | |||||
if (label) { | |||||
label.style.top = group.top + 'px'; | |||||
label.style.height = group.height + 'px'; | |||||
} | |||||
} | |||||
} | |||||
return (changed > 0); | |||||
}; | |||||
/** | |||||
* Create a label for group with given id | |||||
* @param {Number} id | |||||
* @return {Element} label | |||||
* @private | |||||
*/ | |||||
GroupSet.prototype._createLabel = function(id) { | |||||
var group = this.groups[id]; | |||||
var label = document.createElement('div'); | |||||
label.className = 'vlabel'; | |||||
var inner = document.createElement('div'); | |||||
inner.className = 'inner'; | |||||
label.appendChild(inner); | |||||
var content = group.data && group.data.content; | |||||
if (content instanceof Element) { | |||||
inner.appendChild(content); | |||||
} | |||||
else if (content != undefined) { | |||||
inner.innerHTML = content; | |||||
} | |||||
var className = group.data && group.data.className; | |||||
if (className) { | |||||
util.addClassName(label, className); | |||||
} | |||||
group.label = label; // TODO: not so nice, parking labels in the group this way!!! | |||||
return label; | |||||
}; | |||||
/** | |||||
* Get container element | |||||
* @return {HTMLElement} container | |||||
*/ | |||||
GroupSet.prototype.getContainer = function getContainer() { | |||||
return this.dom.frame; | |||||
}; | |||||
/** | |||||
* Get the width of the group labels | |||||
* @return {Number} width | |||||
*/ | |||||
GroupSet.prototype.getLabelsWidth = function getContainer() { | |||||
return this.props.labels.width; | |||||
}; | |||||
/** | |||||
* Reflow the component | |||||
* @return {Boolean} resized | |||||
*/ | |||||
GroupSet.prototype.reflow = function reflow() { | |||||
var changed = 0, | |||||
id, group, | |||||
options = this.options, | |||||
update = util.updateProperty, | |||||
asNumber = util.option.asNumber, | |||||
asSize = util.option.asSize, | |||||
frame = this.dom.frame; | |||||
if (frame) { | |||||
var maxHeight = asNumber(options.maxHeight); | |||||
var fixedHeight = (asSize(options.height) != null); | |||||
var height; | |||||
if (fixedHeight) { | |||||
height = frame.offsetHeight; | |||||
} | |||||
else { | |||||
// height is not specified, calculate the sum of the height of all groups | |||||
height = 0; | |||||
for (id in this.groups) { | |||||
if (this.groups.hasOwnProperty(id)) { | |||||
group = this.groups[id]; | |||||
height += group.height; | |||||
} | |||||
} | |||||
} | |||||
if (maxHeight != null) { | |||||
height = Math.min(height, maxHeight); | |||||
} | |||||
changed += update(this, 'height', height); | |||||
changed += update(this, 'top', frame.offsetTop); | |||||
changed += update(this, 'left', frame.offsetLeft); | |||||
changed += update(this, 'width', frame.offsetWidth); | |||||
} | |||||
// calculate the maximum width of the labels | |||||
var width = 0; | |||||
for (id in this.groups) { | |||||
if (this.groups.hasOwnProperty(id)) { | |||||
group = this.groups[id]; | |||||
var labelWidth = group.props && group.props.label && group.props.label.width || 0; | |||||
width = Math.max(width, labelWidth); | |||||
} | |||||
} | |||||
changed += update(this.props.labels, 'width', width); | |||||
return (changed > 0); | |||||
}; | |||||
/** | |||||
* Hide the component from the DOM | |||||
* @return {Boolean} changed | |||||
*/ | |||||
GroupSet.prototype.hide = function hide() { | |||||
if (this.dom.frame && this.dom.frame.parentNode) { | |||||
this.dom.frame.parentNode.removeChild(this.dom.frame); | |||||
return true; | |||||
} | |||||
else { | |||||
return false; | |||||
} | |||||
}; | |||||
/** | |||||
* Show the component in the DOM (when not already visible). | |||||
* A repaint will be executed when the component is not visible | |||||
* @return {Boolean} changed | |||||
*/ | |||||
GroupSet.prototype.show = function show() { | |||||
if (!this.dom.frame || !this.dom.frame.parentNode) { | |||||
return this.repaint(); | |||||
} | |||||
else { | |||||
return false; | |||||
} | |||||
}; | |||||
/** | |||||
* Handle updated groups | |||||
* @param {Number[]} ids | |||||
* @private | |||||
*/ | |||||
GroupSet.prototype._onUpdate = function _onUpdate(ids) { | |||||
this._toQueue(ids, 'update'); | |||||
}; | |||||
/** | |||||
* Handle changed groups | |||||
* @param {Number[]} ids | |||||
* @private | |||||
*/ | |||||
GroupSet.prototype._onAdd = function _onAdd(ids) { | |||||
this._toQueue(ids, 'add'); | |||||
}; | |||||
/** | |||||
* Handle removed groups | |||||
* @param {Number[]} ids | |||||
* @private | |||||
*/ | |||||
GroupSet.prototype._onRemove = function _onRemove(ids) { | |||||
this._toQueue(ids, 'remove'); | |||||
}; | |||||
/** | |||||
* Put groups in the queue to be added/updated/remove | |||||
* @param {Number[]} ids | |||||
* @param {String} action can be 'add', 'update', 'remove' | |||||
*/ | |||||
GroupSet.prototype._toQueue = function _toQueue(ids, action) { | |||||
var queue = this.queue; | |||||
ids.forEach(function (id) { | |||||
queue[id] = action; | |||||
}); | |||||
if (this.controller) { | |||||
//this.requestReflow(); | |||||
this.requestRepaint(); | |||||
} | |||||
}; | |||||
/** | |||||
* Find the Group from an event target: | |||||
* searches for the attribute 'timeline-groupset' in the event target's element | |||||
* tree, then finds the right group in this groupset | |||||
* @param {Event} event | |||||
* @return {Group | null} group | |||||
*/ | |||||
GroupSet.groupFromTarget = function groupFromTarget (event) { | |||||
var groupset, | |||||
target = event.target; | |||||
while (target) { | |||||
if (target.hasOwnProperty('timeline-groupset')) { | |||||
groupset = target['timeline-groupset']; | |||||
break; | |||||
} | |||||
target = target.parentNode; | |||||
} | |||||
if (groupset) { | |||||
for (var groupId in groupset.groups) { | |||||
if (groupset.groups.hasOwnProperty(groupId)) { | |||||
var group = groupset.groups[groupId]; | |||||
if (group.itemset && ItemSet.itemSetFromTarget(event) == group.itemset) { | |||||
return group; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
return null; | |||||
}; |
@ -1,59 +0,0 @@ | |||||
.vis.timeline .groupset { | |||||
position: absolute; | |||||
padding: 0; | |||||
margin: 0; | |||||
} | |||||
.vis.timeline .labels { | |||||
position: absolute; | |||||
top: 0; | |||||
left: 0; | |||||
width: 100%; | |||||
height: 100%; | |||||
padding: 0; | |||||
margin: 0; | |||||
border-right: 1px solid #bfbfbf; | |||||
-moz-box-sizing: border-box; | |||||
box-sizing: border-box; | |||||
} | |||||
.vis.timeline .labels .label-set { | |||||
position: absolute; | |||||
top: 0; | |||||
left: 0; | |||||
width: 100%; | |||||
height: 100%; | |||||
overflow: hidden; | |||||
border-top: none; | |||||
border-bottom: 1px solid #bfbfbf; | |||||
} | |||||
.vis.timeline .labels .label-set .vlabel { | |||||
position: absolute; | |||||
left: 0; | |||||
top: 0; | |||||
width: 100%; | |||||
color: #4d4d4d; | |||||
} | |||||
.vis.timeline.top .labels .label-set .vlabel, | |||||
.vis.timeline.top .groupset .itemset-axis { | |||||
border-top: 1px solid #bfbfbf; | |||||
border-bottom: none; | |||||
} | |||||
.vis.timeline.bottom .labels .label-set .vlabel, | |||||
.vis.timeline.bottom .groupset .itemset-axis { | |||||
border-top: none; | |||||
border-bottom: 1px solid #bfbfbf; | |||||
} | |||||
.vis.timeline .labels .label-set .vlabel .inner { | |||||
display: inline-block; | |||||
padding: 5px; | |||||
} |
@ -0,0 +1,34 @@ | |||||
.vis.timeline .labelset { | |||||
position: relative; | |||||
width: 100%; | |||||
overflow: hidden; | |||||
box-sizing: border-box; | |||||
} | |||||
.vis.timeline .labelset .vlabel { | |||||
position: relative; | |||||
left: 0; | |||||
top: 0; | |||||
width: 100%; | |||||
color: #4d4d4d; | |||||
box-sizing: border-box; | |||||
} | |||||
.vis.timeline.top .labelset .vlabel { | |||||
border-top: 1px solid #bfbfbf; | |||||
border-bottom: none; | |||||
} | |||||
.vis.timeline.bottom .labelset .vlabel { | |||||
border-top: none; | |||||
border-bottom: 1px solid #bfbfbf; | |||||
} | |||||
.vis.timeline .labelset .vlabel .inner { | |||||
display: inline-block; | |||||
padding: 5px; | |||||
} |
@ -1,304 +1,230 @@ | |||||
/** | /** | ||||
* @constructor ItemBox | * @constructor ItemBox | ||||
* @extends Item | * @extends Item | ||||
* @param {ItemSet} parent | |||||
* @param {Object} data Object containing parameters start | * @param {Object} data Object containing parameters start | ||||
* content, className. | * content, className. | ||||
* @param {Object} [options] Options to set initial property values | * @param {Object} [options] Options to set initial property values | ||||
* @param {Object} [defaultOptions] default options | * @param {Object} [defaultOptions] default options | ||||
* // TODO: describe available options | * // TODO: describe available options | ||||
*/ | */ | ||||
function ItemBox (parent, data, options, defaultOptions) { | |||||
function ItemBox (data, options, defaultOptions) { | |||||
this.props = { | this.props = { | ||||
dot: { | dot: { | ||||
left: 0, | |||||
top: 0, | |||||
width: 0, | width: 0, | ||||
height: 0 | height: 0 | ||||
}, | }, | ||||
line: { | line: { | ||||
top: 0, | |||||
left: 0, | |||||
width: 0, | width: 0, | ||||
height: 0 | height: 0 | ||||
} | } | ||||
}; | }; | ||||
Item.call(this, parent, data, options, defaultOptions); | |||||
// validate data | |||||
if (data) { | |||||
if (data.start == undefined) { | |||||
throw new Error('Property "start" missing in item ' + data); | |||||
} | |||||
} | |||||
Item.call(this, data, options, defaultOptions); | |||||
} | } | ||||
ItemBox.prototype = new Item (null, null); | |||||
ItemBox.prototype = new Item (null); | |||||
/** | |||||
* Check whether this item is visible inside given range | |||||
* @returns {{start: Number, end: Number}} range with a timestamp for start and end | |||||
* @returns {boolean} True if visible | |||||
*/ | |||||
ItemBox.prototype.isVisible = function isVisible (range) { | |||||
// determine visibility | |||||
// TODO: account for the real width of the item. Right now we just add 1/4 to the window | |||||
var interval = (range.end - range.start) / 4; | |||||
return (this.data.start > range.start - interval) && (this.data.start < range.end + interval); | |||||
}; | |||||
/** | /** | ||||
* Repaint the item | * Repaint the item | ||||
* @return {Boolean} changed | |||||
*/ | */ | ||||
ItemBox.prototype.repaint = function repaint() { | ItemBox.prototype.repaint = function repaint() { | ||||
// TODO: make an efficient repaint | |||||
var changed = false; | |||||
var dom = this.dom; | var dom = this.dom; | ||||
if (!dom) { | if (!dom) { | ||||
this._create(); | |||||
// create DOM | |||||
this.dom = {}; | |||||
dom = this.dom; | dom = this.dom; | ||||
changed = true; | |||||
// create main box | |||||
dom.box = document.createElement('DIV'); | |||||
// contents box (inside the background box). used for making margins | |||||
dom.content = document.createElement('DIV'); | |||||
dom.content.className = 'content'; | |||||
dom.box.appendChild(dom.content); | |||||
// line to axis | |||||
dom.line = document.createElement('DIV'); | |||||
dom.line.className = 'line'; | |||||
// dot on axis | |||||
dom.dot = document.createElement('DIV'); | |||||
dom.dot.className = 'dot'; | |||||
// attach this item as attribute | |||||
dom.box['timeline-item'] = this; | |||||
} | } | ||||
if (dom) { | |||||
if (!this.parent) { | |||||
throw new Error('Cannot repaint item: no parent attached'); | |||||
// append DOM to parent DOM | |||||
if (!this.parent) { | |||||
throw new Error('Cannot repaint item: no parent attached'); | |||||
} | |||||
if (!dom.box.parentNode) { | |||||
var foreground = this.parent.getForeground(); | |||||
if (!foreground) throw new Error('Cannot repaint time axis: parent has no foreground container element'); | |||||
foreground.appendChild(dom.box); | |||||
} | |||||
if (!dom.line.parentNode) { | |||||
var background = this.parent.getBackground(); | |||||
if (!background) throw new Error('Cannot repaint time axis: parent has no background container element'); | |||||
background.appendChild(dom.line); | |||||
} | |||||
if (!dom.dot.parentNode) { | |||||
var axis = this.parent.getAxis(); | |||||
if (!background) throw new Error('Cannot repaint time axis: parent has no axis container element'); | |||||
axis.appendChild(dom.dot); | |||||
} | |||||
this.displayed = true; | |||||
// update contents | |||||
if (this.data.content != this.content) { | |||||
this.content = this.data.content; | |||||
if (this.content instanceof Element) { | |||||
dom.content.innerHTML = ''; | |||||
dom.content.appendChild(this.content); | |||||
} | } | ||||
if (!dom.box.parentNode) { | |||||
var foreground = this.parent.getForeground(); | |||||
if (!foreground) { | |||||
throw new Error('Cannot repaint time axis: ' + | |||||
'parent has no foreground container element'); | |||||
} | |||||
foreground.appendChild(dom.box); | |||||
changed = true; | |||||
else if (this.data.content != undefined) { | |||||
dom.content.innerHTML = this.content; | |||||
} | } | ||||
if (!dom.line.parentNode) { | |||||
var background = this.parent.getBackground(); | |||||
if (!background) { | |||||
throw new Error('Cannot repaint time axis: ' + | |||||
'parent has no background container element'); | |||||
} | |||||
background.appendChild(dom.line); | |||||
changed = true; | |||||
else { | |||||
throw new Error('Property "content" missing in item ' + this.data.id); | |||||
} | } | ||||
if (!dom.dot.parentNode) { | |||||
var axis = this.parent.getAxis(); | |||||
if (!background) { | |||||
throw new Error('Cannot repaint time axis: ' + | |||||
'parent has no axis container element'); | |||||
} | |||||
axis.appendChild(dom.dot); | |||||
changed = true; | |||||
} | |||||
this.dirty = true; | |||||
} | |||||
this._repaintDeleteButton(dom.box); | |||||
// update contents | |||||
if (this.data.content != this.content) { | |||||
this.content = this.data.content; | |||||
if (this.content instanceof Element) { | |||||
dom.content.innerHTML = ''; | |||||
dom.content.appendChild(this.content); | |||||
} | |||||
else if (this.data.content != undefined) { | |||||
dom.content.innerHTML = this.content; | |||||
} | |||||
else { | |||||
throw new Error('Property "content" missing in item ' + this.data.id); | |||||
} | |||||
changed = true; | |||||
} | |||||
// update class | |||||
var className = (this.data.className? ' ' + this.data.className : '') + | |||||
(this.selected ? ' selected' : ''); | |||||
if (this.className != className) { | |||||
this.className = className; | |||||
dom.box.className = 'item box' + className; | |||||
dom.line.className = 'item line' + className; | |||||
dom.dot.className = 'item dot' + className; | |||||
// update class | |||||
var className = (this.data.className? ' ' + this.data.className : '') + | |||||
(this.selected ? ' selected' : ''); | |||||
if (this.className != className) { | |||||
this.className = className; | |||||
dom.box.className = 'item box' + className; | |||||
dom.line.className = 'item line' + className; | |||||
dom.dot.className = 'item dot' + className; | |||||
changed = true; | |||||
} | |||||
this.dirty = true; | |||||
} | } | ||||
return changed; | |||||
// recalculate size | |||||
if (this.dirty) { | |||||
this.props.dot.height = dom.dot.offsetHeight; | |||||
this.props.dot.width = dom.dot.offsetWidth; | |||||
this.props.line.width = dom.line.offsetWidth; | |||||
this.width = dom.box.offsetWidth; | |||||
this.height = dom.box.offsetHeight; | |||||
this.dirty = false; | |||||
} | |||||
this._repaintDeleteButton(dom.box); | |||||
}; | }; | ||||
/** | /** | ||||
* Show the item in the DOM (when not already visible). The items DOM will | |||||
* Show the item in the DOM (when not already displayed). The items DOM will | |||||
* be created when needed. | * be created when needed. | ||||
* @return {Boolean} changed | |||||
*/ | */ | ||||
ItemBox.prototype.show = function show() { | ItemBox.prototype.show = function show() { | ||||
if (!this.dom || !this.dom.box.parentNode) { | |||||
return this.repaint(); | |||||
} | |||||
else { | |||||
return false; | |||||
if (!this.displayed) { | |||||
this.repaint(); | |||||
} | } | ||||
}; | }; | ||||
/** | /** | ||||
* Hide the item from the DOM (when visible) | * Hide the item from the DOM (when visible) | ||||
* @return {Boolean} changed | |||||
*/ | */ | ||||
ItemBox.prototype.hide = function hide() { | ItemBox.prototype.hide = function hide() { | ||||
var changed = false, | |||||
dom = this.dom; | |||||
if (dom) { | |||||
if (dom.box.parentNode) { | |||||
dom.box.parentNode.removeChild(dom.box); | |||||
changed = true; | |||||
} | |||||
if (dom.line.parentNode) { | |||||
dom.line.parentNode.removeChild(dom.line); | |||||
} | |||||
if (dom.dot.parentNode) { | |||||
dom.dot.parentNode.removeChild(dom.dot); | |||||
} | |||||
if (this.displayed) { | |||||
var dom = this.dom; | |||||
if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box); | |||||
if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line); | |||||
if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot); | |||||
this.top = null; | |||||
this.left = null; | |||||
this.displayed = false; | |||||
} | } | ||||
return changed; | |||||
}; | }; | ||||
/** | /** | ||||
* Reflow the item: calculate its actual size and position from the DOM | |||||
* @return {boolean} resized returns true if the axis is resized | |||||
* @override | |||||
* Reposition the item horizontally | |||||
* @Override | |||||
*/ | */ | ||||
ItemBox.prototype.reflow = function reflow() { | |||||
var changed = 0, | |||||
update, | |||||
dom, | |||||
props, | |||||
options, | |||||
margin, | |||||
start, | |||||
align, | |||||
orientation, | |||||
top, | |||||
ItemBox.prototype.repositionX = function repositionX() { | |||||
var start = this.defaultOptions.toScreen(this.data.start), | |||||
align = this.options.align || this.defaultOptions.align, | |||||
left, | left, | ||||
data, | |||||
range; | |||||
box = this.dom.box, | |||||
line = this.dom.line, | |||||
dot = this.dom.dot; | |||||
if (this.data.start == undefined) { | |||||
throw new Error('Property "start" missing in item ' + this.data.id); | |||||
// calculate left position of the box | |||||
if (align == 'right') { | |||||
this.left = start - this.width; | |||||
} | } | ||||
data = this.data; | |||||
range = this.parent && this.parent.range; | |||||
if (data && range) { | |||||
// TODO: account for the width of the item | |||||
var interval = (range.end - range.start); | |||||
this.visible = (data.start > range.start - interval) && (data.start < range.end + interval); | |||||
else if (align == 'left') { | |||||
this.left = start; | |||||
} | } | ||||
else { | else { | ||||
this.visible = false; | |||||
// default or 'center' | |||||
this.left = start - this.width / 2; | |||||
} | } | ||||
if (this.visible) { | |||||
dom = this.dom; | |||||
if (dom) { | |||||
update = util.updateProperty; | |||||
props = this.props; | |||||
options = this.options; | |||||
start = this.parent.toScreen(this.data.start) + this.offset; | |||||
align = options.align || this.defaultOptions.align; | |||||
margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis; | |||||
orientation = options.orientation || this.defaultOptions.orientation; | |||||
changed += update(props.dot, 'height', dom.dot.offsetHeight); | |||||
changed += update(props.dot, 'width', dom.dot.offsetWidth); | |||||
changed += update(props.line, 'width', dom.line.offsetWidth); | |||||
changed += update(props.line, 'height', dom.line.offsetHeight); | |||||
changed += update(props.line, 'top', dom.line.offsetTop); | |||||
changed += update(this, 'width', dom.box.offsetWidth); | |||||
changed += update(this, 'height', dom.box.offsetHeight); | |||||
if (align == 'right') { | |||||
left = start - this.width; | |||||
} | |||||
else if (align == 'left') { | |||||
left = start; | |||||
} | |||||
else { | |||||
// default or 'center' | |||||
left = start - this.width / 2; | |||||
} | |||||
changed += update(this, 'left', left); | |||||
changed += update(props.line, 'left', start - props.line.width / 2); | |||||
changed += update(props.dot, 'left', start - props.dot.width / 2); | |||||
changed += update(props.dot, 'top', -props.dot.height / 2); | |||||
if (orientation == 'top') { | |||||
top = margin; | |||||
changed += update(this, 'top', top); | |||||
} | |||||
else { | |||||
// default or 'bottom' | |||||
var parentHeight = this.parent.height; | |||||
top = parentHeight - this.height - margin; | |||||
changed += update(this, 'top', top); | |||||
} | |||||
} | |||||
else { | |||||
changed += 1; | |||||
} | |||||
} | |||||
return (changed > 0); | |||||
}; | |||||
/** | |||||
* Create an items DOM | |||||
* @private | |||||
*/ | |||||
ItemBox.prototype._create = function _create() { | |||||
var dom = this.dom; | |||||
if (!dom) { | |||||
this.dom = dom = {}; | |||||
// reposition box | |||||
box.style.left = this.left + 'px'; | |||||
// create the box | |||||
dom.box = document.createElement('DIV'); | |||||
// className is updated in repaint() | |||||
// reposition line | |||||
line.style.left = (start - this.props.line.width / 2) + 'px'; | |||||
// contents box (inside the background box). used for making margins | |||||
dom.content = document.createElement('DIV'); | |||||
dom.content.className = 'content'; | |||||
dom.box.appendChild(dom.content); | |||||
// line to axis | |||||
dom.line = document.createElement('DIV'); | |||||
dom.line.className = 'line'; | |||||
// dot on axis | |||||
dom.dot = document.createElement('DIV'); | |||||
dom.dot.className = 'dot'; | |||||
// attach this item as attribute | |||||
dom.box['timeline-item'] = this; | |||||
} | |||||
// reposition dot | |||||
dot.style.left = (start - this.props.dot.width / 2) + 'px'; | |||||
}; | }; | ||||
/** | /** | ||||
* Reposition the item, recalculate its left, top, and width, using the current | |||||
* range and size of the items itemset | |||||
* @override | |||||
* Reposition the item vertically | |||||
* @Override | |||||
*/ | */ | ||||
ItemBox.prototype.reposition = function reposition() { | |||||
var dom = this.dom, | |||||
props = this.props, | |||||
orientation = this.options.orientation || this.defaultOptions.orientation; | |||||
if (dom) { | |||||
var box = dom.box, | |||||
line = dom.line, | |||||
dot = dom.dot; | |||||
box.style.left = this.left + 'px'; | |||||
box.style.top = this.top + 'px'; | |||||
line.style.left = props.line.left + 'px'; | |||||
if (orientation == 'top') { | |||||
line.style.top = 0 + 'px'; | |||||
line.style.height = this.top + 'px'; | |||||
} | |||||
else { | |||||
// orientation 'bottom' | |||||
line.style.top = (this.top + this.height) + 'px'; | |||||
line.style.height = Math.max(this.parent.height - this.top - this.height + | |||||
this.props.dot.height / 2, 0) + 'px'; | |||||
} | |||||
ItemBox.prototype.repositionY = function repositionY () { | |||||
var orientation = this.options.orientation || this.defaultOptions.orientation, | |||||
box = this.dom.box, | |||||
line = this.dom.line, | |||||
dot = this.dom.dot; | |||||
if (orientation == 'top') { | |||||
box.style.top = (this.top || 0) + 'px'; | |||||
box.style.bottom = ''; | |||||
line.style.top = '0'; | |||||
line.style.bottom = ''; | |||||
line.style.height = (this.parent.top + this.top + 1) + 'px'; | |||||
} | |||||
else { // orientation 'bottom' | |||||
box.style.top = ''; | |||||
box.style.bottom = (this.top || 0) + 'px'; | |||||
dot.style.left = props.dot.left + 'px'; | |||||
dot.style.top = props.dot.top + 'px'; | |||||
line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px'; | |||||
line.style.bottom = '0'; | |||||
line.style.height = ''; | |||||
} | } | ||||
dot.style.top = (-this.props.dot.height / 2) + 'px'; | |||||
}; | }; |
@ -0,0 +1,112 @@ | |||||
/** | |||||
* Utility functions for ordering and stacking of items | |||||
*/ | |||||
var stack = {}; | |||||
/** | |||||
* Order items by their start data | |||||
* @param {Item[]} items | |||||
*/ | |||||
stack.orderByStart = function orderByStart(items) { | |||||
items.sort(function (a, b) { | |||||
return a.data.start - b.data.start; | |||||
}); | |||||
}; | |||||
/** | |||||
* Order items by their end date. If they have no end date, their start date | |||||
* is used. | |||||
* @param {Item[]} items | |||||
*/ | |||||
stack.orderByEnd = function orderByEnd(items) { | |||||
items.sort(function (a, b) { | |||||
var aTime = ('end' in a.data) ? a.data.end : a.data.start, | |||||
bTime = ('end' in b.data) ? b.data.end : b.data.start; | |||||
return aTime - bTime; | |||||
}); | |||||
}; | |||||
/** | |||||
* Adjust vertical positions of the items such that they don't overlap each | |||||
* other. | |||||
* @param {Item[]} items | |||||
* All visible items | |||||
* @param {{item: number, axis: number}} margin | |||||
* Margins between items and between items and the axis. | |||||
* @param {boolean} [force=false] | |||||
* If true, all items will be repositioned. If false (default), only | |||||
* items having a top===null will be re-stacked | |||||
*/ | |||||
stack.stack = function _stack (items, margin, force) { | |||||
var i, iMax; | |||||
if (force) { | |||||
// reset top position of all items | |||||
for (i = 0, iMax = items.length; i < iMax; i++) { | |||||
items[i].top = null; | |||||
} | |||||
} | |||||
// calculate new, non-overlapping positions | |||||
for (i = 0, iMax = items.length; i < iMax; i++) { | |||||
var item = items[i]; | |||||
if (item.top === null) { | |||||
// initialize top position | |||||
item.top = margin.axis; | |||||
do { | |||||
// TODO: optimize checking for overlap. when there is a gap without items, | |||||
// you only need to check for items from the next item on, not from zero | |||||
var collidingItem = null; | |||||
for (var j = 0, jj = items.length; j < jj; j++) { | |||||
var other = items[j]; | |||||
if (other.top !== null && other !== item && stack.collision(item, other, margin.item)) { | |||||
collidingItem = other; | |||||
break; | |||||
} | |||||
} | |||||
if (collidingItem != null) { | |||||
// There is a collision. Reposition the items above the colliding element | |||||
item.top = collidingItem.top + collidingItem.height + margin.item; | |||||
} | |||||
} while (collidingItem); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* Adjust vertical positions of the items without stacking them | |||||
* @param {Item[]} items | |||||
* All visible items | |||||
* @param {{item: number, axis: number}} margin | |||||
* Margins between items and between items and the axis. | |||||
*/ | |||||
stack.nostack = function nostack (items, margin) { | |||||
var i, iMax; | |||||
// reset top position of all items | |||||
for (i = 0, iMax = items.length; i < iMax; i++) { | |||||
items[i].top = margin.axis; | |||||
} | |||||
}; | |||||
/** | |||||
* Test if the two provided items collide | |||||
* The items must have parameters left, width, top, and height. | |||||
* @param {Item} a The first item | |||||
* @param {Item} b The second item | |||||
* @param {Number} margin A minimum required margin. | |||||
* If margin is provided, the two items will be | |||||
* marked colliding when they overlap or | |||||
* when the margin between the two is smaller than | |||||
* the requested margin. | |||||
* @return {boolean} true if a and b collide, else false | |||||
*/ | |||||
stack.collision = function collision (a, b, margin) { | |||||
return ((a.left - margin) < (b.left + b.width) && | |||||
(a.left + a.width + margin) > b.left && | |||||
(a.top - margin) < (b.top + b.height) && | |||||
(a.top + a.height + margin) > b.top); | |||||
}; |