304 lines
8.7 KiB
JavaScript
304 lines
8.7 KiB
JavaScript
/**
|
|
* Flotr Axis Library
|
|
*/
|
|
|
|
(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);
|
|
|
|
this._setTranslations();
|
|
}
|
|
|
|
|
|
// Prototype
|
|
Axis.prototype = {
|
|
|
|
setScale : function () {
|
|
var length = this.length;
|
|
if (this.options.scaling == LOGARITHMIC) {
|
|
this.scale = length / (log(this.max, this.options.base) - log(this.min, this.options.base));
|
|
} else {
|
|
this.scale = length / (this.max - this.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();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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}) });
|
|
}
|
|
}
|
|
}
|
|
|
|
},
|
|
|
|
_setTranslations : function (logarithmic) {
|
|
this.d2p = (logarithmic ? d2pLog : d2p);
|
|
this.p2d = (logarithmic ? p2dLog : p2d);
|
|
}
|
|
};
|
|
|
|
|
|
// 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
|
|
|
|
function d2p (dataValue) {
|
|
return this.offset + this.orientation * (dataValue - this.min) * this.scale;
|
|
}
|
|
|
|
function p2d (pointValue) {
|
|
return (this.offset + this.orientation * pointValue) / this.scale + this.min;
|
|
}
|
|
|
|
function d2pLog (dataValue) {
|
|
return this.offset + this.orientation * (log(dataValue, this.options.base) - log(this.min, this.options.base)) * this.scale;
|
|
}
|
|
|
|
function p2dLog (pointValue) {
|
|
return exp((this.offset + this.orientation * pointValue) / this.scale + log(this.min, this.options.base), this.options.base);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
Flotr.Axis = Axis;
|
|
|
|
})();
|