Skip to content

Make LogLocator respect numticks #21177

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
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
10 changes: 10 additions & 0 deletions doc/api/next_api_changes/behavior/21177-DS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
``LogLocator`` now respects ``numticks``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The algorithm within `~matplotlib.ticker.LogLocator` has been modified to
respect the ``numticks`` argument. Whereas previously more ticks than
``numticks`` could be returned, ``numticks`` is now an upper limit on the
number of ticks returned.

In some cases, this change results in one more tick being added on a
log-scaled axis, and correctly adds some ticks to log-scaled errorbar plots
that were previously missing.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions lib/matplotlib/tests/test_colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ def test_colorbar_autotickslog():
x = np.arange(-3.0, 4.001)
y = np.arange(-4.0, 3.001)
X, Y = np.meshgrid(x, y)
# Z ranges from -12 to 12
Z = X * Y
Z = Z[:-1, :-1]
pcm = ax[0].pcolormesh(X, Y, 10**Z, norm=LogNorm())
Expand All @@ -423,12 +424,10 @@ def test_colorbar_autotickslog():
pcm = ax[1].pcolormesh(X, Y, 10**Z, norm=LogNorm())
cbar2 = fig.colorbar(pcm, ax=ax[1], extend='both',
orientation='vertical', shrink=0.4)
# note only -12 to +12 are visible
np.testing.assert_almost_equal(cbar.ax.yaxis.get_ticklocs(),
10**np.arange(-16., 16.2, 4.))
# note only -24 to +24 are visible
10**np.arange(-12, 12.01, 4))
np.testing.assert_almost_equal(cbar2.ax.yaxis.get_ticklocs(),
10**np.arange(-24., 25., 12.))
10**np.arange(-12, 12.01, 12))


def test_colorbar_get_ticks():
Expand Down Expand Up @@ -515,6 +514,7 @@ def test_colorbar_log_minortick_labels():

def test_colorbar_renorm():
x, y = np.ogrid[-4:4:31j, -4:4:31j]
# z ranges from ~1.52e-9 to 1.2e5
z = 120000*np.exp(-x**2 - y**2)

fig, ax = plt.subplots()
Expand All @@ -529,7 +529,7 @@ def test_colorbar_renorm():
norm = LogNorm(z.min(), z.max())
im.set_norm(norm)
np.testing.assert_allclose(cbar.ax.yaxis.get_majorticklocs(),
np.logspace(-10, 7, 18))
np.logspace(-9, 5, 8))
# note that set_norm removes the FixedLocator...
assert np.isclose(cbar.vmin, z.min())
cbar.set_ticks([1, 2, 3])
Expand Down Expand Up @@ -564,7 +564,7 @@ def test_colorbar_format():
im.set_norm(LogNorm(vmin=0.1, vmax=10))
fig.canvas.draw()
assert (cbar.ax.yaxis.get_ticklabels()[0].get_text() ==
r'$\mathdefault{10^{-2}}$')
r'$\mathdefault{10^{-1}}$')


def test_colorbar_scale_reset():
Expand Down
42 changes: 35 additions & 7 deletions lib/matplotlib/tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,11 @@ def test_basic(self):
with pytest.raises(ValueError):
loc.tick_values(0, 1000)

test_value = np.array([1.00000000e-05, 1.00000000e-03, 1.00000000e-01,
1.00000000e+01, 1.00000000e+03, 1.00000000e+05,
1.00000000e+07, 1.000000000e+09])
assert_almost_equal(loc.tick_values(0.001, 1.1e5), test_value)
test_value = np.array([1e-3, 1e-1, 1e1, 1e3, 1e5])
assert_almost_equal(loc.tick_values(1e-3, 1e5), test_value)

loc = mticker.LogLocator(base=2)
test_value = np.array([0.5, 1., 2., 4., 8., 16., 32., 64., 128., 256.])
test_value = np.array([1, 2, 4, 8, 16, 32, 64, 128])
assert_almost_equal(loc.tick_values(1, 100), test_value)

