8000 BUG: replace buggy ticker.Base with _Edge_integer. · efiring/matplotlib@2b40260 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2b40260

Browse files
committed
BUG: replace buggy ticker.Base with _Edge_integer.
Closes matplotlib#11587. Tests were working by accident despite bugs in ticker.Base, but the bugs caused problems in some applications of MaxNLocator, as in matplotlib#11587. Since some user code may be relying on the buggy behavior, ticker.Base is left in place but deprecated, and its intended functionality is provided by a new private class, _Edge_integer. MaxNLocator and MultipleLocator are modified to use the new class, with minor cleanups and an additional bug fix along the way. Two new relevant test cases are added to TestMaxNLocator.
1 parent b3e900d commit 2b40260

File tree

3 files changed

+75
-16
lines changed

3 files changed

+75
-16
lines changed

lib/matplotlib/dates.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,7 +1384,7 @@ def __init__(self, base=1, month=1, day=1, tz=None):
13841384
(default jan 1).
13851385
"""
13861386
DateLocator.__init__(self, tz)
1387-
self.base = ticker.Base(base)
1387+
self.base = ticker._Edge_integer(base, 0)
13881388
self.replaced = {'month': month,
13891389
'day': day,
13901390
'hour': 0,
@@ -1403,15 +1403,15 @@ def __call__(self):
14031403
return self.tick_values(dmin, dmax)
14041404

14051405
def tick_values(self, vmin, vmax):
1406-
ymin = self.base.le(vmin.year)
1407-
ymax = self.base.ge(vmax.year)
1406+
ymin = self.base.le(vmin.year) * self.base.step
1407+
ymax = self.base.ge(vmax.year) * self.base.step
14081408

14091409
ticks = [vmin.replace(year=ymin, **self.replaced)]
14101410
while True:
14111411
dt = ticks[-1]
14121412
if dt.year >= ymax:
14131413
return date2num(ticks)
1414-
year = dt.year + self.base.get_base()
1414+
year = dt.year + self.base.step
14151415
ticks.append(dt.replace(year=year, **self.replaced))
14161416

14171417
def autoscale(self):

lib/matplotlib/tests/test_ticker.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ class TestMaxNLocator(object):
1414
(20, 100, np.array([20., 40., 60., 80., 100.])),
1515
(0.001, 0.0001, np.array([0., 0.0002, 0.0004, 0.0006, 0.0008, 0.001])),
1616
(-1e15, 1e15, np.array([-1.0e+15, -5.0e+14, 0e+00, 5e+14, 1.0e+15])),
17+
(0, 0.85e-50, np.arange(6) * 2e-51),
18+
(-0.85e-50, 0, np.arange(-5, 1) * 2e-51),
1719
]
1820

1921
integer_data = [

lib/matplotlib/ticker.py

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,13 +1661,12 @@ def view_limits(self, vmin, vmax):
16611661
return mtransforms.nonsingular(vmin, vmax)
16621662

16631663

1664+
# @cbook.deprecated("3.1")
16641665
def closeto(x, y):
1665-
if abs(x - y) < 1e-10:
1666-
return True
1667-
else:
1668-
return False
1666+
return abs(x - y) < 1e-10
16691667

16701668

1669+
# @cbook.deprecated("3.1")
16711670
class Base(object):
16721671
'this solution has some hacks to deal with floating point inaccuracies'
16731672
def __init__(self, base):
@@ -1766,6 +1765,49 @@ def scale_range(vmin, vmax, n=1, threshold=100):
17661765
return scale, offset
17671766

17681767

1768+
class _Edge_integer:
1769+
"""
1770+
Helper for MaxNLocator.
1771+
1772+
Take floating point precision limitations into account when calculating
1773+
tick locations as integer multiples of a step.
1774+
"""
1775+
def __init__(self, step, offset):
1776+
"""
1777+
*step* is a positive floating-point interval between ticks.
1778+
*offset* is the offset subtracted from the data limits
1779+
prior to calculating tick locations.
1780+
"""
1781+
if step <= 0:
1782+
raise ValueError("'step' must be positive")
1783+
self.step = step
1784+
self._offset = abs(offset)
1785+
1786+
def closeto(self, ms, edge):
1787+
# Allow more slop when the offset is large compared to the step.
1788+
if self._offset > 0:
1789+
digits = np.log10(self._offset / self.step)
1790+
tol = max(1e-10, 10 ** (digits - 12))
1791+
tol = min(0.4999, tol)
1792+
else:
1793+
tol = 1e-10
1794+
return abs(ms - edge) < tol
1795+
1796+
def le(self, x):
1797+
'Return the largest n: n*base <= x'
1798+
d, m = _divmod(x, self.step)
1799+
if self.closeto(m / self.step, 1):
1800+
return (d + 1)
1801+
return d
1802+
1803+
def ge(self, x):
1804+
'Return the smallest n: n*base >= x'
1805+
d, m = _divmod(x, self.step)
1806+
if self.closeto(m / self.step, 0):
1807+
return d
1808+
return (d + 1)
1809+
1810+
17691811
class MaxNLocator(Locator):
17701812
"""
17711813
Select no more than N intervals at nice locations.
@@ -1880,6 +1922,12 @@ def set_params(self, **kwargs):
18801922
self._integer = kwargs['integer']
18811923

