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.

301 lines
11 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. * @param {function} shouldBailItemsRedrawFunction
  35. * bailing function
  36. * @return {boolean} shouldBail
  37. */
  38. exports.stack = function(items, margin, force, shouldBailItemsRedrawFunction) {
  39. if (force) {
  40. // reset top position of all items
  41. for (var i = 0; i < items.length; i++) {
  42. items[i].top = null;
  43. }
  44. }
  45. // calculate new, non-overlapping positions
  46. for (var i = 0; i < items.length; i++) { // eslint-disable-line no-redeclare
  47. var item = items[i];
  48. if (item.stack && item.top === null) {
  49. // initialize top position
  50. item.top = margin.axis;
  51. var shouldBail = false;
  52. do {
  53. // TODO: optimize checking for overlap. when there is a gap without items,
  54. // you only need to check for items from the next item on, not from zero
  55. var collidingItem = null;
  56. for (var j = 0, jj = items.length; j < jj; j++) {
  57. var other = items[j];
  58. shouldBail = shouldBailItemsRedrawFunction() || false;
  59. if (shouldBail) { return true; }
  60. if (other.top !== null && other !== item && other.stack && exports.collision(item, other, margin.item, other.options.rtl)) {
  61. collidingItem = other;
  62. break;
  63. }
  64. }
  65. if (collidingItem != null) {
  66. // There is a collision. Reposition the items above the colliding element
  67. item.top = collidingItem.top + collidingItem.height + margin.item.vertical;
  68. }
  69. } while (collidingItem);
  70. }
  71. }
  72. return shouldBail;
  73. };
  74. /**
  75. * Adjust vertical positions of the items within a single subgroup such that they
  76. * don't overlap each other.
  77. * @param {Item[]} items
  78. * All items withina subgroup
  79. * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
  80. * Margins between items and between items and the axis.
  81. * @param {subgroup} subgroup
  82. * The subgroup that is being stacked
  83. */
  84. exports.substack = function (items, margin, subgroup) {
  85. for (var i = 0; i < items.length; i++) {
  86. items[i].top = null;
  87. }
  88. // Set the initial height
  89. var subgroupHeight = subgroup.height;
  90. // calculate new, non-overlapping positions
  91. for (i = 0; i < items.length; i++) {
  92. var item = items[i];
  93. if (item.stack && item.top === null) {
  94. // initialize top position
  95. item.top = item.baseTop;//margin.axis + item.baseTop;
  96. do {
  97. // TODO: optimize checking for overlap. when there is a gap without items,
  98. // you only need to check for items from the next item on, not from zero
  99. var collidingItem = null;
  100. for (var j = 0, jj = items.length; j < jj; j++) {
  101. var other = items[j];
  102. if (other.top !== null && other !== item /*&& other.stack*/ && exports.collision(item, other, margin.item, other.options.rtl)) {
  103. collidingItem = other;
  104. break;
  105. }
  106. }
  107. if (collidingItem != null) {
  108. // There is a collision. Reposition the items above the colliding element
  109. item.top = collidingItem.top + collidingItem.height + margin.item.vertical;// + item.baseTop;
  110. }
  111. if (item.top + item.height > subgroupHeight) {
  112. subgroupHeight = item.top + item.height;
  113. }
  114. } while (collidingItem);
  115. }
  116. }
  117. // Set the new height
  118. subgroup.height = subgroupHeight - subgroup.top + 0.5 * margin.item.vertical;
  119. };
  120. /**
  121. * Adjust vertical positions of the items without stacking them
  122. * @param {Item[]} items
  123. * All visible items
  124. * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
  125. * Margins between items and between items and the axis.
  126. * @param {subgroups[]} subgroups
  127. * All subgroups
  128. * @param {boolean} stackSubgroups
  129. */
  130. exports.nostack = function(items, margin, subgroups, stackSubgroups) {
  131. for (var i = 0; i < items.length; i++) {
  132. if (items[i].data.subgroup == undefined) {
  133. items[i].top = margin.item.vertical;
  134. } else if (items[i].data.subgroup !== undefined && stackSubgroups) {
  135. var newTop = 0;
  136. for (var subgroup in subgroups) {
  137. if (subgroups.hasOwnProperty(subgroup)) {
  138. if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) {
  139. newTop += subgroups[subgroup].height;
  140. subgroups[items[i].data.subgroup].top = newTop;
  141. }
  142. }
  143. }
  144. items[i].top = newTop + 0.5 * margin.item.vertical;
  145. }
  146. }
  147. if (!stackSubgroups) {
  148. exports.stackSubgroups(items, margin, subgroups)
  149. }
  150. };
  151. /**
  152. * Adjust vertical positions of the subgroups such that they don't overlap each
  153. * other.
  154. * @param {Array.<vis.Item>} items
  155. * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin Margins between items and between items and the axis.
  156. * @param {subgroups[]} subgroups
  157. * All subgroups
  158. */
  159. exports.stackSubgroups = function(items, margin, subgroups) {
  160. for (var subgroup in subgroups) {
  161. if (subgroups.hasOwnProperty(subgroup)) {
  162. subgroups[subgroup].top = 0;
  163. do {
  164. // TODO: optimize checking for overlap. when there is a gap without items,
  165. // you only need to check for items from the next item on, not from zero
  166. var collidingItem = null;
  167. for (var otherSubgroup in subgroups) {
  168. if (subgroups[otherSubgroup].top !== null && otherSubgroup !== subgroup && subgroups[subgroup].index > subgroups[otherSubgroup].index && exports.collisionByTimes(subgroups[subgroup], subgroups[otherSubgroup])) {
  169. collidingItem = subgroups[otherSubgroup];
  170. break;
  171. }
  172. }
  173. if (collidingItem != null) {
  174. // There is a collision. Reposition the subgroups above the colliding element
  175. subgroups[subgroup].top = collidingItem.top + collidingItem.height;
  176. }
  177. } while (collidingItem);
  178. }
  179. }
  180. for (var i = 0; i < items.length; i++) {
  181. if (items[i].data.subgroup !== undefined) {
  182. items[i].top = subgroups[items[i].data.subgroup].top + 0.5 * margin.item.vertical;
  183. }
  184. }
  185. };
  186. /**
  187. * Adjust vertical positions of the subgroups such that they don't overlap each
  188. * other, then stacks the contents of each subgroup individually.
  189. * @param {Item[]} subgroupItems
  190. * All the items in a subgroup
  191. * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
  192. * Margins between items and between items and the axis.
  193. * @param {subgroups[]} subgroups
  194. * All subgroups
  195. */
  196. exports.stackSubgroupsWithInnerStack = function (subgroupItems, margin, subgroups) {
  197. var doSubStack = false;
  198. // Run subgroups in their order (if any)
  199. var subgroupOrder = [];
  200. for(var subgroup in subgroups) {
  201. if (subgroups[subgroup].hasOwnProperty("index")) {
  202. subgroupOrder[subgroups[subgroup].index] = subgroup;
  203. }
  204. else {
  205. subgroupOrder.push(subgroup);
  206. }
  207. }
  208. for(var j = 0; j < subgroupOrder.length; j++) {
  209. subgroup = subgroupOrder[j];
  210. if (subgroups.hasOwnProperty(subgroup)) {
  211. doSubStack = doSubStack || subgroups[subgroup].stack;
  212. subgroups[subgroup].top = 0;
  213. for (var otherSubgroup in subgroups) {
  214. if (subgroups[otherSubgroup].visible && subgroups[subgroup].index > subgroups[otherSubgroup].index) {
  215. subgroups[subgroup].top += subgroups[otherSubgroup].height;
  216. }
  217. }
  218. var items = subgroupItems[subgroup];
  219. for(var i = 0; i < items.length; i++) {
  220. if (items[i].data.subgroup !== undefined) {
  221. items[i].top = subgroups[items[i].data.subgroup].top + 0.5 * margin.item.vertical;
  222. if (subgroups[subgroup].stack) {
  223. items[i].baseTop = items[i].top;
  224. }
  225. }
  226. }
  227. if (doSubStack && subgroups[subgroup].stack) {
  228. exports.substack(subgroupItems[subgroup], margin, subgroups[subgroup]);
  229. }
  230. }
  231. }
  232. };
  233. /**
  234. * Test if the two provided items collide
  235. * The items must have parameters left, width, top, and height.
  236. * @param {Item} a The first item
  237. * @param {Item} b The second item
  238. * @param {{horizontal: number, vertical: number}} margin
  239. * An object containing a horizontal and vertical
  240. * minimum required margin.
  241. * @param {boolean} rtl
  242. * @return {boolean} true if a and b collide, else false
  243. */
  244. exports.collision = function(a, b, margin, rtl) {
  245. if (rtl) {
  246. return ((a.right - margin.horizontal + EPSILON) < (b.right + b.width) &&
  247. (a.right + a.width + margin.horizontal - EPSILON) > b.right &&
  248. (a.top - margin.vertical + EPSILON) < (b.top + b.height) &&
  249. (a.top + a.height + margin.vertical - EPSILON) > b.top);
  250. } else {
  251. return ((a.left - margin.horizontal + EPSILON) < (b.left + b.width) &&
  252. (a.left + a.width + margin.horizontal - EPSILON) > b.left &&
  253. (a.top - margin.vertical + EPSILON) < (b.top + b.height) &&
  254. (a.top + a.height + margin.vertical - EPSILON) > b.top);
  255. }
  256. };
  257. /**
  258. * Test if the two provided objects collide
  259. * The objects must have parameters start, end, top, and height.
  260. * @param {Object} a The first Object
  261. * @param {Object} b The second Object
  262. * @return {boolean} true if a and b collide, else false
  263. */
  264. exports.collisionByTimes = function(a, b) {
  265. return (
  266. (a.start <= b.start && a.end >= b.start && a.top < (b.top + b.height) && (a.top + a.height) > b.top ) ||
  267. (b.start <= a.start && b.end >= a.start && b.top < (a.top + a.height) && (b.top + b.height) > a.top )
  268. )
  269. }