Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2189,7 +2189,18 @@ function numFormat(v, ax, fmtoverride, hover) {
if(ax.hoverformat) tickformat = ax.hoverformat;
}

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

// 'epsilon' - rounding increment
var e = Math.pow(10, -tickRound) / 2;
Expand Down
26 changes: 26 additions & 0 deletions test/jasmine/tests/axes_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var numerical = require('../../../src/constants/numerical');
var BADNUM = numerical.BADNUM;
var ONEDAY = numerical.ONEDAY;
var ONEWEEK = numerical.ONEWEEK;
var MINUS_SIGN = numerical.MINUS_SIGN;

var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
Expand Down Expand Up @@ -3888,6 +3889,31 @@ describe('Test axes', function() {
});
});

it('should not show floating-point artefacts with custom tickformat', function() {
// Floating-point arithmetic can leave tick values slightly off their true
// position (e.g. -8.88e-16 instead of 0). The default formatter is immune
// because it adds an internal rounding epsilon, but custom d3 formats like
// '~r' expose the raw number. Ticks should be snapped to their ideal
// position (tick0 + n*dtick) before formatting. See gh#7765.
var ax = {
type: 'linear',
tickmode: 'linear',
tick0: 0,
dtick: 0.5,
tickformat: '~r',
range: [-0.75, 0.75]
};
var textOut = mockCalc(ax);
// Without the fix the middle tick renders as '−0.0000000000000000888178'
expect(textOut).toEqual([MINUS_SIGN + '0.5', '0', '0.5']);

// Also check with a dtick that itself introduces floating-point error.
ax.dtick = 0.1;
ax.range = [-0.25, 0.25];
textOut = mockCalc(ax);
expect(textOut).toEqual([MINUS_SIGN + '0.2', MINUS_SIGN + '0.1', '0', '0.1', '0.2']);
});

it('should always start at year for date axis hover', function() {
var ax = {
type: 'date',
Expand Down