Skip to content
2 changes: 1 addition & 1 deletion docs/masks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,6 @@ and averaged flux, which is updated live in the text field of the plot as well.

ellipse = EllipsePixelRegion(center=PixCoord(x=126, y=1031), width=8, height=4,
angle=-0*u.deg, visual={'color': 'yellow'})
selector = ellipse.as_mpl_selector(ax, callback=update_sel, use_data_coordinates=True)
selector = ellipse.as_mpl_selector(ax, callback=update_sel)

hdulist.close()
29 changes: 19 additions & 10 deletions regions/shapes/ellipse.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ def as_mpl_selector(self, ax, active=True, sync=True, callback=None,
region is updated. This only has an effect if ``sync`` is
`True`. If a callback is set, it is called for the first
time once the selector has been created.
drag_from_anywhere : bool, optional
If `True`, the selector can be moved by clicking anywhere within
its bounds, else only at the central anchor
(only available with matplotlib 3.5 upwards; default: `False`).
**kwargs : dict
Additional keyword arguments that are passed to
`matplotlib.widgets.EllipseSelector`.
Expand All @@ -258,27 +262,32 @@ def as_mpl_selector(self, ax, active=True, sync=True, callback=None,
``selector.set_active(True)`` or ``selector.set_active(False)``.
"""
from matplotlib.widgets import EllipseSelector
from .utils import MPL_VERSION

if hasattr(self, '_mpl_selector'):
raise Exception('Cannot attach more than one selector to a '
'region.')
raise AttributeError('Cannot attach more than one selector to a region.')

if self.angle.value != 0:
raise NotImplementedError('Cannot create matplotlib selector for '
'rotated ellipse.')
raise NotImplementedError('Cannot create matplotlib selector for rotated ellipse.')

if sync:
sync_callback = self._update_from_mpl_selector
else:
def sync_callback(*args, **kwargs):
pass

self._mpl_selector = EllipseSelector(
ax, sync_callback, interactive=True,
rectprops={'edgecolor': self.visual.get('color', 'black'),
'facecolor': 'none',
'linewidth': self.visual.get('linewidth', 1),
'linestyle': self.visual.get('linestyle', 'solid')})
rectprops = {'edgecolor': self.visual.get('color', 'black'),
'facecolor': 'none',
'linewidth': self.visual.get('linewidth', 1),
'linestyle': self.visual.get('linestyle', 'solid')}
rectprops.update(kwargs.pop('props', dict()))
# `rectprops` renamed `props` in mpl 3.5 and deprecated for 3.7.
if MPL_VERSION < 35:
kwargs.update({'rectprops': rectprops})
else:
kwargs.update({'props': rectprops})

self._mpl_selector = EllipseSelector(ax, sync_callback, interactive=True, **kwargs)

self._mpl_selector.extents = (self.center.x - self.width / 2,
self.center.x + self.width / 2,
Expand Down
26 changes: 18 additions & 8 deletions regions/shapes/rectangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ def as_mpl_selector(self, ax, active=True, sync=True, callback=None,
region is updated. This only has an effect if ``sync`` is
`True`. If a callback is set, it is called for the first
time once the selector has been created.
drag_from_anywhere : bool, optional
If `True`, the selector can be moved by clicking anywhere within
its bounds, else only at the central anchor
(only available with matplotlib 3.5 upwards; default: `False`).
**kwargs : dict
Additional keyword arguments are passed to
`matplotlib.widgets.RectangleSelector`.
Expand All @@ -251,10 +255,10 @@ def as_mpl_selector(self, ax, active=True, sync=True, callback=None,
``selector.set_active(True)`` or ``selector.set_active(False)``.
"""
from matplotlib.widgets import RectangleSelector
from .utils import MPL_VERSION

if hasattr(self, '_mpl_selector'):
raise Exception('Cannot attach more than one selector to a '
'region.')
raise AttributeError('Cannot attach more than one selector to a region.')

if self.angle.value != 0:
raise NotImplementedError('Cannot create matplotlib selector for '
Expand All @@ -266,12 +270,18 @@ def as_mpl_selector(self, ax, active=True, sync=True, callback=None,
def sync_callback(*args, **kwargs):
pass

self._mpl_selector = RectangleSelector(
ax, sync_callback, interactive=True,
rectprops={'edgecolor': self.visual.get('color', 'black'),
'facecolor': 'none',
'linewidth': self.visual.get('linewidth', 1),
'linestyle': self.visual.get('linestyle', 'solid')})
rectprops = {'edgecolor': self.visual.get('color', 'black'),
'facecolor': 'none',
'linewidth': self.visual.get('linewidth', 1),
'linestyle': self.visual.get('linestyle', 'solid')}
rectprops.update(kwargs.pop('props', dict()))
# `rectprops` renamed `props` in mpl 3.5 and deprecated for 3.7.
if MPL_VERSION < 35:
kwargs.update({'rectprops': rectprops})
else:
kwargs.update({'props': rectprops})

self._mpl_selector = RectangleSelector(ax, sync_callback, interactive=True, **kwargs)

self._mpl_selector.extents = (self.center.x - self.width / 2,
self.center.x + self.width / 2,
Expand Down
2 changes: 1 addition & 1 deletion regions/shapes/tests/test_circle.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ...tests.helpers import make_simple_wcs
from ..circle import CirclePixelRegion, CircleSkyRegion
from .test_common import BaseTestPixelRegion, BaseTestSkyRegion
from .utils import HAS_MATPLOTLIB # noqa
from ..utils import HAS_MATPLOTLIB # noqa


@pytest.fixture(scope='session', name='wcs')
Expand Down
141 changes: 111 additions & 30 deletions regions/shapes/tests/test_ellipse.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from ...tests.helpers import make_simple_wcs
from ..ellipse import EllipsePixelRegion, EllipseSkyRegion
from .test_common import BaseTestPixelRegion, BaseTestSkyRegion
from .utils import HAS_MATPLOTLIB # noqa
from ..utils import HAS_MATPLOTLIB, MPL_VERSION # noqa


@pytest.fixture(scope='session', name='wcs')
Expand Down Expand Up @@ -100,13 +100,12 @@ def test_rotate(self):
assert_allclose(reg.center.xy, (1, 4))
assert_allclose(reg.angle.to_value("deg"), 95)

# TODO: Is this MatplotlibDeprecationWarning something to worry about?
@pytest.mark.filterwarnings(r"ignore:The 'rectprops' parameter of "
r"__init__\(\) has been renamed 'props'")
@pytest.mark.skipif(MPL_VERSION < 33, reason='requires `do_event`')
@pytest.mark.parametrize('sync', (False, True))
def test_as_mpl_selector(self, sync):

plt = pytest.importorskip('matplotlib.pyplot')
from matplotlib.testing.widgets import do_event

data = np.random.random((16, 16))
mask = np.zeros_like(data)
Expand All @@ -115,39 +114,23 @@ def test_as_mpl_selector(self, sync):
ax.imshow(data)

def update_mask(reg):
mask[:] = reg.to_mask(mode='subpixels',
subpixels=10).to_image(data.shape)
mask[:] = reg.to_mask(mode='subpixels', subpixels=10).to_image(data.shape)

# For now this will only work with unrotated ellipses. Once this
# works with rotated ellipses, the following exception check can
# be removed as well as the ``angle=0 * u.deg`` in the call to
# copy() below.
with pytest.raises(NotImplementedError,
match=('Cannot create matplotlib selector for '
'rotated ellipse.')):
match=('Cannot create matplotlib selector for rotated ellipse.')):
self.reg.as_mpl_selector(ax)

region = self.reg.copy(angle=0 * u.deg)

selector = region.as_mpl_selector(ax, callback=update_mask, sync=sync) # noqa

from matplotlib.backend_bases import MouseEvent, MouseButton

x, y = ax.transData.transform([[7.3, 4.4]])[0]
ax.figure.canvas.callbacks.process('button_press_event',
MouseEvent('button_press_event',
ax.figure.canvas, x, y,
button=MouseButton.LEFT))
x, y = ax.transData.transform([[9.3, 5.4]])[0]
ax.figure.canvas.callbacks.process('motion_notify_event',
MouseEvent('button_press_event',
ax.figure.canvas, x, y,
button=MouseButton.LEFT))
x, y = ax.transData.transform([[9.3, 5.4]])[0]
ax.figure.canvas.callbacks.process('button_release_event',
MouseEvent('button_press_event',
ax.figure.canvas, x, y,
button=MouseButton.LEFT))
do_event(selector, 'press', xdata=7.3, ydata=4.4, button=1)
do_event(selector, 'onmove', xdata=9.3, ydata=5.4, button=1)
do_event(selector, 'release', xdata=9.3, ydata=5.4, button=1)

ax.figure.canvas.draw()

Expand All @@ -159,9 +142,7 @@ def update_mask(reg):
assert_allclose(region.height, 1)
assert_quantity_allclose(region.angle, 0 * u.deg)

assert_equal(mask,
region.to_mask(mode='subpixels',
subpixels=10).to_image(data.shape))
assert_equal(mask, region.to_mask(mode='subpixels', subpixels=10).to_image(data.shape))

else:

Expand All @@ -173,10 +154,110 @@ def update_mask(reg):

assert_equal(mask, 0)

with pytest.raises(Exception, match=('Cannot attach more than one '
'selector to a region.')):
with pytest.raises(AttributeError, match=('Cannot attach more than one selector to a reg')):
region.as_mpl_selector(ax)

@pytest.mark.skipif(MPL_VERSION < 33, reason='requires `do_event`')
@pytest.mark.parametrize('anywhere', (False, True))
def test_mpl_selector_drag(self, anywhere):
"""Test dragging of entire region from central handle and anywhere."""

plt = pytest.importorskip('matplotlib.pyplot')
from matplotlib.testing.widgets import do_event

data = np.random.random((16, 16))
mask = np.zeros_like(data)

ax = plt.subplot(1, 1, 1)
ax.imshow(data)

def update_mask(reg):
mask[:] = reg.to_mask(mode='subpixels', subpixels=10).to_image(data.shape)

region = self.reg.copy(angle=0 * u.deg)

if anywhere and MPL_VERSION < 35:
pytest.skip('Requires `drag_from_anywhere` kwarg')
elif MPL_VERSION < 35:
selector = region.as_mpl_selector(ax, callback=update_mask)
else:
selector = region.as_mpl_selector(ax, callback=update_mask, drag_from_anywhere=anywhere)
assert selector.drag_from_anywhere is anywhere
assert region._mpl_selector.drag_from_anywhere is anywhere

# click_and_drag(selector, start=(3, 4), end=(3.5, 4.5))
do_event(selector, 'press', xdata=3, ydata=4, button=1)
do_event(selector, 'onmove', xdata=3.5, ydata=4.5, button=1)
do_event(selector, 'release', xdata=3.5, ydata=4.5, button=1)

ax.figure.canvas.draw()

assert_allclose(region.center.x, 3.5)
assert_allclose(region.center.y, 4.5)
assert_allclose(region.width, 4)
assert_allclose(region.height, 3)

do_event(selector, 'press', xdata=3.25, ydata=4.25, button=1)
do_event(selector, 'onmove', xdata=4.25, ydata=5.25, button=1)
do_event(selector, 'release', xdata=4.25, ydata=5.25, button=1)

ax.figure.canvas.draw()

# For drag_from_anywhere=False this will have created a new 1x1 rectangle.
if anywhere:
assert_allclose(region.center.x, 4.5)
assert_allclose(region.center.y, 5.5)
assert_allclose(region.width, 4)
assert_allclose(region.height, 3)
else:
assert_allclose(region.center.x, 4.5)
assert_allclose(region.center.y, 5.5)

assert_equal(mask, region.to_mask(mode='subpixels', subpixels=10).to_image(data.shape))

@pytest.mark.parametrize('userargs',
({'useblit': True},
{'grab_range': 20, 'minspanx': 5, 'minspany': 4},
{'props': {'facecolor': 'blue', 'linewidth': 2}},
{'twit': 'gumby'}))
def test_mpl_selector_kwargs(self, userargs):
"""Test that additional kwargs are passed to selector."""

plt = pytest.importorskip('matplotlib.pyplot')

data = np.random.random((16, 16))
mask = np.zeros_like(data)

ax = plt.subplot(1, 1, 1)
ax.imshow(data)

def update_mask(reg):
mask[:] = reg.to_mask(mode='subpixels', subpixels=10).to_image(data.shape)

region = self.reg.copy(angle=0 * u.deg)
region.visual = {'color': 'red'}

if MPL_VERSION < 35 and 'grab_range' in userargs:
userargs['maxdist'] = userargs.pop('grab_range')

if 'twit' in userargs:
with pytest.raises(TypeError, match=(r'__init__.. got an unexpected keyword argument')):
selector = region.as_mpl_selector(ax, callback=update_mask, **userargs)
else:
selector = region.as_mpl_selector(ax, callback=update_mask, **userargs)
assert region._mpl_selector.artists[0].get_edgecolor() == (1, 0, 0, 1)

if 'props' in userargs:
assert region._mpl_selector.artists[0].get_facecolor() == (0, 0, 1, 1)
assert region._mpl_selector.artists[0].get_linewidth() == 2
else:
assert region._mpl_selector.artists[0].get_facecolor() == (0, 0, 0, 0)
assert region._mpl_selector.artists[0].get_linewidth() == 1

for key, val in userargs.items():
assert getattr(region._mpl_selector, key) == val
assert getattr(selector, key) == val


class TestEllipseSkyRegion(BaseTestSkyRegion):
meta = RegionMeta({'text': 'test'})
Expand Down
2 changes: 1 addition & 1 deletion regions/shapes/tests/test_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from ...tests.helpers import make_simple_wcs
from ..line import LinePixelRegion, LineSkyRegion
from .test_common import BaseTestPixelRegion, BaseTestSkyRegion
from .utils import HAS_MATPLOTLIB # noqa
from ..utils import HAS_MATPLOTLIB # noqa


@pytest.fixture(scope='session', name='wcs')
Expand Down
2 changes: 1 addition & 1 deletion regions/shapes/tests/test_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ...tests.helpers import make_simple_wcs
from ..point import PointPixelRegion, PointSkyRegion
from .test_common import BaseTestPixelRegion, BaseTestSkyRegion
from .utils import HAS_MATPLOTLIB # noqa
from ..utils import HAS_MATPLOTLIB # noqa


@pytest.fixture(scope='session', name='wcs')
Expand Down
2 changes: 1 addition & 1 deletion regions/shapes/tests/test_polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from ..polygon import (PolygonPixelRegion, RegularPolygonPixelRegion,
PolygonSkyRegion)
from .test_common import BaseTestPixelRegion, BaseTestSkyRegion
from .utils import HAS_MATPLOTLIB # noqa
from ..utils import HAS_MATPLOTLIB # noqa


@pytest.fixture(scope='session', name='wcs')
Expand Down
Loading