Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ab8b1d7

Browse files
committed
Use display coordinates for RectangleSelector
Fix drawing a new box Fix minspan calculations in data coords Fix resize logic Save center on press for rotation PEP8 fix Update what's new
1 parent 923a90e commit ab8b1d7

File tree

4 files changed

+354
-277
lines changed

4 files changed

+354
-277
lines changed

doc/users/next_whats_new/selector_improvement.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ Selectors improvement: rotation, aspect ratio correction and add/remove state
22
-----------------------------------------------------------------------------
33

44
The `~matplotlib.widgets.RectangleSelector` and
5-
`~matplotlib.widgets.EllipseSelector` can now be rotated interactively between
6-
-45° and 45°. The range limits are currently dictated by the implementation.
5+
`~matplotlib.widgets.EllipseSelector` can now be rotated.
76
The rotation is enabled or disabled by striking the *r* key
87
('r' is the default key mapped to 'rotate' in *state_modifier_keys*) or by calling
98
``selector.add_state('rotate')``.

lib/matplotlib/patches.py

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -738,13 +738,6 @@ def __init__(self, xy, width, height, angle=0.0, *,
738738
self._height = height
739739
self.angle = float(angle)
740740
self.rotation_point = rotation_point
741-
# Required for RectangleSelector with axes aspect ratio != 1
742-
# The patch is defined in data coordinates and when changing the
743-
# selector with square modifier and not in data coordinates, we need
744-
# to correct for the aspect ratio difference between the data and
745-
# display coordinate systems. Its value is typically provide by
746-
# Axes._get_aspect_ratio()
747-
self._aspect_ratio_correction = 1.0
748741
self._convert_units() # Validate the inputs.
749742

750743
def get_path(self):
@@ -772,13 +765,11 @@ def get_patch_transform(self):
772765
rotation_point = bbox.x0, bbox.y0
773766
else:
774767
rotation_point = self.rotation_point
775-
return transforms.BboxTransformTo(bbox) \
776-
+ transforms.Affine2D() \
777-
.translate(-rotation_point[0], -rotation_point[1]) \
778-
.scale(1, self._aspect_ratio_correction) \
779-
.rotate_deg(self.angle) \
780-
.scale(1, 1 / self._aspect_ratio_correction) \
781-
.translate(*rotation_point)
768+
return (transforms.BboxTransformTo(bbox) +
769+
transforms.Affine2D()
770+
.translate(-rotation_point[0], -rotation_point[1])
771+
.rotate_deg(self.angle)
772+
.translate(*rotation_point))
782773

783774
@property
784775
def rotation_point(self):
@@ -1573,12 +1564,6 @@ def __init__(self, xy, width, height, angle=0, **kwargs):
15731564
self._width, self._height = width, height
15741565
self._angle = angle
15751566
self._path = Path.unit_circle()
1576-
# Required for EllipseSelector with axes aspect ratio != 1
1577-
# The patch is defined in data coordinates and when changing the
1578-
# selector with square modifier and not in data coordinates, we need
1579-
# to correct for the aspect ratio difference between the data and
1580-
# display coordinate systems.
1581-
self._aspect_ratio_correction = 1.0
15821567
# Note: This cannot be calculated until this is added to an Axes
15831568
self._patch_transform = transforms.IdentityTransform()
15841569

@@ -1596,9 +1581,8 @@ def _recompute_transform(self):
15961581
width = self.convert_xunits(self._width)
15971582
height = self.convert_yunits(self._height)
15981583
self._patch_transform = transforms.Affine2D() \
1599-
.scale(width * 0.5, height * 0.5 * self._aspect_ratio_correction) \
1584+
.scale(width * 0.5, height * 0.5) \
16001585
.rotate_deg(self.angle) \
1601-
.scale(1, 1 / self._aspect_ratio_correction) \
16021586
.translate(*center)
16031587

16041588
def get_path(self):

lib/matplotlib/tests/test_widgets.py

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,19 @@ def onselect(epress, erelease):
3232
ax._got_onselect = True
3333
assert epress.xdata == 100
3434
assert epress.ydata == 100
35-
assert erelease.xdata == 199
36-
assert erelease.ydata == 199
35+
assert erelease.xdata == 200
36+
assert erelease.ydata == 200
3737

3838
tool = widgets.RectangleSelector(ax, onselect, **kwargs)
3939
do_event(tool, 'press', xdata=100, ydata=100, button=1)
40-
do_event(tool, 'onmove', xdata=199, ydata=199, button=1)
41-
40+
do_event(tool, 'onmove', xdata=250, ydata=250, button=1)
4241
# purposely drag outside of axis for release
4342
do_event(tool, 'release', xdata=250, ydata=250, button=1)
4443

4544
if kwargs.get('drawtype', None) not in ['line', 'none']:
4645
assert_allclose(tool.geometry,
47-
[[100., 100, 199, 199, 100],
48-
[100, 199, 199, 100, 100]],
46+
[[100., 100, 200, 200, 100],
47+
[100, 200, 200, 100, 100]],
4948
err_msg=tool.geometry)
5049

5150
assert ax._got_onselect
@@ -280,16 +279,16 @@ def test_rectangle_resize_center(ax, add_state):
280279
xdata_new, ydata_new = xdata + xdiff, ydata + ydiff
281280
click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
282281
key=use_key)
283-
assert tool.extents == (xdata_new, extents[1] - xdiff,
284-
ydata_new, extents[3] - ydiff)
282+
assert_allclose(tool.extents, (xdata_new, extents[1] - xdiff,
283+
ydata_new, extents[3] - ydiff))
285284

