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 42c7dc93b08c..ca35346a16f9 100644
--- a/lib/matplotlib/axes.py
+++ b/lib/matplotlib/axes.py
@@ -9072,13 +9072,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 abcf66c790ad..4d3df1f385d5 100644
--- a/lib/matplotlib/backend_bases.py
+++ b/lib/matplotlib/backend_bases.py
@@ -2092,15 +2092,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 b30e89111580..6a080eaa4e65 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):
"""