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

Skip to content

Commit 10e0300

Browse files
committed
Upgrade RectangleSelector with ToolHandles and add Ellipse
1 parent 71e54b5 commit 10e0300

File tree

1 file changed

+265
-41
lines changed

1 file changed

+265
-41
lines changed

lib/matplotlib/widgets.py

Lines changed: 265 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import numpy as np
2020

2121
from .mlab import dist
22-
from .patches import Circle, Rectangle
22+
from .patches import Circle, Rectangle, Ellipse
2323
from .lines import Line2D
2424
from .transforms import blended_transform_factory
2525

@@ -1487,10 +1487,13 @@ def toggle_selector(event):
14871487
connect('key_press_event', toggle_selector)
14881488
show()
14891489
"""
1490-
def __init__(self, ax, onselect, drawtype='box',
1490+
1491+
_shape_klass = Rectangle
1492+
1493+
def __init__(self, ax, onselect, drawtype='patch',
14911494
minspanx=None, minspany=None, useblit=False,
14921495
lineprops=None, rectprops=None, spancoords='data',
1493-
button=None):
1496+
button=None, maxdist=10, marker_props=None):
14941497

14951498
"""
14961499
Create a selector in *ax*. When a selection is made, clear
@@ -1534,30 +1537,32 @@ def __init__(self, ax, onselect, drawtype='box',
15341537
3 = right mouse button
15351538
"""
15361539
_SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
1537-
button=button)
1540+
button=button)
15381541

15391542
self.to_draw = None
1543+
self.visible = True
1544+
1545+
if drawtype == 'box': # backwards compatibility
1546+
drawtype = 'patch'
15401547

15411548
if drawtype == 'none':
15421549
drawtype = 'line' # draw a line but make it
15431550
self.visible = False # invisible
15441551

1545-
if drawtype == 'box':
1552+
if drawtype == 'patch':
15461553
if rectprops is None:
1547-
rectprops = dict(facecolor='white', edgecolor='black',
1548-
alpha=0.5, fill=False)
1554+
rectprops = dict(facecolor='red', edgecolor='black',
1555+
alpha=0.2, fill=True)
15491556
self.rectprops = rectprops
1550-
self.to_draw = Rectangle((0, 0),
1557+
self.to_draw = self._shape_klass((0, 0),
15511558
0, 1, visible=False, **self.rectprops)
15521559
self.ax.add_patch(self.to_draw)
15531560
if drawtype == 'line':
15541561
if lineprops is None:
15551562
lineprops = dict(color='black', linestyle='-',
15561563
linewidth=2, alpha=0.5)
15571564
self.lineprops = lineprops
1558-
if self.useblit:
1559-
self.lineprops['animated'] = True
1560-
self.to_draw = Line2D([0, 0], [0, 0], visible=False,
1565+
self.to_draw = Line2D([0, 0, 0, 0, 0], [0, 0, 0, 0, 0], visible=False,
15611566
**self.lineprops)
15621567
self.ax.add_line(self.to_draw)
15631568

@@ -1568,25 +1573,51 @@ def __init__(self, ax, onselect, drawtype='box',
15681573

15691574
self.spancoords = spancoords
15701575
self.drawtype = drawtype
1571-
self.artists = [self.to_draw]
1576+
1577+
self.maxdist = maxdist
1578+
1579+
if rectprops is None:
1580+
props = dict(mec='r')
1581+
else:
1582+
props = dict(mec=rectprops['edgecolor'])
1583+
self._corner_order = ['NW', 'NE', 'SE', 'SW']
1584+
xc, yc = self.corners
1585+
self._corner_handles = ToolHandles(self.ax, xc, yc, marker_props=props,
1586+
useblit=self.useblit)
1587+
1588+
self._edge_order = ['W', 'N', 'E', 'S']
1589+
xe, ye = self.edge_centers
1590+
self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s',
1591+
marker_props=props, useblit=self.useblit)
1592+
1593+
xc, yc = self.center
1594+
self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s',
1595+
marker_props=props, useblit=self.useblit)
1596+
1597+
self.artists = [self.to_draw, self._center_handle.artist,
1598+
self._corner_handles.artist,
1599+
self._edge_handles.artist]
15721600

15731601
def press(self, event):
15741602
"""on button press event"""
15751603
if not _SelectorWidget.press(self, event):
15761604
return True
1577-
# make the drawed box/line visible
1578-
self.to_draw.set_visible(self.visible)
1579-
return False
1605+
# make the drawed box/line visible get the click-coordinates,
1606+
# button, ...
1607+
self.set_visible(self.visible)
1608+
self._set_active_handle(event)
1609+
1610+
if self.active_handle is None:
1611+
# Clear previous rectangle before drawing new rectangle.
1612+
self.update()
1613+
1614+
self.set_visible(self.visible)
15801615

15811616
def release(self, event):
15821617
"""on button release event"""
15831618
if not _SelectorWidget.release(self, event):
15841619
return True
15851620

1586-
# make the box/line invisible again
1587-
self.to_draw.set_visible(False)
1588-
self.canvas.draw()
1589-
15901621
if self.spancoords == 'data':
15911622
xmin, ymin = self.eventpress.xdata, self.eventpress.ydata
15921623
xmax, ymax = self.eventrelease.xdata, self.eventrelease.ydata
@@ -1614,36 +1645,229 @@ def release(self, event):
16141645
# neither x nor y-direction
16151646
return
16161647

1648+
# update the eventpress and eventrelease with the resulting extents
1649+
x1, x2, y1, y2 = self.extents
1650+
self.eventpress.xdata = x1
1651+
self.eventpress.ydata = y1
1652+
xy1 = self.ax.transData.transform_point([x1, y1])
1653+
self.eventpress.x, self.eventpress.y = xy1
1654+
1655+
self.eventrelease.xdata = x2
1656+
self.eventrelease.ydata = y2
1657+
xy2 = self.ax.transData.transform_point([x2, y2])
1658+
self.eventrelease.x, self.eventrelease.y = xy2
1659+
16171660
self.onselect(self.eventpress, self.eventrelease)
16181661
# call desired function
1619-
self.eventpress = None
1662+
self.update()
1663+
16201664
return False
16211665

16221666
def onmove(self, event):
16231667
"""on motion notify event if box/line is wanted"""
1624-
if self.eventpress is None or self.ignore(event):
1668+
event = _SelectorWidget.onmove(self, event)
1669+
if not event:
1670+
return True
1671+
1672+
key = self.eventpress.key or ''
1673+
1674+
# resize an existing shape
1675+
if self.active_handle and not self.active_handle == 'C':
1676+
x1, x2, y1, y2 = self._extents_on_press
1677+
if self.active_handle in ['E', 'W'] + self._corner_order:
1678+
x2 = event.xdata
1679+
if self.active_handle in ['N', 'S'] + self._corner_order:
1680+
y2 = event.ydata
1681+
1682+
# move existing shape
1683+
elif self.active_handle == 'C':
1684+
x1, x2, y1, y2 = self._extents_on_press
1685+
dx = event.xdata - self.eventpress.xdata
1686+
dy = event.ydata - self.eventpress.ydata
1687+
x1 += dx
1688+
x2 += dx
1689+
y1 += dy
1690+
y2 += dy
1691+
1692+
# new shape
1693+
else:
1694+
center = [self.eventpress.xdata, self.eventpress.ydata]
1695+
center_pix = [self.eventpress.x, self.eventpress.y]
1696+
dx = (event.xdata - center[0]) / 2.
1697+
dy = (event.ydata - center[1]) / 2.
1698+
1699+
# square shape
1700+
if 'shift' in key:
1701+
dx_pix = abs(event.x - center_pix[0])
1702+
dy_pix = abs(event.y - center_pix[1])
1703+
if not dx_pix:
1704+
return
1705+
maxd = max(abs(dx_pix), abs(dy_pix))
1706+
if abs(dx_pix) < maxd:
1707+
dx *= maxd / abs(dx_pix)
1708+
if abs(dy_pix) < maxd:
1709+
dy *= maxd / abs(dy_pix)
1710+
1711+
# from center
1712+
if key == 'control' or key == 'ctrl+shift':
1713+
dx *= 2
1714+
dy *= 2
1715+
1716+
# from corner
1717+
else:
1718+
center[0] += dx
1719+
center[1] += dy
1720+
1721+
x1, x2, y1, y2 = (center[0] - dx, center[0] + dx,
1722+
center[1] - dy, center[1] + dy)
1723+
1724+
self.extents = x1, x2, y1, y2
1725+
1726+
@property
1727+
def _rect_bbox(self):
1728+
if self.drawtype == 'patch':
1729+
x0 = self.to_draw.get_x()
1730+
y0 = self.to_draw.get_y()
1731+
width = self.to_draw.get_width()
1732+
height = self.to_draw.get_height()
1733+
return x0, y0, width, height
1734+
else:
1735+
x, y = self.to_draw.get_data()
1736+
x0, x1 = min(x), max(x)
1737+
y0, y1 = min(y), max(y)
1738+
return x0, y0, x1 - x0, y1 - y0
1739+
1740+
@property
1741+
def corners(self):
1742+
"""Corners of rectangle from lower left, moving clockwise."""
1743+
x0, y0, width, height = self._rect_bbox
1744+
xc = x0, x0 + width, x0 + width, x0
1745+
yc = y0, y0, y0 + height, y0 + height
1746+
return xc, yc
1747+
1748+
@property
1749+
def edge_centers(self):
1750+
"""Midpoint of rectangle edges from left, moving clockwise."""
1751+
x0, y0, width, height = self._rect_bbox
1752+
w = width / 2.
1753+
h = height / 2.
1754+
xe = x0, x0 + w, x0 + width, x0 + w
1755+
ye = y0 + h, y0, y0 + h, y0 + height
1756+
return xe, ye
1757+
1758+
@property
1759+
def center(self):
1760+
"""Center of rectangle"""
1761+
x0, y0, width, height = self._rect_bbox
1762+
return x0 + width / 2., y0 + height / 2.
1763+
1764+
@property
1765+
def extents(self):
1766+
"""Return (xmin, xmax, ymin, ymax)."""
1767+
x0, y0, width, height = self._rect_bbox
1768+
xmin, xmax = sorted([x0, x0 + width])
1769+
ymin, ymax = sorted([y0, y0 + height])
1770+
return xmin, xmax, ymin, ymax
1771+
1772+
@extents.setter
1773+
def extents(self, extents):
1774+
# Update displayed shape
1775+
self.draw_shape(extents)
1776+
# Update displayed handles
1777+
self._corner_handles.set_data(*self.corners)
1778+
self._edge_handles.set_data(*self.edge_centers)
1779+
self._center_handle.set_data(*self.center)
1780+
self.set_visible(self.visible)
1781+
1782+
self.canvas.draw_idle()
1783+
1784+
def draw_shape(self, extents):
1785+
x0, x1, y0, y1 = extents
1786+
xmin, xmax = sorted([x0, x1])
1787+
ymin, ymax = sorted([y0, y1])
1788+
1789+
if self.drawtype == 'patch':
1790+
self.to_draw.set_x(xmin)
1791+
self.to_draw.set_y(ymin)
1792+
self.to_draw.set_width(xmax - xmin)
1793+
self.to_draw.set_height(ymax - ymin)
1794+
1795+
elif self.drawtype == 'line':
1796+
self.to_draw.set_data([xmin, xmin, xmax, xmax, xmin],
1797+
[ymin, ymax, ymax, ymin, ymin])
1798+
1799+
def _set_active_handle(self, event):
1800+
"""Set active handle based on the location of the mouse event"""
1801+
# Note: event.xdata/ydata in data coordinates, event.x/y in pixels
1802+
c_idx, c_dist = self._corner_handles.closest(event.x, event.y)
1803+
e_idx, e_dist = self._edge_handles.closest(event.x, event.y)
1804+
m_idx, m_dist = self._center_handle.closest(event.x, event.y)
1805+
1806+
if event.key in ['alt', ' ']:
1807+
self.active_handle = 'C'
1808+
self._extents_on_press = self.extents
1809+
1810+
# Set active handle as closest handle, if mouse click is close enough.
1811+
elif m_dist < self.maxdist * 2:
1812+
self.active_handle = 'C'
1813+
elif c_dist > self.maxdist and e_dist > self.maxdist:
1814+
self.active_handle = None
16251815
return
1816+
elif c_dist < e_dist:
1817+
self.active_handle = self._corner_order[c_idx]
1818+
else:
1819+
self.active_handle = self._edge_order[e_idx]
1820+
1821+
# Save coordinates of rectangle at the start of handle movement.
1822+
x1, x2, y1, y2 = self.extents
1823+
# Switch variables so that only x2 and/or y2 are updated on move.
1824+
if self.active_handle in ['W', 'SW', 'NW']:
1825+
x1, x2 = x2, event.xdata
1826+
if self.active_handle in ['N', 'NW', 'NE']:
1827+
y1, y2 = y2, event.ydata
1828+
self._extents_on_press = x1, x2, y1, y2
1829+
1830+
1831+
class EllipseSelector(RectangleSelector):
1832+
1833+
_shape_klass = Ellipse
1834+
1835+
def draw_shape(self, extents):
1836+
x1, x2, y1, y2 = extents
1837+
xmin, xmax = sorted([x1, x2])
1838+
ymin, ymax = sorted([y1, y2])
1839+
center = [x1 + (x2 - x1) / 2., y1 + (y2 - y1) / 2.]
1840+
a = (xmax - xmin) / 2.
1841+
b = (ymax - ymin) / 2.
1842+
1843+
if self.drawtype == 'patch':
1844+
self.to_draw.center = center
1845+
self.to_draw.width = 2 * a
1846+
self.to_draw.height = 2 * b
1847+
else:
1848+
rad = np.arange(31) * 12 * np.pi / 180
1849+
x = a * np.cos(rad) + center[0]
1850+
y = b * np.sin(rad) + center[1]
1851+
self.to_draw.set_data(x, y)
1852+
1853+
@property
1854+
def _rect_bbox(self):
1855+
if self.drawtype == 'patch':
1856+
x, y = self.to_draw.center
1857+
width = self.to_draw.width
1858+
height = self.to_draw.height
1859+
return x - width / 2., y - height / 2., width, height
1860+
else:
1861+
x, y = self.to_draw.get_data()
1862+
x0, x1 = min(x), max(x)
1863+
y0, y1 = min(y), max(y)
1864+
return x0, y0, x1 - x0, y1 - y0
1865+
1866+
@property
1867+
def geometry(self):
1868+
x0, y0, width, height = self._rect_bbox
1869+
return x0 + width / 2., y0 + width / 2., width, height
16261870

1627-
x, y = self._get_data(event) # actual position (with
1628-
# (button still pressed)
1629-
if self.drawtype == 'box':
1630-
minx, maxx = self.eventpress.xdata, x # click-x and actual mouse-x
1631-
miny, maxy = self.eventpress.ydata, y # click-y and actual mouse-y
1632-
if minx > maxx:
1633-
minx, maxx = maxx, minx # get them in the right order
1634-
if miny > maxy:
1635-
miny, maxy = maxy, miny
1636-
self.to_draw.set_x(minx) # set lower left of box
1637-
self.to_draw.set_y(miny)
1638-
self.to_draw.set_width(maxx - minx) # set width and height of box
1639-
self.to_draw.set_height(maxy - miny)
1640-
self.update()
1641-
return False
1642-
if self.drawtype == 'line':
1643-
self.to_draw.set_data([self.eventpress.xdata, x],
1644-
[self.eventpress.ydata, y])
1645-
self.update()
1646-
return False
16471871

16481872

16491873
class LassoSelector(_SelectorWidget):

0 commit comments

Comments
 (0)