Browse Source

Halfway large refactoring of ItemSet/GroupSet/Group

css_transitions
jos 10 years ago
parent
commit
d4e2f8c670
10 changed files with 406 additions and 422 deletions
  1. +1
    -2
      Jakefile.js
  2. +1
    -1
      src/module/exports.js
  3. +0
    -168
      src/timeline/Stack.js
  4. +269
    -11
      src/timeline/component/Group.js
  5. +2
    -0
      src/timeline/component/GroupSet.js
  6. +19
    -231
      src/timeline/component/ItemSet.js
  7. +2
    -7
      src/timeline/component/css/groupset.css
  8. +14
    -0
      src/timeline/component/css/itemset.css
  9. +2
    -2
      src/timeline/component/item/ItemBox.js
  10. +96
    -0
      src/timeline/stack.js

+ 1
- 2
Jakefile.js View File

@ -64,8 +64,8 @@ task('build', {async: true}, function () {
'./src/DataSet.js', './src/DataSet.js',
'./src/DataView.js', './src/DataView.js',
'./src/timeline/stack.js',
'./src/timeline/TimeStep.js', './src/timeline/TimeStep.js',
'./src/timeline/Stack.js',
'./src/timeline/Range.js', './src/timeline/Range.js',
'./src/timeline/component/Component.js', './src/timeline/component/Component.js',
'./src/timeline/component/Panel.js', './src/timeline/component/Panel.js',
@ -76,7 +76,6 @@ task('build', {async: true}, function () {
'./src/timeline/component/ItemSet.js', './src/timeline/component/ItemSet.js',
'./src/timeline/component/item/*.js', './src/timeline/component/item/*.js',
'./src/timeline/component/Group.js', './src/timeline/component/Group.js',
'./src/timeline/component/GroupSet.js',
'./src/timeline/Timeline.js', './src/timeline/Timeline.js',
'./src/graph/dotparser.js', './src/graph/dotparser.js',

+ 1
- 1
src/module/exports.js View File

@ -7,7 +7,7 @@ var vis = {
DataSet: DataSet, DataSet: DataSet,
DataView: DataView, DataView: DataView,
Range: Range, Range: Range,
Stack: Stack,
stack: stack,
TimeStep: TimeStep, TimeStep: TimeStep,
components: { components: {

+ 0
- 168
src/timeline/Stack.js View File

@ -1,168 +0,0 @@
// TODO: turn Stack into a Mixin?
/**
* @constructor Stack
* Stacks items on top of each other.
* @param {Object} [options]
*/
function Stack (options) {
this.options = options || {};
this.defaultOptions = {
order: function (a, b) {
// Order: ranges over non-ranges, ranged ordered by width,
// and non-ranges ordered by start.
if (a instanceof ItemRange) {
if (b instanceof ItemRange) {
var aInt = (a.data.end - a.data.start);
var bInt = (b.data.end - b.data.start);
return (aInt - bInt) || (a.data.start - b.data.start);
}
else {
return -1;
}
}
else {
if (b instanceof ItemRange) {
return 1;
}
else {
return (a.data.start - b.data.start);
}
}
},
margin: {
item: 10,
axis: 20
}
};
}
/**
* Set options for the stack
* @param {Object} options Available options:
* {Number} [margin.item=10]
* {Number} [margin.axis=20]
* {function} [order] Stacking order
*/
Stack.prototype.setOptions = function setOptions (options) {
util.extend(this.options, options);
};
/**
* Order an array with items using a predefined order function for items
* @param {Item[]} items
*/
Stack.prototype.order = function order(items) {
//order the items
var order = this.options.order || this.defaultOptions.order;
if (!(typeof order === 'function')) {
throw new Error('Option order must be a function');
}
items.sort(order);
};
/**
* Order items by their start data
* @param {Item[]} items
*/
Stack.prototype.orderByStart = function orderByStart(items) {
items.sort(function (a, b) {
return a.data.start - b.data.start;
});
};
/**
* Order items by their end date. If they have no end date, their start date
* is used.
* @param {Item[]} items
*/
Stack.prototype.orderByEnd = function orderByEnd(items) {
items.sort(function (a, b) {
var aTime = ('end' in a.data) ? a.data.end : a.data.start,
bTime = ('end' in b.data) ? b.data.end : b.data.start;
return aTime - bTime;
});
};
/**
* Adjust vertical positions of the events such that they don't overlap each
* other.
* @param {Item[]} items All visible items
* @param {boolean} [force=false] If true, all items will be re-stacked.
* If false (default), only items having a
* top===null will be re-stacked
*/
Stack.prototype.stack = function stack (items, force) {
var i,
iMax,
options = this.options,
marginItem,
marginAxis;
if (options.margin && options.margin.item !== undefined) {
marginItem = options.margin.item;
}
else {
marginItem = this.defaultOptions.margin.item
}
if (options.margin && options.margin.axis !== undefined) {
marginAxis = options.margin.axis;
}
else {
marginAxis = this.defaultOptions.margin.axis
}
if (force) {
// reset top position of all items
for (i = 0, iMax = items.length; i < iMax; i++) {
items[i].top = null;
}
}
// calculate new, non-overlapping positions
for (i = 0, iMax = items.length; i < iMax; i++) {
var item = items[i];
if (item.top === null) {
// initialize top position
item.top = marginAxis;
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 && this.collision(item, other, marginItem)) {
collidingItem = other;
break;
}
}
if (collidingItem != null) {
// There is a collision. Reposition the event above the colliding element
item.top = collidingItem.top + collidingItem.height + marginItem;
}
} while (collidingItem);
}
}
};
/**
* Test if the two provided items collide
* The items must have parameters left, width, top, and height.
* @param {Item} a The first item
* @param {Item} b The second item
* @param {Number} margin A minimum required margin.
* If margin is provided, the two items will be
* marked colliding when they overlap or
* when the margin between the two is smaller than
* the requested margin.
* @return {boolean} true if a and b collide, else false
*/
Stack.prototype.collision = function collision (a, b, margin) {
return ((a.left - margin) < (b.left + b.width) &&
(a.left + a.width + margin) > b.left &&
(a.top - margin) < (b.top + b.height) &&
(a.top + a.height + margin) > b.top);
};

+ 269
- 11
src/timeline/component/Group.js View File

@ -9,7 +9,12 @@ function Group (groupId, itemSet) {
this.itemSet = itemSet; this.itemSet = itemSet;
this.dom = {}; this.dom = {};
this.items = {}; // items filtered by groupId of this group
this.items = {}; // items filtered by groupId of this group
this.visibleItems = []; // items currently visible in window
this.orderedItems = { // items sorted by start and by end
byStart: [],
byEnd: []
};
this._create(); this._create();
} }
@ -62,19 +67,62 @@ Group.prototype.getAxis = function getAxis() {
return this.dom.axis; return this.dom.axis;
}; };
/**
* Get the height of the itemsets background
* @return {Number} height
*/
Group.prototype.getBackgroundHeight = function getBackgroundHeight() {
return this.itemSet.height;
};
/** /**
* Repaint this group * Repaint this group
* @param {{start: number, end: number}} range
* @param {number | {item: number, axis: number}} margin
* @param {boolean} [restack=false] Force restacking of all items
* @return {boolean} Returns true if the group is resized
*/ */
Group.prototype.repaint = function repaint() {
// TODO: implement Group.repaint
Group.prototype.repaint = function repaint(range, margin, restack) {
if (typeof margin === 'number') {
margin = {
item: margin,
axis: margin
};
}
// update visible items
this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
// reposition visible items vertically
stack.stack(this.visibleItems, margin, restack);
this.stackDirty = false;
for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
this.visibleItems[i].repositionY();
}
// recalculate the height of the group
var height;
// determine the height from the stacked items
var visibleItems = this.visibleItems;
if (visibleItems.length) {
var min = visibleItems[0].top;
var max = visibleItems[0].top + visibleItems[0].height;
util.forEach(visibleItems, function (item) {
min = Math.min(min, item.top);
max = Math.max(max, (item.top + item.height));
});
height = (max - min) + margin.axis + margin.item;
}
else {
height = margin.axis + margin.item;
}
var resized = (this.height != height);
// calculate actual size and position
var foreground = this.dom.foreground;
this.top = foreground.offsetTop;
this.left = foreground.offsetLeft;
this.width = foreground.offsetWidth;
this.height = height;
// apply new height
foreground.style.height = height + 'px';
return resized;
}; };
/** /**
@ -130,6 +178,11 @@ Group.prototype.hide = function hide() {
Group.prototype.add = function add(item) { Group.prototype.add = function add(item) {
this.items[item.id] = item; this.items[item.id] = item;
item.setParent(this); item.setParent(this);
if (item instanceof ItemRange && this.visibleItems.indexOf(item) == -1) {
var range = this.itemSet.range; // TODO: not nice accessing the range like this
this._checkIfVisible(item, this.visibleItems, range);
}
}; };
/** /**
@ -139,5 +192,210 @@ Group.prototype.add = function add(item) {
Group.prototype.remove = function remove(item) { Group.prototype.remove = function remove(item) {
delete this.items[item.id]; delete this.items[item.id];
item.setParent(this.itemSet); item.setParent(this.itemSet);
// remove from visible items
var index = this.visibleItems.indexOf(item);
if (index != -1) this.visibleItems.splice(index, 1);
// TODO: also remove from ordered items?
};
/**
* Order the items
* @private
*/
Group.prototype._order = function _order() {
var array = util.toArray(this.items);
this.orderedItems.byStart = array;
this.orderedItems.byEnd = this._constructByEndArray(array);
// reorder the items
stack.orderByStart(this.orderedItems.byStart);
stack.orderByEnd(this.orderedItems.byEnd);
}; };
/**
* Create an array containing all items being a range (having an end date)
* @param {Item[]} array
* @returns {ItemRange[]}
* @private
*/
Group.prototype._constructByEndArray = function _constructByEndArray(array) {
var endArray = [];
for (var i = 0; i < array.length; i++) {
if (array[i] instanceof ItemRange) {
endArray.push(array[i]);
}
}
return endArray;
};
/**
* Update the visible items
* @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
* @param {Item[]} visibleItems The previously visible items.
* @param {{start: number, end: number}} range Visible range
* @return {Item[]} visibleItems The new visible items.
* @private
*/
Group.prototype._updateVisibleItems = function _updateVisibleItems(orderedItems, visibleItems, range) {
var initialPosByStart,
newVisibleItems = [],
i;
// first check if the items that were in view previously are still in view.
// this handles the case for the ItemRange that is both before and after the current one.
if (visibleItems.length > 0) {
for (i = 0; i < visibleItems.length; i++) {
this._checkIfVisible(visibleItems[i], newVisibleItems, range);
}
}
// If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
if (newVisibleItems.length == 0) {
initialPosByStart = this._binarySearch(orderedItems, range, false);
}
else {
initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
}
// use visible search to find a visible ItemRange (only based on endTime)
var initialPosByEnd = this._binarySearch(orderedItems, range, true);
// if we found a initial ID to use, trace it up and down until we meet an invisible item.
if (initialPosByStart != -1) {
for (i = initialPosByStart; i >= 0; i--) {
if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
}
for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
}
}
// if we found a initial ID to use, trace it up and down until we meet an invisible item.
if (initialPosByEnd != -1) {
for (i = initialPosByEnd; i >= 0; i--) {
if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
}
for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
}
}
return newVisibleItems;
};
/**
* This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
* arrays. This is done by giving a boolean value true if you want to use the byEnd.
* This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check
* if the time we selected (start or end) is within the current range).
*
* The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is
* before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest,
* either the start OR end time has to be in the range.
*
* @param {{byStart: Item[], byEnd: Item[]}} orderedItems
* @param {{start: number, end: number}} range
* @param {Boolean} byEnd
* @returns {number}
* @private
*/
Group.prototype._binarySearch = function _binarySearch(orderedItems, range, byEnd) {
var array = [];
var byTime = byEnd ? 'end' : 'start';
if (byEnd == true) {array = orderedItems.byEnd; }
else {array = orderedItems.byStart;}
var interval = range.end - range.start;
var found = false;
var low = 0;
var high = array.length;
var guess = Math.floor(0.5*(high+low));
var newGuess;
if (high == 0) {guess = -1;}
else if (high == 1) {
if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
guess = 0;
}
else {
guess = -1;
}
}
else {
high -= 1;
while (found == false) {
if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
found = true;
}
else {
if (array[guess].data[byTime] < range.start - interval) { // it is too small --> increase low
low = Math.floor(0.5*(high+low));
}
else { // it is too big --> decrease high
high = Math.floor(0.5*(high+low));
}
newGuess = Math.floor(0.5*(high+low));
// not in list;
if (guess == newGuess) {
guess = -1;
found = true;
}
else {
guess = newGuess;
}
}
}
}
return guess;
};
/**
* this function checks if an item is invisible. If it is NOT we make it visible
* and add it to the global visible items. If it is, return true.
*
* @param {Item} item
* @param {Item[]} visibleItems
* @param {{start:number, end:number}} range
* @returns {boolean}
* @private
*/
Group.prototype._checkIfInvisible = function _checkIfInvisible(item, visibleItems, range) {
if (item.isVisible(range)) {
if (!item.displayed) item.show();
item.repositionX();
if (visibleItems.indexOf(item) == -1) {
visibleItems.push(item);
}
return false;
}
else {
return true;
}
};
/**
* this function is very similar to the _checkIfInvisible() but it does not
* return booleans, hides the item if it should not be seen and always adds to
* the visibleItems.
* this one is for brute forcing and hiding.
*
* @param {Item} item
* @param {Array} visibleItems
* @param {{start:number, end:number}} range
* @private
*/
Group.prototype._checkIfVisible = function _checkIfVisible(item, visibleItems, range) {
if (item.isVisible(range)) {
if (!item.displayed) item.show();
// reposition item horizontally
item.repositionX();
visibleItems.push(item);
}
else {
if (item.displayed) item.hide();
}
};

