Skip to content

API: make MaxNLocator trim out-of-view ticks before returning #12105

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
Closed
56 changes: 11 additions & 45 deletions lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -1051,50 +1051,9 @@ def _update_ticks(self, renderer):
ihigh = locs[-1]
tick_tups = [ti for ti in tick_tups if ilow <= ti[1] <= ihigh]

# so that we don't lose ticks on the end, expand out the interval ever
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interval_contains below is modified to accept floating point slop, so this doesn't need to be here any more.

# so slightly. The "ever so slightly" is defined to be the width of a
# half of a pixel. We don't want to draw a tick that even one pixel
# outside of the defined axis interval.
if interval[0] <= interval[1]:
interval_expanded = interval
else:
interval_expanded = interval[1], interval[0]

if hasattr(self, '_get_pixel_distance_along_axis'):
# normally, one does not want to catch all exceptions that
# could possibly happen, but it is not clear exactly what
# exceptions might arise from a user's projection (their
# rendition of the Axis object). So, we catch all, with
# the idea that one would rather potentially lose a tick
# from one side of the axis or another, rather than see a
# stack trace.
# We also catch users warnings here. These are the result of
# invalid numpy calculations that may be the result of out of
# bounds on axis with finite allowed intervals such as geo
# projections i.e. Mollweide.
with np.errstate(invalid='ignore'):
try:
ds1 = self._get_pixel_distance_along_axis(
interval_expanded[0], -0.5)
except Exception:
warnings.warn("Unable to find pixel distance along axis "
"for interval padding of ticks; assuming no "
"interval padding needed.")
ds1 = 0.0
if np.isnan(ds1):
ds1 = 0.0
try:
ds2 = self._get_pixel_distance_along_axis(
interval_expanded[1], +0.5)
except Exception:
warnings.warn("Unable to find pixel distance along axis "
"for interval padding of ticks; assuming no "
"interval padding needed.")
ds2 = 0.0
if np.isnan(ds2):
ds2 = 0.0
interval_expanded = (interval_expanded[0] - ds1,
interval_expanded[1] + ds2)
if interval[1] <= interval[0]:
interval = interval[1], interval[0]
inter = self.get_transform().transform(interval)

ticks_to_draw = []
for tick, loc, label in tick_tups:
Expand All @@ -1104,8 +1063,15 @@ def _update_ticks(self, renderer):
tick.update_position(loc)
tick.set_label1(label)
tick.set_label2(label)
if not mtransforms.interval_contains(interval_expanded, loc):
try:
loct = self.get_transform().transform(loc)
except AssertionError:
loct = None
continue
if ((loct is None) or
(not mtransforms.interval_contains(inter, loct))):
continue

ticks_to_draw.append(tick)

return ticks_to_draw
Expand Down
5 changes: 3 additions & 2 deletions lib/matplotlib/colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,14 @@ def __init__(self, colorbar):
self._colorbar = colorbar
nbins = 'auto'
steps = [1, 2, 2.5, 5, 10]
ticker.MaxNLocator.__init__(self, nbins=nbins, steps=steps)
ticker.MaxNLocator.__init__(self, nbins=nbins, steps=steps,
trim_outside=True)

def tick_values(self, vmin, vmax):
vmin = max(vmin, self._colorbar.norm.vmin)
vmax = min(vmax, self._colorbar.norm.vmax)
ticks = ticker.MaxNLocator.tick_values(self, vmin, vmax)
return ticks[(ticks >= vmin) & (ticks <= vmax)]
return ticks


class _ColorbarAutoMinorLocator(ticker.AutoMinorLocator):
Expand Down
20 changes: 16 additions & 4 deletions lib/matplotlib/contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,8 @@ def _autolev(self, N):
if self.logscale:
self.locator = ticker.LogLocator()
else:
self.locator = ticker.MaxNLocator(N + 1, min_n_ticks=1)
self.locator = ticker.MaxNLocator(N + 1, min_n_ticks=1,
trim_outside=False)