286285

287286
@pytest.mark.parametrize('add_state', [True, False])
288287
def test_rectangle_resize_square(ax, add_state):
289288
tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True)
290289
# Create rectangle
291290
click_and_drag(tool, start=(70, 65), end=(120, 115))
292-
assert tool.extents == (70.0, 120.0, 65.0, 115.0)
291+
assert_allclose(tool.extents, (70.0, 120.0, 65.0, 115.0))
293292

294293
if add_state:
295294
tool.add_state('square')
@@ -304,8 +303,8 @@ def test_rectangle_resize_square(ax, add_state):
304303
xdata_new, ydata_new = xdata + xdiff, ydata + ydiff
305304
click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
306305
key=use_key)
307-
assert tool.extents == (extents[0], xdata_new,
308-
extents[2], extents[3] + xdiff)
306+
assert_allclose(tool.extents, (extents[0], xdata_new,
307+
extents[2], extents[3] + xdiff))
309308

310309
# resize E handle
311310
extents = tool.extents
@@ -354,11 +353,12 @@ def test_rectangle_resize_square(ax, add_state):
354353
xdata_new, ydata_new = xdata + xdiff, ydata + ydiff
355354
click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new),
356355
key=use_key)
357-
assert_allclose(tool.extents, (extents[0] + ydiff, extents[1],
358-
ydata_new, extents[3]))
356+
assert_allclose(tool.extents, (xdata_new, extents[1],
357+
extents[2] + xdiff, extents[3]))
359358

360359

361360
def test_rectangle_resize_square_center(ax):
361+
ax.set_aspect(1)
362362
tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True)
363363
# Create rectangle
364364
click_and_drag(tool, start=(70, 65), end=(120, 115))
@@ -424,6 +424,7 @@ def test_rectangle_resize_square_center(ax):
424424
@pytest.mark.parametrize('selector_class',
425425
[widgets.RectangleSelector, widgets.EllipseSelector])
426426
def test_rectangle_rotate(ax, selector_class):
427+
ax.set_aspect(1)
427428
tool = selector_class(ax, onselect=noop, interactive=True)
428429
# Draw rectangle
429430
click_and_drag(tool, start=(100, 100), end=(130, 140))
@@ -437,19 +438,19 @@ def test_rectangle_rotate(ax, selector_class):
437438
click_and_drag(tool, start=(130, 140), end=(120, 145))
438439
do_event(tool, 'on_key_press', key='r')
439440
assert len(tool._state) == 0
440-
# Extents shouldn't change (as shape of rectangle hasn't changed)
441-
assert tool.extents == (100, 130, 100, 140)
441+
# Extents change as the selector remains rigid in display coordinates
442+
assert_allclose(tool.extents, (110.10, 119.90, 95.49, 144.51), atol=0.01)
442443
assert_allclose(tool.rotation, 25.56, atol=0.01)
443444
tool.rotation = 45
444445
assert tool.rotation == 45
445446
# Corners should move
446447
assert_allclose(tool.corners,
447-
np.array([[118.53, 139.75, 111.46, 90.25],
448-
[95.25, 116.46, 144.75, 123.54]]), atol=0.01)
448+
np.array([[110.10, 131.31, 103.03, 81.81],
449+
[95.49, 116.70, 144.98, 123.77]]), atol=0.01)
449450

450451
# Scale using top-right corner
451452
click_and_drag(tool, start=(110, 145), end=(110, 160))
452-
assert_allclose(tool.extents, (100, 139.75, 100, 151.82), atol=0.01)
453+
assert_allclose(tool.extents, (110, 110, 145, 160), atol=0.01)
453454

454455
if selector_class == widgets.RectangleSelector:
455456
with pytest.raises(ValueError):
@@ -471,36 +472,38 @@ def test_rectange_add_remove_set(ax):
471472

472473
@pytest.mark.parametrize('use_data_coordinates', [False, True])
473474
def test_rectangle_resize_square_center_aspect(ax, use_data_coordinates):
474-
ax.set_aspect(0.8)
475+
ax = get_ax()
476+
ax.set_aspect(0.5)
477+
# Need to call a draw to update ax.transData
478+
plt.gcf().canvas.draw()
475479

