Skip to content

Commit 9982e40

Browse files
committed
Merge pull request #6919 from efiring/integer_locator
ENH: Rework MaxNLocator, eliminating infinite loop; closes #6849
1 parent 125a296 commit 9982e40

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
@@ -1564,7 +1564,9 @@ def __init__(self, *args, **kwargs):
15641564
e.g., [1, 2, 4, 5, 10]
15651565
15661566
*integer*
1567-
If True, ticks will take only integer values.
1567+
If True, ticks will take only integer values, provided
1568+
at least `min_n_ticks` integers are found within the
1569+
view limits.
15681570
15691571
*symmetric*
15701572
If True, autoscaling will result in a range symmetric
@@ -1574,16 +1576,16 @@ def __init__(self, *args, **kwargs):
15741576
['lower' | 'upper' | 'both' | None]
15751577
Remove edge ticks -- useful for stacked or ganged plots
15761578
where the upper tick of one axes overlaps with the lower
1577-
tick of the axes above it.
1578-
If prune=='lower', the smallest tick will
1579-
be removed. If prune=='upper', the largest tick will be
1580-
removed. If prune=='both', the largest and smallest ticks
1581-
will be removed. If prune==None, no ticks will be removed.
1579+
tick of the axes above it, primarily when
1580+
`rcParams['axes.autolimit_mode']` is `'round_numbers'`.
1581+
If `prune=='lower'`, the smallest tick will
1582+
be removed. If `prune=='upper'`, the largest tick will be
1583+
removed. If `prune=='both'`, the largest and smallest ticks
1584+
will be removed. If `prune==None`, no ticks will be removed.
15821585
15831586
*min_n_ticks*
1584-
While the estimated number of ticks is less than the minimum,
1585-
the target value *nbins* is incremented and the ticks are
1586-
recalculated.
1587+
Relax `nbins` and `integer` constraints if necessary to
1588+
obtain this minimum number of ticks.
15871589
15881590
"""
15891591
if args:
@@ -1604,8 +1606,6 @@ def set_params(self, **kwargs):
16041606
warnings.warn(
16051607
"The 'trim' keyword has no effect since version 2.0.",
16061608
mplDeprecation)
1607-
if 'integer' in kwargs:
1608-
self._integer = kwargs['integer']
16091609
if 'symmetric' in kwargs:
16101610
self._symmetric = kwargs['symmetric']
16111611
if 'prune' in kwargs:
@@ -1623,6 +1623,13 @@ def set_params(self, **kwargs):
16231623
steps = list(steps)
16241624
steps.append(10)
16251625
self._steps = steps
1626+
# Make an extended staircase within which the needed
1627+
# step will be found. This is probably much larger
1628+
# than necessary.
1629+
flights = (0.1 * np.array(self._steps[:-1]),
1630+
self._steps,
1631+
[10 * self._steps[1]])
1632+
self._extended_steps = np.hstack(flights)
16261633
if 'integer' in kwargs:
16271634
self._integer = kwargs['integer']
16281635
if self._integer:
@@ -1637,42 +1644,38 @@ def _raw_ticks(self, vmin, vmax):
16371644
else:
16381645
nbins = self._nbins
16391646

1640-
while True:
1641-
ticks = self._try_raw_ticks(vmin, vmax, nbins)
1647+
scale, offset = scale_range(vmin, vmax, nbins)
1648+
_vmin = vmin - offset
1649+
_vmax = vmax - offset
1650+
raw_step = (vmax - vmin) / nbins
1651+
steps = self._extended_steps * scale
1652+
istep = np.nonzero(steps >= raw_step)[0][0]
1653+
1654+
# Classic round_numbers mode may require a larger step.
1655+
if rcParams['axes.autolimit_mode'] == 'round_numbers':
1656+
for istep in range(istep, len(steps)):
1657+
step = steps[istep]
1658+
best_vmin = (_vmin // step) * step
1659+
best_vmax = best_vmin + step * nbins
1660+
if (best_vmax >= _vmax):
1661+
break
1662+
1663+
# This is an upper limit; move to smaller steps if necessary.
1664+
for i in range(istep):
1665+
step = steps[istep - i]
1666+
if (self._integer and
1667+
np.floor(_vmax) - np.ceil(_vmin) >= self._min_n_ticks - 1):
1668+
step = max(1, step)
1669+
best_vmin = (_vmin // step) * step
1670+
1671+
low = round(Base(step).le(_vmin - best_vmin) / step)
1672+
high = round(Base(step).ge(_vmax - best_vmin) / step)
1673+
ticks = np.arange(low, high + 1) * step + best_vmin + offset
16421674
nticks = ((ticks <= vmax) & (ticks >= vmin)).sum()
16431675
if nticks >= self._min_n_ticks:
16441676
break
1645-
nbins += 1
1646-
1647-
self._nbins_used = nbins # Maybe useful for troubleshooting.
16481677
return ticks
16491678

1650-
def _try_raw_ticks(self, vmin, vmax, nbins):
1651-
scale, offset = scale_range(vmin, vmax, nbins)
1652-
if self._integer:
1653-
scale = max(1, scale)
1654-
vmin = vmin - offset
1655-
vmax = vmax - offset
1656-
raw_step = (vmax - vmin) / nbins
1657-
scaled_raw_step = raw_step / scale
1658-
best_vmax = vmax
1659-
best_vmin = vmin
1660-
1661-
steps = (x for x in self._steps if x >= scaled_raw_step)
1662-
for step in steps:
1663-
step *= scale
1664-
best_vmin = vmin // step * step
1665-
best_vmax = best_vmin + step * nbins
1666-
if best_vmax >= vmax:
1667-
break
1668-
1669-
# More than nbins may be required, e.g. vmin, vmax = -4.1, 4.1 gives
1670-
# nbins=9 but 10 bins are actually required after rounding. So we just
1671-
# create the bins that span the range we need instead.
1672-
low = round(Base(step).le(vmin - best_vmin) / step)
1673-
high = round(Base(step).ge(vmax - best_vmin) / step)
1674-
return np.arange(low, high + 1) * step + best_vmin + offset
1675-
16761679
@cbook.deprecated("2.0")
16771680
def bin_boundaries(self, vmin, vmax):
16781681
return self._raw_ticks(vmin, vmax)
Binary file not shown.

0 commit comments

Comments
 (0)