forked from matplotlib/matplotlib
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_offsetbox.py
More file actions
472 lines (391 loc) · 16.2 KB
/
test_offsetbox.py
File metadata and controls
472 lines (391 loc) · 16.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
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()
def test_draggable_in_subfigure():
fig = plt.figure()
# Put annotation at lower left corner to make it easily pickable below.
ann = fig.subfigures().add_axes((0, 0, 1, 1)).annotate("foo", (0, 0))
ann.draggable(True)
fig.canvas.draw() # Texts are non-pickable until the first draw.
MouseEvent("button_press_event", fig.canvas, 1, 1)._process()
assert ann._draggable.got_artist
# Stop dragging the annotation.
MouseEvent("button_release_event", fig.canvas, 1, 1)._process()
assert not ann._draggable.got_artist
# A scroll event should not initiate a drag.
MouseEvent("scroll_event", fig.canvas, 1, 1)._process()
assert not ann._draggable.got_artist
# An event outside the annotation should not initiate a drag.
bbox = ann.get_window_extent()
MouseEvent("button_press_event", fig.canvas, bbox.x1+2, bbox.y1+2)._process()
assert not ann._draggable.got_artist