vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

453 lines
13 KiB

11 years ago
11 years ago
11 years ago
11 years ago
  1. /**
  2. * @constructor Group
  3. * @param {Number | String} groupId
  4. * @param {Object} data
  5. * @param {ItemSet} itemSet
  6. */
  7. function Group (groupId, data, itemSet) {
  8. this.groupId = groupId;
  9. this.itemSet = itemSet;
  10. this.dom = {};
  11. this.props = {
  12. label: {
  13. width: 0,
  14. height: 0
  15. }
  16. };
  17. this.items = {}; // items filtered by groupId of this group
  18. this.visibleItems = []; // items currently visible in window
  19. this.orderedItems = { // items sorted by start and by end
  20. byStart: [],
  21. byEnd: []
  22. };
  23. this._create();
  24. this.setData(data);
  25. }
  26. /**
  27. * Create DOM elements for the group
  28. * @private
  29. */
  30. Group.prototype._create = function() {
  31. var label = document.createElement('div');
  32. label.className = 'vlabel';
  33. this.dom.label = label;
  34. var inner = document.createElement('div');
  35. inner.className = 'inner';
  36. label.appendChild(inner);
  37. this.dom.inner = inner;
  38. var foreground = document.createElement('div');
  39. foreground.className = 'group';
  40. foreground['timeline-group'] = this;
  41. this.dom.foreground = foreground;
  42. this.dom.background = document.createElement('div');
  43. this.dom.axis = document.createElement('div');
  44. };
  45. /**
  46. * Set the group data for this group
  47. * @param {Object} data Group data, can contain properties content and className
  48. */
  49. Group.prototype.setData = function setData(data) {
  50. // update contents
  51. var content = data && data.content;
  52. if (content instanceof Element) {
  53. this.dom.inner.appendChild(content);
  54. }
  55. else if (content != undefined) {
  56. this.dom.inner.innerHTML = content;
  57. }
  58. else {
  59. this.dom.inner.innerHTML = this.groupId;
  60. }
  61. // update className
  62. var className = data && data.className;
  63. if (className) {
  64. util.addClassName(this.dom.label, className);
  65. }
  66. };
  67. /**
  68. * Get the foreground container element
  69. * @return {HTMLElement} foreground
  70. */
  71. Group.prototype.getForeground = function getForeground() {
  72. return this.dom.foreground;
  73. };
  74. /**
  75. * Get the background container element
  76. * @return {HTMLElement} background
  77. */
  78. Group.prototype.getBackground = function getBackground() {
  79. return this.dom.background;
  80. };
  81. /**
  82. * Get the axis container element
  83. * @return {HTMLElement} axis
  84. */
  85. Group.prototype.getAxis = function getAxis() {
  86. return this.dom.axis;
  87. };
  88. /**
  89. * Get the width of the group label
  90. * @return {number} width
  91. */
  92. Group.prototype.getLabelWidth = function getLabelWidth() {
  93. return this.props.label.width;
  94. };
  95. /**
  96. * Repaint this group
  97. * @param {{start: number, end: number}} range
  98. * @param {number | {item: number, axis: number}} margin
  99. * @param {boolean} [restack=false] Force restacking of all items
  100. * @return {boolean} Returns true if the group is resized
  101. */
  102. Group.prototype.repaint = function repaint(range, margin, restack) {
  103. var resized = false;
  104. if (typeof margin === 'number') {
  105. margin = {
  106. item: margin,
  107. axis: margin
  108. };
  109. }
  110. // update visible items
  111. this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
  112. // reposition visible items vertically
  113. stack.stack(this.visibleItems, margin, restack);
  114. this.stackDirty = false;
  115. for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
  116. this.visibleItems[i].repositionY();
  117. }
  118. // recalculate the height of the group
  119. var height;
  120. var visibleItems = this.visibleItems;
  121. if (visibleItems.length) {
  122. var min = visibleItems[0].top;
  123. var max = visibleItems[0].top + visibleItems[0].height;
  124. util.forEach(visibleItems, function (item) {
  125. min = Math.min(min, item.top);
  126. max = Math.max(max, (item.top + item.height));
  127. });
  128. height = (max - min) + margin.axis + margin.item;
  129. }
  130. else {
  131. height = margin.axis + margin.item;
  132. }
  133. // calculate actual size and position
  134. var foreground = this.dom.foreground;
  135. this.top = foreground.offsetTop;
  136. this.left = foreground.offsetLeft;
  137. this.width = foreground.offsetWidth;
  138. resized = util.updateProperty(this, 'height', height) || resized;
  139. // recalculate size of label
  140. resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
  141. resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
  142. // apply new height
  143. foreground.style.height = height + 'px';
  144. this.dom.label.style.height = height + 'px';
  145. return resized;
  146. };
  147. /**
  148. * Show this group: attach to the DOM
  149. */
  150. Group.prototype.show = function show() {
  151. if (!this.dom.label.parentNode) {
  152. this.itemSet.getLabelSet().appendChild(this.dom.label);
  153. }
  154. if (!this.dom.foreground.parentNode) {
  155. this.itemSet.getForeground().appendChild(this.dom.foreground);
  156. }
  157. if (!this.dom.background.parentNode) {
  158. this.itemSet.getBackground().appendChild(this.dom.background);
  159. }
  160. if (!this.dom.axis.parentNode) {
  161. this.itemSet.getAxis().appendChild(this.dom.axis);
  162. }
  163. };
  164. /**
  165. * Hide this group: remove from the DOM
  166. */
  167. Group.prototype.hide = function hide() {
  168. var label = this.dom.label;
  169. if (label.parentNode) {
  170. label.parentNode.removeChild(label);
  171. }
  172. var foreground = this.dom.foreground;
  173. if (foreground.parentNode) {
  174. foreground.parentNode.removeChild(foreground);
  175. }
  176. var background = this.dom.background;
  177. if (background.parentNode) {
  178. background.parentNode.removeChild(background);
  179. }
  180. var axis = this.dom.axis;
  181. if (axis.parentNode) {
  182. axis.parentNode.removeChild(axis);
  183. }
  184. };
  185. /**
  186. * Add an item to the group
  187. * @param {Item} item
  188. */
  189. Group.prototype.add = function add(item) {
  190. this.items[item.id] = item;
  191. item.setParent(this);
  192. if (item instanceof ItemRange && this.visibleItems.indexOf(item) == -1) {
  193. var range = this.itemSet.range; // TODO: not nice accessing the range like this
  194. this._checkIfVisible(item, this.visibleItems, range);
  195. }
  196. };
  197. /**
  198. * Remove an item from the group
  199. * @param {Item} item
  200. */
  201. Group.prototype.remove = function remove(item) {
  202. delete this.items[item.id];
  203. item.setParent(this.itemSet);
  204. // remove from visible items
  205. var index = this.visibleItems.indexOf(item);
  206. if (index != -1) this.visibleItems.splice(index, 1);
  207. // TODO: also remove from ordered items?
  208. };
  209. /**
  210. * Remove an item from the corresponding DataSet
  211. * @param {Item} item
  212. */
  213. Group.prototype.removeFromDataSet = function removeFromDataSet(item) {
  214. this.itemSet.removeItem(item.id);
  215. };
  216. /**
  217. * Reorder the items
  218. */
  219. Group.prototype.order = function order() {
  220. var array = util.toArray(this.items);
  221. this.orderedItems.byStart = array;
  222. this.orderedItems.byEnd = this._constructByEndArray(array);
  223. stack.orderByStart(this.orderedItems.byStart);
  224. stack.orderByEnd(this.orderedItems.byEnd);
  225. };
  226. /**
  227. * Create an array containing all items being a range (having an end date)
  228. * @param {Item[]} array
  229. * @returns {ItemRange[]}
  230. * @private
  231. */
  232. Group.prototype._constructByEndArray = function _constructByEndArray(array) {
  233. var endArray = [];
  234. for (var i = 0; i < array.length; i++) {
  235. if (array[i] instanceof ItemRange) {
  236. endArray.push(array[i]);
  237. }
  238. }
  239. return endArray;
  240. };
  241. /**
  242. * Update the visible items
  243. * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
  244. * @param {Item[]} visibleItems The previously visible items.
  245. * @param {{start: number, end: number}} range Visible range
  246. * @return {Item[]} visibleItems The new visible items.
  247. * @private
  248. */
  249. Group.prototype._updateVisibleItems = function _updateVisibleItems(orderedItems, visibleItems, range) {
  250. var initialPosByStart,
  251. newVisibleItems = [],
  252. i;
  253. // first check if the items that were in view previously are still in view.
  254. // this handles the case for the ItemRange that is both before and after the current one.
  255. if (visibleItems.length > 0) {
  256. for (i = 0; i < visibleItems.length; i++) {
  257. this._checkIfVisible(visibleItems[i], newVisibleItems, range);
  258. }
  259. }
  260. // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
  261. if (newVisibleItems.length == 0) {
  262. initialPosByStart = this._binarySearch(orderedItems, range, false);
  263. }
  264. else {
  265. initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
  266. }
  267. // use visible search to find a visible ItemRange (only based on endTime)
  268. var initialPosByEnd = this._binarySearch(orderedItems, range, true);
  269. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  270. if (initialPosByStart != -1) {
  271. for (i = initialPosByStart; i >= 0; i--) {
  272. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  273. }
  274. for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
  275. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  276. }
  277. }
  278. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  279. if (initialPosByEnd != -1) {
  280. for (i = initialPosByEnd; i >= 0; i--) {
  281. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  282. }
  283. for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
  284. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  285. }
  286. }
  287. return newVisibleItems;
  288. };
  289. /**
  290. * This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
  291. * arrays. This is done by giving a boolean value true if you want to use the byEnd.
  292. * 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
  293. * if the time we selected (start or end) is within the current range).
  294. *
  295. * 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
  296. * 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,
  297. * either the start OR end time has to be in the range.
  298. *
  299. * @param {{byStart: Item[], byEnd: Item[]}} orderedItems
  300. * @param {{start: number, end: number}} range
  301. * @param {Boolean} byEnd
  302. * @returns {number}
  303. * @private
  304. */
  305. Group.prototype._binarySearch = function _binarySearch(orderedItems, range, byEnd) {
  306. var array = [];
  307. var byTime = byEnd ? 'end' : 'start';
  308. if (byEnd == true) {array = orderedItems.byEnd; }
  309. else {array = orderedItems.byStart;}
  310. var interval = range.end - range.start;
  311. var found = false;
  312. var low = 0;
  313. var high = array.length;
  314. var guess = Math.floor(0.5*(high+low));
  315. var newGuess;
  316. if (high == 0) {guess = -1;}
  317. else if (high == 1) {
  318. if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
  319. guess = 0;
  320. }
  321. else {
  322. guess = -1;
  323. }
  324. }
  325. else {
  326. high -= 1;
  327. while (found == false) {
  328. if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
  329. found = true;
  330. }
  331. else {
  332. if (array[guess].data[byTime] < range.start - interval) { // it is too small --> increase low
  333. low = Math.floor(0.5*(high+low));
  334. }
  335. else { // it is too big --> decrease high
  336. high = Math.floor(0.5*(high+low));
  337. }
  338. newGuess = Math.floor(0.5*(high+low));
  339. // not in list;
  340. if (guess == newGuess) {
  341. guess = -1;
  342. found = true;
  343. }
  344. else {
  345. guess = newGuess;
  346. }
  347. }
  348. }
  349. }
  350. return guess;
  351. };
  352. /**
  353. * this function checks if an item is invisible. If it is NOT we make it visible
  354. * and add it to the global visible items. If it is, return true.
  355. *
  356. * @param {Item} item
  357. * @param {Item[]} visibleItems
  358. * @param {{start:number, end:number}} range
  359. * @returns {boolean}
  360. * @private
  361. */
  362. Group.prototype._checkIfInvisible = function _checkIfInvisible(item, visibleItems, range) {
  363. if (item.isVisible(range)) {
  364. if (!item.displayed) item.show();
  365. item.repositionX();
  366. if (visibleItems.indexOf(item) == -1) {
  367. visibleItems.push(item);
  368. }
  369. return false;
  370. }
  371. else {
  372. return true;
  373. }
  374. };
  375. /**
  376. * this function is very similar to the _checkIfInvisible() but it does not
  377. * return booleans, hides the item if it should not be seen and always adds to
  378. * the visibleItems.
  379. * this one is for brute forcing and hiding.
  380. *
  381. * @param {Item} item
  382. * @param {Array} visibleItems
  383. * @param {{start:number, end:number}} range
  384. * @private
  385. */
  386. Group.prototype._checkIfVisible = function _checkIfVisible(item, visibleItems, range) {
  387. if (item.isVisible(range)) {
  388. if (!item.displayed) item.show();
  389. // reposition item horizontally
  390. item.repositionX();
  391. visibleItems.push(item);
  392. }
  393. else {
  394. if (item.displayed) item.hide();
  395. }
  396. };