plotpick.py: Reroll around `picker` API and bump to version 0.3
authorW. Trevor King <wking@tremily.us>
Sun, 12 May 2013 19:24:14 +0000 (15:24 -0400)
committerW. Trevor King <wking@tremily.us>
Sun, 12 May 2013 19:43:05 +0000 (15:43 -0400)
posts/plotpick.mdwn
posts/plotpick/plotpick.py

index 04389fe385ffa40838ff793d508c9b6eaf47edd9..895d2396532603a31cd8074383bbd6dbad6cd0d2 100644 (file)
@@ -12,10 +12,18 @@ like it :).  I've also written up a simple data generator
 
   $ random-data.py -n 200 | plotpick.py
 
+[[Matplotlib]] has supported the [`picker` infrastructure][picker] in
+various guises [since `pickeps` in 2007][e26b7b9], but I only just
+discovered it.  Before version 0.3, `plotpick.py` used a much less
+elegant cursor implementation based on [cursor_demo.py][].
+
 [[calibcant]], my modern cantilever calibration suite, [repositions
 the piezo before every bump][reposition], so out-of-range bumps are no
 longer an issue.
 
+[picker]: http://matplotlib.org/users/event_handling.html#object-picking
+[e26b7b9]: https://github.com/matplotlib/matplotlib/commit/e26b7b9a78785245d734e22a2cd8314dfa783c7c
+[cursor_demo.py]: http://matplotlib.org/examples/pylab_examples/cursor_demo.html
 [reposition]: http://git.tremily.us/?p=calibcant.git;a=commit;h=77e8244d80306dcea9ab1422cb45630a92494f85
 
 [[!tag tags/code]]
index dfdf782d9e2ca40e15ce8da09af5ad2f36cb4c56..4b83e75bab9d5fa5af70632a18acdab116eed30b 100755 (executable)
@@ -13,135 +13,50 @@ x1 y1
 The points are plotted in a window, and mousing over the plot displays
 a cursor that snaps to the nearest point.  Right-clicking (button 3)
 will print the TAB seperated coordinates (x, y, point-index) to stdout.
-
-The cursor snaps to the nearest datapoint.  Developed from
-Matplotlib's cursor_demo.py.
-
-Faster cursoring is possible using native GUI drawing, as in
-wxcursor_demo.py.  You could also write up a more efficient
-Cursor._get_xy() method.
 """
 
+import matplotlib.pyplot as _pyplot
 import numpy as _numpy
-import pylab as _pylab
-
-from pylab import subplot, connect, show, draw
-from numpy import array, fromfile, double
 
 
-__version__ = '0.2'
+__version__ = '0.3'
 
 
-class Cursor (object):
-    """Like Cursor but the crosshair snaps to the nearest x,y point
-
-    For simplicity, I'm assuming x is sorted.
+class Picker (object):
+    """Pick points from a plot
     """
-    def __init__(self, axes, x, y, selected=None, highlight=False):
+    def __init__(self, axes, x=None, y=None, selected=None, highlight=False):
         self.axes = axes
-        hold = self.axes.ishold()
-        self.axes.hold(True)
-        self.crossx, = axes.plot(
-            (0,0), (0,0), 'k-', zorder=4)  # the horiz crosshair
-        self.crossy, = axes.plot(
-            (0,0), (0,0), 'k-', zorder=4)  # the vert crosshair
-        self.x = x
-        self.y = y
-        self.xscale = max(self.x) - min(self.x)
-        self.yscale = max(self.y) - min(self.y)
-        self._sort()
         if highlight:
             if selected is None:
                 selected = []
-            sx = [self.x[i] for i in selected]
-            sy = [self.y[i] for i in selected]
+            sx = [x[i] for i in selected]
+            sy = [y[i] for i in selected]
+            hold = self.axes.ishold()
+            axes.hold(True)
             self.highlight_line, = axes.plot(sx, sy, 'r.', zorder=5)
+            self.axes.hold(hold)
         self.selected = selected
         self.highlight = highlight
         self.txt = self.axes.title
