Skip to content

Commit

Permalink
Vertical focus (almende#3504)
Browse files Browse the repository at this point in the history
* - Added support for vertical scrolling while the timeline is focusing on an element, both for animated and non-animated calls of focus

* - Adjusted item offset calculations to use the item parent
- Turned on animation for the focus in the example
- Updated function documentation

* - Fixing lint issues

* - Added documentation for the new 'frameCallback' parameter of 'setRange'
- Fixed the documentation on 'setRange' for the 'callback' parameter
- Fixed code not meeting style guidelines

* - Updated the example for "setSelection" to be more clear about what the example buttons do. Focus the language to be more consistent with that fact that the demo uses "setSelection"
  • Loading branch information
Areson authored and Primoz Susa committed Jan 3, 2019
1 parent 5f5d285 commit 360fd99
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 5 deletions.
108 changes: 108 additions & 0 deletions examples/timeline/interaction/setSelection.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ <h1>Set selection</h1>
</p>
<div id="visualization"></div>

<br/>
<p>If the height of the timeline is limited some items may be vertically offscreen. This demo uses <code>Timeline.setSelection(ids, {focus: true})</code> and demonstrates that focusing on an item will
cause the timeline to scroll vertically to the item that is being focused on. You can use the buttons below select a random item either above or below the currently selected item.
</p>
<button id="prevFocus">Select Item Above</button>
<button id="nextFocus">Select Item Below</button>
<br/>

<p>If focusing on multiple items only the first item will be scrolled to. Try entering several ids and hitting <em>select</em>.</p>
<p>
Select item(s): <input type="text" id="selectionVertical" value="g1_5, g2_3"><input type="button" id="selectVertical" value="Select"><br>
</p>

<div id="vertical-visualization"></div>

<script>
// create a dataset with items
// we specify the type of the fields `start` and `end` here to be strings
Expand Down Expand Up @@ -61,6 +76,99 @@ <h1>Set selection</h1>
});
timeline.setSelection(ids, {focus: focus.checked});
};

function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive
};

// Vertical scroll example
var groups = [];
var items = [];
var groupItems = {};

for (var g = 0; g < 10; g++) {
groups.push({
id: g,
content: "Group " + g
});

groupItems[g] = [];

for (var i = 0; i < 30; i++) {
items.push({
id: "g" + g + "_" + i,
content: "g" + g + "_" + i,
group: g,
start: "2014-" + (g + 1) + "-" + getRandomInt(1, 20)
});

groupItems[g].push(items[items.length - 1]);
}
}

var container2 = document.getElementById('vertical-visualization');
var options = {
editable: false,
stack: true,
height: 300,
verticalScroll: true,
groupOrder: 'id'
};

var timeline2 = new vis.Timeline(container2, items, groups, options);

var groupIndex = 0;
var itemIndex = 0;

var moveToItem = function(direction) {
itemIndex += direction;
groupIndex += direction;

if (groupIndex < 0) {
groupIndex = groups.length - 1;
} else if (groupIndex >= groups.length) {
groupIndex = 0;
}

var items = groupItems[groupIndex];

if (itemIndex < 0) {
itemIndex = items.length - 1;
} else if (itemIndex >= items.length) {
itemIndex = 0;
}

var id = items[itemIndex].id;

timeline2.setSelection(id, {focus: true});
}

var nextFocus = document.getElementById('nextFocus');
var prevFocus = document.getElementById('prevFocus');
var selectionVertical = document.getElementById('selectionVertical');
var selectVertical = document.getElementById('selectVertical');

selectVertical.onclick = function () {
var ids = selectionVertical.value.split(',').map(function (value) {
return value.trim();
});
timeline2.setSelection(ids, {focus: focus.checked});
};

nextFocus.onclick = function() {
moveToItem(1);
};

prevFocus.onclick = function() {
moveToItem(-1);
};