18821924
def _raw_ticks(self, vmin, vmax):
1925+
"""
1926+
Generate a list of tick locations including the range *vmin* to
1927+
*vmax*. In some applications, one or both of the end locations
1928+
will not be needed, in which case they are trimmed off
1929+
elsewhere.
1930+
"""
18831931
if self._nbins == 'auto':
18841932
if self.axis is not None:
18851933
nbins = np.clip(self.axis.get_tick_space(),
@@ -1892,7 +1940,7 @@ def _raw_ticks(self, vmin, vmax):
18921940
scale, offset = scale_range(vmin, vmax, nbins)
18931941
_vmin = vmin - offset
18941942
_vmax = vmax - offset
1895-
raw_step = (vmax - vmin) / nbins
1943+
raw_step = (_vmax - _vmin) / nbins
18961944
steps = self._extended_steps * scale
18971945
if self._integer:
18981946
# For steps > 1, keep only integer values.
@@ -1911,20 +1959,29 @@ def _raw_ticks(self, vmin, vmax):
19111959
break
19121960

19131961
# This is an upper limit; move to smaller steps if necessary.
1914-
for i in range(istep):
1915-
step = steps[istep - i]
1962+
# for i in range(istep):
1963+
# step = steps[istep - i]
1964+
for istep in range(istep, -1, -1):
1965+
step = steps[istep]
1966+
19161967
if (self._integer and
19171968
np.floor(_vmax) - np.ceil(_vmin) >= self._min_n_ticks - 1):
19181969
step = max(1, step)
19191970
best_vmin = (_vmin // step) * step
19201971

1921-
low = np.round(Base(step).le(_vmin - best_vmin) / step)
1922-
high = np.round(Base(step).ge(_vmax - best_vmin) / step)
1923-
ticks = np.arange(low, high + 1) * step + best_vmin + offset
1924-
nticks = ((ticks <= vmax) & (ticks >= vmin)).sum()
1972+
# Find tick locations spanning the vmin-vmax range, taking into
1973+
# account degradation of precision when there is a large offset.
1974+
# The edge ticks beyond vmin and/or vmax are needed for the
1975+
# "round_numbers" autolimit mode.
1976+
edge = _Edge_integer(step, offset)
1977+
low = edge.le(_vmin - best_vmin)
1978+
high = edge.ge(_vmax - best_vmin)
1979+
ticks = np.arange(low, high + 1) * step + best_vmin
1980+
# Count only the ticks that will be displayed.
1981+
nticks = ((ticks <= _vmax) & (ticks >= _vmin)).sum()
19251982
if nticks >= self._min_n_ticks:
19261983
break
1927-
return ticks
1984+
return ticks + offset
19281985

19291986
def __call__(self):
19301987
vmin, vmax = self.axis.get_view_interval()

0 commit comments

Comments
 (0)
0