diff --git a/.travis.yml b/.travis.yml index da3c6434a7aa..0a84c2d30d19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,4 +22,4 @@ install: script: - mkdir ../foo - cd ../foo - - python ../matplotlib/tests.py + - python ../matplotlib/tests.py -sv diff --git a/doc/users/whats_new.rst b/doc/users/whats_new.rst index 52573d38f554..2ac0a307f368 100644 --- a/doc/users/whats_new.rst +++ b/doc/users/whats_new.rst @@ -40,6 +40,13 @@ They may be symmetric or weighted. .. plot:: mpl_examples/pylab_examples/stackplot_demo2.py +Improved ``bbox_inches="tight"`` functionality +---------------------------------------------- +Passing ``bbox_inches="tight"`` through to :func:`plt.save` now takes into account +*all* artists on a figure - this was previously not the case and led to several +corner cases which did not function as expected. + + Remember save directory ----------------------- Martin Spacek made the save figure dialog remember the last directory saved diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 5880180ed5d3..8670d3da6530 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -180,6 +180,15 @@ def get_axes(self): """ return self.axes + def get_window_extent(self, renderer): + """ + Get the axes bounding box in display space. + Subclasses should override for inclusion in the bounding box + "tight" calculation. Default is to return an empty bounding + box at 0, 0. + """ + return Bbox([[0, 0], [0, 0]]) + def add_callback(self, func): """ Adds a callback function that will be called whenever one of diff --git a/lib/matplotlib/axes.py b/lib/matplotlib/axes.py index 1fa1413de1ec..2c06ff86f8c0 100644 --- a/lib/matplotlib/axes.py +++ b/lib/matplotlib/axes.py @@ -9076,13 +9076,8 @@ def matshow(self, Z, **kwargs): return im def get_default_bbox_extra_artists(self): - bbox_extra_artists = [t for t in self.texts if t.get_visible()] - if self.legend_: - bbox_extra_artists.append(self.legend_) - if self.tables: - for t in self.tables: - bbox_extra_artists.append(t) - return bbox_extra_artists + return [artist for artist in self.get_children() + if artist.get_visible()] def get_tightbbox(self, renderer, call_axes_locator=True): """ diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 377897470cb7..52752aa99825 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2094,15 +2094,26 @@ def print_figure(self, filename, dpi=None, facecolor='w', edgecolor='w', renderer = self.figure._cachedRenderer bbox_inches = self.figure.get_tightbbox(renderer) - bbox_extra_artists = kwargs.pop("bbox_extra_artists", None) - if bbox_extra_artists is None: - bbox_extra_artists = self.figure.get_default_bbox_extra_artists() + bbox_artists = kwargs.pop("bbox_extra_artists", None) + if bbox_artists is None: + bbox_artists = self.figure.get_default_bbox_extra_artists() + + bbox_filtered = [] + for a in bbox_artists: + bbox = a.get_window_extent(renderer) + if a.get_clip_on(): + clip_box = a.get_clip_box() + if clip_box is not None: + bbox = Bbox.intersection(bbox, clip_box) + clip_path = a.get_clip_path() + if clip_path is not None and bbox is not None: + clip_path = clip_path.get_fully_transformed_path() + bbox = Bbox.intersection(bbox, + clip_path.get_extents()) + if bbox is not None and (bbox.width != 0 or + bbox.height != 0): + bbox_filtered.append(bbox) - bb = [a.get_window_extent(renderer) - for a in bbox_extra_artists] - - bbox_filtered = [b for b in bb - if b.width != 0 or b.height != 0] if bbox_filtered: _bbox = Bbox.union(bbox_filtered) trans = Affine2D().scale(1.0 / self.figure.dpi) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 7862adfc9132..867306a4c873 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -193,10 +193,9 @@ def get_datalim(self, transData): return result def get_window_extent(self, renderer): - bbox = self.get_datalim(transforms.IdentityTransform()) - #TODO:check to ensure that this does not fail for - #cases other than scatter plot legend - return bbox + # TODO:check to ensure that this does not fail for + # cases other than scatter plot legend + return self.get_datalim(transforms.IdentityTransform()) def _prepare_points(self): """Point prep for drawing and hit testing""" diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 03a9966bb0d9..3b97ca45328d 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1504,11 +1504,14 @@ def waitforbuttonpress(self, timeout=-1): return blocking_input(timeout=timeout) def get_default_bbox_extra_artists(self): - bbox_extra_artists = [t for t in self.texts if t.get_visible()] + bbox_artists = [artist for artist in self.get_children() + if artist.get_visible()] for ax in self.axes: if ax.get_visible(): - bbox_extra_artists.extend(ax.get_default_bbox_extra_artists()) - return bbox_extra_artists + bbox_artists.extend(ax.get_default_bbox_extra_artists()) + # we don't want the figure's patch to influence the bbox calculation + bbox_artists.remove(self.patch) + return bbox_artists def get_tightbbox(self, renderer): """ diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 1face41eef41..7d4121dcdc62 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -374,10 +374,10 @@ def set_picker(self, p): self._picker = p def get_window_extent(self, renderer): - bbox = Bbox.unit() - bbox.update_from_data_xy( - self.get_transform().transform(self.get_xydata()), - ignore=True) + bbox = Bbox([[0, 0], [0, 0]]) + trans_data_to_xy = self.get_transform().transform + bbox.update_from_data_xy(trans_data_to_xy(self.get_xydata()), + ignore=True) # correct for marker size, if any if self._marker: ms = (self._markersize / 72.0 * self.figure.dpi) * 0.5 diff --git a/lib/matplotlib/table.py b/lib/matplotlib/table.py index b0036bc953c5..7d3a6813dee2 100644 --- a/lib/matplotlib/table.py +++ b/lib/matplotlib/table.py @@ -202,6 +202,8 @@ def __init__(self, ax, loc=None, bbox=None): self._autoColumns = [] self._autoFontsize = True + self.set_clip_on(False) + self._cachedRenderer = None def add_cell(self, row, col, *args, **kwargs): diff --git a/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_clipping.pdf b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_clipping.pdf new file mode 100644 index 000000000000..47214a646267 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_clipping.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_clipping.png b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_clipping.png new file mode 100644 index 000000000000..95682004be40 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_clipping.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_clipping.svg b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_clipping.svg new file mode 100644 index 000000000000..4fbc1e2de7f6 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_clipping.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_suptile_legend.pdf b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_suptile_legend.pdf new file mode 100644 index 000000000000..0b2bd88dbcd2 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_suptile_legend.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_suptile_legend.png b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_suptile_legend.png new file mode 100644 index 000000000000..0b25e78790c8 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_suptile_legend.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_suptile_legend.svg b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_suptile_legend.svg new file mode 100644 index 000000000000..655cd238927a --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_suptile_legend.svg @@ -0,0 +1,1193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/test_bbox_tight.py b/lib/matplotlib/tests/test_bbox_tight.py index d85a69fe3d7d..0b6ce233c061 100644 --- a/lib/matplotlib/tests/test_bbox_tight.py +++ b/lib/matplotlib/tests/test_bbox_tight.py @@ -1,6 +1,9 @@ from matplotlib import rcParams, rcParamsDefault from matplotlib.testing.decorators import image_comparison import matplotlib.pyplot as plt +import matplotlib.path as mpath +import matplotlib.patches as mpatches +from matplotlib.ticker import FuncFormatter import numpy as np @image_comparison(baseline_images=['bbox_inches_tight'], remove_text=True, @@ -36,6 +39,48 @@ def test_bbox_inches_tight(): rowLabels=rowLabels, colLabels=colLabels, loc='bottom') + +@image_comparison(baseline_images=['bbox_inches_tight_suptile_legend'], + remove_text=False, savefig_kwarg={'bbox_inches': 'tight'}) +def test_bbox_inches_tight_suptile_legend(): + plt.plot(range(10), label='a straight line') + plt.legend(bbox_to_anchor=(0.9, 1), loc=2, ) + plt.title('Axis title') + plt.suptitle('Figure title') + + # put an extra long y tick on to see that the bbox is accounted for + def y_formatter(y, pos): + if int(y) == 4: + return 'The number 4' + else: + return str(y) + plt.gca().yaxis.set_major_formatter(FuncFormatter(y_formatter)) + + plt.xlabel('X axis') + + +@image_comparison(baseline_images=['bbox_inches_tight_clipping'], + remove_text=True, savefig_kwarg={'bbox_inches': 'tight'}) +def test_bbox_inches_tight_clipping(): + # tests bbox clipping on scatter points, and path clipping on a patch + # to generate an appropriately tight bbox + plt.scatter(range(10), range(10)) + ax = plt.gca() + ax.set_xlim([0, 5]) + ax.set_ylim([0, 5]) + + # make a massive rectangle and clip it with a path + patch = mpatches.Rectangle([-50, -50], 100, 100, + transform=ax.transData, + facecolor='blue', alpha=0.5) + + path = mpath.Path.unit_regular_star(5) + path.vertices = path.vertices.copy() + path.vertices *= 0.25 + patch.set_clip_path(path, transform=ax.transAxes) + plt.gcf().artists.append(patch) + + if __name__ == '__main__': import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False) diff --git a/lib/matplotlib/tests/test_transforms.py b/lib/matplotlib/tests/test_transforms.py index bc8d5b93bd6b..1790c04ee9f8 100644 --- a/lib/matplotlib/tests/test_transforms.py +++ b/lib/matplotlib/tests/test_transforms.py @@ -444,6 +444,32 @@ def test_line_extents_for_non_affine_transData(self): expeted_data_lim) +def test_bbox_intersection(): + bbox_from_ext = mtrans.Bbox.from_extents + inter = mtrans.Bbox.intersection + + from numpy.testing import assert_array_equal as assert_a_equal + def assert_bbox_eq(bbox1, bbox2): + assert_a_equal(bbox1.bounds, bbox2.bounds) + + r1 = bbox_from_ext(0, 0, 1, 1) + r2 = bbox_from_ext(0.5, 0.5, 1.5, 1.5) + r3 = bbox_from_ext(0.5, 0, 0.75, 0.75) + r4 = bbox_from_ext(0.5, 1.5, 1, 2.5) + r5 = bbox_from_ext(1, 1, 2, 2) + + # self intersection -> no change + assert_bbox_eq(inter(r1, r1), r1) + # simple intersection + assert_bbox_eq(inter(r1, r2), bbox_from_ext(0.5, 0.5, 1, 1)) + # r3 contains r2 + assert_bbox_eq(inter(r1, r3), r3) + # no intersection + assert_equal(inter(r1, r4), None) + # single point + assert_bbox_eq(inter(r1, r5), bbox_from_ext(1, 1, 1, 1)) + + if __name__=='__main__': import nose nose.runmodule(argv=['-s','--with-doctest'], exit=False) diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index a3c3574eef1a..c95285cb11b0 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -732,6 +732,31 @@ def union(bboxes): return Bbox.from_extents(x0, y0, x1, y1) + @staticmethod + def intersection(bbox1, bbox2): + """ + Return the intersection of the two bboxes or None + if they do not intersect. + + Implements the algorithm described at: + + http://www.tekpool.com/node/2687 + + """ + intersects = not (bbox2.xmin > bbox1.xmax or + bbox2.xmax < bbox1.xmin or + bbox2.ymin > bbox1.ymax or + bbox2.ymax < bbox1.ymin) + + if intersects: + x0 = max([bbox1.xmin, bbox2.xmin]) + x1 = min([bbox1.xmax, bbox2.xmax]) + y0 = max([bbox1.ymin, bbox2.ymin]) + y1 = min([bbox1.ymax, bbox2.ymax]) + return Bbox.from_extents(x0, y0, x1, y1) + + return None + class Bbox(BboxBase): """