【问题标题】:Picking a single artist from a set of overlapping artists in Matplotlib从 Matplotlib 中的一组重叠艺术家中挑选一个艺术家
【发布时间】:2019-05-07 04:53:12
【问题描述】:

这个问题和下面两个关系密切,但是这个问题比较笼统。

Matplotlib pick event order for overlapping artists

Multiple pick events interfering


问题:

在单个画布上选择重叠的艺术家时,会为每个艺术家创建单独的选择事件。在下面的示例中,单击红点会调用两次on_pick,一次调用lines,一次调用points。由于points 位于该线之上(考虑到它们各自的zorder 值),我宁愿只为最高艺术家生成一个选择事件(在本例中:points)。

示例

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

凌乱的解决方案:

一种解决方案是使用 Matplotlib 的 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 将被选中而不是line 以进行重叠选择。

【问题讨论】:

  • event.artist.get_zorder()怎么样?
  • 因为我想要一个单一的选择事件,其艺术家在指定鼠标点的所有艺术家中具有最大的 zorder 值。 zorder 确实可以在上面的 'button_press_event' 解决方案中使用,但是这个 'button_press_event' 只是一个 hack,而不是一个选择事件。
  • 我已经重写了我的答案,包括一个使用提示进行选择事件的示例。

标签: python matplotlib event-handling


【解决方案1】:

button_press_events 发生时创建自己的事件可能是最简单的。为了提出问题中表达的“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__ 函数中。

因为在cmets中引起了这个问题,我们来看看matplotlib为什么不自动做这个过滤。好吧,首先我猜想至少在 50% 的情况下这是不受欢迎的,在这种情况下,您希望为每个 挑选的艺术家举办一个活动。而且,对于 matplotlib 来说,为每个被鼠标事件击中的艺术家发出一个事件比过滤它们要容易得多。对于前者,您只需比较坐标(很像问题中的“混乱解决方案”)。鉴于很难仅获得顶级艺术家;当然,如果两个艺术家的 zorder 不同,这是可能的,但如果他们有相同的 zorder,那么只有他们出现在轴子列表中的顺序才能确定哪个在前面。 “pick_upmost_event”需要检查轴子的完整堆栈以找出要选择的轴。话虽如此,这并非不可能,但到目前为止,可能没有人相信这是值得的。当然,人们可以针对此类“pick_upmost_event”打开问题或将实现作为 PR 提交给 matplotlib。

【讨论】:

  • 谢谢。这很好用。我期待 Matplotlib 有一个更简单的内置解决方案。
  • 不确定期望从何而来,但现在它已经写下来了,您可以在任何地方复制该类并按原样使用它,这很简单,对吧?
  • 我预计这是因为其他 GUI 环境(如 Qt 和 wxPython)根据用户的最高控制来解析选择。在大多数 GUI 中,我知道通过单击鼠标(不拖动)来选择多个对象是不可能的。所以我不明白在单击一次按钮时发出多个选择事件的目的。
  • 我明白了。我在回答中添加了一些关于为什么我认为还没有人实施过这样的统一事件的问题。
  • 当我需要转换 motion_notify_event 以选择它悬停的对象时,此示例很有帮助。由于motion_notify_event 没有这样做,我只是像您的示例一样将其传递给PickEvent
【解决方案2】:

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

除此之外,下面的代码依赖于使用一个计时器来收集list_artists 中触发的事件,然后通过on_pick(list_artists) 每100 毫秒处理一次这个列表。在此功能中,您可以检查是否选中了一位或多位艺术家,然后找到具有最高 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()

【讨论】:

  • 我无法让它在 Python 3 中运行(错误:“AttributeError: 'NoneType' object has no attribute 'format'”),但我明白了这个想法。但是,我认为这不会起作用,因为它不会进行特定于点的 zorder 检查。也就是说,line 应该是 points 艺术家未涵盖的所有点上的最高艺术家。在此代码中,points 艺术家始终位于最顶端。
  • 对不起,我在'you clicked on ..' print 语句中放错了括号,我在上面修复了它。如果我对您的理解正确,您希望始终让 on_click() 仅对情节中最顶层的项目起作用;这意味着您需要cue your click events or apply a custom picker,我可能会选择前者,即为收集的事件提示和比较 zorder。
  • 这行得通,但使用计时器比上面的button_press_event 解决方案更麻烦。我发现@ImportanceOfBeingErnest 的解决方案更简洁、更通用。还是谢谢你的建议!
猜你喜欢
  • 2017-02-19
  • 1970-01-01
  • 1970-01-01
  • 2012-07-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多