From 7f86cadcd12bb2230862ad30a53fb3c028fab7d0 Mon Sep 17 00:00:00 2001 From: yotamberk Date: Sat, 31 Dec 2016 20:39:36 +0200 Subject: [PATCH] feat: new stackSubgroups option (#2519) * Fix redraw order * Fix error when option is not defined * Allow template labels * Fix boolean types bug * Add subgroup stacking * Almost finish subgroup stacking * Play with examples for subgroup order * Fix stacked subgroups * Fix subgroup stacking * Add stackSubgroups option * Fix example * Add docs * Fix onRemove item subgroups recalculate * Return subgroup example and add stackSubgroup example * Split stackSubgroup example to subgroup/html and expectedVsActualTimesItems.html --- docs/timeline/index.html | 7 ++ examples/timeline/groups/subgroups.html | 17 ++- .../items/expectedVsActualTimesItems.html | 111 ++++++++++++++++++ lib/timeline/Stack.js | 84 ++++++++++--- lib/timeline/component/Group.js | 58 ++++++++- lib/timeline/component/ItemSet.js | 3 +- lib/timeline/component/item/BackgroundItem.js | 53 ++------- lib/timeline/optionsTimeline.js | 2 + 8 files changed, 265 insertions(+), 70 deletions(-) create mode 100644 examples/timeline/items/expectedVsActualTimesItems.html diff --git a/docs/timeline/index.html b/docs/timeline/index.html index f536ecc1..5c9c02f4 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -1015,6 +1015,13 @@ function (option, path) { If true (default), items will be stacked on top of each other such that they do not overlap. + + stackSubgroups + boolean + true + If true (default), subgroups will be stacked on top of each other such that they do not overlap. + + snap function or null diff --git a/examples/timeline/groups/subgroups.html b/examples/timeline/groups/subgroups.html index 0056705d..da2670ed 100644 --- a/examples/timeline/groups/subgroups.html +++ b/examples/timeline/groups/subgroups.html @@ -1,7 +1,10 @@ - Timeline | Background areas + Timeline | Subgroups + + + - - - -

This example shows the workings of the subgroups. Subgroups do not use stacking, and only work when stacking is disabled.

+
@@ -66,11 +66,16 @@ start: '2014-01-10', end: '2014-02-10', editable: true, - stack: false + stack: false, + stackSubgroups: true }; var timeline = new vis.Timeline(container, items, groups, options); + function toggleStackSubgroups() { + options.stackSubgroups = !options.stackSubgroups; + timeline.setOptions(options); + } \ No newline at end of file diff --git a/examples/timeline/items/expectedVsActualTimesItems.html b/examples/timeline/items/expectedVsActualTimesItems.html new file mode 100644 index 00000000..8ef427cc --- /dev/null +++ b/examples/timeline/items/expectedVsActualTimesItems.html @@ -0,0 +1,111 @@ + + + + Timeline | expected vs actual times items + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/lib/timeline/Stack.js b/lib/timeline/Stack.js index fa8a47e9..ab921893 100644 --- a/lib/timeline/Stack.js +++ b/lib/timeline/Stack.js @@ -37,16 +37,15 @@ exports.orderByEnd = function(items) { * items having a top===null will be re-stacked */ exports.stack = function(items, margin, force) { - var i, iMax; if (force) { // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { + for (var i = 0; i < items.length; i++) { items[i].top = null; } } // calculate new, non-overlapping positions - for (i = 0, iMax = items.length; i < iMax; i++) { + for (var i = 0; i < items.length; i++) { var item = items[i]; if (item.stack && item.top === null) { // initialize top position @@ -80,29 +79,70 @@ exports.stack = function(items, margin, force) { * All visible items * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin * Margins between items and between items and the axis. +* @param {subgroups[]} subgroups + * All subgroups */ -exports.nostack = function(items, margin, subgroups) { - var i, iMax, newTop; - - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - if (items[i].data.subgroup !== undefined) { - newTop = margin.axis; + exports.nostack = function(items, margin, subgroups, stackSubgroups) { + for (var i = 0; i < items.length; i++) { + if (items[i].data.subgroup == undefined) { + items[i].top = margin.item.vertical; + } else if (items[i].data.subgroup !== undefined && stackSubgroups) { + var newTop = 0; for (var subgroup in subgroups) { if (subgroups.hasOwnProperty(subgroup)) { if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) { - newTop += subgroups[subgroup].height + margin.item.vertical; + newTop += subgroups[subgroup].height; + subgroups[items[i].data.subgroup].top = newTop; } } } - items[i].top = newTop; - } - else { - items[i].top = margin.axis; + items[i].top = newTop + 0.5 * margin.item.vertical; } } + if (!stackSubgroups) { + exports.stackSubgroups(items, margin, subgroups) + } }; +/** + * Adjust vertical positions of the subgroups such that they don't overlap each + * other. + * @param {subgroups[]} subgroups + * All subgroups + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * Margins between items and between items and the axis. + */ +exports.stackSubgroups = function(items, margin, subgroups) { + for (var subgroup in subgroups) { + if (subgroups.hasOwnProperty(subgroup)) { + + + subgroups[subgroup].top = 0; + 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 otherSubgroup in subgroups) { + if (subgroups[otherSubgroup].top !== null && otherSubgroup !== subgroup && subgroups[subgroup].index > subgroups[otherSubgroup].index && exports.collisionByTimes(subgroups[subgroup], subgroups[otherSubgroup])) { + collidingItem = subgroups[otherSubgroup]; + break; + } + } + + if (collidingItem != null) { + // There is a collision. Reposition the subgroups above the colliding element + subgroups[subgroup].top = collidingItem.top + collidingItem.height; + } + } while (collidingItem); + } + } + for (var i = 0; i < items.length; i++) { + if (items[i].data.subgroup !== undefined) { + items[i].top = subgroups[items[i].data.subgroup].top + 0.5 * margin.item.vertical; + } + } +} + /** * Test if the two provided items collide * The items must have parameters left, width, top, and height. @@ -127,3 +167,17 @@ exports.collision = function(a, b, margin, rtl) { (a.top + a.height + margin.vertical - EPSILON) > b.top); } }; + +/** + * Test if the two provided objects collide + * The objects must have parameters start, end, top, and height. + * @param {Object} a The first Object + * @param {Object} b The second Object + * @return {boolean} true if a and b collide, else false + */ +exports.collisionByTimes = function(a, b) { + return ( + (a.start < b.start && a.end > b.start && a.top < (b.top + b.height) && (a.top + a.height) > b.top ) || + (b.start < a.start && b.end > a.start && b.top < (a.top + a.height) && (b.top + b.height) > a.top ) + ) +} \ No newline at end of file diff --git a/lib/timeline/component/Group.js b/lib/timeline/component/Group.js index b2190f43..883d8580 100644 --- a/lib/timeline/component/Group.js +++ b/lib/timeline/component/Group.js @@ -88,7 +88,8 @@ Group.prototype._create = function() { // display:none is changed to visible. this.dom.marker = document.createElement('div'); this.dom.marker.style.visibility = 'hidden'; - this.dom.marker.innerHTML = '?'; + this.dom.marker.style.position = 'absolute'; + this.dom.marker.innerHTML = ''; this.dom.background.appendChild(this.dom.marker); }; @@ -218,7 +219,7 @@ Group.prototype.redraw = function(range, margin, restack) { } // recalculate the height of the subgroups - this._calculateSubGroupHeights(); + this._calculateSubGroupHeights(margin); // calculate actual size and position var foreground = this.dom.foreground; @@ -258,14 +259,17 @@ Group.prototype.redraw = function(range, margin, restack) { // no custom order function, lazy stacking this.visibleItems = this._updateItemsInRange(this.orderedItems, this.visibleItems, range); + if (this.itemSet.options.stack) { // TODO: ugly way to access options... stack.stack(this.visibleItems, margin, restack); } else { // no stacking - stack.nostack(this.visibleItems, margin, this.subgroups); + stack.nostack(this.visibleItems, margin, this.subgroups, this.itemSet.options.stackSubgroups); } } + this._updateSubgroupsSizes(); + // recalculate the height of the group var height = this._calculateHeight(margin); @@ -304,7 +308,7 @@ Group.prototype.redraw = function(range, margin, restack) { * recalculate the height of the subgroups * @private */ -Group.prototype._calculateSubGroupHeights = function () { +Group.prototype._calculateSubGroupHeights = function (margin) { if (Object.keys(this.subgroups).length > 0) { var me = this; @@ -312,7 +316,7 @@ Group.prototype._calculateSubGroupHeights = function () { util.forEach(this.visibleItems, function (item) { if (item.data.subgroup !== undefined) { - me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height, item.height); + me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height, item.height + margin.item.vertical); me.subgroups[item.data.subgroup].visible = true; } }); @@ -422,9 +426,26 @@ Group.prototype.add = function(item) { // add to if (item.data.subgroup !== undefined) { if (this.subgroups[item.data.subgroup] === undefined) { - this.subgroups[item.data.subgroup] = {height:0, visible: false, index:this.subgroupIndex, items: []}; + this.subgroups[item.data.subgroup] = { + height:0, + top: 0, + start: item.data.start, + end: item.data.end, + visible: false, + index:this.subgroupIndex, + items: [] + }; this.subgroupIndex++; } + + + if (new Date(item.data.start) < new Date(this.subgroups[item.data.subgroup].start)) { + this.subgroups[item.data.subgroup].start = item.data.start; + } + if (new Date(item.data.end) > new Date(this.subgroups[item.data.subgroup].end)) { + this.subgroups[item.data.subgroup].end = item.data.end; + } + this.subgroups[item.data.subgroup].items.push(item); } this.orderSubgroups(); @@ -435,6 +456,29 @@ Group.prototype.add = function(item) { } }; +Group.prototype._updateSubgroupsSizes = function () { + var me = this; + if (me.subgroups) { + for (var subgroup in me.subgroups) { + var newStart = me.subgroups[subgroup].items[0].data.start; + var newEnd = me.subgroups[subgroup].items[0].data.end; + + me.subgroups[subgroup].items.forEach(function(item) { + if (new Date(item.data.start) < new Date(newStart)) { + newStart = item.data.start; + } + if (new Date(item.data.end) > new Date(newEnd)) { + newEnd = item.data.end; + } + }) + + me.subgroups[subgroup].start = newStart; + me.subgroups[subgroup].end = newEnd; + + } + } +} + Group.prototype.orderSubgroups = function() { if (this.subgroupOrderer !== undefined) { var sortArray = []; @@ -489,6 +533,8 @@ Group.prototype.remove = function(item) { if (!subgroup.items.length){ delete this.subgroups[item.data.subgroup]; this.subgroupIndex--; + } else { + this._updateSubgroupsSizes(); } this.orderSubgroups(); } diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index e76eb49d..16e7c48d 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -34,6 +34,7 @@ function ItemSet(body, options) { }, align: 'auto', // alignment of box items stack: true, + stackSubgroups: true, groupOrderSwap: function(fromGroup, toGroup, groups) { var targetOrder = toGroup.order; toGroup.order = fromGroup.order; @@ -322,7 +323,7 @@ ItemSet.prototype.setOptions = function(options) { if (options) { // copy all options that we know var fields = [ - 'type', 'rtl', 'align', 'order', 'stack', 'selectable', 'multiselect', 'itemsAlwaysDraggable', + 'type', 'rtl', 'align', 'order', 'stack', 'stackSubgroups', 'selectable', 'multiselect', 'itemsAlwaysDraggable', 'multiselectPerGroup', 'groupOrder', 'dataAttributes', 'template', 'groupTemplate', 'visibleFrameTemplate', 'hide', 'snap', 'groupOrderSwap', 'tooltipOnItemUpdateTime' ]; diff --git a/lib/timeline/component/item/BackgroundItem.js b/lib/timeline/component/item/BackgroundItem.js index 8a1c8859..6d6ce456 100644 --- a/lib/timeline/component/item/BackgroundItem.js +++ b/lib/timeline/component/item/BackgroundItem.js @@ -143,9 +143,6 @@ BackgroundItem.prototype.repositionX = RangeItem.prototype.repositionX; * @Override */ BackgroundItem.prototype.repositionY = function(margin) { - var onTop = this.options.orientation.item === 'top'; - this.dom.content.style.top = onTop ? '' : '0'; - this.dom.content.style.bottom = onTop ? '0' : ''; var height; // special positioning for subgroups @@ -155,44 +152,16 @@ BackgroundItem.prototype.repositionY = function(margin) { var itemSubgroup = this.data.subgroup; var subgroups = this.parent.subgroups; var subgroupIndex = subgroups[itemSubgroup].index; - // if the orientation is top, we need to take the difference in height into account. - if (onTop == true) { - // the first subgroup will have to account for the distance from the top to the first item. - height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; - height += subgroupIndex == 0 ? margin.axis - 0.5*margin.item.vertical : 0; - var newTop = this.parent.top; - for (var subgroup in subgroups) { - if (subgroups.hasOwnProperty(subgroup)) { - if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroupIndex) { - newTop += subgroups[subgroup].height + margin.item.vertical; - } - } - } - - // the others will have to be offset downwards with this same distance. - newTop += subgroupIndex != 0 ? margin.axis - 0.5 * margin.item.vertical : 0; - this.dom.box.style.top = newTop + 'px'; - this.dom.box.style.bottom = ''; - } - // and when the orientation is bottom: - else { - var newTop = this.parent.top; - var totalHeight = 0; - for (var subgroup in subgroups) { - if (subgroups.hasOwnProperty(subgroup)) { - if (subgroups[subgroup].visible == true) { - var newHeight = subgroups[subgroup].height + margin.item.vertical; - totalHeight += newHeight; - if (subgroups[subgroup].index > subgroupIndex) { - newTop += newHeight; - } - } - } - } - height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; - this.dom.box.style.top = (this.parent.height - totalHeight + newTop) + 'px'; - this.dom.box.style.bottom = ''; + + this.dom.box.style.height = this.parent.subgroups[itemSubgroup].height + 'px'; + + var orientation = this.options.orientation.item; + if (orientation == 'top') { + this.dom.box.style.top = this.parent.top + this.parent.subgroups[itemSubgroup].top + 'px'; + } else { + this.dom.box.style.top = (this.parent.top + this.parent.height - this.parent.subgroups[itemSubgroup].top - this.parent.subgroups[itemSubgroup].height) + 'px'; } + this.dom.box.style.bottom = ''; } // and in the case of no subgroups: else { @@ -202,8 +171,8 @@ BackgroundItem.prototype.repositionY = function(margin) { height = Math.max(this.parent.height, this.parent.itemSet.body.domProps.center.height, this.parent.itemSet.body.domProps.centerContainer.height); - this.dom.box.style.top = onTop ? '0' : ''; - this.dom.box.style.bottom = onTop ? '' : '0'; + this.dom.box.style.top = orientation == 'top' ? '0' : ''; + this.dom.box.style.bottom = orientation == 'top' ? '' : '0'; } else { height = this.parent.height; diff --git a/lib/timeline/optionsTimeline.js b/lib/timeline/optionsTimeline.js index 3833f7a6..517a0e00 100644 --- a/lib/timeline/optionsTimeline.js +++ b/lib/timeline/optionsTimeline.js @@ -125,6 +125,7 @@ let allOptions = { showMajorLabels: { 'boolean': bool}, showMinorLabels: { 'boolean': bool}, stack: { 'boolean': bool}, + stackSubgroups: { 'boolean': bool}, snap: {'function': 'function', 'null': 'null'}, start: {date, number, string, moment}, template: {'function': 'function'}, @@ -221,6 +222,7 @@ let configureOptions = { showMajorLabels: true, showMinorLabels: true, stack: true, + stackSubgroups: true, //snap: {'function': 'function', nada}, start: '', //template: {'function': 'function'},