From 160de568e1f6d3e5e1bd10192f049815bf778dea Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 5 Mar 2019 01:47:10 +0100 Subject: [PATCH] Only autoscale_view() when needed, not after every plotting call. This avoids quadratic complexity when accumulating sticky edges. Mostly, this just replaces autoscale_view() calls with setting a flag requesting that autoscale_view() be called the next time viewLim is accessed. Note that we cannot just do this in draw as this would break common idioms like ``` ax.plot(...) ax.set_xlim(0, None) # keep top limit to what plot() set ``` The main nontrivial changes are - Removal of sticky_edges from hexbin(): Previously, hexbin() actually did *not* respect the sticky_egdes settings for some reason (this can be checked visually); but with this patch it would respect them -- breaking the baseline images. So just don't set sticky_edges instead. - Making LinearLocator.numticks a property: Previously, some code using LinearLocator would only work after the locator has been used once, so that tick_values properly set numticks to not-None; but with this patch, tick_values is no longer called early enough; making numticks a property fixes that. Note that LinearLocator is likely extremely rarely used anyways... - In test_bbox_inches_tight (which uses the old "round_numbers" autolimits mode), the autolimits change depending on whether autoscaling happens before the call to `xticks([])` (old behavior) or after (because there's no notion of "round numbers" anymore. Here we can just force these limits. - test_multi_color_hatch relied on ax.bar() triggering an autoscale but ax.add_patch *not* doing so. Just disable autoscaling then. This patch also prepares towards fixing collections autoscaling problems when switching from linear to log scales (as that also needs to be deferred to as late as possible, once the scale is actually known). --- doc/api/next_api_changes/2019-03-04-AL.rst | 20 +++++ lib/matplotlib/axes/_axes.py | 54 +++++++------ lib/matplotlib/axes/_base.py | 86 +++++++++++++++------ lib/matplotlib/collections.py | 3 + lib/matplotlib/table.py | 1 + lib/matplotlib/tests/test_bbox_tight.py | 1 + lib/matplotlib/tests/test_patches.py | 3 + lib/matplotlib/tests/test_simplification.py | 6 ++ lib/matplotlib/ticker.py | 46 ++++++----- 9 files changed, 148 insertions(+), 72 deletions(-) create mode 100644 doc/api/next_api_changes/2019-03-04-AL.rst diff --git a/doc/api/next_api_changes/2019-03-04-AL.rst b/doc/api/next_api_changes/2019-03-04-AL.rst new file mode 100644 index 000000000000..2688774aac59 --- /dev/null +++ b/doc/api/next_api_changes/2019-03-04-AL.rst @@ -0,0 +1,20 @@ +Autoscaling changes +``````````````````` + +Matplotlib used to recompute autoscaled limits after every plotting +(``plot()``, ``bar()``, etc.) call. It now only does so when actually +rendering the canvas, or when the user queries the Axes limits. This is a +major performance improvement for plots with a large number of artists. + +In particular, this means that artists added manually with `Axes.add_line`, +`Axes.add_patch`, etc. will be taken into account by the autoscale, even +without an explicit call to `Axes.autoscale_view`. + +In some cases, this can result in different limits being reported. If this is +an issue, consider triggering a draw with `fig.canvas.draw`. + +LogLocator.nonsingular now maintains the orders of its arguments +```````````````````````````````````````````````````````````````` + +It no longer reorders them in increasing order. The new behavior is consistent +with MaxNLocator. diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index affc329f1be0..464b5f007071 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -840,7 +840,7 @@ def axhline(self, y=0, xmin=0, xmax=1, **kwargs): trans = self.get_yaxis_transform(which='grid') l = mlines.Line2D([xmin, xmax], [y, y], transform=trans, **kwargs) self.add_line(l) - self.autoscale_view(scalex=False, scaley=scaley) + self._request_autoscale_view(scalex=False, scaley=scaley) return l @docstring.dedent_interpd @@ -909,7 +909,7 @@ def axvline(self, x=0, ymin=0, ymax=1, **kwargs): trans = self.get_xaxis_transform(which='grid') l = mlines.Line2D([x, x], [ymin, ymax], transform=trans, **kwargs) self.add_line(l) - self.autoscale_view(scalex=scalex, scaley=False) + self._request_autoscale_view(scalex=scalex, scaley=False) return l @docstring.dedent_interpd @@ -965,7 +965,7 @@ def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs): p = mpatches.Polygon(verts, **kwargs) p.set_transform(trans) self.add_patch(p) - self.autoscale_view(scalex=False) + self._request_autoscale_view(scalex=False) return p def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs): @@ -1030,7 +1030,7 @@ def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs): p = mpatches.Polygon(verts, **kwargs) p.set_transform(trans) self.add_patch(p) - self.autoscale_view(scaley=False) + self._request_autoscale_view(scaley=False) return p @_preprocess_data(replace_names=["y", "xmin", "xmax", "colors"], @@ -1105,7 +1105,7 @@ def hlines(self, y, xmin, xmax, colors='k', linestyles='solid', corners = (minx, miny), (maxx, maxy) self.update_datalim(corners) - self.autoscale_view() + self._request_autoscale_view() return lines @@ -1182,7 +1182,7 @@ def vlines(self, x, ymin, ymax, colors='k', linestyles='solid', corners = (minx, miny), (maxx, maxy) self.update_datalim(corners) - self.autoscale_view() + self._request_autoscale_view() return lines @@ -1398,7 +1398,7 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, else: # "horizontal", None or "none" (see EventCollection) corners = (minpos, minline), (maxpos, maxline) self.update_datalim(corners) - self.autoscale_view() + self._request_autoscale_view() return colls @@ -1642,7 +1642,7 @@ def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs): lines = [*self._get_lines(*args, data=data, **kwargs)] for line in lines: self.add_line(line) - self.autoscale_view(scalex=scalex, scaley=scaley) + self._request_autoscale_view(scalex=scalex, scaley=scaley) return lines @_preprocess_data(replace_names=["x", "y"], label_namer="y") @@ -1718,7 +1718,7 @@ def plot_date(self, x, y, fmt='o', tz=None, xdate=True, ydate=False, ret = self.plot(x, y, fmt, **kwargs) - self.autoscale_view() + self._request_autoscale_view() return ret @@ -2422,7 +2422,7 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", ymin = ymin - np.max(yerr) ymin = max(ymin * 0.9, 1e-100) self.dataLim.intervaly = (ymin, ymax) - self.autoscale_view() + self._request_autoscale_view() bar_container = BarContainer(patches, errorbar, label=label) self.add_container(bar_container) @@ -2623,7 +2623,7 @@ def broken_barh(self, xranges, yrange, **kwargs): col = mcoll.BrokenBarHCollection(xranges_conv, yrange_conv, **kwargs) self.add_collection(col, autolim=True) - self.autoscale_view() + self._request_autoscale_view() return col @@ -3429,7 +3429,7 @@ def extract_err(err, data): for l in caplines: self.add_line(l) - self.autoscale_view() + self._request_autoscale_view() errorbar_container = ErrorbarContainer((data_line, tuple(caplines), tuple(barcols)), has_xerr=(xerr is not None), @@ -4101,7 +4101,7 @@ def dopatch(xs, ys, **kwargs): axis.set_major_formatter(formatter) formatter.seq = [*formatter.seq, *datalabels] - self.autoscale_view( + self._request_autoscale_view( scalex=self._autoscaleXon, scaley=self._autoscaleYon) return dict(whiskers=whiskers, caps=caps, boxes=boxes, @@ -4479,7 +4479,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, self.set_ymargin(0.05) self.add_collection(collection) - self.autoscale_view() + self._request_autoscale_view() return collection @@ -4832,9 +4832,7 @@ def hexbin(self, x, y, C=None, gridsize=100, bins=None, corners = ((xmin, ymin), (xmax, ymax)) self.update_datalim(corners) - collection.sticky_edges.x[:] = [xmin, xmax] - collection.sticky_edges.y[:] = [ymin, ymax] - self.autoscale_view(tight=True) + self._request_autoscale_view(tight=True) # add the collection last self.add_collection(collection, autolim=False) @@ -5004,7 +5002,7 @@ def quiver(self, *args, **kw): q = mquiver.Quiver(self, *args, **kw) self.add_collection(q, autolim=True) - self.autoscale_view() + self._request_autoscale_view() return q quiver.__doc__ = mquiver.Quiver.quiver_doc @@ -5020,7 +5018,7 @@ def barbs(self, *args, **kw): b = mquiver.Barbs(self, *args, **kw) self.add_collection(b, autolim=True) - self.autoscale_view() + self._request_autoscale_view() return b # Uses a custom implementation of data-kwarg handling in @@ -5075,7 +5073,7 @@ def fill(self, *args, data=None, **kwargs): for poly in self._get_patches_for_fill(*args, data=data, **kwargs): self.add_patch(poly) patches.append(poly) - self.autoscale_view() + self._request_autoscale_view() return patches @_preprocess_data(replace_names=["x", "y1", "y2", "where"]) @@ -5257,7 +5255,7 @@ def get_interp_point(ind): self.dataLim.update_from_data_xy(XY2, self.ignore_existing_data_limits, updatex=False, updatey=True) self.add_collection(collection, autolim=False) - self.autoscale_view() + self._request_autoscale_view() return collection @_preprocess_data(replace_names=["y", "x1", "x2", "where"]) @@ -5438,7 +5436,7 @@ def get_interp_point(ind): self.dataLim.update_from_data_xy(X2Y, self.ignore_existing_data_limits, updatex=True, updatey=False) self.add_collection(collection, autolim=False) - self.autoscale_view() + self._request_autoscale_view() return collection #### plotting z(x,y): imshow, pcolor and relatives, contour @@ -5937,7 +5935,7 @@ def pcolor(self, *args, alpha=None, norm=None, cmap=None, vmin=None, collection.sticky_edges.y[:] = [miny, maxy] corners = (minx, miny), (maxx, maxy) self.update_datalim(corners) - self.autoscale_view() + self._request_autoscale_view() return collection @_preprocess_data() @@ -6150,7 +6148,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, collection.sticky_edges.y[:] = [miny, maxy] corners = (minx, miny), (maxx, maxy) self.update_datalim(corners) - self.autoscale_view() + self._request_autoscale_view() return collection @_preprocess_data() @@ -6320,14 +6318,14 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, ret.sticky_edges.x[:] = [xl, xr] ret.sticky_edges.y[:] = [yb, yt] self.update_datalim(np.array([[xl, yb], [xr, yt]])) - self.autoscale_view(tight=True) + self._request_autoscale_view(tight=True) return ret @_preprocess_data() def contour(self, *args, **kwargs): kwargs['filled'] = False contours = mcontour.QuadContourSet(self, *args, **kwargs) - self.autoscale_view() + self._request_autoscale_view() return contours contour.__doc__ = mcontour.QuadContourSet._contour_doc @@ -6335,7 +6333,7 @@ def contour(self, *args, **kwargs): def contourf(self, *args, **kwargs): kwargs['filled'] = True contours = mcontour.QuadContourSet(self, *args, **kwargs) - self.autoscale_view() + self._request_autoscale_view() return contours contourf.__doc__ = mcontour.QuadContourSet._contour_doc @@ -6842,7 +6840,7 @@ def hist(self, x, bins=None, range=None, density=None, weights=None, self.set_autoscalex_on(_saved_autoscalex) self.set_autoscaley_on(_saved_autoscaley) - self.autoscale_view() + self._request_autoscale_view() if label is None: labels = [None] diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 5eee450a29f9..893cdf3775e1 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -470,6 +470,8 @@ def __init__(self, fig, rect, self._aspect = 'auto' self._adjustable = 'box' self._anchor = 'C' + self._stale_viewlim_x = False + self._stale_viewlim_y = False self._sharex = sharex self._sharey = sharey if sharex is not None: @@ -615,12 +617,41 @@ def set_figure(self, fig): fig.transFigure) # these will be updated later as data is added self.dataLim = mtransforms.Bbox.null() - self.viewLim = mtransforms.Bbox.unit() + self._viewLim = mtransforms.Bbox.unit() self.transScale = mtransforms.TransformWrapper( mtransforms.IdentityTransform()) self._set_lim_and_transforms() + def _unstale_viewLim(self): + # We should arrange to store this information once per share-group + # instead of on every axis. + scalex = any(ax._stale_viewlim_x + for ax in self._shared_x_axes.get_siblings(self)) + scaley = any(ax._stale_viewlim_y + for ax in self._shared_y_axes.get_siblings(self)) + if scalex or scaley: + for ax in self._shared_x_axes.get_siblings(self): + ax._stale_viewlim_x = False + for ax in self._shared_y_axes.get_siblings(self): + ax._stale_viewlim_y = False + self.autoscale_view(scalex=scalex, scaley=scaley) + + @property + def viewLim(self): + self._unstale_viewLim() + return self._viewLim + + # API could be better, right now this is just to match the old calls to + # autoscale_view() after each plotting method. + def _request_autoscale_view(self, tight=None, scalex=True, scaley=True): + if tight is not None: + self._tight = tight + if scalex: + self._stale_viewlim_x = True # Else keep old state. + if scaley: + self._stale_viewlim_y = True + def _set_lim_and_transforms(self): """ Set the *_xaxis_transform*, *_yaxis_transform*, *transScale*, @@ -645,7 +676,7 @@ def _set_lim_and_transforms(self): # An affine transformation on the data, generally to limit the # range of the axes self.transLimits = mtransforms.BboxTransformFrom( - mtransforms.TransformedBbox(self.viewLim, self.transScale)) + mtransforms.TransformedBbox(self._viewLim, self.transScale)) # The parentheses are important for efficiency here -- they # group the last two (which are usually affines) separately @@ -1862,6 +1893,9 @@ def add_collection(self, collection, autolim=True): collection.set_clip_path(self.patch) if autolim: + # Make sure viewLim is not stale (mostly to match + # pre-lazy-autoscale behavior, which is not really better). + self._unstale_viewLim() self.update_datalim(collection.get_datalim(self.transData)) self.stale = True @@ -2019,7 +2053,7 @@ def _on_units_changed(self, scalex=False, scaley=False): Currently forces updates of data limits and view limits. """ self.relim() - self.autoscale_view(scalex=scalex, scaley=scaley) + self._request_autoscale_view(scalex=scalex, scaley=scaley) def relim(self, visible_only=False): """ @@ -2313,7 +2347,7 @@ def margins(self, *margins, x=None, y=None, tight=True): if y is not None: self.set_ymargin(y) - self.autoscale_view( + self._request_autoscale_view( tight=tight, scalex=(x is not None), scaley=(y is not None) ) @@ -2375,7 +2409,7 @@ def autoscale(self, enable=True, axis='both', tight=None): self._xmargin = 0 if tight and scaley: self._ymargin = 0 - self.autoscale_view(tight=tight, scalex=scalex, scaley=scaley) + self._request_autoscale_view(tight=tight, scalex=scalex, scaley=scaley) def autoscale_view(self, tight=None, scalex=True, scaley=True): """ @@ -2556,11 +2590,12 @@ def draw(self, renderer=None, inframe=False): """Draw everything (plot lines, axes, labels)""" if renderer is None: renderer = self.figure._cachedRenderer - if renderer is None: raise RuntimeError('No renderer defined') if not self.get_visible(): return + self._unstale_viewLim() + renderer.open_group('axes') # prevent triggering call backs during the draw process @@ -2886,7 +2921,7 @@ def locator_params(self, axis='both', tight=None, **kwargs): self.xaxis.get_major_locator().set_params(**kwargs) if _y: self.yaxis.get_major_locator().set_params(**kwargs) - self.autoscale_view(tight=tight, scalex=_x, scaley=_y) + self._request_autoscale_view(tight=tight, scalex=_x, scaley=_y) def tick_params(self, axis='both', **kwargs): """Change the appearance of ticks, tick labels, and gridlines. @@ -3122,7 +3157,6 @@ def _validate_converted_limits(self, limit, convert): Returns ------- The limit value after call to convert(), or None if limit is None. - """ if limit is not None: converted_limit = convert(limit) @@ -3214,11 +3248,14 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False, left = self._validate_converted_limits(left, self.convert_xunits) right = self._validate_converted_limits(right, self.convert_xunits) - old_left, old_right = self.get_xlim() - if left is None: - left = old_left - if right is None: - right = old_right + if left is None or right is None: + # Axes init calls set_xlim(0, 1) before get_xlim() can be called, + # so only grab the limits if we really need them. + old_left, old_right = self.get_xlim() + if left is None: + left = old_left + if right is None: + right = old_right if self.get_xscale() == 'log': if left <= 0: @@ -3240,7 +3277,7 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False, left, right = self.xaxis.get_major_locator().nonsingular(left, right) left, right = self.xaxis.limit_range_for_scale(left, right) - self.viewLim.intervalx = (left, right) + self._viewLim.intervalx = (left, right) if auto is not None: self._autoscaleXon = bool(auto) @@ -3297,8 +3334,7 @@ def set_xscale(self, value, **kwargs): ax.xaxis._set_scale(value, **kwargs) ax._update_transScale() ax.stale = True - - self.autoscale_view(scaley=False) + self._request_autoscale_view(scaley=False) def get_xticks(self, minor=False): """Return the x ticks as a list of locations""" @@ -3592,12 +3628,14 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False, bottom = self._validate_converted_limits(bottom, self.convert_yunits) top = self._validate_converted_limits(top, self.convert_yunits) - old_bottom, old_top = self.get_ylim() - - if bottom is None: - bottom = old_bottom - if top is None: - top = old_top + if bottom is None or top is None: + # Axes init calls set_ylim(0, 1) before get_ylim() can be called, + # so only grab the limits if we really need them. + old_bottom, old_top = self.get_ylim() + if bottom is None: + bottom = old_bottom + if top is None: + top = old_top if self.get_yscale() == 'log': if bottom <= 0: @@ -3620,7 +3658,7 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False, bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top) bottom, top = self.yaxis.limit_range_for_scale(bottom, top) - self.viewLim.intervaly = (bottom, top) + self._viewLim.intervaly = (bottom, top) if auto is not None: self._autoscaleYon = bool(auto) @@ -3677,7 +3715,7 @@ def set_yscale(self, value, **kwargs): ax.yaxis._set_scale(value, **kwargs) ax._update_transScale() ax.stale = True - self.autoscale_view(scalex=False) + self._request_autoscale_view(scalex=False) def get_yticks(self, minor=False): """Return the y ticks as a list of locations""" diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 53df43a41a45..65285bf85f48 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -365,6 +365,9 @@ def contains(self, mouseevent): self._picker is not True # the bool, not just nonzero or 1 else self._pickradius) + if self.axes and self.get_offset_position() == "data": + self.axes._unstale_viewLim() + transform, transOffset, offsets, paths = self._prepare_points() ind = _path.point_in_path_collection( diff --git a/lib/matplotlib/table.py b/lib/matplotlib/table.py index 55eadc51b0c1..0086d88c2b34 100644 --- a/lib/matplotlib/table.py +++ b/lib/matplotlib/table.py @@ -320,6 +320,7 @@ def __init__(self, ax, loc=None, bbox=None, **kwargs): self._bbox = bbox # use axes coords + ax._unstale_viewLim() self.set_transform(ax.transAxes) self._texts = [] diff --git a/lib/matplotlib/tests/test_bbox_tight.py b/lib/matplotlib/tests/test_bbox_tight.py index 8c99c4572f76..c4566e519dec 100644 --- a/lib/matplotlib/tests/test_bbox_tight.py +++ b/lib/matplotlib/tests/test_bbox_tight.py @@ -32,6 +32,7 @@ def test_bbox_inches_tight(): yoff = yoff + data[row] cellText.append(['']) plt.xticks([]) + plt.xlim(0, 5) plt.legend([''] * 5, loc=(1.2, 0.2)) # Add a table at the bottom of the axes cellText.reverse() diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index 13c5dd1a83eb..d32ada81a73c 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -363,6 +363,9 @@ def test_multi_color_hatch(): rect.set_edgecolor('C{}'.format(i)) rect.set_hatch('/') + ax.autoscale_view() + ax.autoscale(False) + for i in range(5): with mstyle.context({'hatch.color': 'C{}'.format(i)}): r = Rectangle((i - .8 / 2, 5), .8, 1, hatch='//', fc='none') diff --git a/lib/matplotlib/tests/test_simplification.py b/lib/matplotlib/tests/test_simplification.py index 9818c9495e17..e91e91461b35 100644 --- a/lib/matplotlib/tests/test_simplification.py +++ b/lib/matplotlib/tests/test_simplification.py @@ -54,6 +54,8 @@ def test_noise(): fig, ax = plt.subplots() p1 = ax.plot(x, solid_joinstyle='round', linewidth=2.0) + # Ensure that the path's transform takes the new axes limits into account. + fig.canvas.draw() path = p1[0].get_path() transform = p1[0].get_transform() path = transform.transform_path(path) @@ -195,6 +197,8 @@ def test_sine_plus_noise(): fig, ax = plt.subplots() p1 = ax.plot(x, solid_joinstyle='round', linewidth=2.0) + # Ensure that the path's transform takes the new axes limits into account. + fig.canvas.draw() path = p1[0].get_path() transform = p1[0].get_transform() path = transform.transform_path(path) @@ -232,6 +236,8 @@ def test_fft_peaks(): t = np.arange(65536) p1 = ax.plot(abs(np.fft.fft(np.sin(2*np.pi*.01*t)*np.blackman(len(t))))) + # Ensure that the path's transform takes the new axes limits into account. + fig.canvas.draw() path = p1[0].get_path() transform = p1[0].get_transform() path = transform.transform_path(path) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index b29ddba2e0b6..4f48e5ced850 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1670,6 +1670,15 @@ def __init__(self, numticks=None, presets=None): else: self.presets = presets + @property + def numticks(self): + # Old hard-coded default. + return self._numticks if self._numticks is not None else 11 + + @numticks.setter + def numticks(self, numticks): + self._numticks = numticks + def set_params(self, numticks=None, presets=None): """Set parameters within this locator.""" if presets is not None: @@ -1690,18 +1699,12 @@ def tick_values(self, vmin, vmax): if (vmin, vmax) in self.presets: return self.presets[(vmin, vmax)] - if self.numticks is None: - self._set_numticks() - if self.numticks == 0: return [] ticklocs = np.linspace(vmin, vmax, self.numticks) return self.raise_if_exceeds(ticklocs) - def _set_numticks(self): - self.numticks = 11 # todo; be smart here; this is just for dev - def view_limits(self, vmin, vmax): 'Try to choose the view limits intelligently' @@ -2376,25 +2379,28 @@ def view_limits(self, vmin, vmax): return vmin, vmax def nonsingular(self, vmin, vmax): - if not np.isfinite(vmin) or not np.isfinite(vmax): - return 1, 10 # initial range, no data plotted yet - + swap_vlims = False if vmin > vmax: + swap_vlims = True vmin, vmax = vmax, vmin - if vmax <= 0: + if not np.isfinite(vmin) or not np.isfinite(vmax): + vmin, vmax = 1, 10 # Initial range, no data plotted yet. + elif vmax <= 0: cbook._warn_external( "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_less(vmin, self._base) - vmax = _decade_greater(vmax, self._base) + vmin, vmax = 1, 10 + else: + 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_less(vmin, self._base) + vmax = _decade_greater(vmax, self._base) + if swap_vlims: + vmin, vmax = vmax, vmin return vmin, vmax