Skip to content

Commit

Permalink
Merge pull request #6919 from efiring/integer_locator
Browse files Browse the repository at this point in the history
ENH: Rework MaxNLocator, eliminating infinite loop; closes #6849
  • Loading branch information
tacaswell committed Aug 22, 2016
2 parents a53c4b3 + f4e4c10 commit b2a25b2
Show file tree
Hide file tree
Showing 5 changed files with 349 additions and 313 deletions.
11 changes: 11 additions & 0 deletions lib/matplotlib/tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import warnings


@cleanup(style='classic')
def test_MaxNLocator():
loc = mticker.MaxNLocator(nbins=5)
test_value = np.array([20., 40., 60., 80., 100.])
Expand All @@ -26,6 +27,16 @@ def test_MaxNLocator():
assert_almost_equal(loc.tick_values(-1e15, 1e15), test_value)


@cleanup
def test_MaxNLocator_integer():
loc = mticker.MaxNLocator(nbins=5, integer=True)
test_value = np.array([-1, 0, 1, 2])
assert_almost_equal(loc.tick_values(-0.1, 1.1), test_value)

test_value = np.array([-0.25, 0, 0.25, 0.5, 0.75, 1])
assert_almost_equal(loc.tick_values(-0.1, 0.95), test_value)


def test_LinearLocator():
loc = mticker.LinearLocator(numticks=3)
test_value = np.array([-0.8, -0.3, 0.2])
Expand Down
87 changes: 45 additions & 42 deletions lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1674,7 +1674,9 @@ def __init__(self, *args, **kwargs):
e.g., [1, 2, 4, 5, 10]
*integer*
If True, ticks will take only integer values.
If True, ticks will take only integer values, provided
at least `min_n_ticks` integers are found within the
view limits.
*symmetric*
If True, autoscaling will result in a range symmetric
Expand All @@ -1684,16 +1686,16 @@ def __init__(self, *args, **kwargs):
['lower' | 'upper' | 'both' | None]
Remove edge ticks -- useful for stacked or ganged plots
where the upper tick of one axes overlaps with the lower
tick of the axes above it.
If prune=='lower', the smallest tick will
be removed. If prune=='upper', the largest tick will be
removed. If prune=='both', the largest and smallest ticks
will be removed. If prune==None, no ticks will be removed.
tick of the axes above it, primarily when
`rcParams['axes.autolimit_mode']` is `'round_numbers'`.
If `prune=='lower'`, the smallest tick will
be removed. If `prune=='upper'`, the largest tick will be
removed. If `prune=='both'`, the largest and smallest ticks
will be removed. If `prune==None`, no ticks will be removed.
*min_n_ticks*
While the estimated number of ticks is less than the minimum,
the target value *nbins* is incremented and the ticks are
recalculated.
Relax `nbins` and `integer` constraints if necessary to
obtain this minimum number of ticks.
"""
if args:
Expand All @@ -1714,8 +1716,6 @@ def set_params(self, **kwargs):
warnings.warn(
"The 'trim' keyword has no effect since version 2.0.",
mplDeprecation)
if 'integer' in kwargs:
self._integer = kwargs['integer']
if 'symmetric' in kwargs:
self._symmetric = kwargs['symmetric']
if 'prune' in kwargs:
Expand All @@ -1733,6 +1733,13 @@ def set_params(self, **kwargs):
steps = list(steps)
steps.append(10)
self._steps = steps
# Make an extended staircase within which the needed
# step will be found. This is probably much larger
# than necessary.
flights = (0.1 * np.array(self._steps[:-1]),
self._steps,
[10 * self._steps[1]])
self._extended_steps = np.hstack(flights)
if 'integer' in kwargs:
self._integer = kwargs['integer']
if self._integer:
Expand All @@ -1747,42 +1754,38 @@ def _raw_ticks(self, vmin, vmax):
else:
nbins = self._nbins

while True:
ticks = self._try_raw_ticks(vmin, vmax, nbins)
scale, offset = scale_range(vmin, vmax, nbins)
_vmin = vmin - offset
_vmax = vmax - offset
raw_step = (vmax - vmin) / nbins
steps = self._extended_steps * scale
istep = np.nonzero(steps >= raw_step)[0][0]

# Classic round_numbers mode may require a larger step.
if rcParams['axes.autolimit_mode'] == 'round_numbers':
for istep in range(istep, len(steps)):
step = steps[istep]
best_vmin = (_vmin // step) * step
best_vmax = best_vmin + step * nbins
if (best_vmax >= _vmax):
break

# This is an upper limit; move to smaller steps if necessary.
for i in range(istep):
step = steps[istep - i]
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 = round(Base(step).le(_vmin - best_vmin) / step)
high = round(Base(step).ge(_vmax - best_vmin) / step)
ticks = np.arange(low, high + 1) * step + best_vmin + offset
nticks = ((ticks <= vmax) & (ticks >= vmin)).sum()
if nticks >= self._min_n_ticks:
break
nbins += 1

self._nbins_used = nbins # Maybe useful for troubleshooting.
return ticks

def _try_raw_ticks(self, vmin, vmax, nbins):
scale, offset = scale_range(vmin, vmax, nbins)
if self._integer:
scale = max(1, scale)
vmin = vmin - offset
vmax = vmax - offset
raw_step = (vmax - vmin) / nbins
scaled_raw_step = raw_step / scale
best_vmax = vmax
best_vmin = vmin

steps = (x for x in self._steps if x >= scaled_raw_step)
for step in steps:
step *= scale
best_vmin = vmin // step * step
best_vmax = best_vmin + step * nbins
if best_vmax >= vmax:
break

# More than nbins may be required, e.g. vmin, vmax = -4.1, 4.1 gives
# nbins=9 but 10 bins are actually required after rounding. So we just
# create the bins that span the range we need instead.
low = round(Base(step).le(vmin - best_vmin) / step)
high = round(Base(step).ge(vmax - best_vmin) / step)
return np.arange(low, high + 1) * step + best_vmin + offset

@cbook.deprecated("2.0")
def bin_boundaries(self, vmin, vmax):
return self._raw_ticks(vmin, vmax)
Expand Down
Binary file modified lib/mpl_toolkits/tests/baseline_images/test_mplot3d/lines3d.pdf
Binary file not shown.
Binary file modified lib/mpl_toolkits/tests/baseline_images/test_mplot3d/lines3d.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b2a25b2

Please sign in to comment.