From ca08f3a486b90f0c13f91ec46830c18bd6433e99 Mon Sep 17 00:00:00 2001 From: hasnaat Date: Thu, 23 Apr 2026 00:23:44 +0500 Subject: [PATCH 1/5] fix(axes): format tick labels correctly for small numbers in exponential notation --- src/plots/cartesian/axes.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 38a8a7a8909..1fdcb984753 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -2210,9 +2210,27 @@ function numFormat(v, ax, fmtoverride, hover) { v = v.slice(0, Math.max(0, v.length + tickRound)); for(var i = tickRound; i < 0; i++) v += '0'; } else { - v = String(v); - var dp = v.indexOf('.') + 1; - if(dp) v = v.slice(0, dp + tickRound).replace(/\.?0+$/, ''); + var vStr = String(v); + var ep = vStr.indexOf('e'); + if(ep >= 0) { + var mantissa = vStr.slice(0, ep); + var exponentStr = vStr.slice(ep); + var dp = mantissa.indexOf('.') + 1; + var exponentVal = parseInt(exponentStr.slice(1), 10); + var adjustedTickRound = tickRound + exponentVal; + if(dp) { + if(adjustedTickRound < 0) { + mantissa = mantissa.slice(0, dp - 1); + } else { + mantissa = mantissa.slice(0, dp + adjustedTickRound).replace(/\.?0+$/, ''); + } + } + v = mantissa + exponentStr; + } else { + v = vStr; + var dp = v.indexOf('.') + 1; + if(dp) v = v.slice(0, dp + tickRound).replace(/\.?0+$/, ''); + } } // insert appropriate decimal point and thousands separator v = Lib.numSeparate(v, ax._separators, separatethousands); From 8908ff993d27ce385a70161ab963c796dc9ad270 Mon Sep 17 00:00:00 2001 From: hasnaat Date: Sun, 21 Jun 2026 16:19:58 +0500 Subject: [PATCH 2/5] Apply maintainer suggestion: use toFixed instead of string manipulation --- src/plots/cartesian/axes.js | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 1fdcb984753..3307f7358e7 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -2210,27 +2210,7 @@ function numFormat(v, ax, fmtoverride, hover) { v = v.slice(0, Math.max(0, v.length + tickRound)); for(var i = tickRound; i < 0; i++) v += '0'; } else { - var vStr = String(v); - var ep = vStr.indexOf('e'); - if(ep >= 0) { - var mantissa = vStr.slice(0, ep); - var exponentStr = vStr.slice(ep); - var dp = mantissa.indexOf('.') + 1; - var exponentVal = parseInt(exponentStr.slice(1), 10); - var adjustedTickRound = tickRound + exponentVal; - if(dp) { - if(adjustedTickRound < 0) { - mantissa = mantissa.slice(0, dp - 1); - } else { - mantissa = mantissa.slice(0, dp + adjustedTickRound).replace(/\.?0+$/, ''); - } - } - v = mantissa + exponentStr; - } else { - v = vStr; - var dp = v.indexOf('.') + 1; - if(dp) v = v.slice(0, dp + tickRound).replace(/\.?0+$/, ''); - } + v = v.toFixed(Math.max(0, Math.min(20, tickRound))).replace(/\.?0+$/, ''); } // insert appropriate decimal point and thousands separator v = Lib.numSeparate(v, ax._separators, separatethousands); From bc032db643b45af853ab1e510cb89fa59b34278e Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Tue, 23 Jun 2026 16:31:31 -0600 Subject: [PATCH 3/5] Subtract rounding increment when using `toFixed` --- src/plots/cartesian/axes.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 3307f7358e7..8b4446e4c65 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -2210,7 +2210,9 @@ function numFormat(v, ax, fmtoverride, hover) { v = v.slice(0, Math.max(0, v.length + tickRound)); for(var i = tickRound; i < 0; i++) v += '0'; } else { - v = v.toFixed(Math.max(0, Math.min(20, tickRound))).replace(/\.?0+$/, ''); + // subtract the half-epsilon added above so toFixed doesn't double-round + v -= e; + v = v.toFixed(Math.min(20, tickRound)).replace(/\.?0+$/, ''); } // insert appropriate decimal point and thousands separator v = Lib.numSeparate(v, ax._separators, separatethousands); From 40334d546396621be42aacc50be9aeb04d0e2db4 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 24 Jun 2026 09:11:33 -0600 Subject: [PATCH 4/5] Refactor rounding logic; don't apply rounding increment to value --- src/plots/cartesian/axes.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 8b4446e4c65..4a7aa24624b 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -2175,9 +2175,6 @@ function numFormat(v, ax, fmtoverride, hover) { if(tickformat) return ax._numFormat(tickformat)(v).replace(/-/g, MINUS_SIGN); - // 'epsilon' - rounding increment - var e = Math.pow(10, -tickRound) / 2; - // exponentFormat codes: // 'e' (1.2e+6, default) // 'E' (1.2E+6) @@ -2192,26 +2189,26 @@ function numFormat(v, ax, fmtoverride, hover) { // take the sign out, put it back manually at the end // - makes cases easier v = Math.abs(v); + + // 'epsilon' - rounding increment + const e = Math.pow(10, -tickRound) / 2; if(v < e) { // 0 is just 0, but may get exponent if it's the last tick v = '0'; isNeg = false; } else { - v += e; // take out a common exponent, if any if(exponent) { v *= Math.pow(10, -exponent); tickRound += exponent; } // round the mantissa - if(tickRound === 0) v = String(Math.floor(v)); - else if(tickRound < 0) { + if(tickRound === 0) { v = String(Math.round(v)); - v = v.slice(0, Math.max(0, v.length + tickRound)); - for(var i = tickRound; i < 0; i++) v += '0'; + } else if(tickRound < 0) { + const roundingMagnitude = Math.pow(10, -tickRound); + v = String(Math.round(v / roundingMagnitude) * roundingMagnitude); } else { - // subtract the half-epsilon added above so toFixed doesn't double-round - v -= e; v = v.toFixed(Math.min(20, tickRound)).replace(/\.?0+$/, ''); } // insert appropriate decimal point and thousands separator From 8a3a1ccdfec3eb1598f5aafdf08bcff2ac3598ce Mon Sep 17 00:00:00 2001 From: hasnaat Date: Sat, 27 Jun 2026 18:28:26 +0500 Subject: [PATCH 5/5] test: add tests for tick formatting of small numbers in exponential notation and add draftlog --- draftlogs/7768_fix.md | 1 + test/jasmine/tests/axes_test.js | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 draftlogs/7768_fix.md diff --git a/draftlogs/7768_fix.md b/draftlogs/7768_fix.md new file mode 100644 index 00000000000..39ccc08c057 --- /dev/null +++ b/draftlogs/7768_fix.md @@ -0,0 +1 @@ + - Format tick labels correctly for small numbers in exponential notation [[#7768](https://github.com/plotly/plotly.js/pull/7768)] diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 3d1e962312f..755e1747a28 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -3627,6 +3627,50 @@ describe('Test axes', function() { }); }); + it('formats tick labels correctly for small numbers in exponential notation', function() { + var textOut = mockCalc({ + type: 'linear', + tickmode: 'linear', + exponentformat: 'none', + showexponent: 'all', + tick0: 0, + dtick: 1e-9, + range: [8.5e-9, 11.5e-9] + }); + + expect(textOut).toEqual([ + '0.000000009', '0.00000001', '0.000000011' + ]); + + textOut = mockCalc({ + type: 'linear', + tickmode: 'linear', + exponentformat: 'none', + showexponent: 'all', + tick0: 0, + dtick: 1e-15, + range: [8.5e-15, 11.5e-15] + }); + + expect(textOut).toEqual([ + '0.000000000000009', '0.00000000000001', '0.000000000000011' + ]); + + textOut = mockCalc({ + type: 'linear', + tickmode: 'linear', + exponentformat: 'e', + showexponent: 'all', + tick0: 0, + dtick: 1e-15, + range: [8.5e-15, 11.5e-15] + }); + + expect(textOut).toEqual([ + '0.9e\u221214', '1e\u221214', '1.1e\u221214' + ]); + }); + it('provides a new date suffix whenever the suffix changes', function() { var ax = { type: 'date',