Skip to content Skip to sidebar Skip to footer

Picking A Single Artist From A Set Of Overlapping Artists In Matplotlib

This question is closely related to the two below, but this question is more general. Matplotlib pick event order for overlapping artists Multiple pick events interfering The prob

Solution 1:

It might be easiest to create your own event on button_press_events happening. To pusue the idea of a "set_pick_stack" expressed in the question, this could look as follows. The idea is to store a set of artists and upon a button_press_event check if that event is contained by the artist. Then fire a callback on a custom onpick function.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backend_bases import PickEvent

classPickStack():
    def__init__(self, stack, on_pick):
        self.stack = stack
        self.ax = [artist.axes for artist in self.stack][0]
        self.on_pick = on_pick
        self.cid = self.ax.figure.canvas.mpl_connect('button_press_event',
                                                     self.fire_pick_event)

    deffire_pick_event(self, event):
        ifnot event.inaxes:
            return
        cont = [a for a in self.stack if a.contains(event)[0]]
        ifnot cont:
            return
        pick_event = PickEvent("pick_Event", self.ax.figure.canvas, 
                               event, cont[0],
                               guiEvent=event.guiEvent,
                               **cont[0].contains(event)[1])
        self.on_pick(pick_event)

Usage would look like

fig, ax = plt.subplots()

# add line:
x       = np.arange(10)
y       = np.random.randn(10)
line,   = ax.plot(x, y, 'b-', label="Line", picker=5)

# add points overlapping the line:
xpoints = [2, 4, 7]
points,  = ax.plot(x[xpoints], y[xpoints], 'ro', label="Points", picker=5)


defonpick(event):
    txt = f"You picked {event.artist} at xy: " + \
          f"{event.mouseevent.xdata:.2f},{event.mouseevent.xdata:.2f}" + \
          f" index: {event.ind}"print(txt)

p = PickStack([points, line], onpick)

plt.show()

The idea here is to supply a list of artists in the order desired for pick events. Of course one could also use zorder to determine the order. This could look like

self.stack = list(stack).sort(key=lambda x: x.get_zorder(), reverse=True)

in the __init__ function.

Because the question arouse in the comments, let's look at why matplotlib does not do this filtering automatically. Well, first I would guess that it's undesired in 50% of the cases at least, where you would like an event for every artist picked. But also, it is much easier for matplotlib to just emit an event for every artist that gets hit by a mouseevent than to filter them. For the former, you just compare coordinates (much like the "messy solution" from the question). Whereas it is hard to get the topmost artist only; of course in the case two artists have differing zorder, it would be possible, but if they have the same zorder, it's just the order they appear in the lists of axes children that determines which is in front. A "pick_upmost_event" would need to check the complete stack of axes children to find out which one to pick. That being said, it's not impossible, but up to now probably noone was convinced it's worth the effort. Surely, people can open an issue or submit an implementation as PR to matplotlib for such "pick_upmost_event".

Solution 2:

Edited: First of all, it's generally a good idea to keep track of all the artists you're drawing; hence I'd suggest to keep a dictionary artists_dict with all plotted elements as keys, which you can use to store some helpful values (e.g. within another dict).

Apart from this, the code below relies on using a timer which collects the fired events in list_artists, and then processes this list every 100ms via on_pick(list_artists). Within this function, you can check whether one or more than one artists got picked on, then find the one with the highest zorder and do something to it.

import numpy as np
from matplotlib import pyplot

artists_dict={}


defhandler(event):
    print('handler fired')
    list_artists.append(event.artist)

defon_pick(list_artists):
    ## if you still want to use the artist dict for something:# print([artists_dict[a] for a in list_artists])iflen(list_artists)==1:
        print('do something to the line here')

        list_artists.pop(0)## cleanupeliflen(list_artists)>1:### only for more than one plot item
        zorder_list=[ a.get_zorder() for a in list_artists]
        print('highest item has zorder {0}, is at position {1} of list_artists'.format(np.max(zorder_list),np.argmax(zorder_list)))
        print('do something to the scatter plot here')
        print(list(zip(zorder_list,list_artists)))

        list_artists[:]=[]
    else:
        return# create axes:
pyplot.close('all')
fig,ax=pyplot.subplots()

# add line:
x      = np.arange(10)
y      = np.random.randn(10)
line   = ax.plot(x, y, 'b-', zorder=0)[0]

## insert the "line” into our artists_dict with some metadata#  instead of inserting zorder:line.get_zorder(), you could also #  directly insert zorder:0 of course.
artists_dict[line]={'label':'test','zorder':line.get_zorder()}

# add points overlapping the line:
xpoints = [2, 4, 7]
points  = ax.plot(x[xpoints], y[xpoints], 'ro', zorder=1)[0]

## and we also add the scatter plot 'points'
artists_dict[points]={'label':'scatters','zorder':points.get_zorder()}

# set pickers:
line.set_picker(5)
points.set_picker(5)


## connect to handler function
ax.figure.canvas.mpl_connect('pick_event', handler)
list_artists=[]
## wait a bit
timer=fig.canvas.new_timer(interval=100)
timer.add_callback(on_pick,list_artists)
timer.start()

pyplot.show()

Post a Comment for "Picking A Single Artist From A Set Of Overlapping Artists In Matplotlib"