Skip to content

Commit 3f70312

Browse files
Ariel EliAriel Eli
authored andcommitted
fix: snap tick values to grid before applying custom tickformat
When floating-point arithmetic produces a tick value that is slightly off its true position (e.g. -8.88e-16 instead of 0), the default formatter is immune because it adds an internal rounding epsilon before rendering. Custom d3 formats specified via tickformat (such as '~r') don't go through that path and expose the raw number, which can produce labels like '−0.0000000000000000888178' for a tick that should read '0'. Fix by snapping v to the nearest ideal tick position (tick0 + n*dtick) before passing it to the user's formatter, using a relative threshold of 1e-9 of dtick so only genuine floating-point noise is removed. Fixes #7765
1 parent 1dc8553 commit 3f70312

2 files changed

Lines changed: 38 additions & 1 deletion

File tree

src/plots/cartesian/axes.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2189,7 +2189,18 @@ function numFormat(v, ax, fmtoverride, hover) {
21892189
if(ax.hoverformat) tickformat = ax.hoverformat;
21902190
}
21912191

2192-
if(tickformat) return ax._numFormat(tickformat)(v).replace(/-/g, MINUS_SIGN);
2192+
if(tickformat) {
2193+
// Snap v to the nearest ideal tick position before applying a custom tickformat.
2194+
// Floating-point arithmetic can leave tick values slightly off their true position
2195+
// (e.g. -8.88e-16 instead of 0). The default formatter is immune because it adds
2196+
// a rounding epsilon, but custom d3 formats like '~r' expose the raw number.
2197+
// Only snap for linear-style numeric ticks (dtick and tick0 are both numbers).
2198+
if(!hover && isNumeric(ax.dtick) && isNumeric(ax.tick0) && ax.dtick) {
2199+
var ideal = ax.tick0 + Math.round((v - ax.tick0) / ax.dtick) * ax.dtick;
2200+
if(Math.abs(v - ideal) < Math.abs(ax.dtick) * 1e-9) v = ideal;
2201+
}
2202+
return ax._numFormat(tickformat)(v).replace(/-/g, MINUS_SIGN);
2203+
}
21932204

21942205
// 'epsilon' - rounding increment
21952206
var e = Math.pow(10, -tickRound) / 2;

test/jasmine/tests/axes_test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var numerical = require('../../../src/constants/numerical');
1818
var BADNUM = numerical.BADNUM;
1919
var ONEDAY = numerical.ONEDAY;
2020
var ONEWEEK = numerical.ONEWEEK;
21+
var MINUS_SIGN = numerical.MINUS_SIGN;
2122

2223
var createGraphDiv = require('../assets/create_graph_div');
2324
var destroyGraphDiv = require('../assets/destroy_graph_div');
@@ -3888,6 +3889,31 @@ describe('Test axes', function() {
38883889
});
38893890
});
38903891

3892+
it('should not show floating-point artefacts with custom tickformat', function() {
3893+
// Floating-point arithmetic can leave tick values slightly off their true
3894+
// position (e.g. -8.88e-16 instead of 0). The default formatter is immune
3895+
// because it adds an internal rounding epsilon, but custom d3 formats like
3896+
// '~r' expose the raw number. Ticks should be snapped to their ideal
3897+
// position (tick0 + n*dtick) before formatting. See gh#7765.
3898+
var ax = {
3899+
type: 'linear',
3900+
tickmode: 'linear',
3901+
tick0: 0,
3902+
dtick: 0.5,
3903+
tickformat: '~r',
3904+
range: [-0.75, 0.75]
3905+
};
3906+
var textOut = mockCalc(ax);
3907+
// Without the fix the middle tick renders as '−0.0000000000000000888178'
3908+
expect(textOut).toEqual([MINUS_SIGN + '0.5', '0', '0.5']);
3909+
3910+
// Also check with a dtick that itself introduces floating-point error.
3911+
ax.dtick = 0.1;
3912+
ax.range = [-0.25, 0.25];
3913+
textOut = mockCalc(ax);
3914+
expect(textOut).toEqual([MINUS_SIGN + '0.2', MINUS_SIGN + '0.1', '0', '0.1', '0.2']);
3915+
});
3916+
38913917
it('should always start at year for date axis hover', function() {
38923918
var ax = {
38933919
type: 'date',

0 commit comments

Comments
 (0)