44
55class _DragObj :
66 def __init__ (self , ax ):
7+ """Generic draggable object initialization
8+
9+ Draggable object is tagged as 'dragobj' using matplotlib's graphics
10+ objects' url property
11+ """
712 self .parentcanvas = ax .figure .canvas
813 self .parentax = ax
914
@@ -12,10 +17,11 @@ def __init__(self, ax):
1217 self .clicked = False
1318
1419 def on_click (self , event ):
20+ """Wrapper for on_click and motion callback methods"""
1521 # Executed on mouse click
1622 if event .inaxes != self .parentax : return # See if the mouse is over the parent axes object
1723
18- # Check for overlaps, make sure we only fire for one object per click
24+ # Check for object overlap
1925 timetomove = self .shouldthismove (event )
2026 if not timetomove : return
2127
@@ -29,6 +35,11 @@ def on_click(self, event):
2935 self .clicked = True
3036
3137 def shouldthismove (self , event ):
38+ """Determine whether the event firing object is the topmost rendered
39+
40+ Mitigates issues when the on_click callback fires for multiple
41+ overlapping objects at the same time, causing both to move
42+ """
3243 # Check to see if this object has been clicked on
3344 contains , attrs = self .myobj .contains (event )
3445 if not contains :
@@ -52,24 +63,36 @@ def shouldthismove(self, event):
5263 return timetomove
5364
5465 def on_release (self , event ):
66+ """Mouse button release callback"""
5567 self .clicked = False
5668 self .disconnect ()
5769
5870 def disconnect (self ):
71+ """Disconnect mouse motion and click release callbacks from parent canvas"""
5972 self .parentcanvas .mpl_disconnect (self .mousemotion )
6073 self .parentcanvas .mpl_disconnect (self .clickrelease )
6174 self .parentcanvas .draw ()
6275
6376 def stopdrag (self ):
77+ """Disconnect on_click callback and remove dragobj url property tag"""
6478 self .myobj .set_url ('' )
6579 self .parentcanvas .mpl_disconnect (self .clickpress )
6680
6781
6882class _DragLine (_DragObj ):
6983 def __init__ (self , ax ):
84+ """Generic draggable line class
85+
86+ Provides line-specific on_motion callback
87+ """
7088 super ().__init__ (ax )
7189
7290 def on_motion (self , event ):
91+ """Update position of draggable line on mouse motion
92+
93+ If self.snapto is set to a valid lineseries object, dragging will be
94+ limited to the extent of the lineseries
95+ """
7396 # Executed on mouse motion
7497 if not self .clicked :
7598 # See if we've clicked yet
@@ -98,11 +121,26 @@ def on_motion(self, event):
98121
99122class _DragPatch (_DragObj ):
100123 def __init__ (self , ax , xy ):
124+ """Generic draggable line class
125+
126+ Provides patch-specific on_motion callback and helpers
127+
128+ self.oldxy stores the previous location of the patch, or its initial
129+ location if the object has not been moved. This is used for the
130+ on_motion cllback
131+ """
101132 super ().__init__ (ax )
102133
103134 self .oldxy = xy # Store for motion callback
104135
105136 def on_motion (self , event ):
137+ """Update position of draggable patch on mouse motion
138+
139+ self.oldxy is used to calculate the mouse motion delta in the xy
140+ directions. This prevents the patch from jumping to the mouse location
141+ due to (most) objects beind defined from either their lower left corner
142+ or their center.
143+ """
106144 # Executed on mouse motion
107145 if not self .clicked :
108146 # See if we've clicked yet
@@ -129,6 +167,7 @@ def on_motion(self, event):
129167 self .parentcanvas .draw ()
130168
131169 def on_release (self , event ):
170+ """Update helper xy property"""
132171 self .clicked = False
133172
134173 # LBYL for patches with centers (e.g. ellipse) vs. xy location (e.g. rectangle)
@@ -164,18 +203,26 @@ def __init__(self, ax, position, orientation='vertical', snapto=None, **kwargs):
164203
165204 self .snapto = snapto
166205
167- def get_xydata (self ):
206+ @staticmethod
207+ def get_validorientations ():
208+ return ('vertical' , 'horizontal' )
209+
210+ # Expose matplotlib object's property getters
211+ @property
212+ def xydata (self ):
168213 """Return the xy data as a Nx2 numpy array."""
169214 return self .myobj .get_xydata ()
170215
171- def get_xdata (self , orig = True ):
216+ @property
217+ def xdata (self , orig = True ):
172218 """Return the xdata.
173219
174220 If orig is True, return the original data, else the processed data.
175221 """
176222 return self .myobj .get_xdata (orig )
177223
178- def get_ydata (self , orig = True ):
224+ @property
225+ def ydata (self , orig = True ):
179226 """Return the ydata.
180227
181228 If orig is True, return the original data, else the processed data.
@@ -212,10 +259,10 @@ def __init__(self, ax, primaryedge, windowsize, orientation='vertical', snapto=N
212259 alpha = 0.25 , facecolor = 'limegreen' , edgecolor = 'green' , ** kwargs ):
213260 self .orientation = orientation .lower ()
214261 if self .orientation == 'vertical' :
215- axesdimension = get_axesextent (ax )[1 ] # Axes height
262+ axesdimension = axesextent (ax )[1 ] # Axes height
216263 xy = (primaryedge , ax .get_ylim ()[0 ])
217264 elif self .orientation == 'horizontal' :
218- axesdimension = get_axesextent (ax )[0 ] # Axes width
265+ axesdimension = axesextent (ax )[0 ] # Axes width
219266 xy = (ax .get_xlim ()[0 ], primaryedge )
220267 else :
221268 raise ValueError (f"Unsupported orientation string: '{ orientation } '" )
@@ -258,6 +305,18 @@ def on_motion(self, event):
258305
259306 self .parentcanvas .draw ()
260307
308+ @property
309+ def bounds (self ):
310+ xy = self .myobj .get_xy ()
311+ if self .orientation == 'vertical' :
312+ return (xy [0 ], xy [0 ] + self .myobj .get_width ())
313+ elif self .orientation == 'horizontal' :
314+ return (xy [1 ], xy [1 ] + self .myobj .get_height ())
315+
316+ @staticmethod
317+ def validorientations ():
318+ return ('vertical' , 'horizontal' )
319+
261320
262321class Window :
263322 def __init__ (self , ax , primaryedge , windowstartsize , orientation = 'vertical' , snapto = None ,
@@ -267,6 +326,7 @@ def __init__(self, ax, primaryedge, windowstartsize, orientation='vertical', sna
267326 self .edges = []
268327 self .edges .append (DragLine2D (ax , primaryedge , orientation , snapto , color = edgecolor ))
269328 self .edges .append (DragLine2D (ax , (primaryedge + windowstartsize ), orientation , snapto , color = edgecolor ))
329+ self .orientation = orientation
270330
271331 # Add spanning rectangle
272332 xy , width , height = self .spanpatchdims (* self .edges )
@@ -275,6 +335,14 @@ def __init__(self, ax, primaryedge, windowstartsize, orientation='vertical', sna
275335
276336 # TODO: Refactor to monitor changes in edge locations rather than firing on all redraws
277337 ax .figure .canvas .mpl_connect ('draw_event' , self .resizespanpatch )
338+
339+ @property
340+ def bounds (self ):
341+ xy = self .spanpatch .get_xy ()
342+ if self .orientation == 'vertical' :
343+ return (xy [0 ], xy [0 ] + self .spanpatch .get_width ())
344+ elif self .orientation == 'horizontal' :
345+ return (xy [1 ], xy [1 ] + self .spanpatch .get_height ())
278346
279347 def resizespanpatch (self , event ):
280348 if self .spanpatch :
@@ -286,18 +354,22 @@ def resizespanpatch(self, event):
286354 @staticmethod
287355 def spanpatchdims (edge1 , edge2 ):
288356 # Find leftmost, rightmost points
289- minx = min (edge1 .get_xdata () + edge2 .get_xdata () ) # Joining the two lists, not adding them
290- maxx = max (edge1 .get_xdata () + edge2 .get_xdata () ) # Joining the two lists, not adding them
357+ minx = min (edge1 .xdata + edge2 .xdata ) # Joining the two lists, not adding them
358+ maxx = max (edge1 .xdata + edge2 .xdata ) # Joining the two lists, not adding them
291359
292- # Find bottommostm , topmost points
293- miny = min (edge1 .get_ydata () + edge2 .get_ydata () ) # Joining the two lists, not adding them
294- maxy = max (edge1 .get_ydata () + edge2 .get_ydata () ) # Joining the two lists, not adding them
360+ # Find bottommost , topmost points
361+ miny = min (edge1 .ydata + edge2 .ydata ) # Joining the two lists, not adding them
362+ maxy = max (edge1 .ydata + edge2 .ydata ) # Joining the two lists, not adding them
295363
296364 xy = (minx , miny )
297365 width = abs (maxx - minx )
298366 height = abs (maxy - miny )
299367
300368 return xy , width , height
369+
370+ @staticmethod
371+ def validorientations ():
372+ return ('vertical' , 'horizontal' )
301373
302374
303375class DragArc (_DragPatch ):
@@ -323,7 +395,7 @@ def __init__(self, ax, xy, numVertices, radius=5, orientation=0, **kwargs):
323395
324396 super ().__init__ (ax , xy )
325397
326- def get_axesextent (ax ):
398+ def axesextent (ax ):
327399 xlim = ax .get_xlim ()
328400 ylim = ax .get_ylim ()
329401
0 commit comments