Browse Source

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
fix2580
yotamberk 7 years ago
committed by Alexander Wunschik
parent
commit
7f86cadcd1
8 changed files with 265 additions and 70 deletions
  1. +7
    -0
      docs/timeline/index.html
  2. +11
    -6
      examples/timeline/groups/subgroups.html
  3. +111
    -0
      examples/timeline/items/expectedVsActualTimesItems.html
  4. +69
    -15
      lib/timeline/Stack.js
  5. +52
    -6
      lib/timeline/component/Group.js
  6. +2
    -1
      lib/timeline/component/ItemSet.js
  7. +11
    -42
      lib/timeline/component/item/BackgroundItem.js
  8. +2
    -0
      lib/timeline/optionsTimeline.js

+ 7
- 0
docs/timeline/index.html View File

@ -1015,6 +1015,13 @@ function (option, path) {
<td>If true (default), items will be stacked on top of each other such that they do not overlap.</td> <td>If true (default), items will be stacked on top of each other such that they do not overlap.</td>
</tr> </tr>
<tr>
<td>stackSubgroups</td>
<td>boolean</td>
<td><code>true</code></td>
<td>If true (default), subgroups will be stacked on top of each other such that they do not overlap.</td>
</tr>
<tr> <tr>
<td>snap</td> <td>snap</td>
<td>function or null</td> <td>function or null</td>

+ 11
- 6
examples/timeline/groups/subgroups.html View File

@ -1,7 +1,10 @@
<!DOCTYPE HTML> <!DOCTYPE HTML>
<html> <html>
<head> <head>
<title>Timeline | Background areas</title>
<title>Timeline | Subgroups</title>
<script src="../../../dist/vis.js"></script>
<link href="../../../dist/vis.css" rel="stylesheet" type="text/css" />
<style> <style>
body, html { body, html {
@ -19,14 +22,11 @@
border-left: 2px solid green; border-left: 2px solid green;
} }
</style> </style>
<script src="../../../dist/vis.js"></script>
<link href="../../../dist/vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
<script src="../../googleAnalytics.js"></script>
</head> </head>
<body> <body>
<p>This example shows the workings of the subgroups. Subgroups do not use stacking, and only work when stacking is disabled.</p> <p>This example shows the workings of the subgroups. Subgroups do not use stacking, and only work when stacking is disabled.</p>
<button onclick="toggleStackSubgroups()">Toggle stackSubgroups</button>
<div id="visualization"></div> <div id="visualization"></div>
@ -66,11 +66,16 @@
start: '2014-01-10', start: '2014-01-10',
end: '2014-02-10', end: '2014-02-10',
editable: true, editable: true,
stack: false
stack: false,
stackSubgroups: true
}; };
var timeline = new vis.Timeline(container, items, groups, options); var timeline = new vis.Timeline(container, items, groups, options);
function toggleStackSubgroups() {
options.stackSubgroups = !options.stackSubgroups;
timeline.setOptions(options);
}
</script> </script>
</body> </body>
</html> </html>

+ 111
- 0
examples/timeline/items/expectedVsActualTimesItems.html View File

@ -0,0 +1,111 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | expected vs actual times items</title>
<script src="../../../dist/vis.js"></script>
<link href="../../../dist/vis.css" rel="stylesheet" type="text/css" />
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
}
.vis-item.expected {
background-color: transparent;
border-style: dashed!important;
z-index: 0;
}
.vis-item.vis-selected {
opacity: 0.6;
}
.vis-item.vis-background.marker {
border-left: 2px solid green;
}
</style>
</head>
<body>
<div id="visualization"></div>
<script>
// create a dataset with items
// we specify the type of the fields `start` and `end` here to be strings
// containing an ISO date. The fields will be outputted as ISO dates
// automatically getting data from the DataSet via items.get().
var items = new vis.DataSet({
type: { start: 'ISODate', end: 'ISODate' }
});
var groups = new vis.DataSet([
{
id: 'group1',
content:'group1'
}
]);
// add items to the DataSet
items.add([
// group 1
{
id: 'background1',
start: '2014-01-21',
end: '2014-01-22',
type: 'background',
group:'group1'
},
// subgroup 1
{
id: 1,
content: 'item 1 (expected time)',
className: 'expected',
start: '2014-01-23 12:00:00',
end: '2014-01-26 12:00:00',
group:'group1',
subgroup:'sg_1'
},
{
id: 2,
content: 'item 1 (actual time)',
start: '2014-01-24 12:00:00',
end: '2014-01-27 12:00:00',
group:'group1',
subgroup:'sg_1'
},
// subgroup 2
{
id: 3,
content: 'item 2 (expected time)',
className: 'expected',
start: '2014-01-13 12:00:00',
end: '2014-01-16 12:00:00',
group:'group1',
subgroup:'sg_2'
},
{
id: 4,
content: 'item 2 (actual time)',
start: '2014-01-14 12:00:00',
end: '2014-01-17 12:00:00',
group:'group1',
subgroup:'sg_2'
}
]);
var container = document.getElementById('visualization');
var options = {
start: '2014-01-10',
end: '2014-02-10',
editable: true,
stack: false,
stackSubgroups: false
};
var timeline = new vis.Timeline(container, items, groups, options);
</script>
</body>
</html>

