4

I'm hitting a very strange issue with matplotlib pick events. I have two artists that are both pickable and are non-overlapping to begin with ("holes" and "pegs"). When I pick one of them, during the event handling I move the other one to where I just clicked (moving a "peg" into the "hole"). Then, without doing anything else, a pick event from the moved artist (the peg) is generated even though it wasn't there when the first event was generated. My only explanation for it is that somehow the event manager is still moving through artist layers when the event is processed, and therefore hits the second artist after it is moved under the cursor.

So then my question is - how do pick events (or any events for that matter) iterate through overlapping artists on the canvas, and is there a way to control it? I think I would get my desired behavior if it moved from the top down always (rather than bottom up or randomly). I haven't been able to find sufficient enough documentation, and a lengthy search on SO has not revealed this exact issue. Below is a working example that illustrates the problem, with PathCollections from scatter as pegs and holes:

import matplotlib.pyplot as plt
import sys

class peg_tester():
    def __init__(self):
        self.fig = plt.figure(figsize=(3,1))
        self.ax = self.fig.add_axes([0,0,1,1])
        self.ax.set_xlim([-0.5,2.5])
        self.ax.set_ylim([-0.25,0.25])
        self.ax.text(-0.4, 0.15, 'One click on the hole, and I get 2 events not 1',
                     fontsize=8)

        self.holes = self.ax.scatter([1], [0], color='black', picker=0)
        self.pegs = self.ax.scatter([0], [0], s=100, facecolor='#dd8800',
                                    edgecolor='black', picker=0)

        self.fig.canvas.mpl_connect('pick_event', self.handler)
        plt.show()

    def handler(self, event):
        if event.artist is self.holes:
            # If I get a hole event, then move a peg (to that hole) ...
            # but then I get a peg event also with no extra clicks!
            offs = self.pegs.get_offsets()
            offs[0,:] = [1,0] # Moves left peg to the middle
            self.pegs.set_offsets(offs)
            self.fig.canvas.draw()
            print 'picked a hole, moving left peg to center'
        elif event.artist is self.pegs:
            print 'picked a peg'
        sys.stdout.flush() # Necessary when in ipython qtconsole

if __name__ == "__main__":
    pt = peg_tester()

I have tried setting the zorder to make the pegs always above the holes, but that doesn't change how the pick events are generated, and particularly this funny phantom event.

EDIT: The context is an implementation of peg solitaire, so I want to be able to pick up a peg then click on an empty hole to drop it there. Currently the same peg is immediately picked up again as soon as it is dropped.

Ajean
  • 5,528
  • 14
  • 46
  • 69

1 Answers1

4

Problem with the timing

The problem you encounter is due to the way the pick_event function is called, matplotlib test each artist and if it's picked immediatly call you handler functio. The order in which it is tested depend on the order in which you defined holes and pegs (you can verifiy it by changing the order of the definition of pegs and holes).

So one way that seems good to avoid this problem is to use the timer provided by matplotlib. In a first time when there is a picking event you just add the artist to a queue and then every x milliseconds you process this new data.

Here's an example of this kind of implementation :

import matplotlib.pyplot as plt
import sys

class peg_tester():
    def __init__(self):
        self.fig = plt.figure(figsize=(3,1))
        self.ax = self.fig.add_axes([0,0,1,1])
        self.ax.set_xlim([-0.5,2.5])
        self.ax.set_ylim([-0.25,0.25])
        self.ax.text(-0.4, 0.15,
                'One click on the hole, and I get 2 events not 1',
                fontsize=8)

        self.holes = self.ax.scatter([1], [0], color='black', picker=0)
        self.pegs = self.ax.scatter([0], [0], s=100, facecolor='#dd8800',
                edgecolor='black', picker=0)

        self.fig.canvas.mpl_connect('pick_event', self.handler)

        # Creating a timer with a interval of 100 ms
        self.timer=self.fig.canvas.new_timer(interval=100)
        # Queue of pegs and holes that have been picked and waiting to be processed
        self.list_pegs_holes=[]
        self.timer.add_callback(self.update_pos,self.list_pegs_holes)
        self.timer.start()
        plt.show()

    # Add the artist to the queue of artists awaiting to be processed
    def handler(self, event):
        self.list_pegs_holes.append(event.artist)

    # Management of the queue
    def update_pos(self,list_pegs_holes):
        while len(list_pegs_holes)!=0:
            artist=list_pegs_holes.pop(0)
            if artist is self.holes:
                # If I get a hole event, then move a peg (to that hole) ...
                # but then I get a peg event also with no extra clicks!
                offs = self.pegs.get_offsets()
                offs[0,:] = [1,0] # Moves left peg to the middle
                self.pegs.set_offsets(offs)
                self.fig.canvas.draw()
                print 'picked a hole, moving left peg to center'
            elif artist is self.pegs:
                print 'picked a peg'
            sys.stdout.flush() # Necessary when in ipython qtconsole

if __name__ == "__main__":
    pt = peg_tester()

Most of the code is what you provided, I just added the timer implementation.

Not optimal (outdated)

It is possible to fix this behavior by defining specific pickers for your artists. However you won't be able to select several item in the same spot.

See this example that solve the part about the pick of pegs when it shouldn't :

-Replace the definition of pegs by :

self.pegs = self.ax.scatter([0], [0], s=100, facecolor='#dd8800',edgecolor='black', picker=self.pegs_picker)

-Add the function pegs_picker :

def pegs_picker(figure,pegs,event):
    # Check that the pointer is not on holes
    if figure.holes.contains(event)[0]:
        return False, dict()
    else:   
        return True, dict()

With that pegs are pickable only when they're not superposed with a hole.

I think that might be the way to go to have the behavior you want, but since I don't know exactly what it is, I can't provide you more refined picking functions.

takeo999
  • 151
  • 4
  • Thank you very much for your reply! There may be some tweak to this that is helpful, but it doesn't actually solve the problem. The context is a peg solitaire game implementation, (I'll add that to the question), so pegs will always be "superimposed" on holes. I want to be able to "pick up" a peg, then click on an empty hole to drop the peg (and then remove the peg that was jumped over). The problem is that the "dropping" operation results in the immediate "picking up" of the peg again, and there's no way I could see to distinguish that pick-up from a legitimate pick-up. – Ajean Aug 22 '14 at 16:41
  • Ok, thanks for explaining, not being a native english speaker I didn't understood it was about a peg solitaire. I think my edited answer is enough to solve the problem of these pick events, if you're still encountering an issue please let me know. – takeo999 Aug 23 '14 at 14:19
  • Fantastic! This did exactly what I was looking for, I had no idea how to manipulate the timing. Thank you! – Ajean Aug 24 '14 at 04:37