diff --git a/docs/graph2d/index.html b/docs/graph2d/index.html index 8f07e20a..66a488f0 100644 --- a/docs/graph2d/index.html +++ b/docs/graph2d/index.html @@ -1162,7 +1162,7 @@ function (option, path) {
  • x (Number): relative horizontal position of the click event.
  • y (Number): relative vertical position of the click event.
  • time (Date): Date of the clicked event.
  • -
  • value (Number[]): The data value of the click event. The array contains one value when there is one data axis visible, and two values when there are two visible data axes.
  • +
  • value (Number[]): The data value of the click event. The array contains one value when there is one data axis visible, and two values when there are two visible data axes. It is empty when no data is provided.
  • what (String or null): name of the clicked thing: background, axis, dat-axis, custom-time, or current-time, legend.
  • event (Object): the original click event.
  • diff --git a/docs/timeline/index.html b/docs/timeline/index.html index 9a623722..e0d3291c 100644 --- a/docs/timeline/index.html +++ b/docs/timeline/index.html @@ -679,7 +679,7 @@ function (option, path) { groupTemplate function none - A template function used to generate the contents of the groups. The function is called by the Timeline with a groups data as the first argument and the group element as the second, and must return HTML code as result. When the option groupTemplate is specified, the groups do not need to have a field content. See section Templates for a detailed explanation. + A template function used to generate the contents of the groups. The function is called by the Timeline with a groups data as the first argument and the group element as the second, and must return HTML code, a string or a template as result. When the option groupTemplate is specified, the groups do not need to have a field content. See section Templates for a detailed explanation. @@ -943,7 +943,14 @@ function (option, path) { Orientation of the timeline items: 'top' or 'bottom' (default). Determines whether items are aligned to the top or bottom of the Timeline. - + + rollingMode + boolean + false + If true, the timeline will initial in a rolling mode - the current time will always be centered. I the user drags the timeline, the timeline will go out of rolling mode and a toggle button will appear. Clicking that button will go back to rolling mode. Zooming in rolling mode will zoom in to the center without consideration of the mouse position. + + + rtl boolean false @@ -1018,7 +1025,15 @@ function (option, path) { template function none - A template function used to generate the contents of the items. The function is called by the Timeline with an items data as the first argument and the item element as the second, and must return HTML code as result. When the option template is specified, the items do not need to have a field content. See section Templates for a detailed explanation. + A template function used to generate the contents of the items. The function is called by the Timeline with an items' data as the first argument and the item element as the second, and must return HTML code, a string or a template as result. When the option template is specified, the items do not need to have a field content. See section Templates for a detailed explanation. + + + + visibleFrameTemplate + function + none + A template function used to generate the visible frame of the items. The function is called by the Timeline with an items' data as the first argument and the item frame element as the second, and must return HTML code, a string or a template as result. When the option template is specified, the items do not need to have a field content. See section Templates for a detailed explanation. + This would be used as an additional way to add content that is constant in size with the visible frame of the item and does not get visibly hidden with the item's internal container: vis-item-overflow which is overflow:hidden. @@ -1065,7 +1080,7 @@ function (option, path) { template Function none - A template function used to generate the contents of the tooltip. The function is called by the Timeline with an item data as the first argument, and must return HTML code as result. See section Templates for a detailed explanation. + A template function used to generate the contents of the tooltip. The function is called by the Timeline with an item data as the first argument, and must return HTML code, a string or a template as result. See section Templates for a detailed explanation. diff --git a/examples/timeline/interaction/rollingMode.html b/examples/timeline/interaction/rollingMode.html new file mode 100644 index 00000000..80b599f4 --- /dev/null +++ b/examples/timeline/interaction/rollingMode.html @@ -0,0 +1,45 @@ + + + Timeline | rolling mode Option + + + + + + + + + +

    Timeline rolling mode option

    + +
    + + + + + + diff --git a/examples/timeline/items/visibleFrameTemplateContent.html b/examples/timeline/items/visibleFrameTemplateContent.html new file mode 100644 index 00000000..67715766 --- /dev/null +++ b/examples/timeline/items/visibleFrameTemplateContent.html @@ -0,0 +1,67 @@ + + + + Timeline | Dynamic Content + + + + + + + + +
    + + + + \ No newline at end of file diff --git a/lib/timeline/Core.js b/lib/timeline/Core.js index a5034479..d3376e1e 100644 --- a/lib/timeline/Core.js +++ b/lib/timeline/Core.js @@ -50,6 +50,7 @@ Core.prototype._create = function (container) { this.dom.shadowBottomLeft = document.createElement('div'); this.dom.shadowTopRight = document.createElement('div'); this.dom.shadowBottomRight = document.createElement('div'); + this.dom.rollingModeBtn = document.createElement('div'); this.dom.root.className = 'vis-timeline'; this.dom.background.className = 'vis-panel vis-background'; @@ -69,6 +70,7 @@ Core.prototype._create = function (container) { this.dom.shadowBottomLeft.className = 'vis-shadow vis-bottom'; this.dom.shadowTopRight.className = 'vis-shadow vis-top'; this.dom.shadowBottomRight.className = 'vis-shadow vis-bottom'; + this.dom.rollingModeBtn.className = 'vis-rolling-mode-btn'; this.dom.root.appendChild(this.dom.background); this.dom.root.appendChild(this.dom.backgroundVertical); @@ -78,6 +80,8 @@ Core.prototype._create = function (container) { this.dom.root.appendChild(this.dom.rightContainer); this.dom.root.appendChild(this.dom.top); this.dom.root.appendChild(this.dom.bottom); + this.dom.root.appendChild(this.dom.bottom); + this.dom.root.appendChild(this.dom.rollingModeBtn); this.dom.centerContainer.appendChild(this.dom.center); this.dom.leftContainer.appendChild(this.dom.left); @@ -120,7 +124,7 @@ Core.prototype._create = function (container) { this._redraw = util.throttle(this._origRedraw); this.on('_change', function (properties) { - if (me.itemSet.initialItemSetDrawn && properties && properties.queue == true) { + if (me.itemSet && me.itemSet.initialItemSetDrawn && properties && properties.queue == true) { me._redraw() } else { me._origRedraw(); @@ -311,6 +315,8 @@ Core.prototype.setOptions = function (options) { ]; util.selectiveExtend(fields, this.options, options); + this.dom.rollingModeBtn.style.visibility = 'hidden'; + if (this.options.rtl) { this.dom.container.style.direction = "rtl"; this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical-rtl'; diff --git a/lib/timeline/Graph2d.js b/lib/timeline/Graph2d.js index cd350ec3..0e294592 100644 --- a/lib/timeline/Graph2d.js +++ b/lib/timeline/Graph2d.js @@ -299,10 +299,10 @@ Graph2d.prototype.getEventProperties = function (event) { var value = []; var yAxisLeft = this.linegraph.yAxisLeft; var yAxisRight = this.linegraph.yAxisRight; - if (!yAxisLeft.hidden) { + if (!yAxisLeft.hidden && this.itemsData.length > 0) { value.push(yAxisLeft.screenToValue(y)); } - if (!yAxisRight.hidden) { + if (!yAxisRight.hidden && this.itemsData.length > 0) { value.push(yAxisRight.screenToValue(y)); } diff --git a/lib/timeline/Range.js b/lib/timeline/Range.js index 3e250f38..e49951d4 100644 --- a/lib/timeline/Range.js +++ b/lib/timeline/Range.js @@ -14,8 +14,18 @@ var DateUtil = require('./DateUtil'); */ function Range(body, options) { var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); - this.start = now.clone().add(-3, 'days').valueOf(); // Number - this.end = now.clone().add(4, 'days').valueOf(); // Number + var start = now.clone().add(-3, 'days').valueOf(); + var end = now.clone().add(-3, 'days').valueOf(); + + if(options === undefined) { + this.start = start; + this.end = end; + } else { + this.start = options.start || start + this.end = options.end || end + } + + this.rolling = false; this.body = body; this.deltaDifference = 0; @@ -55,6 +65,9 @@ function Range(body, options) { this.body.emitter.on('touch', this._onTouch.bind(this)); this.body.emitter.on('pinch', this._onPinch.bind(this)); + // on click of rolling mode button + this.body.dom.rollingModeBtn.addEventListener('click', this.startRolling.bind(this)); + this.setOptions(options); } @@ -80,11 +93,14 @@ Range.prototype.setOptions = function (options) { if (options) { // copy the options that we know var fields = [ - 'direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', - 'moment', 'activate', 'hiddenDates', 'zoomKey', 'rtl', 'horizontalScroll' + 'animation', 'direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', + 'moment', 'activate', 'hiddenDates', 'zoomKey', 'rtl', 'showCurrentTime', 'rollMode', 'horizontalScroll' ]; util.selectiveExtend(fields, this.options, options); + if (options.rollingMode) { + this.startRolling(); + } if ('start' in options || 'end' in options) { // apply a new range. both start and end are optional this.setRange(options.start, options.end); @@ -103,6 +119,52 @@ function validateDirection (direction) { } } +/** + * Start auto refreshing the current time bar + */ +Range.prototype.startRolling = function() { + var me = this; + + + function update () { + me.stopRolling(); + me.rolling = true; + + + var interval = me.end - me.start; + var t = util.convert(new Date(), 'Date').valueOf(); + + var start = t - interval / 2; + var end = t + interval / 2; + var animation = (me.options && me.options.animation !== undefined) ? me.options.animation : true; + + me.setRange(start, end, false); + + // determine interval to refresh + var scale = me.conversion(me.body.domProps.center.width).scale; + var interval = 1 / scale / 10; + if (interval < 30) interval = 30; + if (interval > 1000) interval = 1000; + + me.body.dom.rollingModeBtn.style.visibility = "hidden"; + // start a renderTimer to adjust for the new time + me.currentTimeTimer = setTimeout(update, interval); + } + + update(); +}; + +/** + * Stop auto refreshing the current time bar + */ +Range.prototype.stopRolling = function() { + if (this.currentTimeTimer !== undefined) { + clearTimeout(this.currentTimeTimer); + this.rolling = false; + this.body.dom.rollingModeBtn.style.visibility = "visible"; + } +}; + /** * Set a new start and end range * @param {Date | Number | String} [start] @@ -388,6 +450,8 @@ Range.prototype._onDragStart = function(event) { // when releasing the fingers in opposite order from the touch screen if (!this.props.touch.allowDragging) return; + this.stopRolling(); + this.props.touch.start = this.start; this.props.touch.end = this.end; this.props.touch.dragging = true; @@ -520,7 +584,7 @@ Range.prototype._onMouseWheel = function(event) { // Prevent default actions caused by mouse wheel // (else the page and timeline both scroll) event.preventDefault(); - + // calculate a single scroll jump relative to the range scale var diff = delta * (this.end - this.start) / 20; // calculate new start and end @@ -555,9 +619,13 @@ Range.prototype._onMouseWheel = function(event) { } // calculate center, the date to zoom around - var pointer = this.getPointer({x: event.clientX, y: event.clientY}, this.body.dom.center); - var pointerDate = this._pointerToDate(pointer); - + var pointerDate + if (this.rolling) { + pointerDate = (this.start + this.end) / 2; + } else { + var pointer = this.getPointer({x: event.clientX, y: event.clientY}, this.body.dom.center); + pointerDate = this._pointerToDate(pointer); + } this.zoom(scale, pointerDate, delta, event); // Prevent default actions caused by mouse wheel @@ -594,6 +662,8 @@ Range.prototype._onPinch = function (event) { this.props.touch.center = this.getPointer(event.center, this.body.dom.center); } + this.stopRolling(); + var scale = 1 / (event.scale + this.scaleOffset); var centerDate = this._pointerToDate(this.props.touch.center); diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js index 7f5d463c..8abe0182 100644 --- a/lib/timeline/Timeline.js +++ b/lib/timeline/Timeline.js @@ -58,10 +58,8 @@ function Timeline (container, items, groups, options) { }; this.options = util.deepExtend({}, this.defaultOptions); - // Create the DOM, props, and emitter this._create(container); - if (!options || (options && typeof options.rtl == "undefined")) { var directionFromDom, domNode = this.dom.root; while (!directionFromDom && domNode) { @@ -72,6 +70,7 @@ function Timeline (container, items, groups, options) { } else { this.options.rtl = options.rtl; } + this.options.rollingMode = options.rollingMode; // all components listed here will be repainted automatically this.components = []; @@ -134,7 +133,7 @@ function Timeline (container, items, groups, options) { //Single time autoscale/fit this.fitDone = false; this.on('changed', function (){ - if (this.itemsData == null) return; + if (this.itemsData == null || this.options.rollingMode) return; if (!me.fitDone) { me.fitDone = true; if (me.options.start != undefined || me.options.end != undefined) { @@ -144,7 +143,6 @@ function Timeline (container, items, groups, options) { var start = me.options.start != undefined ? me.options.start : range.min; var end = me.options.end != undefined ? me.options.end : range.max; - me.setWindow(start, end, {animation: false}); } else { diff --git a/lib/timeline/component/ItemSet.js b/lib/timeline/component/ItemSet.js index 1036b316..53de50af 100644 --- a/lib/timeline/component/ItemSet.js +++ b/lib/timeline/component/ItemSet.js @@ -321,8 +321,8 @@ ItemSet.prototype.setOptions = function(options) { // copy all options that we know var fields = [ 'type', 'rtl', 'align', 'order', 'stack', 'selectable', 'multiselect', 'itemsAlwaysDraggable', - 'multiselectPerGroup', 'groupOrder', 'dataAttributes', 'template', 'groupTemplate', 'hide', 'snap', - 'groupOrderSwap', 'tooltipOnItemUpdateTime' + 'multiselectPerGroup', 'groupOrder', 'dataAttributes', 'template', 'groupTemplate', 'visibleFrameTemplate', + 'hide', 'snap', 'groupOrderSwap', 'tooltipOnItemUpdateTime' ]; util.selectiveExtend(fields, this.options, options); @@ -1213,6 +1213,7 @@ ItemSet.prototype._getGroupIndex = function(groupId) { * @private */ ItemSet.prototype._onDragStart = function (event) { + if (this.touchParams.itemIsDragging) { return; } var item = this.touchParams.item || null; var me = this; var props; @@ -1850,7 +1851,7 @@ ItemSet.prototype._onAddItem = function (event) { }; if (event.type == 'drop') { - var itemData = JSON.parse(event.dataTransfer.getData("text/plain")) + var itemData = JSON.parse(event.dataTransfer.getData("text")) newItemData.content = itemData.content; // content is required newItemData.type = itemData.type || 'box'; newItemData[this.itemsData._fieldId] = itemData.id || util.randomUUID(); diff --git a/lib/timeline/component/css/currenttime.css b/lib/timeline/component/css/currenttime.css index 7d3547a1..658fdfef 100644 --- a/lib/timeline/component/css/currenttime.css +++ b/lib/timeline/component/css/currenttime.css @@ -3,4 +3,27 @@ width: 2px; z-index: 1; pointer-events: none; +} + +.vis-rolling-mode-btn { + height: 40px; + width: 40px; + position: absolute; + top: 7px; + right: 20px; + border-radius: 50%; + font-size: 28px; + cursor: pointer; + opacity: 0.8; + color: white; + font-weight: bold; + text-align: center; + background: #3876c2; +} +.vis-rolling-mode-btn:before { + content: "\26F6"; +} + +.vis-rolling-mode-btn:hover { + opacity: 1; } \ No newline at end of file diff --git a/lib/timeline/component/css/item.css b/lib/timeline/component/css/item.css index 37f05533..67221d23 100644 --- a/lib/timeline/component/css/item.css +++ b/lib/timeline/component/css/item.css @@ -66,6 +66,10 @@ overflow: hidden; } +.vis-item-visible-frame { + white-space: nowrap; +} + .vis-item.vis-range .vis-item-content { position: relative; display: inline-block; diff --git a/lib/timeline/component/item/Item.js b/lib/timeline/component/item/Item.js index 9057dd66..a7522921 100644 --- a/lib/timeline/component/item/Item.js +++ b/lib/timeline/component/item/Item.js @@ -325,9 +325,46 @@ Item.prototype._repaintOnItemUpdateTimeTooltip = function (anchor) { Item.prototype._updateContents = function (element) { var content; var templateFunction; + var itemVisibleFrameContent; + var visibleFrameTemplateFunction; + var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset + + var frameElement = this.dom.box || this.dom.point; + var itemVisibleFrameContentElement = frameElement.getElementsByClassName('vis-item-visible-frame')[0] + + if (this.options.visibleFrameTemplate) { + visibleFrameTemplateFunction = this.options.visibleFrameTemplate.bind(this); + itemVisibleFrameContent = visibleFrameTemplateFunction(itemData, frameElement); + } else { + itemVisibleFrameContent = ''; + } + + if (itemVisibleFrameContentElement) { + if ((itemVisibleFrameContent instanceof Object) && !(itemVisibleFrameContent instanceof Element)) { + visibleFrameTemplateFunction(itemData, itemVisibleFrameContentElement) + } else { + var changed = this._contentToString(this.itemVisibleFrameContent) !== this._contentToString(itemVisibleFrameContent); + if (changed) { + // only replace the content when changed + if (itemVisibleFrameContent instanceof Element) { + itemVisibleFrameContentElement.innerHTML = ''; + itemVisibleFrameContentElement.appendChild(itemVisibleFrameContent); + } + else if (itemVisibleFrameContent != undefined) { + itemVisibleFrameContentElement.innerHTML = itemVisibleFrameContent; + } + else { + if (!(this.data.type == 'background' && this.data.content === undefined)) { + throw new Error('Property "content" missing in item ' + this.id); + } + } + + this.itemVisibleFrameContent = itemVisibleFrameContent; + } + } + } if (this.options.template) { - var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset templateFunction = this.options.template.bind(this); content = templateFunction(itemData, element); } else { diff --git a/lib/timeline/component/item/RangeItem.js b/lib/timeline/component/item/RangeItem.js index a840b529..bc586e8f 100644 --- a/lib/timeline/component/item/RangeItem.js +++ b/lib/timeline/component/item/RangeItem.js @@ -60,10 +60,15 @@ RangeItem.prototype.redraw = function() { dom.box = document.createElement('div'); // className is updated in redraw() - // frame box (to prevent the item contents from overflowing + // frame box (to prevent the item contents from overflowing) dom.frame = document.createElement('div'); dom.frame.className = 'vis-item-overflow'; dom.box.appendChild(dom.frame); + + // visible frame box (showing the frame that is always visible) + dom.visibleFrame = document.createElement('div'); + dom.visibleFrame.className = 'vis-item-visible-frame'; + dom.box.appendChild(dom.visibleFrame); // contents box dom.content = document.createElement('div'); diff --git a/lib/timeline/optionsTimeline.js b/lib/timeline/optionsTimeline.js index aadcf2ff..5ef426b4 100644 --- a/lib/timeline/optionsTimeline.js +++ b/lib/timeline/optionsTimeline.js @@ -26,6 +26,7 @@ let allOptions = { //globals : align: {string}, rtl: {boolean, 'undefined': 'undefined'}, + rollingMode: {boolean, 'undefined': 'undefined'}, verticalScroll: {boolean, 'undefined': 'undefined'}, horizontalScroll: {boolean, 'undefined': 'undefined'}, autoResize: {boolean}, @@ -127,6 +128,7 @@ let allOptions = { start: {date, number, string, moment}, template: {'function': 'function'}, groupTemplate: {'function': 'function'}, + visibleFrameTemplate: {string, 'function': 'function'}, tooltipOnItemUpdateTime: { template: {'function': 'function'}, __type__: {boolean, object}