diff --git a/js/Axis.js b/js/Axis.js index 4bc26bc0..5b4d871e 100644 --- a/js/Axis.js +++ b/js/Axis.js @@ -1,312 +1,382 @@ /** * Flotr Axis Library */ +(function() { -(function () { - -var - _ = Flotr._, - LOGARITHMIC = 'logarithmic'; - -function Axis (o) { - - this.orientation = 1; - this.offset = 0; - this.datamin = Number.MAX_VALUE; - this.datamax = -Number.MAX_VALUE; - - _.extend(this, o); -} - - -// Prototype -Axis.prototype = { - - setScale : function () { var - length = this.length, - max = this.max, - min = this.min, - offset = this.offset, - orientation = this.orientation, - options = this.options, - logarithmic = options.scaling === LOGARITHMIC, - scale; - - if (logarithmic) { - scale = length / (log(max, options.base) - log(min, options.base)); - } else { - scale = length / (max - min); - } - this.scale = scale; - - // Logarithmic? - if (logarithmic) { - this.d2p = function (dataValue) { - return offset + orientation * (log(dataValue, options.base) - log(min, options.base)) * scale; - }; - this.p2d = function (pointValue) { - return exp((offset + orientation * pointValue) / scale + log(min, options.base), options.base); - }; - } else { - this.d2p = function (dataValue) { - return offset + orientation * (dataValue - min) * scale; - }; - this.p2d = function (pointValue) { - return (offset + orientation * pointValue) / scale + min; - }; - } - }, - - calculateTicks : function () { - var options = this.options; - - this.ticks = []; - this.minorTicks = []; - - // User Ticks - if(options.ticks){ - this._cleanUserTicks(options.ticks, this.ticks); - this._cleanUserTicks(options.minorTicks || [], this.minorTicks); - } - else { - if (options.mode == 'time') { - this._calculateTimeTicks(); - } else if (options.scaling === 'logarithmic') { - this._calculateLogTicks(); - } else { - this._calculateTicks(); - } - } - - // Ticks to strings - _.each(this.ticks, function (tick) { tick.label += ''; }); - _.each(this.minorTicks, function (tick) { tick.label += ''; }); - }, - - /** - * Calculates the range of an axis to apply autoscaling. - */ - calculateRange: function () { - - if (!this.used) return; - - var axis = this, - o = axis.options, - min = o.min !== null ? o.min : axis.datamin, - max = o.max !== null ? o.max : axis.datamax, - margin = o.autoscaleMargin; - - if (o.scaling == 'logarithmic') { - if (min <= 0) min = axis.datamin; - - // Let it widen later on - if (max <= 0) max = min; - } - - if (max == min) { - var widen = max ? 0.01 : 1.00; - if (o.min === null) min -= widen; - if (o.max === null) max += widen; - } + _ = Flotr._, + LOGARITHMIC = 'logarithmic'; - if (o.scaling === 'logarithmic') { - if (min < 0) min = max / o.base; // Could be the result of widening - - var maxexp = Math.log(max); - if (o.base != Math.E) maxexp /= Math.log(o.base); - maxexp = Math.ceil(maxexp); - - var minexp = Math.log(min); - if (o.base != Math.E) minexp /= Math.log(o.base); - minexp = Math.ceil(minexp); - - axis.tickSize = Flotr.getTickSize(o.noTicks, minexp, maxexp, o.tickDecimals === null ? 0 : o.tickDecimals); - - // Try to determine a suitable amount of miniticks based on the length of a decade - if (o.minorTickFreq === null) { - if (maxexp - minexp > 10) - o.minorTickFreq = 0; - else if (maxexp - minexp > 5) - o.minorTickFreq = 2; - else - o.minorTickFreq = 5; - } - } else { - axis.tickSize = Flotr.getTickSize(o.noTicks, min, max, o.tickDecimals); - } + function Axis(o) { - axis.min = min; - axis.max = max; //extendRange may use axis.min or axis.max, so it should be set before it is caled + this.orientation = 1; + this.offset = 0; + this.datamin = Number.MAX_VALUE; + this.datamax = -Number.MAX_VALUE; - // Autoscaling. @todo This probably fails with log scale. Find a testcase and fix it - if(o.min === null && o.autoscale){ - axis.min -= axis.tickSize * margin; - // Make sure we don't go below zero if all values are positive. - if(axis.min < 0 && axis.datamin >= 0) axis.min = 0; - axis.min = axis.tickSize * Math.floor(axis.min / axis.tickSize); - } - - if(o.max === null && o.autoscale){ - axis.max += axis.tickSize * margin; - if(axis.max > 0 && axis.datamax <= 0 && axis.datamax != axis.datamin) axis.max = 0; - axis.max = axis.tickSize * Math.ceil(axis.max / axis.tickSize); + _.extend(this, o); } - if (axis.min == axis.max) axis.max = axis.min + 1; - }, - - calculateTextDimensions : function (T, options) { - - var maxLabel = '', - length, - i; - if (this.options.showLabels) { - for (i = 0; i < this.ticks.length; ++i) { - length = this.ticks[i].label.length; - if (length > maxLabel.length){ - maxLabel = this.ticks[i].label; - } - } - } + // Prototype + Axis.prototype = { + + setScale: function() { + var + length = this.length, + max = this.max, + min = this.min, + offset = this.offset, + orientation = this.orientation, + options = this.options, + logarithmic = options.scaling === LOGARITHMIC, + scale; + + if (logarithmic) { + scale = length / (log(max, options.base) - log(min, options.base)); + } else { + scale = length / (max - min); + } + this.scale = scale; + + // Logarithmic? + if (logarithmic) { + this.d2p = function(dataValue) { + return offset + orientation * (log(dataValue, options.base) - log(min, options.base)) * scale; + }; + this.p2d = function(pointValue) { + return exp((offset + orientation * pointValue) / scale + log(min, options.base), options.base); + }; + } else { + this.d2p = function(dataValue) { + return offset + orientation * (dataValue - min) * scale; + }; + this.p2d = function(pointValue) { + return (offset + orientation * pointValue) / scale + min; + }; + } + }, + + calculateTicks: function() { + var options = this.options; + + this.ticks = []; + this.minorTicks = []; + + // User Ticks + if (options.ticks) { + this._cleanUserTicks(options.ticks, this.ticks); + this._cleanUserTicks(options.minorTicks || [], this.minorTicks); + } else { + if (options.mode == 'time') { + this._calculateTimeTicks(); + } else if (options.scaling === 'logarithmic') { + this._calculateLogTicks(); + } else { + this._calculateTicks(); + } + } + + // Ticks to strings + _.each(this.ticks, function(tick) { + tick.label += ''; + }); + _.each(this.minorTicks, function(tick) { + tick.label += ''; + }); + }, + + /** + * Calculates the range of an axis to apply autoscaling. + */ + calculateRange: function() { + + if (!this.used) return; + + var axis = this, + o = axis.options, + min = o.min !== null ? o.min : axis.datamin, + max = o.max !== null ? o.max : axis.datamax, + margin = o.autoscaleMargin; + + if (o.scaling == 'logarithmic') { + if (min <= 0) min = axis.datamin; + + // Let it widen later on + if (max <= 0) max = min; + } + + if (max == min) { + var widen = max ? 0.01 : 1.00; + if (o.min === null) min -= widen; + if (o.max === null) max += widen; + } + + if (o.scaling === 'logarithmic') { + if (min < 0) min = max / o.base; // Could be the result of widening + + var maxexp = Math.log(max); + if (o.base != Math.E) maxexp /= Math.log(o.base); + maxexp = Math.ceil(maxexp); + + var minexp = Math.log(min); + if (o.base != Math.E) minexp /= Math.log(o.base); + minexp = Math.ceil(minexp); + + axis.tickSize = Flotr.getTickSize(o.noTicks, minexp, maxexp, o.tickDecimals === null ? 0 : o.tickDecimals); + + // Try to determine a suitable amount of miniticks based on the length of a decade + if (o.minorTickFreq === null) { + if (maxexp - minexp > 10) + o.minorTickFreq = 0; + else if (maxexp - minexp > 5) + o.minorTickFreq = 2; + else + o.minorTickFreq = 5; + } + } else { + axis.tickSize = Flotr.getTickSize(o.noTicks, min, max, o.tickDecimals); + } + + axis.min = min; + axis.max = max; //extendRange may use axis.min or axis.max, so it should be set before it is caled + + // Autoscaling. @todo This probably fails with log scale. Find a testcase and fix it + if (o.min === null && o.autoscale) { + axis.min -= axis.tickSize * margin; + // Make sure we don't go below zero if all values are positive. + if (axis.min < 0 && axis.datamin >= 0) axis.min = 0; + axis.min = axis.tickSize * Math.floor(axis.min / axis.tickSize); + } + + if (o.max === null && o.autoscale) { + axis.max += axis.tickSize * margin; + if (axis.max > 0 && axis.datamax <= 0 && axis.datamax != axis.datamin) axis.max = 0; + axis.max = axis.tickSize * Math.ceil(axis.max / axis.tickSize); + } + + if (axis.min == axis.max) axis.max = axis.min + 1; + }, + + calculateTextDimensions: function(T, options) { + + var maxLabel = '', + length, + i; + + if (this.options.showLabels) { + for (i = 0; i < this.ticks.length; ++i) { + length = this.ticks[i].label.length; + if (length > maxLabel.length) { + maxLabel = this.ticks[i].label; + } + } + } + + this.maxLabel = T.dimensions( + maxLabel, { + size: options.fontSize, + angle: Flotr.toRad(this.options.labelsAngle) + }, + 'font-size:smaller;', + 'flotr-grid-label' + ); + + this.titleSize = T.dimensions( + this.options.title, { + size: options.fontSize * 1.2, + angle: Flotr.toRad(this.options.titleAngle) + }, + 'font-weight:bold;', + 'flotr-axis-title' + ); + }, + + _cleanUserTicks: function(ticks, axisTicks) { + + var axis = this, + options = this.options, + v, i, label, tick; + + if (_.isFunction(ticks)) ticks = ticks({ + min: axis.min, + max: axis.max + }); + + for (i = 0; i < ticks.length; ++i) { + tick = ticks[i]; + if (typeof(tick) === 'object') { + v = tick[0]; + label = (tick.length > 1) ? tick[1] : options.tickFormatter(v, { + min: axis.min, + max: axis.max + }); + } else { + v = tick; + label = options.tickFormatter(v, { + min: this.min, + max: this.max + }); + } + axisTicks[i] = { + v: v, + label: label + }; + } + }, + + _calculateTimeTicks: function() { + this.ticks = Flotr.Date.generator(this); + }, + + _calculateLogTicks: function() { + + var axis = this, + o = axis.options, + v, + decadeStart; + + var max = Math.log(axis.max); + if (o.base != Math.E) max /= Math.log(o.base); + max = Math.ceil(max); + + var min = Math.log(axis.min); + if (o.base != Math.E) min /= Math.log(o.base); + min = Math.ceil(min); + + for (i = min; i < max; i += axis.tickSize) { + decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i); + // Next decade begins here: + var decadeEnd = decadeStart * ((o.base == Math.E) ? Math.exp(axis.tickSize) : Math.pow(o.base, axis.tickSize)); + var stepSize = (decadeEnd - decadeStart) / o.minorTickFreq; + + axis.ticks.push({ + v: decadeStart, + label: o.tickFormatter(decadeStart, { + min: axis.min, + max: axis.max + }) + }); + for (v = decadeStart + stepSize; v < decadeEnd; v += stepSize) + axis.minorTicks.push({ + v: v, + label: o.tickFormatter(v, { + min: axis.min, + max: axis.max + }) + }); + } + + // Always show the value at the would-be start of next decade (end of this decade) + decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i); + axis.ticks.push({ + v: decadeStart, + label: o.tickFormatter(decadeStart, { + min: axis.min, + max: axis.max + }) + }); + }, + + _calculateTicks: function() { + + var axis = this, + o = axis.options, + tickSize = axis.tickSize, + min = axis.min, + max = axis.max, + start = tickSize * Math.ceil(min / tickSize), // Round to nearest multiple of tick size. + decimals, + minorTickSize, + v, v2, + i, j; + + if (o.minorTickFreq) + minorTickSize = tickSize / o.minorTickFreq; + + // Then store all possible ticks. + for (i = 0; + (v = v2 = start + i * tickSize) <= max; ++i) { + + // Round (this is always needed to fix numerical instability). + decimals = o.tickDecimals; + if (decimals === null) decimals = 1 - Math.floor(Math.log(tickSize) / Math.LN10); + if (decimals < 0) decimals = 0; + + v = v.toFixed(decimals); + axis.ticks.push({ + v: v, + label: o.tickFormatter(v, { + min: axis.min, + max: axis.max + }) + }); + + if (o.minorTickFreq) { + for (j = 0; j < o.minorTickFreq && (i * tickSize + j * minorTickSize) < max; ++j) { + v = v2 + j * minorTickSize; + axis.minorTicks.push({ + v: v, + label: o.tickFormatter(v, { + min: axis.min, + max: axis.max + }) + }); + } + } + } - this.maxLabel = T.dimensions( - maxLabel, - {size:options.fontSize, angle: Flotr.toRad(this.options.labelsAngle)}, - 'font-size:smaller;', - 'flotr-grid-label' - ); - - this.titleSize = T.dimensions( - this.options.title, - {size:options.fontSize*1.2, angle: Flotr.toRad(this.options.titleAngle)}, - 'font-weight:bold;', - 'flotr-axis-title' - ); - }, - - _cleanUserTicks : function (ticks, axisTicks) { - - var axis = this, options = this.options, - v, i, label, tick; - - if(_.isFunction(ticks)) ticks = ticks({min : axis.min, max : axis.max}); - - for(i = 0; i < ticks.length; ++i){ - tick = ticks[i]; - if(typeof(tick) === 'object'){ - v = tick[0]; - label = (tick.length > 1) ? tick[1] : options.tickFormatter(v, {min : axis.min, max : axis.max}); - } else { - v = tick; - label = options.tickFormatter(v, {min : this.min, max : this.max}); - } - axisTicks[i] = { v: v, label: label }; - } - }, - - _calculateTimeTicks : function () { - this.ticks = Flotr.Date.generator(this); - }, - - _calculateLogTicks : function () { - - var axis = this, - o = axis.options, - v, - decadeStart; - - var max = Math.log(axis.max); - if (o.base != Math.E) max /= Math.log(o.base); - max = Math.ceil(max); - - var min = Math.log(axis.min); - if (o.base != Math.E) min /= Math.log(o.base); - min = Math.ceil(min); - - for (i = min; i < max; i += axis.tickSize) { - decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i); - // Next decade begins here: - var decadeEnd = decadeStart * ((o.base == Math.E) ? Math.exp(axis.tickSize) : Math.pow(o.base, axis.tickSize)); - var stepSize = (decadeEnd - decadeStart) / o.minorTickFreq; - - axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})}); - for (v = decadeStart + stepSize; v < decadeEnd; v += stepSize) - axis.minorTicks.push({v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max})}); - } - - // Always show the value at the would-be start of next decade (end of this decade) - decadeStart = (o.base == Math.E) ? Math.exp(i) : Math.pow(o.base, i); - axis.ticks.push({v: decadeStart, label: o.tickFormatter(decadeStart, {min : axis.min, max : axis.max})}); - }, - - _calculateTicks : function () { - - var axis = this, - o = axis.options, - tickSize = axis.tickSize, - min = axis.min, - max = axis.max, - start = tickSize * Math.ceil(min / tickSize), // Round to nearest multiple of tick size. - decimals, - minorTickSize, - v, v2, - i, j; - - if (o.minorTickFreq) - minorTickSize = tickSize / o.minorTickFreq; - - // Then store all possible ticks. - for (i = 0; (v = v2 = start + i * tickSize) <= max; ++i){ - - // Round (this is always needed to fix numerical instability). - decimals = o.tickDecimals; - if (decimals === null) decimals = 1 - Math.floor(Math.log(tickSize) / Math.LN10); - if (decimals < 0) decimals = 0; - - v = v.toFixed(decimals); - axis.ticks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) }); - - if (o.minorTickFreq) { - for (j = 0; j < o.minorTickFreq && (i * tickSize + j * minorTickSize) < max; ++j) { - v = v2 + j * minorTickSize; - axis.minorTicks.push({ v: v, label: o.tickFormatter(v, {min : axis.min, max : axis.max}) }); } - } - } - - } -}; + }; -// Static Methods -_.extend(Axis, { - getAxes : function (options) { - return { - x: new Axis({options: options.xaxis, n: 1, length: this.plotWidth}), - x2: new Axis({options: options.x2axis, n: 2, length: this.plotWidth}), - y: new Axis({options: options.yaxis, n: 1, length: this.plotHeight, offset: this.plotHeight, orientation: -1}), - y2: new Axis({options: options.y2axis, n: 2, length: this.plotHeight, offset: this.plotHeight, orientation: -1}) - }; - } -}); + // Static Methods + _.extend(Axis, { + getAxes: function(options) { + return { + x: new Axis({ + options: options.xaxis, + n: 1, + length: this.plotWidth + }), + x2: new Axis({ + options: options.x2axis, + n: 2, + length: this.plotWidth + }), + y: new Axis({ + options: options.yaxis, + n: 1, + length: this.plotHeight, + offset: this.plotHeight, + orientation: -1 + }), + y2: new Axis({ + options: options.y2axis, + n: 2, + length: this.plotHeight, + offset: this.plotHeight, + orientation: -1 + }) + }; + } + }); -// Helper Methods + // Helper Methods -function log (value, base) { - value = Math.log(Math.max(value, Number.MIN_VALUE)); - if (base !== Math.E) - value /= Math.log(base); - return value; -} + function log(value, base) { + value = Math.log(Math.max(value, Number.MIN_VALUE)); + if (base !== Math.E) + value /= Math.log(base); + return value; + } -function exp (value, base) { - return (base === Math.E) ? Math.exp(value) : Math.pow(base, value); -} + function exp(value, base) { + return (base === Math.E) ? Math.exp(value) : Math.pow(base, value); + } -Flotr.Axis = Axis; + Flotr.Axis = Axis; })();