From 62c22f604f4761141303c6957186afc4cd10f5db Mon Sep 17 00:00:00 2001 From: josdejong Date: Wed, 5 Mar 2014 12:14:09 +0100 Subject: [PATCH] Stacking starts to work --- examples/timeline/03_much_data.html | 2 +- src/timeline/Stack.js | 60 +++++++++++++++----------- src/timeline/component/ItemSet.js | 34 +++++++++------ src/timeline/component/css/item.css | 3 ++ src/timeline/component/item/Item.js | 25 ++++++++--- src/timeline/component/item/ItemBox.js | 60 +++++++++++++++----------- 6 files changed, 114 insertions(+), 70 deletions(-) diff --git a/examples/timeline/03_much_data.html b/examples/timeline/03_much_data.html index dcbdf3e1..b4339928 100644 --- a/examples/timeline/03_much_data.html +++ b/examples/timeline/03_much_data.html @@ -22,7 +22,7 @@

- +

diff --git a/src/timeline/Stack.js b/src/timeline/Stack.js index 66df4435..961e3ea6 100644 --- a/src/timeline/Stack.js +++ b/src/timeline/Stack.js @@ -175,17 +175,19 @@ Stack.prototype._stack = function _stack (items) { }; /** - * Stack an item on top of given set of items - * @param {Item} item - * @param {Item[]} items + * Adjust vertical positions of the events such that they don't overlap each + * other. + * @param {Item[]} items All visible items * @private */ -Stack.prototype.stack = function stack (item, items) { - var options = this.options, +Stack.prototype.stack = function stack (items) { + var i, + iMax, + options = this.options, orientation = options.orientation || this.defaultOptions.orientation, axisOnTop = (orientation == 'top'), margin, - parentHeight = this.itemset.height; // TODO: should use the height of the itemsets parent + parentHeight = this.itemset.height; if (options.margin && options.margin.item !== undefined) { margin = options.margin.item; @@ -194,30 +196,36 @@ Stack.prototype.stack = function stack (item, items) { margin = this.defaultOptions.margin.item } - // initialize top position - if (orientation == 'top') { - item.top = margin; - } - else { - // default or 'bottom' - item.top = parentHeight - item.height - 2 * margin; - } - - // calculate new, non-overlapping position - 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 = this.checkOverlap(item, items, margin); - if (collidingItem != null) { - // There is a collision. Reposition the event above the colliding element + // 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 if (axisOnTop) { - item.top = collidingItem.top + collidingItem.height + margin; + item.top = margin; } else { - item.top = collidingItem.top - item.height - margin; + // default or 'bottom' + item.top = parentHeight - item.height - 2 * margin; } + + var collidingItem; + do { + // TODO: optimize checking for overlap. when there is a gap without items, + // you only need to check for items from the next item on, not from zero + collidingItem = this.checkOverlap (item, items, margin); + if (collidingItem != null) { + // There is a collision. Reposition the event above the colliding element + if (axisOnTop) { + item.top = collidingItem.top + collidingItem.height + margin; + } + else { + item.top = collidingItem.top - item.height - margin; + } + } + } while (collidingItem); } - } while (collidingItem); + } }; /** @@ -235,7 +243,7 @@ Stack.prototype.stack = function stack (item, items) { Stack.prototype.checkOverlap = function checkOverlap (item, items, margin) { for (var i = 0, ii = items.length; i < ii; i++) { var b = items[i]; - if (b !== item && b.top !== null && this.collision(item, b, margin)) { + if (b.top !== null && b !== item && this.collision(item, b, margin)) { return b; } } diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index c010e74e..c8028004 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -232,6 +232,8 @@ ItemSet.prototype.repaint = function repaint() { orientation = this.getOption('orientation'), frame = this.frame; + this._updateConversion(); + if (!frame) { frame = document.createElement('div'); frame.className = 'itemset'; @@ -296,8 +298,6 @@ ItemSet.prototype.repaint = function repaint() { changed += update(this.dom.axis.style, 'top', this.top + 'px'); } - this._updateConversion(); - // find start of visible items var start = this.visibleItemsStart; var item = this.orderedItems[start]; @@ -333,18 +333,27 @@ ItemSet.prototype.repaint = function repaint() { this.visibleItems = this.orderedItems.slice(start, end); + // check whether zoomed (in that case we need to re-stack everything) + var visibleInterval = this.range.end - this.range.start; + var zoomed = this.visibleInterval != visibleInterval; + this.visibleInterval = visibleInterval; + // show visible items - for (var i = start; i < end; i++) { - var item = this.orderedItems[i]; + for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { + var item = this.visibleItems[i]; + if (!item.displayed) item.show(); - item.top = null; // TODO: do not re-stack every time, only on scroll + if (zoomed) item.top = null; // reset stacking position + + // reposition item horizontally + item.repositionX(); } - // reposition visible items - for (var i = start; i < end; i++) { - var item = this.orderedItems[i]; - this.stack.stack(item, this.visibleItems); - item.reposition(); + // reposition visible items vertically + // TODO: improve stacking, when moving the timeline to the right, update stacking in backward order + this.stack.stack(this.visibleItems); + for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { + this.visibleItems[i].repositionY(); } return false; @@ -409,9 +418,8 @@ ItemSet.prototype.reflow = function reflow () { } else { // height is not specified, determine the height from the height and positioned items - var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items - //if (visibleItems.length) { // TODO: calculate max height again - if (false) { + var visibleItems = this.visibleItems; // TODO: not so nice way to get the filtered items + if (visibleItems.length) { // TODO: calculate max height again var min = visibleItems[0].top; var max = visibleItems[0].top + visibleItems[0].height; util.forEach(visibleItems, function (item) { diff --git a/src/timeline/component/css/item.css b/src/timeline/component/css/item.css index 1242f9d1..1a99bbc7 100644 --- a/src/timeline/component/css/item.css +++ b/src/timeline/component/css/item.css @@ -6,6 +6,9 @@ background-color: #D5DDF6; display: inline-block; padding: 5px; + + -webkit-transition: top .4s, height .4s, background-color .4s, -webkit-transform .4s ease-in-out; + transition: top .4s, height .4s, background-color .4s, transform .4s ease-in-out; } .vis.timeline .item.selected { diff --git a/src/timeline/component/item/Item.js b/src/timeline/component/item/Item.js index 4b9faf45..1acc0d8b 100644 --- a/src/timeline/component/item/Item.js +++ b/src/timeline/component/item/Item.js @@ -16,11 +16,11 @@ function Item (parent, data, options, defaultOptions) { this.selected = false; this.visible = false; - this.top = 0; - this.left = 0; - this.width = 0; - this.height = 0; - this.offset = 0; + this.top = null; + this.left = null; + this.width = null; + this.height = null; + this.offset = 0; // TODO: is offset still used or redundant? } /** @@ -73,10 +73,25 @@ Item.prototype.reflow = function reflow() { return false; }; +/** + * Reposition the Item horizontally + */ +Item.prototype.repositionX = function repositionX() { + // should be implemented by the item +}; + +/** + * Reposition the Item vertically + */ +Item.prototype.repositionY = function repositionY() { + // should be implemented by the item +}; + /** * Give the item a display offset in pixels * @param {Number} offset Offset on screen in pixels */ +// TODO: is setOffset redundant? Item.prototype.setOffset = function setOffset(offset) { this.offset = offset; }; diff --git a/src/timeline/component/item/ItemBox.js b/src/timeline/component/item/ItemBox.js index 6f464e38..0a3ba3a0 100644 --- a/src/timeline/component/item/ItemBox.js +++ b/src/timeline/component/item/ItemBox.js @@ -170,9 +170,9 @@ ItemBox.prototype.show = function show() { * Hide the item from the DOM (when visible) */ ItemBox.prototype.hide = function hide() { - var dom = this.dom; - if (this.displayed) { + var dom = this.dom; + if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box); if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line); if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot); @@ -181,27 +181,22 @@ ItemBox.prototype.hide = function hide() { } this.top = null; + this.left = null; }; /** - * Reposition the item, recalculate its left, top, and width, using the current - * range and size of the items ItemSet - * @override + * Reposition the item horizontally + * @Override */ -ItemBox.prototype.reposition = function reposition() { - var dom = this.dom, - props = this.props, - options = this.options, - start = this.parent.toScreen(this.data.start) + this.offset, - align = options.align || this.defaultOptions.align, - orientation = this.options.orientation || this.defaultOptions.orientation, - left; - - var box = dom.box, - line = dom.line, - dot = dom.dot; - - // calculate left and top position of the box +ItemBox.prototype.repositionX = function repositionX() { + var start = this.parent.toScreen(this.data.start) + this.offset, + align = this.options.align || this.defaultOptions.align, + left, + box = this.dom.box, + line = this.dom.line, + dot = this.dom.dot; + + // calculate left position of the box if (align == 'right') { this.left = start - this.width; } @@ -213,14 +208,30 @@ ItemBox.prototype.reposition = function reposition() { this.left = start - this.width / 2; } - // NOTE: this.top is determined when stacking items - // reposition box box.style.left = this.left + 'px'; + + // reposition line + line.style.left = (start - this.props.line.width / 2) + 'px'; + + // reposition dot + dot.style.left = (start - this.props.dot.width / 2) + 'px'; +}; + +/** + * Reposition the item vertically + * @Override + */ +ItemBox.prototype.repositionY = function repositionY () { + var orientation = this.options.orientation || this.defaultOptions.orientation, + box = this.dom.box, + line = this.dom.line, + dot = this.dom.dot; + + // reposition box box.style.top = (this.top || 0) + 'px'; // reposition line - line.style.left = (start - props.line.width / 2) + 'px'; if (orientation == 'top') { line.style.top = 0 + 'px'; line.style.height = this.top + 'px'; @@ -233,6 +244,5 @@ ItemBox.prototype.reposition = function reposition() { } // reposition dot - dot.style.left = (start - props.dot.width / 2) + 'px'; - dot.style.top = (-props.dot.height / 2) + 'px'; -}; + dot.style.top = (-this.props.dot.height / 2) + 'px'; +}