Skip to content
Merged
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
8 changes: 4 additions & 4 deletions lib/matplotlib/dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -1384,7 +1384,7 @@ def __init__(self, base=1, month=1, day=1, tz=None):
(default jan 1).
"""
DateLocator.__init__(self, tz)
self.base = ticker.Base(base)
self.base = ticker._Edge_integer(base, 0)
self.replaced = {'month': month,
'day': day,
'hour': 0,
Expand All @@ -1403,15 +1403,15 @@ def __call__(self):
return self.tick_values(dmin, dmax)

def tick_values(self, vmin, vmax):
ymin = self.base.le(vmin.year)
ymax = self.base.ge(vmax.year)
ymin = self.base.le(vmin.year) * self.base.step
ymax = self.base.ge(vmax.year) * self.base.step

ticks = [vmin.replace(year=ymin, **self.replaced)]
while True:
dt = ticks[-1]
if dt.year >= ymax:
return date2num(ticks)
year = dt.year + self.base.get_base()
year = dt.year + self.base.step
ticks.append(dt.replace(year=year, **self.replaced))

def autoscale(self):
Expand Down
4 changes: 3 additions & 1 deletion lib/matplotlib/tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class TestMaxNLocator(object):
(20, 100, np.array([20., 40., 60., 80., 100.])),
(0.001, 0.0001, np.array([0., 0.0002, 0.0004, 0.0006, 0.0008, 0.001])),
(-1e15, 1e15, np.array([-1.0e+15, -5.0e+14, 0e+00, 5e+14, 1.0e+15])),
(0, 0.85e-50, np.arange(6) * 2e-51),
(-0.85e-50, 0, np.arange(-5, 1) * 2e-51),
]

integer_data = [
Expand Down Expand Up @@ -64,7 +66,7 @@ def test_set_params(self):
"""
mult = mticker.MultipleLocator(base=0.7)
mult.set_params(base=1.7)
assert mult._base == 1.7
assert mult._edge.step == 1.7


class TestAutoMinorLocator(object):
Expand Down
100 changes: 77 additions & 23 deletions lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1661,13 +1661,12 @@ def view_limits(self, vmin, vmax):
return mtransforms.nonsingular(vmin, vmax)


@cbook.deprecated("3.0")
def closeto(x, y):
if abs(x - y) < 1e-10:
return True
else:
return False
return abs(x - y) < 1e-10


@cbook.deprecated("3.0")
class Base(object):
'this solution has some hacks to deal with floating point inaccuracies'
def __init__(self, base):
Expand Down Expand Up @@ -1711,17 +1710,16 @@ def get_base(self):

class MultipleLocator(Locator):
"""
Set a tick on every integer that is multiple of base in the
view interval
Set a tick on each integer multiple of a base within the view interval.
"""

def __init__(self, base=1.0):
self._base = Base(base)
self._edge = _Edge_integer(base, 0)

def set_params(self, base):
"""Set parameters within this locator."""
if base is not None:
self._base = base
self._edge = _Edge_integer(base, 0)

def __call__(self):
'Return the locations of the ticks'
Expand All @@ -1731,20 +1729,20 @@ def __call__(self):
def tick_values(self, vmin, vmax):
if vmax < vmin:
vmin, vmax = vmax, vmin
vmin = self._base.ge(vmin)
base = self._base.get_base()
n = (vmax - vmin + 0.001 * base) // base
locs = vmin - base + np.arange(n + 3) * base
step = self._edge.step
vmin = self._edge.ge(vmin) * step
n = (vmax - vmin + 0.001 * step) // step
locs = vmin - step + np.arange(n + 3) * step
return self.raise_if_exceeds(locs)

