diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 0f7478de0c66..fef2125c54d4 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -118,6 +118,8 @@ def __init__(self): self._sketch = rcParams['path.sketch'] self._path_effects = rcParams['path.effects'] + self._margins = {} + def __getstate__(self): d = self.__dict__.copy() # remove the unpicklable remove method, this will get re-added on load @@ -898,6 +900,99 @@ def set_zorder(self, level): self.pchanged() self.stale = True + def get_top_margin(self): + """ + Get whether a margin should be applied to the top of the Artist. + """ + return self._margins.get('top', True) + + def set_top_margin(self, margin): + """ + Set whether a margin should be applied to the top of the Artist. + """ + if margin != self._margins.get('top', True): + self.stale = True + self._margins['top'] = margin + + top_margin = property(get_top_margin, set_top_margin) + + def get_bottom_margin(self): + """ + Get whether a margin should be applied to the bottom of the Artist. + """ + return self._margins.get('bottom', True) + + def set_bottom_margin(self, margin): + """ + Set whether a margin should be applied to the bottom of the Artist. + """ + if margin != self._margins.get('bottom', True): + self.stale = True + self._margins['bottom'] = margin + + bottom_margin = property(get_bottom_margin, set_bottom_margin) + + def get_left_margin(self): + """ + Get whether a margin should be applied to the left of the Artist. + """ + return self._margins.get('left', True) + + def set_left_margin(self, margin): + """ + Set whether a margin should be applied to the left of the Artist. + """ + if margin != self._margins.get('left', True): + self.stale = True + self._margins['left'] = margin + + left_margin = property(get_left_margin, set_left_margin) + + def get_right_margin(self): + """ + Get whether a margin should be applied to the right of the Artist. + """ + return self._margins.get('right', True) + + def set_right_margin(self, margin): + """ + Set whether a margin should be applied to the right of the Artist. + """ + if margin != self._margins.get('right', True): + self.stale = True + self._margins['right'] = margin + + right_margin = property(get_right_margin, set_right_margin) + + def get_margins(self): + """ + Returns a dictionary describing whether a margin should be applied on + each of the sides (top, bottom, left and right). + """ + return self._margins + + def set_margins(self, margins): + """ + Set the dictionary describing whether a margin should be applied on + each of the sides (top, bottom, left and right). Missing keys are + assumed to be `True`. If `True` or `False` are passed in, all + sides are set to that value. + """ + if margins in (True, False): + margins = { + 'top': margins, + 'bottom': margins, + 'left': margins, + 'right': margins + } + + if margins != self._margins: + self.stale = True + + self._margins = margins + + margins = property(get_margins, set_margins) + def update_from(self, other): 'Copy properties from *other* to *self*.' self._transform = other._transform diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 16a249e0d6e8..938cbefa5bc5 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2104,16 +2104,21 @@ def make_iterable(x): if yerr is not None: yerr = self.convert_yunits(yerr) - if align == 'edge': - pass - elif align == 'center': + margins = {} + + if orientation == 'vertical': + margins = {'bottom': False} + elif orientation == 'horizontal': + margins = {'left': False} + + if align == 'center': if orientation == 'vertical': left = [left[i] - width[i] / 2. for i in xrange(len(left))] elif orientation == 'horizontal': bottom = [bottom[i] - height[i] / 2. for i in xrange(len(bottom))] - else: + elif align != 'edge': raise ValueError('invalid alignment: %s' % align) args = zip(left, bottom, width, height, color, edgecolor, linewidth) @@ -2129,7 +2134,8 @@ def make_iterable(x): facecolor=c, edgecolor=e, linewidth=lw, - label='_nolegend_' + label='_nolegend_', + margins=margins ) r.update(kwargs) r.get_path()._interpolation_steps = 100 @@ -5267,7 +5273,7 @@ def pcolor(self, *args, **kwargs): kwargs.setdefault('snap', False) - collection = mcoll.PolyCollection(verts, **kwargs) + collection = mcoll.PolyCollection(verts, margins=False, **kwargs) collection.set_alpha(alpha) collection.set_array(C) @@ -5302,9 +5308,9 @@ def pcolor(self, *args, **kwargs): maxy = np.amax(y) corners = (minx, miny), (maxx, maxy) + self.add_collection(collection, autolim=False) self.update_datalim(corners) self.autoscale_view() - self.add_collection(collection, autolim=False) return collection @unpack_labeled_data(label_namer=None) @@ -5419,7 +5425,8 @@ def pcolormesh(self, *args, **kwargs): collection = mcoll.QuadMesh( Nx - 1, Ny - 1, coords, - antialiased=antialiased, shading=shading, **kwargs) + antialiased=antialiased, shading=shading, margins=False, + **kwargs) collection.set_alpha(alpha) collection.set_array(C) if norm is not None and not isinstance(norm, mcolors.Normalize): @@ -5451,9 +5458,9 @@ def pcolormesh(self, *args, **kwargs): maxy = np.amax(Y) corners = (minx, miny), (maxx, maxy) + self.add_collection(collection, autolim=False) self.update_datalim(corners) self.autoscale_view() - self.add_collection(collection, autolim=False) return collection @unpack_labeled_data(label_namer=None) @@ -5603,7 +5610,8 @@ def pcolorfast(self, *args, **kwargs): # The QuadMesh class can also be changed to # handle relevant superclass kwargs; the initializer # should do much more than it does now. - collection = mcoll.QuadMesh(nc, nr, coords, 0, edgecolors="None") + collection = mcoll.QuadMesh(nc, nr, coords, 0, edgecolors="None", + margins=False) collection.set_alpha(alpha) collection.set_array(C) collection.set_cmap(cmap) @@ -5649,7 +5657,9 @@ def contour(self, *args, **kwargs): if not self._hold: self.cla() kwargs['filled'] = False - return mcontour.QuadContourSet(self, *args, **kwargs) + contours = mcontour.QuadContourSet(self, *args, **kwargs) + self.autoscale_view() + return contours contour.__doc__ = mcontour.QuadContourSet.contour_doc @unpack_labeled_data() @@ -5657,7 +5667,9 @@ def contourf(self, *args, **kwargs): if not self._hold: self.cla() kwargs['filled'] = True - return mcontour.QuadContourSet(self, *args, **kwargs) + contours = mcontour.QuadContourSet(self, *args, **kwargs) + self.autoscale_view() + return contours contourf.__doc__ = mcontour.QuadContourSet.contour_doc def clabel(self, CS, *args, **kwargs): @@ -6037,6 +6049,11 @@ def hist(self, x, bins=None, range=None, normed=False, weights=None, else: n = [m[slc].cumsum()[slc] for m in n] + if orientation == 'horizontal': + margins = {'left': False} + else: + margins = {'bottom': False} + patches = [] if histtype.startswith('bar'): @@ -6177,14 +6194,16 @@ def hist(self, x, bins=None, range=None, normed=False, weights=None, patches.append(self.fill( x, y, closed=True, - facecolor=c)) + facecolor=c, + margins=margins)) else: for x, y, c in reversed(list(zip(xvals, yvals, color))): split = 2 * len(bins) patches.append(self.fill( x[:split], y[:split], closed=False, edgecolor=c, - fill=False)) + fill=False, + margins=margins)) # we return patches, so put it back in the expected order patches.reverse() diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 8920b5e2b489..0d3ab8b15330 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -989,7 +989,7 @@ def cla(self): self._autoscaleYon = True self._xmargin = rcParams['axes.xmargin'] self._ymargin = rcParams['axes.ymargin'] - self._tight = False + self._tight = None self._update_transScale() # needed? self._get_lines = _process_plot_var_args(self) @@ -2141,62 +2141,107 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True): autoscale_view. """ if tight is None: - # if image data only just use the datalim - _tight = self._tight or (len(self.images) > 0 and - len(self.lines) == 0 and - len(self.patches) == 0) + _tight = self._tight else: _tight = self._tight = bool(tight) - if scalex and self._autoscaleXon: - xshared = self._shared_x_axes.get_siblings(self) - dl = [ax.dataLim for ax in xshared] - # ignore non-finite data limits if good limits exist - finite_dl = [d for d in dl if np.isfinite(d).all()] - if len(finite_dl): - dl = finite_dl - - bb = mtransforms.BboxBase.union(dl) - x0, x1 = bb.intervalx - xlocator = self.xaxis.get_major_locator() - try: - # e.g., DateLocator has its own nonsingular() - x0, x1 = xlocator.nonsingular(x0, x1) - except AttributeError: - # Default nonsingular for, e.g., MaxNLocator - x0, x1 = mtransforms.nonsingular(x0, x1, increasing=False, - expander=0.05) - if self._xmargin > 0: - delta = (x1 - x0) * self._xmargin - x0 -= delta - x1 += delta - if not _tight: - x0, x1 = xlocator.view_limits(x0, x1) - self.set_xbound(x0, x1) - - if scaley and self._autoscaleYon: - yshared = self._shared_y_axes.get_siblings(self) - dl = [ax.dataLim for ax in yshared] - # ignore non-finite data limits if good limits exist - finite_dl = [d for d in dl if np.isfinite(d).all()] - if len(finite_dl): - dl = finite_dl - - bb = mtransforms.BboxBase.union(dl) - y0, y1 = bb.intervaly - ylocator = self.yaxis.get_major_locator() - try: - y0, y1 = ylocator.nonsingular(y0, y1) - except AttributeError: - y0, y1 = mtransforms.nonsingular(y0, y1, increasing=False, - expander=0.05) - if self._ymargin > 0: - delta = (y1 - y0) * self._ymargin - y0 -= delta - y1 += delta - if not _tight: - y0, y1 = ylocator.view_limits(y0, y1) - self.set_ybound(y0, y1) + if self._xmargin or self._ymargin: + margins = { + 'top': True, + 'bottom': True, + 'left': True, + 'right': True + } + for artist_set in [self.collections, self.patches, self.lines, + self.artists, self.images]: + for artist in artist_set: + artist_margins = artist.margins + for key in ['left', 'right', 'top', 'bottom']: + margins[key] &= artist_margins.get(key, True) + + if self._xmargin: + for axes in self._shared_x_axes.get_siblings(self): + for artist_set in [axes.collections, axes.patches, + axes.lines, axes.artists, axes.images]: + for artist in artist_set: + artist_margins = artist.margins + for key in ['left', 'right']: + margins[key] &= artist_margins.get(key, True) + + if self._ymargin: + for axes in self._shared_y_axes.get_siblings(self): + for artist_set in [axes.collections, axes.patches, + axes.lines, axes.artists, axes.images]: + for artist in artist_set: + artist_margins = artist.margins + for key in ['top', 'bottom']: + margins[key] &= artist_margins.get(key, True) + else: + margins = { + 'top': False, + 'bottom': False, + 'left': False, + 'right': False + } + + def handle_single_axis(scale, autoscaleon, shared_axes, interval, + minpos, axis, margin, do_lower_margin, + do_upper_margin, set_bound): + if scale and autoscaleon: + shared = shared_axes.get_siblings(self) + dl = [ax.dataLim for ax in shared] + # ignore non-finite data limits if good limits exist + finite_dl = [d for d in dl if np.isfinite(d).all()] + if len(finite_dl): + dl = finite_dl + + bb = mtransforms.BboxBase.union(dl) + x0, x1 = getattr(bb, interval) + locator = axis.get_major_locator() + try: + # e.g., DateLocator has its own nonsingular() + x0, x1 = locator.nonsingular(x0, x1) + except AttributeError: + # Default nonsingular for, e.g., MaxNLocator + x0, x1 = mtransforms.nonsingular( + x0, x1, increasing=False, expander=0.05) + + if (margin > 0 and do_lower_margin or do_upper_margin): + if axis.get_scale() == 'linear': + delta = (x1 - x0) * margin + if do_lower_margin: + x0 -= delta + if do_upper_margin: + x1 += delta + else: + # If we have a non-linear scale, we need to + # add the margin in figure space and then + # transform back + minpos = getattr(bb, minpos) + transform = axis.get_transform() + inverse_trans = transform.inverted() + x0, x1 = axis._scale.limit_range_for_scale( + x0, x1, minpos) + x0t, x1t = transform.transform([x0, x1]) + delta = (x1t - x0t) * margin + if do_lower_margin: + x0t -= delta + if do_upper_margin: + x1t += delta + x0, x1 = inverse_trans.transform([x0t, x1t]) + + if not _tight: + x0, x1 = locator.view_limits(x0, x1) + set_bound(x0, x1) + + handle_single_axis( + scalex, self._autoscaleXon, self._shared_x_axes, + 'intervalx', 'minposx', self.xaxis, self._xmargin, + margins['left'], margins['right'], self.set_xbound) + handle_single_axis( + scaley, self._autoscaleYon, self._shared_y_axes, + 'intervaly', 'minposy', self.yaxis, self._ymargin, + margins['bottom'], margins['top'], self.set_ybound) def _get_axis_list(self): return (self.xaxis, self.yaxis) diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 5416d58932fc..f6b2fde2f57f 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -947,7 +947,8 @@ def __init__(self, ax, *args, **kwargs): edgecolors='none', alpha=self.alpha, transform=self.get_transform(), - zorder=zorder) + zorder=zorder, + margins=False) self.ax.add_collection(col, autolim=False) self.collections.append(col) else: @@ -968,7 +969,8 @@ def __init__(self, ax, *args, **kwargs): linestyles=[lstyle], alpha=self.alpha, transform=self.get_transform(), - zorder=zorder) + zorder=zorder, + margins=False) col.set_label('_nolegend_') self.ax.add_collection(col, autolim=False) self.collections.append(col) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 7a3cd1dc9c39..eddabdd95922 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -117,6 +117,7 @@ def __init__(self, ax, self.set_interpolation(interpolation) self.set_resample(resample) + self.set_margins(False) self.axes = ax self._imcache = None diff --git a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle index 7660f8575e9c..eb122d29c9c3 100644 --- a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle @@ -205,6 +205,7 @@ axes.prop_cycle : cycler('color', 'bgrcmyk') # as list of string colorspecs: # single letter, long name, or # web-style hex +axes.autolimit_mode : round_numbers axes.xmargin : 0 # x margin. See `axes.Axes.margins` axes.ymargin : 0 # y margin See `axes.Axes.margins` axes.spines.bottom : True diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 70c693563e64..a395b09e60a0 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -984,12 +984,17 @@ def validate_hist_bins(s): # to create the object. 'axes.prop_cycle': [ccycler('color', 'bgrcmyk'), validate_cycler], - 'axes.xmargin': [0, ValidateInterval(0, 1, - closedmin=True, - closedmax=True)], # margin added to xaxis - 'axes.ymargin': [0, ValidateInterval(0, 1, - closedmin=True, - closedmax=True)],# margin added to yaxis + # If 'data', axes limits are set close to the data. + # If 'round_numbers' axes limits are set to the nearest round numbers. + 'axes.autolimit_mode': [ + 'data', + ValidateInStrings('autolimit_mode', ['data', 'round_numbers'])], + 'axes.xmargin': [0.05, ValidateInterval(0, 1, + closedmin=True, + closedmax=True)], # margin added to xaxis + 'axes.ymargin': [0.05, ValidateInterval(0, 1, + closedmin=True, + closedmax=True)],# margin added to yaxis 'polaraxes.grid': [True, validate_bool], # display polar grid or # not diff --git a/lib/matplotlib/stackplot.py b/lib/matplotlib/stackplot.py index 4f48d0b85b9a..8a34f97c389a 100644 --- a/lib/matplotlib/stackplot.py +++ b/lib/matplotlib/stackplot.py @@ -71,8 +71,10 @@ def stackplot(axes, x, *args, **kwargs): stack = np.cumsum(y, axis=0) r = [] + margins = {} if baseline == 'zero': first_line = 0. + margins['bottom'] = False elif baseline == 'sym': first_line = -np.sum(y, 0) * 0.5 @@ -83,6 +85,7 @@ def stackplot(axes, x, *args, **kwargs): first_line = (y * (m - 0.5 - np.arange(0, m)[:, None])).sum(0) first_line /= -m stack += first_line + margins['bottom'] = False elif baseline == 'weighted_wiggle': m, n = y.shape @@ -97,6 +100,8 @@ def stackplot(axes, x, *args, **kwargs): center = np.cumsum(center.sum(0)) first_line = center - 0.5 * total stack += first_line + margins['bottom'] = False + else: errstr = "Baseline method %s not recognised. " % baseline errstr += "Expected 'zero', 'sym', 'wiggle' or 'weighted_wiggle'" @@ -110,6 +115,7 @@ def stackplot(axes, x, *args, **kwargs): r.append(axes.fill_between(x, first_line, stack[0, :], facecolor=color, label= six.next(labels, None), + margins=margins, **kwargs)) # Color between array i-1 and array i @@ -121,5 +127,6 @@ def stackplot(axes, x, *args, **kwargs): r.append(axes.fill_between(x, stack[i, :], stack[i + 1, :], facecolor=color, label= six.next(labels, None), + margins=margins, **kwargs)) return r diff --git a/lib/matplotlib/streamplot.py b/lib/matplotlib/streamplot.py index a31f46cc7ba0..7d564c76eb77 100644 --- a/lib/matplotlib/streamplot.py +++ b/lib/matplotlib/streamplot.py @@ -184,12 +184,14 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, p = patches.FancyArrowPatch(arrow_tail, arrow_head, transform=transform, + margins=False, **arrow_kw) axes.add_patch(p) arrows.append(p) lc = mcollections.LineCollection(streamlines, transform=transform, + margins=False, **line_kw) if use_multicolor_lines: lc.set_array(np.ma.hstack(line_colors)) @@ -198,7 +200,7 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, axes.add_collection(lc) axes.autoscale_view() - ac = matplotlib.collections.PatchCollection(arrows) + ac = matplotlib.collections.PatchCollection(arrows, margins=False) stream_container = StreamplotSet(lc, ac) return stream_container diff --git a/lib/matplotlib/tests/baseline_images/test_artist/default_edges.png b/lib/matplotlib/tests/baseline_images/test_artist/default_edges.png index 932e2d9ec08b..074f1bf404d9 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_artist/default_edges.png and b/lib/matplotlib/tests/baseline_images/test_artist/default_edges.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.pdf b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.pdf index 27c4722fcac9..36b665da2e20 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.pdf and b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.png b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.png index 414972144c25..16fa4a783ceb 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.png and b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.svg b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.svg index 117ab1abf587..6fc0280cb41d 100644 --- a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.svg +++ b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.svg @@ -5,5291 +5,5163 @@ - - - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - +" id="m6af6546fe7" style="stroke:#000000;"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:3.3439690036921244;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:2.992787371147062;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:0.5880496226161019;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:0.4698808500506423;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:1.1466405239365214;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:5.17633233023888;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:4.222007204006107;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:2.1860149908976063;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:4.29207840964949;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:4.316706464023581;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:2.930897433702203;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:3.8223004273876198;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:4.678638301222006;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:4.364802149768131;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:4.586196422339218;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:4.518532669069514;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:3.3770690495371407;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:3.4287055190179165;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:4.320976434506476;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:2.4525451284125355;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:1.7409014095942217;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:1.1709647787347088;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:5.0566743960149685;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:0.4266863346300078;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:0.04263692476679008;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:0.3974587330719239;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:1.0318610241083914;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:1.7549280089940171;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:2.2314084577083357;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:1.8283630330209364;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:2.9493623624496728;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:1.4985233172852523;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:3.2047519384860284;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:2.0176218095607115;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:3.8835187666479025;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:1.4478063572806037;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:2.2152930562336417;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:2.2466094115558994;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:0.7730026696948662;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:1.4359472564656426;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:0.8353100664908387;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:0.9301990528251347;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:1.0546006348442267;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:4.0837288733427;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:0.4967807622126851;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:0.1587558871477691;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:0.6535590915322094;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:2.949675008527799;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:1.420671882841679;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:0.44712651064990816;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:2.7102052908395757;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:2.4099230515610355;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:3.465175895089431;"/> - - + +" style="fill:#0000ff;stroke:#0000ff;stroke-linecap:round;stroke-width:1.9482227110072619;"/> - + - + - + - + - + - + - + - + - + - - + + - + - + - + - - - + + + - + - + - +" id="DejaVuSans-34"/> - - - + + + - + - + - + - - - + + + - + - + - + - - - + + + - + - + - +" id="DejaVuSans-31"/> - - - - + + + + @@ -5298,124 +5170,124 @@ z - + - + - + - + - - + + - + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - + + + - + - + - - - - + + + + @@ -5423,7 +5295,7 @@ L-4 0" id="m81ec2b1422" style="stroke:#000000;stroke-width:0.5;"/> - + diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 21c2f0562513..28223b49694a 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4157,6 +4157,40 @@ def test_loglog(): ax.tick_params(length=15, width=2, which='minor') +@cleanup('default') +def test_axes_margins(): + fig, ax = plt.subplots() + ax.plot([0, 1, 2, 3]) + assert ax.get_ybound()[0] != 0 + + fig, ax = plt.subplots() + ax.bar([0, 1, 2, 3], [1, 1, 1, 1]) + assert ax.get_ybound()[0] == 0 + + fig, ax = plt.subplots() + ax.barh([0, 1, 2, 3], [1, 1, 1, 1]) + assert ax.get_xbound()[0] == 0 + + fig, ax = plt.subplots() + ax.pcolor(np.zeros((10, 10))) + assert ax.get_xbound() == (0, 10) + assert ax.get_ybound() == (0, 10) + + fig, ax = plt.subplots() + ax.pcolorfast(np.zeros((10, 10))) + assert ax.get_xbound() == (0, 10) + assert ax.get_ybound() == (0, 10) + + fig, ax = plt.subplots() + ax.hist(np.arange(10)) + assert ax.get_ybound()[0] == 0 + + fig, ax = plt.subplots() + ax.imshow(np.zeros((10, 10))) + assert ax.get_xbound() == (-0.5, 9.5) + assert ax.get_ybound() == (-0.5, 9.5) + + if __name__ == '__main__': import nose import sys diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index b6f8adc6f69d..eea7e852187c 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1207,13 +1207,13 @@ def view_limits(self, vmin, vmax): vmin -= 1 vmax += 1 - exponent, remainder = divmod(math.log10(vmax - vmin), 1) - - if remainder < 0.5: - exponent -= 1 - scale = 10 ** (-exponent) - vmin = math.floor(scale * vmin) / scale - vmax = math.ceil(scale * vmax) / scale + if rcParams['axes.autolimit_mode'] == 'round_numbers': + exponent, remainder = divmod(math.log10(vmax - vmin), 1) + if remainder < 0.5: + exponent -= 1 + scale = 10 ** (-exponent) + vmin = math.floor(scale * vmin) / scale + vmax = math.ceil(scale * vmax) / scale return mtransforms.nonsingular(vmin, vmax) @@ -1299,11 +1299,15 @@ def view_limits(self, dmin, dmax): Set the view limits to the nearest multiples of base that contain the data """ - vmin = self._base.le(dmin) - vmax = self._base.ge(dmax) - if vmin == vmax: - vmin -= 1 - vmax += 1 + if rcParams['axes.autolimit_mode'] == 'round_numbers': + vmin = self._base.le(dmin) + vmax = self._base.ge(dmax) + if vmin == vmax: + vmin -= 1 + vmax += 1 + else: + vmin = dmin + vmax = dmax return mtransforms.nonsingular(vmin, vmax) @@ -1453,13 +1457,19 @@ def tick_values(self, vmin, vmax): return self.raise_if_exceeds(locs) def view_limits(self, dmin, dmax): - if self._symmetric: - maxabs = max(abs(dmin), abs(dmax)) - dmin = -maxabs - dmax = maxabs + if rcParams['axes.autolimit_mode'] == 'round_numbers': + if self._symmetric: + maxabs = max(abs(dmin), abs(dmax)) + dmin = -maxabs + dmax = maxabs + dmin, dmax = mtransforms.nonsingular(dmin, dmax, expander=1e-12, tiny=1.e-13) - return np.take(self.bin_boundaries(dmin, dmax), [0, -1]) + + if rcParams['axes.autolimit_mode'] == 'round_numbers': + return np.take(self.bin_boundaries(dmin, dmax), [0, -1]) + else: + return dmin, dmax def decade_down(x, base=10): @@ -1616,24 +1626,26 @@ def view_limits(self, vmin, vmax): vmin = b ** (vmax - self.numdecs) return vmin, vmax - minpos = self.axis.get_minpos() + if rcParams['axes.autolimit_mode'] == 'round_numbers': + 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 minpos <= 0 or not np.isfinite(minpos): + raise ValueError( + "Data has no positive values, and therefore can not be " + "log-scaled.") - if vmin <= minpos: - vmin = minpos + if vmin <= minpos: + vmin = minpos - if not is_decade(vmin, self._base): - vmin = decade_down(vmin, self._base) - if not is_decade(vmax, self._base): - vmax = decade_up(vmax, self._base) + if not is_decade(vmin, self._base): + vmin = decade_down(vmin, self._base) + 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) - if vmin == vmax: - vmin = decade_down(vmin, self._base) - vmax = decade_up(vmax, self._base) result = mtransforms.nonsingular(vmin, vmax) return result @@ -1776,24 +1788,26 @@ def view_limits(self, vmin, vmax): if vmax < vmin: vmin, vmax = vmax, vmin - if not is_decade(abs(vmin), b): - if vmin < 0: - vmin = -decade_up(-vmin, b) - else: - vmin = decade_down(vmin, b) - if not is_decade(abs(vmax), b): - if vmax < 0: - vmax = -decade_down(-vmax, b) - else: - vmax = decade_up(vmax, b) + if rcParams['axes.autolimit_mode'] == 'round_numbers': + if not is_decade(abs(vmin), b): + if vmin < 0: + vmin = -decade_up(-vmin, b) + else: + vmin = decade_down(vmin, b) + if not is_decade(abs(vmax), b): + if vmax < 0: + vmax = -decade_down(-vmax, b) + else: + vmax = decade_up(vmax, b) + + if vmin == vmax: + if vmin < 0: + vmin = -decade_up(-vmin, b) + vmax = -decade_down(-vmax, b) + else: + vmin = decade_down(vmin, b) + vmax = decade_up(vmax, b) - if vmin == vmax: - if vmin < 0: - vmin = -decade_up(-vmin, b) - vmax = -decade_down(-vmax, b) - else: - vmin = decade_down(vmin, b) - vmax = decade_up(vmax, b) result = mtransforms.nonsingular(vmin, vmax) return result diff --git a/matplotlibrc.template b/matplotlibrc.template index b4e602b846c5..604b1036d911 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -319,8 +319,11 @@ backend : %(backend)s # as list of string colorspecs: # single letter, long name, or # web-style hex -#axes.xmargin : 0 # x margin. See `axes.Axes.margins` -#axes.ymargin : 0 # y margin See `axes.Axes.margins` +#axes.autolimit_mode : data # How to scale axes limits to the data. + # Use "data" to use data limits, plus some margin + # Use "round_number" move to the nearest "round" number +#axes.xmargin : .05 # x margin. See `axes.Axes.margins` +#axes.ymargin : .05 # y margin See `axes.Axes.margins` #polaraxes.grid : True # display grid on polar axes #axes3d.grid : True # display grid on 3d axes