Matplotlib 中文用戶指南 7.3 事件處理及拾取

事件處理及拾取

原文:Event handling and picking

譯者:飛龍

協議:CC BY-NC-SA 4.0

matplotlib 使用了許多用戶界面工具包(wxpython患整,tkinter瘾婿,qt4寞宫,gtk 和 macosx)尉间,為了支持交互式平移和縮放圖形等功能蔚龙,擁有一套 API 通過按鍵和鼠標移動與圖形交互,并且『GUI中立』堕战,對開發(fā)人員十分有幫助默刚,所以我們不必重復大量的代碼來跨不同的用戶界面蹋绽。雖然事件處理 API 是 GUI 中立的芭毙,但它是基于 GTK 模型,這是 matplotlib 支持的第一個用戶界面卸耘。與標準 GUI 事件相比退敦,被觸發(fā)的事件也比 matplotlib 豐富一些,例如包括發(fā)生事件的matplotlib.axes.Axes的信息蚣抗。事件還能夠理解 matplotlib 坐標系侈百,并且在事件中以像素和數據坐標為單位報告事件位置。

事件連接

要接收事件翰铡,你需要編寫一個回調函數钝域,然后將你的函數連接到事件管理器,它是FigureCanvasBase的一部分锭魔。這是一個簡單的例子例证,打印鼠標點擊的位置和按下哪個按鈕:

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(np.random.rand(10))

def onclick(event):
    print('button=%d, x=%d, y=%d, xdata=%f, ydata=%f' %
          (event.button, event.x, event.y, event.xdata, event.ydata))

cid = fig.canvas.mpl_connect('button_press_event', onclick)

FigureCanvas的方法mpl_connect()返回一個連接id,它只是一個整數迷捧。 當你要斷開回調時织咧,只需調用:

fig.canvas.mpl_disconnect(cid)

注意

畫布僅保留回調的弱引用。 因此党涕,如果回調是類實例的方法烦感,你需要保留對該實例的引用巡社。 否則實例將被垃圾回收膛堤,回調將消失。

以下是可以連接到的事件晌该,在事件發(fā)生時發(fā)回給你的類實例以及事件描述:

| 事件名稱 | 類和描述 |
| --- | --- | --- |
| 'button_press_event' | MouseEvent - 鼠標按鈕被按下 |
| 'button_release_event' | MouseEvent - 鼠標按鈕被釋放 |
| 'draw_event' | DrawEvent - 畫布繪圖 |
| 'key_press_event' | KeyEvent - 按鍵被按下 |
| 'key_release_event' | KeyEvent - 按鍵被釋放 |
| 'motion_notify_event' | MouseEvent - 鼠標移動 |
| 'pick_event' | PickEvent - 畫布中的對象被選中 |
| 'resize_event' | ResizeEvent - 圖形畫布大小改變 |
| 'scroll_event' | MouseEvent - 鼠標滾輪被滾動 |
| 'figure_enter_event' | LocationEvent - 鼠標進入新的圖形 |
| 'figure_leave_event' | LocationEvent - 鼠標離開圖形 |
| 'axes_enter_event' | LocationEvent - 鼠標進入新的軸域 |
| 'axes_leave_event' | LocationEvent - 鼠標離開軸域 |

事件屬性

所有 matplotlib 事件繼承自基類matplotlib.backend_bases.Event肥荔,儲存以下屬性:

name

事件名稱

canvas

生成事件的FigureCanvas實例

guiEvent

觸發(fā) matplotlib 事件的 GUI 事件

最常見的事件是按鍵按下/釋放事件、鼠標按下/釋放和移動事件朝群。 處理這些事件的KeyEventMouseEvent類都派生自LocationEvent燕耿,它具有以下屬性:

x

x 位置,距離畫布左端的像素

y

y 位置姜胖,距離畫布底端的像素

inaxes

如果鼠標經過軸域誉帅,則為Axes實例

xdata

鼠標的x坐標,以數據坐標為單位

ydata

鼠標的y坐標,以數據坐標為單位

