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'},