lev = self.locator.tick_values(self.zmin, self.zmax)

Expand All @@ -1200,10 +1201,21 @@ def _autolev(self, N):
pass

# Trim excess levels the locator may have supplied.
under = np.nonzero(lev < self.zmin)[0]

levt = lev
zmin = self.zmin
zmax = self.zmax
# tolerances need to be in log space if we are a logscale....
if self.logscale:
levt = np.log10(levt)
zmin = np.log10(zmin)
zmax = np.log10(zmax)
rtol = (zmin - zmax) * 1e-10
under = np.nonzero(levt < zmin - rtol)[0]
i0 = under[-1] if len(under) else 0
over = np.nonzero(lev > self.zmax)[0]
i1 = over[0] + 1 if len(over) else len(lev)
over = np.nonzero(levt > zmax + rtol)[0]
i1 = over[0] + 1 if len(over) else len(levt)
# put back extra levels if we want to extend...
if self.extend in ('min', 'both'):
i0 += 1
if self.extend in ('max', 'both'):
Expand Down
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.
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.
Binary file modified lib/matplotlib/tests/baseline_images/test_axes/specgram_noise.png
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.
4 changes: 2 additions & 2 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5196,14 +5196,14 @@ def _helper_x(ax):
ax2.remove()
ax.set_xlim(0, 15)
r = ax.xaxis.get_major_locator()()
assert r[-1] > 14
assert np.allclose(r[-1], 14)

def _helper_y(ax):
ax2 = ax.twiny()
ax2.remove()
ax.set_ylim(0, 15)
r = ax.yaxis.get_major_locator()()
assert r[-1] > 14
assert np.allclose(r[-1], 14)

return {"x": _helper_x, "y": _helper_y}[request.param]

Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/tests/test_contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def test_contourf_decreasing_levels():
def test_contourf_symmetric_locator():
# github issue 7271
z = np.arange(12).reshape((3, 4))
locator = plt.MaxNLocator(nbins=4, symmetric=True)
locator = plt.MaxNLocator(nbins=4, symmetric=True, trim_outside=False)
cs = plt.contourf(z, locator=locator)
assert_array_almost_equal(cs.levels, np.linspace(-12, 12, 5))

Expand Down
5 changes: 3 additions & 2 deletions lib/matplotlib/tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ class TestMaxNLocator(object):

@pytest.mark.parametrize('vmin, vmax, expected', basic_data)
def test_basic(self, vmin, vmax, expected):
loc = mticker.MaxNLocator(nbins=5)
loc = mticker.MaxNLocator(nbins=5, trim_outside=False)
assert_almost_equal(loc.tick_values(vmin, vmax), expected)

@pytest.mark.parametrize('vmin, vmax, steps, expected', integer_data)
def test_integer(self, vmin, vmax, steps, expected):
loc = mticker.MaxNLocator(nbins=5, integer=True, steps=steps)
loc = mticker.MaxNLocator(nbins=5, integer=True, steps=steps,
trim_outside=False)
assert_almost_equal(loc.tick_values(vmin, vmax), expected)


Expand Down
47 changes: 38 additions & 9 deletions lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,20 @@
'SymmetricalLogLocator', 'LogitLocator')


def _keep_in_vlim(locs, vmin, vmax, rtol=1e-10):
"""
trim array locs to be between vmin and vmax within
tolerance.
"""
if vmin > vmax:
vmax, vmin = vmin, vmax

rtol = (vmax - vmin) * rtol
locs = locs[locs >= vmin - rtol]
locs = locs[locs <= vmax + rtol]
return locs


# Work around numpy/numpy#6127.
def _divmod(x, y):
if isinstance(x, np.generic):
Expand Down Expand Up @@ -1823,37 +1837,45 @@ class MaxNLocator(Locator):
steps=None,
integer=False,
symmetric=False,
trim_outside=True,
prune=None,
min_n_ticks=2)