def test_switch_to_autolocator(self):
Expand Down Expand Up @@ -1373,5 +1371,35 @@ def test_small_range_loglocator(numticks):
ll = mticker.LogLocator()
ll.set_params(numticks=numticks)
for top in [5, 7, 9, 11, 15, 50, 100, 1000]:
ticks = ll.tick_values(.5, top)
assert (np.diff(np.log10(ll.tick_values(6, 150))) == 1).all()
ticks = ll.tick_values(0.5, top)
assert len(ticks) > 1


# Test vmin/vmax both on and off an exact decade
@pytest.mark.parametrize('vmin', [0.1, 0.2])
@pytest.mark.parametrize('vmax', [100, 110])
@pytest.mark.parametrize('numticks', np.arange(2, 11))
def test_loglocator_numticks(numticks, vmin, vmax):
ll = mticker.LogLocator(numticks=numticks)
assert len(ll.tick_values(vmin, vmax)) <= numticks


@pytest.mark.parametrize('base', [2, 5, 10, 3.8])
def test_loglocator_bases(base):
ll = mticker.LogLocator(base=base, numticks=2)
vmin, vmax = base**1, base**2
# Ticks should be exactly on a decade
np.testing.assert_equal(ll.tick_values(vmin, vmax),
[base**1, base**2])
# Ticks should extend to cover whole range of values
np.testing.assert_equal(ll.tick_values(vmin - 1, vmax + 1),
[base**0, base**3])
# Even if the range is less than a decade,
# we should always get two ticks back
#
# Range falls within two decades
np.testing.assert_equal(ll.tick_values(vmin + 1, vmax - 1),
[base**1, base**2])
# Range falls across a decade
np.testing.assert_equal(ll.tick_values(vmin - 1, vmin + 1),
[base**0, base**2])
32 changes: 17 additions & 15 deletions lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2292,6 +2292,14 @@ def __call__(self):
return self.tick_values(vmin, vmax)

def tick_values(self, vmin, vmax):
"""
Return tick locations.

Notes
-----
If vmin/vmax are not exactly on a decade, a single tick lower/higher
than vmin/vmax can be returned.
"""
if self.numticks == 'auto':
if self.axis is not None:
numticks = np.clip(self.axis.get_tick_space(), 2, 9)
Expand Down Expand Up @@ -2322,10 +2330,11 @@ def tick_values(self, vmin, vmax):

if vmax < vmin:
vmin, vmax = vmax, vmin
log_vmin = math.log(vmin) / math.log(b)
log_vmax = math.log(vmax) / math.log(b)
log_vmin = np.log10(vmin) / np.log10(b)
log_vmax = np.log10(vmax) / np.log10(b)

numdec = math.floor(log_vmax) - math.ceil(log_vmin)
# Number of decades fully containing range [vmin, vmax]
numdec = math.ceil(log_vmax) - math.floor(log_vmin)

if isinstance(self._subs, str):
_first = 2.0 if self._subs == 'auto' else 1.0
Expand All @@ -2342,22 +2351,15 @@ def tick_values(self, vmin, vmax):
# Get decades between major ticks.
stride = (max(math.ceil(numdec / (numticks - 1)), 1)
if mpl.rcParams['_internal.classic_mode'] else
(numdec + 1) // numticks + 1)

# if we have decided that the stride is as big or bigger than
# the range, clip the stride back to the available range - 1
# with a floor of 1. This prevents getting axis with only 1 tick
# visible.
if stride >= numdec:
stride = max(1, numdec - 1)
max(numdec // numticks, 1))

# Does subs include anything other than 1? Essentially a hack to know
# whether we're a major or a minor locator.
have_subs = len(subs) > 1 or (len(subs) == 1 and subs[0] != 1.0)

decades = np.arange(math.floor(log_vmin) - stride,
math.ceil(log_vmax) + 2 * stride, stride)

decades = np.arange(math.floor(log_vmin),
math.ceil(log_vmax) + 1,
stride)
if hasattr(self, '_transform'):
ticklocs = self._transform.inverted().transform(decades)
if have_subs:
Expand All @@ -2368,7 +2370,7 @@ def tick_values(self, vmin, vmax):
ticklocs = np.array([])
else:
if have_subs:
if stride == 1:
if stride == 1 and len(decades):
ticklocs = np.concatenate(
[subs * decade_start for decade_start in b ** decades])
else:
Expand Down