1 /**
  2  * @file timeline.js
  3  * 
  4  * @brief 
  5  * The Timeline is an interactive visualization chart to visualize events in 
  6  * time, having a start and end date. 
  7  * You can freely move and zoom in the timeline by dragging 
  8  * and scrolling in the Timeline. Items are optionally dragable. The time 
  9  * scale on the axis is adjusted automatically, and supports scales ranging 
 10  * from milliseconds to years.
 11  *
 12  * Timeline is part of the CHAP Links library.
 13  * 
 14  * Timeline is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and
 15  * Internet Explorer 6 to 9.
 16  *
 17  * @license
 18  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 19  * use this file except in compliance with the License. You may obtain a copy 
 20  * of the License at
 21  * 
 22  * http://www.apache.org/licenses/LICENSE-2.0
 23  * 
 24  * Unless required by applicable law or agreed to in writing, software
 25  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 26  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 27  * License for the specific language governing permissions and limitations under
 28  * the License.
 29  *
 30  * Copyright (c) 2011 Almende B.V.
 31  *
 32  * @author 	Jos de Jong, <jos@almende.org>
 33  * @date	  2011-11-08
 34  */
 35 
 36 
 37 /*
 38  * TODO
 39  *
 40  * Bug on IE in Quirks mode. When you have groups, and delete an item, the groups become invisible
 41  */ 
 42 
 43 /**
 44  * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
 45  * "links"
 46  */ 
 47 var links;
 48 if (links === undefined) {
 49   links = {};
 50 }
 51 
 52 /**
 53  * Declare google variable, loaded and used in case of Google DataTable
 54  */ 
 55 var google;
 56 
 57 /**
 58  * @class Timeline
 59  * The timeline is a visualization chart to visualize events in time. 
 60  * 
 61  * The timeline is developed in javascript as a Google Visualization Chart.
 62  * 
 63  * @param {dom_element} container   The DOM element in which the Timeline will
 64  *                                  be created. Normally a div element.
 65  */
 66 links.Timeline = function(container) {
 67   // create variables and set default values
 68   this.dom = {};
 69   this.conversion = {};
 70   this.eventParams = {}; // stores parameters for mouse events
 71   this.groups = [];
 72   this.groupIndexes = {};
 73   this.items = [];
 74   this.selection = undefined; // stores index and item which is currently selected
 75 
 76   this.listeners = {}; // event listener callbacks
 77 
 78   // Initialize sizes. 
 79   // Needed for IE (which gives an error when you try to set an undefined
 80   // value in a style)
 81   this.size = {
 82     'actualHeight': 0,
 83     'axis': {
 84       'characterMajorHeight': 0,
 85       'characterMajorWidth': 0,
 86       'characterMinorHeight': 0,
 87       'characterMinorWidth': 0,
 88       'height': 0,
 89       'labelMajorTop': 0,
 90       'labelMinorTop': 0,
 91       'line': 0,
 92       'lineMajorWidth': 0,
 93       'lineMinorHeight': 0,
 94       'lineMinorTop': 0,
 95       'lineMinorWidth': 0,
 96       'top': 0
 97     },
 98     'contentHeight': 0,
 99     'contentLeft': 0,
100     'contentWidth': 0,
101     'dataChanged': false,
102     'frameHeight': 0,
103     'frameWidth': 0,
104     'groupsLeft': 0,
105     'groupsWidth': 0,
106     'items': {
107       'top': 0
108     }
109   };
110   
111   this.dom.container = container;
112   
113   this.options = {
114     'width': "100%",
115     'height': "auto",
116     'minHeight': 0,       // minimal height in pixels
117     'autoHeight': true,
118     
119     'eventMargin': 10,    // minimal margin between events 
120     'eventMarginAxis': 20, // minimal margin beteen events and the axis
121     'dragAreaWidth': 10, // pixels
122 
123     'moveable': true,
124     'zoomable': true,
125     'selectable': true,
126     'editable': false,
127     'snapEvents': true,
128 
129     'showCurrentTime': true, // show a red bar displaying the current time
130     'showCustomTime': false, // show a blue, draggable bar displaying a custom time    
131     'showMajorLabels': true,
132     'showNavigation': false,
133     'showButtonAdd': true,
134     'groupsOnRight': false,
135     'axisOnTop': false,
136     'stackEvents': true,
137     'animate': true,
138     'animateZoom': true,
139     'style': 'box'
140   };
141   
142   this.clientTimeOffset = 0;    // difference between client time and the time
143                                 // set via Timeline.setCurrentTime()
144 
145   var dom = this.dom;
146   
147   // remove all elements from the container element.
148   while (dom.container.hasChildNodes()) {
149     dom.container.removeChild(dom.container.firstChild);
150   }
151 
152   // create a step for drawing the axis
153   this.step = new links.Timeline.StepDate();
154 
155   // initialize data
156   this.data = [];
157 
158   // date interval must be initialized 
159   this.setVisibleChartRange(undefined, undefined, false);
160 
161   // create all DOM elements
162   this.redrawFrame();
163   
164   // Internet Explorer does not support Array.indexof, 
165   // so we define it here in that case
166   // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
167 	if(!Array.prototype.indexOf) {
168     Array.prototype.indexOf = function(obj){
169       for(var i = 0; i < this.length; i++){
170         if(this[i] == obj){
171           return i;
172         }
173       }
174       return -1;
175     }
176 	}                 
177 
178   // fire the ready event
179   this.trigger('ready');
180 }
181 
182 
183 /** 
184  * Main drawing logic. This is the function that needs to be called 
185  * in the html page, to draw the timeline.
186  * 
187  * A data table with the events must be provided, and an options table. 
188  * 
189  * @param {DataTable}      data    The data containing the events for the timeline.
190  *                                 Object DataTable is defined in 
191  *                                 google.visualization.DataTable
192  * @param {name/value map} options A name/value map containing settings for the
193  *                                 timeline. Optional.
194  */
195 links.Timeline.prototype.draw = function(data, options) {
196   if (options) {
197     // retrieve parameter values
198     for (var i in options) {
199       if (options.hasOwnProperty(i)) {
200         this.options[i] = options[i];
201       }
202     }
203   }
204   this.options.autoHeight = (this.options.height === "auto");
205 
206   // read the data
207   this.setData(data);
208 
209   // set timer range. this will also redraw the timeline
210   if (options && options.start && options.end) {
211     this.setVisibleChartRange(options.start, options.end);
212   }
213   else {
214     this.setVisibleChartRangeAuto();
215   }
216 }
217 
218 /**
219  * Set data for the timeline
220  * @param {DataTable or JSON array} data
221  */ 
222 links.Timeline.prototype.setData = function(data) {
223   // unselect any previously selected item
224   this.unselectItem();     
225 
226   this.items = [];
227   this.data = data;
228   var items = this.items;
229   var options = this.options;
230 
231   // create groups from the data
232   this.setGroups(data);
233 
234   if (google && google.visualization && google.visualization.DataTable &&
235       data instanceof google.visualization.DataTable) {
236     // read DataTable
237     var hasGroups = (data.getNumberOfColumns() > 3);
238     for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
239       items.push(this.createItem({
240         'start': data.getValue(row, 0), 
241         'end': data.getValue(row, 1), 
242         'content': data.getValue(row, 2), 
243         'group': (hasGroups ? data.getValue(row, 3) : undefined)
244       }));
245     }
246   }
247   else if (data instanceof Array) {
248     // read JSON array
249     for (var row = 0, rows = data.length; row < rows; row++) {
250       var itemData = data[row]
251       var item = this.createItem(itemData);
252       items.push(item);
253     }    
254   }
255   else {
256     throw "Unknown data type. DataTable or Array expected.";
257   }
258 
259   // set a flag to force the recalcSize method to recalculate the 
260   // heights and widths of the events
261   this.size.dataChanged = true;
262   this.redrawFrame();      // create the items for the new data
263   this.recalcSize();       // position the items
264   this.stackEvents(false);
265   this.redrawFrame();      // redraw the items on the final positions
266   this.size.dataChanged = false;
267 }
268 
269 /**
270  * Set the groups available in the given dataset
271  * @param {DataTable or JSON array} data
272  */ 
273 links.Timeline.prototype.setGroups = function (data) {
274   this.deleteGroups();
275   var groups = this.groups;
276   var groupIndexes = this.groupIndexes;
277 
278   if (google && google.visualization && google.visualization.DataTable &&
279       data instanceof google.visualization.DataTable) {
280     // get groups from DataTable
281     var hasGroups = (data.getNumberOfColumns() > 3);
282     if (hasGroups) {
283       var groupNames = data.getDistinctValues(3);
284       for (var i = 0, iMax = groupNames.length; i < iMax; i++) {
285         this.addGroup(groupNames[i]);
286       }
287     }
288   }
289   else if (data instanceof Array){
290     // get groups from JSON Array
291     for (var i = 0, iMax = data.length; i < iMax; i++) {
292       var row = data[i],
293         group = row.group;
294       if (group) {
295         this.addGroup(group);
296       }
297     }    
298   }  
299   else {
300     throw 'Unknown data type. DataTable or Array expected.';
301   }
302 }
303 
304 
305 /**
306  * Return the original data table.
307  * @param {Google DataTable or Array} data
308  */ 
309 links.Timeline.prototype.getData = function  () {
310   return this.data;
311 }
312 
313 
314 /**
315  * Update the original data with changed start, end or group. 
316  * 
317  * @param {Number} index
318  * @param {Object} values   An object containing some of the following parameters:
319  *                          {Date} start,
320  *                          {Date} end,
321  *                          {String} content,
322  *                          {String} group
323  */ 
324 links.Timeline.prototype.updateData = function  (index, values) {
325   var data = this.data;
326 
327   if (google && google.visualization && google.visualization.DataTable &&
328     data instanceof google.visualization.DataTable) {  
329     // update the original google DataTable
330     var missingRows = (index + 1) - data.getNumberOfRows();
331     if (missingRows > 0) {
332       data.addRows(missingRows);
333     }
334 
335     if (values.start) {
336       data.setValue(index, 0, values.start);
337     }
338     if (values.end) {
339       data.setValue(index, 1, values.end);
340     }
341     if (values.content) {
342       data.setValue(index, 2, values.content);
343     }
344     if (values.group && data.getNumberOfColumns() > 3) {
345       // TODO: append a column when needed?
346       data.setValue(index, 3, values.group);
347     }    
348   }
349   else if (data instanceof Array) {
350     // update the original JSON table
351     var row = data[index];
352     if (row == undefined) {
353       row = {};
354       data[index] = row;
355     }
356 
357     if (values.start) {
358       row.start = values.start;
359     }
360     if (values.end) {
361       row.end = values.end;
362     }
363     if (values.content) {
364       row.content = values.content;
365     }
366     if (values.group) {
367       row.group = values.group;
368     }    
369   }
370   else {
371     throw "Cannot update data, unknown type of data";
372   }
373 }
374 
375 /**
376  * Find the item index from a given HTML element
377  * If no item index is found, undefined is returned
378  * @param {HTML DOM element} element
379  * @return {Number} index
380  */ 
381 links.Timeline.prototype.getItemIndex = function(element) {
382   var e = element,
383     dom = this.dom,
384     items = this.items,
385     index = undefined;
386   
387   // try to find the frame where the items are located in
388   while (e.parentNode && e.parentNode !== dom.items.frame) {
389     e = e.parentNode;
390   }
391   
392   if (e.parentNode === dom.items.frame) {
393     // yes! we have found the parent element of all items
394     // retrieve its id from the array with items
395     for (var i = 0, iMax = items.length; i < iMax; i++) {
396       if (items[i].dom === e) {
397         index = i;
398         break;
399       }
400     }
401   }
402   
403   return index;  
404 }
405 
406 /**
407  * Set a new size for the timeline
408  * @param {string} width   Width in pixels or percentage (for example "800px"
409  *                         or "50%")
410  * @param {string} height  Height in pixels or percentage  (for example "400px"
411  *                         or "30%")
412  */ 
413 links.Timeline.prototype.setSize = function(width, height) {
414   if (width) {
415     this.options.width = width;
416     this.dom.frame.style.width = width;
417   }
418   if (height) {
419     this.options.height = height;
420     this.options.autoHeight = (this.options.height === "auto");
421     if (height !==  "auto" ) {
422       this.dom.frame.style.height = height;
423     }
424   }
425 
426   this.recalcSize();
427   this.stackEvents(false);
428   this.redrawFrame();
429 }
430 
431 
432 /**
433  * Set a new value for the visible range int the timeline.
434  * Set start to null to include everything from the earliest date to end.
435  * Set end to null to include everything from start to the last date.
436  * Example usage: 
437  *    myTimeline.setVisibleChartRange(new Date("2010-08-22"),
438  *                                    new Date("2010-09-13"));
439  * @param {Date}   start     The start date for the timeline. optional
440  * @param {Date}   end       The end date for the timeline. optional
441  * @param {boolean} redraw   Optional. If true (default) the Timeline is 
442  *                           directly redrawn
443  */
444 links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) {
445   if (start != null) {
446     this.start = new Date(start);
447   } else {
448     // default of 3 days ago
449     this.start = new Date();
450     this.start.setDate(this.start.getDate() - 3);
451   }
452   
453   if (end != null) {
454     this.end = new Date(end);
455   } else {
456     // default of 4 days ahead
457     this.end = new Date();
458     this.end.setDate(this.end.getDate() + 4);
459   }
460 
461   // prevent start Date <= end Date
462   if (this.end.valueOf() <= this.start.valueOf()) {
463     this.end = new Date(this.start);
464     this.end.setDate(this.end.getDate() + 7);
465   }
466 
467   if (redraw == undefined || redraw == true) {
468     this.recalcSize();
469     this.stackEvents(false);  
470     this.redrawFrame();
471   }
472   else {
473     this.recalcConversion();
474   }
475 }
476 
477 
478 /**
479  * Change the visible chart range such that all items become visible
480  */ 
481 links.Timeline.prototype.setVisibleChartRangeAuto = function() {
482   var items = this.items;
483     startMin = undefined, // long value of a data
484     endMax = undefined;   // long value of a data
485 
486   // find earliest start date from the data
487   for (var i = 0, iMax = items.length; i < iMax; i++) {
488     var item = items[i],
489       start = item.start ? item.start.valueOf() : undefined,
490       end = item.end ? item.end.valueOf() : start;
491       
492     if (startMin !== undefined && start !== undefined) {
493       startMin = Math.min(startMin, start);
494     }
495     else {
496       startMin = start;
497     }
498     if (endMax !== undefined && end !== undefined) {
499       endMax = Math.max(endMax, end);
500     }
501     else {
502       endMax = end;
503     }
504   }
505   
506   if (startMin !== undefined && endMax !== undefined) {
507     // zoom out 5% such that you have a little white space on the left and right
508     var center = (endMax + startMin) / 2,
509       diff = (endMax - startMin);
510     startMin = startMin - diff * 0.05;
511     endMax = endMax + diff * 0.05;
512     
513     // adjust the start and end date
514     this.setVisibleChartRange(new Date(startMin), new Date(endMax));    
515   }
516   else {
517     this.setVisibleChartRange(undefined, undefined);    
518   }
519 }
520 
521 /**
522  * Adjust the visible range such that the current time is located in the center 
523  * of the timeline
524  */ 
525 links.Timeline.prototype.setVisibleChartRangeNow = function() {
526   var now = new Date();
527   
528   var diff = (this.end.getTime() - this.start.getTime());
529     
530   var startNew = new Date(now.getTime() - diff/2);
531   var endNew = new Date(startNew.getTime() + diff);
532   this.setVisibleChartRange(startNew, endNew);
533 }
534 
535 
536 /**
537  * Retrieve the current visible range in the timeline.
538  * @return {Object} An object with start and end properties
539  */
540 links.Timeline.prototype.getVisibleChartRange = function() {
541   var range = {
542     'start': new Date(this.start),
543     'end': new Date(this.end)
544   };
545   return range;
546 }
547 
548 
549 /** 
550  * Redraw the timeline. This needs to be executed after the start and/or
551  * end time are changed, or when data is added or removed dynamically. 
552  */ 
553 links.Timeline.prototype.redrawFrame = function() {
554   var dom = this.dom,
555     options = this.options,
556     size = this.size;
557   
558   if (!dom.frame) {
559     // the surrounding main frame
560     dom.frame = document.createElement("DIV");
561     dom.frame.className = "timeline-frame";
562     dom.frame.style.position = "relative";
563     dom.frame.style.overflow = "hidden";
564     dom.frame.style.width = options.width  || "100%";
565     dom.frame.style.height = options.height  || "100%";
566     dom.container.appendChild(dom.frame);
567   }
568 
569   if (options.autoHeight) {
570     dom.frame.style.height = size.frameHeight + "px";
571   }
572   else {
573     dom.frame.style.height = options.height;
574   }
575   
576   this.redrawContent();
577   this.redrawGroups();
578   this.redrawCurrentTime();
579   this.redrawCustomTime();
580   this.redrawNavigation();
581 }
582 
583 
584 /**
585  * Redraw the content of the timeline: the axis and the items
586  */ 
587 links.Timeline.prototype.redrawContent = function() {
588   var dom = this.dom,
589     size = this.size;
590     
591   if (!dom.content) {
592     // create content box where the axis and canvas will 
593     dom.content = document.createElement("DIV");
594     //this.frame.className = "timeline-frame";
595     dom.content.style.position = "relative";
596     dom.content.style.overflow = "hidden";
597     dom.frame.appendChild(dom.content);
598     
599     var timelines = document.createElement("DIV");
600     timelines.style.position = "absolute";
601     timelines.style.left = "0px";
602     timelines.style.top = "0px";    
603     timelines.style.height = "100%";
604     timelines.style.width = "0px";
605     dom.content.appendChild(timelines);
606     dom.contentTimelines = timelines;
607     
608     var params = this.eventParams,
609       me = this;
610     if (!params.onMouseDown) {
611       params.onMouseDown = function (event) {me.onMouseDown(event);};
612       links.Timeline.addEventListener(dom.content, "mousedown", params.onMouseDown);
613     }    
614     if (!params.onTouchStart) {
615       params.onTouchStart = function (event) {me.onTouchStart(event);};
616       links.Timeline.addEventListener(dom.content, "touchstart", params.onTouchStart);
617     }
618     if (!params.onMouseWheel) {
619       params.onMouseWheel = function (event) {me.onMouseWheel(event);};
620       links.Timeline.addEventListener(dom.content, "mousewheel", params.onMouseWheel);
621     }
622     if (!params.onDblClick) {
623       params.onDblClick = function (event) {me.onDblClick(event);};
624       links.Timeline.addEventListener(dom.content, "dblclick", params.onDblClick);
625     }
626   }
627   dom.content.style.left = size.contentLeft + "px";
628   dom.content.style.top = "0px";
629   dom.content.style.width = size.contentWidth + "px";
630   dom.content.style.height = size.frameHeight + "px";
631   
632   this.redrawAxis();
633   this.redrawItems();
634   this.redrawDeleteButton();
635   this.redrawDragAreas();
636 }
637 
638 /**
639  * Redraw the timeline axis with minor and major labels
640  */ 
641 links.Timeline.prototype.redrawAxis = function() {
642   var dom = this.dom,
643     options = this.options,
644     size = this.size,
645     step = this.step;
646 
647   var axis = dom.axis;
648   if (!axis) {
649     axis = {};
650     dom.axis = axis;
651   }
652   if (size.axis.properties === undefined) {
653     size.axis.properties = {};
654   }
655   if (axis.minorTexts === undefined) {
656     axis.minorTexts = [];
657   }
658   if (axis.minorLines === undefined) {
659     axis.minorLines = [];
660   }
661   if (axis.majorTexts === undefined) {
662     axis.majorTexts = [];
663   }
664   if (axis.majorLines === undefined) {
665     axis.majorLines = [];
666   }
667 
668   if (!axis.frame) {
669     axis.frame = document.createElement("DIV");
670     axis.frame.style.position = "absolute";
671     axis.frame.style.left = "0px";
672     axis.frame.style.top = "0px";
673     dom.content.appendChild(axis.frame);
674   }
675 
676   // take axis offline
677   dom.content.removeChild(axis.frame);
678   
679   axis.frame.style.width = (size.contentWidth) + "px";
680   axis.frame.style.height = (size.axis.height) + "px";
681 
682   // the drawn axis is more wide than the actual visual part, such that
683   // the axis can be dragged without having to redraw it each time again.
684   var start = this.screenToTime(0);
685   var end = this.screenToTime(size.contentWidth);
686   var width = size.contentWidth;
687 
688   // calculate minimum step (in milliseconds) based on character size
689   this.minimumStep = this.screenToTime(size.axis.characterMinorWidth * 6).valueOf() - 
690                      this.screenToTime(0).valueOf();
691 
692   step.setRange(start, end, this.minimumStep);
693 
694   this.redrawAxisCharacters();
695   
696   this.redrawAxisStartOverwriting();
697 
698   step.start();
699   var xFirstMajorLabel = undefined;
700   while (!step.end()) {
701     var cur = step.getCurrent(),
702         x = this.timeToScreen(cur),
703         isMajor = step.isMajor();
704 
705     this.redrawAxisMinorText(x, step.getLabelMinor());
706 
707     if (isMajor && options.showMajorLabels) {
708       if (x > 0) {
709         if (xFirstMajorLabel === undefined) {
710           xFirstMajorLabel = x;
711         }
712         this.redrawAxisMajorText(x, step.getLabelMajor());
713       }
714       this.redrawAxisMajorLine(x);
715     }
716     else {
717       this.redrawAxisMinorLine(x);
718     }
719 
720     step.next();
721   }
722 
723   // create a major label on the left when needed
724   if (options.showMajorLabels) {
725     var leftTime = this.screenToTime(0),
726       leftText = this.step.getLabelMajor(leftTime),
727       width = leftText.length * size.axis.characterMajorWidth + 10;// estimation
728 
729     if (xFirstMajorLabel === undefined || width < xFirstMajorLabel) {
730       this.redrawAxisMajorText(0, leftText, leftTime);
731     }
732   }
733 
734   this.redrawAxisHorizontal();
735 
736   // cleanup left over labels
737   this.redrawAxisEndOverwriting();
738   
739   // put axis online
740   dom.content.insertBefore(axis.frame, dom.content.firstChild);
741 
742 }
743 
744 /**
745  * Create characters used to determine the size of text on the axis
746  */ 
747 links.Timeline.prototype.redrawAxisCharacters = function () {
748   // calculate the width and height of a single character
749   // this is used to calculate the step size, and also the positioning of the
750   // axis
751   var dom = this.dom,
752     axis = dom.axis;
753   
754   if (!axis.characterMinor) {
755     var text = document.createTextNode("0");
756     var characterMinor = document.createElement("DIV");
757     characterMinor.className = "timeline-axis-text timeline-axis-text-minor";
758     characterMinor.appendChild(text);
759     characterMinor.style.position = "absolute";
760     characterMinor.style.visibility = "hidden";
761     characterMinor.style.paddingLeft = "0px";
762     characterMinor.style.paddingRight = "0px";
763     axis.frame.appendChild(characterMinor);
764     
765     axis.characterMinor = characterMinor;    
766   }
767   
768   if (!axis.characterMajor) {
769     var text = document.createTextNode("0");
770     var characterMajor = document.createElement("DIV");
771     characterMajor.className = "timeline-axis-text timeline-axis-text-major";
772     characterMajor.appendChild(text);
773     characterMajor.style.position = "absolute";
774     characterMajor.style.visibility = "hidden";
775     characterMajor.style.paddingLeft = "0px";
776     characterMajor.style.paddingRight = "0px";
777     axis.frame.appendChild(characterMajor);
778     
779     axis.characterMajor = characterMajor;
780   }
781 }
782 
783 /**
784  * Initialize redraw of the axis. All existing labels and lines will be
785  * overwritten and reused.  
786  */ 
787 links.Timeline.prototype.redrawAxisStartOverwriting = function () {
788   var properties = this.size.axis.properties;
789   
790   properties.minorTextNum = 0;
791   properties.minorLineNum = 0;
792   properties.majorTextNum = 0;
793   properties.majorLineNum = 0;
794 }
795 
796 /**
797  * End of overwriting HTML DOM elements of the axis.
798  * remaining elements will be removed
799  */ 
800 links.Timeline.prototype.redrawAxisEndOverwriting = function () {
801   var dom = this.dom,
802     props = this.size.axis.properties,
803     frame = this.dom.axis.frame;
804   
805   // remove leftovers
806   var minorTexts = dom.axis.minorTexts,
807       num = props.minorTextNum;
808   while (minorTexts.length > num) {
809     var minorText = minorTexts[num];
810     frame.removeChild(minorText);
811     minorTexts.splice(num, 1);
812   }
813   
814   var minorLines = dom.axis.minorLines,
815       num = props.minorLineNum;
816   while (minorLines.length > num) {
817     var minorLine = minorLines[num];
818     frame.removeChild(minorLine);
819     minorLines.splice(num, 1);
820   }    
821   
822   var majorTexts = dom.axis.majorTexts,
823       num = props.majorTextNum;
824   while (majorTexts.length > num) {
825     var majorText = majorTexts[num];
826     frame.removeChild(majorText);
827     majorTexts.splice(num, 1);
828   }
829   
830   var majorLines = dom.axis.majorLines,
831       num = props.majorLineNum;
832   while (majorLines.length > num) {
833     var majorLine = majorLines[num];
834     frame.removeChild(majorLine);
835     majorLines.splice(num, 1);
836   }    
837 }
838 
839 /**
840  * Redraw the horizontal line and background of the axis
841  */ 
842 links.Timeline.prototype.redrawAxisHorizontal = function() {
843   var axis = this.dom.axis,
844     size = this.size;
845   
846   if (!axis.backgroundLine) {
847     // create the axis line background (for a background color or so)
848     var backgroundLine = document.createElement("DIV");
849     backgroundLine.className = "timeline-axis";
850     backgroundLine.style.position = "absolute";
851     backgroundLine.style.left = "0px"; 
852     backgroundLine.style.width = "100%";
853     backgroundLine.style.border = "none";
854     axis.frame.insertBefore(backgroundLine, axis.frame.firstChild);
855     
856     axis.backgroundLine = backgroundLine;
857   }
858   axis.backgroundLine.style.top = size.axis.top + "px";
859   axis.backgroundLine.style.height = size.axis.height + "px";
860 
861   if (axis.line) {
862     // put this line at the end of all childs
863     var line = axis.frame.removeChild(axis.line);
864     axis.frame.appendChild(line);
865   }
866   else {
867     // make the axis line
868     var line = document.createElement("DIV");
869     line.className = "timeline-axis";
870     line.style.position = "absolute";
871     line.style.left = "0px"; 
872     line.style.width = "100%";
873     line.style.height = "0px";
874     axis.frame.appendChild(line);
875 
876     axis.line = line;
877   }
878   axis.line.style.top = size.axis.line + "px";    
879 
880 }
881 
882 /**
883  * Create a minor label for the axis at position x
884  * @param {Number} x
885  * @param {String} text
886  */ 
887 links.Timeline.prototype.redrawAxisMinorText = function (x, text) {
888   var size = this.size,
889       dom = this.dom,
890       props = size.axis.properties,
891       frame = dom.axis.frame,
892       minorTexts = dom.axis.minorTexts,
893       index = props.minorTextNum,
894       label;
895   
896   if (index < minorTexts.length) {
897     label = minorTexts[index]
898   }
899   else {
900     // create new label
901     var content = document.createTextNode(""),
902       label = document.createElement("DIV");
903     label.appendChild(content);
904     label.className = "timeline-axis-text timeline-axis-text-minor";
905     label.style.position = "absolute";
906     
907     frame.appendChild(label);
908 
909     minorTexts.push(label);    
910   }
911 
912   label.childNodes[0].nodeValue = text;
913   label.style.left = x + "px";
914   label.style.top  = size.axis.labelMinorTop + "px";
915   //label.title = title;  // TODO: this is a heavy operation
916 
917   props.minorTextNum++;
918 }
919 
920 /**
921  * Create a minor line for the axis at position x
922  * @param {Number} x
923  */ 
924 links.Timeline.prototype.redrawAxisMinorLine = function (x) {
925   var axis = this.size.axis,
926       dom = this.dom,
927       props = axis.properties,
928       frame = dom.axis.frame,
929       minorLines = dom.axis.minorLines,
930       index = props.minorLineNum,
931       line;
932 
933   if (index < minorLines.length) {
934     line = minorLines[index];
935   }
936   else {
937     // create vertical line
938     line = document.createElement("DIV");
939     line.className = "timeline-axis-grid timeline-axis-grid-minor";
940     line.style.position = "absolute";
941     line.style.width = "0px";
942     
943     frame.appendChild(line);
944     minorLines.push(line);    
945   }
946 
947   line.style.top = axis.lineMinorTop + "px";
948   line.style.height = axis.lineMinorHeight + "px";
949   line.style.left = (x - axis.lineMinorWidth/2) + "px";
950   
951   props.minorLineNum++;
952 }
953 
954 /**
955  * Create a Major label for the axis at position x
956  * @param {Number} x
957  * @param {String} text
958  */ 
959 links.Timeline.prototype.redrawAxisMajorText = function (x, text) {
960   var size = this.size,
961       props = size.axis.properties,
962       frame = this.dom.axis.frame,
963       majorTexts = this.dom.axis.majorTexts,
964       index = props.majorTextNum,
965       label;
966 
967   if (index < majorTexts.length) {
968     label = majorTexts[index];
969   }
970   else {
971     // create label
972     var content = document.createTextNode(text);
973     label = document.createElement("DIV");
974     label.className = "timeline-axis-text timeline-axis-text-major";
975     label.appendChild(content);
976     label.style.position = "absolute";
977     label.style.top = "0px";
978     
979     frame.appendChild(label);    
980     majorTexts.push(label);
981   }
982   
983   label.childNodes[0].nodeValue = text;
984   label.style.top = size.axis.labelMajorTop + "px";
985   label.style.left = x + "px";
986   //label.title = title; // TODO: this is a heavy operation
987   
988   props.majorTextNum ++;
989 }
990 
991 /**
992  * Create a Major line for the axis at position x
993  * @param {Number} x
994  */ 
995 links.Timeline.prototype.redrawAxisMajorLine = function (x) {
996   var size = this.size,
997       props = size.axis.properties,
998       axis = this.size.axis,
999       frame = this.dom.axis.frame,
1000       majorLines = this.dom.axis.majorLines,
1001       index = props.majorLineNum,
1002       line;
1003       
1004   if (index < majorLines.length) {
1005     var line = majorLines[index];
1006   }
1007   else {
1008     // create vertical line
1009     line = document.createElement("DIV");
1010     line.className = "timeline-axis-grid timeline-axis-grid-major";
1011     line.style.position = "absolute";
1012     line.style.top = "0px";
1013     line.style.width = "0px";
1014     
1015     frame.appendChild(line);
1016     majorLines.push(line);
1017   }
1018   
1019   line.style.left = (x - axis.lineMajorWidth/2) + "px";
1020   line.style.height = size.frameHeight + "px";
1021 
1022   props.majorLineNum ++;
1023 }
1024 
1025 /**
1026  * Redraw all items
1027  */ 
1028 links.Timeline.prototype.redrawItems = function() {
1029   var dom = this.dom,
1030     options = this.options,
1031     size = this.size,
1032     contentWidth = size.contentWidth,
1033     items = this.items;
1034   
1035   if (!dom.items) {
1036     dom.items = {};
1037   }
1038   
1039   // draw the frame containing the items
1040   var frame = dom.items.frame;
1041   if (!frame) {
1042     frame = document.createElement("DIV");
1043     frame.style.position = "relative";
1044     frame.style.left = "0px";
1045     frame.style.width = "0px";
1046     dom.content.appendChild(frame);
1047     dom.items.frame = frame;
1048   }
1049   frame.style.top = size.items.top + "px";
1050   frame.style.height = (size.frameHeight - size.axis.height) + "px";
1051   
1052   // initialize arrarys for storing the items
1053   var ranges = dom.items.ranges;
1054   if (!ranges) {
1055     ranges = [];
1056     dom.items.ranges = ranges;
1057   }
1058   var boxes = dom.items.boxes;
1059   if (!boxes) {
1060     boxes = [];
1061     dom.items.boxes = boxes;
1062   }
1063   var dots = dom.items.dots;
1064   if (!dots) {
1065     dots = [];
1066     dom.items.dots = dots;
1067   }
1068   
1069   // Take frame offline
1070   dom.content.removeChild(frame);
1071   
1072   if (size.dataChanged) {
1073     // create the items
1074     var rangesCreated = ranges.length,
1075       boxesCreated = boxes.length,
1076       dotsCreated = dots.length,
1077       rangesUsed = 0,
1078       boxesUsed = 0,
1079       dotsUsed = 0,
1080       itemsLength = items.length;
1081 
1082     for (var i = 0, iMax = items.length; i < iMax; i++) {
1083       var item = items[i];
1084       switch (item.type) {
1085         case 'range':
1086           if (rangesUsed < rangesCreated) {
1087             // reuse existing range
1088             var domItem = ranges[rangesUsed];
1089             domItem.firstChild.innerHTML = item.content;
1090             domItem.style.display = '';            
1091             item.dom = domItem;
1092             rangesUsed++;
1093           }
1094           else {
1095             // create a new range
1096             var domItem = this.createEventRange(item.content);
1097             ranges[rangesUsed] = domItem;
1098             frame.appendChild(domItem);
1099             item.dom = domItem;
1100             rangesUsed++;
1101             rangesCreated++;
1102           }
1103           break;
1104 
1105         case 'box':
1106           if (boxesUsed < boxesCreated) {
1107             // reuse existing box
1108             var domItem = boxes[boxesUsed];
1109             domItem.firstChild.innerHTML = item.content;
1110             domItem.style.display = '';            
1111             item.dom = domItem;
1112             boxesUsed++;
1113           }
1114           else {
1115             // create a new box
1116             var domItem = this.createEventBox(item.content);
1117             boxes[boxesUsed] = domItem;
1118             frame.appendChild(domItem);
1119             frame.insertBefore(domItem.line, frame.firstChild); 
1120             // Note: line must be added in front of the items, 
1121             //       such that it stays below all items
1122             frame.appendChild(domItem.dot);
1123             item.dom = domItem;
1124             boxesUsed++;
1125             boxesCreated++;
1126           }
1127           break;
1128         
1129         case 'dot':
1130           if (dotsUsed < dotsCreated) {
1131             // reuse existing box
1132             var domItem = dots[dotsUsed];
1133             domItem.firstChild.innerHTML = item.content;
1134             domItem.style.display = '';            
1135             item.dom = domItem;
1136             dotsUsed++;
1137           }
1138           else {
1139             // create a new box
1140             var domItem = this.createEventDot(item.content);
1141             dots[dotsUsed] = domItem;
1142             frame.appendChild(domItem);
1143             item.dom = domItem;
1144             dotsUsed++;
1145             dotsCreated++;
1146           }
1147           break;
1148         
1149         default:
1150           // do nothing
1151           break;
1152       }
1153     }
1154 
1155     // remove redundant items when needed
1156     for (var i = rangesUsed; i < rangesCreated; i++) {
1157       frame.removeChild(ranges[i]);
1158     }
1159     ranges.splice(rangesUsed, rangesCreated - rangesUsed);
1160     for (var i = boxesUsed; i < boxesCreated; i++) {
1161       var box = boxes[i];
1162       frame.removeChild(box.line);
1163       frame.removeChild(box.dot);
1164       frame.removeChild(box);
1165     }
1166     boxes.splice(boxesUsed, boxesCreated - boxesUsed); 
1167     for (var i = dotsUsed; i < dotsCreated; i++) {
1168       frame.removeChild(dots[i]);
1169     }
1170     dots.splice(dotsUsed, dotsCreated - dotsUsed);
1171   }
1172   
1173   // reposition all items
1174   for (var i = 0, iMax = items.length; i < iMax; i++) {
1175     var item = items[i],
1176       domItem = item.dom;
1177     
1178     switch (item.type) {
1179       case 'range':
1180         var left = this.timeToScreen(item.start),
1181           right = this.timeToScreen(item.end);      
1182 
1183         // limit the width of the item, as browsers cannot draw very wide divs
1184         if (left < -contentWidth) {
1185           left = -contentWidth;
1186         }
1187         if (right > 2 * contentWidth) {
1188           right = 2 * contentWidth;
1189         }
1190         
1191         var visible = right > -contentWidth && left < 2 * contentWidth;
1192         if (visible || size.dataChanged) {
1193           // when data is changed, all items must be kept visible, as their heights must be measured
1194           if (item.hidden) {
1195             item.hidden = false; 
1196             domItem.style.display = '';
1197           }
1198           domItem.style.top = item.top + "px";
1199           domItem.style.left = left + "px";
1200           //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px"; // TODO: borderWidth
1201           domItem.style.width = Math.max(right - left, 1) + "px"; 
1202         }
1203         else {
1204           // hide when outside of the current window
1205           if (!item.hidden) {
1206             domItem.style.display = 'none';
1207             item.hidden = true;
1208           }
1209         }      
1210 
1211         break;
1212 
1213       case 'box':
1214         var left = this.timeToScreen(item.start);
1215 
1216         var axisOnTop = options.axisOnTop,
1217           axisHeight = size.axis.height,
1218           axisTop = size.axis.top;
1219         var visible = ((left + item.width/2 > -contentWidth) && 
1220           (left - item.width/2 < 2 * contentWidth));
1221         if (visible || size.dataChanged) {
1222           // when data is changed, all items must be kept visible, as their heights must be measured
1223           if (item.hidden) {
1224             item.hidden = false; 
1225             domItem.style.display = '';
1226             domItem.line.style.display = '';
1227             domItem.dot.style.display = '';
1228           }
1229           domItem.style.top = item.top + "px";
1230           domItem.style.left = (left - item.width/2) + "px";
1231           
1232           var line = domItem.line;
1233           line.style.left = (left - item.lineWidth/2) + "px";
1234           if (axisOnTop) {
1235             //line.style.top = axisHeight + "px"; // TODO: cleanup
1236             //line.style.height = (item.top - axisHeight) + "px";
1237             line.style.top = "0px";
1238             line.style.height = item.top + "px";
1239           }
1240           else {
1241             line.style.top = (item.top + item.height) + "px";
1242             line.style.height = (axisTop - item.top - item.height) + "px";
1243           }
1244           
1245           var dot = domItem.dot;
1246           dot.style.left = (left - item.dotWidth/2) + "px";
1247           dot.style.top = (axisTop - item.dotHeight/2) + "px";
1248         }
1249         else {
1250           // hide when outside of the current window
1251           if (!item.hidden) {
1252             domItem.style.display = 'none';
1253             domItem.line.style.display = 'none';
1254             domItem.dot.style.display = 'none';
1255             item.hidden = true;
1256           }
1257         }      
1258         break;
1259 
1260       case 'dot':
1261         var left = this.timeToScreen(item.start);
1262 
1263         var axisOnTop = options.axisOnTop,
1264           axisHeight = size.axis.height,
1265           axisTop = size.axis.top;
1266         var visible = (left + item.width > -contentWidth) && (left < 2 * contentWidth);
1267         if (visible || size.dataChanged) {
1268           // when data is changed, all items must be kept visible, as their heights must be measured
1269           if (item.hidden) {
1270             item.hidden = false; 
1271             domItem.style.display = '';
1272           }
1273           domItem.style.top = item.top + "px";
1274           domItem.style.left = (left - item.dotWidth / 2) + "px";
1275           
1276           domItem.content.style.marginLeft = (1.5 * item.dotWidth) + "px";
1277           domItem.dot.style.top = ((item.height - item.dotHeight) / 2) + "px";
1278         }
1279         else {
1280           // hide when outside of the current window
1281           if (!item.hidden) {
1282             domItem.style.display = 'none';
1283             item.hidden = true;
1284           }
1285         }      
1286         break;
1287       
1288       default:
1289         // do nothing
1290         break;
1291     }
1292   }
1293   
1294   // move selected item to the end, to ensure that it is always on top
1295   if (this.selection) {
1296     var item = this.selection.item;
1297     frame.removeChild(item);
1298     frame.appendChild(item);
1299   }
1300   
1301   // put frame online again
1302   dom.content.appendChild(frame);  
1303 }
1304 
1305 
1306 /**
1307  * Create an event in the timeline, with (optional) formatting: inside a box 
1308  * with rounded corners, and a vertical line+dot to the axis.
1309  * @param {string} content    The content for the event. This can be plain text
1310  *                            or HTML code.
1311  */ 
1312 links.Timeline.prototype.createEventBox = function(content) {
1313   // background box
1314   var divBox = document.createElement("DIV");
1315   divBox.style.position = "absolute";
1316   divBox.style.left  = "0px";
1317   divBox.style.top = "0px";
1318   divBox.className  = "timeline-event timeline-event-box";
1319 
1320   // contents box (inside the background box). used for making margins
1321   var divContent = document.createElement("DIV");
1322   divContent.className = "timeline-event-content";
1323   divContent.innerHTML = content;
1324   divBox.appendChild(divContent);
1325 
1326   // line to axis
1327   var divLine = document.createElement("DIV");
1328   divLine.style.position = "absolute";
1329   divLine.style.width = "0px";
1330   divLine.className = "timeline-event timeline-event-line";
1331   // important: the vertical line is added at the front of the list of elements,
1332   // so it will be drawn behind all boxes and ranges
1333   divBox.line = divLine;
1334   
1335   // dot on axis
1336   var divDot = document.createElement("DIV");
1337   divDot.style.position = "absolute";
1338   divDot.style.width  = "0px";
1339   divDot.style.height = "0px";
1340   divDot.className  = "timeline-event timeline-event-dot";
1341   divBox.dot = divDot;
1342 
1343   return divBox; 
1344 }
1345 
1346 
1347 /**
1348  * Create an event in the timeline: a dot, followed by the content.
1349  * @param {string} content    The content for the event. This can be plain text
1350  *                            or HTML code.
1351  */ 
1352 links.Timeline.prototype.createEventDot = function(content) {
1353   // background box
1354   var divBox = document.createElement("DIV");
1355   divBox.style.position = "absolute";
1356 
1357   // contents box, right from the dot
1358   var divContent = document.createElement("DIV");
1359   divContent.className = "timeline-event-content";
1360   divContent.innerHTML = content;
1361   divBox.appendChild(divContent);
1362 
1363   // dot at start
1364   var divDot = document.createElement("DIV");
1365   divDot.style.position = "absolute";
1366   divDot.className = "timeline-event timeline-event-dot";
1367   divDot.style.width = "0px";
1368   divDot.style.height = "0px";
1369   divBox.appendChild(divDot);
1370 
1371   divBox.content = divContent;
1372   divBox.dot = divDot;
1373 
1374   return divBox;
1375 }
1376 
1377 
1378 /**
1379  * Create an event range as a beam in the timeline.
1380  * @param {string}  content    The content for the event. This can be plain text
1381  *                             or HTML code.
1382  */ 
1383 links.Timeline.prototype.createEventRange = function(content) {
1384   // background box
1385   var divBox = document.createElement("DIV");
1386   divBox.style.position = "absolute";
1387   divBox.className = "timeline-event timeline-event-range";
1388 
1389   // contents box
1390   var divContent = document.createElement("DIV");
1391   divContent.className = "timeline-event-content";
1392   divContent.innerHTML = content;
1393   divBox.appendChild(divContent);
1394   
1395   return divBox;
1396 }
1397 
1398 /**
1399  * Redraw the group labels
1400  */ 
1401 links.Timeline.prototype.redrawGroups = function() {
1402   var dom = this.dom,
1403     options = this.options,
1404     size = this.size,
1405     groups = this.groups;
1406   
1407   if (dom.groups === undefined) {
1408     dom.groups = {};
1409   }
1410   
1411   var labels = dom.groups.labels;
1412   if (!labels) {
1413     labels = [];
1414     dom.groups.labels = labels;
1415   }    
1416   var labelLines = dom.groups.labelLines;
1417   if (!labelLines) {
1418     labelLines = [];
1419     dom.groups.labelLines = labelLines;
1420   }    
1421   var itemLines = dom.groups.itemLines;
1422   if (!itemLines) {
1423     itemLines = [];
1424     dom.groups.itemLines = itemLines;
1425   }  
1426 
1427   // create the frame for holding the groups
1428   var frame = dom.groups.frame;
1429   if (!frame) {
1430     var frame =  document.createElement("DIV");
1431     frame.className = "timeline-groups-axis";
1432     frame.style.position = "absolute";
1433     frame.style.overflow = "hidden";
1434     frame.style.top = "0px";
1435     frame.style.height = "100%";
1436 
1437     dom.frame.appendChild(frame);    
1438     dom.groups.frame = frame;
1439   }
1440   
1441   frame.style.left = size.groupsLeft + "px";
1442   frame.style.width = (options.groupsWidth !== undefined) ? 
1443     options.groupsWidth : 
1444     size.groupsWidth + "px";
1445 
1446   // hide groups axis when there are no groups
1447   if (groups.length == 0) {
1448     frame.style.display = 'none';
1449   }
1450   else {
1451     frame.style.display = '';
1452   }
1453 
1454   if (size.dataChanged) {
1455     // create the items
1456     var current = labels.length,
1457       needed = groups.length;
1458 
1459     // overwrite existing items
1460     for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) {
1461       var group = groups[i];
1462       var label = labels[i];
1463       label.innerHTML = group.content;
1464       label.style.display = '';
1465     }
1466     
1467     // append new items when needed
1468     for (var i = current; i < needed; i++) {
1469       var group = groups[i];
1470       
1471       // create text label
1472       var label = document.createElement("DIV");
1473       label.className = "timeline-groups-text";
1474       label.style.position = "absolute";
1475       if (options.groupsWidth === undefined) {
1476         label.style.whiteSpace = "nowrap";
1477       }
1478       label.innerHTML = group.content;
1479       frame.appendChild(label);
1480       labels[i] = label;
1481 
1482       // create the grid line between the group labels
1483       var labelLine = document.createElement("DIV");
1484       labelLine.className = "timeline-axis-grid timeline-axis-grid-minor";
1485       labelLine.style.position = "absolute";
1486       labelLine.style.left = "0px";
1487       labelLine.style.width = "100%";
1488       labelLine.style.height = "0px";
1489       labelLine.style.borderTopStyle = "solid";
1490       frame.appendChild(labelLine);
1491       labelLines[i] = labelLine;
1492       
1493       // create the grid line between the items
1494       var itemLine = document.createElement("DIV");
1495       itemLine.className = "timeline-axis-grid timeline-axis-grid-minor";
1496       itemLine.style.position = "absolute";
1497       itemLine.style.left = "0px";
1498       itemLine.style.width = "100%";
1499       itemLine.style.height = "0px";
1500       itemLine.style.borderTopStyle = "solid";
1501       dom.content.insertBefore(itemLine, dom.content.firstChild);
1502       itemLines[i] = itemLine;
1503     }    
1504     
1505     // remove redundant items from the DOM when needed
1506     for (var i = needed; i < current; i++) {
1507       var label = labels[i],
1508         labelLine = labelLines[i],
1509         itemLine = itemLines[i];
1510         
1511       frame.removeChild(label);
1512       frame.removeChild(labelLine);
1513       dom.content.removeChild(itemLine);
1514     }
1515     labels.splice(needed, current - needed);
1516     labelLines.splice(needed, current - needed);
1517     itemLines.splice(needed, current - needed);
1518 
1519     frame.style.borderStyle = options.groupsOnRight ? 
1520       "none none none solid" :
1521       "none solid none none";
1522   }
1523 
1524   // position the groups
1525   for (var i = 0, iMax = groups.length; i < iMax; i++) {
1526     var group = groups[i],
1527       label = labels[i],
1528       labelLine = labelLines[i],
1529       itemLine = itemLines[i];
1530 
1531     label.style.top = group.labelTop + "px";
1532     labelLine.style.top = group.lineTop + "px";
1533     itemLine.style.top = group.lineTop + "px";
1534     itemLine.style.width = size.contentWidth + "px";
1535   }
1536   
1537   if (!dom.groups.background) {
1538     // create the axis grid line background
1539     var background = document.createElement("DIV");
1540     background.className = "timeline-axis";
1541     background.style.position = "absolute";
1542     background.style.left = "0px";
1543     background.style.width = "100%";
1544     background.style.border = "none";
1545     
1546     frame.appendChild(background);
1547     dom.groups.background = background;
1548   }
1549   dom.groups.background.style.top = size.axis.top + 'px';
1550   dom.groups.background.style.height = size.axis.height + 'px';
1551 
1552   if (!dom.groups.line) {
1553     // create the axis grid line
1554     var line = document.createElement("DIV");
1555     line.className = "timeline-axis";
1556     line.style.position = "absolute";
1557     line.style.left = "0px";
1558     line.style.width = "100%";
1559     line.style.height = "0px";
1560     
1561     frame.appendChild(line);
1562     dom.groups.line = line;
1563   }
1564   dom.groups.line.style.top = size.axis.line + 'px';
1565 }
1566 
1567 
1568 /**
1569  * Redraw the current time bar
1570  */ 
1571 links.Timeline.prototype.redrawCurrentTime = function() {
1572   var options = this.options,
1573     dom = this.dom,
1574     size = this.size;
1575   
1576   if (!options.showCurrentTime) {
1577     if (dom.currentTime) {
1578       dom.contentTimelines.removeChild(dom.currentTime);
1579       delete dom.currentTime;
1580     }
1581     
1582     return;
1583   }
1584   
1585   if (!dom.currentTime) {
1586     // create the current time bar
1587     var currentTime = document.createElement("DIV");
1588     currentTime.className = "timeline-currenttime";
1589     currentTime.style.position = "absolute";
1590     currentTime.style.top = "0px";
1591     currentTime.style.height = "100%";
1592 
1593     dom.contentTimelines.appendChild(currentTime);
1594     dom.currentTime = currentTime;  
1595   }
1596   
1597   var now = new Date();
1598   var nowOffset = new Date(now.getTime() + this.clientTimeOffset);
1599   var x = this.timeToScreen(nowOffset);
1600   
1601   var visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
1602   dom.currentTime.style.display = visible ? '' : 'none';
1603   dom.currentTime.style.left = x + "px";
1604   dom.currentTime.title = "Current time: " + nowOffset;
1605 
1606   // start a timer to adjust for the new time
1607   if (this.currentTimeTimer != undefined) {
1608     clearTimeout(this.currentTimeTimer);
1609     delete this.currentTimeTimer;
1610   }
1611   var timeline = this;
1612   var onTimeout = function() {
1613     timeline.redrawCurrentTime();
1614   }
1615   // the time equal to the width of one pixel, divided by 2 for more smoothness
1616   var interval = 1 / this.conversion.factor / 2; 
1617   if (interval < 30) interval = 30; 
1618   this.currentTimeTimer = setTimeout(onTimeout, interval);
1619 }
1620 
1621 /**
1622  * Redraw the custom time bar
1623  */ 
1624 links.Timeline.prototype.redrawCustomTime = function() {
1625   var options = this.options,
1626     dom = this.dom,
1627     size = this.size;
1628 
1629   if (!options.showCustomTime) {
1630     if (dom.customTime) {
1631       dom.contentTimelines.removeChild(dom.customTime);
1632       delete dom.customTime;
1633     }
1634     
1635     return;
1636   }
1637   
1638   if (!dom.customTime) {
1639     var customTime = document.createElement("DIV");
1640     customTime.className = "timeline-customtime";
1641     customTime.style.position = "absolute";    
1642     customTime.style.top = "0px";
1643     customTime.style.height = "100%";
1644     
1645     var drag = document.createElement("DIV");
1646     drag.style.position = "relative";
1647     drag.style.top = "0px";
1648     drag.style.left = "-10px";
1649     drag.style.height = "100%";
1650     drag.style.width = "20px";
1651     customTime.appendChild(drag);
1652 
1653     dom.contentTimelines.appendChild(customTime);
1654     dom.customTime = customTime;  
1655     
1656     // initialize parameter
1657     this.customTime = new Date();
1658   }
1659   
1660   var x = this.timeToScreen(this.customTime),
1661     visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
1662   dom.customTime.style.display = visible ? '' : 'none';
1663   dom.customTime.style.left = x + "px";
1664   dom.customTime.title = "Time: " + this.customTime;
1665 }
PORT 
1667 
1668 /**
1669  * Redraw the delete button, on the top right of the currently selected item
1670  * if there is no item selected, the button is hidden.
1671  */
1672 links.Timeline.prototype.redrawDeleteButton = function () {
1673   var timeline = this,
1674     options = this.options,
1675     dom = this.dom,
1676     size = this.size,
1677     frame = dom.items.frame;
1678   
1679   if (!options.editable) {
1680     return;
1681   }
1682   
1683   var deleteButton = dom.items.deleteButton;
1684   if (!deleteButton) {
1685     // create a delete button
1686     deleteButton = document.createElement("DIV");
1687     deleteButton.className = "timeline-navigation-delete";
1688     deleteButton.style.position = "absolute";
1689     
1690     frame.appendChild(deleteButton);
1691     dom.items.deleteButton = deleteButton;
1692   }
1693   
1694   if (this.selection) {
1695     var index = this.selection.index,
1696       item = this.items[index],
1697       domItem = this.selection.item,
1698       right,
1699       top = item.top;
1700 
1701     switch (item.type) {
1702       case 'range':
1703         right = this.timeToScreen(item.end);
1704         break;
1705 
1706       case 'box':
1707         //right = this.timeToScreen(item.start) + item.width / 2 + item.borderWidth; // TODO: borderWidth
1708         right = this.timeToScreen(item.start) + item.width / 2;
1709         break;
1710         
1711       case 'dot':
1712         right = this.timeToScreen(item.start) + item.width;
1713         break;
1714     }
1715     
1716     // limit the position
1717     if (right < -size.contentWidth) {
1718       right = -size.contentWidth;
1719     }
1720     if (right > 2 * size.contentWidth) {
1721       right = 2 * size.contentWidth;
1722     }
1723     
1724     deleteButton.style.left = right + 'px';
1725     deleteButton.style.top = top + 'px';
1726     deleteButton.style.display = '';
1727     frame.removeChild(deleteButton);
1728     frame.appendChild(deleteButton);
1729   }
1730   else {
1731     deleteButton.style.display = 'none';
1732   }
1733 }
1734 
1735 
1736 /**
1737  * Redraw the drag areas. When an item (ranges only) is selected,
1738  * it gets a drag area on the left and right side, to change its width
1739  */
1740 links.Timeline.prototype.redrawDragAreas = function () {
1741   var timeline = this,
1742     options = this.options,
1743     dom = this.dom,
1744     size = this.size,
1745     frame = this.dom.items.frame;
1746   
1747   if (!options.editable) {
1748     return;
1749   }
1750   
1751   // create left drag area
1752   var dragLeft = dom.items.dragLeft;
1753   if (!dragLeft) {
1754     dragLeft = document.createElement("DIV");
1755     dragLeft.style.width = options.dragAreaWidth + "px";
1756     dragLeft.style.position = "absolute";
1757     dragLeft.style.cursor = "w-resize";
1758 
1759     frame.appendChild(dragLeft);
1760     dom.items.dragLeft = dragLeft;
1761   }
1762   
1763   // create right drag area
1764   var dragRight = dom.items.dragRight;
1765   if (!dragRight) {
1766     dragRight = document.createElement("DIV");
1767     dragRight.style.width = options.dragAreaWidth + "px";
1768     dragRight.style.position = "absolute";
1769     dragRight.style.cursor = "e-resize";
1770 
1771     frame.appendChild(dragRight);
1772     dom.items.dragRight = dragRight;
1773   }
1774 
1775   // reposition left and right drag area
1776   if (this.selection) {
1777     var index = this.selection.index,
1778       item = this.items[index];
1779     
1780     if (item.type == 'range') {
1781       var domItem = item.dom,
1782       left = this.timeToScreen(item.start),
1783       right = this.timeToScreen(item.end),
1784       top = item.top,
1785       height = item.height;
1786     
1787       dragLeft.style.left = left + 'px';
1788       dragLeft.style.top = top + 'px';
1789       dragLeft.style.height = height + 'px';
1790       dragLeft.style.display = '';
1791       frame.removeChild(dragLeft);
1792       frame.appendChild(dragLeft);
1793       
1794       dragRight.style.left = (right - options.dragAreaWidth) + 'px';
1795       dragRight.style.top = top + 'px';
1796       dragRight.style.height = height + 'px';
1797       dragRight.style.display = '';    
1798       frame.removeChild(dragRight);
1799       frame.appendChild(dragRight);
1800     }
1801   }
1802   else {
1803     dragLeft.style.display = 'none';
1804     dragRight.style.display = 'none';
1805   }
1806 }
1807 
1808 
1809 
1810 /**
1811  * Create the navigation buttons for zooming and moving
1812  */
1813 links.Timeline.prototype.redrawNavigation = function () {
1814   var timeline = this,
1815     options = this.options,
1816     dom = this.dom,
1817     frame = dom.frame,
1818     navBar = dom.navBar;
1819 
1820   if (!navBar) {      
1821     if (options.editable || options.showNavigation) {
1822       // create a navigation bar containing the navigation buttons
1823       navBar = document.createElement("DIV");
1824       navBar.style.position = "absolute";
1825       navBar.className = "timeline-navigation";
1826       if (options.groupsOnRight) {
1827         navBar.style.left = '10px';
1828       }
1829       else {
1830         navBar.style.right = '10px';
1831       }
1832       if (options.axisOnTop) {
1833         navBar.style.bottom = '10px';
1834       }
1835       else {
1836         navBar.style.top = '10px';  
1837       }
1838       dom.navBar = navBar;
1839       frame.appendChild(navBar);
1840     }
1841         
1842     if (options.editable && options.showButtonAdd) {
1843       // create a new in button
1844       navBar.addButton = document.createElement("DIV");
1845       navBar.addButton.className = "timeline-navigation-new";
1846       
1847       navBar.addButton.title = "Create new event";
1848       var onAdd = function(event) {
1849         links.Timeline.prevent   ult(event);
1850         links.Timeline.stopPropagation(event);
1851         
1852         // create a new event at the center of the frame
1853         var w = timeline.size.contentWidth;
1854         var x = w / 2;
1855         var xstart = timeline.screenToTime(x - w / 10); // subtract 10% of timeline width
1856         var xend = timeline.screenToTime(x + w / 10); // add 10% of timeline width
1857         if (options.snapEvents) {
1858           timeline.step.snap(xstart);
1859           timeline.step.snap(xend);
1860         }
1861 
1862         var content = "New";
1863         var group = timeline.groups.length ? timeline.groups[0].content : undefined;
1864 
1865         timeline.addItem({
1866           'start': xstart, 
1867           'end': xend, 
1868           'content': content, 
1869           'group': group
1870         });
1871         var index = (timeline.items.length - 1);
1872         timeline.selectItem(index);
1873           
1874         timeline.applyAdd = true;
1875 
1876         // fire an add event. 
1877         // Note that the change can be canceled from within an event listener if 
1878         // this listener calls the method cancelAdd().
1879         timeline.trigger('add');    
1880 
1881         if (!timeline.applyAdd) {
1882           // undo an add
1883           timeline.deleteItem(index);
1884         }
1885         timeline.redrawDeleteButton();
1886         timeline.redrawDragAreas();
1887       };
1888       links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd); 
1889       navBar.appendChild(navBar.addButton);
1890     }
1891     
1892     if (options.editable && options.showButtonAdd && options.showNavigation) {
1893       // create a separator line
1894       navBar.addButton.style.borderRightWidth = "1px";
1895       navBar.addButton.style.borderRightStyle = "solid";
1896     }      
1897       
1898     if (options.showNavigation) {
1899       // create a zoom in button
1900       navBar.zoomInButton = document.createElement("DIV");
1901       navBar.zoomInButton.className = "timeline-navigation-zoom-in";
1902       navBar.zoomInButton.title = "Zoom in";
1903       var onZoomIn = function(event) {
1904         links.Timeline.prevent   ult(event);
1905         links.Timeline.stopPropagation(event);
1906         timeline.zoom(0.4);
1907         timeline.trigger("rangechange");
1908         timeline.trigger("rangechanged");
1909       };
1910       links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn);    
1911       navBar.appendChild(navBar.zoomInButton);
1912       
1913       // create a zoom out button
1914       navBar.zoomOutButton = document.createElement("DIV");
1915       navBar.zoomOutButton.className = "timeline-navigation-zoom-out";
1916       navBar.zoomOutButton.title = "Zoom out";
1917       var onZoomOut = function(event) {
1918         links.Timeline.prevent   ult(event);
1919         links.Timeline.stopPropagation(event);
1920         timeline.zoom(-0.4);
1921         timeline.trigger("rangechange");
1922         timeline.trigger("rangechanged");
1923       };
1924       links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut);    
1925       navBar.appendChild(navBar.zoomOutButton);
1926         
1927       // create a move left button
1928       navBar.moveLeftButton = document.createElement("DIV");
1929       navBar.moveLeftButton.className = "timeline-navigation-move-left";
1930       navBar.moveLeftButton.title = "Move left";
1931       var onMoveLeft = function(event) {
1932         links.Timeline.prevent   ult(event);
1933         links.Timeline.stopPropagation(event);
1934         timeline.move(-0.2);
1935         timeline.trigger("rangechange");
1936         timeline.trigger("rangechanged");
1937       };
1938       links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft);    
1939       navBar.appendChild(navBar.moveLeftButton);
1940       
1941       // create a move right button
1942       navBar.moveRightButton = document.createElement("DIV");
1943       navBar.moveRightButton.className = "timeline-navigation-move-right";
1944       navBar.moveRightButton.title = "Move right";
1945       var onMoveRight = function(event) {
1946         links.Timeline.prevent   ult(event);
1947         links.Timeline.stopPropagation(event);
1948         timeline.move(0.2);
1949         timeline.trigger("rangechange");
1950         timeline.trigger("rangechanged");
1951       };
1952       links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight);    
1953       navBar.appendChild(navBar.moveRightButton);
1954     }
1955   }
1956 }
1957 
1958 
1959 /**
1960  * Set current time. This function can be used to set the time in the client
1961  * timeline equal with the time on a server.
1962  * @param {Date} time 
1963  */ 
1964 links.Timeline.prototype.setCurrentTime = function(time) {
1965   var now = new Date();
1966   this.clientTimeOffset = time.getTime() - now.getTime();
1967   
1968   this.redrawCurrentTime();
1969 }
1970 
1971 /**
1972  * Get current time. The time can have an offset from the real time, when
1973  * the current time has been changed via the method setCurrentTime.
1974  * @return {Date} time 
1975  */ 
1976 links.Timeline.prototype.getCurrentTime = function() {
1977   var now = new Date();
1978   return new Date(now.getTime() + this.clientTimeOffset);
1979 }
1980 
1981 
1982 /**
1983  * Set custom time. 
1984  * The custom time bar can be used to display events in past or future.
1985  * @param {Date} time 
1986  */ 
1987 links.Timeline.prototype.setCustomTime = function(time) {
1988   this.customTime = new Date(time);
1989   this.redrawCustomTime();
1990 }
1991 
1992 /**
1993  * Retrieve the current custom time. 
1994  * @return {Date} customTime 
1995  */ 
1996 links.Timeline.prototype.getCustomTime = function() {
1997   return new Date(this.customTime);
1998 }
1999 
2000 /**
2001  * Set a custom scale. Autoscaling will be disabled.
2002  * For example setScale(SCALE.MINUTES, 5) will result
2003  * in minor steps of 5 minutes, and major steps of an hour. 
2004  * 
2005  * @param {Step.SCALE} newScale  A scale. Choose from SCALE.MILLISECOND,
2006  *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
2007  *                               SCALE.DAY, SCALE.MONTH, SCALE.YEAR.
2008  * @param {int}        newStep   A step size, by default 1. Choose for
2009  *                               example 1, 2, 5, or 10.
2010  */   
2011 links.Timeline.prototype.setScale = function(scale, step) {
2012   this.step.setScale(scale, step);
2013   this.redrawFrame();
2014 }
2015 
2016 /**
2017  * Enable or disable autoscaling
2018  * @param {boolean} enable  If true or not defined, autoscaling is enabled. 
2019  *                          If false, autoscaling is disabled. 
2020  */ 
2021 links.Timeline.prototype.setAutoScale = function(enable) {
2022   this.step.setAutoScale(enable);
2023   this.redrawFrame();
2024 }
2025 
2026 /**
2027  * Redraw the timeline
2028  * Reloads the (linked) data table and redraws the timeline when resized.
2029  * See also the method checkResize
2030  */ 
2031 links.Timeline.prototype.redraw = function() {
2032   this.setData(this.data);
2033 }
2034 
2035 
2036 /**
2037  * Check if the timeline is resized, and if so, redraw the timeline.
2038  * Useful when the webpage is resized.
2039  */ 
2040 links.Timeline.prototype.checkResize = function() {
2041   var resized = this.recalcSize();
2042   if (resized) {
2043     this.redrawFrame();
2044   }
2045 }
2046 
2047 
2048 /**
2049  * Recalculate the sizes of all frames, groups, items, axis
2050  * After recalcSize() is executed, the Timeline should be redrawn normally
2051  * 
2052  * @return {boolean} resized   Returns true when the timeline has been resized
2053  */ 
2054 links.Timeline.prototype.recalcSize = function() {
2055   var resized = false;
2056 
2057   var size = this.size,
2058     options = this.options,
2059     axisOnTop = options.axisOnTop,
2060     dom = this.dom,
2061     axis = dom.axis,
2062     groups = this.groups,
2063     labels = dom.groups.labels,
2064     items = this.items
2065     
2066     groupsWidth = size.groupsWidth,
2067     characterMinorWidth  = axis.characterMinor ? axis.characterMinor.clientWidth : 0,
2068     characterMinorHeight = axis.characterMinor ? axis.characterMinor.clientHeight : 0,
2069     characterMajorWidth  = axis.characterMajor ? axis.characterMajor.clientWidth : 0,
2070     characterMajorHeight = axis.characterMajor ? axis.characterMajor.clientHeight : 0,
2071     axisHeight = characterMinorHeight + (options.showMajorLabels ? characterMajorHeight : 0),
2072     actualHeight = size.actualHeight || axisHeight;
2073 
2074   // check sizes of the groups (width and height) when the data is changed
2075   if (size.dataChanged) {
2076     groupsWidth = 0;
2077     
2078     // loop through all groups to get the maximum width and the heights
2079     for (var i = 0, iMax = labels.length; i < iMax; i++) {
2080       var group = groups[i];
2081       group.width = labels[i].clientWidth;
2082       group.height = labels[i].clientHeight;
2083       group.labelHeight = group.height;
2084       
2085       groupsWidth = Math.max(groupsWidth, group.width);
2086     }
2087     
2088     // loop through the width and height of all items
2089     for (var i = 0, iMax = items.length; i < iMax; i++) {
2090       var item = items[i],
2091         domItem = item.dom,
2092         group = item.group;
2093       
2094       item.width = domItem ? domItem.clientWidth : 0;
2095       item.height = domItem ? domItem.clientHeight : 0;
2096       //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth
2097 
2098       switch (item.type) {
2099         case 'range':
2100           break;
2101         
2102         case 'box':
2103           item.dotHeight = domItem.dot.offsetHeight;
2104           item.dotWidth = domItem.dot.offsetWidth;
2105           item.lineWidth = domItem.line.offsetWidth;
2106           break;
2107 
2108         case 'dot':
2109           item.dotHeight = domItem.dot.offsetHeight;
2110           item.dotWidth = domItem.dot.offsetWidth;
2111           item.contentHeight = domItem.content.offsetHeight;
2112           break;
2113       }
2114       
2115       if (group) {
2116         group.height = group.height ? Math.max(group.height, item.height) : item.height;
2117       }
2118     }
2119 
2120     // calculate the actual height of the timeline (needed for auto sizing
2121     // the timeline)
2122     actualHeight = axisHeight + 2 * options.eventMarginAxis;
2123     for (var i = 0, iMax = groups.length; i < iMax; i++) {
2124       actualHeight += groups[i].height + options.eventMargin;
2125     }
2126   }
2127   
2128   // calculate actual height of the timeline when there are no groups
2129   // but stacked items
2130   if (groups.length == 0 && options.autoHeight) {
2131     var min = 0,
2132       max = 0;
2133 
2134     if (this.animation && this.animation.finalItems) {
2135       // adjust the offset of all finalItems when the actualHeight has been changed
2136       var finalItems = this.animation.finalItems,
2137         finalItem = finalItems[0];
2138       if (finalItem && finalItem.top) {
2139         min = finalItem.top,
2140         max = finalItem.top + finalItem.height;
2141       }      
2142       for (var i = 1, iMax = finalItems.length; i < iMax; i++) {
2143         finalItem = finalItems[i];
2144         min = Math.min(min, finalItem.top);
2145         max = Math.max(max, finalItem.top + finalItem.height);          
2146       } 
2147     }
2148     else {
2149       var item = items[0];
2150       if (item && item.top) {
2151         min = item.top,
2152         max = item.top + item.height;
2153       }    
2154       for (var i = 1, iMax = items.length; i < iMax; i++) {
2155         var item = items[i];
2156         if (item.top) {
2157           min = Math.min(min, item.top);
2158           max = Math.max(max, (item.top + item.height));
2159         }
2160       }
2161     }
2162 
2163     actualHeight = (max - min) + 2 * options.eventMarginAxis + axisHeight;
2164     
2165     if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) {
2166       // adjust the offset of all items when the actualHeight has been changed
2167       var diff = actualHeight - size.actualHeight;
2168       if (this.animation && this.animation.finalItems) {
2169         var finalItems = this.animation.finalItems;        
2170         for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
2171           finalItems[i].top += diff;
2172           finalItems[i].item.top += diff; // TODO
2173         }        
2174       }
2175       else {
2176         for (var i = 0, iMax = items.length; i < iMax; i++) {
2177           items[i].top += diff;
2178         }
2179       }
2180     }  
2181   }
2182  
2183   // now the heights of the elements are known, we can calculate the the 
2184   // width and height of frame and axis and content 
2185   // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead
2186   var frameWidth  = dom.frame ? dom.frame.offsetWidth : 0,
2187     frameHeight = Math.max(options.autoHeight ? 
2188       actualHeight : (dom.frame ? dom.frame.clientHeight : 0),
2189       options.minHeight),
2190     axisTop    = axisOnTop ? 0 : frameHeight - axisHeight,
2191     axisLine   = axisOnTop ? axisHeight : axisTop,
2192     itemsTop  = axisOnTop ? axisHeight : 0, 
2193     contentHeight = Math.max(frameHeight - axisHeight, 0);
2194 
2195   if (options.groupsWidth !== undefined) {
2196     groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0;
2197   }
2198   var groupsLeft = options.groupsOnRight ? frameWidth - groupsWidth : 0;
2199 
2200   if (size.dataChanged) {
2201     // calculate top positions of the group labels and lines
2202     var eventMargin = options.eventMargin,
2203       top = axisOnTop ? 
2204         options.eventMarginAxis + eventMargin/2 : 
2205         contentHeight - options.eventMarginAxis + eventMargin/2;
2206 
2207     for (var i = 0, iMax = groups.length; i < iMax; i++) {
2208       var group = groups[i];
2209       if (axisOnTop) {
2210         group.top = top;
2211         group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2;
2212         group.lineTop = top + axisHeight + group.height + eventMargin/2;
2213         top += group.height + eventMargin;
2214       }
2215       else {
2216         top -= group.height + eventMargin;
2217         group.top = top;
2218         group.labelTop = top + (group.height - group.labelHeight) / 2;
2219         group.lineTop = top - eventMargin/2;
2220       }
2221     }
2222     
2223     // calculate top position of the items
2224     for (var i = 0, iMax = items.length; i < iMax; i++) {
2225       var item = items[i],
2226         group = item.group;
2227 
2228       if (group) {
2229         item.top = group.top;
2230       }
2231     }    
2232     
2233     resized = true;
2234   }
2235 
2236   resized = resized || (size.groupsWidth !== groupsWidth);
2237   resized = resized || (size.groupsLeft !== groupsLeft);
2238   resized = resized || (size.actualHeight !== actualHeight);
2239   size.groupsWidth = groupsWidth;
2240   size.groupsLeft = groupsLeft; 
2241   size.actualHeight = actualHeight;
2242 
2243   resized = resized || (size.frameWidth !== frameWidth);
2244   resized = resized || (size.frameHeight !== frameHeight);
2245   size.frameWidth = frameWidth;
2246   size.frameHeight = frameHeight;
2247   
2248   resized = resized || (size.groupsWidth !== groupsWidth);
2249   size.groupsWidth = groupsWidth;
2250   size.contentLeft = options.groupsOnRight ? 0 : groupsWidth;
2251   size.contentWidth = Math.max(frameWidth - groupsWidth, 0);
2252   size.contentHeight = contentHeight;
2253 
2254   resized = resized || (size.axis.top !== axisTop);
2255   resized = resized || (size.axis.line !== axisLine);
2256   resized = resized || (size.axis.height !== axisHeight);
2257   resized = resized || (size.items.top !== itemsTop);
2258   size.axis.top = axisTop;
2259   size.axis.line = axisLine;
2260   size.axis.height = axisHeight;
2261   size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine + characterMinorHeight;
2262   size.axis.labelMinorTop = options.axisOnTop ? 
2263     (options.showMajorLabels ? characterMajorHeight : 0) :
2264     axisLine;
2265   size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0;
2266   size.axis.lineMinorHeight = options.showMajorLabels ? 
2267     frameHeight - characterMajorHeight:
2268     frameHeight;
2269   size.axis.lineMinorWidth = dom.axis.minorLines.length ? 
2270     dom.axis.minorLines[0].offsetWidth : 1;
2271   size.axis.lineMajorWidth = dom.axis.majorLines.length ? 
2272     dom.axis.majorLines[0].offsetWidth : 1;
2273 
2274   size.items.top = itemsTop;
2275 
2276   resized = resized || (size.axis.characterMinorWidth  !== characterMinorWidth);
2277   resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight);
2278   resized = resized || (size.axis.characterMajorWidth  !== characterMajorWidth);
2279   resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight);
2280   size.axis.characterMinorWidth  = characterMinorWidth;
2281   size.axis.characterMinorHeight = characterMinorHeight;
2282   size.axis.characterMajorWidth  = characterMajorWidth;
2283   size.axis.characterMajorHeight = characterMajorHeight;
2284 
2285   // conversion factors can be changed when width of the Timeline is changed,
2286   // and when start or end are changed
2287   this.recalcConversion();
2288 
2289   return resized;
2290 }
2291 
2292 
2293 
2294 /**
2295  * Calculate the factor and offset to convert a position on screen to the 
2296  * corresponding date and vice versa. 
2297  * After the method calcConversionFactor is executed once, the methods screenToTime and 
2298  * timeToScreen can be used.
2299  */ 
2300 links.Timeline.prototype.recalcConversion = function() {
2301   this.conversion.offset = parseFloat(this.start.valueOf());
2302   this.conversion.factor = parseFloat(this.size.contentWidth) / 
2303     parseFloat(this.end.valueOf() - this.start.valueOf());  
2304 }
2305 
2306 
2307 /** 
2308  * Convert a position on screen (pixels) to a datetime 
2309  * Before this method can be used, the method calcConversionFactor must be 
2310  * executed once.
2311  * @param {int}     x    Position on the screen in pixels
2312  * @return {Date}   time The datetime the corresponds with given position x
2313  */ 
2314 links.Timeline.prototype.screenToTime = function(x) {
2315   var conversion = this.conversion,
2316     time = new Date(parseFloat(x) / conversion.factor + conversion.offset);
2317   return time;
2318 }
2319 
2320 /** 
2321  * Convert a datetime (Date object) into a position on the screen
2322  * Before this method can be used, the method calcConversionFactor must be 
2323  * executed once.
2324  * @param {Date}   time A date
2325  * @return {int}   x    The position on the screen in pixels which corresponds
2326  *                      with the given date.
2327  */ 
2328 links.Timeline.prototype.timeToScreen = function(time) {
2329   var conversion = this.conversion;
2330   var x = (time.valueOf() - conversion.offset) * conversion.factor;
2331   return x;
2332 }
2333 
2334 
2335 
2336 /**
2337  * Event handler for touchstart event on mobile devices 
2338  */ 
2339 links.Timeline.prototype.onTouchStart = function(event) {
2340   var params = this.eventParams,
2341     dom = this.dom,
2342     me = this;
2343   
2344   if (params.touchDown) {
2345     // if already moving, return
2346     return;
2347   }
2348   
2349   params.touchDown = true;
2350   params.zoomed = false;
2351 
2352   this.onMouseDown(event);
2353   
2354   if (!params.onTouchMove) {
2355     params.onTouchMove = function (event) {me.onTouchMove(event);};
2356     links.Timeline.addEventListener(document, "touchmove", params.onTouchMove);
2357   }
2358   if (!params.onTouchEnd) {
2359     params.onTouchEnd  = function (event) {me.onTouchEnd(event);};
2360     links.Timeline.addEventListener(document, "touchend",  params.onTouchEnd);
2361   }
2362 };
2363 
2364 /**
2365  * Event handler for touchmove event on mobile devices 
2366  */ 
2367 links.Timeline.prototype.onTouchMove = function(event) {
2368   var params = this.eventParams;
2369 
2370   if (event.scale && event.scale !== 1) {
2371     params.zoomed = true;
2372   }
2373   
2374   if (!params.zoomed) {
2375     // move 
2376     this.onMouseMove(event);
2377   }
2378   else {
2379     if (this.options.zoomable) {
2380       // pinch
2381       // TODO: pinch only supported on iPhone/iPad. Create something manually for Android?
2382       params.zoomed = true;
2383       
2384       var scale = event.scale,
2385         oldWidth = (params.end.valueOf() - params.start.valueOf()),
2386         newWidth = oldWidth / scale,
2387         diff = newWidth - oldWidth,
2388         start = new Date(parseInt(params.start.valueOf() - diff/2)),
2389         end = new Date(parseInt(params.end.valueOf() + diff/2));
2390       
2391       // TODO: determine zoom-around-date from touch positions?
2392       
2393       this.setVisibleChartRange(start, end);
2394       timeline.trigger("rangechange");
2395     }
2396   }
2397 };
2398 
2399 /**
2400  * Event handler for touchend event on mobile devices 
2401  */ 
2402 links.Timeline.prototype.onTouchEnd = function(event) {
2403   var params = this.eventParams;
2404   params.touchDown = false;
2405 
2406   /* TODO: cleanup
2407   document.getElementById("info").innerHTML = "touchEnd";
2408   */
2409 
2410   if (params.zoomed) {
2411     timeline.trigger("rangechanged");
2412   }
2413 
2414   if (params.onTouchMove) {
2415     links.Timeline.removeEventListener(document, "touchmove", params.onTouchMove);
2416     delete params.onTouchMove;
2417     
2418   }
2419   if (params.onTouchEnd) {
2420     links.Timeline.removeEventListener(document, "touchend",  params.onTouchEnd); 
2421     delete params.onTouchEnd;
2422   }
2423   
2424   this.onMouseUp(event);
2425 };
2426 
2427 
2428 /**
2429  * Start a moving operation inside the provided parent element
2430  * @param {event} event       The event that occurred (required for 
2431  *                             retrieving the  mouse position)
2432  */
2433 links.Timeline.prototype.onMouseDown = function(event) {
2434   event = event || window.event;
2435                                                                      
2436   var params = this.eventParams,
2437     options = this.options,
2438     dom = this.dom;
2439 
2440   // only react on left mouse button down
2441   var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
2442   if (!leftButtonDown && !params.touchDown) {
2443     return;
2444   }
2445 
2446   // check if frame is not resized (causing a mismatch with the end Date) 
2447   this.recalcSize();
2448   
2449   // get mouse position
2450   if (!params.touchDown) {
2451     params.mouseX = event.clientX;
2452     params.mouseY = event.clientY;
2453   }
2454   else {
2455     params.mouseX = event.targetTouches[0].clientX;
2456     params.mouseY = event.targetTouches[0].clientY;
2457   }
2458   if (params.mouseX === undefined) {params.mouseX = 0;}
2459   if (params.mouseY === undefined) {params.mouseY = 0;}
2460   params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content);
2461   params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content); 
2462 
2463   params.moved = false;
2464   params.start = new Date(this.start);
2465   params.end = new Date(this.end);
2466 
2467   params.target = links.Timeline.getTarget(event);
2468   params.itemDragLeft = (params.target === this.dom.items.dragLeft);
2469   params.itemDragRight = (params.target === this.dom.items.dragRight);
2470   
2471   if (params.itemDragLeft || params.itemDragRight) {
2472     params.itemIndex = this.selection ? this.selection.index : undefined;
2473   }
2474   else {
2475     params.itemIndex = this.getItemIndex(params.target);
2476   }
2477 
2478   params.customTime = (params.target === dom.customTime || 
2479     params.target.parentNode === dom.customTime) ? 
2480     this.customTime : 
2481     undefined;
2482 
2483   params.addItem = (options.editable && event.ctrlKey);
2484   if (params.addItem) {
2485     // create a new event at the current mouse position
2486     var x = params.mouseX - params.frameLeft;
2487     var y = params.mouseY - params.frameTop;
2488     
2489     var xstart = this.screenToTime(x);
2490     if (options.snapEvents) {
2491       this.step.snap(xstart);
2492     }
2493     var xend = new Date(xstart);
2494     var content = "New";
2495     var group = this.getGroupFromHeight(y);
2496     this.addItem({
2497       'start': xstart, 
2498       'end': xend, 
2499       'content': content, 
2500       'group': group
2501     });
2502     params.itemIndex = (this.items.length - 1);
2503     this.selectItem(params.itemIndex);
2504     params.itemDragRight = true;
2505   }
2506   
2507   params.editItem = options.editable ? this.isSelected(params.itemIndex) : undefined;
2508   if (params.editItem) {
2509     var item = this.items[params.itemIndex];
2510     params.itemStart = item.start;
2511     params.itemEnd = item.end;
2512     params.itemType = item.type;
2513     if (params.itemType == 'range') {
2514       params.itemLeft = this.timeToScreen(item.start);
2515       params.itemRight = this.timeToScreen(item.end);
2516     }
2517     else {
2518       params.itemLeft = this.timeToScreen(item.start);
2519     }
2520   }
2521   else {
2522     this.dom.frame.style.cursor = 'move';
2523   }
2524   if (!params.touchDown) {
2525     // add event listeners to handle moving the contents
2526     // we store the function onmousemove and onmouseup in the timeline, so we can
2527     // remove the eventlisteners lateron in the function mouseUp()
2528     var me = this;
2529     if (!params.onMouseMove) {
2530       params.onMouseMove = function (event) {me.onMouseMove(event);};
2531       links.Timeline.addEventListener(document, "mousemove", params.onMouseMove);
2532     }
2533     if (!params.onMouseUp) {
2534       params.onMouseUp = function (event) {me.onMouseUp(event);};
2535       links.Timeline.addEventListener(document, "mouseup", params.onMouseUp);
2536     }
2537   }
2538   
2539   links.Timeline.prevent   ult(event);
2540 }
2541 
2542 
2543 /**
2544  * Perform moving operating. 
2545  * This function activated from within the funcion links.Timeline.onMouseDown(). 
2546  * @param {event}   event  Well, eehh, the event
2547  */ 
2548 links.Timeline.prototype.onMouseMove = function (event) {
2549   event = event || window.event;
2550                                   
2551   var params = this.eventParams,
2552     size = this.size,
2553     dom = this.dom,
2554     options = this.options;
2555 
2556   // calculate change in mouse position
2557   if (!params.touchDown) {
2558     var mouseX = event.clientX;
2559     var mouseY = event.clientY;
2560   }
2561   else {
2562     var mouseX = event.targetTouches[0].clientX;
2563     var mouseY = event.targetTouches[0].clientY;
2564   }
2565   if (mouseX === undefined) {mouseX = 0;}
2566   if (mouseY === undefined) {mouseY = 0;}
2567   var diffX = parseFloat(mouseX) - params.mouseX;
2568   var diffY = parseFloat(mouseY) - params.mouseY;
2569 
2570   params.moved = true;
2571   
2572   if (params.customTime) {
2573     var x = this.timeToScreen(params.customTime);
2574     var xnew = x + diffX;
2575     this.customTime = this.screenToTime(xnew);
2576     this.redrawCustomTime();
2577 
2578     // fire a timechange event
2579     this.trigger('timechange');    
2580   }
2581   else if (params.editItem) {
2582     var item = this.items[params.itemIndex],
2583       domItem = item.dom,
2584       left,
2585       right;
2586 
2587     if (params.itemDragLeft) {
2588       // move the start of the item
2589       left = params.itemLeft + diffX;
2590       right = params.itemRight;
2591       
2592       item.start = this.screenToTime(left);
2593       if (options.snapEvents) { 
2594         this.step.snap(item.start);
2595         left = this.timeToScreen(item.start);
2596       }
2597             
2598       if (left > right) {
2599         left = right;
2600         item.start = this.screenToTime(left);
2601       }
2602     }
2603     else if (params.itemDragRight) {
2604       // move the end of the item
2605       left = params.itemLeft;
2606       right = params.itemRight + diffX;
2607 
2608       item.end = this.screenToTime(right);   
2609       if (options.snapEvents) { 
2610         this.step.snap(item.end);
2611         right = this.timeToScreen(item.end);
2612       }
2613 
2614       if (right < left) {
2615         right = left;
2616         item.end = this.screenToTime(right);   
2617       }
2618     }
2619     else {
2620       // move the item
2621       left = params.itemLeft + diffX;
2622       item.start = this.screenToTime(left);
2623       if (options.snapEvents) { 
2624         this.step.snap(item.start);
2625         left = this.timeToScreen(item.start);
2626       }
2627 
2628       if (item.end) {
2629         right = left + (params.itemRight - params.itemLeft);
2630         item.end = this.screenToTime(right);
2631       }
2632     }
2633 
2634     switch(item.type) {
2635       case 'range':
2636         domItem.style.left = left + "px"; 
2637         //domItem.style.width = Math.max(right - left - 2 * item.borderWidth, 1) + "px";  // TODO
2638         domItem.style.width = Math.max(right - left, 1) + "px"; 
2639         break;
2640       
2641       case 'box':
2642         domItem.style.left = (left - item.width / 2) + "px"; 
2643         domItem.line.style.left = (left - item.lineWidth / 2) + "px"; 
2644         domItem.dot.style.left = (left - item.dotWidth / 2) + "px"; 
2645         break;
2646         
2647       case 'dot':
2648         domItem.style.left = (left - item.dotWidth / 2) + "px"; 
2649         break;
2650     }
2651 
2652     if (this.groups.length == 0) {
2653       // TODO: does not work well in FF, forces redraw with every mouse move it seems
2654       this.stackEvents(options.animate);
2655       if (!options.animate) {
2656         this.redrawFrame();
2657       }
2658       // Note: when animate==true, no redraw is needed here, its done by stackEvents animation
2659     }
2660     this.redrawDeleteButton();
2661     this.redrawDragAreas();
2662   }
2663   else if (options.moveable) {
2664     var interval = (params.end.valueOf() - params.start.valueOf());
2665     var diffMillisecs = parseFloat(-diffX) / size.contentWidth * interval;
2666     this.start = new Date(params.start.valueOf() + Math.round(diffMillisecs));
2667     this.end = new Date(params.end.valueOf() + Math.round(diffMillisecs));
2668 
2669     this.recalcConversion();
2670 
2671     // move the items by changing the left position of their frame.
2672     // this is much faster than repositioning all elements individually via the 
2673     // redrawFrame() function (which is done once at mouseup)
2674     // note that we round diffX to prevent wrong positioning on millisecond scale
2675     var diffXRounded = -Math.round(diffMillisecs) / interval * size.contentWidth;
2676     dom.items.frame.style.left = diffXRounded + "px";
2677 
2678     this.redrawCurrentTime();
2679     this.redrawCustomTime();
2680     this.redrawAxis();
2681    
2682     // fire a rangechange event
2683     this.trigger('rangechange');
2684   }
2685 
2686   links.Timeline.prevent   ult(event);
2687 }
2688 
2689 
2690 /**
2691  * Stop moving operating.
2692  * This function activated from within the funcion links.Timeline.onMouseDown(). 
2693  * @param {event}  event   The event
2694  */ 
2695 links.Timeline.prototype.onMouseUp = function (event) {
2696   var params = this.eventParams,
2697     options = this.options;
2698   
2699   event = event || window.event;
2700 
2701   this.dom.frame.style.cursor = 'auto';
2702 
2703   // remove event listeners here, important for Safari
2704   if (params.onMouseMove) {
2705     links.Timeline.removeEventListener(document, "mousemove", params.onMouseMove);
2706     delete params.onMouseMove;
2707   }
2708   if (params.onMouseUp) {
2709     links.Timeline.removeEventListener(document, "mouseup",   params.onMouseUp);
2710     delete params.onMouseUp;
2711   } 
2712   links.Timeline.prevent   ult(event);
2713 
2714   if (params.customTime) {  
2715     // fire a timechanged event
2716     this.trigger('timechanged');    
2717   }
2718   else if (params.editItem) {
2719     var item = this.items[params.itemIndex];
2720     
2721     if (params.moved || params.addItem) {
2722       this.applyChange = true;
2723       this.applyAdd = true;
2724 
2725       this.updateData(params.itemIndex, {
2726         'start': item.start, 
2727         'end': item.end 
2728       });
2729 
2730       // fire an add or change event. 
2731       // Note that the change can be canceled from within an event listener if 
2732       // this listener calls the method cancelChange().
2733       this.trigger(params.addItem ? 'add' : 'change');    
2734 
2735       if (params.addItem) {
2736         if (this.applyAdd) {
2737           this.updateData(params.itemIndex, {
2738             'start': item.start, 
2739             'end': item.end, 
2740             'content': item.content, 
2741             'group': item.group ? item.group.content : undefined
2742           });
2743         }
2744         else {
2745           // undo an add
2746           this.deleteItem(params.itemIndex);
2747         }
2748       }
2749       else {
2750         if (this.applyChange) {
2751           this.updateData(params.itemIndex, {
2752             'start': item.start, 
2753             'end': item.end
2754           });            
2755         }
2756         else {
2757           // undo a change
2758           delete this.applyChange;
2759           delete this.applyAdd;
2760           
2761           var item = this.items[params.itemIndex],
2762             domItem = item.dom;
2763 
2764           item.start = params.itemStart;
2765           item.end = params.itemEnd;    
2766           domItem.style.left = params.itemLeft + "px"; 
2767           domItem.style.width = (params.itemRight - params.itemLeft) + "px"; 
2768         }
2769       }
2770 
2771       this.recalcSize();
2772       this.stackEvents(options.animate);
2773       if (!options.animate) {
2774         this.redrawFrame();
2775       }
2776       this.redrawDeleteButton();
2777       this.redrawDragAreas();      
2778     }
2779   }
2780   else {
2781     if (!params.moved && !params.zoomed) {
2782       // mouse did not move -> user has selected an item
2783       
2784       // delete item
2785       if (options.editable) {
2786         if (params.target === this.dom.items.deleteButton) {
2787           if (this.selection) {
2788             this.confirmDeleteItem(this.selection.index);
2789           }
2790         }
2791       }
2792 
2793       // select/unselect item
2794       if (options.selectable) {
2795         if (params.itemIndex !== undefined) {
2796           if (!this.isSelected(params.itemIndex)) {
2797             this.selectItem(params.itemIndex);
2798             this.trigger('select');        
2799           }
2800         }
2801         else {
2802           this.unselectItem();
2803         }
2804       }
2805       
2806       this.redrawFrame();
2807     }
2808     else {
2809       // timeline is moved      
2810       this.dom.items.frame.style.left = "0px";
2811       this.redrawFrame();
2812   
2813       if ((params.moved && options.moveable) || (params.zoomed && options.zoomable) ) {
2814         // fire a rangechanged event
2815         this.trigger('rangechanged');
2816       }      
2817     }
2818   }
2819 }
2820 
2821 /**
2822  * Double click event occurred for an item
2823  * @param {event}  event
2824  */ 
2825 links.Timeline.prototype.onDblClick = function (event) {
2826   var params = this.eventParams,
2827     options = this.options,
2828     dom = this.dom,
2829     size = this.size;
2830   event = event || window.event;
2831 
2832   if (!options.editable) {
2833     return;
2834   }
2835 
2836   if (params.itemIndex !== undefined) {
2837     // fire the edit event
2838     this.trigger('edit');
2839   }
2840   else {
2841     // create a new item    
2842     var x = event.clientX - links.Timeline.getAbsoluteLeft(dom.content);
2843     var y = event.clientY - links.Timeline.getAbsoluteTop(dom.content);
2844 
2845     // create a new event at the current mouse position
2846     var xstart = this.screenToTime(x);
2847     var xend = this.screenToTime(x  + size.frameWidth / 10); // add 10% of timeline width
2848     if (options.snapEvents) {
2849       this.step.snap(xstart);
2850       this.step.snap(xend);
2851     }
2852 
2853     var content = "New";
2854     var group = this.getGroupFromHeight(y);   // (group may be undefined)
2855     this.addItem({
2856       'start': xstart, 
2857       'end': xend, 
2858       'content': content, 
2859       'group': group
2860     });
2861     params.itemIndex = (this.items.length - 1);
2862     this.selectItem(params.itemIndex);
2863 
2864     this.applyAdd = true;
2865     
2866     // fire an add event. 
2867     // Note that the change can be canceled from within an event listener if 
2868     // this listener calls the method cancelAdd().
2869     this.trigger('add');    
2870 
2871     if (!this.applyAdd) {
2872       // undo an add
2873       this.deleteItem(params.itemIndex);
2874     }
2875     
2876     this.redrawDeleteButton();
2877     this.redrawDragAreas();
2878   }
2879 
2880   links.Timeline.prevent   ult(event);
2881 }
2882 
2883 
2884 /** 
2885  * Event handler for mouse wheel event, used to zoom the timeline
2886  * Code from http://adomas.org/javascript-mouse-wheel/
2887  * @param {event}  event   The event
2888  */
2889 links.Timeline.prototype.onMouseWheel = function(event) {
2890   if (!this.options.zoomable)
2891     return;
2892       
2893   if (!event) { /* For IE. */
2894     event = window.event;
2895   }
2896 
2897   // retrieve delta    
2898   var delta = 0;
2899   if (event.wheelDelta) { /* IE/Opera. */
2900     delta = event.wheelDelta/120;
2901   } else if (event.detail) { /* Mozilla case. */
2902     // In Mozilla, sign of delta is different than in IE.
2903     // Also, delta is multiple of 3.
2904     delta = -event.detail/3;
2905   }
2906 
2907   // If delta is nonzero, handle it.
2908   // Basically, delta is now positive if wheel was scrolled up,
2909   // and negative, if wheel was scrolled down.
2910   if (delta) {
2911     // TODO: on FireFox, the window is not redrawn within repeated scroll-events 
2912     // -> use a delayed redraw? Make a zoom queue?
2913     
2914     var timeline = this;
2915     var z = function () {
2916       // check if frame is not resized (causing a mismatch with the end date) 
2917       timeline.recalcSize();
2918 
2919       // perform the zoom action. Delta is normally 1 or -1
2920       var zoomFactor = delta / 5.0; 
2921       var frameLeft = links.Timeline.getAbsoluteLeft(timeline.dom.content);
2922       var zoomAroundDate = 
2923         (event.clientX != undefined && frameLeft != undefined) ? 
2924         timeline.screenToTime(event.clientX - frameLeft) : 
2925         undefined;
2926       
2927       timeline.zoom(zoomFactor, zoomAroundDate);
2928 
2929       // fire a rangechange and a rangechanged event
2930       timeline.trigger("rangechange");
2931       timeline.trigger("rangechanged");
2932       
2933       timeline.zooming = false;
2934     };
2935     
2936     z();
2937     /*
2938     if (!timeline.zooming) {
2939       timeline.zooming = true;
2940       setTimeout(z, 30);
2941     }
2942     */
2943   }
2944 
2945   // Prevent default actions caused by mouse wheel.
2946   // That might be ugly, but we handle scrolls somehow
2947   // anyway, so don't bother here...
2948   links.Timeline.prevent   ult(event);
2949 }
2950 
2951 
2952 /**
2953  * Zoom the timeline the given zoomfactor in or out. Start and end date will
2954  * be adjusted, and the timeline will be redrawn. You can optionally give a 
2955  * date around which to zoom.
2956  * For example, try zoomfactor = 0.1 or -0.1
2957  * @param {float}  zoomFactor      Zooming amount. Positive value will zoom in,
2958  *                                 negative value will zoom out
2959  * @param {Date}   zoomAroundDate  Date around which will be zoomed. Optional
2960  */ 
2961 links.Timeline.prototype.zoom = function(zoomFactor, zoomAroundDate) {
2962   // if zoomAroundDate is not provided, take it half between start Date and end Date
2963   if (zoomAroundDate == undefined) {
2964     zoomAroundDate = new Date((this.start.valueOf() + this.end.valueOf()) / 2);
2965   }
2966 
2967   // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
2968   // result in a start>=end )
2969   if (zoomFactor >= 1) {
2970     zoomFactor = 0.9;
2971   }
2972   if (zoomFactor <= -1) {
2973     zoomFactor = -0.9;
2974   }
2975 
2976   // adjust a negative factor such that zooming in with 0.1 equals zooming
2977   // out with a factor -0.1
2978   if (zoomFactor < 0) {
2979     zoomFactor = zoomFactor / (1 + zoomFactor);
2980   }
2981   
2982   // zoom start Date and end Date relative to the zoomAroundDate
2983   var startDiff = parseFloat(this.start.valueOf() - zoomAroundDate.valueOf());
2984   var endDiff = parseFloat(this.end.valueOf() - zoomAroundDate.valueOf());
2985 
2986   // calculate new dates
2987   var newStart = new Date(this.start.valueOf() - startDiff * zoomFactor);
2988   var newEnd   = new Date(this.end.valueOf() - endDiff * zoomFactor);
2989   
2990   // prevent scale of less than 10 milliseconds
2991   // TODO: IE has problems with milliseconds
2992   if (zoomFactor > 0 && (newEnd.valueOf() - newStart.valueOf()) < 10) {
2993     return;
2994   }
2995 
2996   // prevent scale of mroe than than 10 thousand years
2997   if (zoomFactor < 0 && (newEnd.getFullYear() - newStart.getFullYear()) > 10000) {
2998     return;
2999   }
3000   
3001   // apply new dates
3002   this.start = newStart;
3003   this.end = newEnd;
3004   
3005   this.recalcSize();
3006   var animate = this.options.animate ? this.options.animateZoom : false;
3007   this.stackEvents(animate);
3008   if (!animate || this.groups.length > 0) {
3009     this.redrawFrame();
3010   }
3011 }
3012 
3013 
3014 /**
3015  * Move the timeline the given movefactor to the left or right. Start and end 
3016  * date will be adjusted, and the timeline will be redrawn. 
3017  * For example, try moveFactor = 0.1 or -0.1
3018  * @param {float}  moveFactor      Moving amount. Positive value will move right,
3019  *                                 negative value will move left
3020  */ 
3021 links.Timeline.prototype.move = function(moveFactor) {
3022   // zoom start Date and end Date relative to the zoomAroundDate
3023   var diff = parseFloat(this.end.valueOf() - this.start.valueOf());
3024   
3025   // apply new dates
3026   this.start = new Date(this.start.valueOf() + diff * moveFactor);
3027   this.end   = new Date(this.end.valueOf() + diff * moveFactor);
3028 
3029   this.recalcConversion();
3030   this.redrawFrame();    
3031 }
3032 
3033 /**
3034  * Delete an item after a confirmation.
3035  * The deletion can be cancelled by executing .cancelDelete() during the 
3036  * triggered event 'delete'.
3037  * @param {int} index   Index of the item to be deleted
3038  */ 
3039 links.Timeline.prototype.confirmDeleteItem = function(index) {
3040   this.applyDelete = true;
3041 
3042   // select the event to be deleted
3043   if (!this.isSelected(index)) {
3044     this.selectItem(index);
3045   }
3046 
3047   // fire a delete event trigger. 
3048   // Note that the delete event can be canceled from within an event listener if 
3049   // this listener calls the method cancelChange().
3050   this.trigger('delete');    
3051   
3052   if (this.applyDelete) {
3053     this.deleteItem(index);
3054   }  
3055   
3056   delete this.applyDelete;
3057 }
3058 
3059 /**
3060  * Delete an item
3061  * @param {int} index   Index of the item to be deleted
3062  */ 
3063 links.Timeline.prototype.deleteItem = function(index) {
3064   if (index >= this.items.length) {
3065     throw "Cannot delete row, index out of range";
3066   }
3067   
3068   this.unselectItem();
3069 
3070   // actually delete the item
3071   this.items.splice(index, 1);
3072   
3073   // delete the row in the original data table
3074   if (this.data) {
3075     if (google && google.visualization && google.visualization.DataTable &&
3076         this.data instanceof google.visualization.DataTable) {
3077       this.data.removeRow(index);
3078     }
3079     else if (this.data instanceof Array) {
3080       this.data.splice(index, 1);
3081     }
3082     else {
3083       throw "Cannot delete row from data, unknown data type";
3084     }
3085   }
3086   
3087   this.size.dataChanged = true;
3088   this.redrawFrame();
3089   this.recalcSize();
3090   this.stackEvents(this.options.animate);
3091   if (!this.options.animate) {
3092     this.redrawFrame();
3093   }
3094   this.size.dataChanged = false;
3095 }
3096 
3097 
3098 /**
3099  * Delete all items
3100  */ 
3101 links.Timeline.prototype.deleteAllItems = function() {
3102   this.unselectItem();
3103 
3104   // delete the loaded data
3105   this.items = [];
3106   
3107   // delete the groups
3108   this.deleteGroups();
3109   
3110   // empty original data table
3111   if (this.data) {
3112     if (google && google.visualization && google.visualization.DataTable &&
3113         this.data instanceof google.visualization.DataTable) {
3114       this.data.removeRows(0, this.data.getNumberOfRows());
3115     }
3116     else if (this.data instanceof Array) {
3117       this.data.splice(0, this.data.length);
3118     }
3119     else {
3120       throw "Cannot delete row from data, unknown data type";
3121     }
3122   }
3123   
3124   this.size.dataChanged = true;
3125   this.redrawFrame();
3126   this.recalcSize();
3127   this.stackEvents(this.options.animate);
3128   if (!this.options.animate) {
3129     this.redrawFrame();
3130   }
3131   this.size.dataChanged = false;
3132 
3133 }
3134   
3135 
3136 /**
3137  * Find the group from a given height in the timeline
3138  * @param {Number} height   Height in the timeline
3139  * @return {String} group   The group name, or undefined if out of range
3140  */ 
3141 links.Timeline.prototype.getGroupFromHeight = function(height) {
3142   var groups = this.groups,
3143     options = this.options,
3144     size = this.size,
3145     y = height - (options.axisOnTop ? size.axis.height : 0);
3146 
3147   if (groups) {
3148     var group;
3149     for (var i = 0, iMax = groups.length; i < iMax; i++) {
3150       group = groups[i];
3151       if (y > group.top && y < group.top + group.height) {
3152         return group.content;
3153       }
3154     }
3155     
3156     return group ? group.content : undefined; // return the last group
3157   }
3158   
3159   return undefined;
3160 }
3161 
3162 /**
3163  * Retrieve the properties of an item.
3164  * @param {Number} index
3165  * @return {Object} properties   Object containing item properties:<br>
3166  *                              {Date} start (required),
3167  *                              {Date} end (optional),
3168  *                              {String} content (required),
3169  *                              {String} group (optional)
3170  */ 
3171 links.Timeline.prototype.getItem = function (index) {
3172   if (index >= this.items.length) {
3173     throw "Cannot get item, index out of range";
3174   }
3175 
3176   var item = this.items[index];
3177 
3178   var properties = {};
3179   properties.start = new Date(item.start);
3180   if (item.end) {
3181     properties.end = new Date(item.end);
3182   }
3183   properties.content = item.content;
3184   if (item.group) {
3185     properties.group = item.group.content;
3186   }  
3187 
3188   return properties;
3189 }
3190 
3191 /**
3192  * Add a new item.
3193  * @param {Object} itemData     Object containing item properties:<br>
3194  *                              {Date} start (required),
3195  *                              {Date} end (optional),
3196  *                              {String} content (required),
3197  *                              {String} group (optional)
3198  */ 
3199 links.Timeline.prototype.addItem = function (itemData) {
3200   var items = [
3201     itemData
3202   ];
3203 
3204   this.addItems(items);
3205 }
3206 
3207 /**
3208  * Add new items.
3209  * @param {Array} items  An array containing Objects. 
3210  *                       The objects must have the following parameters: 
3211  *                         {Date} start,
3212  *                         {Date} end,
3213  *                         {String} content with text or HTML code,
3214  *                         {String} group
3215  */ 
3216 links.Timeline.prototype.addItems = function (items) {
3217   var newItems = items,
3218     curItems = this.items,
3219     groups = this.groups,
3220     groupIndexes = this.groupIndexes;
3221   
3222   // append the items
3223   for (var i = 0, iMax = newItems.length; i < iMax; i++) {
3224     var itemData = items[i];
3225 
3226     this.addGroup(itemData.group);
3227     
3228     curItems.push(this.createItem(itemData));     
3229 
3230     var index = curItems.length - 1;
3231     this.updateData(index, itemData);
3232   }
3233   
3234   // redraw timeline
3235   this.size.dataChanged = true;
3236   this.redrawFrame();
3237   this.recalcSize();
3238   this.stackEvents(false);
3239   this.redrawFrame();
3240   this.size.dataChanged = false;
3241 }
3242 
3243 /**
3244  * Create an item object, containing all needed parameters
3245  * @param {Object} itemData  Object containing parameters start, end
3246  *                           content, group.
3247  * @return {Object} item
3248  */ 
3249 links.Timeline.prototype.createItem = function(itemData) {
3250   var item = {
3251     'start': itemData.start,
3252     'end': itemData.end,
3253     'content': itemData.content,
3254     'type': itemData.end ? 'range' : this.options.style,
3255     'group': this.findGroup(itemData.group),
3256     'top': 0,
3257     'left': 0,
3258     'width': 0,
3259     'height': 0,
3260     'lineWidth' : 0,
3261     'dotWidth': 0,
3262     'dotHeight': 0      
3263   };
3264   return item;
3265 }
3266 
3267 /**
3268  * Edit an item
3269  * @param {Number} index
3270  * @param {Object} itemData     Object containing item properties:<br>
3271  *                              {Date} start (required),
3272  *                              {Date} end (optional),
3273  *                              {String} content (required),
3274  *                              {String} group (optional)
3275  */ 
3276 links.Timeline.prototype.changeItem = function (index, itemData) {
3277   if (index >= this.items.length) {
3278     throw "Cannot change item, index out of range";
3279   }
3280   
3281   var style = this.options.style;
3282   var item = this.items[index];
3283 
3284   // edit the item
3285   if (itemData.start) {
3286     item.start = itemData.start;
3287   }
3288   if (itemData.end) {
3289     item.end = itemData.end;
3290   }
3291   if (itemData.content) {
3292     item.content = itemData.content;
3293   }
3294   if (itemData.group) {
3295     item.group = this.addGroup(itemData.group);
3296   }
3297   
3298   // update the original data table
3299   this.updateData(index, itemData);
3300   
3301   // redraw timeline
3302   this.size.dataChanged = true;
3303   this.redrawFrame();
3304   this.recalcSize();
3305   this.stackEvents(false);
3306   this.redrawFrame();
3307   this.size.dataChanged = false;
3308 }
3309 
3310 
3311 /**
3312  * Find a group by its name.
3313  * @param {String} group
3314  * @return {Object} a group object or undefined when group is not found
3315  */ 
3316 links.Timeline.prototype.findGroup = function (group) {
3317   var index = this.groupIndexes[group];
3318   return (index != undefined) ? this.groups[index] : undefined;
3319 }
3320 
3321 /**
3322  * Delete all groups
3323  */ 
3324 links.Timeline.prototype.deleteGroups = function () {
3325   this.groups = [];
3326   this.groupIndexes = {};
3327 }
3328 
3329 
3330 /**
3331  * Add a group. When the group already exists, no new group is created
3332  * but the existing group is returned.
3333  * @param {String} groupName   the name of the group
3334  * @return {Object} groupObject   
3335  */ 
3336 links.Timeline.prototype.addGroup = function (groupName) {
3337   var groups = this.groups,
3338     groupIndexes = this.groupIndexes;
3339 
3340   var groupObj = groupIndexes[groupName];
3341   if (groupObj === undefined && groupName !== undefined) {
3342     var groupObj = {
3343       'content': groupName,
3344       'labelTop': 0,
3345       'lineTop': 0
3346       // note: this object will lateron get addition information, 
3347       //       such as height and width of the group         
3348     };
3349     groups.push(groupObj);
3350     
3351     // sort the groups
3352     if (this.options.axisOnTop) {
3353       groups.sort(function (a, b) {
3354         return a.content > b.content;
3355       });
3356     }
3357     else {
3358       groups.sort(function (a, b) {
3359         return a.content < b.content;
3360       });
3361     }    
3362 
3363     // rebuilt the groupIndexes
3364     for (var i = 0, iMax = groups.length; i < iMax; i++) {
3365       groupIndexes[groups[i].content] = i;
3366     }
3367   }
3368 
3369   return groupObj;
3370 }
3371 
3372 /**
3373  * Cancel a change item 
3374  * This method can be called insed an event listener which catches the "change" 
3375  * event. The changed event position will be undone. 
3376  */ 
3377 links.Timeline.prototype.cancelChange = function () {
3378   this.applyChange = false;
3379 }
3380 
3381 /**
3382  * Cancel deletion of an item
3383  * This method can be called insed an event listener which catches the "delete" 
3384  * event. Deletion of the event will be undone. 
3385  */ 
3386 links.Timeline.prototype.cancelDelete = function () {
3387   this.applyDelete = false;
3388 }
3389 
3390 
3391 /**
3392  * Cancel creation of a new item
3393  * This method can be called insed an event listener which catches the "new" 
3394  * event. Creation of the new the event will be undone. 
3395  */ 
3396 links.Timeline.prototype.cancelAdd = function () {
3397   this.applyAdd = false;  
3398 }
3399 
3400 
3401 /**
3402  * Select an event. The visible chart range will be moved such that the selected
3403  * event is placed in the middle.
3404  * For example selection = [{row: 5}];
3405  * @param {array} sel   An array with a column row, containing the row number 
3406  *                      (the id) of the event to be selected. 
3407  * @return {boolean}    true if selection is succesfully set, else false.
3408  */ 
3409 links.Timeline.prototype.setSelection = function(selection) {
3410   if (selection != undefined && selection.length > 0) {
3411     if (selection[0].row != undefined) {
3412       var index = selection[0].row;
3413       if (this.items[index]) {
3414         var item = this.items[index];
3415         this.selectItem(index);
3416 
3417         // move the visible chart range to the selected event.
3418         var start = item.start;
3419         var end = item.end;
3420         if (end != undefined) {
3421           var middle = new Date((end.valueOf() + start.valueOf()) / 2);
3422         } else {
3423           var middle = new Date(start);
3424         }
3425         var diff = (this.end.valueOf() - this.start.valueOf()),
3426           newStart = new Date(middle.valueOf() - diff/2),
3427           newEnd = new Date(middle.valueOf() + diff/2);
3428         
3429         this.setVisibleChartRange(newStart, newEnd);
3430         
3431         return true;
3432       }
3433     }
3434   }
3435   return false;
3436 }
3437 
3438 /**
3439  * Retrieve the currently selected event
3440  * @return {array} sel  An array with a column row, containing the row number 
3441  *                      of the selected event. If there is no selection, an 
3442  *                      empty array is returned.
3443  */ 
3444 links.Timeline.prototype.getSelection = function() {
3445   var sel = [];
3446   if (this.selection) {
3447     sel.push({"row": this.selection.index});
3448   }
3449   return sel;
3450 }
3451 
3452 
3453 /**
3454  * Select an item by its index
3455  * @param {Number} index
3456  */ 
3457 links.Timeline.prototype.selectItem = function(index) {
3458   this.unselectItem();
3459 
3460   this.selection = undefined;
3461 
3462   if (this.items[index] !== undefined) {
3463     var item = this.items[index],
3464       domItem = item.dom;
3465     
3466     this.selection = {
3467       'index': index,
3468       'item': domItem
3469     };
3470 
3471     if (this.options.editable) {
3472       domItem.style.cursor = 'move';
3473     }
3474     switch (item.type) {
3475       case 'range':
3476         domItem.className = "timeline-event timeline-event-selected timeline-event-range";
3477         break;
3478       case 'box':
3479         domItem.className = "timeline-event timeline-event-selected timeline-event-box";
3480         domItem.line.className = "timeline-event timeline-event-selected timeline-event-line";
3481         domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot";
3482         break;
3483       case 'dot':
3484         domItem.className = "timeline-event timeline-event-selected";
3485         domItem.dot.className = "timeline-event timeline-event-selected timeline-event-dot";
3486         break;
3487     }
3488   }
3489 }
3490 
3491 /**
3492  * Check if an item is currently selected
3493  * @param {Number} index
3494  * @return {boolean} true if row is selected, else false
3495  */ 
3496 links.Timeline.prototype.isSelected = function (index) {
3497   return (this.selection && this.selection.index === index);
3498 }
3499 
3500 /**
3501  * Unselect the currently selected event (if any)
3502  */ 
3503 links.Timeline.prototype.unselectItem = function() {
3504   if (this.selection) {
3505     var item = this.items[this.selection.index];
3506     
3507     if (item && item.dom) {
3508       var domItem = item.dom;
3509       domItem.style.cursor = '';
3510       switch (item.type) {
3511         case 'range':
3512           domItem.className = "timeline-event timeline-event-range";
3513           break;
3514         case 'box':
3515           domItem.className = "timeline-event timeline-event-box";
3516           domItem.line.className = "timeline-event timeline-event-line";
3517           domItem.dot.className = "timeline-event timeline-event-dot";
3518           break;
3519         case 'dot':
3520           domItem.className = "";
3521           domItem.dot.className = "timeline-event timeline-event-dot";
3522           break;
3523       }
3524     }
3525   }
3526 
3527   this.selection = undefined;
3528 }
3529 
3530 
3531 /**
3532  * Stack the items such that they don't overlap. The items will have a minimal
3533  * distance equal to options.eventMargin.
3534  * @param {boolean} animate     if animate is true, the items are moved to 
3535  *                              their new position animated
3536  */ 
3537 links.Timeline.prototype.stackEvents = function(animate) {
3538   if (this.options.stackEvents == false || this.groups.length > 0) {
3539     // under this conditions we refuse to stack the events
3540     return;
3541   }
3542 
3543   if (animate == undefined) {
3544     animate = false;
3545   }
3546   
3547   var sortedItems = this.stackOrder(this.items);
3548   var finalItems = this.stackCalculateFinal(sortedItems, animate);
3549   
3550   if (animate) {
3551     // move animated to the final positions
3552     var animation = this.animation;
3553     if (!animation) {
3554       animation = {};
3555       this.animation = animation;
3556     }
3557     animation.finalItems = finalItems;
3558 
3559     var timeline = this;
3560     var step = function () {
3561       var arrived = timeline.stackMoveOneStep(sortedItems, animation.finalItems);
3562       timeline.recalcSize();
3563       timeline.redrawFrame();
3564 
3565       if (!arrived) {
3566         animation.timer = setTimeout(step, 30);
3567       }
3568       else {
3569         delete animation.finalItems;
3570         delete animation.timer;
3571       }
3572     }
3573     
3574     if (!animation.timer) {
3575       animation.timer = setTimeout(step, 30);
3576     }  
3577   }
3578   else {
3579     this.stackMoveToFinal(sortedItems, finalItems);
3580     this.recalcSize();
3581     //this.redraw(); // TODO: cleanup
3582   }
3583 }
3584 
3585 
3586 /**
3587  * Order the items in the array this.items. The order is determined via:
3588  * - Ranges go before boxes and dots.
3589  * - The item with the left most location goes first
3590  * @param {Array} items        Array with items
3591  * @return {Array} sortedItems Array with sorted items
3592  */ 
3593 links.Timeline.prototype.stackOrder = function(items) {
3594   // TODO: store the sorted items, to have less work later on
3595   var sortedItems = items.concat([]);
3596   
3597   var f = function (a, b) {
3598     if (a.type == 'range' && b.type != 'range') {
3599       return -1;
3600     }
3601 
3602     if (a.type != 'range' && b.type == 'range') {
3603       return 1;
3604     }
3605 
3606     return (a.left - b.left);
3607   };
3608   
3609   sortedItems.sort(f);
3610 
3611   return sortedItems;
3612 }
3613 
3614 /**
3615  * Adjust vertical positions of the events such that they don't overlap each
3616  * other.
3617  */
3618 links.Timeline.prototype.stackCalculateFinal = function(items) {
3619   var size = this.size,
3620     axisTop = size.axis.top,
3621     options = this.options,
3622     axisOnTop = options.axisOnTop,
3623     eventMargin = options.eventMargin,
3624     eventMarginAxis = options.eventMarginAxis,
3625     finalItems = [];
3626 
3627   // initialize final positions
3628   for (var i = 0, iMax = items.length; i < iMax; i++) {
3629     var item = items[i],
3630       top,
3631       left,
3632       right,
3633       bottom,
3634       height = item.height,
3635       width = item.width;
3636     
3637     if (axisOnTop) {
3638       top = axisTop + eventMarginAxis + eventMargin / 2;    
3639     }
3640     else {
3641       top = axisTop - height - eventMarginAxis - eventMargin / 2;    
3642     }
3643     bottom = top + height;
3644     
3645     switch (item.type) {
3646       case 'range':
3647       case 'dot':
3648         left = this.timeToScreen(item.start);
3649         right = item.end ? this.timeToScreen(item.end) : left + width;
3650         break;
3651         
3652       case 'box':
3653         left = this.timeToScreen(item.start) - width / 2;
3654         right = left + width;
3655         break;
3656     }
3657     
3658     finalItems[i] = {
3659       'left': left,
3660       'top': top,
3661       'right': right,
3662       'bottom': bottom,
3663       'height': height,
3664       'item': item
3665     };    
3666   }
3667 
3668   // calculate new, non-overlapping positions
3669   //var items = sortedItems;
3670   for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
3671     var finalItem = finalItems[i];
3672     var collidingElement = null;
3673     do {
3674       // TODO: optimize checking for overlap. when there is a gap without items, 
3675       //  you only need to check for items from the next item on, not from zero
3676       collidingItem = this.stackEventsCheckOverlap(finalItems, i, 0, i-1);
3677       if (collidingItem != null) {
3678         // There is a collision. Reposition the event above the colliding element
3679         if (axisOnTop) {
3680           finalItem.top = collidingItem.top + collidingItem.height + eventMargin;
3681         }
3682         else {
3683           finalItem.top = collidingItem.top - finalItem.height - eventMargin;
3684         }
3685         finalItem.bottom = finalItem.top + finalItem.height;
3686       }
3687     } while (collidingItem);
3688   }
3689   
3690   return finalItems;
3691 }
3692 
3693 
3694 /**
3695  * Move the events one step in the direction of their final positions
3696  * @param {Array} currentItems   Array with the real items and their current
3697  *                               positions
3698  * @param {Array} finalItems     Array with objects containing the final 
3699  *                               positions of the items
3700  * @return {boolean} arrived     True if all items have reached their final
3701  *                               location, else false
3702  */ 
3703 links.Timeline.prototype.stackMoveOneStep = function(currentItems, finalItems) {
3704   // TODO: check this method
3705   var arrived = true;
3706   
3707   // apply new positions animated
3708   for (i = 0, iMax = currentItems.length; i < iMax; i++) {
3709     var finalItem = finalItems[i],
3710       item = finalItem.item;
3711 
3712     var topNow = parseInt(item.top);
3713     var topFinal = parseInt(finalItem.top);
3714     var diff = (topFinal - topNow);
3715     if (diff) {
3716       var step = (topFinal == topNow) ? 0 : ((topFinal > topNow) ? 1 : -1);
3717       if (Math.abs(diff) > 4) step = diff / 4;
3718       var topNew = parseInt(topNow + step);
3719 
3720       if (topNew != topFinal) {
3721         arrived = false;
3722       }
3723 
3724       item.top = topNew;
3725       item.bottom = item.top + item.height;
3726     }
3727     else {
3728       item.top = finalItem.top;
3729       item.bottom = finalItem.bottom;
3730     }
3731     
3732     item.left = finalItem.left;
3733     item.right = finalItem.right;
3734   }
3735   
3736   return arrived;
3737 }
3738 
3739 
3740 
3741 /**
3742  * Move the events from their current position to the final position
3743  * @param {Array} currentItems   Array with the real items and their current
3744  *                               positions
3745  * @param {Array} finalItems     Array with objects containing the final 
3746  *                               positions of the items
3747  */ 
3748 links.Timeline.prototype.stackMoveToFinal = function(currentItems, finalItems) {
3749   // Put the events directly at there final position
3750   for (i = 0, iMax = currentItems.length; i < iMax; i++) {
3751     var current = currentItems[i],
3752       finalItem = finalItems[i];
3753     
3754     current.left = finalItem.left;
3755     current.top = finalItem.top;
3756     current.right = finalItem.right;
3757     current.bottom = finalItem.bottom;
3758   }
3759 }
3760 
3761 
3762 
3763 /**
3764  * Check if the destiny position of given item overlaps with any 
3765  * of the other items from index itemStart to itemEnd. 
3766  * @param {Array} items      Array with items
3767  * @param {int}  itemIndex   Number of the item to be checked for overlap
3768  * @param {int}  itemStart   First item to be checked. 
3769  * @param {int}  itemEnd     Last item to be checked. 
3770  * @return {Object}          colliding item, or undefined when no collisions
3771  */ 
3772 links.Timeline.prototype.stackEventsCheckOverlap = function(items, itemIndex, 
3773     itemStart, itemEnd) {
3774     eventMargin = this.options.eventMargin,
3775     collision = this.collision;
3776   
3777   var item1 = items[itemIndex];
3778   for (var i = itemStart; i <= itemEnd; i++) {
3779     var item2 = items[i];
3780     if (collision(item1, item2, eventMargin)) {
3781       if (i != itemIndex) {
3782         return item2;
3783       }
3784     }
3785   }
3786 }
3787 
3788 /**
3789  * Test if the two provided items collide
3790  * The items must have parameters left, right, top, and bottom.
3791  * @param {htmlelement} item1   The first item
3792  * @param {htmlelement} item2    The second item
3793  * @param {int}         margin  A minimum required margin. Optional. 
3794  *                              If margin is provided, the two items will be 
3795  *                              marked colliding when they overlap or
3796  *                              when the margin between the two is smaller than
3797  *                              the requested margin.
3798  * @return {boolean}            true if item1 and item2 collide, else false 
3799  */
3800 links.Timeline.prototype.collision = function(item1, item2, margin) {
3801   // set margin if not specified 
3802   if (margin == undefined) {
3803     margin = 0;
3804   }
3805 
3806   // calculate if there is overlap (collision)
3807   return (item1.left - margin < item2.right && 
3808           item1.right + margin > item2.left &&
3809           item1.top - margin < item2.bottom &&
3810           item1.bottom + margin > item2.top);
3811 }
3812 
3813 
3814 /**
3815  * fire an event
3816  * @param {String} event   The name of an event, for example "rangechange" or "edit"
3817  */
3818 links.Timeline.prototype.trigger = function (event) {
3819   // built up properties
3820   var properties = null;
3821   switch (event) {
3822     case 'rangechange':
3823     case 'rangechanged':
3824       properties = {
3825         'start': new Date(this.start), 
3826         'end': new Date(this.end)
3827       };
3828       break;
3829     
3830     case 'timechange':
3831     case 'timechanged':
3832       properties = {
3833         'time': new Date(this.customTime)
3834       };
3835       break;
3836   }
3837   
3838   // trigger the links event bus
3839   links.events.trigger(this, event, properties); 
3840   
3841   // trigger the google event bus
3842   if (google && google.visualization && google.visualization.events) {
3843     google.visualization.events.trigger(this, event, properties);    
3844   }
3845 }
3846 
3847 
3848 
3849 /** ------------------------------------------------------------------------ **/
3850 
3851 
3852 /** 
3853  * Event listener (singleton)
3854  */ 
3855 links.events = {
3856   'listeners': {},
3857 
3858   /**
3859    * Add an event listener
3860    * @param {Object} object
3861    * @param {String} event       The name of an event, for example 'select'
3862    * @param {function} callback  The callback method, called when the 
3863    *                             event takes place
3864    */ 
3865   addListener: function (object, event, callback) {
3866     var objListeners = this.listeners[object];
3867     if (!objListeners) {
3868       objListeners = {};
3869       this.listeners[object] = objListeners;
3870     }
3871     
3872     var callbacks = objListeners[event];
3873     if (!callbacks) {
3874       callbacks = [];
3875       objListeners[event] = callbacks;
3876     }
3877     
3878     // add the callback if it does not yet exist
3879     if (callbacks.indexOf(callback) == -1) {
3880       callbacks.push(callback);
3881     }
3882   },
3883   
3884   /**
3885    * Remove an event listener
3886    * @param {Object} object      
3887    * @param {String} event       The name of an event, for example 'select'
3888    * @param {function} callback  The registered callback method
3889    */ 
3890   removeListener: function (object, event, callback) {
3891     var objListeners = this.listeners[object];    
3892     if (objListeners) {
3893       var callbacks = objListeners[event];      
3894       if (callbacks) {
3895         var index = callbacks.indexOf(callback);
3896         if (index != -1) {
3897           callbacks.splice(index, 1);
3898         }
3899         
3900         // remove the array when empty
3901         if (callbacks.length == 0) {
3902           delete objListeners[event];
3903         }
3904       }
3905 
3906       // count the number of registered events. remove object when empty
3907       var count = 0;
3908       for (var event in objListeners) {
3909         if (objListeners.hasOwnProperty(event)) {
3910           count++;
3911         }
3912       }
3913       if (count == 0) {
3914         delete this.listeners[object];
3915       }
3916     }
3917   },
3918 
3919   /**
3920    * Remove all registered event listeners
3921    */ 
3922   removeAllListeners: function () {
3923     this.listeners = {};
3924   },
3925   
3926   /**
3927    * Trigger an event. All registered event handlers will be called
3928    * @param {Object} object
3929    * @param {String} event
3930    * @param {Object} properties (optional)
3931    */ 
3932   trigger: function (object, event, properties) {
3933     var objListeners = this.listeners[object];    
3934     if (objListeners) {
3935       var callbacks = objListeners[event];      
3936       if (callbacks) {
3937         for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
3938           callbacks[i](properties);
3939         }
3940       }
3941     }
3942   }
3943 };
3944 
3945 
3946 
3947 
3948 /** ------------------------------------------------------------------------ **/
3949 
3950 /** 
3951  * @class StepDate
3952  * The class StepDate is an iterator for dates. You provide a start date and an 
3953  * end date. The class itself determines the best scale (step size) based on the  
3954  * provided start Date, end Date, and minimumStep.
3955  * 
3956  * If minimumStep is provided, the step size is chosen as close as possible
3957  * to the minimumStep but larger than minimumStep. If minimumStep is not
3958  * provided, the scale is set to 1 DAY.
3959  * The minimumStep should correspond with the onscreen size of about 6 characters
3960  * 
3961  * Alternatively, you can set a scale by hand.
3962  * After creation, you can initialize the class by executing start(). Then you
3963  * can iterate from the start date to the end date via next(). You can check if
3964  * the end date is reached with the function end(). After each step, you can 
3965  * retrieve the current date via get().
3966  * The class step has scales ranging from milliseconds, seconds, minutes, hours, 
3967  * days, to years.
3968  * 
3969  * Version: 0.9
3970  * 
3971  * @param {Date} start        The start date, for example new Date(2010, 9, 21)
3972  *                            or new Date(2010, 9,21,23,45,00)
3973  * @param {Date} end          The end date
3974  * @param {int}  minimumStep  Optional. Minimum step size in milliseconds
3975  */
3976 links.Timeline.StepDate = function(start, end, minimumStep) {
3977 
3978   // variables
3979   this.current = new Date();
3980   this._start = new Date();
3981   this._end = new Date();
3982   
3983   this.autoScale  = true;
3984   this.scale = links.Timeline.StepDate.SCALE.DAY;
3985   this.step = 1;
3986 
3987   // initialize the range
3988   this.setRange(start, end, minimumStep);
3989 }
3990 
3991 /// enum scale
3992 links.Timeline.StepDate.SCALE = { MILLISECOND : 1, 
3993                          SECOND : 2, 
3994                          MINUTE : 3, 
3995                          HOUR : 4, 
3996                          DAY : 5, 
3997                          MONTH : 6, 
3998                          YEAR : 7};
3999 
4000 
4001 /**
4002  * Set a new range
4003  * If minimumStep is provided, the step size is chosen as close as possible
4004  * to the minimumStep but larger than minimumStep. If minimumStep is not
4005  * provided, the scale is set to 1 DAY.
4006  * The minimumStep should correspond with the onscreen size of about 6 characters
4007  * @param {Date} start        The start date and time.
4008  * @param {Date} end          The end date and time.
4009  * @param {int}  minimumStep  Optional. Minimum step size in milliseconds
4010  */ 
4011 links.Timeline.StepDate.prototype.setRange = function(start, end, minimumStep) {
4012   if (isNaN(start) || isNaN(end)) {
4013     //throw  "No legal start or end date in method setRange";
4014     return;
4015   }
4016 
4017   this._start      = (start != undefined)  ? new Date(start) : new Date();
4018   this._end        = (end != undefined)    ? new Date(end) : new Date();
4019 
4020   if (this.autoScale) {
4021     this.setMinimumStep(minimumStep);
4022   }
4023 }
4024 
4025 /**
4026  * Set the step iterator to the start date.
4027  */ 
4028 links.Timeline.StepDate.prototype.start = function() {
4029   this.current = new Date(this._start);
4030   this.roundToMinor();
4031 }
4032 
4033 /**
4034  * Round the current date to the first minor date value
4035  * This must be executed once when the current date is set to start Date
4036  */ 
4037 links.Timeline.StepDate.prototype.roundToMinor = function() {
4038   // round to floor
4039   // IMPORTANT: we have no breaks in this switch! (this is no bug)
4040   switch (this.scale) {
4041     case links.Timeline.StepDate.SCALE.YEAR:
4042       this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
4043       this.current.setMonth(0);
4044     case links.Timeline.StepDate.SCALE.MONTH:        this.current.setDate(1);
4045     case links.Timeline.StepDate.SCALE.DAY:          this.current.setHours(0);
4046     case links.Timeline.StepDate.SCALE.HOUR:         this.current.setMinutes(0);
4047     case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setSeconds(0);
4048     case links.Timeline.StepDate.SCALE.SECOND:       this.current.setMilliseconds(0);
4049     //case links.Timeline.StepDate.SCALE.MILLISECOND: // nothing to do for milliseconds
4050   }
4051 
4052   if (this.step != 1) {
4053     // round down to the first minor value that is a multiple of the current step size
4054     switch (this.scale) {
4055       case links.Timeline.StepDate.SCALE.MILLISECOND:  this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);  break;
4056       case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step);  break;
4057       case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step);  break;
4058       case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() - this.current.getHours() % this.step);  break;
4059       case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1);  break;
4060       case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);  break;
4061       case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
4062       default:                      break;
4063     }
4064   }
4065 }
4066 
4067 /**
4068  * Check if the end date is reached
4069  * @return {boolean}  true if the current date has passed the end date
4070  */ 
4071 links.Timeline.StepDate.prototype.end = function () {
4072   return (this.current.getTime() > this._end.getTime());
4073 }
4074 
4075 /** 
4076  * Do the next step
4077  */ 
4078 links.Timeline.StepDate.prototype.next = function() {
4079   var prev = this.current.getTime();
4080   
4081   // Two cases, needed to prevent issues with switching daylight savings 
4082   // (end of March and end of October)
4083   if (this.current.getMonth() < 6)   {
4084     switch (this.scale)
4085     {
4086       case links.Timeline.StepDate.SCALE.MILLISECOND:  
4087 
4088       this.current = new Date(this.current.getTime() + this.step); break;
4089       case links.Timeline.StepDate.SCALE.SECOND:       this.current = new Date(this.current.getTime() + this.step * 1000); break;
4090       case links.Timeline.StepDate.SCALE.MINUTE:       this.current = new Date(this.current.getTime() + this.step * 1000 * 60); break;
4091       case links.Timeline.StepDate.SCALE.HOUR:         
4092         this.current = new Date(this.current.getTime() + this.step * 1000 * 60 * 60); 
4093         // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
4094         var h = this.current.getHours();
4095         this.current.setHours(h - (h % this.step));
4096         break;
4097       case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
4098       case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
4099       case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
4100       default:                      break;
4101     }
4102   }
4103   else {
4104     switch (this.scale)
4105     {
4106       case links.Timeline.StepDate.SCALE.MILLISECOND:  
4107 
4108       this.current = new Date(this.current.getTime() + this.step); break;
4109       case links.Timeline.StepDate.SCALE.SECOND:       this.current.setSeconds(this.current.getSeconds() + this.step); break;
4110       case links.Timeline.StepDate.SCALE.MINUTE:       this.current.setMinutes(this.current.getMinutes() + this.step); break;
4111       case links.Timeline.StepDate.SCALE.HOUR:         this.current.setHours(this.current.getHours() + this.step); break;
4112       case links.Timeline.StepDate.SCALE.DAY:          this.current.setDate(this.current.getDate() + this.step); break;
4113       case links.Timeline.StepDate.SCALE.MONTH:        this.current.setMonth(this.current.getMonth() + this.step); break;
4114       case links.Timeline.StepDate.SCALE.YEAR:         this.current.setFullYear(this.current.getFullYear() + this.step); break;
4115       default:                      break;
4116     }
4117   }
4118 
4119   if (this.step != 1) {
4120     // round down to the correct major value
4121     switch (this.scale) {
4122       case links.Timeline.StepDate.SCALE.MILLISECOND:  if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);  break;
4123       case links.Timeline.StepDate.SCALE.SECOND:       if(this.current.getSeconds() < this.step) this.current.setSeconds(0);  break;
4124       case links.Timeline.StepDate.SCALE.MINUTE:       if(this.current.getMinutes() < this.step) this.current.setMinutes(0);  break;
4125       case links.Timeline.StepDate.SCALE.HOUR:         if(this.current.getHours() < this.step) this.current.setHours(0);  break;
4126       case links.Timeline.StepDate.SCALE.DAY:          if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
4127       case links.Timeline.StepDate.SCALE.MONTH:        if(this.current.getMonth() < this.step) this.current.setMonth(0);  break;
4128       case links.Timeline.StepDate.SCALE.YEAR:         break; // nothing to do for year
4129       default:                break;
4130     }
4131   }
4132 
4133   // safety mechanism: if current time is still unchanged, move to the end
4134   if (this.current.getTime() == prev) {
4135     this.current = new Date(this._end);
4136   }
4137 }
4138 
4139 
4140 /**
4141  * Get the current datetime 
4142  * @return {Date}  current The current date
4143  */ 
4144 links.Timeline.StepDate.prototype.getCurrent = function() {
4145   return this.current;
4146 }
4147 
4148 /**
4149  * Set a custom scale. Autoscaling will be disabled.
4150  * For example setScale(SCALE.MINUTES, 5) will result
4151  * in minor steps of 5 minutes, and major steps of an hour. 
4152  * 
4153  * @param {Step.SCALE} newScale  A scale. Choose from SCALE.MILLISECOND,
4154  *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
4155  *                               SCALE.DAY, SCALE.MONTH, SCALE.YEAR.
4156  * @param {int}        newStep   A step size, by default 1. Choose for
4157  *                               example 1, 2, 5, or 10.
4158  */   
4159 links.Timeline.StepDate.prototype.setScale = function(newScale, newStep) {
4160   this.scale = newScale;
4161   
4162   if (newStep > 0)
4163     this.step = newStep;
4164   
4165   this.autoScale = false;
4166 }
4167 
4168 /**
4169  * Enable or disable autoscaling
4170  * @param {boolean} enable  If true, autoascaling is set true
4171  */ 
4172 links.Timeline.StepDate.prototype.setAutoScale = function (enable) {
4173   this.autoScale = enable;
4174 }
4175 
4176 
4177 /**
4178  * Automatically determine the scale that bests fits the provided minimum step
4179  * @param {int} minimumStep  The minimum step size in milliseconds
4180  */ 
4181 links.Timeline.StepDate.prototype.setMinimumStep = function(minimumStep) {
4182   if (minimumStep == undefined)
4183     return;
4184 
4185   var stepYear       = (1000 * 60 * 60 * 24 * 30 * 12);
4186   var stepMonth      = (1000 * 60 * 60 * 24 * 30);
4187   var stepDay        = (1000 * 60 * 60 * 24);
4188   var stepHour       = (1000 * 60 * 60);
4189   var stepMinute     = (1000 * 60);
4190   var stepSecond     = (1000);
4191   var stepMillisecond= (1);
4192 
4193   // find the smallest step that is larger than the provided minimumStep
4194   if (stepYear*1000 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1000;}
4195   if (stepYear*500 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 500;}
4196   if (stepYear*100 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 100;}
4197   if (stepYear*50 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 50;}
4198   if (stepYear*10 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 10;}
4199   if (stepYear*5 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 5;}
4200   if (stepYear > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.YEAR;        this.step = 1;}
4201   if (stepMonth*3 > minimumStep)          {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 3;}
4202   if (stepMonth > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.MONTH;       this.step = 1;}
4203   if (stepDay*5 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 5;}
4204   if (stepDay*2 > minimumStep)            {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 2;}
4205   if (stepDay > minimumStep)              {this.scale = links.Timeline.StepDate.SCALE.DAY;         this.step = 1;}
4206   if (stepHour*4 > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 4;}
4207   if (stepHour > minimumStep)             {this.scale = links.Timeline.StepDate.SCALE.HOUR;        this.step = 1;}
4208   if (stepMinute*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 15;}
4209   if (stepMinute*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 10;}
4210   if (stepMinute*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 5;}
4211   if (stepMinute > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.MINUTE;      this.step = 1;}
4212   if (stepSecond*15 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 15;}
4213   if (stepSecond*10 > minimumStep)        {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 10;}
4214   if (stepSecond*5 > minimumStep)         {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 5;}
4215   if (stepSecond > minimumStep)           {this.scale = links.Timeline.StepDate.SCALE.SECOND;      this.step = 1;}
4216   if (stepMillisecond*200 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 200;}
4217   if (stepMillisecond*100 > minimumStep)  {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 100;}
4218   if (stepMillisecond*50 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 50;}
4219   if (stepMillisecond*10 > minimumStep)   {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 10;}
4220   if (stepMillisecond*5 > minimumStep)    {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 5;}
4221   if (stepMillisecond > minimumStep)      {this.scale = links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 1;}
4222 }
4223 
4224 /**
4225  * Snap a date to a rounded value. The snap intervals are dependent on the 
4226  * current scale and step.
4227  * @param {Date} date   the date to be snapped
4228  */ 
4229 links.Timeline.StepDate.prototype.snap = function(date) {
4230   if (this.scale == links.Timeline.StepDate.SCALE.YEAR) {
4231     var year = date.getFullYear() + Math.round(date.getMonth() / 12);
4232     date.setFullYear(Math.round(year / this.step) * this.step);
4233     date.setMonth(0);
4234     date.setDate(0);
4235     date.setHours(0);
4236     date.setMinutes(0);
4237     date.setSeconds(0);
4238     date.setMilliseconds(0);
4239   } 
4240   else if (this.scale == links.Timeline.StepDate.SCALE.MONTH) {
4241     if (date.getDate() > 15) {
4242       date.setDate(1); 
4243       date.setMonth(date.getMonth() + 1);
4244       // important: first set Date to 1, after that change the month.      
4245     }
4246     else {
4247       date.setDate(1);
4248     }
4249     
4250     date.setHours(0);
4251     date.setMinutes(0);
4252     date.setSeconds(0);
4253     date.setMilliseconds(0);
4254   } 
4255   else if (this.scale == links.Timeline.StepDate.SCALE.DAY) {
4256     switch (this.step) {
4257       case 5:
4258       case 2: 
4259         date.setHours(Math.round(date.getHours() / 24) * 24); break;
4260       default: 
4261         date.setHours(Math.round(date.getHours() / 12) * 12); break;
4262     }
4263     date.setMinutes(0);
4264     date.setSeconds(0);
4265     date.setMilliseconds(0);
4266   } 
4267   else if (this.scale == links.Timeline.StepDate.SCALE.HOUR) {
4268     switch (this.step) {
4269       case 4:
4270         date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
4271       default: 
4272         date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
4273     }    
4274     date.setSeconds(0);
4275     date.setMilliseconds(0);
4276   } else if (this.scale == links.Timeline.StepDate.SCALE.MINUTE) {
4277     switch (this.step) {
4278       case 15:
4279       case 10:
4280         date.setMinutes(Math.round(date.getMinutes() / 5) * 5); 
4281         date.setSeconds(0);
4282         break;
4283       case 5:
4284         date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
4285       default: 
4286         date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
4287     }    
4288     date.setMilliseconds(0);
4289   } 
4290   else if (this.scale == links.Timeline.StepDate.SCALE.SECOND) {
4291     switch (this.step) {
4292       case 15:
4293       case 10:
4294         date.setSeconds(Math.round(date.getSeconds() / 5) * 5); 
4295         date.setMilliseconds(0);
4296         break;
4297       case 5:
4298         date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
4299       default: 
4300         date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
4301     }
4302   }
4303   else if (this.scale == links.Timeline.StepDate.SCALE.MILLISECOND) {
4304     var step = this.step > 5 ? this.step / 2 : 1;
4305     date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);    
4306   }
4307 }
4308 
4309 /**
4310  * Check if the current step is a major step (for example when the step
4311  * is DAY, a major step is each first day of the MONTH)
4312  * @return true if current date is major, else false.
4313  */ 
4314 links.Timeline.StepDate.prototype.isMajor = function() {
4315   switch (this.scale)
4316   {
4317     case links.Timeline.StepDate.SCALE.MILLISECOND:
4318       return (this.current.getMilliseconds() == 0);
4319     case links.Timeline.StepDate.SCALE.SECOND:
4320       return (this.current.getSeconds() == 0);
4321     case links.Timeline.StepDate.SCALE.MINUTE:
4322       return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);  
4323       // Note: this is no bug. Major label is equal for both minute and hour scale
4324     case links.Timeline.StepDate.SCALE.HOUR:
4325       return (this.current.getHours() == 0);
4326     case links.Timeline.StepDate.SCALE.DAY:  
4327       return (this.current.getDate() == 1);
4328     case links.Timeline.StepDate.SCALE.MONTH:        
4329       return (this.current.getMonth() == 0);
4330     case links.Timeline.StepDate.SCALE.YEAR:         
4331       return false
4332     default:                      
4333       return false;    
4334   }
4335 }
4336 
4337 
4338 /**
4339  * Returns formatted text for the minor axislabel, depending on the current
4340  * date and the scale. For example when scale is MINUTE, the current time is 
4341  * formatted as "hh:mm".
4342  * @param {Date}       optional custom date. if not provided, current date is taken
4343  * @return {string}    minor axislabel
4344  */ 
4345 links.Timeline.StepDate.prototype.getLabelMinor = function(date) {
4346   var MONTHS_SHORT = new Array("Jan", "Feb", "Mar", 
4347                                 "Apr", "May", "Jun", 
4348                                 "Jul", "Aug", "Sep", 
4349                                 "Oct", "Nov", "Dec");
4350 
4351   if (date == undefined) {
4352     date = this.current;
4353   }
4354 
4355   switch (this.scale)
4356   {
4357     case links.Timeline.StepDate.SCALE.MILLISECOND:  return String(date.getMilliseconds());
4358     case links.Timeline.StepDate.SCALE.SECOND:       return String(date.getSeconds());
4359     case links.Timeline.StepDate.SCALE.MINUTE:       return this.addZeros(date.getHours(), 2) + ":" +
4360                                                        this.addZeros(date.getMinutes(), 2);
4361     case links.Timeline.StepDate.SCALE.HOUR:         return this.addZeros(date.getHours(), 2) + ":" +
4362                                                        this.addZeros(date.getMinutes(), 2);
4363     case links.Timeline.StepDate.SCALE.DAY:          return String(date.getDate());
4364     case links.Timeline.StepDate.SCALE.MONTH:        return MONTHS_SHORT[date.getMonth()];   // month is zero based
4365     case links.Timeline.StepDate.SCALE.YEAR:         return String(date.getFullYear());
4366     default:                                         return "";    
4367   }
4368 }
4369 
4370 
4371 /**
4372  * Returns formatted text for the major axislabel, depending on the current
4373  * date and the scale. For example when scale is MINUTE, the major scale is
4374  * hours, and the hour will be formatted as "hh". 
4375  * @param {Date}       optional custom date. if not provided, current date is taken
4376  * @return {string}    major axislabel
4377  */ 
4378 links.Timeline.StepDate.prototype.getLabelMajor = function(date) {
4379   var MONTHS = new Array("January", "February", "March", 
4380                          "April", "May", "June", 
4381                          "July", "August", "September", 
4382                          "October", "November", "December");
4383   var DAYS = new Array("Sunday", "Monday", "Tuesday", 
4384                        "Wednesday", "Thursday", "Friday", "Saturday");  
4385 
4386   if (date == undefined) {
4387     date = this.current;
4388   } 
4389 
4390   switch (this.scale) {
4391     case links.Timeline.StepDate.SCALE.MILLISECOND:
4392       return  this.addZeros(date.getHours(), 2) + ":" +
4393               this.addZeros(date.getMinutes(), 2) + ":" +
4394               this.addZeros(date.getSeconds(), 2);   
4395     case links.Timeline.StepDate.SCALE.SECOND:
4396       return  date.getDate() + " " + 
4397               MONTHS[date.getMonth()] + " " +
4398               this.addZeros(date.getHours(), 2) + ":" +
4399               this.addZeros(date.getMinutes(), 2);
4400     case links.Timeline.StepDate.SCALE.MINUTE:
4401       return  DAYS[date.getDay()] + " " +
4402               date.getDate() + " " + 
4403               MONTHS[date.getMonth()] + " " +
4404               date.getFullYear();
4405     case links.Timeline.StepDate.SCALE.HOUR:
4406       return  DAYS[date.getDay()] + " " +
4407               date.getDate() + " " + 
4408               MONTHS[date.getMonth()] + " " +
4409               date.getFullYear();
4410     case links.Timeline.StepDate.SCALE.DAY:
4411       return  MONTHS[date.getMonth()] + " " +
4412               date.getFullYear();
4413     case links.Timeline.StepDate.SCALE.MONTH:
4414       return String(date.getFullYear());
4415     default:
4416       return "";
4417   }        
4418 }
4419 
4420 /**
4421  * Add leading zeros to the given value to match the desired length.
4422  * For example addZeros(123, 5) returns "00123"
4423  * @param {int} value   A value
4424  * @param {int} len     Desired final length
4425  * @return {string}     value with leading zeros
4426  */ 
4427 links.Timeline.StepDate.prototype.addZeros = function(value, len) {
4428   var str = "" + value;
4429   while (str.length < len) {
4430     str = "0" + str;
4431   }
4432   return str;
4433 }
4434 
4435 
4436 /** ------------------------------------------------------------------------ **/
4437 
4438 
4439 /**
4440  * Add and event listener. Works for all browsers
4441  * @param {DOM Element} element    An html element
4442  * @param {string}      action     The action, for example "click", 
4443  *                                 without the prefix "on"
4444  * @param {function}    listener   The callback function to be executed
4445  * @param {boolean}     useCapture
4446  */ 
4447 links.Timeline.addEventListener = function (element, action, listener, useCapture) {
4448   if (element.addEventListener) {
4449     if (useCapture === undefined)
4450       useCapture = false;
4451       
4452     if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
4453       action = "DOMMouseScroll";  // For Firefox
4454     }
4455       
4456     element.addEventListener(action, listener, useCapture);
4457   } else {    
4458     element.attachEvent("on" + action, listener);  // IE browsers
4459   }
4460 };
4461 
4462 /**
4463  * Remove an event listener from an element
4464  * @param {DOM element}  element   An html dom element
4465  * @param {string}       action    The name of the event, for example "mousedown"
4466  * @param {function}     listener  The listener function
4467  * @param {boolean}      useCapture
4468  */ 
4469 links.Timeline.removeEventListener = function(element, action, listener, useCapture) {
4470   if (element.removeEventListener) {
4471     // non-IE browsers
4472     if (useCapture === undefined)
4473       useCapture = false;    
4474           
4475     if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
4476       action = "DOMMouseScroll";  // For Firefox
4477     }
4478 
4479     element.removeEventListener(action, listener, useCapture); 
4480   } else {
4481     // IE browsers
4482     element.detachEvent("on" + action, listener);
4483   }
4484 };
4485 
4486 
4487 /**
4488  * Get HTML element which is the target of the event
4489  * @param {MouseEvent} event
4490  * @return {HTML DOM} target element
4491  */ 
4492 links.Timeline.getTarget = function (event) {
4493   // code from http://www.quirksmode.org/js/events_properties.html
4494   if (!event) {
4495     var event = window.event;
4496   }
4497 
4498   var target;
4499   
4500   if (event.target) {
4501     target = event.target;
4502   }
4503   else if (event.srcElement) {
4504     target = event.srcElement;
4505   }
4506   
4507   if (target.nodeType !== undefined && target.nodeType == 3) {
4508     // defeat Safari bug
4509     target = target.parentNode;
4510   }
4511   
4512   return target;
4513 }
4514 
4515 /**
4516  * Stop event propagation
4517  */ 
4518 links.Timeline.stopPropagation = function (event) {
4519   if (!event) 
4520     var event = window.event;
4521   
4522   if (event.stopPropagation) {
4523     event.stopPropagation();  // non-IE browsers
4524   }
4525   else {
4526     event.cancelBubble = true;  // IE browsers
4527   }
4528 }
4529 
4530 
4531 /**
4532  * Cancels the event if it is cancelable, without stopping further propagation of the event.
4533  */ 
4534 links.Timeline.prevent   ult = function (event) {
4535   if (!event) 
4536     var event = window.event;
4537   
4538   if (event.prevent   ult) {
4539     event.prevent   ult();  // non-IE browsers
4540   }
4541   else {    
4542     event.returnValue = false;  // IE browsers
4543   }
4544 }
4545 
4546 
4547 /**
4548  * Retrieve the absolute left value of a DOM element
4549  * @param {DOM element} elem    A dom element, for example a div
4550  * @return {number} left        The absolute left position of this element
4551  *                              in the browser page.
4552  */ 
4553 links.Timeline.getAbsoluteLeft = function(elem)
4554 {
4555   var left = 0;
4556   while( elem != null ) {
4557     left += elem.offsetLeft;
4558     //left -= elem.srcollLeft;  // TODO: adjust for scroll positions. check if it works in IE too
4559     elem = elem.offsetParent;
4560   }
4561   return left;
4562 }
4563 
4564 /**
4565  * Retrieve the absolute top value of a DOM element
4566  * @param {DOM element} elem    A dom element, for example a div
4567  * @return {number} top        The absolute top position of this element
4568  *                              in the browser page.
4569  */ 
4570 links.Timeline.getAbsoluteTop = function(elem)
4571 {
4572   var top = 0;
4573   while( elem != null ) {
4574     top += elem.offsetTop;
4575     //left -= elem.srcollLeft;  // TODO: adjust for scroll positions. check if it works in IE too
4576     elem = elem.offsetParent;
4577   }
4578   return top;
4579 }
4580