      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 }
    1666
    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.preventDefault(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.preventDefault(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.preventDefault(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.preventDefault(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.preventDefault(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.preventDefault(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.preventDefault(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.preventDefault(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.preventDefault(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.preventDefault(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.preventDefault = function (event) {
    4535   if (!event)
    4536     var event = window.event;
    4537
    4538   if (event.preventDefault) {
    4539     event.preventDefault();  // 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