diff --git a/src/constants/numerical.js b/src/constants/numerical.js index 6b0d6b55ed2..ac18cf60ee3 100644 --- a/src/constants/numerical.js +++ b/src/constants/numerical.js @@ -47,5 +47,11 @@ module.exports = { /* * Are two values nearly equal? Compare to 1PPM */ - ALMOST_EQUAL: 1 - 1e-6 + ALMOST_EQUAL: 1 - 1e-6, + + /* + * not a number, but for displaying numbers: the "minus sign" symbol is + * wider than the regular ascii dash "-" + */ + MINUS_SIGN: '\u2212' }; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 8a25ad031c4..c2009959aa3 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -27,6 +27,7 @@ var ONEDAY = constants.ONEDAY; var ONEHOUR = constants.ONEHOUR; var ONEMIN = constants.ONEMIN; var ONESEC = constants.ONESEC; +var MINUS_SIGN = constants.MINUS_SIGN; var MID_SHIFT = require('../../constants/alignment').MID_SHIFT; @@ -1055,7 +1056,7 @@ function autoTickRound(ax) { var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01); if(Math.abs(rangeexp) > 3) { - if(ax.exponentformat === 'SI' || ax.exponentformat === 'B') { + if(isSIFormat(ax.exponentformat) && !beyondSI(rangeexp)) { ax._tickexponent = 3 * Math.round((rangeexp - 1) / 3); } else ax._tickexponent = rangeexp; @@ -1299,12 +1300,13 @@ function formatLog(ax, out, hover, extraPrecision, hideexp) { out.text = numFormat(Math.pow(10, x), ax, hideexp, extraPrecision); } else if(isNumeric(dtick) || ((dtick.charAt(0) === 'D') && (Lib.mod(x + 0.01, 1) < 0.1))) { - if(['e', 'E', 'power'].indexOf(ax.exponentformat) !== -1) { - var p = Math.round(x); + var p = Math.round(x); + if(['e', 'E', 'power'].indexOf(ax.exponentformat) !== -1 || + (isSIFormat(ax.exponentformat) && beyondSI(p))) { if(p === 0) out.text = 1; else if(p === 1) out.text = '10'; else if(p > 1) out.text = '10' + p + ''; - else out.text = '10\u2212' + -p + ''; + else out.text = '10' + MINUS_SIGN + -p + ''; out.fontSize *= 1.25; } @@ -1359,6 +1361,21 @@ function formatLinear(ax, out, hover, extraPrecision, hideexp) { // also automatically switch to sci. notation var SIPREFIXES = ['f', 'p', 'n', 'μ', 'm', '', 'k', 'M', 'G', 'T']; +function isSIFormat(exponentFormat) { + return exponentFormat === 'SI' || exponentFormat === 'B'; +} + +// are we beyond the range of common SI prefixes? +// 10^-16 -> 1x10^-16 +// 10^-15 -> 1f +// ... +// 10^14 -> 100T +// 10^15 -> 1x10^15 +// 10^16 -> 1x10^16 +function beyondSI(exponent) { + return exponent > 14 || exponent < -15; +} + function numFormat(v, ax, fmtoverride, hover) { // negative? var isNeg = v < 0, @@ -1387,7 +1404,7 @@ function numFormat(v, ax, fmtoverride, hover) { if(ax.hoverformat) tickformat = ax.hoverformat; } - if(tickformat) return d3.format(tickformat)(v).replace(/-/g, '\u2212'); + if(tickformat) return d3.format(tickformat)(v).replace(/-/g, MINUS_SIGN); // 'epsilon' - rounding increment var e = Math.pow(10, -tickRound) / 2; @@ -1436,14 +1453,14 @@ function numFormat(v, ax, fmtoverride, hover) { // add exponent if(exponent && exponentFormat !== 'hide') { + if(isSIFormat(exponentFormat) && beyondSI(exponent)) exponentFormat = 'power'; + var signedExponent; - if(exponent < 0) signedExponent = '\u2212' + -exponent; + if(exponent < 0) signedExponent = MINUS_SIGN + -exponent; else if(exponentFormat !== 'power') signedExponent = '+' + exponent; else signedExponent = String(exponent); - if(exponentFormat === 'e' || - ((exponentFormat === 'SI' || exponentFormat === 'B') && - (exponent > 12 || exponent < -15))) { + if(exponentFormat === 'e') { v += 'e' + signedExponent; } else if(exponentFormat === 'E') { @@ -1455,7 +1472,7 @@ function numFormat(v, ax, fmtoverride, hover) { else if(exponentFormat === 'B' && exponent === 9) { v += 'B'; } - else if(exponentFormat === 'SI' || exponentFormat === 'B') { + else if(isSIFormat(exponentFormat)) { v += SIPREFIXES[exponent / 3 + 5]; } } @@ -1463,11 +1480,10 @@ function numFormat(v, ax, fmtoverride, hover) { // put sign back in and return // replace standard minus character (which is technically a hyphen) // with a true minus sign - if(isNeg) return '\u2212' + v; + if(isNeg) return MINUS_SIGN + v; return v; } - axes.subplotMatch = /^x([0-9]*)y([0-9]*)$/; // getSubplots - extract all combinations of axes we need to make plots for diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index b76259e1e48..0367649a9e1 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1929,6 +1929,112 @@ describe('Test axes', function() { }); } + it('reverts to "power" for SI/B exponentformat beyond the prefix range (linear case)', function() { + var textOut = mockCalc({ + type: 'linear', + tickmode: 'linear', + exponentformat: 'B', + showexponent: 'all', + tick0: 0, + dtick: 1e13, + range: [8.5e13, 11.5e13] + }); + + expect(textOut).toEqual([ + '90T', '100T', '110T' + ]); + + textOut = mockCalc({ + type: 'linear', + tickmode: 'linear', + exponentformat: 'B', + showexponent: 'all', + tick0: 0, + dtick: 1e14, + range: [8.5e14, 11.5e14] + }); + + expect(textOut).toEqual([ + '0.9×1015', + '1×1015', + '1.1×1015' + ]); + + textOut = mockCalc({ + type: 'linear', + tickmode: 'linear', + exponentformat: 'SI', + showexponent: 'all', + tick0: 0, + dtick: 1e-16, + range: [8.5e-16, 11.5e-16] + }); + + expect(textOut).toEqual([ + '0.9f', '1f', '1.1f' + ]); + + textOut = mockCalc({ + type: 'linear', + tickmode: 'linear', + exponentformat: 'SI', + showexponent: 'all', + tick0: 0, + dtick: 1e-17, + range: [8.5e-17, 11.5e-17] + }); + + expect(textOut).toEqual([ + '0.9×10\u221216', + '1×10\u221216', + '1.1×10\u221216' + ]); + }); + + it('reverts to "power" for SI/B exponentformat beyond the prefix range (log case)', function() { + var textOut = mockCalc({ + type: 'log', + tickmode: 'linear', + exponentformat: 'B', + showexponent: 'all', + tick0: 0, + dtick: 1, + range: [-18.5, 18.5] + }); + + expect(textOut).toEqual([ + '10\u221218', + '10\u221217', + '10\u221216', + '1f', '10f', '100f', '1p', '10p', '100p', '1n', '10n', '100n', + '1μ', '10μ', '100μ', '0.001', '0.01', '0.1', '1', '10', '100', + '1000', '10k', '100k', '1M', '10M', '100M', '1B', '10B', '100B', + '1T', '10T', '100T', + '1015', + '1016', + '1017', + '1018' + ]); + + textOut = mockCalc({ + type: 'log', + tickmode: 'linear', + exponentformat: 'SI', + showexponent: 'all', + tick0: 0, + dtick: 'D2', + range: [7.9, 12.1] + }); + + expect(textOut).toEqual([ + '100M', '2', '5', + '1G', '2', '5', + '10G', '2', '5', + '100G', '2', '5', + '1T' + ]); + }); + it('provides a new date suffix whenever the suffix changes', function() { var ax = { type: 'date',