476480
tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True,
477481
use_data_coordinates=use_data_coordinates)
478-
# Create rectangle
479-
click_and_drag(tool, start=(70, 65), end=(120, 115))
480-
assert_allclose(tool.extents, (70.0, 120.0, 65.0, 115.0))
481482
tool.add_state('square')
482483
tool.add_state('center')
484+
# Create rectangle, width 50 in data coordinates
485+
click_and_drag(tool, start=(70, 65), end=(120, 65))
483486

484487
if use_data_coordinates:
485-
# resize E handle
486-
extents = tool.extents
487-
xdata, ydata, width = extents[1], extents[3], extents[1] - extents[0]
488-
xdiff, ycenter = 10, extents[2] + (extents[3] - extents[2]) / 2
489-
xdata_new, ydata_new = xdata + xdiff, ydata
490-
ychange = width / 2 + xdiff
491-
click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
492-
assert_allclose(tool.extents, [extents[0] - xdiff, xdata_new,
493-
ycenter - ychange, ycenter + ychange])
488+
assert_allclose(tool.extents, (20, 120, 15, 115))
494489
else:
495-
# resize E handle
496-
extents = tool.extents
497-
xdata, ydata = extents[1], extents[3]
498-
xdiff = 10
499-
xdata_new, ydata_new = xdata + xdiff, ydata
500-
ychange = xdiff * 1 / tool._aspect_ratio_correction
501-
click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
502-
assert_allclose(tool.extents, [extents[0] - xdiff, xdata_new,
503-
46.25, 133.75])
490+
assert_allclose(tool.extents, (20, 120, -35, 165))
491+
492+
# resize E handle
493+
extents = tool.extents
494+
xdata, ydata = extents[1], extents[3]
495+
xdiff = 10
496+
xdata_new, ydata_new = xdata + xdiff, ydata
497+
if use_data_coordinates:
498+
# In data coordinates the difference should be equal in both directions
499+
ydiff = xdiff
500+
else:
501+
# In display coordiantes, the change in data coordinates should be
502+
# different in each direction
503+
ydiff = xdiff / tool.ax._get_aspect_ratio()
504+
click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new))
505+
assert_allclose(tool.extents, [extents[0] - xdiff, xdata_new,
506+
extents[2] - ydiff, extents[3] + ydiff])
504507

505508

506509
def test_ellipse(ax):
@@ -1409,6 +1412,14 @@ def test_polygon_selector_box(ax):
14091412
polygon_place_vertex(*verts[3]) +
14101413
polygon_place_vertex(*verts[0]))
14111414

1415+
# Set smaller axes limits to reduce errors in converting from data to
1416+
# display coords. The canvas size is 640 x 640, so we need a tolerance of
1417+
# (data width / canvas width) = 50 / 640 ~ 0.08 when comparing points in
1418+
# data space
1419+
ax.set_xlim(0, 50)
1420+
ax.set_ylim(0, 50)
1421+
atol = 0.08
1422+
14121423
# Create selector
14131424
tool = widgets.PolygonSelector(ax, onselect=noop, draw_bounding_box=True)
14141425
for (etype, event_args) in event_sequence:
@@ -1424,25 +1435,25 @@ def test_polygon_selector_box(ax):
14241435
canvas.motion_notify_event(*t.transform((20, 20)))
14251436
canvas.button_release_event(*t.transform((20, 20)), 1)
14261437
np.testing.assert_allclose(
1427-
tool.verts, [(10, 0), (0, 10), (10, 20), (20, 10)])
1438+
tool.verts, [(10, 0), (0, 10), (10, 20), (20, 10)], atol=atol)
14281439

14291440
# Move using the center of the bounding box
14301441
canvas.button_press_event(*t.transform((10, 10)), 1)
14311442
canvas.motion_notify_event(*t.transform((30, 30)))
14321443
canvas.button_release_event(*t.transform((30, 30)), 1)
14331444
np.testing.assert_allclose(
1434-
tool.verts, [(30, 20), (20, 30), (30, 40), (40, 30)])
1445+
tool.verts, [(30, 20), (20, 30), (30, 40), (40, 30)], atol=atol)
14351446

14361447
# Remove a point from the polygon and check that the box extents update
14371448
np.testing.assert_allclose(
1438-
tool._box.extents, (20.0, 40.0, 20.0, 40.0))
1449+
tool._box.extents, (20.0, 40.0, 20.0, 40.0), atol=atol)
14391450

14401451
canvas.button_press_event(*t.transform((30, 20)), 3)
14411452
canvas.button_release_event(*t.transform((30, 20)), 3)
14421453
np.testing.assert_allclose(
1443-
tool.verts, [(20, 30), (30, 40), (40, 30)])
1454+
tool.verts, [(20, 30), (30, 40), (40, 30)], atol=atol)
14441455
np.testing.assert_allclose(
1445-
tool._box.extents, (20.0, 40.0, 30.0, 40.0))
1456+
tool._box.extents, (20.0, 40.0, 30.0, 40.0), atol=atol)
14461457

14471458

14481459
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)