Skip to content

Commit 0153c9e

Browse files
committed
Snap near-zero tick values to tick0 to fix float artefacts in labels
When ticks are built by repeated increments, a tick that should sit exactly on tick0 can come out as a tiny nonzero value like -8.9e-17. The default numeric format rounds that away, but d3-format specs such as `~r` render it literally, so a tick at zero showed as -0.0000000000000000888178. When a computed tick value is far closer to tick0 than one dtick, snap it to exactly tick0. Fixes #7765
1 parent 227bba9 commit 0153c9e

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

‎src/plots/cartesian/axes.js‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,20 @@ function expandRange(range) {
9090
];
9191
}
9292

93+
// Correct a floating-point roundoff artefact. After repeated increments a tick
94+
// that should land exactly on tick0 can come out as e.g. -8.9e-17. The default
95+
// numeric tickformat rounds that away, but d3-format specs like `~r` render the
96+
// tiny value literally, so a tick at zero shows up as -0.0000000000000000888.
97+
// See https://github.com/plotly/plotly.js/issues/7765
98+
// When a value is far closer to tick0 than one dtick, snap it to exactly tick0.
99+
function snapToTick0(l, tick0l, dtick) {
100+
if(isNumeric(dtick) && isNumeric(tick0l) && isNumeric(l) &&
101+
l !== tick0l && Math.abs(l - tick0l) < Math.abs(dtick) * 1e-6) {
102+
return tick0l;
103+
}
104+
return l;
105+
}
106+
93107
/*
94108
* find the list of possible axes to reference with an xref or yref attribute
95109
* and coerce it to that list
@@ -1067,6 +1081,7 @@ axes.calcTicks = function calcTicks(ax, opts) {
10671081
}
10681082

10691083
var dtick = mockAx.dtick;
1084+
var tick0l = (type === 'linear') ? mockAx.r2l(mockAx.tick0) : undefined;
10701085

10711086
if(mockAx.rangebreaks && mockAx._tick0Init !== mockAx.tick0) {
10721087
// adjust tick0
@@ -1110,7 +1125,7 @@ axes.calcTicks = function calcTicks(ax, opts) {
11101125
if(tickVals.length > maxTicks || x === prevX) break;
11111126
prevX = x;
11121127

1113-
var obj = { value: x };
1128+
var obj = { value: snapToTick0(x, tick0l, dtick) };
11141129

11151130
if(major) {
11161131
if(isDLog && (x !== (x | 0))) {

‎test/jasmine/tests/axes_test.js‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3502,6 +3502,29 @@ describe('Test axes', function() {
35023502
]);
35033503
});
35043504

3505+
it('snaps a near-zero tick to exactly tick0 so d3-format specs do not show float artefacts (issue 7765)', function() {
3506+
// With this range the increments accumulate a roundoff error and the
3507+
// tick that should be exactly 0 comes out as ~-8.9e-17. The default
3508+
// format rounds that away, but `~r` would render it literally as
3509+
// -0.0000000000000000888178 without the snap-to-tick0 correction.
3510+
var spec = {
3511+
type: 'linear',
3512+
tickmode: 'linear',
3513+
tick0: 0,
3514+
dtick: 0.2,
3515+
range: [-0.65, 0.65]
3516+
};
3517+
3518+
expect(mockCalc(Lib.extendDeep({}, spec, {tickformat: '~r'}))).toEqual([
3519+
'−0.6', '−0.4', '−0.2', '0', '0.2', '0.4', '0.6'
3520+
]);
3521+
3522+
// the default numeric format was already fine; make sure it stays fine
3523+
expect(mockCalc(Lib.extendDeep({}, spec))).toEqual([
3524+
'−0.6', '−0.4', '−0.2', '0', '0.2', '0.4', '0.6'
3525+
]);
3526+
});
3527+
35053528
it('reverts to "power" for SI/B exponentformat beyond the prefix range (log case)', function() {
35063529
var textOut = mockCalc({
35073530
type: 'log',

0 commit comments

Comments
 (0)