-        self.axes.hold(hold)
-
-    def _sort(self):
-        "Ideally, here is where you build the Voronoi lookup tree"
-        pass
-
-    def _get_xy(self, x, y):
-        """Return `(x_p, y_p, i_p)` for the point nearest `(x, y)`.
-
-        Terrible hack.  Should compute Voronoi diagram with some sort
-        of lookup tree.  Work for my free time...
-
-        http://en.wikipedia.org/wiki/Voronoi_diagram
-        http://en.wikipedia.org/wiki/Point_location#Triangulation_refinement
-        http://www.cs.cmu.edu/~quake/triangle.html
-        """
-        dist = float("infinity")
-        indx = -1
-        xs = self.xscale
-        ys = self.yscale
-        for xp,yp,i in zip(self.x, self.y,range(len(self.x))):
-            d = (((x-xp)/xs)**2 + ((y-yp)/ys)**2)**0.5
-            if d < dist:
-                dist = d
-                xpm = xp
-                ypm = yp
-                indx = i
-        return (xpm,ypm,indx)
-
-    def mouse_move(self, event):
-        if not event.inaxes: return
-        ax = event.inaxes
-        minx,maxx = ax.get_xlim()
-        miny,maxy = ax.get_ylim()
-        x,y,i = self._get_xy(event.xdata, event.ydata)
-        # update the line positions
-        self.crossx.set_data((minx, maxx), (y, y))
-        self.crossy.set_data((x, x), (miny, maxy))
-        # update the label
-        self.txt.set_text('x={:1.2g}, y={:1.2g}, indx={}'.format(x, y, i))
-        draw()
-
-    def mouse_click(self, event):
-        if not event.inaxes:
-            return
-        x,y,i = self._get_xy(event.xdata, event.ydata)
-        if event.button != 3:
-            return # ignore non-button-3 clicks
-        if self.highlight:
-            if i in self.selected:
-                self.selected.remove(i)
+
+    def onpick(self, event):
+        thisline = event.artist
+        xdata = thisline.get_xdata()
+        ydata = thisline.get_ydata()
+        for i in event.ind:
+            if self.highlight:
+                if i in self.selected:
+                    self.selected.remove(i)
+                else:
+                    self.selected.append(i)
+                sx = [xdata[i] for i in self.selected]
+                sy = [ydata[i] for i in self.selected]
+                self.highlight_line.set_data(sx, sy)
+                _pyplot.draw()
             else:
-                self.selected.append(i)
-            sx = [self.x[i] for i in self.selected]
-            sy = [self.y[i] for i in self.selected]
-            self.highlight_line.set_data(sx, sy)
-            draw()
-        else:
-            print('{}\t{}\t{}'.format(x, y, i))
-
-
-def read_file(fid, cols=2, sep='\t'):
-    """This is the lower level reader.  It works on all file types, but
-    you need to know the number of columns in the file ahead of time."""
-    data = fromfile(file=fid, dtype=double, sep=sep)
-    rows = len(data)/cols
-    data = data.reshape((rows,cols))
-    x = data[:,0]
-    y = data[:,1]
-    return x,y
-
-def read_filename(filename, sep='\t'):
-    """Requires rewinding file if first line is not a comment.
-    Therefore only useful on
-      * seekable files (e.g. read from disk)
-      * FIFOs/piped-data with headers"""
-    with open(filename,'r') as f:
-        headline = f.readline()
-        if headline[0] != "#":
-            f.seek(0) # rewind if headline was data
-        cols = headline.count(sep)+1
-        x,y = read_file(f, cols=cols, sep=sep)
-    return (x, y)
+                print('{}\t{}\t{}'.format(xdata[i], ydata[i], i))
+            break  # only deal with the first picked point
 
 
 if __name__ == "__main__" :
@@ -161,21 +76,22 @@ if __name__ == "__main__" :
     args = parser.parse_args()
 
     if args.datafile:
-        x,y = read_filename(args.datafile)
+        data = _numpy.genfromtxt(args.datafile)
     else:
-        x,y = read_file(_sys.stdin)
+        data = _numpy.genfromtxt(_sys.stdin)
+    x = data[:,0].squeeze()
+    y = data[:,1].squeeze()
 
-    axes = _pylab.subplot(1, 1, 1)    
-    cursor = Cursor(axes, x, y, highlight=args.highlight)
-    _pylab.connect('motion_notify_event', cursor.mouse_move)
-    _pylab.connect('button_press_event', cursor.mouse_click)    
-    axes.plot(x, y, '.')
-    axes.set_xlim(min(x), max(x))
-    axes.set_ylim(min(y), max(y))
+    figure = _pyplot.figure()
+    axes = figure.add_subplot(1, 1, 1)
+    axes.plot(x, y, '.', picker=2)
+    axes.autoscale(tight=True)
+    picker = Picker(axes=axes, highlight=args.highlight)
+    figure.canvas.mpl_connect('pick_event', picker.onpick)
 
     print('#x\ty\tindex')
-    show()
+    _pyplot.show()
     if args.highlight:
-        for i in cursor.selected:
+        for i in picker.selected:
             point = (x[i], y[i], i)
             print('\t'.join(str(x) for x in point))