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.

645 lines
19 KiB

9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. var util = require('../../util');
  2. var stack = require('../Stack');
  3. var RangeItem = require('./item/RangeItem');
  4. /**
  5. * @constructor Group
  6. * @param {Number | String} groupId
  7. * @param {Object} data
  8. * @param {ItemSet} itemSet
  9. */
  10. function Group (groupId, data, itemSet) {
  11. this.groupId = groupId;
  12. this.subgroups = {};
  13. this.subgroupIndex = 0;
  14. this.subgroupOrderer = data && data.subgroupOrder;
  15. this.itemSet = itemSet;
  16. this.isVisible = null;
  17. this.dom = {};
  18. this.props = {
  19. label: {
  20. width: 0,
  21. height: 0
  22. }
  23. };
  24. this.className = null;
  25. this.items = {}; // items filtered by groupId of this group
  26. this.visibleItems = []; // items currently visible in window
  27. this.itemsInRange = []; // items currently in range
  28. this.orderedItems = {
  29. byStart: [],
  30. byEnd: []
  31. };
  32. this.checkRangedItems = false; // needed to refresh the ranged items if the window is programatically changed with NO overlap.
  33. var me = this;
  34. this.itemSet.body.emitter.on("checkRangedItems", function () {
  35. me.checkRangedItems = true;
  36. })
  37. this._create();
  38. this.setData(data);
  39. }
  40. /**
  41. * Create DOM elements for the group
  42. * @private
  43. */
  44. Group.prototype._create = function() {
  45. var label = document.createElement('div');
  46. if (this.itemSet.options.groupEditable.order) {
  47. label.className = 'vis-label draggable';
  48. } else {
  49. label.className = 'vis-label';
  50. }
  51. this.dom.label = label;
  52. var inner = document.createElement('div');
  53. inner.className = 'vis-inner';
  54. label.appendChild(inner);
  55. this.dom.inner = inner;
  56. var foreground = document.createElement('div');
  57. foreground.className = 'vis-group';
  58. foreground['timeline-group'] = this;
  59. this.dom.foreground = foreground;
  60. this.dom.background = document.createElement('div');
  61. this.dom.background.className = 'vis-group';
  62. this.dom.axis = document.createElement('div');
  63. this.dom.axis.className = 'vis-group';
  64. // create a hidden marker to detect when the Timelines container is attached
  65. // to the DOM, or the style of a parent of the Timeline is changed from
  66. // display:none is changed to visible.
  67. this.dom.marker = document.createElement('div');
  68. this.dom.marker.style.visibility = 'hidden';
  69. this.dom.marker.innerHTML = '?';
  70. this.dom.background.appendChild(this.dom.marker);
  71. };
  72. /**
  73. * Set the group data for this group
  74. * @param {Object} data Group data, can contain properties content and className
  75. */
  76. Group.prototype.setData = function(data) {
  77. // update contents
  78. var content;
  79. var templateFunction;
  80. if (this.itemSet.options && this.itemSet.options.groupTemplate) {
  81. templateFunction = this.itemSet.options.groupTemplate.bind(this);
  82. content = templateFunction(data, this.dom.inner);
  83. } else {
  84. content = data && data.content;
  85. }
  86. if (content instanceof Element) {
  87. this.dom.inner.appendChild(content);
  88. while (this.dom.inner.firstChild) {
  89. this.dom.inner.removeChild(this.dom.inner.firstChild);
  90. }
  91. this.dom.inner.appendChild(content);
  92. } else if (content instanceof Object) {
  93. templateFunction(data, this.dom.inner);
  94. } else if (content !== undefined && content !== null) {
  95. this.dom.inner.innerHTML = content;
  96. } else {
  97. this.dom.inner.innerHTML = this.groupId || ''; // groupId can be null
  98. }
  99. // update title
  100. this.dom.label.title = data && data.title || '';
  101. if (!this.dom.inner.firstChild) {
  102. util.addClassName(this.dom.inner, 'vis-hidden');
  103. }
  104. else {
  105. util.removeClassName(this.dom.inner, 'vis-hidden');
  106. }
  107. // update className
  108. var className = data && data.className || null;
  109. if (className != this.className) {
  110. if (this.className) {
  111. util.removeClassName(this.dom.label, this.className);
  112. util.removeClassName(this.dom.foreground, this.className);
  113. util.removeClassName(this.dom.background, this.className);
  114. util.removeClassName(this.dom.axis, this.className);
  115. }
  116. util.addClassName(this.dom.label, className);
  117. util.addClassName(this.dom.foreground, className);
  118. util.addClassName(this.dom.background, className);
  119. util.addClassName(this.dom.axis, className);
  120. this.className = className;
  121. }
  122. // update style
  123. if (this.style) {
  124. util.removeCssText(this.dom.label, this.style);
  125. this.style = null;
  126. }
  127. if (data && data.style) {
  128. util.addCssText(this.dom.label, data.style);
  129. this.style = data.style;
  130. }
  131. };
  132. /**
  133. * Get the width of the group label
  134. * @return {number} width
  135. */
  136. Group.prototype.getLabelWidth = function() {
  137. return this.props.label.width;
  138. };
  139. /**
  140. * Repaint this group
  141. * @param {{start: number, end: number}} range
  142. * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
  143. * @param {boolean} [restack=false] Force restacking of all items
  144. * @return {boolean} Returns true if the group is resized
  145. */
  146. Group.prototype.redraw = function(range, margin, restack) {
  147. var resized = false;
  148. // force recalculation of the height of the items when the marker height changed
  149. // (due to the Timeline being attached to the DOM or changed from display:none to visible)
  150. var markerHeight = this.dom.marker.clientHeight;
  151. if (markerHeight != this.lastMarkerHeight) {
  152. this.lastMarkerHeight = markerHeight;
  153. util.forEach(this.items, function (item) {
  154. item.dirty = true;
  155. if (item.displayed) item.redraw();
  156. });
  157. restack = true;
  158. }
  159. // recalculate the height of the subgroups
  160. this._calculateSubGroupHeights();
  161. // calculate actual size and position
  162. var foreground = this.dom.foreground;
  163. this.top = foreground.offsetTop;
  164. this.right = foreground.offsetLeft;
  165. this.width = foreground.offsetWidth;
  166. this.isVisible = this._isGroupVisible(range, margin);
  167. // reposition visible items vertically
  168. if (typeof this.itemSet.options.order === 'function') {
  169. // a custom order function
  170. if (restack) {
  171. // brute force restack of all items
  172. // show all items
  173. var me = this;
  174. var limitSize = false;
  175. util.forEach(this.items, function (item) {
  176. if (!item.displayed) {
  177. item.redraw();
  178. me.visibleItems.push(item);
  179. }
  180. item.repositionX(limitSize);
  181. });
  182. // order all items and force a restacking
  183. var customOrderedItems = this.orderedItems.byStart.slice().sort(function (a, b) {
  184. return me.itemSet.options.order(a.data, b.data);
  185. });
  186. stack.stack(customOrderedItems, margin, true /* restack=true */);
  187. }
  188. this.visibleItems = this._updateItemsInRange(this.orderedItems, this.visibleItems, range);
  189. }
  190. else {
  191. // no custom order function, lazy stacking
  192. this.visibleItems = this._updateItemsInRange(this.orderedItems, this.visibleItems, range);
  193. if (this.itemSet.options.stack) { // TODO: ugly way to access options...
  194. stack.stack(this.visibleItems, margin, restack);
  195. }
  196. else { // no stacking
  197. stack.nostack(this.visibleItems, margin, this.subgroups);
  198. }
  199. }
  200. // recalculate the height of the group
  201. var height = this._calculateHeight(margin);
  202. // calculate actual size and position
  203. var foreground = this.dom.foreground;
  204. this.top = foreground.offsetTop;
  205. this.right = foreground.offsetLeft;
  206. this.width = foreground.offsetWidth;
  207. resized = util.updateProperty(this, 'height', height) || resized;
  208. // recalculate size of label
  209. resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
  210. resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
  211. // apply new height
  212. this.dom.background.style.height = height + 'px';
  213. this.dom.foreground.style.height = height + 'px';
  214. this.dom.label.style.height = height + 'px';
  215. // update vertical position of items after they are re-stacked and the height of the group is calculated
  216. for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
  217. var item = this.visibleItems[i];
  218. item.repositionY(margin);
  219. if (!this.isVisible && this.groupId != "__background__") {
  220. if (item.displayed) item.hide();
  221. }
  222. }
  223. if (!this.isVisible && this.height) {
  224. return resized = false;
  225. }
  226. return resized;
  227. };
  228. /**
  229. * recalculate the height of the subgroups
  230. * @private
  231. */
  232. Group.prototype._calculateSubGroupHeights = function () {
  233. if (Object.keys(this.subgroups).length > 0) {
  234. var me = this;
  235. this.resetSubgroups();
  236. util.forEach(this.visibleItems, function (item) {
  237. if (item.data.subgroup !== undefined) {
  238. me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height, item.height);
  239. me.subgroups[item.data.subgroup].visible = true;
  240. }
  241. });
  242. }
  243. };
  244. /**
  245. * check if group is visible
  246. * @private
  247. */
  248. Group.prototype._isGroupVisible = function (range, margin) {
  249. var isVisible =
  250. (this.top <= range.body.domProps.centerContainer.height - range.body.domProps.scrollTop + margin.axis)
  251. && (this.top + this.height + margin.axis >= - range.body.domProps.scrollTop);
  252. return isVisible;
  253. }
  254. /**
  255. * recalculate the height of the group
  256. * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
  257. * @returns {number} Returns the height
  258. * @private
  259. */
  260. Group.prototype._calculateHeight = function (margin) {
  261. // recalculate the height of the group
  262. var height;
  263. var itemsInRange = this.visibleItems;
  264. if (itemsInRange.length > 0) {
  265. var min = itemsInRange[0].top;
  266. var max = itemsInRange[0].top + itemsInRange[0].height;
  267. util.forEach(itemsInRange, function (item) {
  268. min = Math.min(min, item.top);
  269. max = Math.max(max, (item.top + item.height));
  270. });
  271. if (min > margin.axis) {
  272. // there is an empty gap between the lowest item and the axis
  273. var offset = min - margin.axis;
  274. max -= offset;
  275. util.forEach(itemsInRange, function (item) {
  276. item.top -= offset;
  277. });
  278. }
  279. height = max + margin.item.vertical / 2;
  280. }
  281. else {
  282. height = 0;
  283. }
  284. height = Math.max(height, this.props.label.height);
  285. return height;
  286. };
  287. /**
  288. * Show this group: attach to the DOM
  289. */
  290. Group.prototype.show = function() {
  291. if (!this.dom.label.parentNode) {
  292. this.itemSet.dom.labelSet.appendChild(this.dom.label);
  293. }
  294. if (!this.dom.foreground.parentNode) {
  295. this.itemSet.dom.foreground.appendChild(this.dom.foreground);
  296. }
  297. if (!this.dom.background.parentNode) {
  298. this.itemSet.dom.background.appendChild(this.dom.background);
  299. }
  300. if (!this.dom.axis.parentNode) {
  301. this.itemSet.dom.axis.appendChild(this.dom.axis);
  302. }
  303. };
  304. /**
  305. * Hide this group: remove from the DOM
  306. */
  307. Group.prototype.hide = function() {
  308. var label = this.dom.label;
  309. if (label.parentNode) {
  310. label.parentNode.removeChild(label);
  311. }
  312. var foreground = this.dom.foreground;
  313. if (foreground.parentNode) {
  314. foreground.parentNode.removeChild(foreground);
  315. }
  316. var background = this.dom.background;
  317. if (background.parentNode) {
  318. background.parentNode.removeChild(background);
  319. }
  320. var axis = this.dom.axis;
  321. if (axis.parentNode) {
  322. axis.parentNode.removeChild(axis);
  323. }
  324. };
  325. /**
  326. * Add an item to the group
  327. * @param {Item} item
  328. */
  329. Group.prototype.add = function(item) {
  330. this.items[item.id] = item;
  331. item.setParent(this);
  332. // add to
  333. if (item.data.subgroup !== undefined) {
  334. if (this.subgroups[item.data.subgroup] === undefined) {
  335. this.subgroups[item.data.subgroup] = {height:0, visible: false, index:this.subgroupIndex, items: []};
  336. this.subgroupIndex++;
  337. }
  338. this.subgroups[item.data.subgroup].items.push(item);
  339. }
  340. this.orderSubgroups();
  341. if (this.visibleItems.indexOf(item) == -1) {
  342. var range = this.itemSet.body.range; // TODO: not nice accessing the range like this
  343. this._checkIfVisible(item, this.visibleItems, range);
  344. }
  345. };
  346. Group.prototype.orderSubgroups = function() {
  347. if (this.subgroupOrderer !== undefined) {
  348. var sortArray = [];
  349. if (typeof this.subgroupOrderer == 'string') {
  350. for (var subgroup in this.subgroups) {
  351. sortArray.push({subgroup: subgroup, sortField: this.subgroups[subgroup].items[0].data[this.subgroupOrderer]})
  352. }
  353. sortArray.sort(function (a, b) {
  354. return a.sortField - b.sortField;
  355. })
  356. }
  357. else if (typeof this.subgroupOrderer == 'function') {
  358. for (var subgroup in this.subgroups) {
  359. sortArray.push(this.subgroups[subgroup].items[0].data);
  360. }
  361. sortArray.sort(this.subgroupOrderer);
  362. }
  363. if (sortArray.length > 0) {
  364. for (var i = 0; i < sortArray.length; i++) {
  365. this.subgroups[sortArray[i].subgroup].index = i;
  366. }
  367. }
  368. }
  369. };
  370. Group.prototype.resetSubgroups = function() {
  371. for (var subgroup in this.subgroups) {
  372. if (this.subgroups.hasOwnProperty(subgroup)) {
  373. this.subgroups[subgroup].visible = false;
  374. }
  375. }
  376. };
  377. /**
  378. * Remove an item from the group
  379. * @param {Item} item
  380. */
  381. Group.prototype.remove = function(item) {
  382. delete this.items[item.id];
  383. item.setParent(null);
  384. // remove from visible items
  385. var index = this.visibleItems.indexOf(item);
  386. if (index != -1) this.visibleItems.splice(index, 1);
  387. if(item.data.subgroup !== undefined){
  388. var subgroup = this.subgroups[item.data.subgroup];
  389. if (subgroup){
  390. var itemIndex = subgroup.items.indexOf(item);
  391. subgroup.items.splice(itemIndex,1);
  392. if (!subgroup.items.length){
  393. delete this.subgroups[item.data.subgroup];
  394. this.subgroupIndex--;
  395. }
  396. this.orderSubgroups();
  397. }
  398. }
  399. };
  400. /**
  401. * Remove an item from the corresponding DataSet
  402. * @param {Item} item
  403. */
  404. Group.prototype.removeFromDataSet = function(item) {
  405. this.itemSet.removeItem(item.id);
  406. };
  407. /**
  408. * Reorder the items
  409. */
  410. Group.prototype.order = function() {
  411. var array = util.toArray(this.items);
  412. var startArray = [];
  413. var endArray = [];
  414. for (var i = 0; i < array.length; i++) {
  415. if (array[i].data.end !== undefined) {
  416. endArray.push(array[i]);
  417. }
  418. startArray.push(array[i]);
  419. }
  420. this.orderedItems = {
  421. byStart: startArray,
  422. byEnd: endArray
  423. };
  424. stack.orderByStart(this.orderedItems.byStart);
  425. stack.orderByEnd(this.orderedItems.byEnd);
  426. };
  427. /**
  428. * Update the visible items
  429. * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
  430. * @param {Item[]} visibleItems The previously visible items.
  431. * @param {{start: number, end: number}} range Visible range
  432. * @return {Item[]} visibleItems The new visible items.
  433. * @private
  434. */
  435. Group.prototype._updateItemsInRange = function(orderedItems, oldVisibleItems, range) {
  436. var visibleItems = [];
  437. var visibleItemsLookup = {}; // we keep this to quickly look up if an item already exists in the list without using indexOf on visibleItems
  438. var interval = (range.end - range.start) / 4;
  439. var lowerBound = range.start - interval;
  440. var upperBound = range.end + interval;
  441. // this function is used to do the binary search.
  442. var searchFunction = function (value) {
  443. if (value < lowerBound) {return -1;}
  444. else if (value <= upperBound) {return 0;}
  445. else {return 1;}
  446. }
  447. // first check if the items that were in view previously are still in view.
  448. // IMPORTANT: this handles the case for the items with startdate before the window and enddate after the window!
  449. // also cleans up invisible items.
  450. if (oldVisibleItems.length > 0) {
  451. for (var i = 0; i < oldVisibleItems.length; i++) {
  452. this._checkIfVisibleWithReference(oldVisibleItems[i], visibleItems, visibleItemsLookup, range);
  453. }
  454. }
  455. // we do a binary search for the items that have only start values.
  456. var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, 'data','start');
  457. // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the start values.
  458. this._traceVisible(initialPosByStart, orderedItems.byStart, visibleItems, visibleItemsLookup, function (item) {
  459. return (item.data.start < lowerBound || item.data.start > upperBound);
  460. });
  461. // if the window has changed programmatically without overlapping the old window, the ranged items with start < lowerBound and end > upperbound are not shown.
  462. // We therefore have to brute force check all items in the byEnd list
  463. if (this.checkRangedItems == true) {
  464. this.checkRangedItems = false;
  465. for (i = 0; i < orderedItems.byEnd.length; i++) {
  466. this._checkIfVisibleWithReference(orderedItems.byEnd[i], visibleItems, visibleItemsLookup, range);
  467. }
  468. }
  469. else {
  470. // we do a binary search for the items that have defined end times.
  471. var initialPosByEnd = util.binarySearchCustom(orderedItems.byEnd, searchFunction, 'data','end');
  472. // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the end values.
  473. this._traceVisible(initialPosByEnd, orderedItems.byEnd, visibleItems, visibleItemsLookup, function (item) {
  474. return (item.data.end < lowerBound || item.data.end > upperBound);
  475. });
  476. }
  477. // finally, we reposition all the visible items.
  478. for (var i = 0; i < visibleItems.length; i++) {
  479. var item = visibleItems[i];
  480. if (!item.displayed) item.show();
  481. // reposition item horizontally
  482. item.repositionX();
  483. }
  484. return visibleItems;
  485. };
  486. Group.prototype._traceVisible = function (initialPos, items, visibleItems, visibleItemsLookup, breakCondition) {
  487. if (initialPos != -1) {
  488. for (var i = initialPos; i >= 0; i--) {
  489. var item = items[i];
  490. if (breakCondition(item)) {
  491. break;
  492. }
  493. else {
  494. if (visibleItemsLookup[item.id] === undefined) {
  495. visibleItemsLookup[item.id] = true;
  496. visibleItems.push(item);
  497. }
  498. }
  499. }
  500. for (var i = initialPos + 1; i < items.length; i++) {
  501. var item = items[i];
  502. if (breakCondition(item)) {
  503. break;
  504. }
  505. else {
  506. if (visibleItemsLookup[item.id] === undefined) {
  507. visibleItemsLookup[item.id] = true;
  508. visibleItems.push(item);
  509. }
  510. }
  511. }
  512. }
  513. }
  514. /**
  515. * this function is very similar to the _checkIfInvisible() but it does not
  516. * return booleans, hides the item if it should not be seen and always adds to
  517. * the visibleItems.
  518. * this one is for brute forcing and hiding.
  519. *
  520. * @param {Item} item
  521. * @param {Array} visibleItems
  522. * @param {{start:number, end:number}} range
  523. * @private
  524. */
  525. Group.prototype._checkIfVisible = function(item, visibleItems, range) {
  526. if (item.isVisible(range)) {
  527. if (!item.displayed) item.show();
  528. // reposition item horizontally
  529. item.repositionX();
  530. visibleItems.push(item);
  531. }
  532. else {
  533. if (item.displayed) item.hide();
  534. }
  535. };
  536. /**
  537. * this function is very similar to the _checkIfInvisible() but it does not
  538. * return booleans, hides the item if it should not be seen and always adds to
  539. * the visibleItems.
  540. * this one is for brute forcing and hiding.
  541. *
  542. * @param {Item} item
  543. * @param {Array} visibleItems
  544. * @param {{start:number, end:number}} range
  545. * @private
  546. */
  547. Group.prototype._checkIfVisibleWithReference = function(item, visibleItems, visibleItemsLookup, range) {
  548. if (item.isVisible(range)) {
  549. if (visibleItemsLookup[item.id] === undefined) {
  550. visibleItemsLookup[item.id] = true;
  551. visibleItems.push(item);
  552. }
  553. }
  554. else {
  555. if (item.displayed) item.hide();
  556. }
  557. };
  558. module.exports = Group;