-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
Description
Bug summary:
SymmetricalLogLocator appears to suffer a floating-point precision error in calculating the boundaries of the log regions of a symlog plot, which results in the addition of extra major ticks in the linear region of the plot.
Code reproducing the issue:
import numpy as np
import matplotlib.pyplot as plt # v3.2.2
x = y = np.linspace(-1e5,1e5,100)
plt.plot(x,y)
plt.xscale('symlog', linthreshx=1e3, subsx=[2,3,4,5,6,7,8,9])(OS=Windows 10, Python=3.8.3, Anaconda=2020.07)
Output (with error highlighted in red):

Discussion:
Major ticks generated by SymmetricalLogLocator sometimes include an extra tick in the linear region:
>>> locator = mpl.ticker.SymmetricalLogLocator(linthresh=1e3, base=10)
>>> locator.tick_values(vmin=0, vmax=1e6)
# array([0.e+00, 1.e+02, 1.e+03, 1.e+04, 1.e+05, 1.e+06]) <-- 1.e+02 is in the linear regionSymmetricalLogLocator calculates the inner (towards zero) boundaries of the log regions via the following base-agnostic logarithm:
lo = np.floor(np.log(linthresh) / np.log(base))However, when base=10, some power-of-ten thresholds cause floating-point precision problems:
>>> base=10
>>> [np.log(linthresh)/np.log(base) for linthresh in base**np.arange(10)]
# [0.0, 1.0, 2.0, 2.9999999999999996, 4.0, 5.0, 5.999999999999999, 7.0, 8.0, 8.999999999999998]With the np.floor() calculation, this introduces an additional erroneous innermost major tick.
@dstansby proposed a possible fix to a related issue in #14309, which changes np.floor() to np.ceil(). This looks like it would work with the precision errors I identified above, which all err towards zero, but it would fail with bases that err away from zero, e.g., base=5:
>>> base=5
>>> [np.log(linthresh)/np.log(base) for linthresh in base**np.arange(10)]
# [0.0, 1.0, 2.0, 3.0000000000000004, 4.0, 5.0, 6.000000000000001, 7.0, 8.0, 9.0]Other possible solutions I can think of: (1) rounding to base within a certain proximity, or (2) adding a special case when base=10 wherein np.log10() is used instead of np.log()/np.log(base).