但我們看一看畫布的簡單示例蚜锨,其中每次按下鼠標時都會創(chuàng)建一條線段档插。

from matplotlib import pyplot as plt

class LineBuilder:
    def __init__(self, line):
        self.line = line
        self.xs = list(line.get_xdata())
        self.ys = list(line.get_ydata())
        self.cid = line.figure.canvas.mpl_connect('button_press_event', self)

    def __call__(self, event):
        print('click', event)
        if event.inaxes!=self.line.axes: return
        self.xs.append(event.xdata)
        self.ys.append(event.ydata)
        self.line.set_data(self.xs, self.ys)
        self.line.figure.canvas.draw()

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('click to build line segments')
line, = ax.plot([0], [0])  # empty line
linebuilder = LineBuilder(line)

plt.show()

我們剛剛使用的MouseEvent是一個LocationEvent,因此我們可以訪問event.xevent.xdata中的數據和像素坐標亚再。 除了LocationEvent屬性郭膛,它擁有:

button

按下的按鈕,None氛悬、1则剃、2、3如捅、'up'棍现、'down''up''down'用于滾動事件)

key

按下的鍵镜遣,None轴咱,任何字符,'shift'烈涮、'win'或者'control'

可拖拽的矩形練習

編寫使用Rectangle實例初始化的可拖動矩形類,但在拖動時會移動其x戈稿,y位置讶舰。 提示:你需要存儲矩形的原始xy位置鞍盗,存儲為rect.xy并連接到按下,移動和釋放鼠標事件跳昼。 當鼠標按下時,檢查點擊是否發(fā)生在你的矩形上(見matplotlib.patches.Rectangle.contains())敷存,如果是,存儲矩形xy和數據坐標為單位的鼠標點擊位置堪伍。 在移動事件回調中锚烦,計算鼠標移動的deltaxdeltay,并將這些增量添加到存儲的原始矩形涮俄,并重新繪圖尸闸。 在按鈕釋放事件中孕锄,只需將所有你存儲的按鈕按下數據重置為None苞尝。

這里是解決方案:

import numpy as np
import matplotlib.pyplot as plt

class DraggableRectangle:
    def __init__(self, rect):
        self.rect = rect
        self.press = None

    def connect(self):
        'connect to all the events we need'
        self.cidpress = self.rect.figure.canvas.mpl_connect(
            'button_press_event', self.on_press)
        self.cidrelease = self.rect.figure.canvas.mpl_connect(
            'button_release_event', self.on_release)
        self.cidmotion = self.rect.figure.canvas.mpl_connect(
            'motion_notify_event', self.on_motion)

    def on_press(self, event):
        'on button press we will see if the mouse is over us and store some data'
        if event.inaxes != self.rect.axes: return

        contains, attrd = self.rect.contains(event)
        if not contains: return
        print('event contains', self.rect.xy)
        x0, y0 = self.rect.xy
        self.press = x0, y0, event.xdata, event.ydata

    def on_motion(self, event):
        'on motion we will move the rect if the mouse is over us'
        if self.press is None: return
        if event.inaxes != self.rect.axes: return
        x0, y0, xpress, ypress = self.press
        dx = event.xdata - xpress
        dy = event.ydata - ypress
        #print('x0=%f, xpress=%f, event.xdata=%f, dx=%f, x0+dx=%f' %
        #      (x0, xpress, event.xdata, dx, x0+dx))
        self.rect.set_x(x0+dx)
        self.rect.set_y(y0+dy)

        self.rect.figure.canvas.draw()


    def on_release(self, event):
        'on release we reset the press data'
        self.press = None
        self.rect.figure.canvas.draw()

    def disconnect(self):
        'disconnect all the stored connection ids'
        self.rect.figure.canvas.mpl_disconnect(self.cidpress)
        self.rect.figure.canvas.mpl_disconnect(self.cidrelease)
        self.rect.figure.canvas.mpl_disconnect(self.cidmotion)

