static/js/dncp/util/smartLogSteps.js
/**
* 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;
});