From b4b4c02488104ddbc5ccdb568e058b1859b0eb38 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 17 May 2012 17:12:40 -0400 Subject: [PATCH] Add --highlight option to plotpick.py and cleanup styling. Also: * Update plotpick post accordingly. * Add random-data.py for easy plotpick.py demos. --- posts/plotpick.mdwn | 11 +- posts/plotpick/plotpick.py | 186 ++++++++++++++++++++++------------ posts/plotpick/random-data.py | 46 +++++++++ 3 files changed, 176 insertions(+), 67 deletions(-) mode change 100644 => 100755 posts/plotpick/plotpick.py create mode 100755 posts/plotpick/random-data.py diff --git a/posts/plotpick.mdwn b/posts/plotpick.mdwn index f986e47..04389fe 100644 --- a/posts/plotpick.mdwn +++ b/posts/plotpick.mdwn @@ -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]] diff --git a/posts/plotpick/plotpick.py b/posts/plotpick/plotpick.py old mode 100644 new mode 100755 index 8564b71..b12eff5 --- a/posts/plotpick/plotpick.py +++ b/posts/plotpick/plotpick.py @@ -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 index 0000000..5b864b8 --- /dev/null +++ b/posts/plotpick/random-data.py @@ -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)) -- 2.26.2