在Matplotlib中从一组重叠的艺术家中挑选一个艺术家

时间:2019-05-07 04:53:13

标签: python matplotlib event-handling

这个问题与下面的两个问题密切相关,但是这个问题更笼统。

Matplotlib pick event order for overlapping artists

Multiple pick events interfering


问题:

在单个画布上拾取重叠的艺术家时,将为每个艺术家创建单独的拾取事件。在下面的示例中,单击一个红色点会两次调用<topic2>,一次调用on_pick,一次调用lines。由于points位于该行上方(考虑到它们各自的points值),我希望只为最顶级的艺术家(在这种情况下为zorder)生成一个单独的选择事件。

示例

line_with_overlapping_points

points

混乱的解决方案:

一种解决方案是使用Matplotlib的import numpy as np from matplotlib import pyplot def on_pick(event): if event.artist == line: print('Line picked') elif event.artist == points: print('Point picked') # create axes: pyplot.close('all') ax = pyplot.axes() # add line: x = np.arange(10) y = np.random.randn(10) line = ax.plot(x, y, 'b-', zorder=0)[0] # add points overlapping the line: xpoints = [2, 4, 7] points = ax.plot(x[xpoints], y[xpoints], 'ro', zorder=1)[0] # set pickers: line.set_picker(5) points.set_picker(5) ax.figure.canvas.mpl_connect('pick_event', on_pick) pyplot.show() ,然后计算鼠标与所有艺术家之间的距离,如下所示。但是,此解决方案非常混乱,因为添加其他重叠的美术师会使该代码变得相当复杂,从而增加了要检查的案例和条件的数量。

button_press_event

问题摘要: 是否有更好的方法从一组重叠的艺术家中选择最高的艺术家?

理想情况下,我希望能够执行以下操作:

def on_press(event):
    if event.xdata is not None:
        x,y   = event.xdata, event.ydata  #mouse click coordinates
        lx,ly = line.get_xdata(), line.get_ydata()     #line point coordinates
        px,py = points.get_xdata(), points.get_ydata() #points
        dl    = np.sqrt((x - lx)**2 + (y - ly)**2)     #distances to line points
        dp    = np.sqrt((x - px)**2 + (y - py)**2)     #distances to points
        if dp.min() < 0.05:
            print('Point selected')
        elif dl.min() < 0.05:
            print('Line selected')


pyplot.close('all')
ax      = pyplot.axes()

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

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

# set picker:
ax.figure.canvas.mpl_connect('button_press_event', on_press)

pyplot.show()

表示将选择pyplot.set_pick_stack( [points, line] ) 而不是points进行重叠选择。

2 个答案:

答案 0 :(得分:1)

button_press_event发生的情况下创建自己的事件可能是最简单的。为了说明问题中表达的“ set_pick_stack”的想法,可能如下所示。想法是存储一组艺术家,并在button_press_event中检查该事件是否包含在艺术家中。然后在自定义onpick函数上触发回调。

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

class PickStack():
    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)

    def fire_pick_event(self, event):
        if not event.inaxes:
            return
        cont = [a for a in self.stack if a.contains(event)[0]]
        if not 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)

用法看起来像

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)


def onpick(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()

这里的想法是按挑选事件所需的顺序提供艺术家列表。当然,也可以使用zorder确定顺序。看起来像

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

__init__函数中。

由于注释中引起了问题,我们来看看为什么matplotlib不自动执行此过滤。好吧,首先,我想至少在50%的情况下都是不希望的,因为您希望为每个艺术家挑选的事件。而且,对于matplotlib而言,为每个受mouseevent击中的艺术家发出事件比过滤它们要容易得多。对于前者,您只需比较坐标(非常类似于问题中的“混乱解决方案”)。很难只获得最高的艺术家;当然,如果两个艺术家的zorder不同,这是有可能的,但是如果他们具有相同的zorder,则只是他们在子代轴列表中的出现顺序决定了哪个在前。 “ pick_upmost_event”将需要检查完整的轴子代堆栈,以找出要选择的轴。话虽如此,这并不是没有可能,但是到目前为止,可能还没有人相信这样做是值得的。当然,人们可以针对这样的“ pick_upmost_event”提出问题或向Matplotlib提交PR的实现。

答案 1 :(得分:0)

已编辑: 首先,保持追踪您正在绘制的所有艺术家通常是一个好主意;因此,我建议保留一个字典artists_dict,其中所有绘制的元素都作为 key ,您可以使用它来存储一些有用的值(例如在另一个字典中)。

除此之外,以下代码依赖于使用计时器来收集list_artists中的触发事件,然后每隔100ms通过on_pick(list_artists)处理此列表。在此功能中,您可以检查是否选中了一位或多位艺术家,然后找到zorder最高的艺术家并做某事

import numpy as np
from matplotlib import pyplot

artists_dict={}


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

def on_pick(list_artists):
    ## if you still want to use the artist dict for something:
    # print([artists_dict[a] for a in list_artists])

    if len(list_artists)==1:
        print('do something to the line here')

        list_artists.pop(0)## cleanup
    elif len(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()