fig = plt.figure()
ax = fig.add_subplot(111)
rects = ax.bar(range(10), 20*np.random.rand(10))
drs = []
for rect in rects:
    dr = DraggableRectangle(rect)
    dr.connect()
    drs.append(dr)

plt.show()

附加題:使用動畫秘籍中討論的動畫 blit 技術野来,使動畫繪制更快更流暢。

附加題解決方案:

# draggable rectangle with the animation blit techniques; see
# http://www.scipy.org/Cookbook/Matplotlib/Animations
import numpy as np
import matplotlib.pyplot as plt

class DraggableRectangle:
    lock = None  # only one can be animated at a time
    def __init__(self, rect):
        self.rect = rect
        self.press = None
        self.background = None

    def connect(self):
        'connect to all the events we need'
        self.cidpress = self.rect.figure.canvas.mpl_connect(
            'button_press_event', self.on_press)
        self.cidrelease = self.rect.figure.canvas.mpl_connect(
            'button_release_event', self.on_release)
        self.cidmotion = self.rect.figure.canvas.mpl_connect(
            'motion_notify_event', self.on_motion)

    def on_press(self, event):
        'on button press we will see if the mouse is over us and store some data'
        if event.inaxes != self.rect.axes: return
        if DraggableRectangle.lock is not None: return
        contains, attrd = self.rect.contains(event)
        if not contains: return
        print('event contains', self.rect.xy)
        x0, y0 = self.rect.xy
        self.press = x0, y0, event.xdata, event.ydata
        DraggableRectangle.lock = self

        # draw everything but the selected rectangle and store the pixel buffer
        canvas = self.rect.figure.canvas
        axes = self.rect.axes
        self.rect.set_animated(True)
        canvas.draw()
        self.background = canvas.copy_from_bbox(self.rect.axes.bbox)

        # now redraw just the rectangle
        axes.draw_artist(self.rect)

        # and blit just the redrawn area
        canvas.blit(axes.bbox)

    def on_motion(self, event):
        'on motion we will move the rect if the mouse is over us'
        if DraggableRectangle.lock is not self:
            return
        if event.inaxes != self.rect.axes: return
        x0, y0, xpress, ypress = self.press
        dx = event.xdata - xpress
        dy = event.ydata - ypress
        self.rect.set_x(x0+dx)
        self.rect.set_y(y0+dy)

        canvas = self.rect.figure.canvas
        axes = self.rect.axes
        # restore the background region
        canvas.restore_region(self.background)

        # redraw just the current rectangle
        axes.draw_artist(self.rect)

        # blit just the redrawn area
        canvas.blit(axes.bbox)

    def on_release(self, event):
        'on release we reset the press data'
        if DraggableRectangle.lock is not self:
            return

        self.press = None
        DraggableRectangle.lock = None

        # turn off the rect animation property and reset the background
        self.rect.set_animated(False)
        self.background = None

        # redraw the full figure
        self.rect.figure.canvas.draw()

    def disconnect(self):
        'disconnect all the stored connection ids'
        self.rect.figure.canvas.mpl_disconnect(self.cidpress)
        self.rect.figure.canvas.mpl_disconnect(self.cidrelease)
        self.rect.figure.canvas.mpl_disconnect(self.cidmotion)

fig = plt.figure()
ax = fig.add_subplot(111)
rects = ax.bar(range(10), 20*np.random.rand(10))
drs = []
for rect in rects:
    dr = DraggableRectangle(rect)
    dr.connect()
    drs.append(dr)

plt.show()

鼠標進入和離開

如果希望在鼠標進入或離開圖形時通知你,你可以連接到圖形/軸域進入/離開事件徽级。 下面是一個簡單的例子聊浅,它改變了鼠標所在的軸域和圖形的背景顏色:

"""
Illustrate the figure and axes enter and leave events by changing the
frame colors on enter and leave
"""
import matplotlib.pyplot as plt

def enter_axes(event):
    print('enter_axes', event.inaxes)
    event.inaxes.patch.set_facecolor('yellow')
    event.canvas.draw()

