Hi,

Attached is a patch (created by issuing svn diff from the
matplotlib/trunk/matplotlib directory) for adding the capability to
manually select the location of contour labels in clabel.  Though the
existing algorithm for automatically placing contour labels is very
good, for complex figures with multiple elements it is often desirable
to manually place labels.  This functionality is meant to imitate the
matlab functionality of clabel(cs,'manual').  

The attached patch includes a modified version of the changes I
previously made to add a "waitforbuttonpress" command to matplotlib as
these changes are a prerequisite for using the added functionality of
clabel (i.e., you shouldn't apply both patches, just this last one).

The changes work as follows:

cs = contour(x,y,z)
cl = clabel(cs,'manual')

(use the third mouse button to finish placing labels, second button to
erase a previously added label)

The patch isn't done - manually selected labels won't be rotated or
inline.  There is also a need for general cleaning up and documentation.
I just want to see what people think about the approach before investing
more time.  I added this functionality by adding a class
ContourLabelerWithManual that inherits from ContourLabeler and
BlockingMouseInput (the class used by ginput to interactively select
points).  ContourSet then inherits from ContourLabelerWithManual instead
of ContourLabeler.  If manual is selected, then it enters interactive
mode, if not, then results should be as before.  

I also had to move the classes Blocking*Input from figure.py to a
separate file blocking_input.py to avoid circular imports.

Please let me know what you think.  Also, I am wondering if the powers
that be would be willing to give me commit access to my own branch of
the matplotlib svn.  I don't want to modify the trunk, but for my own
sanity, it would be nice to be able to keep track of my changes
somewhere.  If not, I would like to here what other non-commit
developers do to best organize changes.

Thanks,
David

-- 
**********************************
David M. Kaplan
Assistant Researcher
UCSC / Institute of Marine Sciences
Ocean Sciences
1156 High St.
SC, CA 95064

Phone: 831-459-4789
Fax: 831-459-4882
http://pmc.ucsc.edu/~dmk/
**********************************
Index: lib/matplotlib/pyplot.py
===================================================================
--- lib/matplotlib/pyplot.py	(revision 5770)
+++ lib/matplotlib/pyplot.py	(working copy)
@@ -320,7 +320,21 @@
 if Figure.ginput.__doc__ is not None:
     ginput.__doc__ = dedent(Figure.ginput.__doc__)
 
+def waitforbuttonpress(*args, **kwargs):
+    """
+    Blocking call to interact with the figure.
 
+    This will wait for *n* key or mouse clicks from the user and
+    return a list containing True's for keyboard clicks and False's
+    for mouse clicks.
+
+    If *timeout* is negative, does not timeout.
+    """
+    return gcf().waitforbuttonpress(*args, **kwargs)
+if Figure.waitforbuttonpress.__doc__ is not None:
+    waitforbuttonpress.__doc__ = dedent(Figure.waitforbuttonpress.__doc__)
+
+
 # Putting things in figures
 
 def figtext(*args, **kwargs):
Index: lib/matplotlib/contour.py
===================================================================
--- lib/matplotlib/contour.py	(revision 5770)
+++ lib/matplotlib/contour.py	(working copy)
@@ -17,6 +17,9 @@
 import matplotlib.text as text
 import matplotlib.cbook as cbook
 
+# Import needed for adding manual selection capability to clabel
+from blocking_input import BlockingMouseInput
+
 # We can't use a single line collection for contour because a line
 # collection can have only a single line style, and we want to be able to have
 # dashed negative contours, for example, and solid positive contours.
@@ -128,9 +131,6 @@
 
         self.labels(inline)
 
-        for label in self.cl:
-            self.ax.add_artist(label)
-
         self.label_list =  cbook.silent_list('text.Text', self.cl)
         return self.label_list
 
@@ -335,6 +335,25 @@
 
         return x,y, rotation, dind
 
+    def add_label(self,x,y,rotation,lev,fmt,color,cvalue):
+        dx,dy = self.ax.transData.inverted().transform_point((x,y))
+        t = text.Text(dx, dy, rotation = rotation,
+                      horizontalalignment='center',
+                      verticalalignment='center')
+        _text = self.get_text(lev,fmt)
+        self.set_label_props(t, _text, color)
+        self.cl.append(t)
+        self.cl_cvalues.append(cvalue)
+        
+        # Add label to plot here - useful for manual mode label selection
+        self.ax.add_artist(t)
+
+    def remove_label(self,index=-1):
+        '''Defaults to removing last label, but any index can be supplied'''
+        self.cl_cvalues.pop(index)
+        t = self.cl.pop(index)
+        t.remove()
+
     def labels(self, inline):
         levels = self.label_levels
         fslist = self.fslist
@@ -362,16 +381,8 @@
                 slc = trans.transform(linecontour)
                 if self.print_label(slc,lw):
                     x,y, rotation, ind  = self.locate_label(slc, lw)
-                    # transfer the location of the label back to
-                    # data coordinates
-                    dx,dy = trans.inverted().transform_point((x,y))
-                    t = text.Text(dx, dy, rotation = rotation,
-                             horizontalalignment='center',
-                             verticalalignment='center')
-                    _text = self.get_text(lev,fmt)
-                    self.set_label_props(t, _text, color)
-                    self.cl.append(t)
-                    self.cl_cvalues.append(cvalue)
+
+                    self.add_label(x,y,rotation,lev,fmt,color,cvalue)
                     if inline:
                         new = self.break_linecontour(linecontour, rotation, lw, ind)
                         if len(new[0]):
@@ -380,8 +391,93 @@
                             additions.append(path.Path(new[1]))
             paths.extend(additions)
 
+class ContourLabelerWithManual( ContourLabeler, BlockingMouseInput ):
+    """
+    Mixin to provide labeling capability to ContourSet.  This version
+    includes additions to the base ContourLabeler class that allows
+    labels to be manually selected with a mouse using
+    BlockingMouseInput class.
+    """
+    def clabel(self, *args, **kwargs):
+        self.manual_selection = False
+        if len(args)>0:
+            if args[0]=='manual':
+                self.manual_selection = True
+                args=args[1:]
+            elif args[0] == 'remove':
+                while len(self.cl)>0:
+                    self.remove_label()
+                return self.cl
+            elif isinstance(args[0],str):
+                raise ValueError( 'Unknown string argument "%s"' % args[0] )
+        return ContourLabeler.clabel(self,*args,**kwargs)
 
-class ContourSet(cm.ScalarMappable, ContourLabeler):
+    def post_event(self):
+        event = self.events[-1]
+        
+        if event.button == 2:
+            self.done = True
+        elif event.button == 3:
+            if len(self.cl)>0:
+                self.remove_label()
+                self.fig.canvas.draw()
+        elif event.inaxes == self.ax:
+            conmin,segmin,xmin,ymin = self.find_nearest_contour(event.x, 
+                                                                event.y)[:4]
+            color = self.label_mappable.to_rgba(self.label_cvalues[conmin],
+                                                alpha=self.alpha)
+
+            self.add_label(xmin,ymin,0.0,self.label_levels[conmin],self.fmt,
+                           color,self.label_cvalues[conmin])
+            self.fig.canvas.draw()
+
+    def find_nearest_contour( self, x, y ):
+        '''Finds contour that is closest in pixels'''
+
+        # This function uses a method that is probably quite
+        # inefficient based on converting each contour segment to
+        # pixel coordinates and then comparing the given point to
+        # those coordinates for each contour.  This will probably be
+        # quite slow for complex contours, but for normal use it works
+        # sufficiently well that the time is not noticeable.
+        # Nonetheless, improvements could probably be made.
+
+        dmin = 1e10
+        conmin = None
+        segmin = None
+        xmin = None
+        ymin = None
+
+        for icon, lev in zip(self.label_indices,self.label_levels):
+            con = self.collections[icon]
+            paths = con.get_paths()
+            for segNum, linepath in enumerate(paths):
+                linecontour = linepath.vertices
+
+                # transfer all data points to screen coordinates
+                slc = self.ax.transData.transform(linecontour)
+                
+                ds = (slc[:,0]-x)**2 + (slc[:,1]-y)**2
+                d = min( ds )
+                if d < dmin:
+                    dmin = d
+                    conmin = icon
+                    segmin = segNum
+                    imin = mpl.mlab.find( ds == d )[0]
+                    xmin = slc[imin,0]
+                    ymin = slc[imin,1]
+
+        return (conmin,segmin,xmin,ymin,dmin,imin)
+
+    def labels(self, inline ):
+        if self.manual_selection:
+            BlockingMouseInput.__init__(self,self.ax.figure)
+            BlockingMouseInput.__call__(self,n=-1,timeout=-1,
+                                        verbose=True,show_clicks=False)
+        else:
+            ContourLabeler.labels(self,inline)
+
+class ContourSet(cm.ScalarMappable, ContourLabelerWithManual):
     """
     Create and store a set of contour lines or filled regions.
 
Index: lib/matplotlib/blocking_input.py
===================================================================
--- lib/matplotlib/blocking_input.py	(revision 0)
+++ lib/matplotlib/blocking_input.py	(revision 0)
@@ -0,0 +1,185 @@
+"""
+This provides several classes used for interaction with figure windows:
+
+:class:`BlockingInput`
+    creates a callable object to retrieve events in a blocking way for interactive sessions
+
+:class:`BlockingKeyMouseInput`
+    creates a callable object to retrieve key or mouse clicks in a blocking way for interactive sessions.  
+    Note: Subclass of BlockingInput. Used by waitforbuttonpress
+
+:class:`BlockingMouseInput`
+    creates a callable object to retrieve mouse clicks in a blocking way for interactive sessions.  
+    Note: Subclass of BlockingInput.  Used by ginput
+"""
+
+import time
+
+class BlockingInput(object):
+    """
+    Class that creates a callable object to retrieve events in a
+    blocking way.
+    """
+    def __init__(self, fig, eventslist=()):
+        self.fig = fig
+        assert isinstance(eventslist, tuple), \
+            "Requires a tuple of event name strings"
+        self.eventslist = eventslist
+
+    def on_event(self, event):
+        """
+        Event handler that will be passed to the current figure to
+        retrieve events.
+        """
+        self.events.append(event)
+
+        if self.verbose:
+            print "Event %i" % len(self.events)
+
+        # This will extract info from events
+        self.post_event()
+        
+        if len(self.events) >= self.n and self.n > 0:
+            self.done = True
+
+    def post_event(self):
+        """For baseclass, do nothing but collect events"""
+        pass
+
+    def cleanup(self):
+        """Remove callbacks"""
+        for cb in self.callbacks:
+            self.fig.canvas.mpl_disconnect(cb)
+
+        self.callbacks=[]
+
+    def __call__(self, n=1, timeout=30, verbose=False ):
+        """
+        Blocking call to retrieve n events
+        """
+        
+        assert isinstance(n, int), "Requires an integer argument"
+        self.n = n
+
+        self.events = []
+        self.done = False
+        self.verbose = verbose
+        self.callbacks = []
+
+        # Ensure that the figure is shown
+        self.fig.show()
+        
+        # connect the events to the on_event function call
+        for n in self.eventslist:
+            self.callbacks.append( self.fig.canvas.mpl_connect(n, self.on_event) )
+
+        try:
+            # wait for n clicks
+            counter = 0
+            while not self.done:
+                self.fig.canvas.flush_events()
+                time.sleep(0.01)
+
+                # check for a timeout
+                counter += 1
+                if timeout > 0 and counter > timeout/0.01:
+                    print "Timeout reached";
+                    break;
+        finally: # Activated on exception like ctrl-c
+            self.cleanup()
+
+        # Disconnect the callbacks
+        self.cleanup()
+
+        # Return the events in this case
+        return self.events
+
+class BlockingMouseInput(BlockingInput):
+    """
+    Class that creates a callable object to retrieve mouse clicks in a
+    blocking way.
+    """
+    def __init__(self, fig):
+        BlockingInput.__init__(self, fig=fig, eventslist=('button_press_event',) )
+
+    def post_event(self):
+        """
+        This will be called to process events
+        """
+        assert len(self.events)>0, "No events yet"
+
+        event = self.events[-1]
+
+        if event.button == 3:
+            # If it's a right click, pop the last coordinates.
+            if len(self.clicks) > 0:
+                self.clicks.pop()
+                del self.events[-2:] # Remove button=3 event and previous event
+
+                if self.show_clicks:
+                    mark = self.marks.pop()
+                    mark.remove()
+                    self.fig.canvas.draw()
+        elif event.button == 2 and self.n < 0:
+            # If it's a middle click, and we are in infinite mode, finish
+            self.done = True
+        elif event.inaxes:
+            # If it's a valid click, append the coordinates to the list
+            self.clicks.append((event.xdata, event.ydata))
+            if self.verbose:
+                print "input %i: %f,%f" % (len(self.clicks),
+                                    event.xdata, event.ydata)
+            if self.show_clicks:
+                self.marks.extend(
+                    event.inaxes.plot([event.xdata,], [event.ydata,], 'r+') )
+                self.fig.canvas.draw()
+
+    def cleanup(self):
+        # clean the figure
+        if self.show_clicks:
+            for mark in self.marks:
+                mark.remove()
+            self.marks = []
+            self.fig.canvas.draw()
+
+        # Call base class to remove callbacks
+        BlockingInput.cleanup(self)
+        
+    def __call__(self, n=1, timeout=30, verbose=False, show_clicks=True):
+        """
+        Blocking call to retrieve n coordinate pairs through mouse
+        clicks.
+        """
+        self.show_clicks = show_clicks
+        self.clicks      = []
+        self.marks       = []
+        BlockingInput.__call__(self,n=n,timeout=timeout,verbose=verbose)
+
+        return self.clicks
+
+class BlockingKeyMouseInput(BlockingInput):
+    """
+    Class that creates a callable object to retrieve a single mouse or
+    keyboard click
+    """
+    def __init__(self, fig):
+        BlockingInput.__init__(self, fig=fig, eventslist=('button_press_event','key_press_event') )
+
+    def post_event(self):
+        """
+        Determines if it is a key event
+        """
+        assert len(self.events)>0, "No events yet"
+
+        self.keyormouse = self.events[-1].name == 'key_press_event'
+
+    def __call__(self, timeout=30, verbose=False):
+        """
+        Blocking call to retrieve a single mouse or key click
+        Returns True if key click, False if mouse, or None if timeout
+        """
+        self.keyormouse = None
+        BlockingInput.__call__(self,n=1,timeout=timeout,verbose=verbose)
+
+        return self.keyormouse
+
Index: lib/matplotlib/figure.py
===================================================================
--- lib/matplotlib/figure.py	(revision 5770)
+++ lib/matplotlib/figure.py	(working copy)
@@ -6,9 +6,6 @@
 :class:`SubplotParams`
     control the default spacing of the subplots
 
-:class:`BlockingMouseInput`
-    creates a callable object to retrieve mouse clicks in a blocking way for interactive sessions
-
 :class:`Figure`
     top level container for all plot elements
 
@@ -32,6 +29,7 @@
 from transforms import Affine2D, Bbox, BboxTransformTo, TransformedBbox
 from projections import projection_factory, get_projection_names, \
     get_projection_class
+from blocking_input import *
 
 import matplotlib.cbook as cbook
 
@@ -117,87 +115,6 @@
 
         setattr(self, s, val)
 
-
-class BlockingMouseInput(object):
-    """
-    Class that creates a callable object to retrieve mouse clicks in a
-    blocking way.
-    """
-    def __init__(self, fig):
-        self.fig = fig
-
-
-    def on_click(self, event):
-        """
-        Event handler that will be passed to the current figure to
-        retrieve clicks.
-        """
-        if event.button == 3:
-            # If it's a right click, pop the last coordinates.
-            if len(self.clicks) > 0:
-                self.clicks.pop()
-                if self.show_clicks:
-                    mark = self.marks.pop()
-                    mark.remove()
-                    self.fig.canvas.draw()
-        elif event.button == 2 and self.n < 0:
-            # If it's a middle click, and we are in infinite mode, finish
-            self.done = True
-        elif event.inaxes:
-            # If it's a valid click, append the coordinates to the list
-            self.clicks.append((event.xdata, event.ydata))
-            if self.verbose:
-                print "input %i: %f,%f" % (len(self.clicks),
-                                    event.xdata, event.ydata)
-            if self.show_clicks:
-                self.marks.extend(
-                    event.inaxes.plot([event.xdata,], [event.ydata,], 'r+') )
-                self.fig.canvas.draw()
-            if self.n > 0 and len(self.clicks) >= self.n:
-                self.done = True
-
-
-    def __call__(self, n=1, timeout=30, verbose=False, show_clicks=True):
-        """
-        Blocking call to retrieve n coordinate pairs through mouse
-        clicks.
-        """
-        self.verbose     = verbose
-        self.done        = False
-        self.clicks      = []
-        self.show_clicks = True
-        self.marks       = []
-
-        assert isinstance(n, int), "Requires an integer argument"
-        self.n = n
-
-        # Ensure that the figure is shown
-        self.fig.show()
-        # connect the click events to the on_click function call
-        self.callback = self.fig.canvas.mpl_connect('button_press_event',
-                                                    self.on_click)
-        # wait for n clicks
-        counter = 0
-        while not self.done:
-            self.fig.canvas.flush_events()
-            time.sleep(0.01)
-
-            # check for a timeout
-            counter += 1
-            if timeout > 0 and counter > timeout/0.01:
-                print "ginput timeout";
-                break;
-
-        # Disconnect the event, clean the figure, and return what we have
-        self.fig.canvas.mpl_disconnect(self.callback)
-        self.callback = None
-        if self.show_clicks:
-            for mark in self.marks:
-                mark.remove()
-            self.fig.canvas.draw()
-        return self.clicks
-
-
 class Figure(Artist):
 
     """
@@ -1117,10 +1034,27 @@
 
         blocking_mouse_input = BlockingMouseInput(self)
         return blocking_mouse_input(n=n, timeout=timeout,
-                                          verbose=verbose, show_clicks=True)
+                                          verbose=verbose, show_clicks=show_clicks)
 
+    def waitforbuttonpress(self, timeout=-1):
+        """
+        call signature::
 
+          waitforbuttonpress(self, timeout=-1)
 
+        Blocking call to interact with the figure.
+
+        This will return True is a key was pressed, False if a mouse
+        button was pressed and None if *timeout* was reached without
+        either being pressed.
+
+        If *timeout* is negative, does not timeout.
+        """
+
+        blocking_input = BlockingKeyMouseInput(self)
+        return blocking_input(timeout=timeout)
+
+
 def figaspect(arg):
     """
     Create a figure with specified aspect ratio.  If *arg* is a number,
-------------------------------------------------------------------------
This SF.Net email is sponsored by the Moblin Your Move Developer's challenge
Build the coolest Linux based applications with Moblin SDK & win great prizes
Grand prize is a trip for two to an Open Source event anywhere in the world
http://moblin-contest.org/redirect.php?banner_id=100&url=/
_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/matplotlib-devel

Reply via email to