diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 5164567ae1c7..bb7e58abfc6d 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -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 - # 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: @@ -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 diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 8e38192c89f4..778ca3a04a7e 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -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): diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 5354b315340c..09c6d1a9b87b 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -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) @@ -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'): diff --git a/lib/matplotlib/tests/baseline_images/test_axes/specgram_angle_noise.png b/lib/matplotlib/tests/baseline_images/test_axes/specgram_angle_noise.png index 6cef67f8a483..36a3283ea37d 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/specgram_angle_noise.png and b/lib/matplotlib/tests/baseline_images/test_axes/specgram_angle_noise.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_freqs.png b/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_freqs.png index dab0acc3e2aa..2f5cc0060430 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_freqs.png and b/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_freqs.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_freqs_linear.png b/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_freqs_linear.png index dab0acc3e2aa..2f5cc0060430 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_freqs_linear.png and b/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_freqs_linear.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_noise.png b/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_noise.png index 2c52f47c8fc5..057f1a864627 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_noise.png and b/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_noise.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_noise_linear.png b/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_noise_linear.png index 2c52f47c8fc5..057f1a864627 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_noise_linear.png and b/lib/matplotlib/tests/baseline_images/test_axes/specgram_magnitude_noise_linear.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/specgram_noise.png b/lib/matplotlib/tests/baseline_images/test_axes/specgram_noise.png index 9bc8bfa31440..85faf4742fac 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/specgram_noise.png and b/lib/matplotlib/tests/baseline_images/test_axes/specgram_noise.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/specgram_noise_linear.png b/lib/matplotlib/tests/baseline_images/test_axes/specgram_noise_linear.png index 9bc8bfa31440..85faf4742fac 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/specgram_noise_linear.png and b/lib/matplotlib/tests/baseline_images/test_axes/specgram_noise_linear.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/specgram_phase_noise.png b/lib/matplotlib/tests/baseline_images/test_axes/specgram_phase_noise.png index eea4e75f58d0..1ca03e1b1472 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/specgram_phase_noise.png and b/lib/matplotlib/tests/baseline_images/test_axes/specgram_phase_noise.png differ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 6fd0aab28408..1c1b063828d8 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -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] diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index 85eb2bcd81fd..a6e3a0de2b56 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -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)) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 9d3501059799..8300c8438dda 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -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) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index a7487144885e..a28e5a74e7d0 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -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): @@ -1823,19 +1837,21 @@ 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 @@ -1843,17 +1859,23 @@ def __init__(self, *args, **kwargs): 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 @@ -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. @@ -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']: @@ -1993,6 +2018,7 @@ 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 @@ -2000,6 +2026,9 @@ def tick_values(self, vmin, vmax): 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:] @@ -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): diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 1a7d5abae88a..25a9a4684b63 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -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. @@ -2902,6 +2902,9 @@ 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 ------- @@ -2909,7 +2912,8 @@ def interval_contains(interval, val): 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):