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.

183 lines
6.6 KiB

  1. // Utility functions for ordering and stacking of items
  2. var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors
  3. /**
  4. * Order items by their start data
  5. * @param {Item[]} items
  6. */
  7. exports.orderByStart = function(items) {
  8. items.sort(function (a, b) {
  9. return a.data.start - b.data.start;
  10. });
  11. };
  12. /**
  13. * Order items by their end date. If they have no end date, their start date
  14. * is used.
  15. * @param {Item[]} items
  16. */
  17. exports.orderByEnd = function(items) {
  18. items.sort(function (a, b) {
  19. var aTime = ('end' in a.data) ? a.data.end : a.data.start,
  20. bTime = ('end' in b.data) ? b.data.end : b.data.start;
  21. return aTime - bTime;
  22. });
  23. };
  24. /**
  25. * Adjust vertical positions of the items such that they don't overlap each
  26. * other.
  27. * @param {Item[]} items
  28. * All visible items
  29. * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
  30. * Margins between items and between items and the axis.
  31. * @param {boolean} [force=false]
  32. * If true, all items will be repositioned. If false (default), only
  33. * items having a top===null will be re-stacked
  34. */
  35. exports.stack = function(items, margin, force) {
  36. if (force) {
  37. // reset top position of all items
  38. for (var i = 0; i < items.length; i++) {
  39. items[i].top = null;
  40. }
  41. }
  42. // calculate new, non-overlapping positions
  43. for (var i = 0; i < items.length; i++) { // eslint-disable-line no-redeclare
  44. var item = items[i];
  45. if (item.stack && item.top === null) {
  46. // initialize top position
  47. item.top = margin.axis;
  48. do {
  49. // TODO: optimize checking for overlap. when there is a gap without items,
  50. // you only need to check for items from the next item on, not from zero
  51. var collidingItem = null;
  52. for (var j = 0, jj = items.length; j < jj; j++) {
  53. var other = items[j];
  54. if (other.top !== null && other !== item && other.stack && exports.collision(item, other, margin.item, other.options.rtl)) {
  55. collidingItem = other;
  56. break;
  57. }
  58. }
  59. if (collidingItem != null) {
  60. // There is a collision. Reposition the items above the colliding element
  61. item.top = collidingItem.top + collidingItem.height + margin.item.vertical;
  62. }
  63. } while (collidingItem);
  64. }
  65. }
  66. };
  67. /**
  68. * Adjust vertical positions of the items without stacking them
  69. * @param {Item[]} items
  70. * All visible items
  71. * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
  72. * Margins between items and between items and the axis.
  73. * @param {subgroups[]} subgroups
  74. * All subgroups
  75. * @param {boolean} stackSubgroups
  76. */
  77. exports.nostack = function(items, margin, subgroups, stackSubgroups) {
  78. for (var i = 0; i < items.length; i++) {
  79. if (items[i].data.subgroup == undefined) {
  80. items[i].top = margin.item.vertical;
  81. } else if (items[i].data.subgroup !== undefined && stackSubgroups) {
  82. var newTop = 0;
  83. for (var subgroup in subgroups) {
  84. if (subgroups.hasOwnProperty(subgroup)) {
  85. if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) {
  86. newTop += subgroups[subgroup].height;
  87. subgroups[items[i].data.subgroup].top = newTop;
  88. }
  89. }
  90. }
  91. items[i].top = newTop + 0.5 * margin.item.vertical;
  92. }
  93. }
  94. if (!stackSubgroups) {
  95. exports.stackSubgroups(items, margin, subgroups)
  96. }
  97. };
  98. /**
  99. * Adjust vertical positions of the subgroups such that they don't overlap each
  100. * other.
  101. * @param {Array.<vis.Item>} items
  102. * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin Margins between items and between items and the axis.
  103. * @param {subgroups[]} subgroups
  104. * All subgroups
  105. */
  106. exports.stackSubgroups = function(items, margin, subgroups) {
  107. for (var subgroup in subgroups) {
  108. if (subgroups.hasOwnProperty(subgroup)) {
  109. subgroups[subgroup].top = 0;
  110. do {
  111. // TODO: optimize checking for overlap. when there is a gap without items,
  112. // you only need to check for items from the next item on, not from zero
  113. var collidingItem = null;
  114. for (var otherSubgroup in subgroups) {
  115. if (subgroups[otherSubgroup].top !== null && otherSubgroup !== subgroup && subgroups[subgroup].index > subgroups[otherSubgroup].index && exports.collisionByTimes(subgroups[subgroup], subgroups[otherSubgroup])) {
  116. collidingItem = subgroups[otherSubgroup];
  117. break;
  118. }
  119. }
  120. if (collidingItem != null) {
  121. // There is a collision. Reposition the subgroups above the colliding element
  122. subgroups[subgroup].top = collidingItem.top + collidingItem.height;
  123. }
  124. } while (collidingItem);
  125. }
  126. }
  127. for (var i = 0; i < items.length; i++) {
  128. if (items[i].data.subgroup !== undefined) {
  129. items[i].top = subgroups[items[i].data.subgroup].top + 0.5 * margin.item.vertical;
  130. }
  131. }
  132. };
  133. /**
  134. * Test if the two provided items collide
  135. * The items must have parameters left, width, top, and height.
  136. * @param {Item} a The first item
  137. * @param {Item} b The second item
  138. * @param {{horizontal: number, vertical: number}} margin
  139. * An object containing a horizontal and vertical
  140. * minimum required margin.
  141. * @param {boolean} rtl
  142. * @return {boolean} true if a and b collide, else false
  143. */
  144. exports.collision = function(a, b, margin, rtl) {
  145. if (rtl) {
  146. return ((a.right - margin.horizontal + EPSILON) < (b.right + b.width) &&
  147. (a.right + a.width + margin.horizontal - EPSILON) > b.right &&
  148. (a.top - margin.vertical + EPSILON) < (b.top + b.height) &&
  149. (a.top + a.height + margin.vertical - EPSILON) > b.top);
  150. } else {
  151. return ((a.left - margin.horizontal + EPSILON) < (b.left + b.width) &&
  152. (a.left + a.width + margin.horizontal - EPSILON) > b.left &&
  153. (a.top - margin.vertical + EPSILON) < (b.top + b.height) &&
  154. (a.top + a.height + margin.vertical - EPSILON) > b.top);
  155. }
  156. };
  157. /**
  158. * Test if the two provided objects collide
  159. * The objects must have parameters start, end, top, and height.
  160. * @param {Object} a The first Object
  161. * @param {Object} b The second Object
  162. * @return {boolean} true if a and b collide, else false
  163. */
  164. exports.collisionByTimes = function(a, b) {
  165. return (
  166. (a.start <= b.start && a.end >= b.start && a.top < (b.top + b.height) && (a.top + a.height) > b.top ) ||
  167. (b.start <= a.start && b.end >= a.start && b.top < (a.top + a.height) && (b.top + b.height) > a.top )
  168. )
  169. }