Add --highlight option to plotpick.py and cleanup styling.
authorW. Trevor King <wking@tremily.us>
Thu, 17 May 2012 21:12:40 +0000 (17:12 -0400)
committerW. Trevor King <wking@tremily.us>
Thu, 17 May 2012 21:25:53 +0000 (17:25 -0400)
Also:
* Update plotpick post accordingly.
* Add random-data.py for easy plotpick.py demos.

posts/plotpick.mdwn
posts/plotpick/plotpick.py [changed mode: 0644->0755]
posts/plotpick/random-data.py [new file with mode: 0755]

index f986e47cf68c82319cd190c14eaaedcd48dcc001..04389fe385ffa40838ff793d508c9b6eaf47edd9 100644 (file)
@@ -7,7 +7,16 @@ a little utility to record clicks on key data points, so I could pick
 out the “good data”.
 
 Enter [[plotpick.py]] the raw-data version of [[clickloc]].  Hope you
-like it :).
+like it :).  I've also written up a simple data generator
+([[random-data.py]]), which allows you to try out plotpick::
+
+  $ random-data.py -n 200 | plotpick.py
+
+[[calibcant]], my modern cantilever calibration suite, [repositions
+the piezo before every bump][reposition], so out-of-range bumps are no
+longer an issue.
+
+[reposition]: http://git.tremily.us/?p=calibcant.git;a=commit;h=77e8244d80306dcea9ab1422cb45630a92494f85
 
 [[!tag tags/code]]
 [[!tag tags/linux]]
old mode 100644 (file)
new mode 100755 (executable)
index 8564b71..b12eff5
@@ -1,39 +1,76 @@
 #!/usr/bin/env python
-"""
-Pick data-points with a cursor.  The cursor snaps to the nearest
-datapoint.  Developed from Matplotlib's cursor_demo.py.
+
+"""Pick data-points with a cursor.
+
+Reads in ASCII data from datafile, or, if datafile is not given, from
+stdin.  Data files define an array of (x,y) points and consist of two
+columns (x&y) seperated by TABs, with ENDLINEs between the points:
+
+x0     y0
+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 numpy as _numpy
+import pylab as _pylab
+
 from pylab import subplot, connect, show, draw
 from numpy import array, fromfile, double
 
-class Cursor:
-    """
-    Like Cursor but the crosshair snaps to the nearest x,y point
-    For simplicity, I'm assuming x is sorted
+
+__version__ = '0.2'
+
+
+class Cursor (object):
+    """Like Cursor but the crosshair snaps to the nearest x,y point
+
+    For simplicity, I'm assuming x is sorted.
     """
-    def __init__(self, ax, x, y):
-        self.ax = ax
-        self.lx, = ax.plot( (0,0), (0,0), 'k-' )  # the horiz line
-        self.ly, = ax.plot( (0,0), (0,0), 'k-' )  # the vert line
+    def __init__(self, axes, x, y, 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()
-        # text location in axes coords
-        #self.txt = ax.text( 0.6, 0.9, '', transform=ax.transAxes)
-        self.txt = self.ax.title
+        if highlight:
+            if selected is None:
+                selected = []
+            sx = [self.x[i] for i in selected]
+            sy = [self.y[i] for i in selected]
+            self.highlight_line, = axes.plot(sx, sy, 'r.', zorder=5)
+        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"
+        "Ideally, here is where you build the Voronoi lookup tree"
         pass
+
     def _get_xy(self, x, y):
-        """terrible hack.  Should compute Voronoi diagram with
-        some sort of lookup tree.  Work for my free time...
+        """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
@@ -50,25 +87,39 @@ class Cursor:
                 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)
+        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.lx.set_data( (minx, maxx), (y, y) )
-        self.ly.set_data( (x, x), (miny, maxy) )
+        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=%d'%(x,y,i) )
+        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
-        print '%g\t%g\t%d'%(x,y,i)
+        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)
+            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)
+        else:
+            print('{}\t{}\t{}'.format(x, y, i))
+
 
-def readFile(fid, cols=2, sep='\t'):
+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)
@@ -78,49 +129,52 @@ def readFile(fid, cols=2, sep='\t'):
     y = data[:,1]
     return x,y
 
-def readFilename(filename, sep='\t'):
+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"""
-    fid = file(filename,'r')
-    headline = fid.readline()
-    if headline[0] != "#": fid.seek(0) # rewind if headline was data
-    cols = headline.count(sep)+1
-    x,y = readFile(fid, cols=cols, sep=sep)
-    fid.close()
-    return x,y
+    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)
+
 
 if __name__ == "__main__" :
