diff --git a/doc/api/api_changes.rst b/doc/api/api_changes.rst index fdc4c20547b1..7ec22368c975 100644 --- a/doc/api/api_changes.rst +++ b/doc/api/api_changes.rst @@ -141,8 +141,8 @@ the kwarg is None which internally sets it to the 'auto' string, triggering a new algorithm for adjusting the maximum according to the axis length relative to the ticklabel font size. -`matplotlib.ticker.LogFormatter` gains minor_thresholds kwarg -------------------------------------------------------------- +`matplotlib.ticker.LogFormatter`: two new kwargs +------------------------------------------------ Previously, minor ticks on log-scaled axes were not labeled by default. An algorithm has been added to the @@ -151,6 +151,9 @@ ticks between integer powers of the base. The algorithm uses two parameters supplied in a kwarg tuple named 'minor_thresholds'. See the docstring for further explanation. +To improve support for axes using `~matplotlib.ticker.SymmetricLogLocator`, +a 'linthresh' kwarg was added. + New defaults for 3D quiver function in mpl_toolkits.mplot3d.axes3d.py --------------------------------------------------------------------- diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index bad1661e39fe..ca0292e96377 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -264,6 +264,10 @@ def limit_range_for_scale(self, vmin, vmax, minpos): """ Limit the domain to positive values. """ + if not np.isfinite(minpos): + minpos = 1e-300 # This value should rarely if ever + # end up with a visible effect. + return (minpos if vmin <= 0 else vmin, minpos if vmax <= 0 else vmax) @@ -499,7 +503,10 @@ def limit_range_for_scale(self, vmin, vmax, minpos): """ Limit the domain to values between 0 and 1 (excluded). """ - return (minpos if vmin <= 0 else minpos, + if not np.isfinite(minpos): + minpos = 1e-7 # This value should rarely if ever + # end up with a visible effect. + return (minpos if vmin <= 0 else vmin, 1 - minpos if vmax >= 1 else vmax) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 07bb89adfc46..8203c0ad3a03 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -176,6 +176,25 @@ def test_autoscale_tight(): assert_allclose(ax.get_xlim(), (-0.15, 3.15)) assert_allclose(ax.get_ylim(), (1.0, 4.0)) + +@cleanup(style='default') +def test_autoscale_log_shared(): + # related to github #7587 + # array starts at zero to trigger _minpos handling + x = np.arange(100, dtype=float) + fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True) + ax1.loglog(x, x) + ax2.semilogx(x, x) + ax1.autoscale(tight=True) + ax2.autoscale(tight=True) + plt.draw() + lims = (x[1], x[-1]) + assert_allclose(ax1.get_xlim(), lims) + assert_allclose(ax1.get_ylim(), lims) + assert_allclose(ax2.get_xlim(), lims) + assert_allclose(ax2.get_ylim(), (x[0], x[-1])) + + @cleanup(style='default') def test_use_sticky_edges(): fig, ax = plt.subplots() diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index cd40422e2f97..2b7f19c20ab0 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -902,10 +902,6 @@ def set_locs(self, locs=None): self._sublabels = None return - b = self._base - - vmin, vmax = self.axis.get_view_interval() - # Handle symlog case: linthresh = self._linthresh if linthresh is None: @@ -914,6 +910,18 @@ def set_locs(self, locs=None): except AttributeError: pass + vmin, vmax = self.axis.get_view_interval() + if vmin > vmax: + vmin, vmax = vmax, vmin + + if linthresh is None and vmin <= 0: + # It's probably a colorbar with + # a format kwarg setting a LogFormatter in the manner + # that worked with 1.5.x, but that doesn't work now. + self._sublabels = set((1,)) # label powers of base + return + + b = self._base if linthresh is not None: # symlog # Only compute the number of decades in the logarithmic part of the # axis @@ -943,37 +951,38 @@ def set_locs(self, locs=None): # Label all integer multiples of base**n. self._sublabels = set(np.arange(1, b + 1)) + def _num_to_string(self, x, vmin, vmax): + if x > 10000: + s = '%1.0e' % x + elif x < 1: + s = '%1.0e' % x + else: + s = self.pprint_val(x, vmax - vmin) + def __call__(self, x, pos=None): """ Return the format for tick val `x`. """ - vmin, vmax = self.axis.get_view_interval() - vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) - d = abs(vmax - vmin) - b = self._base - if x == 0.0: + if x == 0.0: # Symlog return '0' + sign = np.sign(x) x = abs(x) + b = self._base # only label the decades fx = math.log(x) / math.log(b) is_x_decade = is_close_to_int(fx) exponent = np.round(fx) if is_x_decade else np.floor(fx) coeff = np.round(x / b ** exponent) + if self.labelOnlyBase and not is_x_decade: return '' if self._sublabels is not None and coeff not in self._sublabels: return '' - if x > 10000: - s = '%1.0e' % x - elif x < 1: - s = '%1.0e' % x - else: - s = self.pprint_val(x, d) - if sign == -1: - s = '-%s' % s - + vmin, vmax = self.axis.get_view_interval() + vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) + s = self._num_to_string(x, vmin, vmax) return self.fix_minus(s) def format_data(self, value): @@ -1026,41 +1035,16 @@ class LogFormatterExponent(LogFormatter): """ Format values for log axis using ``exponent = log_base(value)``. """ - def __call__(self, x, pos=None): - """ - Return the format for tick value `x`. - """ - vmin, vmax = self.axis.get_view_interval() - vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) - d = abs(vmax - vmin) - b = self._base - if x == 0: - return '0' - sign = np.sign(x) - x = abs(x) - # only label the decades - fx = math.log(x) / math.log(b) - - is_x_decade = is_close_to_int(fx) - exponent = np.round(fx) if is_x_decade else np.floor(fx) - coeff = np.round(x / b ** exponent) - - if self.labelOnlyBase and not is_x_decade: - return '' - if self._sublabels is not None and coeff not in self._sublabels: - return '' - + def _num_to_string(self, x, vmin, vmax): + fx = math.log(x) / math.log(self._base) if abs(fx) > 10000: s = '%1.0g' % fx elif abs(fx) < 1: s = '%1.0g' % fx else: - fd = math.log(abs(d)) / math.log(b) + fd = math.log(vmax - vmin) / math.log(self._base) s = self.pprint_val(fx, fd) - if sign == -1: - s = '-%s' % s - - return self.fix_minus(s) + return s class LogFormatterMathtext(LogFormatter): @@ -1082,11 +1066,8 @@ def __call__(self, x, pos=None): The position `pos` is ignored. """ - b = self._base usetex = rcParams['text.usetex'] - - # only label the decades - if x == 0: + if x == 0: # Symlog if usetex: return '$0$' else: @@ -1094,23 +1075,25 @@ def __call__(self, x, pos=None): sign_string = '-' if x < 0 else '' x = abs(x) + b = self._base + # only label the decades fx = math.log(x) / math.log(b) is_x_decade = is_close_to_int(fx) exponent = np.round(fx) if is_x_decade else np.floor(fx) coeff = np.round(x / b ** exponent) + if self.labelOnlyBase and not is_x_decade: + return '' + if self._sublabels is not None and coeff not in self._sublabels: + return '' + # use string formatting of the base if it is not an integer if b % 1 == 0.0: base = '%d' % b else: base = '%s' % b - if self.labelOnlyBase and not is_x_decade: - return '' - if self._sublabels is not None and coeff not in self._sublabels: - return '' - if not is_x_decade: return self._non_decade_format(sign_string, base, fx, usetex) else: @@ -2032,23 +2015,11 @@ def view_limits(self, vmin, vmax): 'Try to choose the view limits intelligently' b = self._base - if vmax < vmin: - vmin, vmax = vmax, vmin + vmin, vmax = self.nonsingular(vmin, vmax) if self.axis.axes.name == 'polar': vmax = math.ceil(math.log(vmax) / math.log(b)) vmin = b ** (vmax - self.numdecs) - return vmin, vmax - - minpos = self.axis.get_minpos() - - if minpos <= 0 or not np.isfinite(minpos): - raise ValueError( - "Data has no positive values, and therefore can not be " - "log-scaled.") - - if vmin <= 0: - vmin = minpos if rcParams['axes.autolimit_mode'] == 'round_numbers': if not is_decade(vmin, self._base): @@ -2056,12 +2027,29 @@ def view_limits(self, vmin, vmax): if not is_decade(vmax, self._base): vmax = decade_up(vmax, self._base) - if vmin == vmax: - vmin = decade_down(vmin, self._base) - vmax = decade_up(vmax, self._base) + return vmin, vmax - result = mtransforms.nonsingular(vmin, vmax) - return result + def nonsingular(self, vmin, vmax): + if not np.isfinite(vmin) or not np.isfinite(vmax): + return 1, 10 # initial range, no data plotted yet + + if vmin > vmax: + vmin, vmax = vmax, vmin + if vmax <= 0: + warnings.warn( + "Data has no positive values, and therefore cannot be " + "log-scaled.") + return 1, 10 + + minpos = self.axis.get_minpos() + if not np.isfinite(minpos): + minpos = 1e-300 # This should never take effect. + if vmin <= 0: + vmin = minpos + if vmin == vmax: + vmin = decade_down(vmin, self._base) + vmax = decade_up(vmax, self._base) + return vmin, vmax class SymmetricalLogLocator(Locator): @@ -2260,32 +2248,7 @@ def tick_values(self, vmin, vmax): if hasattr(self.axis, 'axes') and self.axis.axes.name == 'polar': raise NotImplementedError('Polar axis cannot be logit scaled yet') - # what to do if a window beyond ]0, 1[ is chosen - if vmin <= 0.0: - if self.axis is not None: - vmin = self.axis.get_minpos() - - if (vmin <= 0.0) or (not np.isfinite(vmin)): - raise ValueError( - "Data has no values in ]0, 1[ and therefore can not be " - "logit-scaled.") - - # NOTE: for vmax, we should query a property similar to get_minpos, but - # related to the maximal, less-than-one data point. Unfortunately, - # get_minpos is defined very deep in the BBox and updated with data, - # so for now we use the trick below. - if vmax >= 1.0: - if self.axis is not None: - vmax = 1 - self.axis.get_minpos() - - if (vmax >= 1.0) or (not np.isfinite(vmax)): - raise ValueError( - "Data has no values in ]0, 1[ and therefore can not be " - "logit-scaled.") - - if vmax < vmin: - vmin, vmax = vmax, vmin - + vmin, vmax = self.nonsingular(vmin, vmax) vmin = np.log10(vmin / (1 - vmin)) vmax = np.log10(vmax / (1 - vmax)) @@ -2320,6 +2283,36 @@ def tick_values(self, vmin, vmax): return self.raise_if_exceeds(np.array(ticklocs)) + def nonsingular(self, vmin, vmax): + initial_range = (1e-7, 1 - 1e-7) + if not np.isfinite(vmin) or not np.isfinite(vmax): + return initial_range # no data plotted yet + + if vmin > vmax: + vmin, vmax = vmax, vmin + + # what to do if a window beyond ]0, 1[ is chosen + if self.axis is not None: + minpos = self.axis.get_minpos() + if not np.isfinite(minpos): + return initial_range # again, no data plotted + else: + minpos = 1e-7 # should not occur in normal use + + # NOTE: for vmax, we should query a property similar to get_minpos, but + # related to the maximal, less-than-one data point. Unfortunately, + # Bbox._minpos is defined very deep in the BBox and updated with data, + # so for now we use 1 - minpos as a substitute. + + if vmin <= 0: + vmin = minpos + if vmax >= 1: + vmax = 1 - minpos + if vmin == vmax: + return 0.1 * vmin, 1 - 0.1 * vmin + + return vmin, vmax + class AutoLocator(MaxNLocator): def __init__(self): diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index a2ef105b7b21..16889e7048d7 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -792,7 +792,7 @@ def __init__(self, points, **kwargs): raise ValueError('Bbox points must be of the form ' '"[[x0, y0], [x1, y1]]".') self._points = points - self._minpos = np.array([0.0000001, 0.0000001]) + self._minpos = np.array([np.inf, np.inf]) self._ignore = True # it is helpful in some contexts to know if the bbox is a # default or has been mutated; we store the orig points to