+ 2
- 0
src/timeline/component/GroupSet.js View File

@ -1,3 +1,5 @@
// TODO: remove groupset
/** /**
* An GroupSet holds a set of groups * An GroupSet holds a set of groups
* @param {Panel} contentPanel Panel where the ItemSets will be created * @param {Panel} contentPanel Panel where the ItemSets will be created

+ 19
- 231
src/timeline/component/ItemSet.js View File

@ -61,18 +61,11 @@ function ItemSet(backgroundPanel, axisPanel, labelPanel, options) {
} }
}; };
this.items = {}; // object with an Item for every data item
this.orderedItems = {
byStart: [],
byEnd: []
};
this.groups = {}; // Group object for every group
this.items = {}; // object with an Item for every data item
this.groups = {}; // Group object for every group
this.groupIds = []; this.groupIds = [];
this.visibleItems = []; // visible, ordered items
this.selection = []; // list with the ids of all selected nodes this.selection = []; // list with the ids of all selected nodes
this.stack = new Stack(Object.create(this.options));
this.stackDirty = true; // if true, all items will be restacked on next repaint this.stackDirty = true; // if true, all items will be restacked on next repaint
this.touchParams = {}; // stores properties while dragging this.touchParams = {}; // stores properties while dragging
@ -269,127 +262,18 @@ ItemSet.prototype.getFrame = function getFrame() {
return this.frame; return this.frame;
}; };
/**
* This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
* arrays. This is done by giving a boolean value true if you want to use the byEnd.
* This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check
* if the time we selected (start or end) is within the current range).
*
* The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is
* before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest,
* either the start OR end time has to be in the range.
*
* @param {{byStart: Item[], byEnd: Item[]}} orderedItems
* @param {{start: number, end: number}} range
* @param {Boolean} byEnd
* @returns {number}
* @private
*/
ItemSet.prototype._binarySearch = function _binarySearch(orderedItems, range, byEnd) {
var array = [];
var byTime = byEnd ? "end" : "start";
if (byEnd == true) {array = orderedItems.byEnd; }
else {array = orderedItems.byStart;}
var interval = range.end - range.start;
var found = false;
var low = 0;
var high = array.length;
var guess = Math.floor(0.5*(high+low));
var newGuess;
if (high == 0) {guess = -1;}
else if (high == 1) {
if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
guess = 0;
}
else {
guess = -1;
}
}
else {
high -= 1;
while (found == false) {
if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
found = true;
}
else {
if (array[guess].data[byTime] < range.start - interval) { // it is too small --> increase low
low = Math.floor(0.5*(high+low));
}
else { // it is too big --> decrease high
high = Math.floor(0.5*(high+low));
}
newGuess = Math.floor(0.5*(high+low));
// not in list;
if (guess == newGuess) {
guess = -1;
found = true;
}
else {
guess = newGuess;
}
}
}
}
return guess;
};
/**
* this function checks if an item is invisible. If it is NOT we make it visible and add it to the global visible items. If it is, return true.
*
* @param {Item} item
* @param {Item[]} visibleItems
* @returns {boolean}
* @private
*/
ItemSet.prototype._checkIfInvisible = function _checkIfInvisible(item, visibleItems) {
if (item.isVisible(this.range)) {
if (!item.displayed) item.show();
item.repositionX();
if (visibleItems.indexOf(item) == -1) {
visibleItems.push(item);
}
return false;
}
else {
return true;
}
};
/**
* this function is very similar to the _checkIfInvisible() but it does not return booleans, hides the item if it should not be seen and always adds to the visibleItems.
* this one is for brute forcing and hiding.
*
* @param {Item} item
* @param {Array} visibleItems
* @private
*/
ItemSet.prototype._checkIfVisible = function _checkIfVisible(item, visibleItems) {
if (item.isVisible(this.range)) {
if (!item.displayed) item.show();
// reposition item horizontally
item.repositionX();
visibleItems.push(item);
}
else {
if (item.displayed) item.hide();
}
};
/** /**
* Repaint the component * Repaint the component
* @return {boolean} Returns true if the component is resized * @return {boolean} Returns true if the component is resized
*/ */
ItemSet.prototype.repaint = function repaint() { ItemSet.prototype.repaint = function repaint() {
var asSize = util.option.asSize,
var margin = this.options.margin,
range = this.range,
asSize = util.option.asSize,
asString = util.option.asString, asString = util.option.asString,
options = this.options, options = this.options,
orientation = this.getOption('orientation'), orientation = this.getOption('orientation'),
frame = this.frame,
i, ii;
frame = this.frame;
// update className // update className
frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : ''); frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
@ -400,36 +284,14 @@ ItemSet.prototype.repaint = function repaint() {
this.lastVisibleInterval = visibleInterval; this.lastVisibleInterval = visibleInterval;
this.lastWidth = this.width; this.lastWidth = this.width;
this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, this.range);
// reposition visible items vertically.
//this.stack.order(this.visibleItems); // TODO: improve ordering
var force = this.stackDirty || zoomed; // force re-stacking of all items if true
this.stack.stack(this.visibleItems, force);
// repaint all groups
var restack = zoomed || this.stackDirty;
var height = 0;
util.forEach(this.groups, function (group) {
group.repaint(range, margin, restack);
height += group.height;
});
this.stackDirty = false; this.stackDirty = false;
for (i = 0, ii = this.visibleItems.length; i < ii; i++) {
this.visibleItems[i].repositionY();
}
// recalculate the height of the itemset
var marginAxis = (options.margin && 'axis' in options.margin) ? options.margin.axis : this.itemOptions.margin.axis,
marginItem = (options.margin && 'item' in options.margin) ? options.margin.item : this.itemOptions.margin.item,
height;
// determine the height from the stacked items
var visibleItems = this.visibleItems;
if (visibleItems.length) {
var min = visibleItems[0].top;
var max = visibleItems[0].top + visibleItems[0].height;
util.forEach(visibleItems, function (item) {
min = Math.min(min, item.top);
max = Math.max(max, (item.top + item.height));
});
height = (max - min) + marginAxis + marginItem;
}
else {
height = marginAxis + marginItem;
}
// reposition frame // reposition frame
frame.style.left = asSize(options.left, ''); frame.style.left = asSize(options.left, '');
@ -457,61 +319,6 @@ ItemSet.prototype.repaint = function repaint() {
return this._isResized(); return this._isResized();
}; };
/**
* Update the visible items
* @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
* @param {Item[]} visibleItems The previously visible items.
* @param {{start: number, end: number}} range Visible range
* @return {Item[]} visibleItems The new visible items.
* @private
*/
ItemSet.prototype._updateVisibleItems = function _updateVisibleItems(orderedItems, visibleItems, range) {
var initialPosByStart,
newVisibleItems = [],
i;
// first check if the items that were in view previously are still in view.
// this handles the case for the ItemRange that is both before and after the current one.
if (visibleItems.length > 0) {
for (i = 0; i < visibleItems.length; i++) {
this._checkIfVisible(visibleItems[i], newVisibleItems);
}
}
// If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
if (newVisibleItems.length == 0) {
initialPosByStart = this._binarySearch(orderedItems, range, false);
}
else {
initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
}
// use visible search to find a visible ItemRange (only based on endTime)
var initialPosByEnd = this._binarySearch(orderedItems, range, true);
// if we found a initial ID to use, trace it up and down until we meet an invisible item.
if (initialPosByStart != -1) {
for (i = initialPosByStart; i >= 0; i--) {
if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems)) {break;}
}
for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems)) {break;}
}
}
// if we found a initial ID to use, trace it up and down until we meet an invisible item.
if (initialPosByEnd != -1) {
for (i = initialPosByEnd; i >= 0; i--) {
if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems)) {break;}
}
for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems)) {break;}
}
}
return newVisibleItems;
};
/** /**
* Create or delete the group holding all ungrouped items. This group is used when * Create or delete the group holding all ungrouped items. This group is used when
* there are no groups specified. * there are no groups specified.
@ -751,13 +558,14 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) {
throw new TypeError('Unknown item type "' + type + '"'); throw new TypeError('Unknown item type "' + type + '"');
} }
} }
});
if (type == 'range' && me.visibleItems.indexOf(item) == -1) {
me._checkIfVisible(item, me.visibleItems);
}
// reorder the items in all groups
// TODO: optimization: only reorder groups affected by the changed items
util.forEach(this.groups, function (group) {
group._order();
}); });
this._order();
this.stackDirty = true; // force re-stacking of all items next repaint this.stackDirty = true; // force re-stacking of all items next repaint
this.emit('change'); this.emit('change');
}; };
@ -923,12 +731,8 @@ ItemSet.prototype._removeItem = function _removeItem(item) {
// remove from items // remove from items
delete this.items[item.id]; delete this.items[item.id];
// remove from visible items
var index = this.visibleItems.indexOf(item);
if (index != -1) this.visibleItems.splice(index, 1);
// remove from selection // remove from selection
index = this.selection.indexOf(item.id);
var index = this.selection.indexOf(item.id);
if (index != -1) this.selection.splice(index, 1); if (index != -1) this.selection.splice(index, 1);
// remove from group // remove from group
@ -937,22 +741,6 @@ ItemSet.prototype._removeItem = function _removeItem(item) {
if (group) group.remove(item); if (group) group.remove(item);
}; };
/**
* Order the items
* @private
*/
ItemSet.prototype._order = function _order() {
var array = util.toArray(this.items);
this.orderedItems.byStart = array;
this.orderedItems.byEnd = this._constructByEndArray(array);
//this.orderedItems.byEnd = [].concat(array); // this copies the array
// reorder the items
this.stack.orderByStart(this.orderedItems.byStart);
this.stack.orderByEnd(this.orderedItems.byEnd);
};
/** /**
* Create an array containing all items being a range (having an end date) * Create an array containing all items being a range (having an end date)
* @param array * @param array

+ 2
- 7
src/timeline/component/css/groupset.css View File

@ -1,6 +1,3 @@
.vis.timeline .groupset {
position: relative;
}
.vis.timeline .labelset { .vis.timeline .labelset {
position: relative; position: relative;
@ -24,15 +21,13 @@
} }
.vis.timeline.bottom .labelset .vlabel, .vis.timeline.bottom .labelset .vlabel,
.vis.timeline.top .vpanel.side-content,
.vis.timeline.top .groupset .itemset {
.vis.timeline.top .vpanel.side-content {
border-top: 1px solid #bfbfbf; border-top: 1px solid #bfbfbf;
border-bottom: none; border-bottom: none;
} }
.vis.timeline.top .labelset .vlabel, .vis.timeline.top .labelset .vlabel,
.vis.timeline.bottom .vpanel.side-content,
.vis.timeline.bottom .groupset .itemset {
.vis.timeline.bottom .vpanel.side-content {
border-top: none; border-top: none;
border-bottom: 1px solid #bfbfbf; border-bottom: 1px solid #bfbfbf;
} }

+ 14
- 0
src/timeline/component/css/itemset.css View File

@ -22,3 +22,17 @@
.vis.timeline .axis { .vis.timeline .axis {
overflow: visible; overflow: visible;
} }
.vis.timeline .group {
position: relative;
}
.vis.timeline.top .group {
border-top: 1px solid #bfbfbf;
border-bottom: none;
}
.vis.timeline.bottom .group {
border-top: none;
border-bottom: 1px solid #bfbfbf;
}

+ 2
- 2
src/timeline/component/item/ItemBox.js View File

@ -215,13 +215,13 @@ ItemBox.prototype.repositionY = function repositionY () {
line.style.top = '0'; line.style.top = '0';
line.style.bottom = ''; line.style.bottom = '';
line.style.height = (this.top + 1) + 'px';
line.style.height = (this.parent.top + this.top + 1) + 'px';
} }
else { // orientation 'bottom' else { // orientation 'bottom'
box.style.top = ''; box.style.top = '';
box.style.bottom = (this.top || 0) + 'px'; box.style.bottom = (this.top || 0) + 'px';
line.style.top = (this.parent.getBackgroundHeight() - this.top - 1) + 'px';
line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px';
line.style.bottom = '0'; line.style.bottom = '0';
line.style.height = ''; line.style.height = '';
} }

+ 96
- 0
src/timeline/stack.js View File

@ -0,0 +1,96 @@
/**
* Utility functions for ordering and stacking of items
*/
var stack = {};
/**
* Order items by their start data
* @param {Item[]} items
*/
stack.orderByStart = function orderByStart(items) {
items.sort(function (a, b) {
return a.data.start - b.data.start;
});
};
/**
* Order items by their end date. If they have no end date, their start date
* is used.
* @param {Item[]} items
*/
stack.orderByEnd = function orderByEnd(items) {
items.sort(function (a, b) {
var aTime = ('end' in a.data) ? a.data.end : a.data.start,
bTime = ('end' in b.data) ? b.data.end : b.data.start;
return aTime - bTime;
});
};
/**
* Adjust vertical positions of the events such that they don't overlap each
* other.
* @param {Item[]} items
* All visible items
* @param {{item: number, axis: number}} margin
* Margins between items and between items and the axis.
* @param {boolean} [force=false]
* If true, all items will be re-stacked. If false (default), only
* items having a top===null will be re-stacked
*/
stack.stack = function _stack (items, margin, force) {
var i, iMax;
if (force) {
// reset top position of all items
for (i = 0, iMax = items.length; i < iMax; i++) {
items[i].top = null;
}
}
// calculate new, non-overlapping positions
for (i = 0, iMax = items.length; i < iMax; i++) {
var item = items[i];
if (item.top === null) {
// initialize top position
item.top = margin.axis;
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 && stack.collision(item, other, margin.item)) {
collidingItem = other;
break;
}
}
if (collidingItem != null) {
// There is a collision. Reposition the event above the colliding element
item.top = collidingItem.top + collidingItem.height + margin.item;
}
} while (collidingItem);
}
}
};
/**
* Test if the two provided items collide
* The items must have parameters left, width, top, and height.
* @param {Item} a The first item
* @param {Item} b The second item
* @param {Number} margin A minimum required margin.
* If margin is provided, the two items will be
* marked colliding when they overlap or
* when the margin between the two is smaller than
* the requested margin.
* @return {boolean} true if a and b collide, else false
*/
stack.collision = function collision (a, b, margin) {
return ((a.left - margin) < (b.left + b.width) &&
(a.left + a.width + margin) > b.left &&
(a.top - margin) < (b.top + b.height) &&
(a.top + a.height + margin) > b.top);
};

Loading…
Cancel
Save