From 9218b01b2914b2602bda0e93e3c2f6fe57b533e2 Mon Sep 17 00:00:00 2001 From: Ian Oberst Date: Sat, 30 Sep 2017 08:29:32 -0700 Subject: [PATCH] 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 --- docs/timeline/index.html | 10 +++ examples/timeline/groups/subgroups.html | 94 ++++++++++++++++++-- lib/timeline/Stack.js | 109 ++++++++++++++++++++++++ lib/timeline/component/Group.js | 77 ++++++++++++++--- 4 files changed, 269 insertions(+), 21 deletions(-) diff --git a/docs/timeline/index.html b/docs/timeline/index.html index b46fcde8..9e7c4d81 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -454,6 +454,16 @@ var groups = [ By default, groups are ordered by first-come, first-show. + + subgroupStack + Object or Boolean + none + Enables stacking within individual subgroups. Example: {'subgroup0': true, 'subgroup1': false, 'subgroup2': true} + 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 true all subgroups will be stacked. + If a value was specified for the order parameter in the options, that ordering will be used when stacking the items. + + title String diff --git a/examples/timeline/groups/subgroups.html b/examples/timeline/groups/subgroups.html index 4647d191..e17db9b3 100644 --- a/examples/timeline/groups/subgroups.html +++ b/examples/timeline/groups/subgroups.html @@ -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; + } -

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

- +

This example shows the workings of the subgroups. Subgroups can be stacked on each other, and the items within each subgroup can be stacked.

+

When stacking is on for the whole timeline, all items in the timeline will be stacked with respect to each other unless the stackSubgroups option is set to true + 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. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionStatusToggle
Stackingfalse
stackSubgroupstrue
Stack Subgroup 0false
Stack Subgroup 1false
Stack Subgroup 2false
+
@@ -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); + } + diff --git a/lib/timeline/Stack.js b/lib/timeline/Stack.js index 0aac7b73..b25179b4 100644 --- a/lib/timeline/Stack.js +++ b/lib/timeline/Stack.js @@ -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. diff --git a/lib/timeline/component/Group.js b/lib/timeline/component/Group.js index 734e0d15..cbbcbee6 100644 --- a/lib/timeline/component/Group.js +++ b/lib/timeline/component/Group.js @@ -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; } })