+ 69
- 15
lib/timeline/Stack.js View File

@ -37,16 +37,15 @@ exports.orderByEnd = function(items) {
* items having a top===null will be re-stacked * items having a top===null will be re-stacked
*/ */
exports.stack = function(items, margin, force) { exports.stack = function(items, margin, force) {
var i, iMax;
if (force) { if (force) {
// reset top position of all items // 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; items[i].top = null;
} }
} }
// calculate new, non-overlapping positions // 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]; var item = items[i];
if (item.stack && item.top === null) { if (item.stack && item.top === null) {
// initialize top position // initialize top position
@ -80,29 +79,70 @@ exports.stack = function(items, margin, force) {
* All visible items * All visible items
* @param {{item: {horizontal: number, vertical: number}, axis: number}} margin * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
* Margins between items and between items and the axis. * 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) { for (var subgroup in subgroups) {
if (subgroups.hasOwnProperty(subgroup)) { if (subgroups.hasOwnProperty(subgroup)) {
if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) { 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 * Test if the two provided items collide
* The items must have parameters left, width, top, and height. * 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); (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 )
)
}

+ 52
- 6
lib/timeline/component/Group.js View File

@ -88,7 +88,8 @@ Group.prototype._create = function() {
// display:none is changed to visible. // display:none is changed to visible.
this.dom.marker = document.createElement('div'); this.dom.marker = document.createElement('div');
this.dom.marker.style.visibility = 'hidden'; 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); this.dom.background.appendChild(this.dom.marker);
}; };
@ -218,7 +219,7 @@ Group.prototype.redraw = function(range, margin, restack) {
} }
// recalculate the height of the subgroups // recalculate the height of the subgroups
this._calculateSubGroupHeights();
this._calculateSubGroupHeights(margin);
// calculate actual size and position // calculate actual size and position
var foreground = this.dom.foreground; var foreground = this.dom.foreground;
@ -258,14 +259,17 @@ Group.prototype.redraw = function(range, margin, restack) {
// no custom order function, lazy stacking // no custom order function, lazy stacking
this.visibleItems = this._updateItemsInRange(this.orderedItems, this.visibleItems, range); this.visibleItems = this._updateItemsInRange(this.orderedItems, this.visibleItems, range);
if (this.itemSet.options.stack) { // TODO: ugly way to access options... if (this.itemSet.options.stack) { // TODO: ugly way to access options...
stack.stack(this.visibleItems, margin, restack); stack.stack(this.visibleItems, margin, restack);
} }
else { // no stacking 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 // recalculate the height of the group
var height = this._calculateHeight(margin); var height = this._calculateHeight(margin);
@ -304,7 +308,7 @@ Group.prototype.redraw = function(range, margin, restack) {
* recalculate the height of the subgroups * recalculate the height of the subgroups
* @private * @private
*/ */
Group.prototype._calculateSubGroupHeights = function () {
Group.prototype._calculateSubGroupHeights = function (margin) {
if (Object.keys(this.subgroups).length > 0) { if (Object.keys(this.subgroups).length > 0) {
var me = this; var me = this;
@ -312,7 +316,7 @@ Group.prototype._calculateSubGroupHeights = function () {
util.forEach(this.visibleItems, function (item) { util.forEach(this.visibleItems, function (item) {
if (item.data.subgroup !== undefined) { 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; me.subgroups[item.data.subgroup].visible = true;
} }
}); });
@ -422,9 +426,26 @@ Group.prototype.add = function(item) {
// add to // add to
if (item.data.subgroup !== undefined) { if (item.data.subgroup !== undefined) {
if (this.subgroups[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++; 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.subgroups[item.data.subgroup].items.push(item);
} }
this.orderSubgroups(); 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() { Group.prototype.orderSubgroups = function() {
if (this.subgroupOrderer !== undefined) { if (this.subgroupOrderer !== undefined) {
var sortArray = []; var sortArray = [];
@ -489,6 +533,8 @@ Group.prototype.remove = function(item) {
if (!subgroup.items.length){ if (!subgroup.items.length){
delete this.subgroups[item.data.subgroup]; delete this.subgroups[item.data.subgroup];
this.subgroupIndex--; this.subgroupIndex--;
} else {
this._updateSubgroupsSizes();
} }
this.orderSubgroups(); this.orderSubgroups();
} }

+ 2
- 1
lib/timeline/component/ItemSet.js View File

@ -34,6 +34,7 @@ function ItemSet(body, options) {
}, },
align: 'auto', // alignment of box items align: 'auto', // alignment of box items
stack: true, stack: true,
stackSubgroups: true,
groupOrderSwap: function(fromGroup, toGroup, groups) { groupOrderSwap: function(fromGroup, toGroup, groups) {
var targetOrder = toGroup.order; var targetOrder = toGroup.order;
toGroup.order = fromGroup.order; toGroup.order = fromGroup.order;
@ -322,7 +323,7 @@ ItemSet.prototype.setOptions = function(options) {
if (options) { if (options) {
// copy all options that we know // copy all options that we know
var fields = [ 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', 'multiselectPerGroup', 'groupOrder', 'dataAttributes', 'template', 'groupTemplate', 'visibleFrameTemplate',
'hide', 'snap', 'groupOrderSwap', 'tooltipOnItemUpdateTime' 'hide', 'snap', 'groupOrderSwap', 'tooltipOnItemUpdateTime'
]; ];

+ 11
- 42
lib/timeline/component/item/BackgroundItem.js View File

@ -143,9 +143,6 @@ BackgroundItem.prototype.repositionX = RangeItem.prototype.repositionX;
* @Override * @Override
*/ */
BackgroundItem.prototype.repositionY = function(margin) { 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; var height;
// special positioning for subgroups // special positioning for subgroups
@ -155,44 +152,16 @@ BackgroundItem.prototype.repositionY = function(margin) {
var itemSubgroup = this.data.subgroup; var itemSubgroup = this.data.subgroup;
var subgroups = this.parent.subgroups; var subgroups = this.parent.subgroups;
var subgroupIndex = subgroups[itemSubgroup].index; 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: // and in the case of no subgroups:
else { else {
@ -202,8 +171,8 @@ BackgroundItem.prototype.repositionY = function(margin) {
height = Math.max(this.parent.height, height = Math.max(this.parent.height,
this.parent.itemSet.body.domProps.center.height, this.parent.itemSet.body.domProps.center.height,
this.parent.itemSet.body.domProps.centerContainer.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 { else {
height = this.parent.height; height = this.parent.height;

+ 2
- 0
lib/timeline/optionsTimeline.js View File

@ -125,6 +125,7 @@ let allOptions = {
showMajorLabels: { 'boolean': bool}, showMajorLabels: { 'boolean': bool},
showMinorLabels: { 'boolean': bool}, showMinorLabels: { 'boolean': bool},
stack: { 'boolean': bool}, stack: { 'boolean': bool},
stackSubgroups: { 'boolean': bool},
snap: {'function': 'function', 'null': 'null'}, snap: {'function': 'function', 'null': 'null'},
start: {date, number, string, moment}, start: {date, number, string, moment},
template: {'function': 'function'}, template: {'function': 'function'},
@ -221,6 +222,7 @@ let configureOptions = {
showMajorLabels: true, showMajorLabels: true,
showMinorLabels: true, showMinorLabels: true,
stack: true, stack: true,
stackSubgroups: true,
//snap: {'function': 'function', nada}, //snap: {'function': 'function', nada},
start: '', start: '',
//template: {'function': 'function'}, //template: {'function': 'function'},

Loading…
Cancel
Save