// Set the initial focus
setTimeout(function() {
moveToItem(0);
}, 500);
</script>
</body>
</html>
14 changes: 10 additions & 4 deletions lib/timeline/Range.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,15 @@ Range.prototype.stopRolling = function() {
* function is 'easeInOutQuad'.
* {boolean} [byUser=false]
* {Event} event Mouse event
* {Function} a callback funtion to be executed at the end of this function
*
* @param {Function} callback a callback function to be executed at the end of this function
* @param {Function} frameCallback a callback function executed each frame of the range animation.
* The callback will be passed three parameters:
* {number} easeCoefficient an easing coefficent
* {boolean} willDraw If true the caller will redraw after the callback completes
* {boolean} done If true then animation is ending after the current frame
*/

Range.prototype.setRange = function(start, end, options, callback) {
Range.prototype.setRange = function(start, end, options, callback, frameCallback) {
if (!options) {
options = {};
}
Expand Down Expand Up @@ -238,7 +242,9 @@ Range.prototype.setRange = function(start, end, options, callback) {
event: options.event
};

if (changed) {
if (frameCallback) { frameCallback(ease, changed, done); }

if (changed) {
me.body.emitter.emit('rangechange', params);
}

Expand Down
101 changes: 100 additions & 1 deletion lib/timeline/Timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -392,13 +392,68 @@ Timeline.prototype.focus = function(id, options) {
}
});


if (start !== null && end !== null) {
var me = this;
// Use the first item for the vertical focus
var item = this.itemSet.items[ids[0]];
var startPos = this._getScrollTop() * -1;
var initialVerticalScroll = null;

// Setup a handler for each frame of the vertical scroll
var verticalAnimationFrame = function(ease, willDraw, done) {
var verticalScroll = getItemVerticalScroll(me, item);

if(!initialVerticalScroll) {
initialVerticalScroll = verticalScroll;
}

if(initialVerticalScroll.itemTop == verticalScroll.itemTop && !initialVerticalScroll.shouldScroll) {
return; // We don't need to scroll, so do nothing
}
else if(initialVerticalScroll.itemTop != verticalScroll.itemTop && verticalScroll.shouldScroll) {
// The redraw shifted elements, so reset the animation to correct
initialVerticalScroll = verticalScroll;
startPos = me._getScrollTop() * -1;
}

var from = startPos;
var to = initialVerticalScroll.scrollOffset;
var scrollTop = done ? to : (from + (to - from) * ease);

me._setScrollTop(-scrollTop);

if(!willDraw) {
me._redraw();
}
};

// Perform one last check at the end to make sure the final vertical
// position is correct
var finalVerticalCallback = function() {
var finalVerticalScroll = getItemVerticalScroll(me, item);

if(finalVerticalScroll.shouldScroll && finalVerticalScroll.itemTop != initialVerticalScroll.itemTop) {
me._setScrollTop(-finalVerticalScroll.scrollOffset);
me._redraw();
}
};

// calculate the new middle and interval for the window
var middle = (start + end) / 2;
var interval = Math.max((this.range.end - this.range.start), (end - start) * 1.1);

var animation = (options && options.animation !== undefined) ? options.animation : true;
this.range.setRange(middle - interval / 2, middle + interval / 2, { animation: animation });

if(!animation) {
// We aren't animating so set a default so that the final callback forces the vertical location
initialVerticalScroll = {shouldScroll: false, scrollOffset: -1, itemTop: -1};
}

this.range.setRange(middle - interval / 2, middle + interval / 2, { animation: animation }, finalVerticalCallback, verticalAnimationFrame);

// Let the redraw settle and finalize the position
setTimeout(finalVerticalCallback, 100);
}
};

Expand Down Expand Up @@ -448,6 +503,50 @@ function getEnd(item) {
return util.convert(end, 'Date').valueOf();
}

/**
* @param {vis.Timeline} timeline
* @param {vis.Item} item
* @return {{shouldScroll: bool, scrollOffset: number, itemTop: number}}
*/
function getItemVerticalScroll(timeline, item) {
var leftHeight = timeline.props.leftContainer.height;
var contentHeight = timeline.props.left.height;

var group = item.parent;
var offset = group.top;
var shouldScroll = true;
var orientation = timeline.timeAxis.options.orientation.axis;

var itemTop = function () {
if (orientation == "bottom") {
return group.height - item.top - item.height;
}
else {
return item.top;
}
};

var currentScrollHeight = timeline._getScrollTop() * -1;
var targetOffset = offset + itemTop();
var height = item.height;

if (targetOffset < currentScrollHeight) {
if (offset + leftHeight <= offset + itemTop() + height) {
offset += itemTop() - timeline.itemSet.options.margin.item.vertical;
}
}
else if (targetOffset + height > currentScrollHeight + leftHeight) {
offset += itemTop() + height - leftHeight + timeline.itemSet.options.margin.item.vertical;
}
else {
shouldScroll = false;
}

offset = Math.min(offset, contentHeight - leftHeight);

return { shouldScroll: shouldScroll, scrollOffset: offset, itemTop: targetOffset };
}

/**
* Determine the range of the items, taking into account their actual width
* and a margin of 10 pixels on both sides.
Expand Down

0 comments on commit 360fd99

Please sign in to comment.