-    import sys
+    from argparse import ArgumentParser as _ArgumentParser
+    import sys as _sys
 
-    if len(sys.argv) > 2 :
-        print "usage: plotpick.py [datafile]"
-        print """
-Reads in ASCII data from datafile, or, if datafile is not given, from
-stdin.  Data files define an array of (x,y) points and consist of two
-columns (x&y) seperated by TABs, with ENDLINEs between the points:
-x0     y0
-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.
-"""
-        sys.exit(1)
-    elif len(sys.argv) == 1:
-        x,y = readFile(sys.stdin)
-    else: # len(sys.argv) == 2
-        datafile = file(sys.argv[1], 'r')
-        x,y =  readFile(datafile)
-        datafile.close
-
-    ax = subplot(111)    
-    cursor = Cursor(ax, x, y)
-    connect('motion_notify_event', cursor.mouse_move)
-    connect('button_press_event', cursor.mouse_click)    
-    ax.plot(x, y, '.')
-    ax.axis([min(x), max(x), min(y), max(y)])
-
-    print '#x\ty\tindex'
+    parser = _ArgumentParser(
+        description=__doc__, version=__version__)
+    parser.add_argument(
+        '--highlight', action='store_const', const=True, default=False,
+        help=('Highlight selected points and print them afterwards, instead '
+              'of printing them as they are clicked'))
+    parser.add_argument(
+        'datafile', nargs='?', default=None,
+        help='Path to the datafile.  Defaults to stdin')
+
+    args = parser.parse_args()
+
+    if args.datafile:
+        x,y = read_filename(args.datafile)
+    else:
+        x,y = read_file(_sys.stdin)
+
+    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))
+
+    print('#x\ty\tindex')
     show()
+    if args.highlight:
+        for i in cursor.selected:
+            point = (x[i], y[i], i)
+            print('\t'.join(str(x) for x in point))
diff --git a/posts/plotpick/random-data.py b/posts/plotpick/random-data.py
new file mode 100755 (executable)
index 0000000..5b864b8
--- /dev/null
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+#
+"""Generate random data for testing plotpick.py
+
+For example::
+
+  $ random-data.py | plotpick.py
+"""
+
+import random as _random
+
+import numpy as _numpy
+
+
+__version__ = '0.1'
+
+
+class Linear (object):
+    def __init__(self, a=1, b=1):
+        self.a = a
+        self.b = b
+
+    def __call__(self, x):
+        return self.a*x + self.b
+
+def random_data(xmin=0, xmax=1, n=2**15, dx=0.1, dy=0.1, model=Linear()):
+    for x in _numpy.linspace(xmin, xmax, n):
+        y = model(x)
+        rx = _random.gauss(mu=x, sigma=dx)
+        ry = _random.gauss(mu=y, sigma=dy)
+        yield (rx, ry)
+
+
+if __name__ == '__main__':
+    from argparse import ArgumentParser as _ArgumentParser
+
+    parser = _ArgumentParser(
+        description=__doc__, version=__version__)
+    parser.add_argument(
+        '-n', type=int, default=100,
+        help='Number of random points to generate')
+
+    args = parser.parse_args()
+
+    for data in random_data(n=args.n):
+        print('\t'.join(str(x) for x in data))