Skip to content

Commit b2a25b2

Browse files
authored
Merge pull request #6919 from efiring/integer_locator
ENH: Rework MaxNLocator, eliminating infinite loop; closes #6849
2 parents a53c4b3 + f4e4c10 commit b2a25b2

File tree

5 files changed

+349
-313
lines changed

5 files changed

+349
-313
lines changed

lib/matplotlib/tests/test_ticker.py

Lines changed: 11 additions & 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.])
@@ -26,6 +27,16 @@ def test_MaxNLocator():
2627
assert_almost_equal(loc.tick_values(-1e15, 1e15), test_value)
2728

2829

30+
@cleanup
31+
def test_MaxNLocator_integer():
32+
loc = mticker.MaxNLocator(nbins=5, integer=True)
33+
test_value = np.array([-1, 0, 1, 2])
34+
assert_almost_equal(loc.tick_values(-0.1, 1.1), test_value)
35+
36+
test_value = np.array([-0.25, 0, 0.25, 0.5, 0.75, 1])
37+
assert_almost_equal(loc.tick_values(-0.1, 0.95), test_value)
38+
39+
2940
def test_LinearLocator():
3041
loc = mticker.LinearLocator(numticks=3)
3142
test_value = np.array([-0.8, -0.3, 0.2])

lib/matplotlib/ticker.py

Lines changed: 45 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,7 +1674,9 @@ def __init__(self, *args, **kwargs):
16741674
e.g., [1, 2, 4, 5, 10]
16751675
16761676
*integer*
1677-
If True, ticks will take only integer values.
1677+
If True, ticks will take only integer values, provided
1678+
at least `min_n_ticks` integers are found within the
1679+
view limits.
16781680
16791681
*symmetric*
16801682
If True, autoscaling will result in a range symmetric
@@ -1684,16 +1686,16 @@ def __init__(self, *args, **kwargs):
16841686
['lower' | 'upper' | 'both' | None]
16851687
Remove edge ticks -- useful for stacked or ganged plots
16861688
where the upper tick of one axes overlaps with the lower
1687-
tick of the axes above it.
1688-
If prune=='lower', the smallest tick will
1689-
be removed. If prune=='upper', the largest tick will be
1690-
removed. If prune=='both', the largest and smallest ticks
1691-
will be removed. If prune==None, no ticks will be removed.
1689+
tick of the axes above it, primarily when
1690+
`rcParams['axes.autolimit_mode']` is `'round_numbers'`.
1691+
If `prune=='lower'`, the smallest tick will
1692+
be removed. If `prune=='upper'`, the largest tick will be
1693+
removed. If `prune=='both'`, the largest and smallest ticks
1694+
will be removed. If `prune==None`, no ticks will be removed.
16921695
16931696
*min_n_ticks*
1694-
While the estimated number of ticks is less than the minimum,
1695-
the target value *nbins* is incremented and the ticks are
1696-
recalculated.
1697+
Relax `nbins` and `integer` constraints if necessary to
1698+
obtain this minimum number of ticks.
16971699
16981700
"""
16991701
if args:
@@ -1714,8 +1716,6 @@ def set_params(self, **kwargs):
17141716
warnings.warn(
17151717
"The 'trim' keyword has no effect since version 2.0.",
17161718
mplDeprecation)
1717-
if 'integer' in kwargs:
1718-
self._integer = kwargs['integer']
17191719
if 'symmetric' in kwargs:
17201720
self._symmetric = kwargs['symmetric']
17211721
if 'prune' in kwargs:
@@ -1733,6 +1733,13 @@ def set_params(self, **kwargs):
17331733
steps = list(steps)
17341734
steps.append(10)
17351735
self._steps = steps
1736+
# Make an extended staircase within which the needed
1737+
# step will be found. This is probably much larger
1738+
# than necessary.
1739+
flights = (0.1 * np.array(self._steps[:-1]),
1740+
self._steps,
1741+
[10 * self._steps[1]])
1742+
self._extended_steps = np.hstack(flights)
17361743
if 'integer' in kwargs:
17371744
self._integer = kwargs['integer']
17381745
if self._integer:
@@ -1747,42 +1754,38 @@ def _raw_ticks(self, vmin, vmax):
17471754
else:
17481755
nbins = self._nbins
17491756

1750-
while True:
1751-
ticks = self._try_raw_ticks(vmin, vmax, nbins)
1757+
scale, offset = scale_range(vmin, vmax, nbins)
1758+
_vmin = vmin - offset
1759+
_vmax = vmax - offset
1760+
raw_step = (vmax - vmin) / nbins
1761+
steps = self._extended_steps * scale
1762+
istep = np.nonzero(steps >= raw_step)[0][0]
1763+
1764+
# Classic round_numbers mode may require a larger step.
1765+
if rcParams['axes.autolimit_mode'] == 'round_numbers':
1766+
for istep in range(istep, len(steps)):
1767+
step = steps[istep]
1768+
best_vmin = (_vmin // step) * step
1769+
best_vmax = best_vmin + step * nbins
1770+
if (best_vmax >= _vmax):
1771+
break
1772+
1773+
# This is an upper limit; move to smaller steps if necessary.
1774+
for i in range(istep):
1775+
step = steps[istep - i]
1776+
if (self._integer and
1777+
np.floor(_vmax) - np.ceil(_vmin) >= self._min_n_ticks - 1):
1778+
step = max(1, step)
1779+
best_vmin = (_vmin // step) * step
1780+
1781+
low = round(Base(step).le(_vmin - best_vmin) / step)
1782+
high = round(Base(step).ge(_vmax - best_vmin) / step)
1783+
ticks = np.arange(low, high + 1) * step + best_vmin + offset
17521784
nticks = ((ticks <= vmax) & (ticks >= vmin)).sum()
17531785
if nticks >= self._min_n_ticks:
17541786
break
1755-
nbins += 1
1756-
1757-
self._nbins_used = nbins # Maybe useful for troubleshooting.
17581787
return ticks
17591788

1760-
def _try_raw_ticks(self, vmin, vmax, nbins):
1761-
scale, offset = scale_range(vmin, vmax, nbins)
1762-
if self._integer:
1763-
scale = max(1, scale)
1764-
vmin = vmin - offset
1765-
vmax = vmax - offset
1766-
raw_step = (vmax - vmin) / nbins
1767-
scaled_raw_step = raw_step / scale
1768-
best_vmax = vmax
1769-
best_vmin = vmin
1770-
1771-
steps = (x for x in self._steps if x >= scaled_raw_step)
1772-
for step in steps:
1773-
step *= scale
1774-
best_vmin = vmin // step * step
1775-
best_vmax = best_vmin + step * nbins
1776-
if best_vmax >= vmax:
1777-
break
1778-
1779-
# More than nbins may be required, e.g. vmin, vmax = -4.1, 4.1 gives
1780-
# nbins=9 but 10 bins are actually required after rounding. So we just
1781-
# create the bins that span the range we need instead.
1782-
low = round(Base(step).le(vmin - best_vmin) / step)
1783-
high = round(Base(step).ge(vmax - best_vmin) / step)
1784-
return np.arange(low, high + 1) * step + best_vmin + offset
1785-
17861789
@cbook.deprecated("2.0")
17871790
def bin_boundaries(self, vmin, vmax):
17881791
return self._raw_ticks(vmin, vmax)
Binary file not shown.

0 commit comments

Comments
 (0)