Matplotlib chooses event order for duplicate artists

I am hitting a very strange problem with matplotlib pick events. I have two artists who are matched and not overlapped at the same time ("holes" and "bindings"). When I select one of them, during event processing, I move the other to where I just clicked (moving the "anchor" to the "hole"). Then, without doing anything else, the pick event from the moved executor (binding) is generated even if it was not where the first event was created . My only explanation for this is that somehow the event dispatcher is still traversing the executor layers when the event is handled, and hence hits the second executor after it is moved under the cursor.

So my question is, how do I select events (or any events for that matter), iterate over the intersecting artists on the canvas, and is there a way to control it? I think I get the desired behavior if it always moves from top to bottom (not bottom to top or randomly). I have not been able to find sufficient documentation and a lengthy search on SO has not brought up this exact problem. Below is a working example to illustrate the problem: PathCollections

from scatter

in the form of 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 tried setting the zorder so that the bindings are always above the holes, but that doesn't change the way the selection events are fired, and especially this fun phantom event.

EDIT : Context is an implementation of anchor solitaire, so I want to be able to pick up the anchor, then click on the empty hole to discard it. Currently, the same binding is immediately raised again as soon as it is dropped.

+3


source to share


1 answer


Time problem

The problem you are having is with how the pick_event function is called, matplotlib checks each executor, and if it is immediately called by the functio handler. The order in which it is tested depends on the order in which you define the holes and bindings (you can test it by changing the order in which you define the pegs and holes).

So, one way that seems like a good way to avoid this problem is to use the timer provided by matplotlib. The first time there is a select event, you simply add the executor to the queue, and then process this new data every milliseconds.

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)

This can be fixed by specifying specific pickers for your artists. However, you cannot select multiple items in one location.



See this example, which resolves the peg selection part if it shouldn't:

-Set the bindings definition to:

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

      

-Add pegs_picker function:

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 these pegs can only be picked up when they do not overlap the hole.

I think this might be the way to go for what you want, but since I don’t know exactly what it is, I cannot provide you with more advanced selection functions.

+3


source







All Articles