You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@climate.apache.org by sk...@apache.org on 2013/06/21 09:50:25 UTC

svn commit: r1495306 [3/3] - in /incubator/climate/trunk/rcmet/src/main/ui/app: ./ css/ js/controllers/ js/directives/ lib/timeline/

Added: incubator/climate/trunk/rcmet/src/main/ui/app/lib/timeline/timeline.js
URL: http://svn.apache.org/viewvc/incubator/climate/trunk/rcmet/src/main/ui/app/lib/timeline/timeline.js?rev=1495306&view=auto
==============================================================================
--- incubator/climate/trunk/rcmet/src/main/ui/app/lib/timeline/timeline.js (added)
+++ incubator/climate/trunk/rcmet/src/main/ui/app/lib/timeline/timeline.js Fri Jun 21 07:50:24 2013
@@ -0,0 +1,6381 @@
+/**
+ * @file timeline.js
+ *
+ * @brief
+ * The Timeline is an interactive visualization chart to visualize events in
+ * time, having a start and end date.
+ * You can freely move and zoom in the timeline by dragging
+ * and scrolling in the Timeline. Items are optionally dragable. The time
+ * scale on the axis is adjusted automatically, and supports scales ranging
+ * from milliseconds to years.
+ *
+ * Timeline is part of the CHAP Links library.
+ *
+ * Timeline is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera 10.6, and
+ * Internet Explorer 6+.
+ *
+ * @license
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ *
+ * Copyright (c) 2011-2013 Almende B.V.
+ *
+ * @author     Jos de Jong, <jo...@almende.org>
+ * @date    2013-04-18
+ * @version 2.4.2
+ */
+
+/*
+ * i18n mods by github user iktuz (https://gist.github.com/iktuz/3749287/)
+ * added to v2.4.1 with da_DK language by @bjarkebech
+ */
+
+/*
+ * TODO
+ *
+ * Add zooming with pinching on Android
+ * 
+ * Bug: when an item contains a javascript onclick or a link, this does not work
+ *      when the item is not selected (when the item is being selected,
+ *      it is redrawn, which cancels any onclick or link action)
+ * Bug: when an item contains an image without size, or a css max-width, it is not sized correctly
+ * Bug: neglect items when they have no valid start/end, instead of throwing an error
+ * Bug: Pinching on ipad does not work very well, sometimes the page will zoom when pinching vertically
+ * Bug: cannot set max width for an item, like div.timeline-event-content {white-space: normal; max-width: 100px;}
+ * Bug on IE in Quirks mode. When you have groups, and delete an item, the groups become invisible
+ */
+
+/**
+ * Declare a unique namespace for CHAP's Common Hybrid Visualisation Library,
+ * "links"
+ */
+if (typeof links === 'undefined') {
+    links = {};
+    // important: do not use var, as "var links = {};" will overwrite 
+    //            the existing links variable value with undefined in IE8, IE7.  
+}
+
+
+/**
+ * Ensure the variable google exists
+ */
+if (typeof google === 'undefined') {
+    google = undefined;
+    // important: do not use var, as "var google = undefined;" will overwrite 
+    //            the existing google variable value with undefined in IE8, IE7.
+}
+
+
+
+// Internet Explorer 8 and older does not support Array.indexOf,
+// so we define it here in that case
+// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
+if(!Array.prototype.indexOf) {
+    Array.prototype.indexOf = function(obj){
+        for(var i = 0; i < this.length; i++){
+            if(this[i] == obj){
+                return i;
+            }
+        }
+        return -1;
+    }
+}
+
+// Internet Explorer 8 and older does not support Array.forEach,
+// so we define it here in that case
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
+if (!Array.prototype.forEach) {
+    Array.prototype.forEach = function(fn, scope) {
+        for(var i = 0, len = this.length; i < len; ++i) {
+            fn.call(scope || this, this[i], i, this);
+        }
+    }
+}
+
+
+/**
+ * @constructor links.Timeline
+ * The timeline is a visualization chart to visualize events in time.
+ *
+ * The timeline is developed in javascript as a Google Visualization Chart.
+ *
+ * @param {Element} container   The DOM element in which the Timeline will
+ *                                  be created. Normally a div element.
+ */
+links.Timeline = function(container) {
+    if (!container) {
+        // this call was probably only for inheritance, no constructor-code is required
+        return;
+    }
+
+    // create variables and set default values
+    this.dom = {};
+    this.conversion = {};
+    this.eventParams = {}; // stores parameters for mouse events
+    this.groups = [];
+    this.groupIndexes = {};
+    this.items = [];
+    this.renderQueue = {
+        show: [],   // Items made visible but not yet added to DOM
+        hide: [],   // Items currently visible but not yet removed from DOM
+        update: []  // Items with changed data but not yet adjusted DOM
+    };
+    this.renderedItems = [];  // Items currently rendered in the DOM
+    this.clusterGenerator = new links.Timeline.ClusterGenerator(this);
+    this.currentClusters = [];
+    this.selection = undefined; // stores index and item which is currently selected
+
+    this.listeners = {}; // event listener callbacks
+
+    // Initialize sizes. 
+    // Needed for IE (which gives an error when you try to set an undefined
+    // value in a style)
+    this.size = {
+        'actualHeight': 0,
+        'axis': {
+            'characterMajorHeight': 0,
+            'characterMajorWidth': 0,
+            'characterMinorHeight': 0,
+            'characterMinorWidth': 0,
+            'height': 0,
+            'labelMajorTop': 0,
+            'labelMinorTop': 0,
+            'line': 0,
+            'lineMajorWidth': 0,
+            'lineMinorHeight': 0,
+            'lineMinorTop': 0,
+            'lineMinorWidth': 0,
+            'top': 0
+        },
+        'contentHeight': 0,
+        'contentLeft': 0,
+        'contentWidth': 0,
+        'frameHeight': 0,
+        'frameWidth': 0,
+        'groupsLeft': 0,
+        'groupsWidth': 0,
+        'items': {
+            'top': 0
+        }
+    };
+
+    this.dom.container = container;
+
+    this.options = {
+        'width': "100%",
+        'height': "auto",
+        'minHeight': 0,        // minimal height in pixels
+        'autoHeight': true,
+
+        'eventMargin': 10,     // minimal margin between events
+        'eventMarginAxis': 20, // minimal margin between events and the axis
+        'dragAreaWidth': 10,   // pixels
+
+        'min': undefined,
+        'max': undefined,
+        'zoomMin': 10,     // milliseconds
+        'zoomMax': 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
+
+        'moveable': true,
+        'zoomable': true,
+        'selectable': true,
+        'editable': false,
+        'snapEvents': true,
+        'groupChangeable': true,
+
+        'showCurrentTime': true, // show a red bar displaying the current time
+        'showCustomTime': false, // show a blue, draggable bar displaying a custom time    
+        'showMajorLabels': true,
+        'showMinorLabels': true,
+        'showNavigation': false,
+        'showButtonNew': false,
+        'groupsOnRight': false,
+        'axisOnTop': false,
+        'stackEvents': true,
+        'animate': true,
+        'animateZoom': true,
+        'cluster': false,
+        'style': 'box',
+        'customStackOrder': false, //a function(a,b) for determining stackorder amongst a group of items. Essentially a comparator, -ve value for "a before b" and vice versa
+        
+        // i18n: Timeline only has built-in English text per default. Include timeline-locales.js to support more localized text.
+        'locale': 'en',
+        'MONTHS': new Array("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"),
+        'MONTHS_SHORT': new Array("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"),
+        'DAYS': new Array("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"),
+        'DAYS_SHORT': new Array("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"),
+        'ZOOM_IN': "Zoom in",
+        'ZOOM_OUT': "Zoom out",
+        'MOVE_LEFT': "Move left",
+        'MOVE_RIGHT': "Move right",
+        'NEW': "New",
+        'CREATE_NEW_EVENT': "Create new event"
+    };
+
+    this.clientTimeOffset = 0;    // difference between client time and the time
+    // set via Timeline.setCurrentTime()
+    var dom = this.dom;
+
+    // remove all elements from the container element.
+    while (dom.container.hasChildNodes()) {
+        dom.container.removeChild(dom.container.firstChild);
+    }
+
+    // create a step for drawing the axis
+    this.step = new links.Timeline.StepDate();
+
+    // add standard item types
+    this.itemTypes = {
+        box:   links.Timeline.ItemBox,
+        range: links.Timeline.ItemRange,
+        dot:   links.Timeline.ItemDot
+    };
+
+    // initialize data
+    this.data = [];
+    this.firstDraw = true;
+
+    // date interval must be initialized 
+    this.setVisibleChartRange(undefined, undefined, false);
+
+    // render for the first time
+    this.render();
+
+    // fire the ready event
+    var me = this;
+    setTimeout(function () {
+        me.trigger('ready');
+    }, 0);
+};
+
+
+/**
+ * Main drawing logic. This is the function that needs to be called
+ * in the html page, to draw the timeline.
+ *
+ * A data table with the events must be provided, and an options table.
+ *
+ * @param {google.visualization.DataTable}      data
+ *                                 The data containing the events for the timeline.
+ *                                 Object DataTable is defined in
+ *                                 google.visualization.DataTable
+ * @param {Object} options         A name/value map containing settings for the
+ *                                 timeline. Optional.
+ */
+links.Timeline.prototype.draw = function(data, options) {
+    this.setOptions(options);
+
+    // read the data
+    this.setData(data);
+
+    // set timer range. this will also redraw the timeline
+    if (options && (options.start || options.end)) {
+        this.setVisibleChartRange(options.start, options.end);
+    }
+    else if (this.firstDraw) {
+        this.setVisibleChartRangeAuto();
+    }
+
+    this.firstDraw = false;
+};
+
+
+/**
+ * Set options for the timeline.
+ * Timeline must be redrawn afterwards
+ * @param {Object} options A name/value map containing settings for the
+ *                                 timeline. Optional.
+ */
+links.Timeline.prototype.setOptions = function(options) {
+    if (options) {
+        // retrieve parameter values
+        for (var i in options) {
+            if (options.hasOwnProperty(i)) {
+                this.options[i] = options[i];
+            }
+        }
+        
+        // prepare i18n dependent on set locale
+        if (typeof links.locales !== 'undefined' && this.options.locale !== 'en') {
+            var localeOpts = links.locales[this.options.locale];
+            if(localeOpts) {
+                for (var l in localeOpts) {
+                    if (localeOpts.hasOwnProperty(l)) {
+                        this.options[l] = localeOpts[l];
+                    }
+                }
+            }
+        }
+
+        // check for deprecated options
+        if (options.showButtonAdd != undefined) {
+            this.options.showButtonNew = options.showButtonAdd;
+            console.log('WARNING: Option showButtonAdd is deprecated. Use showButtonNew instead');
+        }
+        if (options.intervalMin != undefined) {
+            this.options.zoomMin = options.intervalMin;
+            console.log('WARNING: Option intervalMin is deprecated. Use zoomMin instead');
+        }
+        if (options.intervalMax != undefined) {
+            this.options.zoomMax = options.intervalMax;
+            console.log('WARNING: Option intervalMax is deprecated. Use zoomMax instead');
+        }
+
+        if (options.scale && options.step) {
+            this.step.setScale(options.scale, options.step);
+        }
+    }
+
+    // validate options
+    this.options.autoHeight = (this.options.height === "auto");
+};
+
+/**
+ * Add new type of items
+ * @param {String} typeName  Name of new type
+ * @param {links.Timeline.Item} typeFactory Constructor of items
+ */
+links.Timeline.prototype.addItemType = function (typeName, typeFactory) {
+    this.itemTypes[typeName] = typeFactory;
+};
+
+/**
+ * Retrieve a map with the column indexes of the columns by column name.
+ * For example, the method returns the map
+ *     {
+ *         start: 0,
+ *         end: 1,
+ *         content: 2,
+ *         group: undefined,
+ *         className: undefined
+ *         editable: undefined
+ *     }
+ * @param {google.visualization.DataTable} dataTable
+ * @type {Object} map
+ */
+links.Timeline.mapColumnIds = function (dataTable) {
+    var cols = {},
+        colMax = dataTable.getNumberOfColumns(),
+        allUndefined = true;
+
+    // loop over the columns, and map the column id's to the column indexes
+    for (var col = 0; col < colMax; col++) {
+        var id = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
+        cols[id] = col;
+        if (id == 'start' || id == 'end' || id == 'content' ||
+            id == 'group' || id == 'className' || id == 'editable') {
+            allUndefined = false;
+        }
+    }
+
+    // if no labels or ids are defined,
+    // use the default mapping for start, end, content
+    if (allUndefined) {
+        cols.start = 0;
+        cols.end = 1;
+        cols.content = 2;
+    }
+
+    return cols;
+};
+
+/**
+ * Set data for the timeline
+ * @param {google.visualization.DataTable | Array} data
+ */
+links.Timeline.prototype.setData = function(data) {
+    // unselect any previously selected item
+    this.unselectItem();
+
+    if (!data) {
+        data = [];
+    }
+
+    // clear all data
+    this.stackCancelAnimation();
+    this.clearItems();
+    this.data = data;
+    var items = this.items;
+    this.deleteGroups();
+
+    if (google && google.visualization &&
+        data instanceof google.visualization.DataTable) {
+        // map the datatable columns
+        var cols = links.Timeline.mapColumnIds(data);
+
+        // read DataTable
+        for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
+            items.push(this.createItem({
+                'start':     ((cols.start != undefined)     ? data.getValue(row, cols.start)     : undefined),
+                'end':       ((cols.end != undefined)       ? data.getValue(row, cols.end)       : undefined),
+                'content':   ((cols.content != undefined)   ? data.getValue(row, cols.content)   : undefined),
+                'group':     ((cols.group != undefined)     ? data.getValue(row, cols.group)     : undefined),
+                'className': ((cols.className != undefined) ? data.getValue(row, cols.className) : undefined),
+                'editable':  ((cols.editable != undefined)  ? data.getValue(row, cols.editable)  : undefined)
+            }));
+        }
+    }
+    else if (links.Timeline.isArray(data)) {
+        // read JSON array
+        for (var row = 0, rows = data.length; row < rows; row++) {
+            var itemData = data[row];
+            var item = this.createItem(itemData);
+            items.push(item);
+        }
+    }
+    else {
+        throw "Unknown data type. DataTable or Array expected.";
+    }
+
+    // prepare data for clustering, by filtering and sorting by type
+    if (this.options.cluster) {
+        this.clusterGenerator.setData(this.items);
+    }
+
+    this.render({
+        animate: false
+    });
+};
+
+/**
+ * Return the original data table.
+ * @return {google.visualization.DataTable | Array} data
+ */
+links.Timeline.prototype.getData = function  () {
+    return this.data;
+};
+
+
+/**
+ * Update the original data with changed start, end or group.
+ *
+ * @param {Number} index
+ * @param {Object} values   An object containing some of the following parameters:
+ *                          {Date} start,
+ *                          {Date} end,
+ *                          {String} content,
+ *                          {String} group
+ */
+links.Timeline.prototype.updateData = function  (index, values) {
+    var data = this.data,
+        prop;
+
+    if (google && google.visualization &&
+        data instanceof google.visualization.DataTable) {
+        // update the original google DataTable
+        var missingRows = (index + 1) - data.getNumberOfRows();
+        if (missingRows > 0) {
+            data.addRows(missingRows);
+        }
+
+        // map the column id's by name
+        var cols = links.Timeline.mapColumnIds(data);
+
+        // merge all fields from the provided data into the current data
+        for (prop in values) {
+            if (values.hasOwnProperty(prop)) {
+                var col = cols[prop];
+                if (col == undefined) {
+                    // create new column
+                    var value = values[prop];
+                    var valueType = 'string';
+                    if (typeof(value) == 'number')       {valueType = 'number';}
+                    else if (typeof(value) == 'boolean') {valueType = 'boolean';}
+                    else if (value instanceof Date)      {valueType = 'datetime';}
+                    col = data.addColumn(valueType, prop);
+                }
+                data.setValue(index, col, values[prop]);
+
+                // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number)
+            }
+        }
+    }
+    else if (links.Timeline.isArray(data)) {
+        // update the original JSON table
+        var row = data[index];
+        if (row == undefined) {
+            row = {};
+            data[index] = row;
+        }
+
+        // merge all fields from the provided data into the current data
+        for (prop in values) {
+            if (values.hasOwnProperty(prop)) {
+                row[prop] = values[prop];
+
+                // TODO: correctly serialize the start and end Date to the desired type (Date, String, or Number)
+            }
+        }
+    }
+    else {
+        throw "Cannot update data, unknown type of data";
+    }
+};
+
+/**
+ * Find the item index from a given HTML element
+ * If no item index is found, undefined is returned
+ * @param {Element} element
+ * @return {Number | undefined} index
+ */
+links.Timeline.prototype.getItemIndex = function(element) {
+    var e = element,
+        dom = this.dom,
+        frame = dom.items.frame,
+        items = this.items,
+        index = undefined;
+
+    // try to find the frame where the items are located in
+    while (e.parentNode && e.parentNode !== frame) {
+        e = e.parentNode;
+    }
+
+    if (e.parentNode === frame) {
+        // yes! we have found the parent element of all items
+        // retrieve its id from the array with items
+        for (var i = 0, iMax = items.length; i < iMax; i++) {
+            if (items[i].dom === e) {
+                index = i;
+                break;
+            }
+        }
+    }
+
+    return index;
+};
+
+/**
+ * Set a new size for the timeline
+ * @param {string} width   Width in pixels or percentage (for example "800px"
+ *                         or "50%")
+ * @param {string} height  Height in pixels or percentage  (for example "400px"
+ *                         or "30%")
+ */
+links.Timeline.prototype.setSize = function(width, height) {
+    if (width) {
+        this.options.width = width;
+        this.dom.frame.style.width = width;
+    }
+    if (height) {
+        this.options.height = height;
+        this.options.autoHeight = (this.options.height === "auto");
+        if (height !==  "auto" ) {
+            this.dom.frame.style.height = height;
+        }
+    }
+
+    this.render({
+        animate: false
+    });
+};
+
+
+/**
+ * Set a new value for the visible range int the timeline.
+ * Set start undefined to include everything from the earliest date to end.
+ * Set end undefined to include everything from start to the last date.
+ * Example usage:
+ *    myTimeline.setVisibleChartRange(new Date("2010-08-22"),
+ *                                    new Date("2010-09-13"));
+ * @param {Date}   start     The start date for the timeline. optional
+ * @param {Date}   end       The end date for the timeline. optional
+ * @param {boolean} redraw   Optional. If true (default) the Timeline is
+ *                           directly redrawn
+ */
+links.Timeline.prototype.setVisibleChartRange = function(start, end, redraw) {
+    var range = {};
+    if (!start || !end) {
+        // retrieve the date range of the items
+        range = this.getDataRange(true);
+    }
+
+    if (!start) {
+        if (end) {
+            if (range.min && range.min.valueOf() < end.valueOf()) {
+                // start of the data
+                start = range.min;
+            }
+            else {
+                // 7 days before the end
+                start = new Date(end.valueOf());
+                start.setDate(start.getDate() - 7);
+            }
+        }
+        else {
+            // default of 3 days ago
+            start = new Date();
+            start.setDate(start.getDate() - 3);
+        }
+    }
+
+    if (!end) {
+        if (range.max) {
+            // end of the data
+            end = range.max;
+        }
+        else {
+            // 7 days after start
+            end = new Date(start.valueOf());
+            end.setDate(end.getDate() + 7);
+        }
+    }
+
+    // prevent start Date <= end Date
+    if (end <= start) {
+        end = new Date(start.valueOf());
+        end.setDate(end.getDate() + 7);
+    }
+
+    // limit to the allowed range (don't let this do by applyRange,
+    // because that method will try to maintain the interval (end-start)
+    var min = this.options.min ? this.options.min : undefined; // date
+    if (min != undefined && start.valueOf() < min.valueOf()) {
+        start = new Date(min.valueOf()); // date
+    }
+    var max = this.options.max ? this.options.max : undefined; // date
+    if (max != undefined && end.valueOf() > max.valueOf()) {
+        end = new Date(max.valueOf()); // date
+    }
+
+    this.applyRange(start, end);
+
+    if (redraw == undefined || redraw == true) {
+        this.render({
+            animate: false
+        });  // TODO: optimize, no reflow needed
+    }
+    else {
+        this.recalcConversion();
+    }
+};
+
+
+/**
+ * Change the visible chart range such that all items become visible
+ */
+links.Timeline.prototype.setVisibleChartRangeAuto = function() {
+    var range = this.getDataRange(true);
+    this.setVisibleChartRange(range.min, range.max);
+};
+
+/**
+ * Adjust the visible range such that the current time is located in the center
+ * of the timeline
+ */
+links.Timeline.prototype.setVisibleChartRangeNow = function() {
+    var now = new Date();
+
+    var diff = (this.end.valueOf() - this.start.valueOf());
+
+    var startNew = new Date(now.valueOf() - diff/2);
+    var endNew = new Date(startNew.valueOf() + diff);
+    this.setVisibleChartRange(startNew, endNew);
+};
+
+
+/**
+ * Retrieve the current visible range in the timeline.
+ * @return {Object} An object with start and end properties
+ */
+links.Timeline.prototype.getVisibleChartRange = function() {
+    return {
+        'start': new Date(this.start.valueOf()),
+        'end': new Date(this.end.valueOf())
+    };
+};
+
+/**
+ * Get the date range of the items.
+ * @param {boolean} [withMargin]  If true, 5% of whitespace is added to the
+ *                                left and right of the range. Default is false.
+ * @return {Object} range    An object with parameters min and max.
+ *                           - {Date} min is the lowest start date of the items
+ *                           - {Date} max is the highest start or end date of the items
+ *                           If no data is available, the values of min and max
+ *                           will be undefined
+ */
+links.Timeline.prototype.getDataRange = function (withMargin) {
+    var items = this.items,
+        min = undefined, // number
+        max = undefined; // number
+
+    if (items) {
+        for (var i = 0, iMax = items.length; i < iMax; i++) {
+            var item = items[i],
+                start = item.start != undefined ? item.start.valueOf() : undefined,
+                end   = item.end != undefined   ? item.end.valueOf() : start;
+
+            if (min != undefined && start != undefined) {
+                min = Math.min(min.valueOf(), start.valueOf());
+            }
+            else {
+                min = start;
+            }
+
+            if (max != undefined && end != undefined) {
+                max = Math.max(max, end);
+            }
+            else {
+                max = end;
+            }
+        }
+    }
+
+    if (min && max && withMargin) {
+        // zoom out 5% such that you have a little white space on the left and right
+        var diff = (max - min);
+        min = min - diff * 0.05;
+        max = max + diff * 0.05;
+    }
+
+    return {
+        'min': min != undefined ? new Date(min) : undefined,
+        'max': max != undefined ? new Date(max) : undefined
+    };
+};
+
+/**
+ * Re-render (reflow and repaint) all components of the Timeline: frame, axis,
+ * items, ...
+ * @param {Object} [options]  Available options:
+ *                            {boolean} renderTimesLeft   Number of times the
+ *                                                        render may be repeated
+ *                                                        5 times by default.
+ *                            {boolean} animate           takes options.animate
+ *                                                        as default value
+ */
+links.Timeline.prototype.render = function(options) {
+    var frameResized = this.reflowFrame();
+    var axisResized = this.reflowAxis();
+    var groupsResized = this.reflowGroups();
+    var itemsResized = this.reflowItems();
+    var resized = (frameResized || axisResized || groupsResized || itemsResized);
+
+    // TODO: only stackEvents/filterItems when resized or changed. (gives a bootstrap issue).
+    // if (resized) {
+    var animate = this.options.animate;
+    if (options && options.animate != undefined) {
+        animate = options.animate;
+    }
+
+    this.recalcConversion();
+    this.clusterItems();
+    this.filterItems();
+    this.stackItems(animate);
+
+    this.recalcItems();
+
+    // TODO: only repaint when resized or when filterItems or stackItems gave a change?
+    var needsReflow = this.repaint();
+
+    // re-render once when needed (prevent endless re-render loop)
+    if (needsReflow) {
+        var renderTimesLeft = options ? options.renderTimesLeft : undefined;
+        if (renderTimesLeft == undefined) {
+            renderTimesLeft = 5;
+        }
+        if (renderTimesLeft > 0) {
+            this.render({
+                'animate': options ? options.animate: undefined,
+                'renderTimesLeft': (renderTimesLeft - 1)
+            });
+        }
+    }
+};
+
+/**
+ * Repaint all components of the Timeline
+ * @return {boolean} needsReflow   Returns true if the DOM is changed such that
+ *                                 a reflow is needed.
+ */
+links.Timeline.prototype.repaint = function() {
+    var frameNeedsReflow = this.repaintFrame();
+    var axisNeedsReflow  = this.repaintAxis();
+    var groupsNeedsReflow  = this.repaintGroups();
+    var itemsNeedsReflow = this.repaintItems();
+    this.repaintCurrentTime();
+    this.repaintCustomTime();
+
+    return (frameNeedsReflow || axisNeedsReflow || groupsNeedsReflow || itemsNeedsReflow);
+};
+
+/**
+ * Reflow the timeline frame
+ * @return {boolean} resized    Returns true if any of the frame elements
+ *                              have been resized.
+ */
+links.Timeline.prototype.reflowFrame = function() {
+    var dom = this.dom,
+        options = this.options,
+        size = this.size,
+        resized = false;
+
+    // Note: IE7 has issues with giving frame.clientWidth, therefore I use offsetWidth instead
+    var frameWidth  = dom.frame ? dom.frame.offsetWidth : 0,
+        frameHeight = dom.frame ? dom.frame.clientHeight : 0;
+
+    resized = resized || (size.frameWidth !== frameWidth);
+    resized = resized || (size.frameHeight !== frameHeight);
+    size.frameWidth = frameWidth;
+    size.frameHeight = frameHeight;
+
+    return resized;
+};
+
+/**
+ * repaint the Timeline frame
+ * @return {boolean} needsReflow   Returns true if the DOM is changed such that
+ *                                 a reflow is needed.
+ */
+links.Timeline.prototype.repaintFrame = function() {
+    var needsReflow = false,
+        dom = this.dom,
+        options = this.options,
+        size = this.size;
+
+    // main frame
+    if (!dom.frame) {
+        dom.frame = document.createElement("DIV");
+        dom.frame.className = "timeline-frame";
+        dom.frame.style.position = "relative";
+        dom.frame.style.overflow = "hidden";
+        dom.container.appendChild(dom.frame);
+        needsReflow = true;
+    }
+
+    var height = options.autoHeight ?
+        (size.actualHeight + "px") :
+        (options.height || "100%");
+    var width  = options.width || "100%";
+    needsReflow = needsReflow || (dom.frame.style.height != height);
+    needsReflow = needsReflow || (dom.frame.style.width != width);
+    dom.frame.style.height = height;
+    dom.frame.style.width = width;
+
+    // contents
+    if (!dom.content) {
+        // create content box where the axis and items will be created
+        dom.content = document.createElement("DIV");
+        dom.content.style.position = "relative";
+        dom.content.style.overflow = "hidden";
+        dom.frame.appendChild(dom.content);
+
+        var timelines = document.createElement("DIV");
+        timelines.style.position = "absolute";
+        timelines.style.left = "0px";
+        timelines.style.top = "0px";
+        timelines.style.height = "100%";
+        timelines.style.width = "0px";
+        dom.content.appendChild(timelines);
+        dom.contentTimelines = timelines;
+
+        var params = this.eventParams,
+            me = this;
+        if (!params.onMouseDown) {
+            params.onMouseDown = function (event) {me.onMouseDown(event);};
+            links.Timeline.addEventListener(dom.content, "mousedown", params.onMouseDown);
+        }
+        if (!params.onTouchStart) {
+            params.onTouchStart = function (event) {me.onTouchStart(event);};
+            links.Timeline.addEventListener(dom.content, "touchstart", params.onTouchStart);
+        }
+        if (!params.onMouseWheel) {
+            params.onMouseWheel = function (event) {me.onMouseWheel(event);};
+            links.Timeline.addEventListener(dom.content, "mousewheel", params.onMouseWheel);
+        }
+        if (!params.onDblClick) {
+            params.onDblClick = function (event) {me.onDblClick(event);};
+            links.Timeline.addEventListener(dom.content, "dblclick", params.onDblClick);
+        }
+
+        needsReflow = true;
+    }
+    dom.content.style.left = size.contentLeft + "px";
+    dom.content.style.top = "0px";
+    dom.content.style.width = size.contentWidth + "px";
+    dom.content.style.height = size.frameHeight + "px";
+
+    this.repaintNavigation();
+
+    return needsReflow;
+};
+
+/**
+ * Reflow the timeline axis. Calculate its height, width, positioning, etc...
+ * @return {boolean} resized    returns true if the axis is resized
+ */
+links.Timeline.prototype.reflowAxis = function() {
+    var resized = false,
+        dom = this.dom,
+        options = this.options,
+        size = this.size,
+        axisDom = dom.axis;
+
+    var characterMinorWidth  = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientWidth : 0,
+        characterMinorHeight = (axisDom && axisDom.characterMinor) ? axisDom.characterMinor.clientHeight : 0,
+        characterMajorWidth  = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientWidth : 0,
+        characterMajorHeight = (axisDom && axisDom.characterMajor) ? axisDom.characterMajor.clientHeight : 0,
+        axisHeight = (options.showMinorLabels ? characterMinorHeight : 0) +
+            (options.showMajorLabels ? characterMajorHeight : 0);
+
+    var axisTop  = options.axisOnTop ? 0 : size.frameHeight - axisHeight,
+        axisLine = options.axisOnTop ? axisHeight : axisTop;
+
+    resized = resized || (size.axis.top !== axisTop);
+    resized = resized || (size.axis.line !== axisLine);
+    resized = resized || (size.axis.height !== axisHeight);
+    size.axis.top = axisTop;
+    size.axis.line = axisLine;
+    size.axis.height = axisHeight;
+    size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine +
+        (options.showMinorLabels ? characterMinorHeight : 0);
+    size.axis.labelMinorTop = options.axisOnTop ?
+        (options.showMajorLabels ? characterMajorHeight : 0) :
+        axisLine;
+    size.axis.lineMinorTop = options.axisOnTop ? size.axis.labelMinorTop : 0;
+    size.axis.lineMinorHeight = options.showMajorLabels ?
+        size.frameHeight - characterMajorHeight:
+        size.frameHeight;
+    if (axisDom && axisDom.minorLines && axisDom.minorLines.length) {
+        size.axis.lineMinorWidth = axisDom.minorLines[0].offsetWidth;
+    }
+    else {
+        size.axis.lineMinorWidth = 1;
+    }
+    if (axisDom && axisDom.majorLines && axisDom.majorLines.length) {
+        size.axis.lineMajorWidth = axisDom.majorLines[0].offsetWidth;
+    }
+    else {
+        size.axis.lineMajorWidth = 1;
+    }
+
+    resized = resized || (size.axis.characterMinorWidth  !== characterMinorWidth);
+    resized = resized || (size.axis.characterMinorHeight !== characterMinorHeight);
+    resized = resized || (size.axis.characterMajorWidth  !== characterMajorWidth);
+    resized = resized || (size.axis.characterMajorHeight !== characterMajorHeight);
+    size.axis.characterMinorWidth  = characterMinorWidth;
+    size.axis.characterMinorHeight = characterMinorHeight;
+    size.axis.characterMajorWidth  = characterMajorWidth;
+    size.axis.characterMajorHeight = characterMajorHeight;
+
+    var contentHeight = Math.max(size.frameHeight - axisHeight, 0);
+    size.contentLeft = options.groupsOnRight ? 0 : size.groupsWidth;
+    size.contentWidth = Math.max(size.frameWidth - size.groupsWidth, 0);
+    size.contentHeight = contentHeight;
+
+    return resized;
+};
+
+/**
+ * Redraw the timeline axis with minor and major labels
+ * @return {boolean} needsReflow     Returns true if the DOM is changed such
+ *                                   that a reflow is needed.
+ */
+links.Timeline.prototype.repaintAxis = function() {
+    var needsReflow = false,
+        dom = this.dom,
+        options = this.options,
+        size = this.size,
+        step = this.step;
+
+    var axis = dom.axis;
+    if (!axis) {
+        axis = {};
+        dom.axis = axis;
+    }
+    if (!size.axis.properties) {
+        size.axis.properties = {};
+    }
+    if (!axis.minorTexts) {
+        axis.minorTexts = [];
+    }
+    if (!axis.minorLines) {
+        axis.minorLines = [];
+    }
+    if (!axis.majorTexts) {
+        axis.majorTexts = [];
+    }
+    if (!axis.majorLines) {
+        axis.majorLines = [];
+    }
+
+    if (!axis.frame) {
+        axis.frame = document.createElement("DIV");
+        axis.frame.style.position = "absolute";
+        axis.frame.style.left = "0px";
+        axis.frame.style.top = "0px";
+        dom.content.appendChild(axis.frame);
+    }
+
+    // take axis offline
+    dom.content.removeChild(axis.frame);
+
+    axis.frame.style.width = (size.contentWidth) + "px";
+    axis.frame.style.height = (size.axis.height) + "px";
+
+    // the drawn axis is more wide than the actual visual part, such that
+    // the axis can be dragged without having to redraw it each time again.
+    var start = this.screenToTime(0);
+    var end = this.screenToTime(size.contentWidth);
+
+    // calculate minimum step (in milliseconds) based on character size
+    if (size.axis.characterMinorWidth) {
+        this.minimumStep = this.screenToTime(size.axis.characterMinorWidth * 6) -
+            this.screenToTime(0);
+
+        step.setRange(start, end, this.minimumStep);
+    }
+
+    var charsNeedsReflow = this.repaintAxisCharacters();
+    needsReflow = needsReflow || charsNeedsReflow;
+
+    // The current labels on the axis will be re-used (much better performance),
+    // therefore, the repaintAxis method uses the mechanism with
+    // repaintAxisStartOverwriting, repaintAxisEndOverwriting, and
+    // this.size.axis.properties is used.
+    this.repaintAxisStartOverwriting();
+
+    step.start();
+    var xFirstMajorLabel = undefined;
+    var max = 0;
+    while (!step.end() && max < 1000) {
+        max++;
+        var cur = step.getCurrent(),
+            x = this.timeToScreen(cur),
+            isMajor = step.isMajor();
+
+        if (options.showMinorLabels) {
+            this.repaintAxisMinorText(x, step.getLabelMinor(options));
+        }
+
+        if (isMajor && options.showMajorLabels) {
+            if (x > 0) {
+                if (xFirstMajorLabel == undefined) {
+                    xFirstMajorLabel = x;
+                }
+                this.repaintAxisMajorText(x, step.getLabelMajor(options));
+            }
+            this.repaintAxisMajorLine(x);
+        }
+        else {
+            this.repaintAxisMinorLine(x);
+        }
+
+        step.next();
+    }
+
+    // create a major label on the left when needed
+    if (options.showMajorLabels) {
+        var leftTime = this.screenToTime(0),
+            leftText = this.step.getLabelMajor(options, leftTime),
+            width = leftText.length * size.axis.characterMajorWidth + 10; // upper bound estimation
+
+        if (xFirstMajorLabel == undefined || width < xFirstMajorLabel) {
+            this.repaintAxisMajorText(0, leftText, leftTime);
+        }
+    }
+
+    // cleanup left over labels
+    this.repaintAxisEndOverwriting();
+
+    this.repaintAxisHorizontal();
+
+    // put axis online
+    dom.content.insertBefore(axis.frame, dom.content.firstChild);
+
+    return needsReflow;
+};
+
+/**
+ * Create characters used to determine the size of text on the axis
+ * @return {boolean} needsReflow   Returns true if the DOM is changed such that
+ *                                 a reflow is needed.
+ */
+links.Timeline.prototype.repaintAxisCharacters = function () {
+    // calculate the width and height of a single character
+    // this is used to calculate the step size, and also the positioning of the
+    // axis
+    var needsReflow = false,
+        dom = this.dom,
+        axis = dom.axis,
+        text;
+
+    if (!axis.characterMinor) {
+        text = document.createTextNode("0");
+        var characterMinor = document.createElement("DIV");
+        characterMinor.className = "timeline-axis-text timeline-axis-text-minor";
+        characterMinor.appendChild(text);
+        characterMinor.style.position = "absolute";
+        characterMinor.style.visibility = "hidden";
+        characterMinor.style.paddingLeft = "0px";
+        characterMinor.style.paddingRight = "0px";
+        axis.frame.appendChild(characterMinor);
+
+        axis.characterMinor = characterMinor;
+        needsReflow = true;
+    }
+
+    if (!axis.characterMajor) {
+        text = document.createTextNode("0");
+        var characterMajor = document.createElement("DIV");
+        characterMajor.className = "timeline-axis-text timeline-axis-text-major";
+        characterMajor.appendChild(text);
+        characterMajor.style.position = "absolute";
+        characterMajor.style.visibility = "hidden";
+        characterMajor.style.paddingLeft = "0px";
+        characterMajor.style.paddingRight = "0px";
+        axis.frame.appendChild(characterMajor);
+
+        axis.characterMajor = characterMajor;
+        needsReflow = true;
+    }
+
+    return needsReflow;
+};
+
+/**
+ * Initialize redraw of the axis. All existing labels and lines will be
+ * overwritten and reused.
+ */
+links.Timeline.prototype.repaintAxisStartOverwriting = function () {
+    var properties = this.size.axis.properties;
+
+    properties.minorTextNum = 0;
+    properties.minorLineNum = 0;
+    properties.majorTextNum = 0;
+    properties.majorLineNum = 0;
+};
+
+/**
+ * End of overwriting HTML DOM elements of the axis.
+ * remaining elements will be removed
+ */
+links.Timeline.prototype.repaintAxisEndOverwriting = function () {
+    var dom = this.dom,
+        props = this.size.axis.properties,
+        frame = this.dom.axis.frame,
+        num;
+
+    // remove leftovers
+    var minorTexts = dom.axis.minorTexts;
+    num = props.minorTextNum;
+    while (minorTexts.length > num) {
+        var minorText = minorTexts[num];
+        frame.removeChild(minorText);
+        minorTexts.splice(num, 1);
+    }
+
+    var minorLines = dom.axis.minorLines;
+    num = props.minorLineNum;
+    while (minorLines.length > num) {
+        var minorLine = minorLines[num];
+        frame.removeChild(minorLine);
+        minorLines.splice(num, 1);
+    }
+
+    var majorTexts = dom.axis.majorTexts;
+    num = props.majorTextNum;
+    while (majorTexts.length > num) {
+        var majorText = majorTexts[num];
+        frame.removeChild(majorText);
+        majorTexts.splice(num, 1);
+    }
+
+    var majorLines = dom.axis.majorLines;
+    num = props.majorLineNum;
+    while (majorLines.length > num) {
+        var majorLine = majorLines[num];
+        frame.removeChild(majorLine);
+        majorLines.splice(num, 1);
+    }
+};
+
+/**
+ * Repaint the horizontal line and background of the axis
+ */
+links.Timeline.prototype.repaintAxisHorizontal = function() {
+    var axis = this.dom.axis,
+        size = this.size,
+        options = this.options;
+
+    // line behind all axis elements (possibly having a background color)
+    var hasAxis = (options.showMinorLabels || options.showMajorLabels);
+    if (hasAxis) {
+        if (!axis.backgroundLine) {
+            // create the axis line background (for a background color or so)
+            var backgroundLine = document.createElement("DIV");
+            backgroundLine.className = "timeline-axis";
+            backgroundLine.style.position = "absolute";
+            backgroundLine.style.left = "0px";
+            backgroundLine.style.width = "100%";
+            backgroundLine.style.border = "none";
+            axis.frame.insertBefore(backgroundLine, axis.frame.firstChild);
+
+            axis.backgroundLine = backgroundLine;
+        }
+
+        if (axis.backgroundLine) {
+            axis.backgroundLine.style.top = size.axis.top + "px";
+            axis.backgroundLine.style.height = size.axis.height + "px";
+        }
+    }
+    else {
+        if (axis.backgroundLine) {
+            axis.frame.removeChild(axis.backgroundLine);
+            delete axis.backgroundLine;
+        }
+    }
+
+    // line before all axis elements
+    if (hasAxis) {
+        if (axis.line) {
+            // put this line at the end of all childs
+            var line = axis.frame.removeChild(axis.line);
+            axis.frame.appendChild(line);
+        }
+        else {
+            // make the axis line
+            var line = document.createElement("DIV");
+            line.className = "timeline-axis";
+            line.style.position = "absolute";
+            line.style.left = "0px";
+            line.style.width = "100%";
+            line.style.height = "0px";
+            axis.frame.appendChild(line);
+
+            axis.line = line;
+        }
+
+        axis.line.style.top = size.axis.line + "px";
+    }
+    else {
+        if (axis.line && axis.line.parentElement) {
+            axis.frame.removeChild(axis.line);
+            delete axis.line;
+        }
+    }
+};
+
+/**
+ * Create a minor label for the axis at position x
+ * @param {Number} x
+ * @param {String} text
+ */
+links.Timeline.prototype.repaintAxisMinorText = function (x, text) {
+    var size = this.size,
+        dom = this.dom,
+        props = size.axis.properties,
+        frame = dom.axis.frame,
+        minorTexts = dom.axis.minorTexts,
+        index = props.minorTextNum,
+        label;
+
+    if (index < minorTexts.length) {
+        label = minorTexts[index]
+    }
+    else {
+        // create new label
+        var content = document.createTextNode("");
+        label = document.createElement("DIV");
+        label.appendChild(content);
+        label.className = "timeline-axis-text timeline-axis-text-minor";
+        label.style.position = "absolute";
+
+        frame.appendChild(label);
+
+        minorTexts.push(label);
+    }
+
+    label.childNodes[0].nodeValue = text;
+    label.style.left = x + "px";
+    label.style.top  = size.axis.labelMinorTop + "px";
+    //label.title = title;  // TODO: this is a heavy operation
+
+    props.minorTextNum++;
+};
+
+/**
+ * Create a minor line for the axis at position x
+ * @param {Number} x
+ */
+links.Timeline.prototype.repaintAxisMinorLine = function (x) {
+    var axis = this.size.axis,
+        dom = this.dom,
+        props = axis.properties,
+        frame = dom.axis.frame,
+        minorLines = dom.axis.minorLines,
+        index = props.minorLineNum,
+        line;
+
+    if (index < minorLines.length) {
+        line = minorLines[index];
+    }
+    else {
+        // create vertical line
+        line = document.createElement("DIV");
+        line.className = "timeline-axis-grid timeline-axis-grid-minor";
+        line.style.position = "absolute";
+        line.style.width = "0px";
+
+        frame.appendChild(line);
+        minorLines.push(line);
+    }
+
+    line.style.top = axis.lineMinorTop + "px";
+    line.style.height = axis.lineMinorHeight + "px";
+    line.style.left = (x - axis.lineMinorWidth/2) + "px";
+
+    props.minorLineNum++;
+};
+
+/**
+ * Create a Major label for the axis at position x
+ * @param {Number} x
+ * @param {String} text
+ */
+links.Timeline.prototype.repaintAxisMajorText = function (x, text) {
+    var size = this.size,
+        props = size.axis.properties,
+        frame = this.dom.axis.frame,
+        majorTexts = this.dom.axis.majorTexts,
+        index = props.majorTextNum,
+        label;
+
+    if (index < majorTexts.length) {
+        label = majorTexts[index];
+    }
+    else {
+        // create label
+        var content = document.createTextNode(text);
+        label = document.createElement("DIV");
+        label.className = "timeline-axis-text timeline-axis-text-major";
+        label.appendChild(content);
+        label.style.position = "absolute";
+        label.style.top = "0px";
+
+        frame.appendChild(label);
+        majorTexts.push(label);
+    }
+
+    label.childNodes[0].nodeValue = text;
+    label.style.top = size.axis.labelMajorTop + "px";
+    label.style.left = x + "px";
+    //label.title = title; // TODO: this is a heavy operation
+
+    props.majorTextNum ++;
+};
+
+/**
+ * Create a Major line for the axis at position x
+ * @param {Number} x
+ */
+links.Timeline.prototype.repaintAxisMajorLine = function (x) {
+    var size = this.size,
+        props = size.axis.properties,
+        axis = this.size.axis,
+        frame = this.dom.axis.frame,
+        majorLines = this.dom.axis.majorLines,
+        index = props.majorLineNum,
+        line;
+
+    if (index < majorLines.length) {
+        line = majorLines[index];
+    }
+    else {
+        // create vertical line
+        line = document.createElement("DIV");
+        line.className = "timeline-axis-grid timeline-axis-grid-major";
+        line.style.position = "absolute";
+        line.style.top = "0px";
+        line.style.width = "0px";
+
+        frame.appendChild(line);
+        majorLines.push(line);
+    }
+
+    line.style.left = (x - axis.lineMajorWidth/2) + "px";
+    line.style.height = size.frameHeight + "px";
+
+    props.majorLineNum ++;
+};
+
+/**
+ * Reflow all items, retrieve their actual size
+ * @return {boolean} resized    returns true if any of the items is resized
+ */
+links.Timeline.prototype.reflowItems = function() {
+    var resized = false,
+        i,
+        iMax,
+        group,
+        groups = this.groups,
+        renderedItems = this.renderedItems;
+
+    if (groups) { // TODO: need to check if labels exists?
+        // loop through all groups to reset the items height
+        groups.forEach(function (group) {
+            group.itemsHeight = 0;
+        });
+    }
+
+    // loop through the width and height of all visible items
+    for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
+        var item = renderedItems[i],
+            domItem = item.dom;
+        group = item.group;
+
+        if (domItem) {
+            // TODO: move updating width and height into item.reflow
+            var width = domItem ? domItem.clientWidth : 0;
+            var height = domItem ? domItem.clientHeight : 0;
+            resized = resized || (item.width != width);
+            resized = resized || (item.height != height);
+            item.width = width;
+            item.height = height;
+            //item.borderWidth = (domItem.offsetWidth - domItem.clientWidth - 2) / 2; // TODO: borderWidth
+            item.reflow();
+        }
+
+        if (group) {
+            group.itemsHeight = group.itemsHeight ?
+                Math.max(group.itemsHeight, item.height) :
+                item.height;
+        }
+    }
+
+    return resized;
+};
+
+/**
+ * Recalculate item properties:
+ * - the height of each group.
+ * - the actualHeight, from the stacked items or the sum of the group heights
+ * @return {boolean} resized    returns true if any of the items properties is
+ *                              changed
+ */
+links.Timeline.prototype.recalcItems = function () {
+    var resized = false,
+        i,
+        iMax,
+        item,
+        finalItem,
+        finalItems,
+        group,
+        groups = this.groups,
+        size = this.size,
+        options = this.options,
+        renderedItems = this.renderedItems;
+
+    var actualHeight = 0;
+    if (groups.length == 0) {
+        // calculate actual height of the timeline when there are no groups
+        // but stacked items
+        if (options.autoHeight || options.cluster) {
+            var min = 0,
+                max = 0;
+
+            if (this.stack && this.stack.finalItems) {
+                // adjust the offset of all finalItems when the actualHeight has been changed
+                finalItems = this.stack.finalItems;
+                finalItem = finalItems[0];
+                if (finalItem && finalItem.top) {
+                    min = finalItem.top;
+                    max = finalItem.top + finalItem.height;
+                }
+                for (i = 1, iMax = finalItems.length; i < iMax; i++) {
+                    finalItem = finalItems[i];
+                    min = Math.min(min, finalItem.top);
+                    max = Math.max(max, finalItem.top + finalItem.height);
+                }
+            }
+            else {
+                item = renderedItems[0];
+                if (item && item.top) {
+                    min = item.top;
+                    max = item.top + item.height;
+                }
+                for (i = 1, iMax = renderedItems.length; i < iMax; i++) {
+                    item = renderedItems[i];
+                    if (item.top) {
+                        min = Math.min(min, item.top);
+                        max = Math.max(max, (item.top + item.height));
+                    }
+                }
+            }
+
+            actualHeight = (max - min) + 2 * options.eventMarginAxis + size.axis.height;
+            if (actualHeight < options.minHeight) {
+                actualHeight = options.minHeight;
+            }
+
+            if (size.actualHeight != actualHeight && options.autoHeight && !options.axisOnTop) {
+                // adjust the offset of all items when the actualHeight has been changed
+                var diff = actualHeight - size.actualHeight;
+                if (this.stack && this.stack.finalItems) {
+                    finalItems = this.stack.finalItems;
+                    for (i = 0, iMax = finalItems.length; i < iMax; i++) {
+                        finalItems[i].top += diff;
+                        finalItems[i].item.top += diff;
+                    }
+                }
+                else {
+                    for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
+                        renderedItems[i].top += diff;
+                    }
+                }
+            }
+        }
+    }
+    else {
+        // loop through all groups to get the height of each group, and the
+        // total height
+        actualHeight = size.axis.height + 2 * options.eventMarginAxis;
+        for (i = 0, iMax = groups.length; i < iMax; i++) {
+            group = groups[i];
+
+            var groupHeight = Math.max(group.labelHeight || 0, group.itemsHeight || 0);
+            resized = resized || (groupHeight != group.height);
+            group.height = groupHeight;
+
+            actualHeight += groups[i].height + options.eventMargin;
+        }
+
+        // calculate top positions of the group labels and lines
+        var eventMargin = options.eventMargin,
+            top = options.axisOnTop ?
+                options.eventMarginAxis + eventMargin/2 :
+                size.contentHeight - options.eventMarginAxis + eventMargin/ 2,
+            axisHeight = size.axis.height;
+
+        for (i = 0, iMax = groups.length; i < iMax; i++) {
+            group = groups[i];
+            if (options.axisOnTop) {
+                group.top = top + axisHeight;
+                group.labelTop = top + axisHeight + (group.height - group.labelHeight) / 2;
+                group.lineTop = top + axisHeight + group.height + eventMargin/2;
+                top += group.height + eventMargin;
+            }
+            else {
+                top -= group.height + eventMargin;
+                group.top = top;
+                group.labelTop = top + (group.height - group.labelHeight) / 2;
+                group.lineTop = top - eventMargin/2;
+            }
+        }
+
+        // calculate top position of the visible items
+        for (i = 0, iMax = renderedItems.length; i < iMax; i++) {
+            item = renderedItems[i];
+            group = item.group;
+
+            if (group) {
+                item.top = group.top;
+            }
+        }
+
+        resized = true;
+    }
+
+    if (actualHeight < options.minHeight) {
+        actualHeight = options.minHeight;
+    }
+    resized = resized || (actualHeight != size.actualHeight);
+    size.actualHeight = actualHeight;
+
+    return resized;
+};
+
+/**
+ * This method clears the (internal) array this.items in a safe way: neatly
+ * cleaning up the DOM, and accompanying arrays this.renderedItems and
+ * the created clusters.
+ */
+links.Timeline.prototype.clearItems = function() {
+    // add all visible items to the list to be hidden
+    var hideItems = this.renderQueue.hide;
+    this.renderedItems.forEach(function (item) {
+        hideItems.push(item);
+    });
+
+    // clear the cluster generator
+    this.clusterGenerator.clear();
+
+    // actually clear the items
+    this.items = [];
+};
+
+/**
+ * Repaint all items
+ * @return {boolean} needsReflow   Returns true if the DOM is changed such that
+ *                                 a reflow is needed.
+ */
+links.Timeline.prototype.repaintItems = function() {
+    var i, iMax, item, index;
+
+    var needsReflow = false,
+        dom = this.dom,
+        size = this.size,
+        timeline = this,
+        renderedItems = this.renderedItems;
+
+    if (!dom.items) {
+        dom.items = {};
+    }
+
+    // draw the frame containing the items
+    var frame = dom.items.frame;
+    if (!frame) {
+        frame = document.createElement("DIV");
+        frame.style.position = "relative";
+        dom.content.appendChild(frame);
+        dom.items.frame = frame;
+    }
+
+    frame.style.left = "0px";
+    frame.style.top = size.items.top + "px";
+    frame.style.height = "0px";
+
+    // Take frame offline (for faster manipulation of the DOM)
+    dom.content.removeChild(frame);
+
+    // process the render queue with changes
+    var queue = this.renderQueue;
+    var newImageUrls = [];
+    needsReflow = needsReflow ||
+        (queue.show.length > 0) ||
+        (queue.update.length > 0) ||
+        (queue.hide.length > 0);   // TODO: reflow needed on hide of items?
+
+    while (item = queue.show.shift()) {
+        item.showDOM(frame);
+        item.getImageUrls(newImageUrls);
+        renderedItems.push(item);
+    }
+    while (item = queue.update.shift()) {
+        item.updateDOM(frame);
+        item.getImageUrls(newImageUrls);
+        index = this.renderedItems.indexOf(item);
+        if (index == -1) {
+            renderedItems.push(item);
+        }
+    }
+    while (item = queue.hide.shift()) {
+        item.hideDOM(frame);
+        index = this.renderedItems.indexOf(item);
+        if (index != -1) {
+            renderedItems.splice(index, 1);
+        }
+    }
+
+    // reposition all visible items
+    renderedItems.forEach(function (item) {
+        item.updatePosition(timeline);
+    });
+
+    // redraw the delete button and dragareas of the selected item (if any)
+    this.repaintDeleteButton();
+    this.repaintDragAreas();
+
+    // put frame online again
+    dom.content.appendChild(frame);
+
+    if (newImageUrls.length) {
+        // retrieve all image sources from the items, and set a callback once
+        // all images are retrieved
+        var callback = function () {
+            timeline.render();
+        };
+        var sendCallbackWhenAlreadyLoaded = false;
+        links.imageloader.loadAll(newImageUrls, callback, sendCallbackWhenAlreadyLoaded);
+    }
+
+    return needsReflow;
+};
+
+/**
+ * Reflow the size of the groups
+ * @return {boolean} resized    Returns true if any of the frame elements
+ *                              have been resized.
+ */
+links.Timeline.prototype.reflowGroups = function() {
+    var resized = false,
+        options = this.options,
+        size = this.size,
+        dom = this.dom;
+
+    // calculate the groups width and height
+    // TODO: only update when data is changed! -> use an updateSeq
+    var groupsWidth = 0;
+
+    // loop through all groups to get the labels width and height
+    var groups = this.groups;
+    var labels = this.dom.groups ? this.dom.groups.labels : [];
+    for (var i = 0, iMax = groups.length; i < iMax; i++) {
+        var group = groups[i];
+        var label = labels[i];
+        group.labelWidth  = label ? label.clientWidth : 0;
+        group.labelHeight = label ? label.clientHeight : 0;
+        group.width = group.labelWidth;  // TODO: group.width is redundant with labelWidth
+
+        groupsWidth = Math.max(groupsWidth, group.width);
+    }
+
+    // limit groupsWidth to the groups width in the options
+    if (options.groupsWidth !== undefined) {
+        groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0;
+    }
+
+    // compensate for the border width. TODO: calculate the real border width
+    groupsWidth += 1;
+
+    var groupsLeft = options.groupsOnRight ? size.frameWidth - groupsWidth : 0;
+    resized = resized || (size.groupsWidth !== groupsWidth);
+    resized = resized || (size.groupsLeft !== groupsLeft);
+    size.groupsWidth = groupsWidth;
+    size.groupsLeft = groupsLeft;
+
+    return resized;
+};
+
+/**
+ * Redraw the group labels
+ */
+links.Timeline.prototype.repaintGroups = function() {
+    var dom = this.dom,
+        timeline = this,
+        options = this.options,
+        size = this.size,
+        groups = this.groups;
+
+    if (dom.groups === undefined) {
+        dom.groups = {};
+    }
+
+    var labels = dom.groups.labels;
+    if (!labels) {
+        labels = [];
+        dom.groups.labels = labels;
+    }
+    var labelLines = dom.groups.labelLines;
+    if (!labelLines) {
+        labelLines = [];
+        dom.groups.labelLines = labelLines;
+    }
+    var itemLines = dom.groups.itemLines;
+    if (!itemLines) {
+        itemLines = [];
+        dom.groups.itemLines = itemLines;
+    }
+
+    // create the frame for holding the groups
+    var frame = dom.groups.frame;
+    if (!frame) {
+        frame =  document.createElement("DIV");
+        frame.className = "timeline-groups-axis";
+        frame.style.position = "absolute";
+        frame.style.overflow = "hidden";
+        frame.style.top = "0px";
+        frame.style.height = "100%";
+
+        dom.frame.appendChild(frame);
+        dom.groups.frame = frame;
+    }
+
+    frame.style.left = size.groupsLeft + "px";
+    frame.style.width = (options.groupsWidth !== undefined) ?
+        options.groupsWidth :
+        size.groupsWidth + "px";
+
+    // hide groups axis when there are no groups
+    if (groups.length == 0) {
+        frame.style.display = 'none';
+    }
+    else {
+        frame.style.display = '';
+    }
+
+    // TODO: only create/update groups when data is changed.
+
+    // create the items
+    var current = labels.length,
+        needed = groups.length;
+
+    // overwrite existing group labels
+    for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) {
+        var group = groups[i];
+        var label = labels[i];
+        label.innerHTML = this.getGroupName(group);
+        label.style.display = '';
+    }
+
+    // append new items when needed
+    for (var i = current; i < needed; i++) {
+        var group = groups[i];
+
+        // create text label
+        var label = document.createElement("DIV");
+        label.className = "timeline-groups-text";
+        label.style.position = "absolute";
+        if (options.groupsWidth === undefined) {
+            label.style.whiteSpace = "nowrap";
+        }
+        label.innerHTML = this.getGroupName(group);
+        frame.appendChild(label);
+        labels[i] = label;
+
+        // create the grid line between the group labels
+        var labelLine = document.createElement("DIV");
+        labelLine.className = "timeline-axis-grid timeline-axis-grid-minor";
+        labelLine.style.position = "absolute";
+        labelLine.style.left = "0px";
+        labelLine.style.width = "100%";
+        labelLine.style.height = "0px";
+        labelLine.style.borderTopStyle = "solid";
+        frame.appendChild(labelLine);
+        labelLines[i] = labelLine;
+
+        // create the grid line between the items
+        var itemLine = document.createElement("DIV");
+        itemLine.className = "timeline-axis-grid timeline-axis-grid-minor";
+        itemLine.style.position = "absolute";
+        itemLine.style.left = "0px";
+        itemLine.style.width = "100%";
+        itemLine.style.height = "0px";
+        itemLine.style.borderTopStyle = "solid";
+        dom.content.insertBefore(itemLine, dom.content.firstChild);
+        itemLines[i] = itemLine;
+    }
+
+    // remove redundant items from the DOM when needed
+    for (var i = needed; i < current; i++) {
+        var label = labels[i],
+            labelLine = labelLines[i],
+            itemLine = itemLines[i];
+
+        frame.removeChild(label);
+        frame.removeChild(labelLine);
+        dom.content.removeChild(itemLine);
+    }
+    labels.splice(needed, current - needed);
+    labelLines.splice(needed, current - needed);
+    itemLines.splice(needed, current - needed);
+
+    frame.style.borderStyle = options.groupsOnRight ?
+        "none none none solid" :
+        "none solid none none";
+
+    // position the groups
+    for (var i = 0, iMax = groups.length; i < iMax; i++) {
+        var group = groups[i],
+            label = labels[i],
+            labelLine = labelLines[i],
+            itemLine = itemLines[i];
+
+        label.style.top = group.labelTop + "px";
+        labelLine.style.top = group.lineTop + "px";
+        itemLine.style.top = group.lineTop + "px";
+        itemLine.style.width = size.contentWidth + "px";
+    }
+
+    if (!dom.groups.background) {
+        // create the axis grid line background
+        var background = document.createElement("DIV");
+        background.className = "timeline-axis";
+        background.style.position = "absolute";
+        background.style.left = "0px";
+        background.style.width = "100%";
+        background.style.border = "none";
+
+        frame.appendChild(background);
+        dom.groups.background = background;
+    }
+    dom.groups.background.style.top = size.axis.top + 'px';
+    dom.groups.background.style.height = size.axis.height + 'px';
+
+    if (!dom.groups.line) {
+        // create the axis grid line
+        var line = document.createElement("DIV");
+        line.className = "timeline-axis";
+        line.style.position = "absolute";
+        line.style.left = "0px";
+        line.style.width = "100%";
+        line.style.height = "0px";
+
+        frame.appendChild(line);
+        dom.groups.line = line;
+    }
+    dom.groups.line.style.top = size.axis.line + 'px';
+
+    // create a callback when there are images which are not yet loaded
+    // TODO: more efficiently load images in the groups
+    if (dom.groups.frame && groups.length) {
+        var imageUrls = [];
+        links.imageloader.filterImageUrls(dom.groups.frame, imageUrls);
+        if (imageUrls.length) {
+            // retrieve all image sources from the items, and set a callback once
+            // all images are retrieved
+            var callback = function () {
+                timeline.render();
+            };
+            var sendCallbackWhenAlreadyLoaded = false;
+            links.imageloader.loadAll(imageUrls, callback, sendCallbackWhenAlreadyLoaded);
+        }
+    }
+};
+
+
+/**
+ * Redraw the current time bar
+ */
+links.Timeline.prototype.repaintCurrentTime = function() {
+    var options = this.options,
+        dom = this.dom,
+        size = this.size;
+
+    if (!options.showCurrentTime) {
+        if (dom.currentTime) {
+            dom.contentTimelines.removeChild(dom.currentTime);
+            delete dom.currentTime;
+        }
+
+        return;
+    }
+
+    if (!dom.currentTime) {
+        // create the current time bar
+        var currentTime = document.createElement("DIV");
+        currentTime.className = "timeline-currenttime";
+        currentTime.style.position = "absolute";
+        currentTime.style.top = "0px";
+        currentTime.style.height = "100%";
+
+        dom.contentTimelines.appendChild(currentTime);
+        dom.currentTime = currentTime;
+    }
+
+    var now = new Date();
+    var nowOffset = new Date(now.valueOf() + this.clientTimeOffset);
+    var x = this.timeToScreen(nowOffset);
+
+    var visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
+    dom.currentTime.style.display = visible ? '' : 'none';
+    dom.currentTime.style.left = x + "px";
+    dom.currentTime.title = "Current time: " + nowOffset;
+
+    // start a timer to adjust for the new time
+    if (this.currentTimeTimer != undefined) {
+        clearTimeout(this.currentTimeTimer);
+        delete this.currentTimeTimer;
+    }
+    var timeline = this;
+    var onTimeout = function() {
+        timeline.repaintCurrentTime();
+    };
+    // the time equal to the width of one pixel, divided by 2 for more smoothness
+    var interval = 1 / this.conversion.factor / 2;
+    if (interval < 30) interval = 30;
+    this.currentTimeTimer = setTimeout(onTimeout, interval);
+};
+
+/**
+ * Redraw the custom time bar
+ */
+links.Timeline.prototype.repaintCustomTime = function() {
+    var options = this.options,
+        dom = this.dom,
+        size = this.size;
+
+    if (!options.showCustomTime) {
+        if (dom.customTime) {
+            dom.contentTimelines.removeChild(dom.customTime);
+            delete dom.customTime;
+        }
+
+        return;
+    }
+
+    if (!dom.customTime) {
+        var customTime = document.createElement("DIV");
+        customTime.className = "timeline-customtime";
+        customTime.style.position = "absolute";
+        customTime.style.top = "0px";
+        customTime.style.height = "100%";
+
+        var drag = document.createElement("DIV");
+        drag.style.position = "relative";
+        drag.style.top = "0px";
+        drag.style.left = "-10px";
+        drag.style.height = "100%";
+        drag.style.width = "20px";
+        customTime.appendChild(drag);
+
+        dom.contentTimelines.appendChild(customTime);
+        dom.customTime = customTime;
+
+        // initialize parameter
+        this.customTime = new Date();
+    }
+
+    var x = this.timeToScreen(this.customTime),
+        visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
+    dom.customTime.style.display = visible ? '' : 'none';
+    dom.customTime.style.left = x + "px";
+    dom.customTime.title = "Time: " + this.customTime;
+};
+
+
+/**
+ * Redraw the delete button, on the top right of the currently selected item
+ * if there is no item selected, the button is hidden.
+ */
+links.Timeline.prototype.repaintDeleteButton = function () {
+    var timeline = this,
+        dom = this.dom,
+        frame = dom.items.frame;
+
+    var deleteButton = dom.items.deleteButton;
+    if (!deleteButton) {
+        // create a delete button
+        deleteButton = document.createElement("DIV");
+        deleteButton.className = "timeline-navigation-delete";
+        deleteButton.style.position = "absolute";
+
+        frame.appendChild(deleteButton);
+        dom.items.deleteButton = deleteButton;
+    }
+
+    var index = this.selection ? this.selection.index : -1,
+        item = this.selection ? this.items[index] : undefined;
+    if (item && item.rendered && this.isEditable(item)) {
+        var right = item.getRight(this),
+            top = item.top;
+
+        deleteButton.style.left = right + 'px';
+        deleteButton.style.top = top + 'px';
+        deleteButton.style.display = '';
+        frame.removeChild(deleteButton);
+        frame.appendChild(deleteButton);
+    }
+    else {
+        deleteButton.style.display = 'none';
+    }
+};
+
+
+/**
+ * Redraw the drag areas. When an item (ranges only) is selected,
+ * it gets a drag area on the left and right side, to change its width
+ */
+links.Timeline.prototype.repaintDragAreas = function () {
+    var timeline = this,
+        options = this.options,
+        dom = this.dom,
+        frame = this.dom.items.frame;
+
+    // create left drag area
+    var dragLeft = dom.items.dragLeft;
+    if (!dragLeft) {
+        dragLeft = document.createElement("DIV");
+        dragLeft.className="timeline-event-range-drag-left";
+        dragLeft.style.position = "absolute";
+
+        frame.appendChild(dragLeft);
+        dom.items.dragLeft = dragLeft;
+    }
+
+    // create right drag area
+    var dragRight = dom.items.dragRight;
+    if (!dragRight) {
+        dragRight = document.createElement("DIV");
+        dragRight.className="timeline-event-range-drag-right";
+        dragRight.style.position = "absolute";
+
+        frame.appendChild(dragRight);
+        dom.items.dragRight = dragRight;
+    }
+
+    // reposition left and right drag area
+    var index = this.selection ? this.selection.index : -1,
+        item = this.selection ? this.items[index] : undefined;
+    if (item && item.rendered && this.isEditable(item) &&
+        (item instanceof links.Timeline.ItemRange)) {
+        var left = this.timeToScreen(item.start),
+            right = this.timeToScreen(item.end),
+            top = item.top,
+            height = item.height;
+
+        dragLeft.style.left = left + 'px';
+        dragLeft.style.top = top + 'px';
+        dragLeft.style.width = options.dragAreaWidth + "px";
+        dragLeft.style.height = height + 'px';
+        dragLeft.style.display = '';
+        frame.removeChild(dragLeft);
+        frame.appendChild(dragLeft);
+
+        dragRight.style.left = (right - options.dragAreaWidth) + 'px';
+        dragRight.style.top = top + 'px';
+        dragRight.style.width = options.dragAreaWidth + "px";
+        dragRight.style.height = height + 'px';
+        dragRight.style.display = '';
+        frame.removeChild(dragRight);
+        frame.appendChild(dragRight);
+    }
+    else {
+        dragLeft.style.display = 'none';
+        dragRight.style.display = 'none';
+    }
+};
+
+/**
+ * Create the navigation buttons for zooming and moving
+ */
+links.Timeline.prototype.repaintNavigation = function () {
+    var timeline = this,
+        options = this.options,
+        dom = this.dom,
+        frame = dom.frame,
+        navBar = dom.navBar;
+
+    if (!navBar) {
+        var showButtonNew = options.showButtonNew && options.editable;
+        var showNavigation = options.showNavigation && (options.zoomable || options.moveable);
+        if (showNavigation || showButtonNew) {
+            // create a navigation bar containing the navigation buttons
+            navBar = document.createElement("DIV");
+            navBar.style.position = "absolute";
+            navBar.className = "timeline-navigation";
+            if (options.groupsOnRight) {
+                navBar.style.left = '10px';
+            }
+            else {
+                navBar.style.right = '10px';
+            }
+            if (options.axisOnTop) {
+                navBar.style.bottom = '10px';
+            }
+            else {
+                navBar.style.top = '10px';
+            }
+            dom.navBar = navBar;
+            frame.appendChild(navBar);
+        }
+
+        if (showButtonNew) {
+            // create a new in button
+            navBar.addButton = document.createElement("DIV");
+            navBar.addButton.className = "timeline-navigation-new";
+
+            navBar.addButton.title = options.CREATE_NEW_EVENT;
+            var onAdd = function(event) {
+                links.Timeline.preventDefault(event);
+                links.Timeline.stopPropagation(event);
+
+                // create a new event at the center of the frame
+                var w = timeline.size.contentWidth;
+                var x = w / 2;
+                var xstart = timeline.screenToTime(x - w / 10); // subtract 10% of timeline width
+                var xend = timeline.screenToTime(x + w / 10);   // add 10% of timeline width
+                if (options.snapEvents) {
+                    timeline.step.snap(xstart);
+                    timeline.step.snap(xend);
+                }
+
+                var content = options.NEW;
+                var group = timeline.groups.length ? timeline.groups[0].content : undefined;
+                var preventRender = true;
+                timeline.addItem({
+                    'start': xstart,
+                    'end': xend,
+                    'content': content,
+                    'group': group
+                }, preventRender);
+                var index = (timeline.items.length - 1);
+                timeline.selectItem(index);
+
+                timeline.applyAdd = true;
+
+                // fire an add event.
+                // Note that the change can be canceled from within an event listener if
+                // this listener calls the method cancelAdd().
+                timeline.trigger('add');
+
+                if (timeline.applyAdd) {
+                    // render and select the item
+                    timeline.render({animate: false});
+                    timeline.selectItem(index);
+                }
+                else {
+                    // undo an add
+                    timeline.deleteItem(index);
+                }
+            };
+            links.Timeline.addEventListener(navBar.addButton, "mousedown", onAdd);
+            navBar.appendChild(navBar.addButton);
+        }
+
+        if (showButtonNew && showNavigation) {
+            // create a separator line
+            navBar.addButton.style.borderRightWidth = "1px";
+            navBar.addButton.style.borderRightStyle = "solid";
+        }
+
+        if (showNavigation) {
+            if (options.zoomable) {
+                // create a zoom in button
+                navBar.zoomInButton = document.createElement("DIV");
+                navBar.zoomInButton.className = "timeline-navigation-zoom-in";
+                navBar.zoomInButton.title = this.options.ZOOM_IN;
+                var onZoomIn = function(event) {
+                    links.Timeline.preventDefault(event);
+                    links.Timeline.stopPropagation(event);
+                    timeline.zoom(0.4);
+                    timeline.trigger("rangechange");
+                    timeline.trigger("rangechanged");
+                };
+                links.Timeline.addEventListener(navBar.zoomInButton, "mousedown", onZoomIn);
+                navBar.appendChild(navBar.zoomInButton);
+
+                // create a zoom out button
+                navBar.zoomOutButton = document.createElement("DIV");
+                navBar.zoomOutButton.className = "timeline-navigation-zoom-out";
+                navBar.zoomOutButton.title = this.options.ZOOM_OUT;
+                var onZoomOut = function(event) {
+                    links.Timeline.preventDefault(event);
+                    links.Timeline.stopPropagation(event);
+                    timeline.zoom(-0.4);
+                    timeline.trigger("rangechange");
+                    timeline.trigger("rangechanged");
+                };
+                links.Timeline.addEventListener(navBar.zoomOutButton, "mousedown", onZoomOut);
+                navBar.appendChild(navBar.zoomOutButton);
+            }
+
+            if (options.moveable) {
+                // create a move left button
+                navBar.moveLeftButton = document.createElement("DIV");
+                navBar.moveLeftButton.className = "timeline-navigation-move-left";
+                navBar.moveLeftButton.title = this.options.MOVE_LEFT;
+                var onMoveLeft = function(event) {
+                    links.Timeline.preventDefault(event);
+                    links.Timeline.stopPropagation(event);
+                    timeline.move(-0.2);
+                    timeline.trigger("rangechange");
+                    timeline.trigger("rangechanged");
+                };
+                links.Timeline.addEventListener(navBar.moveLeftButton, "mousedown", onMoveLeft);
+                navBar.appendChild(navBar.moveLeftButton);
+
+                // create a move right button
+                navBar.moveRightButton = document.createElement("DIV");
+                navBar.moveRightButton.className = "timeline-navigation-move-right";
+                navBar.moveRightButton.title = this.options.MOVE_RIGHT;
+                var onMoveRight = function(event) {
+                    links.Timeline.preventDefault(event);
+                    links.Timeline.stopPropagation(event);
+                    timeline.move(0.2);
+                    timeline.trigger("rangechange");
+                    timeline.trigger("rangechanged");
+                };
+                links.Timeline.addEventListener(navBar.moveRightButton, "mousedown", onMoveRight);
+                navBar.appendChild(navBar.moveRightButton);
+            }
+        }
+    }
+};
+
+
+/**
+ * Set current time. This function can be used to set the time in the client
+ * timeline equal with the time on a server.
+ * @param {Date} time
+ */
+links.Timeline.prototype.setCurrentTime = function(time) {
+    var now = new Date();
+    this.clientTimeOffset = (time.valueOf() - now.valueOf());
+
+    this.repaintCurrentTime();
+};
+
+/**
+ * Get current time. The time can have an offset from the real time, when
+ * the current time has been changed via the method setCurrentTime.
+ * @return {Date} time
+ */
+links.Timeline.prototype.getCurrentTime = function() {
+    var now = new Date();
+    return new Date(now.valueOf() + this.clientTimeOffset);
+};
+
+
+/**
+ * Set custom time.
+ * The custom time bar can be used to display events in past or future.
+ * @param {Date} time
+ */
+links.Timeline.prototype.setCustomTime = function(time) {
+    this.customTime = new Date(time.valueOf());
+    this.repaintCustomTime();
+};
+
+/**
+ * Retrieve the current custom time.
+ * @return {Date} customTime
+ */
+links.Timeline.prototype.getCustomTime = function() {
+    return new Date(this.customTime.valueOf());
+};
+
+/**
+ * Set a custom scale. Autoscaling will be disabled.
+ * For example setScale(SCALE.MINUTES, 5) will result
+ * in minor steps of 5 minutes, and major steps of an hour.
+ *
+ * @param {links.Timeline.StepDate.SCALE} scale
+ *                               A scale. Choose from SCALE.MILLISECOND,
+ *                               SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
+ *                               SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
+ *                               SCALE.YEAR.
+ * @param {int}        step   A step size, by default 1. Choose for
+ *                               example 1, 2, 5, or 10.
+ */
+links.Timeline.prototype.setScale = function(scale, step) {
+    this.step.setScale(scale, step);
+    this.render(); // TODO: optimize: only reflow/repaint axis
+};
+
+/**
+ * Enable or disable autoscaling
+ * @param {boolean} enable  If true or not defined, autoscaling is enabled.
+ *                          If false, autoscaling is disabled.
+ */
+links.Timeline.prototype.setAutoScale = function(enable) {
+    this.step.setAutoScale(enable);
+    this.render(); // TODO: optimize: only reflow/repaint axis
+};
+
+/**
+ * Redraw the timeline
+ * Reloads the (linked) data table and redraws the timeline when resized.
+ * See also the method checkResize
+ */
+links.Timeline.prototype.redraw = function() {
+    this.setData(this.data);
+};
+
+
+/**
+ * Check if the timeline is resized, and if so, redraw the timeline.
+ * Useful when the webpage is resized.
+ */
+links.Timeline.prototype.checkResize = function() {
+    // TODO: re-implement the method checkResize, or better, make it redundant as this.render will be smarter
+    this.render();
+};
+
+/**
+ * Check whether a given item is editable
+ * @param {links.Timeline.Item} item
+ * @return {boolean} editable
+ */
+links.Timeline.prototype.isEditable = function (item) {
+    if (item) {
+        if (item.editable != undefined) {
+            return item.editable;
+        }
+        else {
+            return this.options.editable;
+        }
+    }
+    return false;
+};
+
+/**
+ * Calculate the factor and offset to convert a position on screen to the
+ * corresponding date and vice versa.
+ * After the method calcConversionFactor is executed once, the methods screenToTime and
+ * timeToScreen can be used.
+ */
+links.Timeline.prototype.recalcConversion = function() {
+    this.conversion.offset = this.start.valueOf();
+    this.conversion.factor = this.size.contentWidth /
+        (this.end.valueOf() - this.start.valueOf());
+};
+
+
+/**
+ * Convert a position on screen (pixels) to a datetime
+ * Before this method can be used, the method calcConversionFactor must be
+ * executed once.
+ * @param {int}     x    Position on the screen in pixels
+ * @return {Date}   time The datetime the corresponds with given position x
+ */
+links.Timeline.prototype.screenToTime = function(x) {
+    var conversion = this.conversion;
+    return new Date(x / conversion.factor + conversion.offset);
+};
+
+/**
+ * Convert a datetime (Date object) into a position on the screen
+ * Before this method can be used, the method calcConversionFactor must be
+ * executed once.
+ * @param {Date}   time A date
+ * @return {int}   x    The position on the screen in pixels which corresponds
+ *                      with the given date.
+ */
+links.Timeline.prototype.timeToScreen = function(time) {
+    var conversion = this.conversion;
+    return (time.valueOf() - conversion.offset) * conversion.factor;
+};
+
+
+
+/**
+ * Event handler for touchstart event on mobile devices
+ */
+links.Timeline.prototype.onTouchStart = function(event) {
+    var params = this.eventParams,
+        me = this;
+
+    if (params.touchDown) {
+        // if already moving, return
+        return;
+    }
+
+    params.touchDown = true;
+    params.zoomed = false;
+
+    this.onMouseDown(event);
+
+    if (!params.onTouchMove) {
+        params.onTouchMove = function (event) {me.onTouchMove(event);};
+        links.Timeline.addEventListener(document, "touchmove", params.onTouchMove);
+    }
+    if (!params.onTouchEnd) {
+        params.onTouchEnd  = function (event) {me.onTouchEnd(event);};
+        links.Timeline.addEventListener(document, "touchend",  params.onTouchEnd);
+    }
+
+    /* TODO
+     // check for double tap event
+     var delta = 500; // ms
+     var doubleTapStart = (new Date()).valueOf();
+     var target = links.Timeline.getTarget(event);
+     var doubleTapItem = this.getItemIndex(target);
+     if (params.doubleTapStart &&
+     (doubleTapStart - params.doubleTapStart) < delta &&
+     doubleTapItem == params.doubleTapItem) {
+     delete params.doubleTapStart;
+     delete params.doubleTapItem;
+     me.onDblClick(event);
+     params.touchDown = false;
+     }
+     params.doubleTapStart = doubleTapStart;
+     params.doubleTapItem = doubleTapItem;
+     */
+    // store timing for double taps
+    var target = links.Timeline.getTarget(event);
+    var item = this.getItemIndex(target);
+    params.doubleTapStartPrev = params.doubleTapStart;
+    params.doubleTapStart = (new Date()).valueOf();
+    params.doubleTapItemPrev = params.doubleTapItem;
+    params.doubleTapItem = item;
+
+    links.Timeline.preventDefault(event);
+};
+
+/**
+ * Event handler for touchmove event on mobile devices
+ */
+links.Timeline.prototype.onTouchMove = function(event) {
+    var params = this.eventParams;
+
+    if (event.scale && event.scale !== 1) {
+        params.zoomed = true;
+    }
+
+    if (!params.zoomed) {
+        // move 
+        this.onMouseMove(event);
+    }
+    else {
+        if (this.options.zoomable) {
+            // pinch
+            // TODO: pinch only supported on iPhone/iPad. Create something manually for Android?
+            params.zoomed = true;
+
+            var scale = event.scale,
+                oldWidth = (params.end.valueOf() - params.start.valueOf()),
+                newWidth = oldWidth / scale,
+                diff = newWidth - oldWidth,
+                start = new Date(parseInt(params.start.valueOf() - diff/2)),
+                end = new Date(parseInt(params.end.valueOf() + diff/2));
+
+            // TODO: determine zoom-around-date from touch positions?
+
+            this.setVisibleChartRange(start, end);
+            this.trigger("rangechange");
+        }
+    }
+
+    links.Timeline.preventDefault(event);
+};
+
+/**
+ * Event handler for touchend event on mobile devices
+ */
+links.Timeline.prototype.onTouchEnd = function(event) {
+    var params = this.eventParams;
+    var me = this;
+    params.touchDown = false;
+
+    if (params.zoomed) {
+        this.trigger("rangechanged");
+    }
+
+    if (params.onTouchMove) {
+        links.Timeline.removeEventListener(document, "touchmove", params.onTouchMove);
+        delete params.onTouchMove;
+
+    }
+    if (params.onTouchEnd) {
+        links.Timeline.removeEventListener(document, "touchend",  params.onTouchEnd);
+        delete params.onTouchEnd;
+    }
+
+    this.onMouseUp(event);
+
+    // check for double tap event
+    var delta = 500; // ms
+    var doubleTapEnd = (new Date()).valueOf();
+    var target = links.Timeline.getTarget(event);
+    var doubleTapItem = this.getItemIndex(target);
+    if (params.doubleTapStartPrev &&
+        (doubleTapEnd - params.doubleTapStartPrev) < delta &&
+        params.doubleTapItem == params.doubleTapItemPrev) {
+        params.touchDown = true;
+        me.onDblClick(event);
+        params.touchDown = false;
+    }
+
+    links.Timeline.preventDefault(event);
+};
+
+
+/**
+ * Start a moving operation inside the provided parent element
+ * @param {Event} event       The event that occurred (required for
+ *                             retrieving the  mouse position)
+ */
+links.Timeline.prototype.onMouseDown = function(event) {
+    event = event || window.event;
+
+    var params = this.eventParams,
+        options = this.options,
+        dom = this.dom;
+
+    // only react on left mouse button down
+    var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
+    if (!leftButtonDown && !params.touchDown) {
+        return;
+    }
+
+    // get mouse position
+    params.mouseX = links.Timeline.getPageX(event);
+    params.mouseY = links.Timeline.getPageY(event);
+    params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content);
+    params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content);
+    params.previousLeft = 0;
+    params.previousOffset = 0;
+
+    params.moved = false;
+    params.start = new Date(this.start.valueOf());
+    params.end = new Date(this.end.valueOf());
+
+    params.target = links.Timeline.getTarget(event);

[... 3780 lines stripped ...]