Skip to content

Commit 6b54152

Browse files
authored
Merge pull request #1930 from plotly/big-exponents
Better exponent treatment beyond SI prefixes
2 parents e78ed04 + 52ac7f4 commit 6b54152

File tree

3 files changed

+141
-13
lines changed

3 files changed

+141
-13
lines changed

src/constants/numerical.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,11 @@ module.exports = {
4747
/*
4848
* Are two values nearly equal? Compare to 1PPM
4949
*/
50-
ALMOST_EQUAL: 1 - 1e-6
50+
ALMOST_EQUAL: 1 - 1e-6,
51+
52+
/*
53+
* not a number, but for displaying numbers: the "minus sign" symbol is
54+
* wider than the regular ascii dash "-"
55+
*/
56+
MINUS_SIGN: '\u2212'
5157
};

src/plots/cartesian/axes.js

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ var ONEDAY = constants.ONEDAY;
2727
var ONEHOUR = constants.ONEHOUR;
2828
var ONEMIN = constants.ONEMIN;
2929
var ONESEC = constants.ONESEC;
30+
var MINUS_SIGN = constants.MINUS_SIGN;
3031

3132
var MID_SHIFT = require('../../constants/alignment').MID_SHIFT;
3233

@@ -1055,7 +1056,7 @@ function autoTickRound(ax) {
10551056

10561057
var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01);
10571058
if(Math.abs(rangeexp) > 3) {
1058-
if(ax.exponentformat === 'SI' || ax.exponentformat === 'B') {
1059+
if(isSIFormat(ax.exponentformat) && !beyondSI(rangeexp)) {
10591060
ax._tickexponent = 3 * Math.round((rangeexp - 1) / 3);
10601061
}
10611062
else ax._tickexponent = rangeexp;
@@ -1299,12 +1300,13 @@ function formatLog(ax, out, hover, extraPrecision, hideexp) {
12991300
out.text = numFormat(Math.pow(10, x), ax, hideexp, extraPrecision);
13001301
}
13011302
else if(isNumeric(dtick) || ((dtick.charAt(0) === 'D') && (Lib.mod(x + 0.01, 1) < 0.1))) {
1302-
if(['e', 'E', 'power'].indexOf(ax.exponentformat) !== -1) {
1303-
var p = Math.round(x);
1303+
var p = Math.round(x);
1304+
if(['e', 'E', 'power'].indexOf(ax.exponentformat) !== -1 ||
1305+
(isSIFormat(ax.exponentformat) && beyondSI(p))) {
13041306
if(p === 0) out.text = 1;
13051307
else if(p === 1) out.text = '10';
13061308
else if(p > 1) out.text = '10<sup>' + p + '</sup>';
1307-
else out.text = '10<sup>\u2212' + -p + '</sup>';
1309+
else out.text = '10<sup>' + MINUS_SIGN + -p + '</sup>';
13081310

13091311
out.fontSize *= 1.25;
13101312
}
@@ -1359,6 +1361,21 @@ function formatLinear(ax, out, hover, extraPrecision, hideexp) {
13591361
// also automatically switch to sci. notation
13601362
var SIPREFIXES = ['f', 'p', 'n', 'μ', 'm', '', 'k', 'M', 'G', 'T'];
13611363

1364+
function isSIFormat(exponentFormat) {
1365+
return exponentFormat === 'SI' || exponentFormat === 'B';
1366+
}
1367+
1368+
// are we beyond the range of common SI prefixes?
1369+
// 10^-16 -> 1x10^-16
1370+
// 10^-15 -> 1f
1371+
// ...
1372+
// 10^14 -> 100T
1373+
// 10^15 -> 1x10^15
1374+
// 10^16 -> 1x10^16
1375+
function beyondSI(exponent) {
1376+
return exponent > 14 || exponent < -15;
1377+
}
1378+
13621379
function numFormat(v, ax, fmtoverride, hover) {
13631380
// negative?
13641381
var isNeg = v < 0,
@@ -1387,7 +1404,7 @@ function numFormat(v, ax, fmtoverride, hover) {
13871404
if(ax.hoverformat) tickformat = ax.hoverformat;
13881405
}
13891406

1390-
if(tickformat) return d3.format(tickformat)(v).replace(/-/g, '\u2212');
1407+
if(tickformat) return d3.format(tickformat)(v).replace(/-/g, MINUS_SIGN);
13911408

13921409
// 'epsilon' - rounding increment
13931410
var e = Math.pow(10, -tickRound) / 2;
@@ -1436,14 +1453,14 @@ function numFormat(v, ax, fmtoverride, hover) {
14361453

14371454
// add exponent
14381455
if(exponent && exponentFormat !== 'hide') {
1456+
if(isSIFormat(exponentFormat) && beyondSI(exponent)) exponentFormat = 'power';
1457+
14391458
var signedExponent;
1440-
if(exponent < 0) signedExponent = '\u2212' + -exponent;
1459+
if(exponent < 0) signedExponent = MINUS_SIGN + -exponent;
14411460
else if(exponentFormat !== 'power') signedExponent = '+' + exponent;
14421461
else signedExponent = String(exponent);
14431462

1444-
if(exponentFormat === 'e' ||
1445-
((exponentFormat === 'SI' || exponentFormat === 'B') &&
1446-
(exponent > 12 || exponent < -15))) {
1463+
if(exponentFormat === 'e') {
14471464
v += 'e' + signedExponent;
14481465
}
14491466
else if(exponentFormat === 'E') {
@@ -1455,19 +1472,18 @@ function numFormat(v, ax, fmtoverride, hover) {
14551472
else if(exponentFormat === 'B' && exponent === 9) {
14561473
v += 'B';
14571474
}
1458-
else if(exponentFormat === 'SI' || exponentFormat === 'B') {
1475+
else if(isSIFormat(exponentFormat)) {
14591476
v += SIPREFIXES[exponent / 3 + 5];
14601477
}
14611478
}
14621479

14631480
// put sign back in and return
14641481
// replace standard minus character (which is technically a hyphen)
14651482
// with a true minus sign
1466-
if(isNeg) return '\u2212' + v;
1483+
if(isNeg) return MINUS_SIGN + v;
14671484
return v;
14681485
}
14691486

1470-
14711487
axes.subplotMatch = /^x([0-9]*)y([0-9]*)$/;
14721488

14731489
// getSubplots - extract all combinations of axes we need to make plots for

test/jasmine/tests/axes_test.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1929,6 +1929,112 @@ describe('Test axes', function() {
19291929
});
19301930
}
19311931

1932+
it('reverts to "power" for SI/B exponentformat beyond the prefix range (linear case)', function() {
1933+
var textOut = mockCalc({
1934+
type: 'linear',
1935+
tickmode: 'linear',
1936+
exponentformat: 'B',
1937+
showexponent: 'all',
1938+
tick0: 0,
1939+
dtick: 1e13,
1940+
range: [8.5e13, 11.5e13]
1941+
});
1942+
1943+
expect(textOut).toEqual([
1944+
'90T', '100T', '110T'
1945+
]);
1946+
1947+
textOut = mockCalc({
1948+
type: 'linear',
1949+
tickmode: 'linear',
1950+
exponentformat: 'B',
1951+
showexponent: 'all',
1952+
tick0: 0,
1953+
dtick: 1e14,
1954+
range: [8.5e14, 11.5e14]
1955+
});
1956+
1957+
expect(textOut).toEqual([
1958+
'0.9×10<sup>15</sup>',
1959+
'1×10<sup>15</sup>',
1960+
'1.1×10<sup>15</sup>'
1961+
]);
1962+
1963+
textOut = mockCalc({
1964+
type: 'linear',
1965+
tickmode: 'linear',
1966+
exponentformat: 'SI',
1967+
showexponent: 'all',
1968+
tick0: 0,
1969+
dtick: 1e-16,
1970+
range: [8.5e-16, 11.5e-16]
1971+
});
1972+
1973+
expect(textOut).toEqual([
1974+
'0.9f', '1f', '1.1f'
1975+
]);
1976+
1977+
textOut = mockCalc({
1978+
type: 'linear',
1979+
tickmode: 'linear',
1980+
exponentformat: 'SI',
1981+
showexponent: 'all',
1982+
tick0: 0,
1983+
dtick: 1e-17,
1984+
range: [8.5e-17, 11.5e-17]
1985+
});
1986+
1987+
expect(textOut).toEqual([
1988+
'0.9×10<sup>\u221216</sup>',
1989+
'1×10<sup>\u221216</sup>',
1990+
'1.1×10<sup>\u221216</sup>'
1991+
]);
1992+
});
1993+
1994+
it('reverts to "power" for SI/B exponentformat beyond the prefix range (log case)', function() {
1995+
var textOut = mockCalc({
1996+
type: 'log',
1997+
tickmode: 'linear',
1998+
exponentformat: 'B',
1999+
showexponent: 'all',
2000+
tick0: 0,
2001+
dtick: 1,
2002+
range: [-18.5, 18.5]
2003+
});
2004+
2005+
expect(textOut).toEqual([
2006+
'10<sup>\u221218</sup>',
2007+
'10<sup>\u221217</sup>',
2008+
'10<sup>\u221216</sup>',
2009+
'1f', '10f', '100f', '1p', '10p', '100p', '1n', '10n', '100n',
2010+
'1μ', '10μ', '100μ', '0.001', '0.01', '0.1', '1', '10', '100',
2011+
'1000', '10k', '100k', '1M', '10M', '100M', '1B', '10B', '100B',
2012+
'1T', '10T', '100T',
2013+
'10<sup>15</sup>',
2014+
'10<sup>16</sup>',
2015+
'10<sup>17</sup>',
2016+
'10<sup>18</sup>'
2017+
]);
2018+
2019+
textOut = mockCalc({
2020+
type: 'log',
2021+
tickmode: 'linear',
2022+
exponentformat: 'SI',
2023+
showexponent: 'all',
2024+
tick0: 0,
2025+
dtick: 'D2',
2026+
range: [7.9, 12.1]
2027+
});
2028+
2029+
expect(textOut).toEqual([
2030+
'100M', '2', '5',
2031+
'1G', '2', '5',
2032+
'10G', '2', '5',
2033+
'100G', '2', '5',
2034+
'1T'
2035+
]);
2036+
});
2037+
19322038
it('provides a new date suffix whenever the suffix changes', function() {
19332039
var ax = {
19342040
type: 'date',

0 commit comments

Comments
 (0)