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.

467 lines
12 KiB

  1. // TODO: remove groupset
  2. /**
  3. * An GroupSet holds a set of groups
  4. * @param {Panel} contentPanel Panel where the ItemSets will be created
  5. * @param {Panel} labelPanel Panel where the labels will be created
  6. * @param {Panel} backgroundPanel Panel where the vertical lines of box
  7. * items are created
  8. * @param {Panel} axisPanel Panel on the axis where the dots of box
  9. * items will be created
  10. * @param {Object} [options] See GroupSet.setOptions for the available
  11. * options.
  12. * @constructor GroupSet
  13. * @extends Panel
  14. */
  15. function GroupSet(contentPanel, labelPanel, backgroundPanel, axisPanel, options) {
  16. this.id = util.randomUUID();
  17. this.contentPanel = contentPanel;
  18. this.labelPanel = labelPanel;
  19. this.backgroundPanel = backgroundPanel;
  20. this.axisPanel = axisPanel;
  21. this.options = options || {};
  22. this.range = null; // Range or Object {start: number, end: number}
  23. this.itemsData = null; // DataSet with items
  24. this.groupsData = null; // DataSet with groups
  25. this.groups = {}; // map with groups
  26. this.groupIds = []; // list with ordered group ids
  27. this.dom = {};
  28. this.props = {
  29. labels: {
  30. width: 0
  31. }
  32. };
  33. // TODO: implement right orientation of the labels (left/right)
  34. var me = this;
  35. this.listeners = {
  36. 'add': function (event, params) {
  37. me._onAdd(params.items);
  38. },
  39. 'update': function (event, params) {
  40. me._onUpdate(params.items);
  41. },
  42. 'remove': function (event, params) {
  43. me._onRemove(params.items);
  44. }
  45. };
  46. // create HTML DOM
  47. this._create();
  48. }
  49. GroupSet.prototype = new Panel();
  50. /**
  51. * Create the HTML DOM elements for the GroupSet
  52. * @private
  53. */
  54. GroupSet.prototype._create = function _create () {
  55. // TODO: reimplement groupSet DOM elements
  56. var frame = document.createElement('div');
  57. frame.className = 'groupset';
  58. frame['timeline-groupset'] = this;
  59. this.frame = frame;
  60. this.labelSet = new Panel({
  61. className: 'labelset',
  62. width: '100%',
  63. height: '100%'
  64. });
  65. this.labelPanel.appendChild(this.labelSet);
  66. };
  67. /**
  68. * Get the frame element of component
  69. * @returns {null} Get frame is not supported by GroupSet
  70. */
  71. GroupSet.prototype.getFrame = function getFrame() {
  72. return this.frame;
  73. };
  74. /**
  75. * Set options for the GroupSet. Existing options will be extended/overwritten.
  76. * @param {Object} [options] The following options are available:
  77. * {String | function} groupsOrder
  78. * TODO: describe options
  79. */
  80. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  81. /**
  82. * Set range (start and end).
  83. * @param {Range | Object} range A Range or an object containing start and end.
  84. */
  85. GroupSet.prototype.setRange = function (range) {
  86. this.range = range;
  87. for (var id in this.groups) {
  88. if (this.groups.hasOwnProperty(id)) {
  89. this.groups[id].setRange(range);
  90. }
  91. }
  92. };
  93. /**
  94. * Set items
  95. * @param {vis.DataSet | null} items
  96. */
  97. GroupSet.prototype.setItems = function setItems(items) {
  98. this.itemsData = items;
  99. for (var id in this.groups) {
  100. if (this.groups.hasOwnProperty(id)) {
  101. var group = this.groups[id];
  102. // TODO: every group will emit a change event, causing a lot of unnecessary repaints. improve this.
  103. group.setItems(items);
  104. }
  105. }
  106. };
  107. /**
  108. * Get items
  109. * @return {vis.DataSet | null} items
  110. */
  111. GroupSet.prototype.getItems = function getItems() {
  112. return this.itemsData;
  113. };
  114. /**
  115. * Set range (start and end).
  116. * @param {Range | Object} range A Range or an object containing start and end.
  117. */
  118. GroupSet.prototype.setRange = function setRange(range) {
  119. this.range = range;
  120. };
  121. /**
  122. * Set groups
  123. * @param {vis.DataSet} groups
  124. */
  125. GroupSet.prototype.setGroups = function setGroups(groups) {
  126. var me = this,
  127. ids;
  128. // unsubscribe from current dataset
  129. if (this.groupsData) {
  130. util.forEach(this.listeners, function (callback, event) {
  131. me.groupsData.unsubscribe(event, callback);
  132. });
  133. // remove all drawn groups
  134. ids = this.groupsData.getIds();
  135. this._onRemove(ids);
  136. }
  137. // replace the dataset
  138. if (!groups) {
  139. this.groupsData = null;
  140. }
  141. else if (groups instanceof DataSet) {
  142. this.groupsData = groups;
  143. }
  144. else {
  145. this.groupsData = new DataSet({
  146. convert: {
  147. start: 'Date',
  148. end: 'Date'
  149. }
  150. });
  151. this.groupsData.add(groups);
  152. }
  153. if (this.groupsData) {
  154. // subscribe to new dataset
  155. var id = this.id;
  156. util.forEach(this.listeners, function (callback, event) {
  157. me.groupsData.on(event, callback, id);
  158. });
  159. // draw all new groups
  160. ids = this.groupsData.getIds();
  161. this._onAdd(ids);
  162. }
  163. this.emit('change');
  164. };
  165. /**
  166. * Get groups
  167. * @return {vis.DataSet | null} groups
  168. */
  169. GroupSet.prototype.getGroups = function getGroups() {
  170. return this.groupsData;
  171. };
  172. /**
  173. * Set selected items by their id. Replaces the current selection.
  174. * Unknown id's are silently ignored.
  175. * @param {Array} [ids] An array with zero or more id's of the items to be
  176. * selected. If ids is an empty array, all items will be
  177. * unselected.
  178. */
  179. GroupSet.prototype.setSelection = function setSelection(ids) {
  180. var selection = [],
  181. groups = this.groups;
  182. // iterate over each of the groups
  183. for (var id in groups) {
  184. if (groups.hasOwnProperty(id)) {
  185. var group = groups[id];
  186. group.setSelection(ids);
  187. }
  188. }
  189. return selection;
  190. };
  191. /**
  192. * Get the selected items by their id
  193. * @return {Array} ids The ids of the selected items
  194. */
  195. GroupSet.prototype.getSelection = function getSelection() {
  196. var selection = [],
  197. groups = this.groups;
  198. // iterate over each of the groups
  199. for (var id in groups) {
  200. if (groups.hasOwnProperty(id)) {
  201. var group = groups[id];
  202. selection = selection.concat(group.getSelection());
  203. }
  204. }
  205. return selection;
  206. };
  207. /**
  208. * Repaint the component
  209. * @return {boolean} Returns true if the component was resized since previous repaint
  210. */
  211. GroupSet.prototype.repaint = function repaint() {
  212. var i, id, group,
  213. asSize = util.option.asSize,
  214. asString = util.option.asString,
  215. options = this.options,
  216. orientation = this.getOption('orientation'),
  217. frame = this.frame,
  218. resized = false,
  219. groups = this.groups;
  220. // repaint all groups in order
  221. this.groupIds.forEach(function (id) {
  222. var groupResized = groups[id].repaint();
  223. resized = resized || groupResized;
  224. });
  225. // reposition the labels and calculate the maximum label width
  226. var maxWidth = 0;
  227. for (id in groups) {
  228. if (groups.hasOwnProperty(id)) {
  229. group = groups[id];
  230. maxWidth = Math.max(maxWidth, group.props.label.width);
  231. }
  232. }
  233. resized = util.updateProperty(this.props.labels, 'width', maxWidth) || resized;
  234. // recalculate the height of the groupset, and recalculate top positions of the groups
  235. var fixedHeight = (asSize(options.height) != null);
  236. var height;
  237. if (!fixedHeight) {
  238. // height is not specified, calculate the sum of the height of all groups
  239. height = 0;
  240. this.groupIds.forEach(function (id) {
  241. var group = groups[id];
  242. group.top = height;
  243. if (group.itemSet) group.itemSet.top = group.top; // TODO: this is an ugly hack
  244. height += group.height;
  245. });
  246. }
  247. // update classname
  248. frame.className = 'groupset' + (options.className ? (' ' + asString(options.className)) : '');
  249. // calculate actual size and position
  250. this.top = frame.offsetTop;
  251. this.left = frame.offsetLeft;
  252. this.width = frame.offsetWidth;
  253. this.height = height;
  254. return resized;
  255. };
  256. /**
  257. * Update the groupIds. Requires a repaint afterwards
  258. * @private
  259. */
  260. GroupSet.prototype._updateGroupIds = function () {
  261. // reorder the groups
  262. this.groupIds = this.groupsData.getIds({
  263. order: this.options.groupOrder
  264. });
  265. // hide the groups now, they will be shown again in the next repaint
  266. // in correct order
  267. var groups = this.groups;
  268. this.groupIds.forEach(function (id) {
  269. groups[id].hide();
  270. });
  271. };
  272. /**
  273. * Get the width of the group labels
  274. * @return {Number} width
  275. */
  276. GroupSet.prototype.getLabelsWidth = function getLabelsWidth() {
  277. return this.props.labels.width;
  278. };
  279. /**
  280. * Hide the component from the DOM
  281. */
  282. GroupSet.prototype.hide = function hide() {
  283. // hide labelset
  284. this.labelPanel.removeChild(this.labelSet);
  285. // hide each of the groups
  286. for (var groupId in this.groups) {
  287. if (this.groups.hasOwnProperty(groupId)) {
  288. this.groups[groupId].hide();
  289. }
  290. }
  291. };
  292. /**
  293. * Show the component in the DOM (when not already visible).
  294. * @return {Boolean} changed
  295. */
  296. GroupSet.prototype.show = function show() {
  297. // show label set
  298. if (!this.labelPanel.hasChild(this.labelSet)) {
  299. this.labelPanel.removeChild(this.labelSet);
  300. }
  301. // show each of the groups
  302. for (var groupId in this.groups) {
  303. if (this.groups.hasOwnProperty(groupId)) {
  304. this.groups[groupId].show();
  305. }
  306. }
  307. };
  308. /**
  309. * Handle updated groups
  310. * @param {Number[]} ids
  311. * @private
  312. */
  313. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  314. this._onAdd(ids);
  315. };
  316. /**
  317. * Handle changed groups
  318. * @param {Number[]} ids
  319. * @private
  320. */
  321. GroupSet.prototype._onAdd = function _onAdd(ids) {
  322. var me = this;
  323. ids.forEach(function (id) {
  324. var group = me.groups[id];
  325. if (!group) {
  326. var groupOptions = Object.create(me.options);
  327. util.extend(groupOptions, {
  328. height: null
  329. });
  330. group = new Group(me, me.labelSet, me.backgroundPanel, me.axisPanel, id, groupOptions);
  331. group.on('change', me.emit.bind(me, 'change')); // propagate change event
  332. group.setRange(me.range);
  333. group.setItems(me.itemsData); // attach items data
  334. me.groups[id] = group;
  335. group.parent = me;
  336. }
  337. // update group data
  338. group.setData(me.groupsData.get(id));
  339. });
  340. this._updateGroupIds();
  341. this.emit('change');
  342. };
  343. /**
  344. * Handle removed groups
  345. * @param {Number[]} ids
  346. * @private
  347. */
  348. GroupSet.prototype._onRemove = function _onRemove(ids) {
  349. var groups = this.groups;
  350. ids.forEach(function (id) {
  351. var group = groups[id];
  352. if (group) {
  353. group.setItems(); // detach items data
  354. group.hide(); // FIXME: for some reason when doing setItems after hide, setItems again makes the label visible
  355. delete groups[id];
  356. }
  357. });
  358. this._updateGroupIds();
  359. this.emit('change');
  360. };
  361. /**
  362. * Find the GroupSet from an event target:
  363. * searches for the attribute 'timeline-groupset' in the event target's element
  364. * tree, then finds the right group in this groupset
  365. * @param {Event} event
  366. * @return {Group | null} group
  367. */
  368. GroupSet.groupSetFromTarget = function groupSetFromTarget (event) {
  369. var target = event.target;
  370. while (target) {
  371. if (target.hasOwnProperty('timeline-groupset')) {
  372. return target['timeline-groupset'];
  373. }
  374. target = target.parentNode;
  375. }
  376. return null;
  377. };
  378. /**
  379. * Find the Group from an event target:
  380. * searches for the two elements having attributes 'timeline-groupset' and
  381. * 'timeline-itemset' in the event target's element, then finds the right group.
  382. * @param {Event} event
  383. * @return {Group | null} group
  384. */
  385. GroupSet.groupFromTarget = function groupFromTarget (event) {
  386. // find the groupSet
  387. var groupSet = GroupSet.groupSetFromTarget(event);
  388. // find the ItemSet
  389. var itemSet = ItemSet.itemSetFromTarget(event);
  390. // find the right group
  391. if (groupSet && itemSet) {
  392. for (var groupId in groupSet.groups) {
  393. if (groupSet.groups.hasOwnProperty(groupId)) {
  394. var group = groupSet.groups[groupId];
  395. if (group.itemSet == itemSet) {
  396. return group;
  397. }
  398. }
  399. }
  400. }
  401. return null;
  402. };