diff --git a/Jakefile.js b/Jakefile.js index c85d1b40..4340aa5e 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -64,8 +64,8 @@ task('build', {async: true}, function () { './src/DataSet.js', './src/DataView.js', + './src/timeline/stack.js', './src/timeline/TimeStep.js', - './src/timeline/Stack.js', './src/timeline/Range.js', './src/timeline/component/Component.js', './src/timeline/component/Panel.js', @@ -76,7 +76,6 @@ task('build', {async: true}, function () { './src/timeline/component/ItemSet.js', './src/timeline/component/item/*.js', './src/timeline/component/Group.js', - './src/timeline/component/GroupSet.js', './src/timeline/Timeline.js', './src/graph/dotparser.js', diff --git a/src/module/exports.js b/src/module/exports.js index a2f25d6e..7c2cd456 100644 --- a/src/module/exports.js +++ b/src/module/exports.js @@ -7,7 +7,7 @@ var vis = { DataSet: DataSet, DataView: DataView, Range: Range, - Stack: Stack, + stack: stack, TimeStep: TimeStep, components: { diff --git a/src/timeline/Stack.js b/src/timeline/Stack.js deleted file mode 100644 index 0e249c12..00000000 --- a/src/timeline/Stack.js +++ /dev/null @@ -1,168 +0,0 @@ -// TODO: turn Stack into a Mixin? - -/** - * @constructor Stack - * Stacks items on top of each other. - * @param {Object} [options] - */ -function Stack (options) { - this.options = options || {}; - this.defaultOptions = { - order: function (a, b) { - // Order: ranges over non-ranges, ranged ordered by width, - // and non-ranges ordered by start. - if (a instanceof ItemRange) { - if (b instanceof ItemRange) { - var aInt = (a.data.end - a.data.start); - var bInt = (b.data.end - b.data.start); - return (aInt - bInt) || (a.data.start - b.data.start); - } - else { - return -1; - } - } - else { - if (b instanceof ItemRange) { - return 1; - } - else { - return (a.data.start - b.data.start); - } - } - }, - margin: { - item: 10, - axis: 20 - } - }; -} - -/** - * Set options for the stack - * @param {Object} options Available options: - * {Number} [margin.item=10] - * {Number} [margin.axis=20] - * {function} [order] Stacking order - */ -Stack.prototype.setOptions = function setOptions (options) { - util.extend(this.options, options); -}; - -/** - * Order an array with items using a predefined order function for items - * @param {Item[]} items - */ -Stack.prototype.order = function order(items) { - //order the items - var order = this.options.order || this.defaultOptions.order; - if (!(typeof order === 'function')) { - throw new Error('Option order must be a function'); - } - items.sort(order); -}; - -/** - * Order items by their start data - * @param {Item[]} items - */ -Stack.prototype.orderByStart = function orderByStart(items) { - items.sort(function (a, b) { - return a.data.start - b.data.start; - }); -}; - -/** - * Order items by their end date. If they have no end date, their start date - * is used. - * @param {Item[]} items - */ -Stack.prototype.orderByEnd = function orderByEnd(items) { - items.sort(function (a, b) { - var aTime = ('end' in a.data) ? a.data.end : a.data.start, - bTime = ('end' in b.data) ? b.data.end : b.data.start; - - return aTime - bTime; - }); -}; - -/** - * Adjust vertical positions of the events such that they don't overlap each - * other. - * @param {Item[]} items All visible items - * @param {boolean} [force=false] If true, all items will be re-stacked. - * If false (default), only items having a - * top===null will be re-stacked - */ -Stack.prototype.stack = function stack (items, force) { - var i, - iMax, - options = this.options, - marginItem, - marginAxis; - - if (options.margin && options.margin.item !== undefined) { - marginItem = options.margin.item; - } - else { - marginItem = this.defaultOptions.margin.item - } - if (options.margin && options.margin.axis !== undefined) { - marginAxis = options.margin.axis; - } - else { - marginAxis = this.defaultOptions.margin.axis - } - - if (force) { - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - items[i].top = null; - } - } - - // calculate new, non-overlapping positions - for (i = 0, iMax = items.length; i < iMax; i++) { - var item = items[i]; - if (item.top === null) { - // initialize top position - item.top = marginAxis; - - do { - // TODO: optimize checking for overlap. when there is a gap without items, - // you only need to check for items from the next item on, not from zero - var collidingItem = null; - for (var j = 0, jj = items.length; j < jj; j++) { - var other = items[j]; - if (other.top !== null && other !== item && this.collision(item, other, marginItem)) { - collidingItem = other; - break; - } - } - - if (collidingItem != null) { - // There is a collision. Reposition the event above the colliding element - item.top = collidingItem.top + collidingItem.height + marginItem; - } - } while (collidingItem); - } - } -}; - -/** - * Test if the two provided items collide - * The items must have parameters left, width, top, and height. - * @param {Item} a The first item - * @param {Item} b The second item - * @param {Number} margin A minimum required margin. - * If margin is provided, the two items will be - * marked colliding when they overlap or - * when the margin between the two is smaller than - * the requested margin. - * @return {boolean} true if a and b collide, else false - */ -Stack.prototype.collision = function collision (a, b, margin) { - return ((a.left - margin) < (b.left + b.width) && - (a.left + a.width + margin) > b.left && - (a.top - margin) < (b.top + b.height) && - (a.top + a.height + margin) > b.top); -}; diff --git a/src/timeline/component/Group.js b/src/timeline/component/Group.js index 66e6a19b..408cad52 100644 --- a/src/timeline/component/Group.js +++ b/src/timeline/component/Group.js @@ -9,7 +9,12 @@ function Group (groupId, itemSet) { this.itemSet = itemSet; this.dom = {}; - this.items = {}; // items filtered by groupId of this group + this.items = {}; // items filtered by groupId of this group + this.visibleItems = []; // items currently visible in window + this.orderedItems = { // items sorted by start and by end + byStart: [], + byEnd: [] + }; this._create(); } @@ -62,19 +67,62 @@ Group.prototype.getAxis = function getAxis() { return this.dom.axis; }; -/** - * Get the height of the itemsets background - * @return {Number} height - */ -Group.prototype.getBackgroundHeight = function getBackgroundHeight() { - return this.itemSet.height; -}; - /** * Repaint this group + * @param {{start: number, end: number}} range + * @param {number | {item: number, axis: number}} margin + * @param {boolean} [restack=false] Force restacking of all items + * @return {boolean} Returns true if the group is resized */ -Group.prototype.repaint = function repaint() { - // TODO: implement Group.repaint +Group.prototype.repaint = function repaint(range, margin, restack) { + if (typeof margin === 'number') { + margin = { + item: margin, + axis: margin + }; + } + + // update visible items + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + + // reposition visible items vertically + stack.stack(this.visibleItems, margin, restack); + this.stackDirty = false; + for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { + this.visibleItems[i].repositionY(); + } + + // recalculate the height of the group + var height; + + // determine the height from the stacked items + var visibleItems = this.visibleItems; + if (visibleItems.length) { + var min = visibleItems[0].top; + var max = visibleItems[0].top + visibleItems[0].height; + util.forEach(visibleItems, function (item) { + min = Math.min(min, item.top); + max = Math.max(max, (item.top + item.height)); + }); + height = (max - min) + margin.axis + margin.item; + } + else { + height = margin.axis + margin.item; + } + + var resized = (this.height != height); + + // calculate actual size and position + var foreground = this.dom.foreground; + this.top = foreground.offsetTop; + this.left = foreground.offsetLeft; + this.width = foreground.offsetWidth; + this.height = height; + + // apply new height + foreground.style.height = height + 'px'; + + return resized; }; /** @@ -130,6 +178,11 @@ Group.prototype.hide = function hide() { Group.prototype.add = function add(item) { this.items[item.id] = item; item.setParent(this); + + if (item instanceof ItemRange && this.visibleItems.indexOf(item) == -1) { + var range = this.itemSet.range; // TODO: not nice accessing the range like this + this._checkIfVisible(item, this.visibleItems, range); + } }; /** @@ -139,5 +192,210 @@ Group.prototype.add = function add(item) { Group.prototype.remove = function remove(item) { delete this.items[item.id]; item.setParent(this.itemSet); + + // remove from visible items + var index = this.visibleItems.indexOf(item); + if (index != -1) this.visibleItems.splice(index, 1); + + // TODO: also remove from ordered items? +}; + +/** + * Order the items + * @private + */ +Group.prototype._order = function _order() { + var array = util.toArray(this.items); + this.orderedItems.byStart = array; + this.orderedItems.byEnd = this._constructByEndArray(array); + + // reorder the items + stack.orderByStart(this.orderedItems.byStart); + stack.orderByEnd(this.orderedItems.byEnd); }; +/** + * Create an array containing all items being a range (having an end date) + * @param {Item[]} array + * @returns {ItemRange[]} + * @private + */ +Group.prototype._constructByEndArray = function _constructByEndArray(array) { + var endArray = []; + + for (var i = 0; i < array.length; i++) { + if (array[i] instanceof ItemRange) { + endArray.push(array[i]); + } + } + return endArray; +}; + +/** + * Update the visible items + * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date + * @param {Item[]} visibleItems The previously visible items. + * @param {{start: number, end: number}} range Visible range + * @return {Item[]} visibleItems The new visible items. + * @private + */ +Group.prototype._updateVisibleItems = function _updateVisibleItems(orderedItems, visibleItems, range) { + var initialPosByStart, + newVisibleItems = [], + i; + + // first check if the items that were in view previously are still in view. + // this handles the case for the ItemRange that is both before and after the current one. + if (visibleItems.length > 0) { + for (i = 0; i < visibleItems.length; i++) { + this._checkIfVisible(visibleItems[i], newVisibleItems, range); + } + } + + // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime) + if (newVisibleItems.length == 0) { + initialPosByStart = this._binarySearch(orderedItems, range, false); + } + else { + initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]); + } + + // use visible search to find a visible ItemRange (only based on endTime) + var initialPosByEnd = this._binarySearch(orderedItems, range, true); + + // if we found a initial ID to use, trace it up and down until we meet an invisible item. + if (initialPosByStart != -1) { + for (i = initialPosByStart; i >= 0; i--) { + if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;} + } + for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) { + if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;} + } + } + + // if we found a initial ID to use, trace it up and down until we meet an invisible item. + if (initialPosByEnd != -1) { + for (i = initialPosByEnd; i >= 0; i--) { + if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;} + } + for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) { + if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;} + } + } + + return newVisibleItems; +}; + +/** + * This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd + * arrays. This is done by giving a boolean value true if you want to use the byEnd. + * This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check + * if the time we selected (start or end) is within the current range). + * + * The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is + * before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest, + * either the start OR end time has to be in the range. + * + * @param {{byStart: Item[], byEnd: Item[]}} orderedItems + * @param {{start: number, end: number}} range + * @param {Boolean} byEnd + * @returns {number} + * @private + */ +Group.prototype._binarySearch = function _binarySearch(orderedItems, range, byEnd) { + var array = []; + var byTime = byEnd ? 'end' : 'start'; + if (byEnd == true) {array = orderedItems.byEnd; } + else {array = orderedItems.byStart;} + + var interval = range.end - range.start; + + var found = false; + var low = 0; + var high = array.length; + var guess = Math.floor(0.5*(high+low)); + var newGuess; + + if (high == 0) {guess = -1;} + else if (high == 1) { + if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) { + guess = 0; + } + else { + guess = -1; + } + } + else { + high -= 1; + while (found == false) { + if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) { + found = true; + } + else { + if (array[guess].data[byTime] < range.start - interval) { // it is too small --> increase low + low = Math.floor(0.5*(high+low)); + } + else { // it is too big --> decrease high + high = Math.floor(0.5*(high+low)); + } + newGuess = Math.floor(0.5*(high+low)); + // not in list; + if (guess == newGuess) { + guess = -1; + found = true; + } + else { + guess = newGuess; + } + } + } + } + return guess; +}; + +/** + * this function checks if an item is invisible. If it is NOT we make it visible + * and add it to the global visible items. If it is, return true. + * + * @param {Item} item + * @param {Item[]} visibleItems + * @param {{start:number, end:number}} range + * @returns {boolean} + * @private + */ +Group.prototype._checkIfInvisible = function _checkIfInvisible(item, visibleItems, range) { + if (item.isVisible(range)) { + if (!item.displayed) item.show(); + item.repositionX(); + if (visibleItems.indexOf(item) == -1) { + visibleItems.push(item); + } + return false; + } + else { + return true; + } +}; + +/** + * this function is very similar to the _checkIfInvisible() but it does not + * return booleans, hides the item if it should not be seen and always adds to + * the visibleItems. + * this one is for brute forcing and hiding. + * + * @param {Item} item + * @param {Array} visibleItems + * @param {{start:number, end:number}} range + * @private + */ +Group.prototype._checkIfVisible = function _checkIfVisible(item, visibleItems, range) { + if (item.isVisible(range)) { + if (!item.displayed) item.show(); + // reposition item horizontally + item.repositionX(); + visibleItems.push(item); + } + else { + if (item.displayed) item.hide(); + } +}; diff --git a/src/timeline/component/GroupSet.js b/src/timeline/component/GroupSet.js index 1e94fa3c..387324da 100644 --- a/src/timeline/component/GroupSet.js +++ b/src/timeline/component/GroupSet.js @@ -1,3 +1,5 @@ +// TODO: remove groupset + /** * An GroupSet holds a set of groups * @param {Panel} contentPanel Panel where the ItemSets will be created diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index 349e93aa..a9f2445a 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -61,18 +61,11 @@ function ItemSet(backgroundPanel, axisPanel, labelPanel, options) { } }; - this.items = {}; // object with an Item for every data item - this.orderedItems = { - byStart: [], - byEnd: [] - }; - - this.groups = {}; // Group object for every group + this.items = {}; // object with an Item for every data item + this.groups = {}; // Group object for every group this.groupIds = []; - this.visibleItems = []; // visible, ordered items this.selection = []; // list with the ids of all selected nodes - this.stack = new Stack(Object.create(this.options)); this.stackDirty = true; // if true, all items will be restacked on next repaint this.touchParams = {}; // stores properties while dragging @@ -269,127 +262,18 @@ ItemSet.prototype.getFrame = function getFrame() { return this.frame; }; -/** - * This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd - * arrays. This is done by giving a boolean value true if you want to use the byEnd. - * This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check - * if the time we selected (start or end) is within the current range). - * - * The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is - * before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest, - * either the start OR end time has to be in the range. - * - * @param {{byStart: Item[], byEnd: Item[]}} orderedItems - * @param {{start: number, end: number}} range - * @param {Boolean} byEnd - * @returns {number} - * @private - */ -ItemSet.prototype._binarySearch = function _binarySearch(orderedItems, range, byEnd) { - var array = []; - var byTime = byEnd ? "end" : "start"; - if (byEnd == true) {array = orderedItems.byEnd; } - else {array = orderedItems.byStart;} - - var interval = range.end - range.start; - - var found = false; - var low = 0; - var high = array.length; - var guess = Math.floor(0.5*(high+low)); - var newGuess; - - if (high == 0) {guess = -1;} - else if (high == 1) { - if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) { - guess = 0; - } - else { - guess = -1; - } - } - else { - high -= 1; - while (found == false) { - if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) { - found = true; - } - else { - if (array[guess].data[byTime] < range.start - interval) { // it is too small --> increase low - low = Math.floor(0.5*(high+low)); - } - else { // it is too big --> decrease high - high = Math.floor(0.5*(high+low)); - } - newGuess = Math.floor(0.5*(high+low)); - // not in list; - if (guess == newGuess) { - guess = -1; - found = true; - } - else { - guess = newGuess; - } - } - } - } - return guess; -}; - -/** - * this function checks if an item is invisible. If it is NOT we make it visible and add it to the global visible items. If it is, return true. - * - * @param {Item} item - * @param {Item[]} visibleItems - * @returns {boolean} - * @private - */ -ItemSet.prototype._checkIfInvisible = function _checkIfInvisible(item, visibleItems) { - if (item.isVisible(this.range)) { - if (!item.displayed) item.show(); - item.repositionX(); - if (visibleItems.indexOf(item) == -1) { - visibleItems.push(item); - } - return false; - } - else { - return true; - } -}; - - -/** - * this function is very similar to the _checkIfInvisible() but it does not return booleans, hides the item if it should not be seen and always adds to the visibleItems. - * this one is for brute forcing and hiding. - * - * @param {Item} item - * @param {Array} visibleItems - * @private - */ -ItemSet.prototype._checkIfVisible = function _checkIfVisible(item, visibleItems) { - if (item.isVisible(this.range)) { - if (!item.displayed) item.show(); - // reposition item horizontally - item.repositionX(); - visibleItems.push(item); - } - else { - if (item.displayed) item.hide(); - } -}; - /** * Repaint the component * @return {boolean} Returns true if the component is resized */ ItemSet.prototype.repaint = function repaint() { - var asSize = util.option.asSize, + var margin = this.options.margin, + range = this.range, + asSize = util.option.asSize, asString = util.option.asString, options = this.options, orientation = this.getOption('orientation'), - frame = this.frame, - i, ii; + frame = this.frame; // update className frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : ''); @@ -400,36 +284,14 @@ ItemSet.prototype.repaint = function repaint() { this.lastVisibleInterval = visibleInterval; this.lastWidth = this.width; - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, this.range); - - // reposition visible items vertically. - //this.stack.order(this.visibleItems); // TODO: improve ordering - var force = this.stackDirty || zoomed; // force re-stacking of all items if true - this.stack.stack(this.visibleItems, force); + // repaint all groups + var restack = zoomed || this.stackDirty; + var height = 0; + util.forEach(this.groups, function (group) { + group.repaint(range, margin, restack); + height += group.height; + }); this.stackDirty = false; - for (i = 0, ii = this.visibleItems.length; i < ii; i++) { - this.visibleItems[i].repositionY(); - } - - // recalculate the height of the itemset - var marginAxis = (options.margin && 'axis' in options.margin) ? options.margin.axis : this.itemOptions.margin.axis, - marginItem = (options.margin && 'item' in options.margin) ? options.margin.item : this.itemOptions.margin.item, - height; - - // determine the height from the stacked items - var visibleItems = this.visibleItems; - if (visibleItems.length) { - var min = visibleItems[0].top; - var max = visibleItems[0].top + visibleItems[0].height; - util.forEach(visibleItems, function (item) { - min = Math.min(min, item.top); - max = Math.max(max, (item.top + item.height)); - }); - height = (max - min) + marginAxis + marginItem; - } - else { - height = marginAxis + marginItem; - } // reposition frame frame.style.left = asSize(options.left, ''); @@ -457,61 +319,6 @@ ItemSet.prototype.repaint = function repaint() { return this._isResized(); }; -/** - * Update the visible items - * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date - * @param {Item[]} visibleItems The previously visible items. - * @param {{start: number, end: number}} range Visible range - * @return {Item[]} visibleItems The new visible items. - * @private - */ -ItemSet.prototype._updateVisibleItems = function _updateVisibleItems(orderedItems, visibleItems, range) { - var initialPosByStart, - newVisibleItems = [], - i; - - // first check if the items that were in view previously are still in view. - // this handles the case for the ItemRange that is both before and after the current one. - if (visibleItems.length > 0) { - for (i = 0; i < visibleItems.length; i++) { - this._checkIfVisible(visibleItems[i], newVisibleItems); - } - } - - // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime) - if (newVisibleItems.length == 0) { - initialPosByStart = this._binarySearch(orderedItems, range, false); - } - else { - initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]); - } - - // use visible search to find a visible ItemRange (only based on endTime) - var initialPosByEnd = this._binarySearch(orderedItems, range, true); - - // if we found a initial ID to use, trace it up and down until we meet an invisible item. - if (initialPosByStart != -1) { - for (i = initialPosByStart; i >= 0; i--) { - if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems)) {break;} - } - for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) { - if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems)) {break;} - } - } - - // if we found a initial ID to use, trace it up and down until we meet an invisible item. - if (initialPosByEnd != -1) { - for (i = initialPosByEnd; i >= 0; i--) { - if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems)) {break;} - } - for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) { - if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems)) {break;} - } - } - - return newVisibleItems; -}; - /** * Create or delete the group holding all ungrouped items. This group is used when * there are no groups specified. @@ -751,13 +558,14 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) { throw new TypeError('Unknown item type "' + type + '"'); } } + }); - if (type == 'range' && me.visibleItems.indexOf(item) == -1) { - me._checkIfVisible(item, me.visibleItems); - } + // reorder the items in all groups + // TODO: optimization: only reorder groups affected by the changed items + util.forEach(this.groups, function (group) { + group._order(); }); - this._order(); this.stackDirty = true; // force re-stacking of all items next repaint this.emit('change'); }; @@ -923,12 +731,8 @@ ItemSet.prototype._removeItem = function _removeItem(item) { // remove from items delete this.items[item.id]; - // remove from visible items - var index = this.visibleItems.indexOf(item); - if (index != -1) this.visibleItems.splice(index, 1); - // remove from selection - index = this.selection.indexOf(item.id); + var index = this.selection.indexOf(item.id); if (index != -1) this.selection.splice(index, 1); // remove from group @@ -937,22 +741,6 @@ ItemSet.prototype._removeItem = function _removeItem(item) { if (group) group.remove(item); }; -/** - * Order the items - * @private - */ -ItemSet.prototype._order = function _order() { - var array = util.toArray(this.items); - this.orderedItems.byStart = array; - this.orderedItems.byEnd = this._constructByEndArray(array); - - //this.orderedItems.byEnd = [].concat(array); // this copies the array - - // reorder the items - this.stack.orderByStart(this.orderedItems.byStart); - this.stack.orderByEnd(this.orderedItems.byEnd); -}; - /** * Create an array containing all items being a range (having an end date) * @param array diff --git a/src/timeline/component/css/groupset.css b/src/timeline/component/css/groupset.css index 69fb5a52..cbc501b9 100644 --- a/src/timeline/component/css/groupset.css +++ b/src/timeline/component/css/groupset.css @@ -1,6 +1,3 @@ -.vis.timeline .groupset { - position: relative; -} .vis.timeline .labelset { position: relative; @@ -24,15 +21,13 @@ } .vis.timeline.bottom .labelset .vlabel, -.vis.timeline.top .vpanel.side-content, -.vis.timeline.top .groupset .itemset { +.vis.timeline.top .vpanel.side-content { border-top: 1px solid #bfbfbf; border-bottom: none; } .vis.timeline.top .labelset .vlabel, -.vis.timeline.bottom .vpanel.side-content, -.vis.timeline.bottom .groupset .itemset { +.vis.timeline.bottom .vpanel.side-content { border-top: none; border-bottom: 1px solid #bfbfbf; } diff --git a/src/timeline/component/css/itemset.css b/src/timeline/component/css/itemset.css index f185baa4..7b75428d 100644 --- a/src/timeline/component/css/itemset.css +++ b/src/timeline/component/css/itemset.css @@ -22,3 +22,17 @@ .vis.timeline .axis { overflow: visible; } + +.vis.timeline .group { + position: relative; +} + +.vis.timeline.top .group { + border-top: 1px solid #bfbfbf; + border-bottom: none; +} + +.vis.timeline.bottom .group { + border-top: none; + border-bottom: 1px solid #bfbfbf; +} diff --git a/src/timeline/component/item/ItemBox.js b/src/timeline/component/item/ItemBox.js index dd43d4a8..463814ff 100644 --- a/src/timeline/component/item/ItemBox.js +++ b/src/timeline/component/item/ItemBox.js @@ -215,13 +215,13 @@ ItemBox.prototype.repositionY = function repositionY () { line.style.top = '0'; line.style.bottom = ''; - line.style.height = (this.top + 1) + 'px'; + line.style.height = (this.parent.top + this.top + 1) + 'px'; } else { // orientation 'bottom' box.style.top = ''; box.style.bottom = (this.top || 0) + 'px'; - line.style.top = (this.parent.getBackgroundHeight() - this.top - 1) + 'px'; + line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px'; line.style.bottom = '0'; line.style.height = ''; } diff --git a/src/timeline/stack.js b/src/timeline/stack.js new file mode 100644 index 00000000..06050f43 --- /dev/null +++ b/src/timeline/stack.js @@ -0,0 +1,96 @@ +/** + * Utility functions for ordering and stacking of items + */ +var stack = {}; + +/** + * Order items by their start data + * @param {Item[]} items + */ +stack.orderByStart = function orderByStart(items) { + items.sort(function (a, b) { + return a.data.start - b.data.start; + }); +}; + +/** + * Order items by their end date. If they have no end date, their start date + * is used. + * @param {Item[]} items + */ +stack.orderByEnd = function orderByEnd(items) { + items.sort(function (a, b) { + var aTime = ('end' in a.data) ? a.data.end : a.data.start, + bTime = ('end' in b.data) ? b.data.end : b.data.start; + + return aTime - bTime; + }); +}; + +/** + * Adjust vertical positions of the events such that they don't overlap each + * other. + * @param {Item[]} items + * All visible items + * @param {{item: number, axis: number}} margin + * Margins between items and between items and the axis. + * @param {boolean} [force=false] + * If true, all items will be re-stacked. If false (default), only + * items having a top===null will be re-stacked + */ +stack.stack = function _stack (items, margin, force) { + var i, iMax; + + if (force) { + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + items[i].top = null; + } + } + + // calculate new, non-overlapping positions + for (i = 0, iMax = items.length; i < iMax; i++) { + var item = items[i]; + if (item.top === null) { + // initialize top position + item.top = margin.axis; + + do { + // TODO: optimize checking for overlap. when there is a gap without items, + // you only need to check for items from the next item on, not from zero + var collidingItem = null; + for (var j = 0, jj = items.length; j < jj; j++) { + var other = items[j]; + if (other.top !== null && other !== item && stack.collision(item, other, margin.item)) { + collidingItem = other; + break; + } + } + + if (collidingItem != null) { + // There is a collision. Reposition the event above the colliding element + item.top = collidingItem.top + collidingItem.height + margin.item; + } + } while (collidingItem); + } + } +}; + +/** + * Test if the two provided items collide + * The items must have parameters left, width, top, and height. + * @param {Item} a The first item + * @param {Item} b The second item + * @param {Number} margin A minimum required margin. + * If margin is provided, the two items will be + * marked colliding when they overlap or + * when the margin between the two is smaller than + * the requested margin. + * @return {boolean} true if a and b collide, else false + */ +stack.collision = function collision (a, b, margin) { + return ((a.left - margin) < (b.left + b.width) && + (a.left + a.width + margin) > b.left && + (a.top - margin) < (b.top + b.height) && + (a.top + a.height + margin) > b.top); +};