def __init__(self, *args, **kwargs):
"""
Keyword args:
Parameters
----------

*nbins*
nbins : integer
Maximum number of intervals; one less than max number of
ticks. If the string `'auto'`, the number of bins will be
automatically determined based on the length of the axis.

*steps*
steps : integer
Sequence of nice numbers starting with 1 and ending with 10;
e.g., [1, 2, 4, 5, 10], where the values are acceptable
tick multiples. i.e. for the example, 20, 40, 60 would be
an acceptable set of ticks, as would 0.4, 0.6, 0.8, because
they are multiples of 2. However, 30, 60, 90 would not
be allowed because 3 does not appear in the list of steps.

*integer*
integer : bool
If True, ticks will take only integer values, provided
at least `min_n_ticks` integers are found within the
view limits.

*symmetric*
symmetric : bool
If True, autoscaling will result in a range symmetric
about zero.

*prune*
['lower' | 'upper' | 'both' | None]
trim_outside: bool
By default (``False``) calling ``MaxNLocator`` will return one
tick thats less than vmin, and one tick thats greater than vmax.
This flag suppresses that behaviour. Note its different than
``prune`` (below), which prunes the lower or upper tick, regardless
of whether it is in the view limits.

prune : ['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, primarily when :rc:`axes.autolimit_mode` is
Expand All @@ -1862,7 +1884,7 @@ def __init__(self, *args, **kwargs):
removed. If ``prune == 'both'``, the largest and smallest ticks
will be removed. If ``prune == None``, no ticks will be removed.

*min_n_ticks*
min_n_ticks : integer
Relax `nbins` and `integer` constraints if necessary to
obtain this minimum number of ticks.

Expand Down Expand Up @@ -1910,6 +1932,9 @@ def set_params(self, **kwargs):
self._nbins = int(self._nbins)
if 'symmetric' in kwargs:
self._symmetric = kwargs['symmetric']

self._trim_outside = kwargs.pop('trim_outside', True)

if 'prune' in kwargs:
prune = kwargs['prune']
if prune is not None and prune not in ['upper', 'lower', 'both']:
Expand Down Expand Up @@ -1993,13 +2018,17 @@ def __call__(self):
return self.tick_values(vmin, vmax)

def tick_values(self, vmin, vmax):

if self._symmetric:
vmax = max(abs(vmin), abs(vmax))
vmin = -vmax
vmin, vmax = mtransforms.nonsingular(
vmin, vmax, expander=1e-13, tiny=1e-14)
locs = self._raw_ticks(vmin, vmax)

if self._trim_outside:
locs = _keep_in_vlim(locs, vmin, vmax)

prune = self._prune
if prune == 'lower':
locs = locs[1:]
Expand Down Expand Up @@ -2543,7 +2572,7 @@ def __init__(self):
else:
nbins = 'auto'
steps = [1, 2, 2.5, 5, 10]
MaxNLocator.__init__(self, nbins=nbins, steps=steps)
MaxNLocator.__init__(self, nbins=nbins, steps=steps, trim_outside=True)


class AutoMinorLocator(Locator):
Expand Down
8 changes: 6 additions & 2 deletions lib/matplotlib/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2892,7 +2892,7 @@ def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True):
return vmin, vmax


def interval_contains(interval, val):
def interval_contains(interval, val, rtol=1e-10):
"""
Check, inclusively, whether an interval includes a given value.

Expand All @@ -2902,14 +2902,18 @@ def interval_contains(interval, val):
A 2-length sequence, endpoints that define the interval.
val : scalar
Value to check is within interval.
rtol : float
Floating point tolerance relatice to b-a. So tol = (b - a) * rtol, and
return True if a - tol <= val <= b + tol (for b>a).

Returns
-------
bool
Returns true if given val is within the interval.
"""
a, b = interval
return a <= val <= b or a >= val >= b
rtol = np.abs(b - a) * rtol
return a - rtol <= val <= b + rtol or a + rtol >= val >= b - rtol


def interval_contains_open(interval, val):
Expand Down