Browse Source

Performance boost. updated itemSet.repaint(). Now only ranged items have to be bruteforced ONCE.

css_transitions
Alex de Mulder 10 years ago
parent
commit
d9bdeef916
5 changed files with 592 additions and 216 deletions
  1. +373
    -148
      dist/vis.js
  2. +10
    -10
      dist/vis.min.js
  3. +22
    -7
      examples/timeline/01_basic.html
  4. +1
    -1
      examples/timeline/03_much_data.html
  5. +186
    -50
      src/timeline/component/ItemSet.js

+ 373
- 148
dist/vis.js View File

@ -4,8 +4,8 @@
*
* A dynamic, browser-based visualization library.
*
* @version 0.7.5-SNAPSHOT
* @date 2014-04-23
* @version @@version
* @date @@date
*
* @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
@ -4772,16 +4772,14 @@ function ItemSet(backgroundPanel, axisPanel, options) {
byStart: [],
byEnd: []
};
this.systemLoaded = false;
this.visibleItems = []; // visible, ordered items
this.visibleItemsStart = 0; // start index of visible items in this.orderedItems // TODO: cleanup
this.visibleItemsEnd = 0; // start index of visible items in this.orderedItems // TODO: cleanup
this.selection = []; // list with the ids of all selected nodes
this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
this.stack = new Stack(Object.create(this.options));
this.stackDirty = true; // if true, all items will be restacked on next repaint
this.touchParams = {}; // stores properties while dragging
// create the HTML DOM
this._create();
}
@ -4971,6 +4969,71 @@ ItemSet.prototype.getFrame = function getFrame() {
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 {Boolean} byEnd
* @returns {number}
* @private
*/
ItemSet.prototype._binarySearch = function _binarySearch(byEnd) {
var array = []
var byTime = byEnd ? "end" : "start";
if (byEnd == true) {array = this.orderedItems.byEnd; }
else {array = this.orderedItems.byStart;}
var interval = this.range.end - this.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] > this.range.start - interval) && (array[guess].data[byTime] < this.range.end + interval)) {
guess = 0;
}
else {
guess = -1;
}
}
else {
high -= 1;
while (found == false) {
if ((array[guess].data[byTime] > this.range.start - interval) && (array[guess].data[byTime] < this.range.end + interval)) {
found = true;
}
else {
if (array[guess].data[byTime] < this.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;
}
/**
* Repaint the component
* @return {boolean} Returns true if the component is resized
@ -4991,70 +5054,129 @@ ItemSet.prototype.repaint = function repaint() {
this.lastVisibleInterval = visibleInterval;
this.lastWidth = this.width;
/* TODO: implement+fix smarter way to update visible items
// find the first visible item
// TODO: use faster search, not linear
var byEnd = this.orderedItems.byEnd;
var start = 0;
var item = null;
while ((item = byEnd[start]) &&
(('end' in item.data) ? item.data.end : item.data.start) < this.range.start) {
start++;
}
var newVisibleItems = [];
var item;
var range = this.range;
var orderedItems = this.orderedItems;
// 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 (this.visibleItems.length > 0) {
for (var i = 0; i < this.visibleItems.length; i++) {
item = this.visibleItems[i];
if (item.isVisible(range)) {
if (!item.displayed) item.show();
// reposition item horizontally
item.repositionX();
// find the last visible item
// TODO: use faster search, not linear
var byStart = this.orderedItems.byStart;
var end = 0;
while ((item = byStart[end]) && item.data.start < this.range.end) {
end++;
newVisibleItems.push(item);
}
else {
if (item.displayed) item.hide();
}
}
}
console.log('visible items', start, end); // TODO: cleanup
console.log('visible item ids', byStart[start] && byStart[start].id, byEnd[end-1] && byEnd[end-1].id); // TODO: cleanup
// If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
if (newVisibleItems.length == 0) {var initialPosByStart = this._binarySearch(false);}
else {var initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);}
// use visible search to find a visible ItemRange (only based on endTime)
var initialPosByEnd = this._binarySearch(true);
this.visibleItems = [];
var i = start;
item = byStart[i];
var lastItem = byEnd[end];
while (item && item !== lastItem) {
this.visibleItems.push(item);
item = byStart[++i];
// if we found a initial ID to use, trace it up and down until we meet an invisible item.
if (initialPosByStart != -1) {
for (var i = initialPosByStart; i >= 0; i--) {
item = orderedItems.byStart[i];
if (item.isVisible(range)) {
if (!item.displayed) item.show();
item.repositionX();
if (newVisibleItems.indexOf(item) == -1) {
newVisibleItems.push(item);
}
}
else {
break;
}
}
// and up
for (var i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
item = orderedItems.byStart[i];
if (item.isVisible(range)) {
if (!item.displayed) item.show();
item.repositionX();
if (newVisibleItems.indexOf(item) == -1) {
newVisibleItems.push(item);
}
}
else {
break;
}
}
}
this.stack.order(this.visibleItems);
// show visible items
for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
item = this.visibleItems[i];
// if we found a initial ID to use, trace it up and down until we meet an invisible item.
if (initialPosByEnd != -1) {
for (var i = initialPosByEnd; i >= 0; i--) {
item = orderedItems.byEnd[i];
if (item.isVisible(range)) {
if (!item.displayed) item.show();
// reposition item horizontally
item.repositionX();
if (newVisibleItems.indexOf(item) == -1) {
newVisibleItems.push(item);
}
}
else {
break;
}
}
if (!item.displayed) item.show();
item.top = null; // reset stacking position
// and up
for (var i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
item = orderedItems.byEnd[i];
if (item.isVisible(range)) {
if (!item.displayed) item.show();
// reposition item horizontally
item.repositionX();
// reposition item horizontally
item.repositionX();
if (newVisibleItems.indexOf(item) == -1) {
newVisibleItems.push(item);
}
}
else {
break;
}
}
}
*/
// simple, brute force calculation of visible items
// TODO: replace with a faster, more sophisticated solution
this.visibleItems = [];
for (var id in this.items) {
if (this.items.hasOwnProperty(id)) {
var item = this.items[id];
if (item.isVisible(this.range)) {
if (!this.systemLoaded) {
// initial setup is brute force all the ranged items;
// TODO: implement this in the onUpdate function to only load the new items.
for (var i = 0; i < orderedItems.byEnd.length; i++) {
item = orderedItems.byEnd[i];
if (item.isVisible(range)) {
if (!item.displayed) item.show();
// reposition item horizontally
item.repositionX();
this.visibleItems.push(item);
newVisibleItems.push(item);
}
else {
if (item.displayed) item.hide();
}
}
this.systemLoaded = true;
}
this.visibleItems = newVisibleItems;
// 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
@ -5253,7 +5375,7 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) {
});
this._order();
this.systemLoaded = false;
this.stackDirty = true; // force re-stacking of all items next repaint
this.emit('change');
};
@ -5279,10 +5401,12 @@ ItemSet.prototype._onRemove = function _onRemove(ids) {
count++;
item.hide();
delete me.items[id];
delete me.visibleItems[id];
// remove from visible items
var index = me.visibleItems.indexOf(me.item);
me.visibleItems.splice(index,1);
// remove from selection
var index = me.selection.indexOf(id);
index = me.selection.indexOf(id);
if (index != -1) me.selection.splice(index, 1);
}
});
@ -5302,13 +5426,25 @@ ItemSet.prototype._onRemove = function _onRemove(ids) {
ItemSet.prototype._order = function _order() {
var array = util.toArray(this.items);
this.orderedItems.byStart = array;
this.orderedItems.byEnd = [].concat(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);
};
ItemSet.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;
};
/**
* Start dragging the selected events
* @param {Event} event
@ -19168,8 +19304,8 @@ else {
}
})(this);
},{}],4:[function(require,module,exports){
//! moment.js
//! version : 2.5.1
var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};//! moment.js
//! version : 2.6.0
//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
//! license : MIT
//! momentjs.com
@ -19181,8 +19317,10 @@ else {
************************************/
var moment,
VERSION = "2.5.1",
global = this,
VERSION = "2.6.0",
// the global-scope this is NOT the global object in Node.js
globalScope = typeof global !== 'undefined' ? global : this,
oldGlobalMoment,
round = Math.round,
i,
@ -19211,7 +19349,7 @@ else {
},
// check for nodeJS
hasModule = (typeof module !== 'undefined' && module.exports && typeof require !== 'undefined'),
hasModule = (typeof module !== 'undefined' && module.exports),
// ASP.NET json date format regex
aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
@ -19222,7 +19360,7 @@ else {
isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
// format tokens
formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
// parsing token regexes
@ -19235,6 +19373,7 @@ else {
parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
parseTokenT = /T/i, // T (ISO separator)
parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
parseTokenOrdinal = /\d{1,2}/,
//strict parsing regexes
parseTokenOneDigit = /\d/, // 0 - 9
@ -19260,7 +19399,7 @@ else {
// iso time formats and regexes
isoTimes = [
['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
['HH:mm', /(T| )\d\d:\d\d/],
['HH', /(T| )\d\d/]
@ -19291,6 +19430,7 @@ else {
w : 'week',
W : 'isoWeek',
M : 'month',
Q : 'quarter',
y : 'year',
DDD : 'dayOfYear',
e : 'weekday',
@ -19466,6 +19606,23 @@ else {
};
}
function deprecate(msg, fn) {
var firstTime = true;
function printMsg() {
if (moment.suppressDeprecationWarnings === false &&
typeof console !== 'undefined' && console.warn) {
console.warn("Deprecation warning: " + msg);
}
}
return extend(function () {
if (firstTime) {
printMsg();
firstTime = false;
}
return fn.apply(this, arguments);
}, fn);
}
function padToken(func, count) {
return function (a) {
return leftZeroFill(func.call(this, a), count);
@ -19506,6 +19663,7 @@ else {
function Duration(duration) {
var normalizedInput = normalizeObjectUnits(duration),
years = normalizedInput.year || 0,
quarters = normalizedInput.quarter || 0,
months = normalizedInput.month || 0,
weeks = normalizedInput.week || 0,
days = normalizedInput.day || 0,
@ -19527,6 +19685,7 @@ else {
// which months you are are talking about, so we have to store
// it separately.
this._months = +months +
quarters * 3 +
years * 12;
this._data = {};
@ -19589,34 +19748,23 @@ else {
}
// helper function for _.addTime and _.subtractTime
function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) {
function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
var milliseconds = duration._milliseconds,
days = duration._days,
months = duration._months,
minutes,
hours;
months = duration._months;
updateOffset = updateOffset == null ? true : updateOffset;
if (milliseconds) {
mom._d.setTime(+mom._d + milliseconds * isAdding);
}
// store the minutes and hours so we can restore them
if (days || months) {
minutes = mom.minute();
hours = mom.hour();
}
if (days) {
mom.date(mom.date() + days * isAdding);
rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
}
if (months) {
mom.month(mom.month() + months * isAdding);
rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
}
if (milliseconds && !ignoreUpdateOffset) {
moment.updateOffset(mom);
}
// restore the minutes and hours after possibly changing dst
if (days || months) {
mom.minute(minutes);
mom.hour(hours);
if (updateOffset) {
moment.updateOffset(mom, days || months);
}
}
@ -19731,6 +19879,10 @@ else {
return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
}
function weeksInYear(year, dow, doy) {
return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
}
function daysInYear(year) {
return isLeapYear(year) ? 366 : 365;
}
@ -20121,6 +20273,8 @@ else {
function getParseRegexForToken(token, config) {
var a, strict = config._strict;
switch (token) {
case 'Q':
return parseTokenOneDigit;
case 'DDDD':
return parseTokenThreeDigits;
case 'YYYY':
@ -20189,6 +20343,8 @@ else {
case 'e':
case 'E':
return parseTokenOneOrTwoDigits;
case 'Do':
return parseTokenOrdinal;
default :
a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
return a;
@ -20210,6 +20366,12 @@ else {
var a, datePartArray = config._a;
switch (token) {
// QUARTER
case 'Q':
if (input != null) {
datePartArray[MONTH] = (toInt(input) - 1) * 3;
}
break;
// MONTH
case 'M' : // fall through to MM
case 'MM' :
@ -20234,6 +20396,11 @@ else {
datePartArray[DATE] = toInt(input);
}
break;
case 'Do' :
if (input != null) {
datePartArray[DATE] = toInt(parseInt(input, 10));
}
break;
// DAY OF YEAR
case 'DDD' : // fall through to DDDD
case 'DDDD' :
@ -20244,7 +20411,7 @@ else {
break;
// YEAR
case 'YY' :
datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
datePartArray[YEAR] = moment.parseTwoDigitYear(input);
break;
case 'YYYY' :
case 'YYYYY' :
@ -20333,9 +20500,9 @@ else {
//compute day of the year from weeks and weekdays
if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
fixYear = function (val) {
var int_val = parseInt(val, 10);
var intVal = parseInt(val, 10);
return val ?
(val.length < 3 ? (int_val > 68 ? 1900 + int_val : 2000 + int_val) : int_val) :
(val.length < 3 ? (intVal > 68 ? 1900 + intVal : 2000 + intVal) : intVal) :
(config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
};
@ -20571,7 +20738,7 @@ else {
makeDateFromStringAndFormat(config);
}
else {
config._d = new Date(string);
moment.createFromInputFallback(config);
}
}
@ -20592,8 +20759,11 @@ else {
config._d = new Date(+input);
} else if (typeof(input) === 'object') {
dateFromObject(config);
} else {
} else if (typeof(input) === 'number') {
// from milliseconds
config._d = new Date(input);
} else {
moment.createFromInputFallback(config);
}
}
@ -20720,7 +20890,7 @@ else {
var input = config._i,
format = config._f;
if (input === null) {
if (input === null || (format === undefined && input === '')) {
return moment.invalid({nullInput: true});
}
@ -20766,6 +20936,17 @@ else {
return makeMoment(c);
};
moment.suppressDeprecationWarnings = false;
moment.createFromInputFallback = deprecate(
"moment construction falls back to js Date. This is " +
"discouraged and will be removed in upcoming major " +
"release. Please refer to " +
"https://github.com/moment/moment/issues/1407 for more info.",
function (config) {
config._d = new Date(config._i);
});
// creating with utc
moment.utc = function (input, format, lang, strict) {
var c;
@ -20862,6 +21043,10 @@ else {
// default format
moment.defaultFormat = isoFormat;
// Plugins that add properties should also add the key here (null value),
// so we can properly clone ourselves.
moment.momentProperties = momentProperties;
// This function will be called whenever a moment is mutated.
// It is intended to keep the offset in sync with the timezone.
moment.updateOffset = function () {};
@ -20925,8 +21110,12 @@ else {
return m;
};
moment.parseZone = function (input) {
return moment(input).parseZone();
moment.parseZone = function () {
return moment.apply(null, arguments).parseZone();
};
moment.parseTwoDigitYear = function (input) {
return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
};
/************************************
@ -21113,29 +21302,7 @@ else {
}
},
month : function (input) {
var utc = this._isUTC ? 'UTC' : '',
dayOfMonth;
if (input != null) {
if (typeof input === 'string') {
input = this.lang().monthsParse(input);
if (typeof input !== 'number') {
return this;
}
}
dayOfMonth = this.date();
this.date(1);
this._d['set' + utc + 'Month'](input);
this.date(Math.min(dayOfMonth, this.daysInMonth()));
moment.updateOffset(this);
return this;
} else {
return this._d['get' + utc + 'Month']();
}
},
month : makeAccessor('Month', true),
startOf: function (units) {
units = normalizeUnits(units);
@ -21145,6 +21312,7 @@ else {
case 'year':
this.month(0);
/* falls through */
case 'quarter':
case 'month':
this.date(1);
/* falls through */
@ -21171,6 +21339,11 @@ else {
this.isoWeekday(1);
}
// quarters are also special
if (units === 'quarter') {
this.month(Math.floor(this.month() / 3) * 3);
}
return this;
},
@ -21204,7 +21377,17 @@ else {
return other > this ? this : other;
},
zone : function (input) {
// keepTime = true means only change the timezone, without affecting
// the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
// It is possible that 5:31:26 doesn't exist int zone +0200, so we
// adjust the time as needed, to be valid.
//
// Keeping the time actually adds/subtracts (one hour)
// from the actual represented time. That is why we call updateOffset
// a second time. In case it wants us to change the offset again
// _changeInProgress == true case, then we have to adjust, because
// there is no such time in the given timezone.
zone : function (input, keepTime) {
var offset = this._offset || 0;
if (input != null) {
if (typeof input === "string") {
@ -21216,7 +21399,14 @@ else {
this._offset = input;
this._isUTC = true;
if (offset !== input) {
addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true);
if (!keepTime || this._changeInProgress) {
addOrSubtractDurationFromMoment(this,
moment.duration(offset - input, 'm'), 1, false);
} else if (!this._changeInProgress) {
this._changeInProgress = true;
moment.updateOffset(this, true);
this._changeInProgress = null;
}
}
} else {
return this._isUTC ? offset : this._d.getTimezoneOffset();
@ -21261,8 +21451,8 @@ else {
return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
},
quarter : function () {
return Math.ceil((this.month() + 1.0) / 3.0);
quarter : function (input) {
return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
},
weekYear : function (input) {
@ -21297,6 +21487,15 @@ else {
return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
},
isoWeeksInYear : function () {
return weeksInYear(this.year(), 1, 4);
},
weeksInYear : function () {
var weekInfo = this._lang._week;
return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
},
get : function (units) {
units = normalizeUnits(units);
return this[units]();
@ -21323,33 +21522,68 @@ else {
}
});
// helper for adding shortcuts
function makeGetterAndSetter(name, key) {
moment.fn[name] = moment.fn[name + 's'] = function (input) {
var utc = this._isUTC ? 'UTC' : '';
if (input != null) {
this._d['set' + utc + key](input);
moment.updateOffset(this);
function rawMonthSetter(mom, value) {
var dayOfMonth;
// TODO: Move this out of here!
if (typeof value === 'string') {
value = mom.lang().monthsParse(value);
// TODO: Another silent failure?
if (typeof value !== 'number') {
return mom;
}
}
dayOfMonth = Math.min(mom.date(),
daysInMonth(mom.year(), value));
mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
return mom;
}
function rawGetter(mom, unit) {
return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
}
function rawSetter(mom, unit, value) {
if (unit === 'Month') {
return rawMonthSetter(mom, value);
} else {
return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
}
}
function makeAccessor(unit, keepTime) {
return function (value) {
if (value != null) {
rawSetter(this, unit, value);
moment.updateOffset(this, keepTime);
return this;
} else {
return this._d['get' + utc + key]();
return rawGetter(this, unit);
}
};
}
// loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
for (i = 0; i < proxyGettersAndSetters.length; i ++) {
makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
}
// add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
makeGetterAndSetter('year', 'FullYear');
moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
// Setting the hour should keep the time, because the user explicitly
// specified which hour he wants. So trying to maintain the same hour (in
// a new timezone) makes sense. Adding/subtracting hours does not follow
// this rule.
moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
// moment.fn.month is defined separately
moment.fn.date = makeAccessor('Date', true);
moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true));
moment.fn.year = makeAccessor('FullYear', true);
moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true));
// add plural methods
moment.fn.days = moment.fn.day;
moment.fn.months = moment.fn.month;
moment.fn.weeks = moment.fn.week;
moment.fn.isoWeeks = moment.fn.isoWeek;
moment.fn.quarters = moment.fn.quarter;
// add aliased format methods
moment.fn.toJSON = moment.fn.toISOString;
@ -21525,45 +21759,36 @@ else {
Exposing Moment
************************************/
function makeGlobal(deprecate) {
var warned = false, local_moment = moment;
function makeGlobal(shouldDeprecate) {
/*global ender:false */
if (typeof ender !== 'undefined') {
return;
}
// here, `this` means `window` in the browser, or `global` on the server
// add `moment` as a global object via a string identifier,
// for Closure Compiler "advanced" mode
if (deprecate) {
global.moment = function () {
if (!warned && console && console.warn) {
warned = true;
console.warn(
"Accessing Moment through the global scope is " +
"deprecated, and will be removed in an upcoming " +
"release.");
}
return local_moment.apply(null, arguments);
};
extend(global.moment, local_moment);
oldGlobalMoment = globalScope.moment;
if (shouldDeprecate) {
globalScope.moment = deprecate(
"Accessing Moment through the global scope is " +
"deprecated, and will be removed in an upcoming " +
"release.",
moment);
} else {
global['moment'] = moment;
globalScope.moment = moment;
}
}
// CommonJS module is defined
if (hasModule) {
module.exports = moment;
makeGlobal(true);
} else if (typeof define === "function" && define.amd) {
define("moment", function (require, exports, module) {
if (module.config && module.config() && module.config().noGlobal !== true) {
// If user provided noGlobal, he is aware of global
makeGlobal(module.config().noGlobal === undefined);
if (module.config && module.config() && module.config().noGlobal === true) {
// release the global variable
globalScope.moment = oldGlobalMoment;
}
return moment;
});
makeGlobal(true);
} else {
makeGlobal();
}

+ 10
- 10
dist/vis.min.js
File diff suppressed because it is too large
View File


+ 22
- 7
examples/timeline/01_basic.html View File

@ -18,14 +18,29 @@
<script type="text/javascript">
var container = document.getElementById('visualization');
var items = [
{id: 1, content: 'item 1', start: '2013-04-20'},
{id: 2, content: 'item 2', start: '2013-04-14'},
{id: 3, content: 'item 3', start: '2013-04-18'},
{id: 4, content: 'item 4', start: '2013-04-16', end: '2013-04-19'},
{id: 5, content: 'item 5', start: '2013-04-25'},
{id: 6, content: 'item 6', start: '2013-04-27'}
{id: 1, content: 'item 1', start: '2014-04-20'},
{id: 2, content: 'item 2', start: '2014-04-14'},
{id: 3, content: 'item 3', start: '2014-04-18'},
{id: 5, content: 'item 5', start: '2014-04-25'},
{id: 6, content: 'item 6', start: '2014-04-27'},
{id: 7, content: 'item 3', start: '2014-06-18'},
{id: 8, content: 'item 5', start: '2014-06-25'},
{id: 9, content: 'item 6', start: '2014-06-27'},
{id: 10, content: 'item 3', start: '2014-06-18'},
{id: 11, content: 'item 5', start: '2014-06-25'},
{id: 12, content: 'item 6', start: '2014-01-27'},
{id: 13, content: 'item 3', start: '2014-01-18'},
{id: 14, content: 'item 5', start: '2014-01-25'},
{id: 15, content: 'item 6', start: '2014-02-27'},
{id: 16, content: 'item 3', start: '2014-02-18'},
{id: 17, content: 'item 5', start: '2014-02-25'},
{id: 18, content: 'item 6', start: '2014-09-27'},
{id: 24, content: 'item 4', start: '2014-05-16', end: '2015-06-19'}
];
var options = {};
var options = {
start: '2014-02-10',
end: '2014-05-10'
};
var timeline = new vis.Timeline(container, items, options);
</script>
</body>

+ 1
- 1
examples/timeline/03_much_data.html View File

@ -22,7 +22,7 @@
</h1>
<p>
<label for="count">Number of items</label>
<input id="count" value="1000">
<input id="count" value="100">
<input id="draw" type="button" value="draw">
</p>
<div id="visualization"></div>

+ 186
- 50
src/timeline/component/ItemSet.js View File

@ -43,16 +43,14 @@ function ItemSet(backgroundPanel, axisPanel, options) {
byStart: [],
byEnd: []
};
this.systemLoaded = false;
this.visibleItems = []; // visible, ordered items
this.visibleItemsStart = 0; // start index of visible items in this.orderedItems // TODO: cleanup
this.visibleItemsEnd = 0; // start index of visible items in this.orderedItems // TODO: cleanup
this.selection = []; // list with the ids of all selected nodes
this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
this.stack = new Stack(Object.create(this.options));
this.stackDirty = true; // if true, all items will be restacked on next repaint
this.touchParams = {}; // stores properties while dragging
// create the HTML DOM
this._create();
}
@ -242,6 +240,71 @@ ItemSet.prototype.getFrame = function getFrame() {
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 {Boolean} byEnd
* @returns {number}
* @private
*/
ItemSet.prototype._binarySearch = function _binarySearch(byEnd) {
var array = []
var byTime = byEnd ? "end" : "start";
if (byEnd == true) {array = this.orderedItems.byEnd; }
else {array = this.orderedItems.byStart;}
var interval = this.range.end - this.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] > this.range.start - interval) && (array[guess].data[byTime] < this.range.end + interval)) {
guess = 0;
}
else {
guess = -1;
}
}
else {
high -= 1;
while (found == false) {
if ((array[guess].data[byTime] > this.range.start - interval) && (array[guess].data[byTime] < this.range.end + interval)) {
found = true;
}
else {
if (array[guess].data[byTime] < this.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;
}
/**
* Repaint the component
* @return {boolean} Returns true if the component is resized
@ -262,70 +325,129 @@ ItemSet.prototype.repaint = function repaint() {
this.lastVisibleInterval = visibleInterval;
this.lastWidth = this.width;
/* TODO: implement+fix smarter way to update visible items
// find the first visible item
// TODO: use faster search, not linear
var byEnd = this.orderedItems.byEnd;
var start = 0;
var item = null;
while ((item = byEnd[start]) &&
(('end' in item.data) ? item.data.end : item.data.start) < this.range.start) {
start++;
}
var newVisibleItems = [];
var item;
var range = this.range;
var orderedItems = this.orderedItems;
// 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 (this.visibleItems.length > 0) {
for (var i = 0; i < this.visibleItems.length; i++) {
item = this.visibleItems[i];
if (item.isVisible(range)) {
if (!item.displayed) item.show();
// reposition item horizontally
item.repositionX();
// find the last visible item
// TODO: use faster search, not linear
var byStart = this.orderedItems.byStart;
var end = 0;
while ((item = byStart[end]) && item.data.start < this.range.end) {
end++;
newVisibleItems.push(item);
}
else {
if (item.displayed) item.hide();
}
}
}
console.log('visible items', start, end); // TODO: cleanup
console.log('visible item ids', byStart[start] && byStart[start].id, byEnd[end-1] && byEnd[end-1].id); // TODO: cleanup
// If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
if (newVisibleItems.length == 0) {var initialPosByStart = this._binarySearch(false);}
else {var initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);}
// use visible search to find a visible ItemRange (only based on endTime)
var initialPosByEnd = this._binarySearch(true);
// if we found a initial ID to use, trace it up and down until we meet an invisible item.
if (initialPosByStart != -1) {
for (var i = initialPosByStart; i >= 0; i--) {
item = orderedItems.byStart[i];
if (item.isVisible(range)) {
if (!item.displayed) item.show();
item.repositionX();
if (newVisibleItems.indexOf(item) == -1) {
newVisibleItems.push(item);
}
}
else {
break;
}
}
// and up
for (var i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
item = orderedItems.byStart[i];
if (item.isVisible(range)) {
if (!item.displayed) item.show();
this.visibleItems = [];
var i = start;
item = byStart[i];
var lastItem = byEnd[end];
while (item && item !== lastItem) {
this.visibleItems.push(item);
item = byStart[++i];
item.repositionX();
if (newVisibleItems.indexOf(item) == -1) {
newVisibleItems.push(item);
}
}
else {
break;
}
}
}
this.stack.order(this.visibleItems);
// show visible items
for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
item = this.visibleItems[i];
// if we found a initial ID to use, trace it up and down until we meet an invisible item.
if (initialPosByEnd != -1) {
for (var i = initialPosByEnd; i >= 0; i--) {
item = orderedItems.byEnd[i];
if (item.isVisible(range)) {
if (!item.displayed) item.show();
if (!item.displayed) item.show();
item.top = null; // reset stacking position
// reposition item horizontally
item.repositionX();
if (newVisibleItems.indexOf(item) == -1) {
newVisibleItems.push(item);
}
}
else {
break;
}
}
// and up
for (var i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
item = orderedItems.byEnd[i];
if (item.isVisible(range)) {
if (!item.displayed) item.show();
// reposition item horizontally
item.repositionX();
// reposition item horizontally
item.repositionX();
if (newVisibleItems.indexOf(item) == -1) {
newVisibleItems.push(item);
}
}
else {
break;
}
}
}
*/
// simple, brute force calculation of visible items
// TODO: replace with a faster, more sophisticated solution
this.visibleItems = [];
for (var id in this.items) {
if (this.items.hasOwnProperty(id)) {
var item = this.items[id];
if (item.isVisible(this.range)) {
if (!this.systemLoaded) {
// initial setup is brute force all the ranged items;
// TODO: implement this in the onUpdate function to only load the new items.
for (var i = 0; i < orderedItems.byEnd.length; i++) {
item = orderedItems.byEnd[i];
if (item.isVisible(range)) {
if (!item.displayed) item.show();
// reposition item horizontally
item.repositionX();
this.visibleItems.push(item);
newVisibleItems.push(item);
}
else {
if (item.displayed) item.hide();
}
}
this.systemLoaded = true;
}
this.visibleItems = newVisibleItems;
// 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
@ -524,7 +646,7 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) {
});
this._order();
this.systemLoaded = false;
this.stackDirty = true; // force re-stacking of all items next repaint
this.emit('change');
};
@ -550,10 +672,12 @@ ItemSet.prototype._onRemove = function _onRemove(ids) {
count++;
item.hide();
delete me.items[id];
delete me.visibleItems[id];
// remove from visible items
var index = me.visibleItems.indexOf(me.item);
me.visibleItems.splice(index,1);
// remove from selection
var index = me.selection.indexOf(id);
index = me.selection.indexOf(id);
if (index != -1) me.selection.splice(index, 1);
}
});
@ -573,13 +697,25 @@ ItemSet.prototype._onRemove = function _onRemove(ids) {
ItemSet.prototype._order = function _order() {
var array = util.toArray(this.items);
this.orderedItems.byStart = array;
this.orderedItems.byEnd = [].concat(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);
};
ItemSet.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;
};
/**
* Start dragging the selected events
* @param {Event} event

Loading…
Cancel
Save