vendor/assets/vis/timeline/component/TimeAxis.js in vis-rails-0.0.6 vs vendor/assets/vis/timeline/component/TimeAxis.js in vis-rails-1.0.0

- old
+ new

@@ -1,19 +1,14 @@ /** * A horizontal time axis - * @param {Component} parent - * @param {Component[]} [depends] Components on which this components depends - * (except for the parent) * @param {Object} [options] See TimeAxis.setOptions for the available * options. * @constructor TimeAxis * @extends Component */ -function TimeAxis (parent, depends, options) { +function TimeAxis (options) { this.id = util.randomUUID(); - this.parent = parent; - this.depends = depends; this.dom = { majorLines: [], majorTexts: [], minorLines: [], @@ -40,20 +35,29 @@ // TODO: implement timeaxis orientations 'left' and 'right' showMinorLabels: true, showMajorLabels: true }; - this.conversion = null; this.range = null; + + // create the HTML DOM + this._create(); } TimeAxis.prototype = new Component(); // TODO: comment options TimeAxis.prototype.setOptions = Component.prototype.setOptions; /** + * Create the HTML DOM for the TimeAxis + */ +TimeAxis.prototype._create = function _create() { + this.frame = document.createElement('div'); +}; + +/** * Set a range (start and end) * @param {Range | Object} range A Range or an object containing start and end. */ TimeAxis.prototype.setRange = function (range) { if (!(range instanceof Range) && (!range || !range.start || !range.end)) { @@ -62,130 +66,74 @@ } this.range = range; }; /** - * Convert a position on screen (pixels) to a datetime - * @param {int} x Position on the screen in pixels - * @return {Date} time The datetime the corresponds with given position x + * Get the outer frame of the time axis + * @return {HTMLElement} frame */ -TimeAxis.prototype.toTime = function(x) { - var conversion = this.conversion; - return new Date(x / conversion.scale + conversion.offset); +TimeAxis.prototype.getFrame = function getFrame() { + return this.frame; }; /** - * Convert a datetime (Date object) into a position on the screen - * @param {Date} time A date - * @return {int} x The position on the screen in pixels which corresponds - * with the given date. - * @private - */ -TimeAxis.prototype.toScreen = function(time) { - var conversion = this.conversion; - return (time.valueOf() - conversion.offset) * conversion.scale; -}; - -/** * Repaint the component - * @return {Boolean} changed + * @return {boolean} Returns true if the component is resized */ TimeAxis.prototype.repaint = function () { - var changed = 0, - update = util.updateProperty, - asSize = util.option.asSize, + var asSize = util.option.asSize, options = this.options, - orientation = this.getOption('orientation'), props = this.props, - step = this.step; + frame = this.frame; - var frame = this.frame; - if (!frame) { - frame = document.createElement('div'); - this.frame = frame; - changed += 1; - } - frame.className = 'axis'; - // TODO: custom className? + // update classname + frame.className = 'timeaxis'; // TODO: add className from options if defined - if (!frame.parentNode) { - if (!this.parent) { - throw new Error('Cannot repaint time axis: no parent attached'); - } - var parentContainer = this.parent.getContainer(); - if (!parentContainer) { - throw new Error('Cannot repaint time axis: parent has no container element'); - } - parentContainer.appendChild(frame); - - changed += 1; - } - var parent = frame.parentNode; if (parent) { - var beforeChild = frame.nextSibling; - parent.removeChild(frame); // take frame offline while updating (is almost twice as fast) + // calculate character width and height + this._calculateCharSize(); - var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ? - (this.props.parentHeight - this.height) + 'px' : - '0px'; - changed += update(frame.style, 'top', asSize(options.top, defaultTop)); - changed += update(frame.style, 'left', asSize(options.left, '0px')); - changed += update(frame.style, 'width', asSize(options.width, '100%')); - changed += update(frame.style, 'height', asSize(options.height, this.height + 'px')); + // TODO: recalculate sizes only needed when parent is resized or options is changed + var orientation = this.getOption('orientation'), + showMinorLabels = this.getOption('showMinorLabels'), + showMajorLabels = this.getOption('showMajorLabels'); - // get characters width and height - this._repaintMeasureChars(); + // determine the width and height of the elemens for the axis + var parentHeight = this.parent.height; + props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; + props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; + this.height = props.minorLabelHeight + props.majorLabelHeight; + this.width = frame.offsetWidth; // TODO: only update the width when the frame is resized? - if (this.step) { - this._repaintStart(); + props.minorLineHeight = parentHeight + props.minorLabelHeight; + props.minorLineWidth = 1; // TODO: really calculate width + props.majorLineHeight = parentHeight + this.height; + props.majorLineWidth = 1; // TODO: really calculate width - step.first(); - var xFirstMajorLabel = undefined; - var max = 0; - while (step.hasNext() && max < 1000) { - max++; - var cur = step.getCurrent(), - x = this.toScreen(cur), - isMajor = step.isMajor(); + // take frame offline while updating (is almost twice as fast) + var beforeChild = frame.nextSibling; + parent.removeChild(frame); - // TODO: lines must have a width, such that we can create css backgrounds - - if (this.getOption('showMinorLabels')) { - this._repaintMinorText(x, step.getLabelMinor()); - } - - if (isMajor && this.getOption('showMajorLabels')) { - if (x > 0) { - if (xFirstMajorLabel == undefined) { - xFirstMajorLabel = x; - } - this._repaintMajorText(x, step.getLabelMajor()); - } - this._repaintMajorLine(x); - } - else { - this._repaintMinorLine(x); - } - - step.next(); - } - - // create a major label on the left when needed - if (this.getOption('showMajorLabels')) { - var leftTime = this.toTime(0), - leftText = step.getLabelMajor(leftTime), - widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation - - if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { - this._repaintMajorText(0, leftText); - } - } - - this._repaintEnd(); + // TODO: top/bottom positioning should be determined by options set in the Timeline, not here + if (orientation == 'top') { + frame.style.top = '0'; + frame.style.left = '0'; + frame.style.bottom = ''; + frame.style.width = asSize(options.width, '100%'); + frame.style.height = this.height + 'px'; } + else { // bottom + frame.style.top = ''; + frame.style.bottom = '0'; + frame.style.left = '0'; + frame.style.width = asSize(options.width, '100%'); + frame.style.height = this.height + 'px'; + } + this._repaintLabels(); + this._repaintLine(); // put frame online again if (beforeChild) { parent.insertBefore(frame, beforeChild); @@ -193,56 +141,102 @@ else { parent.appendChild(frame) } } - return (changed > 0); + return this._isResized(); }; /** - * Start a repaint. Move all DOM elements to a redundant list, where they - * can be picked for re-use, or can be cleaned up in the end + * Repaint major and minor text labels and vertical grid lines * @private */ -TimeAxis.prototype._repaintStart = function () { - var dom = this.dom, - redundant = dom.redundant; +TimeAxis.prototype._repaintLabels = function () { + var orientation = this.getOption('orientation'); - redundant.majorLines = dom.majorLines; - redundant.majorTexts = dom.majorTexts; - redundant.minorLines = dom.minorLines; - redundant.minorTexts = dom.minorTexts; + // calculate range and step (step such that we have space for 7 characters per label) + var start = util.convert(this.range.start, 'Number'), + end = util.convert(this.range.end, 'Number'), + minimumStep = this.options.toTime((this.props.minorCharWidth || 10) * 7).valueOf() + -this.options.toTime(0).valueOf(); + var step = new TimeStep(new Date(start), new Date(end), minimumStep); + this.step = step; + // Move all DOM elements to a "redundant" list, where they + // can be picked for re-use, and clear the lists with lines and texts. + // At the end of the function _repaintLabels, left over elements will be cleaned up + var dom = this.dom; + dom.redundant.majorLines = dom.majorLines; + dom.redundant.majorTexts = dom.majorTexts; + dom.redundant.minorLines = dom.minorLines; + dom.redundant.minorTexts = dom.minorTexts; dom.majorLines = []; dom.majorTexts = []; dom.minorLines = []; dom.minorTexts = []; -}; -/** - * End a repaint. Cleanup leftover DOM elements in the redundant list - * @private - */ -TimeAxis.prototype._repaintEnd = function () { + step.first(); + var xFirstMajorLabel = undefined; + var max = 0; + while (step.hasNext() && max < 1000) { + max++; + var cur = step.getCurrent(), + x = this.options.toScreen(cur), + isMajor = step.isMajor(); + + // TODO: lines must have a width, such that we can create css backgrounds + + if (this.getOption('showMinorLabels')) { + this._repaintMinorText(x, step.getLabelMinor(), orientation); + } + + if (isMajor && this.getOption('showMajorLabels')) { + if (x > 0) { + if (xFirstMajorLabel == undefined) { + xFirstMajorLabel = x; + } + this._repaintMajorText(x, step.getLabelMajor(), orientation); + } + this._repaintMajorLine(x, orientation); + } + else { + this._repaintMinorLine(x, orientation); + } + + step.next(); + } + + // create a major label on the left when needed + if (this.getOption('showMajorLabels')) { + var leftTime = this.options.toTime(0), + leftText = step.getLabelMajor(leftTime), + widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation + + if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { + this._repaintMajorText(0, leftText, orientation); + } + } + + // Cleanup leftover DOM elements from the redundant list util.forEach(this.dom.redundant, function (arr) { while (arr.length) { var elem = arr.pop(); if (elem && elem.parentNode) { elem.parentNode.removeChild(elem); } } }); }; - /** * Create a minor label for the axis at position x * @param {Number} x * @param {String} text + * @param {String} orientation "top" or "bottom" (default) * @private */ -TimeAxis.prototype._repaintMinorText = function (x, text) { +TimeAxis.prototype._repaintMinorText = function (x, text, orientation) { // reuse redundant label var label = this.dom.redundant.minorTexts.shift(); if (!label) { // create new label @@ -253,22 +247,31 @@ this.frame.appendChild(label); } this.dom.minorTexts.push(label); label.childNodes[0].nodeValue = text; + + if (orientation == 'top') { + label.style.top = this.props.majorLabelHeight + 'px'; + label.style.bottom = ''; + } + else { + label.style.top = ''; + label.style.bottom = this.props.majorLabelHeight + 'px'; + } label.style.left = x + 'px'; - label.style.top = this.props.minorLabelTop + 'px'; //label.title = title; // TODO: this is a heavy operation }; /** * Create a Major label for the axis at position x * @param {Number} x * @param {String} text + * @param {String} orientation "top" or "bottom" (default) * @private */ -TimeAxis.prototype._repaintMajorText = function (x, text) { +TimeAxis.prototype._repaintMajorText = function (x, text, orientation) { // reuse redundant label var label = this.dom.redundant.majorTexts.shift(); if (!label) { // create label @@ -279,21 +282,30 @@ this.frame.appendChild(label); } this.dom.majorTexts.push(label); label.childNodes[0].nodeValue = text; - label.style.top = this.props.majorLabelTop + 'px'; - label.style.left = x + 'px'; //label.title = title; // TODO: this is a heavy operation + + if (orientation == 'top') { + label.style.top = '0px'; + label.style.bottom = ''; + } + else { + label.style.top = ''; + label.style.bottom = '0px'; + } + label.style.left = x + 'px'; }; /** * Create a minor line for the axis at position x * @param {Number} x + * @param {String} orientation "top" or "bottom" (default) * @private */ -TimeAxis.prototype._repaintMinorLine = function (x) { +TimeAxis.prototype._repaintMinorLine = function (x, orientation) { // reuse redundant line var line = this.dom.redundant.minorLines.shift(); if (!line) { // create vertical line @@ -302,21 +314,29 @@ this.frame.appendChild(line); } this.dom.minorLines.push(line); var props = this.props; - line.style.top = props.minorLineTop + 'px'; + if (orientation == 'top') { + line.style.top = this.props.majorLabelHeight + 'px'; + line.style.bottom = ''; + } + else { + line.style.top = ''; + line.style.bottom = this.props.majorLabelHeight + 'px'; + } line.style.height = props.minorLineHeight + 'px'; line.style.left = (x - props.minorLineWidth / 2) + 'px'; }; /** * Create a Major line for the axis at position x * @param {Number} x + * @param {String} orientation "top" or "bottom" (default) * @private */ -TimeAxis.prototype._repaintMajorLine = function (x) { +TimeAxis.prototype._repaintMajorLine = function (x, orientation) { // reuse redundant line var line = this.dom.redundant.majorLines.shift(); if (!line) { // create vertical line @@ -325,11 +345,18 @@ this.frame.appendChild(line); } this.dom.majorLines.push(line); var props = this.props; - line.style.top = props.majorLineTop + 'px'; + if (orientation == 'top') { + line.style.top = '0px'; + line.style.bottom = ''; + } + else { + line.style.top = ''; + line.style.bottom = '0px'; + } line.style.left = (x - props.majorLineWidth / 2) + 'px'; line.style.height = props.majorLineHeight + 'px'; }; @@ -338,11 +365,11 @@ * @private */ TimeAxis.prototype._repaintLine = function() { var line = this.dom.line, frame = this.frame, - options = this.options; + orientation = this.getOption('orientation'); // line before all axis elements if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) { if (line) { // put this line at the end of all childs @@ -355,170 +382,57 @@ line.className = 'grid horizontal major'; frame.appendChild(line); this.dom.line = line; } - line.style.top = this.props.lineTop + 'px'; + if (orientation == 'top') { + line.style.top = this.height + 'px'; + line.style.bottom = ''; + } + else { + line.style.top = ''; + line.style.bottom = this.height + 'px'; + } } else { - if (line && line.parentElement) { - frame.removeChild(line.line); + if (line && line.parentNode) { + line.parentNode.removeChild(line); delete this.dom.line; } } }; /** - * Create characters used to determine the size of text on the axis + * Determine the size of text on the axis (both major and minor axis). + * The size is calculated only once and then cached in this.props. * @private */ -TimeAxis.prototype._repaintMeasureChars = function () { - // calculate the width and height of a single character - // this is used to calculate the step size, and also the positioning of the - // axis - var dom = this.dom, - text; - - if (!dom.measureCharMinor) { - text = document.createTextNode('0'); +TimeAxis.prototype._calculateCharSize = function () { + // determine the char width and height on the minor axis + if (!('minorCharHeight' in this.props)) { + var textMinor = document.createTextNode('0'); var measureCharMinor = document.createElement('DIV'); measureCharMinor.className = 'text minor measure'; - measureCharMinor.appendChild(text); + measureCharMinor.appendChild(textMinor); this.frame.appendChild(measureCharMinor); - dom.measureCharMinor = measureCharMinor; + this.props.minorCharHeight = measureCharMinor.clientHeight; + this.props.minorCharWidth = measureCharMinor.clientWidth; + + this.frame.removeChild(measureCharMinor); } - if (!dom.measureCharMajor) { - text = document.createTextNode('0'); + if (!('majorCharHeight' in this.props)) { + var textMajor = document.createTextNode('0'); var measureCharMajor = document.createElement('DIV'); measureCharMajor.className = 'text major measure'; - measureCharMajor.appendChild(text); + measureCharMajor.appendChild(textMajor); this.frame.appendChild(measureCharMajor); - dom.measureCharMajor = measureCharMajor; - } -}; + this.props.majorCharHeight = measureCharMajor.clientHeight; + this.props.majorCharWidth = measureCharMajor.clientWidth; -/** - * Reflow the component - * @return {Boolean} resized - */ -TimeAxis.prototype.reflow = function () { - var changed = 0, - update = util.updateProperty, - frame = this.frame, - range = this.range; - - if (!range) { - throw new Error('Cannot repaint time axis: no range configured'); - } - - if (frame) { - changed += update(this, 'top', frame.offsetTop); - changed += update(this, 'left', frame.offsetLeft); - - // calculate size of a character - var props = this.props, - showMinorLabels = this.getOption('showMinorLabels'), - showMajorLabels = this.getOption('showMajorLabels'), - measureCharMinor = this.dom.measureCharMinor, - measureCharMajor = this.dom.measureCharMajor; - if (measureCharMinor) { - props.minorCharHeight = measureCharMinor.clientHeight; - props.minorCharWidth = measureCharMinor.clientWidth; - } - if (measureCharMajor) { - props.majorCharHeight = measureCharMajor.clientHeight; - props.majorCharWidth = measureCharMajor.clientWidth; - } - - var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0; - if (parentHeight != props.parentHeight) { - props.parentHeight = parentHeight; - changed += 1; - } - switch (this.getOption('orientation')) { - case 'bottom': - props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; - props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; - - props.minorLabelTop = 0; - props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight; - - props.minorLineTop = -this.top; - props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0); - props.minorLineWidth = 1; // TODO: really calculate width - - props.majorLineTop = -this.top; - props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0); - props.majorLineWidth = 1; // TODO: really calculate width - - props.lineTop = 0; - - break; - - case 'top': - props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; - props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; - - props.majorLabelTop = 0; - props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight; - - props.minorLineTop = props.minorLabelTop; - props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top); - props.minorLineWidth = 1; // TODO: really calculate width - - props.majorLineTop = 0; - props.majorLineHeight = Math.max(parentHeight - this.top); - props.majorLineWidth = 1; // TODO: really calculate width - - props.lineTop = props.majorLabelHeight + props.minorLabelHeight; - - break; - - default: - throw new Error('Unkown orientation "' + this.getOption('orientation') + '"'); - } - - var height = props.minorLabelHeight + props.majorLabelHeight; - changed += update(this, 'width', frame.offsetWidth); - changed += update(this, 'height', height); - - // calculate range and step - this._updateConversion(); - - var start = util.convert(range.start, 'Number'), - end = util.convert(range.end, 'Number'), - minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf() - -this.toTime(0).valueOf(); - this.step = new TimeStep(new Date(start), new Date(end), minimumStep); - changed += update(props.range, 'start', start); - changed += update(props.range, 'end', end); - changed += update(props.range, 'minimumStep', minimumStep.valueOf()); - } - - return (changed > 0); -}; - -/** - * Calculate the scale and offset to convert a position on screen to the - * corresponding date and vice versa. - * After the method _updateConversion is executed once, the methods toTime - * and toScreen can be used. - * @private - */ -TimeAxis.prototype._updateConversion = function() { - var range = this.range; - if (!range) { - throw new Error('No range configured'); - } - - if (range.conversion) { - this.conversion = range.conversion(this.width); - } - else { - this.conversion = Range.conversion(range.start, range.end, this.width); + this.frame.removeChild(measureCharMajor); } }; /** * Snap a date to a rounded value.