diff --git a/HISTORY.md b/HISTORY.md index f8c236ee..ab9aaa08 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -8,6 +8,7 @@ http://visjs.org - Fixed uneven stepsized with hidden dates. - Fixed multiple bugs with regards to hidden dates. +- Fixed subgroups and added subgroup sorting. Subgroup labels will be in future releases. ## 2014-10-21, version 3.6.0 diff --git a/dist/vis.js b/dist/vis.js index 5239a8b6..31c422c2 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -100,33 +100,33 @@ return /******/ (function(modules) { // webpackBootstrap // Timeline exports.Timeline = __webpack_require__(17); - exports.Graph2d = __webpack_require__(41); + exports.Graph2d = __webpack_require__(40); exports.timeline = { DateUtil: __webpack_require__(23), - DataStep: __webpack_require__(44), + DataStep: __webpack_require__(43), Range: __webpack_require__(20), - stack: __webpack_require__(32), + stack: __webpack_require__(46), TimeStep: __webpack_require__(26), components: { items: { Item: __webpack_require__(34), - BackgroundItem: __webpack_require__(38), - BoxItem: __webpack_require__(36), - PointItem: __webpack_require__(37), - RangeItem: __webpack_require__(33) + BackgroundItem: __webpack_require__(37), + BoxItem: __webpack_require__(33), + PointItem: __webpack_require__(35), + RangeItem: __webpack_require__(36) }, Component: __webpack_require__(22), CurrentTime: __webpack_require__(27), CustomTime: __webpack_require__(29), - DataAxis: __webpack_require__(43), - GraphGroup: __webpack_require__(45), + DataAxis: __webpack_require__(42), + GraphGroup: __webpack_require__(44), Group: __webpack_require__(31), - BackgroundGroup: __webpack_require__(35), + BackgroundGroup: __webpack_require__(32), ItemSet: __webpack_require__(30), - Legend: __webpack_require__(46), - LineGraph: __webpack_require__(42), + Legend: __webpack_require__(45), + LineGraph: __webpack_require__(41), TimeAxis: __webpack_require__(25) } }; @@ -13044,7 +13044,7 @@ return /******/ (function(modules) { // webpackBootstrap var CurrentTime = __webpack_require__(27); var CustomTime = __webpack_require__(29); var ItemSet = __webpack_require__(30); - var Activator = __webpack_require__(39); + var Activator = __webpack_require__(38); var DateUtil = __webpack_require__(23); /** @@ -15219,11 +15219,11 @@ return /******/ (function(modules) { // webpackBootstrap var DataView = __webpack_require__(8); var Component = __webpack_require__(22); var Group = __webpack_require__(31); - var BackgroundGroup = __webpack_require__(35); - var BoxItem = __webpack_require__(36); - var PointItem = __webpack_require__(37); - var RangeItem = __webpack_require__(33); - var BackgroundItem = __webpack_require__(38); + var BackgroundGroup = __webpack_require__(32); + var BoxItem = __webpack_require__(33); + var PointItem = __webpack_require__(35); + var RangeItem = __webpack_require__(36); + var BackgroundItem = __webpack_require__(37); var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items @@ -16693,8 +16693,8 @@ return /******/ (function(modules) { // webpackBootstrap /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); - var stack = __webpack_require__(32); - var RangeItem = __webpack_require__(33); + var stack = __webpack_require__(46); + var RangeItem = __webpack_require__(36); /** * @constructor Group @@ -16705,7 +16705,8 @@ return /******/ (function(modules) { // webpackBootstrap function Group (groupId, data, itemSet) { this.groupId = groupId; this.subgroups = {}; - this.visibleSubgroups = 0; + this.subgroupIndex = 0; + this.subgroupOrderer = data && data.subgroupOrder; this.itemSet = itemSet; this.dom = {}; @@ -16990,13 +16991,14 @@ return /******/ (function(modules) { // webpackBootstrap item.setParent(this); // add to - var index = 0; if (item.data.subgroup !== undefined) { if (this.subgroups[item.data.subgroup] === undefined) { - this.subgroups[item.data.subgroup] = {height:0, visible: false, index:index}; - index++; + this.subgroups[item.data.subgroup] = {height:0, visible: false, index:this.subgroupIndex, items: []}; + this.subgroupIndex++; } + this.subgroups[item.data.subgroup].items.push(item); } + this.orderSubgroups(); if (this.visibleItems.indexOf(item) == -1) { var range = this.itemSet.body.range; // TODO: not nice accessing the range like this @@ -17004,6 +17006,32 @@ return /******/ (function(modules) { // webpackBootstrap } }; + Group.prototype.orderSubgroups = function() { + if (this.subgroupOrderer !== undefined) { + var sortArray = []; + if (typeof this.subgroupOrderer == 'string') { + for (var subgroup in this.subgroups) { + sortArray.push({subgroup: subgroup, sortField: this.subgroups[subgroup].items[0].data[this.subgroupOrderer]}) + } + sortArray.sort(function (a, b) { + return a.sortField - b.sortField; + }) + } + else if (typeof this.subgroupOrderer == 'function') { + for (var subgroup in this.subgroups) { + sortArray.push(this.subgroups[subgroup].items[0].data); + } + sortArray.sort(this.subgroupOrderer); + } + + if (sortArray.length > 0) { + for (var i = 0; i < sortArray.length; i++) { + this.subgroups[sortArray[i].subgroup].index = i; + } + } + } + } + Group.prototype.resetSubgroups = function() { for (var subgroup in this.subgroups) { if (this.subgroups.hasOwnProperty(subgroup)) { @@ -17176,201 +17204,144 @@ return /******/ (function(modules) { // webpackBootstrap /* 32 */ /***/ function(module, exports, __webpack_require__) { - // Utility functions for ordering and stacking of items - var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors + var util = __webpack_require__(1); + var Group = __webpack_require__(31); /** - * Order items by their start data - * @param {Item[]} items + * @constructor BackgroundGroup + * @param {Number | String} groupId + * @param {Object} data + * @param {ItemSet} itemSet */ - exports.orderByStart = function(items) { - items.sort(function (a, b) { - return a.data.start - b.data.start; - }); - }; + function BackgroundGroup (groupId, data, itemSet) { + Group.call(this, groupId, data, itemSet); - /** - * Order items by their end date. If they have no end date, their start date - * is used. - * @param {Item[]} items - */ - exports.orderByEnd = function(items) { - items.sort(function (a, b) { - var aTime = ('end' in a.data) ? a.data.end : a.data.start, - bTime = ('end' in b.data) ? b.data.end : b.data.start; + this.width = 0; + this.height = 0; + this.top = 0; + this.left = 0; + } - return aTime - bTime; - }); - }; + BackgroundGroup.prototype = Object.create(Group.prototype); /** - * Adjust vertical positions of the items such that they don't overlap each - * other. - * @param {Item[]} items - * All visible items + * Repaint this group + * @param {{start: number, end: number}} range * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * Margins between items and between items and the axis. - * @param {boolean} [force=false] - * If true, all items will be repositioned. If false (default), only - * items having a top===null will be re-stacked + * @param {boolean} [restack=false] Force restacking of all items + * @return {boolean} Returns true if the group is resized */ - exports.stack = function(items, margin, force) { - var i, iMax; + BackgroundGroup.prototype.redraw = function(range, margin, restack) { + var resized = false; - if (force) { - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - items[i].top = null; - } - } + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - // calculate new, non-overlapping positions - for (i = 0, iMax = items.length; i < iMax; i++) { - var item = items[i]; - if (item.stack && item.top === null) { - // initialize top position - item.top = margin.axis; + // calculate actual size + this.width = this.dom.background.offsetWidth; - do { - // TODO: optimize checking for overlap. when there is a gap without items, - // you only need to check for items from the next item on, not from zero - var collidingItem = null; - for (var j = 0, jj = items.length; j < jj; j++) { - var other = items[j]; - if (other.top !== null && other !== item && other.stack && exports.collision(item, other, margin.item)) { - collidingItem = other; - break; - } - } + // apply new height (just always zero for BackgroundGroup + this.dom.background.style.height = '0'; - if (collidingItem != null) { - // There is a collision. Reposition the items above the colliding element - item.top = collidingItem.top + collidingItem.height + margin.item.vertical; - } - } while (collidingItem); - } + // update vertical position of items after they are re-stacked and the height of the group is calculated + for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { + var item = this.visibleItems[i]; + item.repositionY(margin); } - }; + return resized; + }; /** - * Adjust vertical positions of the items without stacking them - * @param {Item[]} items - * All visible items - * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * Margins between items and between items and the axis. + * Show this group: attach to the DOM */ - exports.nostack = function(items, margin, subgroups) { - var i, iMax, newTop; - - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - if (items[i].data.subgroup !== undefined) { - newTop = margin.axis; - for (var subgroup in subgroups) { - if (subgroups.hasOwnProperty(subgroup)) { - if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) { - newTop += subgroups[subgroup].height + margin.item.vertical; - } - } - } - items[i].top = newTop; - } - else { - items[i].top = margin.axis; - } + BackgroundGroup.prototype.show = function() { + if (!this.dom.background.parentNode) { + this.itemSet.dom.background.appendChild(this.dom.background); } }; - /** - * Test if the two provided items collide - * The items must have parameters left, width, top, and height. - * @param {Item} a The first item - * @param {Item} b The second item - * @param {{horizontal: number, vertical: number}} margin - * An object containing a horizontal and vertical - * minimum required margin. - * @return {boolean} true if a and b collide, else false - */ - exports.collision = function(a, b, margin) { - return ((a.left - margin.horizontal + EPSILON) < (b.left + b.width) && - (a.left + a.width + margin.horizontal - EPSILON) > b.left && - (a.top - margin.vertical + EPSILON) < (b.top + b.height) && - (a.top + a.height + margin.vertical - EPSILON) > b.top); - }; + module.exports = BackgroundGroup; /***/ }, /* 33 */ /***/ function(module, exports, __webpack_require__) { - var Hammer = __webpack_require__(18); var Item = __webpack_require__(34); + var util = __webpack_require__(1); /** - * @constructor RangeItem + * @constructor BoxItem * @extends Item - * @param {Object} data Object containing parameters start, end + * @param {Object} data Object containing parameters start * content, className. * @param {{toScreen: function, toTime: function}} conversion * Conversion functions from time to screen and vice versa * @param {Object} [options] Configuration options - * // TODO: describe options + * // TODO: describe available options */ - function RangeItem (data, conversion, options) { + function BoxItem (data, conversion, options) { this.props = { - content: { - width: 0 + dot: { + width: 0, + height: 0 + }, + line: { + width: 0, + height: 0 } }; - this.overflow = false; // if contents can overflow (css styling), this flag is set to true // validate data if (data) { if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data.id); - } - if (data.end == undefined) { - throw new Error('Property "end" missing in item ' + data.id); + throw new Error('Property "start" missing in item ' + data); } } Item.call(this, data, conversion, options); } - RangeItem.prototype = new Item (null, null, null); - - RangeItem.prototype.baseClassName = 'item range'; + BoxItem.prototype = new Item (null, null, null); /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ - RangeItem.prototype.isVisible = function(range) { + BoxItem.prototype.isVisible = function(range) { // determine visibility - return (this.data.start < range.end) && (this.data.end > range.start); + // TODO: account for the real width of the item. Right now we just add 1/4 to the window + var interval = (range.end - range.start) / 4; + return (this.data.start > range.start - interval) && (this.data.start < range.end + interval); }; /** * Repaint the item */ - RangeItem.prototype.redraw = function() { + BoxItem.prototype.redraw = function() { var dom = this.dom; if (!dom) { // create DOM this.dom = {}; dom = this.dom; - // background box - dom.box = document.createElement('div'); - // className is updated in redraw() + // create main box + dom.box = document.createElement('DIV'); - // contents box - dom.content = document.createElement('div'); + // contents box (inside the background box). used for making margins + dom.content = document.createElement('DIV'); dom.content.className = 'content'; dom.box.appendChild(dom.content); + // line to axis + dom.line = document.createElement('DIV'); + dom.line.className = 'line'; + + // dot on axis + dom.dot = document.createElement('DIV'); + dom.dot.className = 'dot'; + // attach this item as attribute dom.box['timeline-item'] = this; @@ -17383,11 +17354,19 @@ return /******/ (function(modules) { // webpackBootstrap } if (!dom.box.parentNode) { var foreground = this.parent.dom.foreground; - if (!foreground) { - throw new Error('Cannot redraw item: parent has no foreground container element'); - } + if (!foreground) throw new Error('Cannot redraw item: parent has no foreground container element'); foreground.appendChild(dom.box); } + if (!dom.line.parentNode) { + var background = this.parent.dom.background; + if (!background) throw new Error('Cannot redraw item: parent has no background container element'); + background.appendChild(dom.line); + } + if (!dom.dot.parentNode) { + var axis = this.parent.dom.axis; + if (!background) throw new Error('Cannot redraw item: parent has no axis container element'); + axis.appendChild(dom.dot); + } this.displayed = true; // Update DOM when item is marked dirty. An item is marked dirty when: @@ -17401,30 +17380,30 @@ return /******/ (function(modules) { // webpackBootstrap this._updateStyle(this.dom.box); // update class - var className = (this.data.className ? (' ' + this.data.className) : '') + + var className = (this.data.className? ' ' + this.data.className : '') + (this.selected ? ' selected' : ''); - dom.box.className = this.baseClassName + className; - - // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden'; + dom.box.className = 'item box' + className; + dom.line.className = 'item line' + className; + dom.dot.className = 'item dot' + className; // recalculate size - this.props.content.width = this.dom.content.offsetWidth; - this.height = this.dom.box.offsetHeight; + this.props.dot.height = dom.dot.offsetHeight; + this.props.dot.width = dom.dot.offsetWidth; + this.props.line.width = dom.line.offsetWidth; + this.width = dom.box.offsetWidth; + this.height = dom.box.offsetHeight; this.dirty = false; } this._repaintDeleteButton(dom.box); - this._repaintDragLeft(); - this._repaintDragRight(); }; /** - * Show the item in the DOM (when not already visible). The items DOM will + * Show the item in the DOM (when not already displayed). The items DOM will * be created when needed. */ - RangeItem.prototype.show = function() { + BoxItem.prototype.show = function() { if (!this.displayed) { this.redraw(); } @@ -17432,15 +17411,14 @@ return /******/ (function(modules) { // webpackBootstrap /** * Hide the item from the DOM (when visible) - * @return {Boolean} changed */ - RangeItem.prototype.hide = function() { + BoxItem.prototype.hide = function() { if (this.displayed) { - var box = this.dom.box; + var dom = this.dom; - if (box.parentNode) { - box.parentNode.removeChild(box); - } + if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box); + if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line); + if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot); this.top = null; this.left = null; @@ -17453,150 +17431,66 @@ return /******/ (function(modules) { // webpackBootstrap * Reposition the item horizontally * @Override */ - RangeItem.prototype.repositionX = function() { - var parentWidth = this.parent.width; + BoxItem.prototype.repositionX = function() { var start = this.conversion.toScreen(this.data.start); - var end = this.conversion.toScreen(this.data.end); - var contentLeft; - var contentWidth; - - // limit the width of the this, as browsers cannot draw very wide divs - if (start < -parentWidth) { - start = -parentWidth; - } - if (end > 2 * parentWidth) { - end = 2 * parentWidth; - } - var boxWidth = Math.max(end - start, 1); + var align = this.options.align; + var left; + var box = this.dom.box; + var line = this.dom.line; + var dot = this.dom.dot; - if (this.overflow) { + // calculate left position of the box + if (align == 'right') { + this.left = start - this.width; + } + else if (align == 'left') { this.left = start; - this.width = boxWidth + this.props.content.width; - contentWidth = this.props.content.width; - - // Note: The calculation of width is an optimistic calculation, giving - // a width which will not change when moving the Timeline - // So no re-stacking needed, which is nicer for the eye; } else { - this.left = start; - this.width = boxWidth; - contentWidth = Math.min(end - start, this.props.content.width); + // default or 'center' + this.left = start - this.width / 2; } - this.dom.box.style.left = this.left + 'px'; - this.dom.box.style.width = boxWidth + 'px'; - - switch (this.options.align) { - case 'left': - this.dom.content.style.left = '0'; - break; - - case 'right': - this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding), 0) + 'px'; - break; + // reposition box + box.style.left = this.left + 'px'; - case 'center': - this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + 'px'; - break; + // reposition line + line.style.left = (start - this.props.line.width / 2) + 'px'; - default: // 'auto' - if (this.overflow) { - // when range exceeds left of the window, position the contents at the left of the visible area - contentLeft = Math.max(-start, 0); - } - else { - // when range exceeds left of the window, position the contents at the left of the visible area - if (start < 0) { - contentLeft = Math.min(-start, - (end - start - this.props.content.width - 2 * this.options.padding)); - // TODO: remove the need for options.padding. it's terrible. - } - else { - contentLeft = 0; - } - } - this.dom.content.style.left = contentLeft + 'px'; - } + // reposition dot + dot.style.left = (start - this.props.dot.width / 2) + 'px'; }; /** * Reposition the item vertically * @Override */ - RangeItem.prototype.repositionY = function() { - var orientation = this.options.orientation, - box = this.dom.box; + BoxItem.prototype.repositionY = function() { + var orientation = this.options.orientation; + var box = this.dom.box; + var line = this.dom.line; + var dot = this.dom.dot; if (orientation == 'top') { - box.style.top = this.top + 'px'; - } - else { - box.style.top = (this.parent.height - this.top - this.height) + 'px'; - } - }; - - /** - * Repaint a drag area on the left side of the range when the range is selected - * @protected - */ - RangeItem.prototype._repaintDragLeft = function () { - if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) { - // create and show drag area - var dragLeft = document.createElement('div'); - dragLeft.className = 'drag-left'; - dragLeft.dragLeftItem = this; - - // TODO: this should be redundant? - Hammer(dragLeft, { - preventDefault: true - }).on('drag', function () { - //console.log('drag left') - }); + box.style.top = (this.top || 0) + 'px'; - this.dom.box.appendChild(dragLeft); - this.dom.dragLeft = dragLeft; - } - else if (!this.selected && this.dom.dragLeft) { - // delete drag area - if (this.dom.dragLeft.parentNode) { - this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft); - } - this.dom.dragLeft = null; + line.style.top = '0'; + line.style.height = (this.parent.top + this.top + 1) + 'px'; + line.style.bottom = ''; } - }; - - /** - * Repaint a drag area on the right side of the range when the range is selected - * @protected - */ - RangeItem.prototype._repaintDragRight = function () { - if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) { - // create and show drag area - var dragRight = document.createElement('div'); - dragRight.className = 'drag-right'; - dragRight.dragRightItem = this; - - // TODO: this should be redundant? - Hammer(dragRight, { - preventDefault: true - }).on('drag', function () { - //console.log('drag right') - }); + else { // orientation 'bottom' + var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty + var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top; - this.dom.box.appendChild(dragRight); - this.dom.dragRight = dragRight; - } - else if (!this.selected && this.dom.dragRight) { - // delete drag area - if (this.dom.dragRight.parentNode) { - this.dom.dragRight.parentNode.removeChild(this.dom.dragRight); - } - this.dom.dragRight = null; + box.style.top = (this.parent.height - this.top - this.height || 0) + 'px'; + line.style.top = (itemSetHeight - lineHeight) + 'px'; + line.style.bottom = '0'; } + + dot.style.top = (-this.props.dot.height / 2) + 'px'; }; - module.exports = RangeItem; + module.exports = BoxItem; /***/ }, @@ -17866,76 +17760,12 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, /* 35 */ -/***/ function(module, exports, __webpack_require__) { - - var util = __webpack_require__(1); - var Group = __webpack_require__(31); - - /** - * @constructor BackgroundGroup - * @param {Number | String} groupId - * @param {Object} data - * @param {ItemSet} itemSet - */ - function BackgroundGroup (groupId, data, itemSet) { - Group.call(this, groupId, data, itemSet); - - this.width = 0; - this.height = 0; - this.top = 0; - this.left = 0; - } - - BackgroundGroup.prototype = Object.create(Group.prototype); - - /** - * Repaint this group - * @param {{start: number, end: number}} range - * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * @param {boolean} [restack=false] Force restacking of all items - * @return {boolean} Returns true if the group is resized - */ - BackgroundGroup.prototype.redraw = function(range, margin, restack) { - var resized = false; - - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - - // calculate actual size - this.width = this.dom.background.offsetWidth; - - // apply new height (just always zero for BackgroundGroup - this.dom.background.style.height = '0'; - - // update vertical position of items after they are re-stacked and the height of the group is calculated - for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { - var item = this.visibleItems[i]; - item.repositionY(margin); - } - - return resized; - }; - - /** - * Show this group: attach to the DOM - */ - BackgroundGroup.prototype.show = function() { - if (!this.dom.background.parentNode) { - this.itemSet.dom.background.appendChild(this.dom.background); - } - }; - - module.exports = BackgroundGroup; - - -/***/ }, -/* 36 */ /***/ function(module, exports, __webpack_require__) { var Item = __webpack_require__(34); - var util = __webpack_require__(1); /** - * @constructor BoxItem + * @constructor PointItem * @extends Item * @param {Object} data Object containing parameters start * content, className. @@ -17944,15 +17774,16 @@ return /******/ (function(modules) { // webpackBootstrap * @param {Object} [options] Configuration options * // TODO: describe available options */ - function BoxItem (data, conversion, options) { + function PointItem (data, conversion, options) { this.props = { dot: { + top: 0, width: 0, height: 0 }, - line: { - width: 0, - height: 0 + content: { + height: 0, + marginLeft: 0 } }; @@ -17966,14 +17797,14 @@ return /******/ (function(modules) { // webpackBootstrap Item.call(this, data, conversion, options); } - BoxItem.prototype = new Item (null, null, null); + PointItem.prototype = new Item (null, null, null); /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ - BoxItem.prototype.isVisible = function(range) { + PointItem.prototype.isVisible = function(range) { // determine visibility // TODO: account for the real width of the item. Right now we just add 1/4 to the window var interval = (range.end - range.start) / 4; @@ -17983,31 +17814,28 @@ return /******/ (function(modules) { // webpackBootstrap /** * Repaint the item */ - BoxItem.prototype.redraw = function() { + PointItem.prototype.redraw = function() { var dom = this.dom; if (!dom) { // create DOM this.dom = {}; dom = this.dom; - // create main box - dom.box = document.createElement('DIV'); + // background box + dom.point = document.createElement('div'); + // className is updated in redraw() - // contents box (inside the background box). used for making margins - dom.content = document.createElement('DIV'); + // contents box, right from the dot + dom.content = document.createElement('div'); dom.content.className = 'content'; - dom.box.appendChild(dom.content); - - // line to axis - dom.line = document.createElement('DIV'); - dom.line.className = 'line'; + dom.point.appendChild(dom.content); - // dot on axis - dom.dot = document.createElement('DIV'); - dom.dot.className = 'dot'; + // dot at start + dom.dot = document.createElement('div'); + dom.point.appendChild(dom.dot); // attach this item as attribute - dom.box['timeline-item'] = this; + dom.point['timeline-item'] = this; this.dirty = true; } @@ -18016,20 +17844,12 @@ return /******/ (function(modules) { // webpackBootstrap if (!this.parent) { throw new Error('Cannot redraw item: no parent attached'); } - if (!dom.box.parentNode) { + if (!dom.point.parentNode) { var foreground = this.parent.dom.foreground; - if (!foreground) throw new Error('Cannot redraw item: parent has no foreground container element'); - foreground.appendChild(dom.box); - } - if (!dom.line.parentNode) { - var background = this.parent.dom.background; - if (!background) throw new Error('Cannot redraw item: parent has no background container element'); - background.appendChild(dom.line); - } - if (!dom.dot.parentNode) { - var axis = this.parent.dom.axis; - if (!background) throw new Error('Cannot redraw item: parent has no axis container element'); - axis.appendChild(dom.dot); + if (!foreground) { + throw new Error('Cannot redraw item: parent has no foreground container element'); + } + foreground.appendChild(dom.point); } this.displayed = true; @@ -18039,35 +17859,41 @@ return /******/ (function(modules) { // webpackBootstrap // - the item is selected/deselected if (this.dirty) { this._updateContents(this.dom.content); - this._updateTitle(this.dom.box); - this._updateDataAttributes(this.dom.box); - this._updateStyle(this.dom.box); + this._updateTitle(this.dom.point); + this._updateDataAttributes(this.dom.point); + this._updateStyle(this.dom.point); // update class var className = (this.data.className? ' ' + this.data.className : '') + (this.selected ? ' selected' : ''); - dom.box.className = 'item box' + className; - dom.line.className = 'item line' + className; + dom.point.className = 'item point' + className; dom.dot.className = 'item dot' + className; // recalculate size - this.props.dot.height = dom.dot.offsetHeight; + this.width = dom.point.offsetWidth; + this.height = dom.point.offsetHeight; this.props.dot.width = dom.dot.offsetWidth; - this.props.line.width = dom.line.offsetWidth; - this.width = dom.box.offsetWidth; - this.height = dom.box.offsetHeight; + this.props.dot.height = dom.dot.offsetHeight; + this.props.content.height = dom.content.offsetHeight; + + // resize contents + dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; + //dom.content.style.marginRight = ... + 'px'; // TODO: margin right + + dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px'; + dom.dot.style.left = (this.props.dot.width / 2) + 'px'; this.dirty = false; } - this._repaintDeleteButton(dom.box); + this._repaintDeleteButton(dom.point); }; /** - * Show the item in the DOM (when not already displayed). The items DOM will + * Show the item in the DOM (when not already visible). The items DOM will * be created when needed. */ - BoxItem.prototype.show = function() { + PointItem.prototype.show = function() { if (!this.displayed) { this.redraw(); } @@ -18076,13 +17902,11 @@ return /******/ (function(modules) { // webpackBootstrap /** * Hide the item from the DOM (when visible) */ - BoxItem.prototype.hide = function() { + PointItem.prototype.hide = function() { if (this.displayed) { - var dom = this.dom; - - if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box); - if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line); - if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot); + if (this.dom.point.parentNode) { + this.dom.point.parentNode.removeChild(this.dom.point); + } this.top = null; this.left = null; @@ -18095,146 +17919,107 @@ return /******/ (function(modules) { // webpackBootstrap * Reposition the item horizontally * @Override */ - BoxItem.prototype.repositionX = function() { + PointItem.prototype.repositionX = function() { var start = this.conversion.toScreen(this.data.start); - var align = this.options.align; - var left; - var box = this.dom.box; - var line = this.dom.line; - var dot = this.dom.dot; - - // calculate left position of the box - if (align == 'right') { - this.left = start - this.width; - } - else if (align == 'left') { - this.left = start; - } - else { - // default or 'center' - this.left = start - this.width / 2; - } - - // reposition box - box.style.left = this.left + 'px'; - // reposition line - line.style.left = (start - this.props.line.width / 2) + 'px'; + this.left = start - this.props.dot.width; - // reposition dot - dot.style.left = (start - this.props.dot.width / 2) + 'px'; + // reposition point + this.dom.point.style.left = this.left + 'px'; }; /** * Reposition the item vertically * @Override */ - BoxItem.prototype.repositionY = function() { - var orientation = this.options.orientation; - var box = this.dom.box; - var line = this.dom.line; - var dot = this.dom.dot; + PointItem.prototype.repositionY = function() { + var orientation = this.options.orientation, + point = this.dom.point; if (orientation == 'top') { - box.style.top = (this.top || 0) + 'px'; - - line.style.top = '0'; - line.style.height = (this.parent.top + this.top + 1) + 'px'; - line.style.bottom = ''; + point.style.top = this.top + 'px'; } - else { // orientation 'bottom' - var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty - var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top; - - box.style.top = (this.parent.height - this.top - this.height || 0) + 'px'; - line.style.top = (itemSetHeight - lineHeight) + 'px'; - line.style.bottom = '0'; + else { + point.style.top = (this.parent.height - this.top - this.height) + 'px'; } - - dot.style.top = (-this.props.dot.height / 2) + 'px'; }; - module.exports = BoxItem; + module.exports = PointItem; /***/ }, -/* 37 */ +/* 36 */ /***/ function(module, exports, __webpack_require__) { + var Hammer = __webpack_require__(18); var Item = __webpack_require__(34); /** - * @constructor PointItem + * @constructor RangeItem * @extends Item - * @param {Object} data Object containing parameters start + * @param {Object} data Object containing parameters start, end * content, className. * @param {{toScreen: function, toTime: function}} conversion * Conversion functions from time to screen and vice versa * @param {Object} [options] Configuration options - * // TODO: describe available options + * // TODO: describe options */ - function PointItem (data, conversion, options) { + function RangeItem (data, conversion, options) { this.props = { - dot: { - top: 0, - width: 0, - height: 0 - }, content: { - height: 0, - marginLeft: 0 + width: 0 } }; + this.overflow = false; // if contents can overflow (css styling), this flag is set to true // validate data if (data) { if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data); + throw new Error('Property "start" missing in item ' + data.id); + } + if (data.end == undefined) { + throw new Error('Property "end" missing in item ' + data.id); } } Item.call(this, data, conversion, options); } - PointItem.prototype = new Item (null, null, null); + RangeItem.prototype = new Item (null, null, null); + + RangeItem.prototype.baseClassName = 'item range'; /** * Check whether this item is visible inside given range * @returns {{start: Number, end: Number}} range with a timestamp for start and end * @returns {boolean} True if visible */ - PointItem.prototype.isVisible = function(range) { + RangeItem.prototype.isVisible = function(range) { // determine visibility - // TODO: account for the real width of the item. Right now we just add 1/4 to the window - var interval = (range.end - range.start) / 4; - return (this.data.start > range.start - interval) && (this.data.start < range.end + interval); + return (this.data.start < range.end) && (this.data.end > range.start); }; /** * Repaint the item */ - PointItem.prototype.redraw = function() { + RangeItem.prototype.redraw = function() { var dom = this.dom; if (!dom) { // create DOM this.dom = {}; dom = this.dom; - // background box - dom.point = document.createElement('div'); + // background box + dom.box = document.createElement('div'); // className is updated in redraw() - // contents box, right from the dot + // contents box dom.content = document.createElement('div'); dom.content.className = 'content'; - dom.point.appendChild(dom.content); - - // dot at start - dom.dot = document.createElement('div'); - dom.point.appendChild(dom.dot); + dom.box.appendChild(dom.content); // attach this item as attribute - dom.point['timeline-item'] = this; + dom.box['timeline-item'] = this; this.dirty = true; } @@ -18243,12 +18028,12 @@ return /******/ (function(modules) { // webpackBootstrap if (!this.parent) { throw new Error('Cannot redraw item: no parent attached'); } - if (!dom.point.parentNode) { + if (!dom.box.parentNode) { var foreground = this.parent.dom.foreground; if (!foreground) { throw new Error('Cannot redraw item: parent has no foreground container element'); } - foreground.appendChild(dom.point); + foreground.appendChild(dom.box); } this.displayed = true; @@ -18258,102 +18043,217 @@ return /******/ (function(modules) { // webpackBootstrap // - the item is selected/deselected if (this.dirty) { this._updateContents(this.dom.content); - this._updateTitle(this.dom.point); - this._updateDataAttributes(this.dom.point); - this._updateStyle(this.dom.point); + this._updateTitle(this.dom.box); + this._updateDataAttributes(this.dom.box); + this._updateStyle(this.dom.box); // update class - var className = (this.data.className? ' ' + this.data.className : '') + + var className = (this.data.className ? (' ' + this.data.className) : '') + (this.selected ? ' selected' : ''); - dom.point.className = 'item point' + className; - dom.dot.className = 'item dot' + className; + dom.box.className = this.baseClassName + className; + + // determine from css whether this box has overflow + this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden'; // recalculate size - this.width = dom.point.offsetWidth; - this.height = dom.point.offsetHeight; - this.props.dot.width = dom.dot.offsetWidth; - this.props.dot.height = dom.dot.offsetHeight; - this.props.content.height = dom.content.offsetHeight; + this.props.content.width = this.dom.content.offsetWidth; + this.height = this.dom.box.offsetHeight; - // resize contents - dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; - //dom.content.style.marginRight = ... + 'px'; // TODO: margin right + this.dirty = false; + } - dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px'; - dom.dot.style.left = (this.props.dot.width / 2) + 'px'; + this._repaintDeleteButton(dom.box); + this._repaintDragLeft(); + this._repaintDragRight(); + }; - this.dirty = false; + /** + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. + */ + RangeItem.prototype.show = function() { + if (!this.displayed) { + this.redraw(); } + }; - this._repaintDeleteButton(dom.point); + /** + * Hide the item from the DOM (when visible) + * @return {Boolean} changed + */ + RangeItem.prototype.hide = function() { + if (this.displayed) { + var box = this.dom.box; + + if (box.parentNode) { + box.parentNode.removeChild(box); + } + + this.top = null; + this.left = null; + + this.displayed = false; + } }; - /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. - */ - PointItem.prototype.show = function() { - if (!this.displayed) { - this.redraw(); + /** + * Reposition the item horizontally + * @Override + */ + RangeItem.prototype.repositionX = function() { + var parentWidth = this.parent.width; + var start = this.conversion.toScreen(this.data.start); + var end = this.conversion.toScreen(this.data.end); + var contentLeft; + var contentWidth; + + // limit the width of the this, as browsers cannot draw very wide divs + if (start < -parentWidth) { + start = -parentWidth; + } + if (end > 2 * parentWidth) { + end = 2 * parentWidth; + } + var boxWidth = Math.max(end - start, 1); + + if (this.overflow) { + this.left = start; + this.width = boxWidth + this.props.content.width; + contentWidth = this.props.content.width; + + // Note: The calculation of width is an optimistic calculation, giving + // a width which will not change when moving the Timeline + // So no re-stacking needed, which is nicer for the eye; + } + else { + this.left = start; + this.width = boxWidth; + contentWidth = Math.min(end - start, this.props.content.width); + } + + this.dom.box.style.left = this.left + 'px'; + this.dom.box.style.width = boxWidth + 'px'; + + switch (this.options.align) { + case 'left': + this.dom.content.style.left = '0'; + break; + + case 'right': + this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding), 0) + 'px'; + break; + + case 'center': + this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + 'px'; + break; + + default: // 'auto' + if (this.overflow) { + // when range exceeds left of the window, position the contents at the left of the visible area + contentLeft = Math.max(-start, 0); + } + else { + // when range exceeds left of the window, position the contents at the left of the visible area + if (start < 0) { + contentLeft = Math.min(-start, + (end - start - this.props.content.width - 2 * this.options.padding)); + // TODO: remove the need for options.padding. it's terrible. + } + else { + contentLeft = 0; + } + } + this.dom.content.style.left = contentLeft + 'px'; } }; /** - * Hide the item from the DOM (when visible) + * Reposition the item vertically + * @Override */ - PointItem.prototype.hide = function() { - if (this.displayed) { - if (this.dom.point.parentNode) { - this.dom.point.parentNode.removeChild(this.dom.point); - } - - this.top = null; - this.left = null; + RangeItem.prototype.repositionY = function() { + var orientation = this.options.orientation, + box = this.dom.box; - this.displayed = false; + if (orientation == 'top') { + box.style.top = this.top + 'px'; + } + else { + box.style.top = (this.parent.height - this.top - this.height) + 'px'; } }; /** - * Reposition the item horizontally - * @Override + * Repaint a drag area on the left side of the range when the range is selected + * @protected */ - PointItem.prototype.repositionX = function() { - var start = this.conversion.toScreen(this.data.start); + RangeItem.prototype._repaintDragLeft = function () { + if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) { + // create and show drag area + var dragLeft = document.createElement('div'); + dragLeft.className = 'drag-left'; + dragLeft.dragLeftItem = this; - this.left = start - this.props.dot.width; + // TODO: this should be redundant? + Hammer(dragLeft, { + preventDefault: true + }).on('drag', function () { + //console.log('drag left') + }); - // reposition point - this.dom.point.style.left = this.left + 'px'; + this.dom.box.appendChild(dragLeft); + this.dom.dragLeft = dragLeft; + } + else if (!this.selected && this.dom.dragLeft) { + // delete drag area + if (this.dom.dragLeft.parentNode) { + this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft); + } + this.dom.dragLeft = null; + } }; /** - * Reposition the item vertically - * @Override + * Repaint a drag area on the right side of the range when the range is selected + * @protected */ - PointItem.prototype.repositionY = function() { - var orientation = this.options.orientation, - point = this.dom.point; + RangeItem.prototype._repaintDragRight = function () { + if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) { + // create and show drag area + var dragRight = document.createElement('div'); + dragRight.className = 'drag-right'; + dragRight.dragRightItem = this; - if (orientation == 'top') { - point.style.top = this.top + 'px'; + // TODO: this should be redundant? + Hammer(dragRight, { + preventDefault: true + }).on('drag', function () { + //console.log('drag right') + }); + + this.dom.box.appendChild(dragRight); + this.dom.dragRight = dragRight; } - else { - point.style.top = (this.parent.height - this.top - this.height) + 'px'; + else if (!this.selected && this.dom.dragRight) { + // delete drag area + if (this.dom.dragRight.parentNode) { + this.dom.dragRight.parentNode.removeChild(this.dom.dragRight); + } + this.dom.dragRight = null; } }; - module.exports = PointItem; + module.exports = RangeItem; /***/ }, -/* 38 */ +/* 37 */ /***/ function(module, exports, __webpack_require__) { var Hammer = __webpack_require__(18); var Item = __webpack_require__(34); - var BackgroundGroup = __webpack_require__(35); - var RangeItem = __webpack_require__(33); + var BackgroundGroup = __webpack_require__(32); + var RangeItem = __webpack_require__(36); /** * @constructor BackgroundItem @@ -18558,10 +18458,10 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 39 */ +/* 38 */ /***/ function(module, exports, __webpack_require__) { - var mousetrap = __webpack_require__(40); + var mousetrap = __webpack_require__(39); var Emitter = __webpack_require__(10); var Hammer = __webpack_require__(18); var util = __webpack_require__(1); @@ -18710,7 +18610,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 40 */ +/* 39 */ /***/ function(module, exports, __webpack_require__) { /** @@ -19515,7 +19415,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 41 */ +/* 40 */ /***/ function(module, exports, __webpack_require__) { var Emitter = __webpack_require__(10); @@ -19528,7 +19428,7 @@ return /******/ (function(modules) { // webpackBootstrap var TimeAxis = __webpack_require__(25); var CurrentTime = __webpack_require__(27); var CustomTime = __webpack_require__(29); - var LineGraph = __webpack_require__(42); + var LineGraph = __webpack_require__(41); /** * Create a timeline visualization @@ -19765,7 +19665,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 42 */ +/* 41 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); @@ -19773,9 +19673,9 @@ return /******/ (function(modules) { // webpackBootstrap var DataSet = __webpack_require__(7); var DataView = __webpack_require__(8); var Component = __webpack_require__(22); - var DataAxis = __webpack_require__(43); - var GraphGroup = __webpack_require__(45); - var Legend = __webpack_require__(46); + var DataAxis = __webpack_require__(42); + var GraphGroup = __webpack_require__(44); + var Legend = __webpack_require__(45); var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items @@ -21072,13 +20972,13 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 43 */ +/* 42 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); var DOMutil = __webpack_require__(6); var Component = __webpack_require__(22); - var DataStep = __webpack_require__(44); + var DataStep = __webpack_require__(43); /** * A horizontal time axis @@ -21579,7 +21479,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 44 */ +/* 43 */ /***/ function(module, exports, __webpack_require__) { /** @@ -21807,7 +21707,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 45 */ +/* 44 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); @@ -21948,7 +21848,7 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 46 */ +/* 45 */ /***/ function(module, exports, __webpack_require__) { var util = __webpack_require__(1); @@ -22151,13 +22051,141 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = Legend; +/***/ }, +/* 46 */ +/***/ function(module, exports, __webpack_require__) { + + // Utility functions for ordering and stacking of items + var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors + + /** + * Order items by their start data + * @param {Item[]} items + */ + exports.orderByStart = function(items) { + items.sort(function (a, b) { + return a.data.start - b.data.start; + }); + }; + + /** + * Order items by their end date. If they have no end date, their start date + * is used. + * @param {Item[]} items + */ + exports.orderByEnd = function(items) { + items.sort(function (a, b) { + var aTime = ('end' in a.data) ? a.data.end : a.data.start, + bTime = ('end' in b.data) ? b.data.end : b.data.start; + + return aTime - bTime; + }); + }; + + /** + * Adjust vertical positions of the items such that they don't overlap each + * other. + * @param {Item[]} items + * All visible items + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * Margins between items and between items and the axis. + * @param {boolean} [force=false] + * If true, all items will be repositioned. If false (default), only + * items having a top===null will be re-stacked + */ + exports.stack = function(items, margin, force) { + var i, iMax; + + if (force) { + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + items[i].top = null; + } + } + + // calculate new, non-overlapping positions + for (i = 0, iMax = items.length; i < iMax; i++) { + var item = items[i]; + if (item.stack && item.top === null) { + // initialize top position + item.top = margin.axis; + + do { + // TODO: optimize checking for overlap. when there is a gap without items, + // you only need to check for items from the next item on, not from zero + var collidingItem = null; + for (var j = 0, jj = items.length; j < jj; j++) { + var other = items[j]; + if (other.top !== null && other !== item && other.stack && exports.collision(item, other, margin.item)) { + collidingItem = other; + break; + } + } + + if (collidingItem != null) { + // There is a collision. Reposition the items above the colliding element + item.top = collidingItem.top + collidingItem.height + margin.item.vertical; + } + } while (collidingItem); + } + } + }; + + + /** + * Adjust vertical positions of the items without stacking them + * @param {Item[]} items + * All visible items + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * Margins between items and between items and the axis. + */ + exports.nostack = function(items, margin, subgroups) { + var i, iMax, newTop; + + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + if (items[i].data.subgroup !== undefined) { + newTop = margin.axis; + for (var subgroup in subgroups) { + if (subgroups.hasOwnProperty(subgroup)) { + if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) { + newTop += subgroups[subgroup].height + margin.item.vertical; + } + } + } + items[i].top = newTop; + } + else { + items[i].top = margin.axis; + } + } + }; + + /** + * Test if the two provided items collide + * The items must have parameters left, width, top, and height. + * @param {Item} a The first item + * @param {Item} b The second item + * @param {{horizontal: number, vertical: number}} margin + * An object containing a horizontal and vertical + * minimum required margin. + * @return {boolean} true if a and b collide, else false + */ + exports.collision = function(a, b, margin) { + return ((a.left - margin.horizontal + EPSILON) < (b.left + b.width) && + (a.left + a.width + margin.horizontal - EPSILON) > b.left && + (a.top - margin.vertical + EPSILON) < (b.top + b.height) && + (a.top + a.height + margin.vertical - EPSILON) > b.top); + }; + + /***/ }, /* 47 */ /***/ function(module, exports, __webpack_require__) { var Emitter = __webpack_require__(10); var Hammer = __webpack_require__(18); - var mousetrap = __webpack_require__(40); + var mousetrap = __webpack_require__(39); var util = __webpack_require__(1); var hammerUtil = __webpack_require__(21); var DataSet = __webpack_require__(7); @@ -22170,7 +22198,7 @@ return /******/ (function(modules) { // webpackBootstrap var Edge = __webpack_require__(53); var Popup = __webpack_require__(54); var MixinLoader = __webpack_require__(55); - var Activator = __webpack_require__(39); + var Activator = __webpack_require__(38); var locales = __webpack_require__(66); // Load custom shapes into CanvasRenderingContext2D diff --git a/docs/timeline.html b/docs/timeline.html index de2e2175..05d970b6 100644 --- a/docs/timeline.html +++ b/docs/timeline.html @@ -347,6 +347,14 @@ var groups = [ The title can only contain plain text. + + subgroupOrder + String | Function + none + Order the subgroups by a field name or custom sort function. + By default, groups are ordered by first-come, first-show. + + diff --git a/examples/timeline/30_subgroups.html b/examples/timeline/30_subgroups.html index 86511fb3..4c342658 100644 --- a/examples/timeline/30_subgroups.html +++ b/examples/timeline/30_subgroups.html @@ -25,7 +25,7 @@ -

