123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- from collections import namedtuple
- import io
- import numpy as np
- from numpy.testing import assert_allclose
- import pytest
- from matplotlib.testing.decorators import check_figures_equal, image_comparison
- import matplotlib.pyplot as plt
- import matplotlib.patches as mpatches
- import matplotlib.lines as mlines
- from matplotlib.backend_bases import MouseButton, MouseEvent
- from matplotlib.offsetbox import (
- AnchoredOffsetbox, AnnotationBbox, AnchoredText, DrawingArea, HPacker,
- OffsetBox, OffsetImage, PaddedBox, TextArea, VPacker, _get_packed_offsets)
- @image_comparison(['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)
- assert da.clip_children
- 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='center',
- 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))
- def test_offsetbox_clip_children():
- # - 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='center',
- 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)
- fig.canvas.draw()
- assert not fig.stale
- da.clip_children = True
- assert fig.stale
- def test_offsetbox_loc_codes():
- # Check that valid string location codes all work with an AnchoredOffsetbox
- codes = {'upper right': 1,
- 'upper left': 2,
- 'lower left': 3,
- 'lower right': 4,
- 'right': 5,
- 'center left': 6,
- 'center right': 7,
- 'lower center': 8,
- 'upper center': 9,
- 'center': 10,
- }
- fig, ax = plt.subplots()
- da = DrawingArea(100, 100)
- for code in codes:
- anchored_box = AnchoredOffsetbox(loc=code, child=da)
- ax.add_artist(anchored_box)
- fig.canvas.draw()
- def test_expand_with_tight_layout():
- # Check issue reported in #10476, and updated due to #10784
- fig, ax = plt.subplots()
- d1 = [1, 2]
- d2 = [2, 1]
- ax.plot(d1, label='series 1')
- ax.plot(d2, label='series 2')
- ax.legend(ncols=2, mode='expand')
- fig.tight_layout() # where the crash used to happen
- @pytest.mark.parametrize('widths',
- ([150], [150, 150, 150], [0.1], [0.1, 0.1]))
- @pytest.mark.parametrize('total', (250, 100, 0, -1, None))
- @pytest.mark.parametrize('sep', (250, 1, 0, -1))
- @pytest.mark.parametrize('mode', ("expand", "fixed", "equal"))
- def test_get_packed_offsets(widths, total, sep, mode):
- # Check a (rather arbitrary) set of parameters due to successive similar
- # issue tickets (at least #10476 and #10784) related to corner cases
- # triggered inside this function when calling higher-level functions
- # (e.g. `Axes.legend`).
- # These are just some additional smoke tests. The output is untested.
- _get_packed_offsets(widths, total, sep, mode=mode)
- _Params = namedtuple('_Params', 'wd_list, total, sep, expected')
- @pytest.mark.parametrize('widths, total, sep, expected', [
- _Params( # total=None
- [3, 1, 2], total=None, sep=1, expected=(8, [0, 4, 6])),
- _Params( # total larger than required
- [3, 1, 2], total=10, sep=1, expected=(10, [0, 4, 6])),
- _Params( # total smaller than required
- [3, 1, 2], total=5, sep=1, expected=(5, [0, 4, 6])),
- ])
- def test_get_packed_offsets_fixed(widths, total, sep, expected):
- result = _get_packed_offsets(widths, total, sep, mode='fixed')
- assert result[0] == expected[0]
- assert_allclose(result[1], expected[1])
- @pytest.mark.parametrize('widths, total, sep, expected', [
- _Params( # total=None (implicit 1)
- [.1, .1, .1], total=None, sep=None, expected=(1, [0, .45, .9])),
- _Params( # total larger than sum of widths
- [3, 1, 2], total=10, sep=1, expected=(10, [0, 5, 8])),
- _Params( # total smaller sum of widths: overlapping boxes
- [3, 1, 2], total=5, sep=1, expected=(5, [0, 2.5, 3])),
- ])
- def test_get_packed_offsets_expand(widths, total, sep, expected):
- result = _get_packed_offsets(widths, total, sep, mode='expand')
- assert result[0] == expected[0]
- assert_allclose(result[1], expected[1])
- @pytest.mark.parametrize('widths, total, sep, expected', [
- _Params( # total larger than required
- [3, 2, 1], total=6, sep=None, expected=(6, [0, 2, 4])),
- _Params( # total smaller sum of widths: overlapping boxes
- [3, 2, 1, .5], total=2, sep=None, expected=(2, [0, 0.5, 1, 1.5])),
- _Params( # total larger than required
- [.5, 1, .2], total=None, sep=1, expected=(6, [0, 2, 4])),
- # the case total=None, sep=None is tested separately below
- ])
- def test_get_packed_offsets_equal(widths, total, sep, expected):
- result = _get_packed_offsets(widths, total, sep, mode='equal')
- assert result[0] == expected[0]
- assert_allclose(result[1], expected[1])
- def test_get_packed_offsets_equal_total_none_sep_none():
- with pytest.raises(ValueError):
- _get_packed_offsets([1, 1, 1], total=None, sep=None, mode='equal')
- @pytest.mark.parametrize('child_type', ['draw', 'image', 'text'])
- @pytest.mark.parametrize('boxcoords',
- ['axes fraction', 'axes pixels', 'axes points',
- 'data'])
- def test_picking(child_type, boxcoords):
- # These all take up approximately the same area.
- if child_type == 'draw':
- picking_child = DrawingArea(5, 5)
- picking_child.add_artist(mpatches.Rectangle((0, 0), 5, 5, linewidth=0))
- elif child_type == 'image':
- im = np.ones((5, 5))
- im[2, 2] = 0
- picking_child = OffsetImage(im)
- elif child_type == 'text':
- picking_child = TextArea('\N{Black Square}', textprops={'fontsize': 5})
- else:
- assert False, f'Unknown picking child type {child_type}'
- fig, ax = plt.subplots()
- ab = AnnotationBbox(picking_child, (0.5, 0.5), boxcoords=boxcoords)
- ab.set_picker(True)
- ax.add_artist(ab)
- calls = []
- fig.canvas.mpl_connect('pick_event', lambda event: calls.append(event))
- # Annotation should be picked by an event occurring at its center.
- if boxcoords == 'axes points':
- x, y = ax.transAxes.transform_point((0, 0))
- x += 0.5 * fig.dpi / 72
- y += 0.5 * fig.dpi / 72
- elif boxcoords == 'axes pixels':
- x, y = ax.transAxes.transform_point((0, 0))
- x += 0.5
- y += 0.5
- else:
- x, y = ax.transAxes.transform_point((0.5, 0.5))
- fig.canvas.draw()
- calls.clear()
- MouseEvent(
- "button_press_event", fig.canvas, x, y, MouseButton.LEFT)._process()
- assert len(calls) == 1 and calls[0].artist == ab
- # Annotation should *not* be picked by an event at its original center
- # point when the limits have changed enough to hide the *xy* point.
- ax.set_xlim(-1, 0)
- ax.set_ylim(-1, 0)
- fig.canvas.draw()
- calls.clear()
- MouseEvent(
- "button_press_event", fig.canvas, x, y, MouseButton.LEFT)._process()
- assert len(calls) == 0
- @image_comparison(['anchoredtext_align.png'], remove_text=True, style='mpl20')
- def test_anchoredtext_horizontal_alignment():
- fig, ax = plt.subplots()
- text0 = AnchoredText("test\ntest long text", loc="center left",
- pad=0.2, prop={"ha": "left"})
- ax.add_artist(text0)
- text1 = AnchoredText("test\ntest long text", loc="center",
- pad=0.2, prop={"ha": "center"})
- ax.add_artist(text1)
- text2 = AnchoredText("test\ntest long text", loc="center right",
- pad=0.2, prop={"ha": "right"})
- ax.add_artist(text2)
- @pytest.mark.parametrize("extent_kind", ["window_extent", "tightbbox"])
- def test_annotationbbox_extents(extent_kind):
- plt.rcParams.update(plt.rcParamsDefault)
- fig, ax = plt.subplots(figsize=(4, 3), dpi=100)
- ax.axis([0, 1, 0, 1])
- an1 = ax.annotate("Annotation", xy=(.9, .9), xytext=(1.1, 1.1),
- arrowprops=dict(arrowstyle="->"), clip_on=False,
- va="baseline", ha="left")
- da = DrawingArea(20, 20, 0, 0, clip=True)
- p = mpatches.Circle((-10, 30), 32)
- da.add_artist(p)
- ab3 = AnnotationBbox(da, [.5, .5], xybox=(-0.2, 0.5), xycoords='data',
- boxcoords="axes fraction", box_alignment=(0., .5),
- arrowprops=dict(arrowstyle="->"))
- ax.add_artist(ab3)
- im = OffsetImage(np.random.rand(10, 10), zoom=3)
- im.image.axes = ax
- ab6 = AnnotationBbox(im, (0.5, -.3), xybox=(0, 75),
- xycoords='axes fraction',
- boxcoords="offset points", pad=0.3,
- arrowprops=dict(arrowstyle="->"))
- ax.add_artist(ab6)
- # Test Annotation
- bb1 = getattr(an1, f"get_{extent_kind}")()
- target1 = [332.9, 242.8, 467.0, 298.9]
- assert_allclose(bb1.extents, target1, atol=2)
- # Test AnnotationBbox
- bb3 = getattr(ab3, f"get_{extent_kind}")()
- target3 = [-17.6, 129.0, 200.7, 167.9]
- assert_allclose(bb3.extents, target3, atol=2)
- bb6 = getattr(ab6, f"get_{extent_kind}")()
- target6 = [180.0, -32.0, 230.0, 92.9]
- assert_allclose(bb6.extents, target6, atol=2)
- # Test bbox_inches='tight'
- buf = io.BytesIO()
- fig.savefig(buf, bbox_inches='tight')
- buf.seek(0)
- shape = plt.imread(buf).shape
- targetshape = (350, 504, 4)
- assert_allclose(shape, targetshape, atol=2)
- # Simple smoke test for tight_layout, to make sure it does not error out.
- fig.canvas.draw()
- fig.tight_layout()
- fig.canvas.draw()
- def test_zorder():
- assert OffsetBox(zorder=42).zorder == 42
- def test_arrowprops_copied():
- da = DrawingArea(20, 20, 0, 0, clip=True)
- arrowprops = {"arrowstyle": "->", "relpos": (.3, .7)}
- ab = AnnotationBbox(da, [.5, .5], xybox=(-0.2, 0.5), xycoords='data',
- boxcoords="axes fraction", box_alignment=(0., .5),
- arrowprops=arrowprops)
- assert ab.arrowprops is not ab
- assert arrowprops["relpos"] == (.3, .7)
- @pytest.mark.parametrize("align", ["baseline", "bottom", "top",
- "left", "right", "center"])
- def test_packers(align):
- # set the DPI to match points to make the math easier below
- fig = plt.figure(dpi=72)
- renderer = fig.canvas.get_renderer()
- x1, y1 = 10, 30
- x2, y2 = 20, 60
- r1 = DrawingArea(x1, y1)
- r2 = DrawingArea(x2, y2)
- # HPacker
- hpacker = HPacker(children=[r1, r2], align=align)
- hpacker.draw(renderer)
- bbox = hpacker.get_bbox(renderer)
- px, py = hpacker.get_offset(bbox, renderer)
- # width, height, xdescent, ydescent
- assert_allclose(bbox.bounds, (0, 0, x1 + x2, max(y1, y2)))
- # internal element placement
- if align in ("baseline", "left", "bottom"):
- y_height = 0
- elif align in ("right", "top"):
- y_height = y2 - y1
- elif align == "center":
- y_height = (y2 - y1) / 2
- # x-offsets, y-offsets
- assert_allclose([child.get_offset() for child in hpacker.get_children()],
- [(px, py + y_height), (px + x1, py)])
- # VPacker
- vpacker = VPacker(children=[r1, r2], align=align)
- vpacker.draw(renderer)
- bbox = vpacker.get_bbox(renderer)
- px, py = vpacker.get_offset(bbox, renderer)
- # width, height, xdescent, ydescent
- assert_allclose(bbox.bounds, (0, -max(y1, y2), max(x1, x2), y1 + y2))
- # internal element placement
- if align in ("baseline", "left", "bottom"):
- x_height = 0
- elif align in ("right", "top"):
- x_height = x2 - x1
- elif align == "center":
- x_height = (x2 - x1) / 2
- # x-offsets, y-offsets
- assert_allclose([child.get_offset() for child in vpacker.get_children()],
- [(px + x_height, py), (px, py - y2)])
- def test_paddedbox_default_values():
- # smoke test paddedbox for correct default value
- fig, ax = plt.subplots()
- at = AnchoredText("foo", 'upper left')
- pb = PaddedBox(at, patch_attrs={'facecolor': 'r'}, draw_frame=True)
- ax.add_artist(pb)
- fig.draw_without_rendering()
- def test_annotationbbox_properties():
- ab = AnnotationBbox(DrawingArea(20, 20, 0, 0, clip=True), (0.5, 0.5),
- xycoords='data')
- assert ab.xyann == (0.5, 0.5) # xy if xybox not given
- assert ab.anncoords == 'data' # xycoords if boxcoords not given
- ab = AnnotationBbox(DrawingArea(20, 20, 0, 0, clip=True), (0.5, 0.5),
- xybox=(-0.2, 0.4), xycoords='data',
- boxcoords='axes fraction')
- assert ab.xyann == (-0.2, 0.4) # xybox if given
- assert ab.anncoords == 'axes fraction' # boxcoords if given
- def test_textarea_properties():
- ta = TextArea('Foo')
- assert ta.get_text() == 'Foo'
- assert not ta.get_multilinebaseline()
- ta.set_text('Bar')
- ta.set_multilinebaseline(True)
- assert ta.get_text() == 'Bar'
- assert ta.get_multilinebaseline()
- @check_figures_equal()
- def test_textarea_set_text(fig_test, fig_ref):
- ax_ref = fig_ref.add_subplot()
- text0 = AnchoredText("Foo", "upper left")
- ax_ref.add_artist(text0)
- ax_test = fig_test.add_subplot()
- text1 = AnchoredText("Bar", "upper left")
- ax_test.add_artist(text1)
- text1.txt.set_text("Foo")
- @image_comparison(['paddedbox.png'], remove_text=True, style='mpl20')
- def test_paddedbox():
- fig, ax = plt.subplots()
- ta = TextArea("foo")
- pb = PaddedBox(ta, pad=5, patch_attrs={'facecolor': 'r'}, draw_frame=True)
- ab = AnchoredOffsetbox('upper left', child=pb)
- ax.add_artist(ab)
- ta = TextArea("bar")
- pb = PaddedBox(ta, pad=10, patch_attrs={'facecolor': 'b'})
- ab = AnchoredOffsetbox('upper right', child=pb)
- ax.add_artist(ab)
- ta = TextArea("foobar")
- pb = PaddedBox(ta, pad=15, draw_frame=True)
- ab = AnchoredOffsetbox('lower right', child=pb)
- ax.add_artist(ab)
- def test_remove_draggable():
- fig, ax = plt.subplots()
- an = ax.annotate("foo", (.5, .5))
- an.draggable(True)
- an.remove()
- MouseEvent("button_release_event", fig.canvas, 1, 1)._process()
|