文章/Matplotlib/第 42 章
交互:事件、picker 与 widgets
Matplotlib · 第 42 / 43 章
127 字约 1 分钟
事件处理
Python37 行
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots()
sc = ax.scatter(np.random.default_rng(0).normal(size=200),
np.random.default_rng(1).normal(size=200), s=40)
state = {'clicks': 0, 'last': None, 'zoom': 1.0}
def on_click(event):
state['clicks'] += 1
if event.inaxes is ax:
state['last'] = (event.xdata, event.ydata)
x, y = state['last'] # 元组不能直接 :.3f 格式化
print(f'click#{state["clicks"]} 数据坐标 ({x:.3f}, {y:.3f})')
def on_key(event):
if event.key == 'z':
state['zoom'] *= .8
ax.set_xlim(-state['zoom'], state['zoom']); ax.set_ylim(-state['zoom'], state['zoom'])
fig.canvas.draw_idle()
elif event.key == 'r':
ax.autoscale(); fig.canvas.draw_idle()
def n_cb():
# ★ 别断言绝对数量:matplotlib 自己就挂了 4 个(缩放平移、图例、工具栏联动),
# 所以刚画完的图上这里是 4,不是 0。
return len(fig.canvas.callbacks.callbacks.get('button_press_event', {}))
print(n_cb()) # 输出: 4 ← 框架自带的
cid1 = fig.canvas.mpl_connect('button_press_event', on_click)
cid2 = fig.canvas.mpl_connect('key_press_event', on_key)
print(n_cb()) # 输出: 5
cid3 = fig.canvas.mpl_connect('button_press_event', on_key) # 同一事件可注册多个
print(n_cb()) # 输出: 6
fig.canvas.mpl_disconnect(cid3) # 断开用返回的 cid,不是函数名
print(n_cb()) # 输出: 5| 事件名 | 触发 |
|---|---|
button_press_event / button_release_event |
鼠标按下/松开,event.button 是 1/2/3 |
motion_notify_event |
鼠标移动(最频繁,注意性能) |
scroll_event |
滚轮,event.step 为 ±1 |
key_press_event / key_release_event |
键盘,event.key |
resize_event / close_event |
尺寸变化 / 关闭 |
draw_event |
每帧绘制前触发 |
要点:event.inaxes 判断是否在轴内,event.xdata/ydata 是数据坐标(鼠标在轴外是 None),event.xpixel/ypixel 是像素坐标。draw_idle() 请求重绘(推荐),draw() 同步强制重绘。
拾取 picker
Python16 行
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots()
x = np.arange(10); y = x ** 2
sc = ax.scatter(x, y, s=60, picker=True) # True = 命中半径 5 pt
line, = ax.plot(x, y, lw=3, picker=3) # 数值 = 命中半径
bar = ax.bar(x, y, picker=True)
def on_pick(event):
art = event.artist
idx = event.ind if hasattr(event, 'ind') else None
print(f'picked {type(art).__name__} idx={idx}')
art.set_alpha(.4) # 高亮反馈
fig.canvas.mpl_connect('pick_event', on_pick)Widgets
Python34 行
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import Slider, Button, RadioButtons, RangeSlider
fig, ax = plt.subplots(figsize=(6, 4))
x = np.linspace(0, 4*np.pi, 1000)
line, = ax.plot([], [], lw=1.5, color='C0')
ax.set(xlabel='t', ylabel='sin(k·t)')
lines = {'sin': np.sin, 'cos': np.cos, 'tan': np.tan} # 名字 → 函数
ax_freq = fig.add_axes([.15, .12, .7, .03])
s_freq = Slider(ax_freq, '频率', 0.1, 10, valinit=1, valfmt='%.2f Hz') # 3.11:valfmt 支持 callable
ax_amp = fig.add_axes([.15, .06, .7, .03])
s_amp = Slider(ax_amp, '振幅', 0, 3, valinit=1)
ax_band = fig.add_axes([.15, .89, .7, .03])
s_band = RangeSlider(ax_band, '频率区间', 0.1, 10, valinit=(0.5, 3))
ax_radio = fig.add_axes([.02, .2, .15, .15], facecolor='0.9')
radio = RadioButtons(ax_radio, list(lines), active=0,
activecolor='crimson', layout='vertical') # 3.11 新增 layout
ax_btn = fig.add_axes([.78, .2, .12, .07])
btn = Button(ax_btn, '重置')
def update(val):
amp = s_amp.val
f = lines[radio.value_selected] # value_selected 是字符串本身,不是列表
y = amp * f(s_freq.val * x)
line.set_data(x, y) # x 和 y 必须同时给全长度
fig.canvas.draw_idle()
s_freq.on_changed(update); s_amp.on_changed(update); radio.on_clicked(update)
btn.on_clicked(lambda ev: (s_freq.reset(), s_amp.reset()))
update(s_freq.val)
plt.show()⚠️ widgets 占用的
add_axes区域会覆盖数据区,必须单独开位置,且不受layout='constrained'管辖(约束引擎会忽略手动 axes 的位置要求)。