This example demonstrates the item type "background", see "Period A" and "Period B". The background areas can be styled with css.

+

This example shows the workings of the subgroups. Subgroups do not use stacking, and only work when stacking is disabled.

@@ -38,23 +38,25 @@ type: { start: 'ISODate', end: 'ISODate' } }); var groups = new vis.DataSet([{ - id: 'bar', content:'bar' + id: 'bar', content:'bar', subgroupOrder: function (a,b) {return a.subgroupOrder - b.subgroupOrder;} },{ - id: 'foo', content:'foo' + id: 'foo', content:'foo', subgroupOrder: 'subgroupOrder' // this group has no subgroups but this would be the other method to do the sorting. }]); // add items to the DataSet items.add([ - {id: 'A', content:'1',start: '2014-01-20', end: '2014-01-22', type: 'background', group:'foo'}, - {id: 'B', content:'1',start: '2014-01-22', end: '2014-01-23', type: 'background', group:'foo', className: 'negative'}, - {id: 0, content: 'item 4', start: '2014-01-20', end: '2014-01-22',group:'foo'}, + {id: 'A',start: '2014-01-20', end: '2014-01-22', type: 'background', group:'foo'}, + {id: 'B',start: '2014-01-22', end: '2014-01-23', type: 'background', group:'foo', className: 'negative'}, + {id: 0, content: 'no subgroup', start: '2014-01-20', end: '2014-01-22',group:'foo'}, - {id: 'ab', content:'1',start: '2014-01-25', end: '2014-01-27', type: 'background', group:'bar', subgroup:'banana'}, - {id: 'bb', content:'1',start: '2014-01-26', end: '2014-01-27', type: 'background', className: 'positive',group:'bar', subgroup:'banana'}, - {id: 1, content: '0', start: '2014-01-25 12:00:00', end: '2014-01-26 12:00:00',group:'bar', subgroup:'banana'}, - {id: 'aab', content:'1',start: '2014-01-27', end: '2014-01-29', type: 'background', group:'bar', subgroup:'putty'}, - {id: 'bab', content:'1',start: '2014-01-27', end: '2014-01-28', type: 'background', className: 'negative',group:'bar', subgroup:'putty'}, - {id: 'bdab', content:'1',start: '2014-01-29', end: '2014-01-30', type: 'background', className: 'negative',group:'bar'}, - {id: 2, content: 'subgroup1', start: '2014-01-27', end: '2014-01-29',group:'bar', subgroup:'putty'}, + {id: 'SG_1_1',start: '2014-01-25', end: '2014-01-27', type: 'background', group:'bar', subgroup:'sg_1', subgroupOrder:0}, + {id: 'SG_1_2', start: '2014-01-26', end: '2014-01-27', type: 'background', className: 'positive',group:'bar', subgroup:'sg_1', subgroupOrder:0}, + {id: 1, content: 'subgroup0', start: '2014-01-23 12:00:00', end: '2014-01-26 12:00:00',group:'bar', subgroup:'sg_1', subgroupOrder:0}, + {id: 'SG_2_1', start: '2014-01-27', end: '2014-01-29', type: 'background', group:'bar', subgroup:'sg_2', subgroupOrder:1}, + {id: 'SG_2_2', start: '2014-01-27', end: '2014-01-28', type: 'background', className: 'negative',group:'bar', subgroup:'sg_2', subgroupOrder:1}, + {id: 2, content: 'subgroup1', start: '2014-01-27', end: '2014-01-29',group:'bar', subgroup:'sg_2', subgroupOrder:1}, + + {id: 'background', start: '2014-01-29', end: '2014-01-30', type: 'background', className: 'negative',group:'bar'}, + {id: 'background_all', start: '2014-01-31', end: '2014-02-02', type: 'background', className: 'positive'}, ]); var container = document.getElementById('visualization'); diff --git a/lib/timeline/component/Group.js b/lib/timeline/component/Group.js index bb986a25..69ad46a4 100644 --- a/lib/timeline/component/Group.js +++ b/lib/timeline/component/Group.js @@ -11,7 +11,8 @@ var RangeItem = require('./item/RangeItem'); function Group (groupId, data, itemSet) { this.groupId = groupId; this.subgroups = {}; - this.visibleSubgroups = 0; + this.subgroupIndex = 0; + this.subgroupOrderer = data && data.subgroupOrder; this.itemSet = itemSet; this.dom = {}; @@ -296,13 +297,14 @@ Group.prototype.add = function(item) { item.setParent(this); // add to - var index = 0; if (item.data.subgroup !== undefined) { if (this.subgroups[item.data.subgroup] === undefined) { - this.subgroups[item.data.subgroup] = {height:0, visible: false, index:index}; - index++; + this.subgroups[item.data.subgroup] = {height:0, visible: false, index:this.subgroupIndex, items: []}; + this.subgroupIndex++; } + this.subgroups[item.data.subgroup].items.push(item); } + this.orderSubgroups(); if (this.visibleItems.indexOf(item) == -1) { var range = this.itemSet.body.range; // TODO: not nice accessing the range like this @@ -310,6 +312,32 @@ Group.prototype.add = function(item) { } }; +Group.prototype.orderSubgroups = function() { + if (this.subgroupOrderer !== undefined) { + var sortArray = []; + if (typeof this.subgroupOrderer == 'string') { + for (var subgroup in this.subgroups) { + sortArray.push({subgroup: subgroup, sortField: this.subgroups[subgroup].items[0].data[this.subgroupOrderer]}) + } + sortArray.sort(function (a, b) { + return a.sortField - b.sortField; + }) + } + else if (typeof this.subgroupOrderer == 'function') { + for (var subgroup in this.subgroups) { + sortArray.push(this.subgroups[subgroup].items[0].data); + } + sortArray.sort(this.subgroupOrderer); + } + + if (sortArray.length > 0) { + for (var i = 0; i < sortArray.length; i++) { + this.subgroups[sortArray[i].subgroup].index = i; + } + } + } +} + Group.prototype.resetSubgroups = function() { for (var subgroup in this.subgroups) { if (this.subgroups.hasOwnProperty(subgroup)) {