def leave_axes(event):
    print('leave_axes', event.inaxes)
    event.inaxes.patch.set_facecolor('white')
    event.canvas.draw()

def enter_figure(event):
    print('enter_figure', event.canvas.figure)
    event.canvas.figure.patch.set_facecolor('red')
    event.canvas.draw()

def leave_figure(event):
    print('leave_figure', event.canvas.figure)
    event.canvas.figure.patch.set_facecolor('grey')
    event.canvas.draw()

fig1 = plt.figure()
fig1.suptitle('mouse hover over figure or axes to trigger events')
ax1 = fig1.add_subplot(211)
ax2 = fig1.add_subplot(212)

fig1.canvas.mpl_connect('figure_enter_event', enter_figure)
fig1.canvas.mpl_connect('figure_leave_event', leave_figure)
fig1.canvas.mpl_connect('axes_enter_event', enter_axes)
fig1.canvas.mpl_connect('axes_leave_event', leave_axes)

fig2 = plt.figure()
fig2.suptitle('mouse hover over figure or axes to trigger events')
ax1 = fig2.add_subplot(211)
ax2 = fig2.add_subplot(212)

fig2.canvas.mpl_connect('figure_enter_event', enter_figure)
fig2.canvas.mpl_connect('figure_leave_event', leave_figure)
fig2.canvas.mpl_connect('axes_enter_event', enter_axes)
fig2.canvas.mpl_connect('axes_leave_event', leave_axes)

plt.show()

對象拾取

你可以通過設置藝術家的picker屬性(例如,matplotlib Line2D旷痕,Text顽冶,PatchPolygon强重,AxesImage等)來啟用選擇,

picker屬性有多種含義:

None

選擇對于該藝術家已禁用(默認)

boolean

如果為True佃声,則啟用選擇倘要,當鼠標移動到該藝術家上方時,會觸發(fā)事件

float

如果選擇器是數字碗誉,則將其解釋為點的 ε 公差,并且如果其數據在鼠標事件的 ε 內,則藝術家將觸發(fā)事件甲喝。 對于像線條和補丁集合的一些藝術家,藝術家可以向生成的選擇事件提供附加數據糠溜,例如,在選擇事件的 ε 內的數據的索引非竿。

函數

如果拾取器是可調用的,則它是用戶提供的函數红柱,用于確定藝術家是否被鼠標事件擊中。 簽名為hit, props = picker(artist, mouseevent)韧骗,用于測試是否命中零聚。 如果鼠標事件在藝術家上,返回hit = True隶症,props是一個屬性字典,它們會添加到PickEvent屬性览徒。

通過設置picker屬性啟用對藝術家進行拾取后颂龙,你需要連接到圖畫布的pick_event,以便在鼠標按下事件中獲取拾取回調措嵌。 例如:

def pick_handler(event):
    mouseevent = event.mouseevent
    artist = event.artist
    # now do something with this...

傳給你的回調的PickEvent事件永遠有兩個屬性:

mouseevent

是生成拾取事件的鼠標事件企巢。鼠標事件具有像xy(顯示空間中的坐標,例如或听,距離左笋婿,下的像素)和xdataydata(數據空間中的坐標)的屬性缸濒。 此外粱腻,你可以獲取有關按下哪些按鈕绍些,按下哪些鍵耀鸦,鼠標在哪個軸域上面等信息。詳細信息請參閱matplotlib.backend_bases.MouseEvent袖订。

artist

生成拾取事件的Artist

另外揪漩,像Line2DPatchCollection的某些藝術家可以將附加的元數據(如索引)附加到滿足選擇器標準的數據中(例如吏口,行中在指定 ε 容差內的所有點)

簡單拾取示例

在下面的示例中,我們將行選擇器屬性設置為標量昂勒,因此它表示以點為單位的容差(72 點/英寸)舟铜。 當拾取事件位于距離線條的容差范圍時,將調用onpick回調函數塘娶,并且?guī)в性谑叭【嚯x容差內的數據頂點索引痊夭。 我們的onpick回調函數只打印在拾取位置上的數據。 不同的 matplotlib 藝術家可以將不同的數據附加到PickEvent虹曙。 例如番舆,Line2Dind屬性作為索引附加到拾取點下面的行數據中。 有關LinePickEvent屬性的詳細信息恨狈,請參閱pick()拴事。 這里是代碼:

