From: W. Trevor King Date: Sun, 12 May 2013 19:24:14 +0000 (-0400) Subject: plotpick.py: Reroll around `picker` API and bump to version 0.3 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=ab0144334244a2bc998367758ff1f05b7d0d1357;p=blog.git plotpick.py: Reroll around `picker` API and bump to version 0.3 --- diff --git a/posts/plotpick.mdwn b/posts/plotpick.mdwn index 04389fe..895d239 100644 --- a/posts/plotpick.mdwn +++ b/posts/plotpick.mdwn @@ -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]] diff --git a/posts/plotpick/plotpick.py b/posts/plotpick/plotpick.py index dfdf782..4b83e75 100755 --- a/posts/plotpick/plotpick.py +++ b/posts/plotpick/plotpick.py @@ -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))