Browse Source

Subgroup stacking (#3501)

* - Added support for stacking items within individual subgroups while subgroupStack is on

* - Adjusted location of visibility check to cover subgroup stacking

* - Fixing linting issues

* - Updated subgroup stacking to optionally take a 'subgroupStack' parameter of "true", which enables stacking in all subgroups
- Fixed code to meet style guidelines
- Updated documentation
jittering-top
Ian Oberst 7 years ago
committed by Yotam Berkowitz
parent
commit
9218b01b29
4 changed files with 269 additions and 21 deletions
  1. +10
    -0
      docs/timeline/index.html
  2. +87
    -7
      examples/timeline/groups/subgroups.html
  3. +109
    -0
      lib/timeline/Stack.js
  4. +63
    -14
      lib/timeline/component/Group.js

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

@ -454,6 +454,16 @@ var groups = [
By default, groups are ordered by first-come, first-show.
</td>
</tr>
<tr>
<td>subgroupStack</td>
<td>Object or Boolean</td>
<td>none</td>
<td>Enables stacking within individual subgroups. Example: <code>{'subgroup0': true, 'subgroup1': false, 'subgroup2': true}</code>
For each subgroup where stacking is enabled, items will be stacked on top of each other within that subgroup such that they do no overlap.
If set to <code>true</code> all subgroups will be stacked.
If a value was specified for the <code>order</code> parameter in the options, that ordering will be used when stacking the items.
</td>
</tr>
<tr>
<td>title</td>
<td>String</td>

+ 87
- 7
examples/timeline/groups/subgroups.html View File

@ -21,12 +21,68 @@
.vis-item.vis-background.marker {
border-left: 2px solid green;
}
table {
border: 1px solid gray;
}
td {
text-align: center
}
code {
padding: 2px 4px;
font-size: 90%;
color: #c7254e;
background-color: #f9f2f4;
border-radius: 4px;
}
</style>
</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>
<p>This example shows the workings of the subgroups. Subgroups can be stacked on each other, and the items within each subgroup can be stacked.</p>
<p>When stacking is on for the whole timeline, all items in the timeline will be stacked with respect to each other <em>unless</em> the <code>stackSubgroups</code> option is set to <code>true</code>
and at least one subgroup has stacking enabled. In that case the subgroups will be stacked with respect to each other and the elements in each subgroup will be stacked based on the individual
stacking settings for each subgroup.
</p>
<table>
<thead>
<tr>
<th>Option</th>
<th>Status</th>
<th>Toggle</th>
</tr>
</thead>
<tbody>
<tr >
<td>Stacking</td>
<td id="stackingStatus">false</td>
<td><button onclick="toggleStacking()">Toggle</button></td>
</tr>
<tr>
<td>stackSubgroups</td>
<td id="stackSubgroupsStatus">true</td>
<td><button onclick="toggleStackSubgroups()">Toggle</button></td>
</tr>
<tr>
<td>Stack Subgroup 0</td>
<td id="stacksg_1">false</td>
<td><button onclick="toggleSubgroupStack('sg_1')">Toggle</button></td>
</tr>
<tr>
<td>Stack Subgroup 1</td>
<td id="stacksg_2">false</td>
<td><button onclick="toggleSubgroupStack('sg_2')">Toggle</button></td>
</tr>
<tr>
<td>Stack Subgroup 2</td>
<td id="stacksg_3">false</td>
<td><button onclick="toggleSubgroupStack('sg_3')">Toggle</button></td>
</tr>
</tbody>
</table>
<br/>
<div id="visualization"></div>
@ -39,7 +95,7 @@
type: { start: 'ISODate', end: 'ISODate' }
});
var groups = new vis.DataSet([{
id: 'bar', content:'bar', subgroupOrder: function (a,b) {return a.subgroupOrder - b.subgroupOrder;}
id: 'bar', content:'bar', subgroupOrder: function (a,b) {return a.subgroupOrder - b.subgroupOrder;}, subgroupStack: {'sg_1': false, 'sg_2': false, 'sg_3': false }
},{
id: 'foo', content:'foo', subgroupOrder: 'subgroupOrder' // this group has no subgroups but this would be the other method to do the sorting.
}]);
@ -51,16 +107,26 @@
{id: 'SG_1_1',start: '2014-01-25', end: '2014-01-27', type: 'background', group:'bar', subgroup:'sg_1', subgroupOrder:0},
{id: 'SG_1_2', start: '2014-01-26', end: '2014-01-27', type: 'background', className: 'positive',group:'bar', subgroup:'sg_1', subgroupOrder:0},
{id: 1, content: 'subgroup0', start: '2014-01-23T12:00:00', end: '2014-01-26T12:00:00',group:'bar', subgroup:'sg_1', subgroupOrder:0},
{id: 'SG_2_1', start: '2014-01-27', end: '2014-01-29', type: 'background', group:'bar', subgroup:'sg_2', subgroupOrder:1},
{id: 'SG_2_2', start: '2014-01-27', end: '2014-01-28', type: 'background', className: 'negative',group:'bar', subgroup:'sg_2', subgroupOrder:1},
{id: 2, content: 'subgroup1', start: '2014-01-27', end: '2014-01-29',group:'bar', subgroup:'sg_2', subgroupOrder:1},
{id: 1, content: 'subgroup0_1', start: '2014-01-23T12:00:00', end: '2014-01-26T12:00:00',group:'bar', subgroup:'sg_1', subgroupOrder:0},
{id: 2, content: 'subgroup0_2', start: '2014-01-22T12:00:01', end: '2014-01-25T12:00:00',group:'bar', subgroup:'sg_1', subgroupOrder:0},
{id: 'SG_2_1', start: '2014-02-01', end: '2014-02-02', type: 'background', group:'bar', subgroup:'sg_2', subgroupOrder:1},
{id: 'SG_2_2', start: '2014-02-2', end: '2014-02-03', type: 'background', className: 'negative',group:'bar', subgroup:'sg_2', subgroupOrder:1},
{id: 3, content: 'subgroup1_1', start: '2014-01-27T02:00:00', end: '2014-01-29',group:'bar', subgroup:'sg_2', subgroupOrder:1},
{id: 4, content: 'subgroup1_2', start: '2014-01-28', end: '2014-02-02',group:'bar', subgroup:'sg_2', subgroupOrder:1},
{id: 'SG_3_1',start: '2014-01-23', end: '2014-01-25', type: 'background', group:'bar', subgroup:'sg_3', subgroupOrder:2, content:"a"},
{id: 'SG_3_2', start: '2014-01-26', end: '2014-01-28', type: 'background', className: 'positive',group:'bar', subgroup:'sg_3', subgroupOrder:2, content:"b"},
{id: 5, content: 'subgroup2_1', start: '2014-01-23T12:00:00', end: '2014-01-26T12:00:00',group:'bar', subgroup:'sg_3', subgroupOrder:2},
{id: 6, content: 'subgroup2_2', start: '2014-01-26T12:00:01', end: '2014-01-29T12:00:00',group:'bar', subgroup:'sg_3', subgroupOrder:2},
{id: 'background', start: '2014-01-29', end: '2014-01-30', type: 'background', className: 'negative',group:'bar'},
{id: 'background_all', start: '2014-01-31', end: '2014-02-02', type: 'background', className: 'positive'},
]);
var container = document.getElementById('visualization');
var stackingStatus = document.getElementById('stackingStatus');
var stackSubgroupsStatus = document.getElementById('stackSubgroupsStatus');
var options = {
// orientation:'top'
start: '2014-01-10',
@ -72,10 +138,24 @@
var timeline = new vis.Timeline(container, items, groups, options);
function toggleStacking() {
options.stack = !options.stack;
stackingStatus.innerHTML = options.stack.toString();
timeline.setOptions(options);
}
function toggleStackSubgroups() {
options.stackSubgroups = !options.stackSubgroups;
stackSubgroupsStatus.innerHTML = options.stackSubgroups.toString();
timeline.setOptions(options);
}
function toggleSubgroupStack(subgroup) {
groups.get("bar").subgroupStack[subgroup] = !groups.get("bar").subgroupStack[subgroup];
document.getElementById('stack' + subgroup).innerHTML = groups.get("bar").subgroupStack[subgroup].toString();
timeline.setGroups(groups);
}
</script>
</body>
</html>

+ 109
- 0
lib/timeline/Stack.js View File

@ -72,6 +72,59 @@ exports.stack = function(items, margin, force) {
}
};
/**
* Adjust vertical positions of the items within a single subgroup such that they
* don't overlap each other.
* @param {Item[]} items
* All items withina subgroup
* @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
* Margins between items and between items and the axis.
* @param {subgroup} subgroup
* The subgroup that is being stacked
*/
exports.substack = function (items, margin, subgroup) {
for (var i = 0; i < items.length; i++) {
items[i].top = null;
}
// Set the initial height
var subgroupHeight = subgroup.height;
// calculate new, non-overlapping positions
for (i = 0; i < items.length; i++) {
var item = items[i];
if (item.stack && item.top === null) {
// initialize top position
item.top = item.baseTop;//margin.axis + item.baseTop;
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 j = 0, jj = items.length; j < jj; j++) {
var other = items[j];
if (other.top !== null && other !== item /*&& other.stack*/ && exports.collision(item, other, margin.item, other.options.rtl)) {
collidingItem = other;
break;
}
}
if (collidingItem != null) {
// There is a collision. Reposition the items above the colliding element
item.top = collidingItem.top + collidingItem.height + margin.item.vertical;// + item.baseTop;
}
if (item.top + item.height > subgroupHeight) {
subgroupHeight = item.top + item.height;
}
} while (collidingItem);
}
}
// Set the new height
subgroup.height = subgroupHeight - subgroup.top + 0.5 * margin.item.vertical;
};
/**
* Adjust vertical positions of the items without stacking them
@ -144,6 +197,62 @@ exports.stackSubgroups = function(items, margin, subgroups) {
}
};
/**
* Adjust vertical positions of the subgroups such that they don't overlap each
* other, then stacks the contents of each subgroup individually.
* @param {Item[]} subgroupItems
* All the items in a subgroup
* @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
* Margins between items and between items and the axis.
* @param {subgroups[]} subgroups
* All subgroups
*/
exports.stackSubgroupsWithInnerStack = function (subgroupItems, margin, subgroups) {
var doSubStack = false;
// Run subgroups in their order (if any)
var subgroupOrder = [];
for(var subgroup in subgroups) {
if (subgroups[subgroup].hasOwnProperty("index")) {
subgroupOrder[subgroups[subgroup].index] = subgroup;
}
else {
subgroupOrder.push(subgroup);
}
}
for(var j = 0; j < subgroupOrder.length; j++) {
subgroup = subgroupOrder[j];
if (subgroups.hasOwnProperty(subgroup)) {
doSubStack = doSubStack || subgroups[subgroup].stack;
subgroups[subgroup].top = 0;
for (var otherSubgroup in subgroups) {
if (subgroups[otherSubgroup].visible && subgroups[subgroup].index > subgroups[otherSubgroup].index) {
subgroups[subgroup].top += subgroups[otherSubgroup].height;
}
}
var items = subgroupItems[subgroup];
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;
if (subgroups[subgroup].stack) {
items[i].baseTop = items[i].top;
}
}
}
if (doSubStack && subgroups[subgroup].stack) {
exports.substack(subgroupItems[subgroup], margin, subgroups[subgroup]);
}
}
}
};
/**
* Test if the two provided items collide
* The items must have parameters left, width, top, and height.

+ 63
- 14
lib/timeline/component/Group.js View File

@ -10,6 +10,9 @@ var stack = require('../Stack');
function Group (groupId, data, itemSet) {
this.groupId = groupId;
this.subgroups = {};
this.subgroupStack = {};
this.subgroupStackAll = false;
this.doInnerStack = false;
this.subgroupIndex = 0;
this.subgroupOrderer = data && data.subgroupOrder;
this.itemSet = itemSet;
@ -25,6 +28,21 @@ function Group (groupId, data, itemSet) {
}
}
if (data && data.subgroupStack) {
if (typeof data.subgroupStack === "boolean") {
this.doInnerStack = data.subgroupStack;
this.subgroupStackAll = data.subgroupStack;
}
else {
// We might be doing stacking on specific sub groups, but only
// if at least one is set to do stacking
for(var key in data.subgroupStack) {
this.subgroupStack[key] = data.subgroupStack[key];
this.doInnerStack = this.doInnerStack || data.subgroupStack[key];
}
}
}
this.nestedInGroup = null;
this.dom = {};
@ -249,6 +267,9 @@ Group.prototype._redrawItems = function(forceRestack, lastIsVisible, margin, ran
// if restacking, reposition visible items vertically
if (restack) {
var visibleSubgroups = {};
var subgroup = null;
if (typeof this.itemSet.options.order === 'function') {
// a custom order function
// brute force restack of all items
@ -283,19 +304,41 @@ Group.prototype._redrawItems = function(forceRestack, lastIsVisible, margin, ran
this.items[i].repositionX(limitSize);
}
// order all items and force a restacking
var customOrderedItems = this.orderedItems.byStart.slice().sort(function (a, b) {
return me.itemSet.options.order(a.data, b.data);
});
stack.stack(customOrderedItems, margin, true /* restack=true */);
if (this.doInnerStack && this.itemSet.options.stackSubgroups) {
// Order the items within each subgroup
for(subgroup in this.subgroups) {
visibleSubgroups[subgroup] = this.subgroups[subgroup].items.slice().sort(function (a, b) {
return me.itemSet.options.order(a.data, b.data);
});
}
stack.stackSubgroupsWithInnerStack(visibleSubgroups, margin, this.subgroups);
}
else {
// order all items and force a restacking
var customOrderedItems = this.orderedItems.byStart.slice().sort(function (a, b) {
return me.itemSet.options.order(a.data, b.data);
});
stack.stack(customOrderedItems, margin, true /* restack=true */);
}
this.visibleItems = this._updateItemsInRange(this.orderedItems, this.visibleItems, range);
} else {
// 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, true /* restack=true */);
if (this.doInnerStack && this.itemSet.options.stackSubgroups) {
for(subgroup in this.subgroups) {
visibleSubgroups[subgroup] = this.subgroups[subgroup].items;
}
stack.stackSubgroupsWithInnerStack(visibleSubgroups, margin, this.subgroups);
}
else {
// TODO: ugly way to access options...
stack.stack(this.visibleItems, margin, true /* restack=true */);
}
} else {
// no stacking
stack.nostack(this.visibleItems, margin, this.subgroups, this.itemSet.options.stackSubgroups);
@ -557,10 +600,11 @@ Group.prototype._addToSubgroup = function(item, subgroupId) {
height:0,
top: 0,
start: item.data.start,
end: item.data.end,
end: item.data.end || item.data.start,
visible: false,
index:this.subgroupIndex,
items: []
items: [],
stack: this.subgroupStackAll || this.subgroupStack[subgroupId] || false
};
this.subgroupIndex++;
}
@ -569,8 +613,10 @@ Group.prototype._addToSubgroup = function(item, subgroupId) {
if (new Date(item.data.start) < new Date(this.subgroups[subgroupId].start)) {
this.subgroups[subgroupId].start = item.data.start;
}
if (new Date(item.data.end) > new Date(this.subgroups[subgroupId].end)) {
this.subgroups[subgroupId].end = item.data.end;
var itemEnd = item.data.end || item.data.start;
if (new Date(itemEnd) > new Date(this.subgroups[subgroupId].end)) {
this.subgroups[subgroupId].end = itemEnd;
}
this.subgroups[subgroupId].items.push(item);
@ -581,15 +627,18 @@ Group.prototype._updateSubgroupsSizes = function () {
var me = this;
if (me.subgroups) {
for (var subgroup in me.subgroups) {
var initialEnd = me.subgroups[subgroup].items[0].data.end || me.subgroups[subgroup].items[0].data.start;
var newStart = me.subgroups[subgroup].items[0].data.start;
var newEnd = me.subgroups[subgroup].items[0].data.end - 1;
var newEnd = initialEnd - 1;
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;
var itemEnd = item.data.end || item.data.start;
if (new Date(itemEnd) > new Date(newEnd)) {
newEnd = itemEnd;
}
})

Loading…
Cancel
Save