def view_limits(self, dmin, dmax):
"""
Set the view limits to the nearest multiples of base that
contain the data
contain the data.
"""
if rcParams['axes.autolimit_mode'] == 'round_numbers':
vmin = self._base.le(dmin)
vmax = self._base.ge(dmax)
vmin = self._edge.le(dmin) * self._edge.step
vmax = self._base.ge(dmax) * self._edge.step
if vmin == vmax:
vmin -= 1
vmax += 1
Expand All @@ -1766,6 +1764,49 @@ def scale_range(vmin, vmax, n=1, threshold=100):
return scale, offset


class _Edge_integer:
"""
Helper for MaxNLocator, MultipleLocator, etc.

Take floating point precision limitations into account when calculating
tick locations as integer multiples of a step.
"""
def __init__(self, step, offset):
"""
*step* is a positive floating-point interval between ticks.
*offset* is the offset subtracted from the data limits
prior to calculating tick locations.
"""
if step <= 0:
raise ValueError("'step' must be positive")
self.step = step
self._offset = abs(offset)

def closeto(self, ms, edge):
# Allow more slop when the offset is large compared to the step.
if self._offset > 0:
digits = np.log10(self._offset / self.step)
tol = max(1e-10, 10 ** (digits - 12))
tol = min(0.4999, tol)
else:
tol = 1e-10
return abs(ms - edge) < tol

def le(self, x):
'Return the largest n: n*step <= x.'
d, m = _divmod(x, self.step)
if self.closeto(m / self.step, 1):
return (d + 1)
return d

def ge(self, x):
'Return the smallest n: n*step >= x.'
d, m = _divmod(x, self.step)
if self.closeto(m / self.step, 0):
return d
return (d + 1)


class MaxNLocator(Locator):
"""
Select no more than N intervals at nice locations.
Expand Down Expand Up @@ -1880,6 +1921,12 @@ def set_params(self, **kwargs):
self._integer = kwargs['integer']

def _raw_ticks(self, vmin, vmax):
"""
Generate a list of tick locations including the range *vmin* to
*vmax*. In some applications, one or both of the end locations
will not be needed, in which case they are trimmed off
elsewhere.
"""
if self._nbins == 'auto':
if self.axis is not None:
nbins = np.clip(self.axis.get_tick_space(),
Expand All @@ -1892,7 +1939,7 @@ def _raw_ticks(self, vmin, vmax):
scale, offset = scale_range(vmin, vmax, nbins)
_vmin = vmin - offset
_vmax = vmax - offset
raw_step = (vmax - vmin) / nbins
raw_step = (_vmax - _vmin) / nbins
steps = self._extended_steps * scale
if self._integer:
# For steps > 1, keep only integer values.
Expand All @@ -1911,20 +1958,27 @@ def _raw_ticks(self, vmin, vmax):
break

# This is an upper limit; move to smaller steps if necessary.
for i in range(istep):
step = steps[istep - i]
for istep in reversed(range(istep + 1)):
step = steps[istep]

if (self._integer and
np.floor(_vmax) - np.ceil(_vmin) >= self._min_n_ticks - 1):
step = max(1, step)
best_vmin = (_vmin // step) * step

low = np.round(Base(step).le(_vmin - best_vmin) / step)
high = np.round(Base(step).ge(_vmax - best_vmin) / step)
ticks = np.arange(low, high + 1) * step + best_vmin + offset
nticks = ((ticks <= vmax) & (ticks >= vmin)).sum()
# Find tick locations spanning the vmin-vmax range, taking into
# account degradation of precision when there is a large offset.
# The edge ticks beyond vmin and/or vmax are needed for the
# "round_numbers" autolimit mode.
edge = _Edge_integer(step, offset)
low = edge.le(_vmin - best_vmin)
high = edge.ge(_vmax - best_vmin)
ticks = np.arange(low, high + 1) * step + best_vmin
# Count only the ticks that will be displayed.
nticks = ((ticks <= _vmax) & (ticks >= _vmin)).sum()
if nticks >= self._min_n_ticks:
break
return ticks
return ticks + offset

def __call__(self):
vmin, vmax = self.axis.get_view_interval()
Expand Down