19
19
import numpy as np
20
20
21
21
from .mlab import dist
22
- from .patches import Circle , Rectangle
22
+ from .patches import Circle , Rectangle , Ellipse
23
23
from .lines import Line2D
24
24
from .transforms import blended_transform_factory
25
25
@@ -1487,10 +1487,13 @@ def toggle_selector(event):
1487
1487
connect('key_press_event', toggle_selector)
1488
1488
show()
1489
1489
"""
1490
- def __init__ (self , ax , onselect , drawtype = 'box' ,
1490
+
1491
+ _shape_klass = Rectangle
1492
+
1493
+ def __init__ (self , ax , onselect , drawtype = 'patch' ,
1491
1494
minspanx = None , minspany = None , useblit = False ,
1492
1495
lineprops = None , rectprops = None , spancoords = 'data' ,
1493
- button = None ):
1496
+ button = None , maxdist = 10 , marker_props = None ):
1494
1497
1495
1498
"""
1496
1499
Create a selector in *ax*. When a selection is made, clear
@@ -1534,30 +1537,32 @@ def __init__(self, ax, onselect, drawtype='box',
1534
1537
3 = right mouse button
1535
1538
"""
1536
1539
_SelectorWidget .__init__ (self , ax , onselect , useblit = useblit ,
1537
- button = button )
1540
+ button = button )
1538
1541
1539
1542
self .to_draw = None
1543
+ self .visible = True
1544
+
1545
+ if drawtype == 'box' : # backwards compatibility
1546
+ drawtype = 'patch'
1540
1547
1541
1548
if drawtype == 'none' :
1542
1549
drawtype = 'line' # draw a line but make it
1543
1550
self .visible = False # invisible
1544
1551
1545
- if drawtype == 'box ' :
1552
+ if drawtype == 'patch ' :
1546
1553
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 )
1549
1556
self .rectprops = rectprops
1550
- self .to_draw = Rectangle ((0 , 0 ),
1557
+ self .to_draw = self . _shape_klass ((0 , 0 ),
1551
1558
0 , 1 , visible = False , ** self .rectprops )
1552
1559
self .ax .add_patch (self .to_draw )
1553
1560
if drawtype == 'line' :
1554
1561
if lineprops is None :
1555
1562
lineprops = dict (color = 'black' , linestyle = '-' ,
1556
1563
linewidth = 2 , alpha = 0.5 )
1557
1564
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 ,
1561
1566
** self .lineprops )
1562
1567
self .ax .add_line (self .to_draw )
1563
1568
@@ -1568,25 +1573,51 @@ def __init__(self, ax, onselect, drawtype='box',
1568
1573
1569
1574
self .spancoords = spancoords
1570
1575
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 ]
1572
1600
1573
1601
def press (self , event ):
1574
1602
"""on button press event"""
1575
1603
if not _SelectorWidget .press (self , event ):
1576
1604
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 )
1580
1615
1581
1616
def release (self , event ):
1582
1617
"""on button release event"""
1583
1618
if not _SelectorWidget .release (self , event ):
1584
1619
return True
1585
1620
1586
- # make the box/line invisible again
1587
- self .to_draw .set_visible (False )
1588
- self .canvas .draw ()
1589
-
1590
1621
if self .spancoords == 'data' :
1591
1622
xmin , ymin = self .eventpress .xdata , self .eventpress .ydata
1592
1623
xmax , ymax = self .eventrelease .xdata , self .eventrelease .ydata
@@ -1614,36 +1645,229 @@ def release(self, event):
1614
1645
# neither x nor y-direction
1615
1646
return
1616
1647
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
+
1617
1660
self .onselect (self .eventpress , self .eventrelease )
1618
1661
# call desired function
1619
- self .eventpress = None
1662
+ self .update ()
1663
+
1620
1664
return False
1621
1665
1622
1666
def onmove (self , event ):
1623
1667
"""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
1625
1815
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
1626
1870
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
1647
1871
1648
1872
1649
1873
class LassoSelector (_SelectorWidget ):
0 commit comments