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>
</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>
<td>snap</td>
<td>function or null</td>

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

@ -1,7 +1,10 @@
<!DOCTYPE HTML>
<html>
<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>
body, html {
@ -19,14 +22,11 @@
border-left: 2px solid green;
}
</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>
<body>
<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>
@ -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);
}
</script>
</body>
</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
*/
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 )
)
}

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

@ -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();
}

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

@ -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'
];

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

@ -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;

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

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

Loading…
Cancel
Save