Skip to content

Commit 0648b73

Browse files
committed
Rework MaxNLocator, eliminating infinite loop; closes #6849
1 parent f3b6c4e commit 0648b73

File tree

5 files changed

+339
-313
lines changed

5 files changed

+339
-313
lines changed

lib/matplotlib/tests/test_ticker.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import warnings
1515

1616

17+
@cleanup(style='classic')
1718
def test_MaxNLocator():
1819
loc = mticker.MaxNLocator(nbins=5)
1920
test_value = np.array([20., 40., 60., 80., 100.])

lib/matplotlib/ticker.py

Lines changed: 45 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,7 +1603,9 @@ def __init__(self, *args, **kwargs):
16031603
e.g., [1, 2, 4, 5, 10]
16041604
16051605
*integer*
1606-
If True, ticks will take only integer values.
1606+
If True, ticks will take only integer values, provided
1607+
at least `min_n_ticks` integers are found within the
1608+
view limits.
16071609
16081610
*symmetric*
16091611
If True, autoscaling will result in a range symmetric
@@ -1613,16 +1615,16 @@ def __init__(self, *args, **kwargs):
16131615
['lower' | 'upper' | 'both' | None]
16141616
Remove edge ticks -- useful for stacked or ganged plots
16151617
where the upper tick of one axes overlaps with the lower
1616-
tick of the axes above it.
1617-
If prune=='lower', the smallest tick will
1618-
be removed. If prune=='upper', the largest tick will be
1619-
removed. If prune=='both', the largest and smallest ticks
1620-
will be removed. If prune==None, no ticks will be removed.
1618+
tick of the axes above it, primarily when
1619+
`rcParams['axes.autolimit_mode']` is `'round_numbers'`.
1620+
If `prune=='lower'`, the smallest tick will
1621+
be removed. If `prune=='upper'`, the largest tick will be
1622+
removed. If `prune=='both'`, the largest and smallest ticks
1623+
will be removed. If `prune==None`, no ticks will be removed.
16211624
16221625
*min_n_ticks*
1623-
While the estimated number of ticks is less than the minimum,
1624-
the target value *nbins* is incremented and the ticks are
1625-
recalculated.
1626+
Relax `nbins` and `integer` constraints if necessary to
1627+
obtain this minimum number of ticks.
16261628
16271629
"""
16281630
if args:
@@ -1643,8 +1645,6 @@ def set_params(self, **kwargs):
16431645
warnings.warn(
16441646
"The 'trim' keyword has no effect since version 2.0.",
16451647
mplDeprecation)
1646-
if 'integer' in kwargs:
1647-
self._integer = kwargs['integer']
16481648
if 'symmetric' in kwargs:
16491649
self._symmetric = kwargs['symmetric']
16501650
if 'prune' in kwargs:
@@ -1662,6 +1662,13 @@ def set_params(self, **kwargs):
16621662
steps = list(steps)
16631663
steps.append(10)
16641664
self._steps = steps
1665+
# Make an extended staircase within which the needed
1666+
# step will be found. This is probably much larger
1667+
# than necessary.
1668+
flights = (0.1 * np.array(self._steps[:-1]),
1669+
self._steps,
1670+
[10 * self._steps[1]])
1671+
self._extended_steps = np.hstack(flights)
16651672
if 'integer' in kwargs:
16661673
self._integer = kwargs['integer']
16671674
if self._integer:
@@ -1676,42 +1683,38 @@ def _raw_ticks(self, vmin, vmax):
16761683
else:
16771684
nbins = self._nbins
16781685

1679-
while True:
1680-
ticks = self._try_raw_ticks(vmin, vmax, nbins)
1686+
scale, offset = scale_range(vmin, vmax, nbins)
1687+
_vmin = vmin - offset
1688+
_vmax = vmax - offset
1689+
raw_step = (vmax - vmin) / nbins
1690+
steps = self._extended_steps * scale
1691+
istep = np.nonzero(steps >= raw_step)[0][0]
1692+
1693+
# Classic round_numbers mode may require a larger step.
1694+
if rcParams['axes.autolimit_mode'] == 'round_numbers':
1695+
for istep in range(istep, len(steps)):
1696+
step = steps[istep]
1697+
best_vmin = (_vmin // step) * step
1698+
best_vmax = best_vmin + step * nbins
1699+
if (best_vmax >= _vmax):
1700+
break
1701+
1702+
# This is an upper limit; move to smaller steps if necessary.
1703+
for i in range(istep):
1704+
step = steps[istep - i]
1705+
if (self._integer and
1706+
np.floor(_vmax) - np.ceil(_vmin) > self._min_n_ticks - 1):
1707+
step = max(1, step)
1708+
best_vmin = (_vmin // step) * step
1709+
1710+
low = round(Base(step).le(_vmin - best_vmin) / step)
1711+
high = round(Base(step).ge(_vmax - best_vmin) / step)
1712+
ticks = np.arange(low, high + 1) * step + best_vmin + offset
16811713
nticks = ((ticks <= vmax) & (ticks >= vmin)).sum()
16821714
if nticks >= self._min_n_ticks:
16831715
break
1684-
nbins += 1
1685-
1686-
self._nbins_used = nbins # Maybe useful for troubleshooting.
16871716
return ticks
16881717

1689-
def _try_raw_ticks(self, vmin, vmax, nbins):
1690-
scale, offset = scale_range(vmin, vmax, nbins)
1691-
if self._integer:
1692-
scale = max(1, scale)
1693-
vmin = vmin - offset
1694-
vmax = vmax - offset
1695-
raw_step = (vmax - vmin) / nbins
1696-
scaled_raw_step = raw_step / scale
1697-
best_vmax = vmax
1698-
best_vmin = vmin
1699-
1700-
steps = (x for x in self._steps if x >= scaled_raw_step)
1701-
for step in steps:
1702-
step *= scale
1703-
best_vmin = vmin // step * step
1704-
best_vmax = best_vmin + step * nbins
1705-
if best_vmax >= vmax:
1706-
break
1707-
1708-
# More than nbins may be required, e.g. vmin, vmax = -4.1, 4.1 gives
1709-
# nbins=9 but 10 bins are actually required after rounding. So we just
1710-
# create the bins that span the range we need instead.
1711-
low = round(Base(step).le(vmin - best_vmin) / step)
1712-
high = round(Base(step).ge(vmax - best_vmin) / step)
1713-
return np.arange(low, high + 1) * step + best_vmin + offset
1714-
17151718
@cbook.deprecated("2.0")
17161719
def bin_boundaries(self, vmin, vmax):
17171720
return self._raw_ticks(vmin, vmax)
Binary file not shown.

0 commit comments

Comments
 (0)