0

Hoping someone can tell me what I'm doing wrong in my first program using callbacks.

The goal:

  • Show a plot containing data
  • Allow the user to click on the plot 4 times. Each time, the X-coordinate is appended to a saved list.
  • While the mouse is moving, its horizontal location is tracked by a vertical line that moves back and forth within the plot. (I save the 2D line object as self.currentLine)
  • When user selects a point by clicking, the vertical line is dropped at the x-coordinate of interest, and a new one is generated to continue tracking mouse position.

At the end of user input, there should be four vertical lines and the class should return a list containing their x-coordinates.

At present, I can't figure out the proper way to update the line objects in the plot (i.e. so I can get the mouse tracking effect I want). I also can't get the class to return the list of values when finished.

I know the while loop probably isn't the right approach, but I can't figure out the proper one.

import matplotlib.pyplot as plt
import pdb

class getBval:
    def __init__(self):
        figWH = (8,5) # in
        self.fig = plt.figure(figsize=figWH)
        plt.plot(range(10),range(10),'k--')
        self.ax = self.fig.get_axes()[0]
        self.x = [] # will contain 4 "x" values
        self.lines = [] # will contain 2D line objects for each of 4 lines            

        self.connect =    self.ax.figure.canvas.mpl_connect
        self.disconnect = self.ax.figure.canvas.mpl_disconnect

        self.mouseMoveCid = self.connect("motion_notify_event",self.updateCurrentLine)
        self.clickCid     = self.connect("button_press_event",self.onClick)
    def updateCurrentLine(self,event):
        xx = [event.xdata]*2
        self.currentLine, = self.ax.plot(xx,self.ax.get_ylim(),'k')
        plt.show()
    def onClick(self, event):
        if event.inaxes:
            self.updateCurrentLine(event)
            self.x.append(event.xdata)
            self.lines.append(self.currentLine)
            del self.currentLine
            if len(self.x)==4:
                self.cleanup()
    def cleanup(self):
        self.disconnect(self.mouseMoveCid)
        self.disconnect(self.clickCid)
        return self


xvals = getBval()
print xvals.x
nivek
  • 515
  • 1
  • 7
  • 18
  • 1
    Due to the way that call backs work you can't get a 'return' pre-say but you can ask the object if a) it is done and b) what it's values are. Event driven programming can take a bit of time to get your head around. – tacaswell Feb 11 '14 at 13:53
  • Thanks. If a `return` statement isn't appropriate, what is the accepted protocol for waiting until the object is done? Just a `while` loop that repeatedly checks the value of a variable until it meets criteria (e.g. a list of 4 items in my problem)? This seems inefficient, but I don't know what to google for to find the right answer. Thanks again. – nivek Feb 11 '14 at 15:59
  • 1
    `signals` can be handy or just while + sleep (assuming you have threads). With what you are doing you might want to give up on a simple script and embrace the gui frame work you are using (embedding mpl) which would give you more call backs you can trigger when enough points are selceted. – tacaswell Feb 11 '14 at 16:54
  • _assuming you have threads_ Sleep + while prevents window from showing up at all (maybe this should tell me I don't have threads). Do you mean I need to import & use `threading` module? Also, by embedding mpl, do you mean [like this? (tkinter)](http://stackoverflow.com/questions/11140787/closing-pyplot-windows) (partly addresses issues I am having now) – nivek Feb 11 '14 at 22:56
  • no, embedding like this: http://matplotlib.org/examples/user_interfaces/embedding_in_tk.html – tacaswell Feb 12 '14 at 03:03

1 Answers1

0

I recommend you to use a Cursor widget for your vertical line, and just collect the x values of the clicks (not the entire Line2D), here is an example:

import matplotlib.pyplot as plt
import matplotlib.widgets as mwidgets

class getBval:

    def __init__(self):
        figWH = (8,5) # in
        self.fig = plt.figure(figsize=figWH)
        plt.plot(range(10),range(10),'k--')
        self.ax = self.fig.get_axes()[0]
        self.x = [] # will contain 4 "x" values
        self.lines = [] # will contain 2D line objects for each of 4 lines            

        self.cursor = mwidgets.Cursor(self.ax, useblit=True, color='k')
        self.cursor.horizOn = False

        self.connect = self.ax.figure.canvas.mpl_connect
        self.disconnect = self.ax.figure.canvas.mpl_disconnect

        self.clickCid = self.connect("button_press_event",self.onClick)

    def onClick(self, event):
        if event.inaxes:
            self.x.append(event.xdata)
            if len(self.x)==4:
                self.cleanup()

    def cleanup(self):
        self.disconnect(self.clickCid)
        plt.close()


xvals = getBval()
plt.show()

print xvals.x
Alvaro Fuentes
  • 16,937
  • 4
  • 56
  • 68
  • Thank you. I didn't know about mpl 'Widgets'--these look useful. I guess I didn't phrase my question well, but one obstacle I am having is not knowing how to control flow of my program---I want it to wait for user input, and then once fourth item is saved to list, continue to next part of program and use that data elsewhere. Any tips? Thanks. – nivek Feb 11 '14 at 16:00
  • 1
    Well, the combination `plt.show()` and the just added `plt.close()` on `cleanup` should do what you want (check the code of my answer), though not the better approach (at least from the OOP point of view) – Alvaro Fuentes Feb 11 '14 at 16:30
  • `plt.close` doesn't work with `plot.show()` ([see relevant post](http://stackoverflow.com/questions/11140787/closing-pyplot-windows))---but using `draw()` doesn't seem to work either so long as program flow is continuing. This includes `sleep` as @tcaswell indicated above. I am reluctant to learn tkinter just for this task, because I wouldn't expect to use it much (PhD student; this is just a hack for me to rapidly process some test data). Hmmm. Stumped for now! Thanks again. – nivek Feb 11 '14 at 23:07
  • Did you run my code and `plt.close()` didn't close the window?... I've just copy-pasted the code today and it runs smoothly, it even prints the list of x values as the final statement says. But my back-end is PyQt maybe it has something to do – Alvaro Fuentes Feb 12 '14 at 13:44