You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
455 lines
15 KiB
455 lines
15 KiB
/**
|
|
* Echarts, logarithmic axis reform
|
|
*
|
|
* @author sushuang (sushuang@baidu.com),
|
|
* Ievgenii (@Ievgeny, ievgeny@zoomdata.com)
|
|
*/
|
|
|
|
define(function(require) {
|
|
|
|
// Reference
|
|
var number = require('./number');
|
|
var Mt = Math;
|
|
var mathLog = Mt.log;
|
|
var mathPow = Mt.pow;
|
|
var mathAbs = Mt.abs;
|
|
var mathCeil = Mt.ceil;
|
|
var mathFloor = Mt.floor;
|
|
|
|
// Constant
|
|
var LOG_BASE = Mt.E; // It is not necessary to specify log base,
|
|
// because log(logBase, x) = ln(x) / ln(logBase),
|
|
// thus final result (axis tick location) is only determined by ln(x).
|
|
var LN10 = Mt.LN10;
|
|
var LN2 = Mt.LN2;
|
|
var LN2D10 = LN2 / LN10;
|
|
var EPSILON = 1e-9;
|
|
var DEFAULT_SPLIT_NUMBER = 5;
|
|
var MIN_BASE_10_SPLIT_NUMBER = 2;
|
|
var SUPERSCRIPTS = {
|
|
'0': '⁰',
|
|
'1': '¹',
|
|
'2': '²',
|
|
'3': '³',
|
|
'4': '⁴',
|
|
'5': '⁵',
|
|
'6': '⁶',
|
|
'7': '⁷',
|
|
'8': '⁸',
|
|
'9': '⁹',
|
|
'-': '⁻'
|
|
};
|
|
|
|
// Static variable
|
|
var logPositive;
|
|
var logLabelBase;
|
|
var logLabelMode; // enumeration:
|
|
// 'plain' (i.e. axis labels are shown like 10000)
|
|
// 'exponent' (i.e. axis labels are shown like 10²)
|
|
var lnBase;
|
|
var custOpts;
|
|
var splitNumber;
|
|
var logMappingOffset;
|
|
var absMin;
|
|
var absMax;
|
|
var tickList;
|
|
|
|
/**
|
|
* Test cases:
|
|
* [2, 4, 8, 16, 32, 64, 128]
|
|
* [0.01, 0.1, 10, 100, 1000] logLabelBase: 3
|
|
* [0.01, 0.1, 10, 100, 1000] logLabelBase: -12
|
|
* [-2, -4, -8, -16, -32, -64, -128] logLabelBase: 3
|
|
* [2, 4, 8, 16, '-', 64, 128]
|
|
* [2, 4, 8, 16, 32, 64]
|
|
* [2, 4, 8, 16, 32]
|
|
* [0.00000256, 0.0016, 0.04, 0.2]
|
|
* [0.1, 1, 10, 100, 1000, 10000, 100000, 1000000] splitNumber: 3
|
|
* [1331, 3434, 500, 1, 1212, 4]
|
|
* [0.14, 2, 45, 1001, 200, 0.33, 10001]
|
|
* [0.00001, 0.00005]
|
|
* [0.00001, 0.00005] boundaryGap: [0.2, 0.4]
|
|
* [0.001, 2, -45, 1001, 200, 0.33, 10000]
|
|
* [0.00000001, 0.00000012]
|
|
* [0.000000000000001]
|
|
* [0.00000001, 0.00000001]
|
|
* [3, 3]
|
|
* [12, -3, 47, 19]
|
|
* [12, -3, 47, 19] logPositive: false
|
|
* [-2, -4, -8, -16, -32, -64, -128]
|
|
* [-2, -4, -8, -16, -32, -64]
|
|
* [2, 4, 8, 16, 32] boundaryGap: [0.2, 0.4]
|
|
* []
|
|
* [0]
|
|
* [10, 10, 10]
|
|
* [0.00003, 0.00003, 0.00003]
|
|
* [0.00001, 0.00001, 0.00001]
|
|
* [-0.00001, -0.00001, -0.00001]
|
|
* ['-', '-']
|
|
* ['-', 10]
|
|
* logarithmic axis in scatter (try dataZoom)
|
|
* logarithmic axis width dataZoom component (try xAxis and yAxis)
|
|
*/
|
|
|
|
/**
|
|
* Main function. Return data object with values for axis building.
|
|
*
|
|
* @public
|
|
* @param {Object} [opts] Configurable options
|
|
* @param {number} opts.dataMin data Minimum
|
|
* @param {number} opts.dataMax data Maximum
|
|
* @param {number=} opts.logPositive Logarithmic sign. If not specified, it will be auto-detected.
|
|
* @param {number=} opts.logLabelBase Logaithmic base in axis label.
|
|
* If not specified, it will be set to 10 (and use 2 for detail)
|
|
* @param {number=} opts.splitNumber Number of sections perfered.
|
|
* @return {Object} {
|
|
* dataMin: New min,
|
|
* dataMax: New max,
|
|
* tickList: [Array of tick data]
|
|
* logPositive: Type of data sign
|
|
* dataMappingMethods: [Set of logarithmic methods]
|
|
* }
|
|
*/
|
|
function smartLogSteps(opts) {
|
|
clearStaticVariables();
|
|
custOpts = opts || {};
|
|
|
|
reformSetting();
|
|
makeTicksList();
|
|
|
|
return [
|
|
makeResult(),
|
|
clearStaticVariables()
|
|
][0];
|
|
}
|
|
|
|
/**
|
|
* All of static variables must be clear here.
|
|
*/
|
|
function clearStaticVariables() {
|
|
logPositive = custOpts = logMappingOffset = lnBase =
|
|
absMin = absMax = splitNumber = tickList = logLabelBase = logLabelMode = null;
|
|
}
|
|
|
|
/**
|
|
* Determine sign (logPositive, negative) of data set, if not specified.
|
|
* Reform min and max of data.
|
|
*/
|
|
function reformSetting() {
|
|
// Settings of log label base
|
|
logLabelBase = custOpts.logLabelBase;
|
|
if (logLabelBase == null) {
|
|
logLabelMode = 'plain';
|
|
logLabelBase = 10;
|
|
lnBase = LN10;
|
|
}
|
|
else {
|
|
logLabelBase = +logLabelBase;
|
|
if (logLabelBase < 1) { // log base less than 1 is not supported.
|
|
logLabelBase = 10;
|
|
}
|
|
logLabelMode = 'exponent';
|
|
lnBase = mathLog(logLabelBase);
|
|
}
|
|
|
|
// Settings of split number
|
|
splitNumber = custOpts.splitNumber;
|
|
splitNumber == null && (splitNumber = DEFAULT_SPLIT_NUMBER);
|
|
|
|
// Setting of data min and max
|
|
var dataMin = parseFloat(custOpts.dataMin);
|
|
var dataMax = parseFloat(custOpts.dataMax);
|
|
|
|
if (!isFinite(dataMin) && !isFinite(dataMax)) {
|
|
dataMin = dataMax = 1;
|
|
}
|
|
else if (!isFinite(dataMin)) {
|
|
dataMin = dataMax;
|
|
}
|
|
else if (!isFinite(dataMax)) {
|
|
dataMax = dataMin;
|
|
}
|
|
else if (dataMin > dataMax) {
|
|
dataMax = [dataMin, dataMin = dataMax][0]; // Exchange min, max.
|
|
}
|
|
|
|
// Settings of log positive
|
|
logPositive = custOpts.logPositive;
|
|
// If not specified, determine sign by data.
|
|
if (logPositive == null) {
|
|
// LogPositive is false when dataMax <= 0 && dataMin < 0.
|
|
// LogPositive is true when dataMin >= 0.
|
|
// LogPositive is true when dataMax >= 0 && dataMin < 0 (singular points may exists)
|
|
logPositive = dataMax > 0 || dataMin === 0;
|
|
}
|
|
|
|
// Settings of absMin and absMax, which must be greater than 0.
|
|
absMin = logPositive ? dataMin : -dataMax;
|
|
absMax = logPositive ? dataMax : -dataMin;
|
|
// FIXME
|
|
// If there is any data item less then zero, it is suppose to be igonred and min should be re-calculated.
|
|
// But it is difficult to do that in current code stucture.
|
|
// So refactor of xxAxis.js is desired.
|
|
absMin < EPSILON && (absMin = EPSILON);
|
|
absMax < EPSILON && (absMax = EPSILON);
|
|
}
|
|
|
|
/**
|
|
* Make tick list.
|
|
*/
|
|
function makeTicksList() {
|
|
tickList = [];
|
|
|
|
// Estimate max exponent and min exponent
|
|
var maxDataLog = fixAccurate(mathLog(absMax) / lnBase);
|
|
var minDataLog = fixAccurate(mathLog(absMin) / lnBase);
|
|
var maxExpon = mathCeil(maxDataLog);
|
|
var minExpon = mathFloor(minDataLog);
|
|
var spanExpon = maxExpon - minExpon;
|
|
var spanDataLog = maxDataLog - minDataLog;
|
|
|
|
if (logLabelMode === 'exponent') {
|
|
baseAnalysis();
|
|
}
|
|
else { // logLabelMode === 'plain', we will self-adapter
|
|
!(
|
|
spanExpon <= MIN_BASE_10_SPLIT_NUMBER
|
|
&& splitNumber > MIN_BASE_10_SPLIT_NUMBER
|
|
)
|
|
? baseAnalysis() : detailAnalysis();
|
|
}
|
|
|
|
// In this situation, only draw base-10 ticks.
|
|
// Base-10 ticks: 10^h (i.e. 0.01, 0.1, 1, 10, 100, ...)
|
|
function baseAnalysis() {
|
|
if (spanExpon < splitNumber) {
|
|
splitNumber = spanExpon;
|
|
}
|
|
// Suppose:
|
|
// spanExpon > splitNumber
|
|
// stepExpon := floor(spanExpon / splitNumber)
|
|
// splitNumberFloat := spanExpon / stepExpon
|
|
// There are tow expressions which are identically-true:
|
|
// splitNumberFloat - splitNumber <= 1
|
|
// stepExpon * ceil(splitNumberFloat) - spanExpon <= stepExpon
|
|
// So we can calculate as follows:
|
|
var stepExpon = mathFloor(fixAccurate(spanExpon / splitNumber));
|
|
|
|
// Put the plot in the middle of the min, max.
|
|
var splitNumberAdjust = mathCeil(fixAccurate(spanExpon / stepExpon));
|
|
var spanExponAdjust = stepExpon * splitNumberAdjust;
|
|
var halfDiff = (spanExponAdjust - spanDataLog) / 2;
|
|
var minExponAdjust = mathFloor(fixAccurate(minDataLog - halfDiff));
|
|
|
|
if (aroundZero(minExponAdjust - minDataLog)) {
|
|
minExponAdjust -= 1;
|
|
}
|
|
|
|
// Build logMapping offset
|
|
logMappingOffset = -minExponAdjust * lnBase;
|
|
|
|
// Build tickList
|
|
for (var n = minExponAdjust; n - stepExpon <= maxDataLog; n += stepExpon) {
|
|
tickList.push(mathPow(logLabelBase, n));
|
|
}
|
|
}
|
|
|
|
// In this situation, base-2|10 ticks are used to make detailed split.
|
|
// Base-2|10 ticks: 10^h * 2^k (i.e. 0.1, 0.2, 0.4, 1, 2, 4, 10, 20, 40),
|
|
// where k in [0, 1, 2].
|
|
// Because LN2 * 3 < LN10 and LN2 * 4 > LN10, k should be less than 3.
|
|
// And when k === 3, the tick is too close to that of k === 0, which looks weird.
|
|
// So we do not use 3.
|
|
function detailAnalysis() {
|
|
// Find max exponent and min exponent.
|
|
// Calculate base on 3-hexadecimal (0, 1, 2, 10, 11, 12, 20).
|
|
var minDecimal = toDecimalFrom4Hex(minExpon, 0);
|
|
var endDecimal = minDecimal + 2;
|
|
while (
|
|
minDecimal < endDecimal
|
|
&& toH(minDecimal + 1) + toK(minDecimal + 1) * LN2D10 < minDataLog
|
|
) {
|
|
minDecimal++;
|
|
}
|
|
var maxDecimal = toDecimalFrom4Hex(maxExpon, 0);
|
|
var endDecimal = maxDecimal - 2; // maxDecimal is greater than 4
|
|
while (
|
|
maxDecimal > endDecimal
|
|
&& toH(maxDecimal - 1) + toK(maxDecimal - 1) * LN2D10 > maxDataLog
|
|
) {
|
|
maxDecimal--;
|
|
}
|
|
|
|
// Build logMapping offset
|
|
logMappingOffset = -(toH(minDecimal) * LN10 + toK(minDecimal) * LN2);
|
|
|
|
// Build logMapping tickList
|
|
for (var i = minDecimal; i <= maxDecimal; i++) {
|
|
var h = toH(i);
|
|
var k = toK(i);
|
|
tickList.push(mathPow(10, h) * mathPow(2, k));
|
|
}
|
|
}
|
|
|
|
// Convert to decimal number from 4-hexadecimal number,
|
|
// where h, k means: if there is a 4-hexadecimal numer 23, then h is 2, k is 3.
|
|
// h can be any integer (notice: h can be greater than 10 or less than 0),
|
|
// and k belongs to [0, 1, 2, 3].
|
|
function toDecimalFrom4Hex(h, k) {
|
|
return h * 3 + k;
|
|
}
|
|
|
|
function toK(decimal) {
|
|
return decimal - toH(decimal) * 3; // Can not calculate by '%'
|
|
}
|
|
|
|
function toH(decimal) {
|
|
return mathFloor(fixAccurate(decimal / 3));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make result
|
|
*/
|
|
function makeResult() {
|
|
var resultTickList = [];
|
|
for (var i = 0, len = tickList.length; i < len; i++) {
|
|
resultTickList[i] = (logPositive ? 1 : -1) * tickList[i];
|
|
}
|
|
!logPositive && resultTickList.reverse();
|
|
|
|
var dataMappingMethods = makeDataMappingMethods();
|
|
var value2Coord = dataMappingMethods.value2Coord;
|
|
|
|
var newDataMin = value2Coord(resultTickList[0]);
|
|
var newDataMax = value2Coord(resultTickList[resultTickList.length - 1]);
|
|
|
|
if (newDataMin === newDataMax) {
|
|
newDataMin -= 1;
|
|
newDataMax += 1;
|
|
}
|
|
|
|
return {
|
|
dataMin: newDataMin,
|
|
dataMax: newDataMax,
|
|
tickList: resultTickList,
|
|
logPositive: logPositive,
|
|
labelFormatter: makeLabelFormatter(),
|
|
dataMappingMethods: dataMappingMethods
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Make axis label formatter.
|
|
*/
|
|
function makeLabelFormatter() {
|
|
if (logLabelMode === 'exponent') { // For label style like 3⁴.
|
|
// Static variables should be fixed in the scope of the methods.
|
|
var myLogLabelBase = logLabelBase;
|
|
var myLnBase = lnBase;
|
|
|
|
return function (value) {
|
|
if (!isFinite(parseFloat(value))) {
|
|
return '';
|
|
}
|
|
var sign = '';
|
|
if (value < 0) {
|
|
value = -value;
|
|
sign = '-';
|
|
}
|
|
return sign + myLogLabelBase + makeSuperscriptExponent(mathLog(value) / myLnBase);
|
|
};
|
|
}
|
|
else {
|
|
return function (value) { // Normal style like 0.001, 10,000,0
|
|
if (!isFinite(parseFloat(value))) {
|
|
return '';
|
|
}
|
|
return number.addCommas(formatNumber(value));
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make calculate methods.
|
|
*/
|
|
function makeDataMappingMethods() {
|
|
// Static variables should be fixed in the scope of the methods.
|
|
var myLogPositive = logPositive;
|
|
var myLogMappingOffset = logMappingOffset;
|
|
|
|
return {
|
|
value2Coord: function (x) {
|
|
if (x == null || isNaN(x) || !isFinite(x)) {
|
|
return x;
|
|
}
|
|
x = parseFloat(x); // to number
|
|
if (!isFinite(x)) {
|
|
x = EPSILON;
|
|
}
|
|
else if (myLogPositive && x < EPSILON) {
|
|
// FIXME
|
|
// It is suppose to be ignore, but not be set to EPSILON. See comments above.
|
|
x = EPSILON;
|
|
}
|
|
else if (!myLogPositive && x > -EPSILON) {
|
|
x = -EPSILON;
|
|
}
|
|
x = mathAbs(x);
|
|
return (myLogPositive ? 1 : -1) * (mathLog(x) + myLogMappingOffset);
|
|
},
|
|
coord2Value: function (x) {
|
|
if (x == null || isNaN(x) || !isFinite(x)) {
|
|
return x;
|
|
}
|
|
x = parseFloat(x); // to number
|
|
if (!isFinite(x)) {
|
|
x = EPSILON;
|
|
}
|
|
return myLogPositive
|
|
? mathPow(LOG_BASE, x - myLogMappingOffset)
|
|
: -mathPow(LOG_BASE, -x + myLogMappingOffset);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* For example, Math.log(1000) / Math.LN10 get the result of 2.9999999999999996, rather than 3.
|
|
* This method trys to fix it.
|
|
* (accMath.div can not fix this problem yet.)
|
|
*/
|
|
function fixAccurate(result) {
|
|
return +Number(+result).toFixed(14);
|
|
}
|
|
|
|
/**
|
|
* Avoid show float number like '1e-9', '-1e-10', ...
|
|
* @return {string}
|
|
*/
|
|
function formatNumber(num) {
|
|
return Number(num).toFixed(15).replace(/\.?0*$/, '');
|
|
}
|
|
|
|
/**
|
|
* Make superscript exponent
|
|
*/
|
|
function makeSuperscriptExponent(exponent) {
|
|
exponent = formatNumber(Math.round(exponent)); // Do not support float superscript.
|
|
// (because I can not find superscript style of '.')
|
|
var result = [];
|
|
for (var i = 0, len = exponent.length; i < len; i++) {
|
|
var cha = exponent.charAt(i);
|
|
result.push(SUPERSCRIPTS[cha] || '');
|
|
}
|
|
return result.join('');
|
|
}
|
|
|
|
/**
|
|
* Decide whether near zero
|
|
*/
|
|
function aroundZero(val) {
|
|
return val > -EPSILON && val < EPSILON;
|
|
}
|
|
|
|
return smartLogSteps;
|
|
});
|
|
|