import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('click on points')

line, = ax.plot(np.random.rand(100), 'o', picker=5)  # 5 points tolerance

def onpick(event):
    thisline = event.artist
    xdata = thisline.get_xdata()
    ydata = thisline.get_ydata()
    ind = event.ind
    points = tuple(zip(xdata[ind], ydata[ind]))
    print('onpick points:', points)

fig.canvas.mpl_connect('pick_event', onpick)

plt.show()

拾取練習

創(chuàng)建含有 100 個數組的數據集刃宵,包含 1000 個高斯隨機數,并計算每個數組的樣本平均值和標準差(提示:numpy數組具有meanstd方法)牲证,并制作 100 個均值與 100 個標準的xy標記圖坦袍。 將繪圖命令創(chuàng)建的線條連接到拾取事件,并繪制數據的原始時間序列捂齐,這些數據生成了被點擊的點。 如果在被點擊的點的容差范圍內存在多于一個點包颁,則可以使用多個子圖來繪制多個時間序列压真。

練習的解決方案:

"""
compute the mean and stddev of 100 data sets and plot mean vs stddev.
When you click on one of the mu, sigma points, plot the raw data from
the dataset that generated the mean and stddev
"""
import numpy as np
import matplotlib.pyplot as plt

X = np.random.rand(100, 1000)
xs = np.mean(X, axis=1)
ys = np.std(X, axis=1)

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('click on point to plot time series')
line, = ax.plot(xs, ys, 'o', picker=5)  # 5 points tolerance


def onpick(event):

    if event.artist!=line: return True

    N = len(event.ind)
    if not N: return True


    figi = plt.figure()
    for subplotnum, dataind in enumerate(event.ind):
        ax = figi.add_subplot(N,1,subplotnum+1)
        ax.plot(X[dataind])
        ax.text(0.05, 0.9, 'mu=%1.3f\nsigma=%1.3f'%(xs[dataind], ys[dataind]),
                transform=ax.transAxes, va='top')
        ax.set_ylim(-0.5, 1.5)
    figi.show()
    return True

fig.canvas.mpl_connect('pick_event', onpick)

plt.show()
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末滴肿,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子贵少,更是在濱河造成了極大的恐慌堆缘,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宽气,死亡現場離奇詭異潜沦,居然都是意外死亡,警方通過查閱死者的電腦和手機涝影,發(fā)現死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門争占,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人伯襟,你說我怎么就攤上這事∨炎” “怎么了稽揭?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長事镣。 經常有香客問我揪胃,道長,這世上最難降的妖魔是什么沮稚? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任册舞,我火速辦了婚禮,結果婚禮上盛杰,老公的妹妹穿的比我還像新娘藐石。我一直安慰自己,他們只是感情好于微,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布株依。 她就那樣靜靜地躺著,像睡著了一般抹锄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上伙单,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天吻育,我揣著相機與錄音,去河邊找鬼扫沼。 笑死庄吼,一個胖子當著我的面吹牛总寻,可吹牛的內容都是我干的。 我是一名探鬼主播渐行,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼祟印,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蕴忆?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤站蝠,失蹤者是張志新(化名)和其女友劉穎卓鹿,沒想到半個月后吟孙,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡藻治,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年稚失,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吸占。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖兼蕊,靈堂內的尸體忽然破棺而出件蚕,到底是詐尸還是另有隱情,我是刑警寧澤排作,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布妄痪,位于F島的核電站,受9級特大地震影響衫生,放射性物質發(fā)生泄漏。R本人自食惡果不足惜彭羹,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一泪酱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧愈腾,春花似錦岂津、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至危纫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間种蝶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工搪桂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留盯滚,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓内列,卻偏偏與公主長得像泼疑,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內容