问题
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.
回答1:
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.
来源:https://stackoverflow.com/questions/25272701/matplotlib-pick-event-order-for-overlapping-artists