diff --git a/doc/api/api_changes/2015-04-18-drawingarea.rst b/doc/api/api_changes/2015-04-18-drawingarea.rst new file mode 100644 index 000000000000..c977dd93e01c --- /dev/null +++ b/doc/api/api_changes/2015-04-18-drawingarea.rst @@ -0,0 +1,8 @@ +'OffsetBox.DrawingArea' respects the 'clip' keyword argument +```````````````````````````````````````````````````````````` + +The call signature was `OffsetBox.DrawingArea(..., clip=True)` but nothing +was done with the `clip` argument. The object did not do any clipping +regardless of that parameter. Now the object can and does clip the child `Artists` if they are set to be clipped. + +You can turn off the clipping on a per-child basis using `child.set_clip_on(False)`. diff --git a/doc/users/whats_new/offsetbox.rst b/doc/users/whats_new/offsetbox.rst new file mode 100644 index 000000000000..c1d16eb1dbb4 --- /dev/null +++ b/doc/users/whats_new/offsetbox.rst @@ -0,0 +1,11 @@ +OffsetBoxes now support clipping +```````````````````````````````` + +`Artists` draw onto objects of type :class:`~OffsetBox` +through :class:`~OffsetBox.DrawingArea` and :class:`~OffsetBox.TextArea`. +The `TextArea` calculates the required space for the text and so the +text is always within the bounds, for this nothing has changed. + +However, `DrawingArea` acts as a parent for zero or more `Artists` that +draw on it and may do so beyond the bounds. Now child `Artists` can be +clipped to the bounds of the `DrawingArea`. diff --git a/examples/pylab_examples/anchored_artists.py b/examples/pylab_examples/anchored_artists.py index f8600d0cfec4..af4585f819fe 100644 --- a/examples/pylab_examples/anchored_artists.py +++ b/examples/pylab_examples/anchored_artists.py @@ -63,7 +63,7 @@ class AnchoredDrawingArea(AnchoredOffsetbox): def __init__(self, width, height, xdescent, ydescent, loc, pad=0.4, borderpad=0.5, prop=None, frameon=True): - self.da = DrawingArea(width, height, xdescent, ydescent, clip=True) + self.da = DrawingArea(width, height, xdescent, ydescent) super(AnchoredDrawingArea, self).__init__(loc, pad=pad, borderpad=borderpad, child=self.da, diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index c2cf103d53ca..814d73290f37 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1416,6 +1416,7 @@ def tk_window_focus(): 'matplotlib.tests.test_lines', 'matplotlib.tests.test_mathtext', 'matplotlib.tests.test_mlab', + 'matplotlib.tests.test_offsetbox', 'matplotlib.tests.test_patches', 'matplotlib.tests.test_path', 'matplotlib.tests.test_patheffects', diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 47a402911b37..691076c42ae0 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -24,6 +24,7 @@ import matplotlib.transforms as mtransforms import matplotlib.artist as martist import matplotlib.text as mtext +import matplotlib.path as mpath import numpy as np from matplotlib.transforms import Bbox, BboxBase, TransformedBbox @@ -569,14 +570,16 @@ class DrawingArea(OffsetBox): """ The DrawingArea can contain any Artist as a child. The DrawingArea has a fixed width and height. The position of children relative to - the parent is fixed. + the parent is fixed. The children can be clipped at the + boundaries of the parent. """ def __init__(self, width, height, xdescent=0., - ydescent=0., clip=True): + ydescent=0., clip=False): """ *width*, *height* : width and height of the container box. *xdescent*, *ydescent* : descent of the box in x- and y-direction. + *clip* : Whether to clip the children """ super(DrawingArea, self).__init__() @@ -585,6 +588,7 @@ def __init__(self, width, height, xdescent=0., self.height = height self.xdescent = xdescent self.ydescent = ydescent + self._clip_children = clip self.offset_transform = mtransforms.Affine2D() self.offset_transform.clear() @@ -656,7 +660,17 @@ def draw(self, renderer): self.dpi_transform.clear() self.dpi_transform.scale(dpi_cor, dpi_cor) + # At this point the DrawingArea has a transform + # to the display space so the path created is + # good for clipping children + tpath = mtransforms.TransformedPath( + mpath.Path([[0, 0], [0, self.height], + [self.width, self.height], + [self.width, 0]]), + self.get_transform()) for c in self._children: + if self._clip_children and not (c.clipbox or c._clippath): + c.set_clip_path(tpath) c.draw(renderer) bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) diff --git a/lib/matplotlib/tests/baseline_images/test_offsetbox/offsetbox_clipping.pdf b/lib/matplotlib/tests/baseline_images/test_offsetbox/offsetbox_clipping.pdf new file mode 100644 index 000000000000..18e1169d303f Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_offsetbox/offsetbox_clipping.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_offsetbox/offsetbox_clipping.png b/lib/matplotlib/tests/baseline_images/test_offsetbox/offsetbox_clipping.png new file mode 100644 index 000000000000..3b62fc31883b Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_offsetbox/offsetbox_clipping.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_offsetbox/offsetbox_clipping.svg b/lib/matplotlib/tests/baseline_images/test_offsetbox/offsetbox_clipping.svg new file mode 100644 index 000000000000..dda77394fef2 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_offsetbox/offsetbox_clipping.svg @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/test_offsetbox.py b/lib/matplotlib/tests/test_offsetbox.py new file mode 100644 index 000000000000..1c421d4c0b1c --- /dev/null +++ b/lib/matplotlib/tests/test_offsetbox.py @@ -0,0 +1,49 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import nose + +from matplotlib.testing.decorators import image_comparison +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches +import matplotlib.lines as mlines +from matplotlib.offsetbox import AnchoredOffsetbox, DrawingArea + + +@image_comparison(baseline_images=['offsetbox_clipping'], remove_text=True) +def test_offsetbox_clipping(): + # - create a plot + # - put an AnchoredOffsetbox with a child DrawingArea + # at the center of the axes + # - give the DrawingArea a gray background + # - put a black line across the bounds of the DrawingArea + # - see that the black line is clipped to the edges of + # the DrawingArea. + fig, ax = plt.subplots() + size = 100 + da = DrawingArea(size, size, clip=True) + bg = mpatches.Rectangle((0, 0), size, size, + facecolor='#CCCCCC', + edgecolor='None', + linewidth=0) + line = mlines.Line2D([-size*.5, size*1.5], [size/2, size/2], + color='black', + linewidth=10) + anchored_box = AnchoredOffsetbox( + loc=10, + child=da, + pad=0., + frameon=False, + bbox_to_anchor=(.5, .5), + bbox_transform=ax.transAxes, + borderpad=0.) + + da.add_artist(bg) + da.add_artist(line) + ax.add_artist(anchored_box) + ax.set_xlim((0, 1)) + ax.set_ylim((0, 1)) + + +if __name__ == '__main__': + nose.runmodule(argv=['-s', '--with-doctest'], exit=False) diff --git a/lib/mpl_toolkits/axes_grid1/anchored_artists.py b/lib/mpl_toolkits/axes_grid1/anchored_artists.py index bfcfeea344a7..17881eba5a55 100644 --- a/lib/mpl_toolkits/axes_grid1/anchored_artists.py +++ b/lib/mpl_toolkits/axes_grid1/anchored_artists.py @@ -24,7 +24,7 @@ def __init__(self, width, height, xdescent, ydescent, *prop* : font property. This is only used for scaling the paddings. """ - self.da = DrawingArea(width, height, xdescent, ydescent, clip=True) + self.da = DrawingArea(width, height, xdescent, ydescent) self.drawing_area = self.da super(AnchoredDrawingArea, self).__init__(loc, pad=pad, borderpad=borderpad,