上篇博客:python【matplotlib】鼠标拖动滚动缩放坐标范围和拖动图例共存,得到启发,我们已经可以通过鼠标拖动缩放坐标范围和移动图例,来实现动态交互式绘图了,对于x轴是时间序列的绘图需求,能否也实现动态交互式绘图呢?
答案是肯定的,接下来我将详细描述其实现的方式。
首先,我们需要导入必要的库,包括datetime、timedelta、matplotlib.pyplot等。同时,我们设置中文显示字体为"Microsoft YaHei",并准备一些时间序列数据。
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import matplotlib.dates as mdate
from matplotlib.ticker import MaxNLocator
plt.rcParams["font.family"] = "Microsoft YaHei"
x = ['2023/12/20 15:23:01', '2023/12/20 15:23:02', '2023/12/20 15:23:03', ]
接下来,我们定义一个函数get_time,用于将时间字符串列表转换为datetime对象。
def get_time(time_list):
date_object = []
for time in time_list:
date_object.append(datetime.strptime(time, "%Y/%m/%d %H:%M:%S")) # TODO 重点,把字符串转换成时间对象
return date_object
然后,我们调用这个函数将时间字符串列表转换为datetime对象,并打印结果。
date_object = get_time(x)
print(date_object)
现在,我们开始创建一个示例时间序列图。在这个例子中,我们将时间序列数据与位移数据绘制在同一张图上。
# 创建一个示例图形
fig, ax = plt.subplots()
ax.plot(date_object, [2, 0, 6, ], label=f'位移', ls='-') # TODO 重点,直接把datetime类型的时间赋值为x轴即可
ax.xaxis.set_major_formatter(mdate.DateFormatter('%H:%M:%S'))#设置时间标签显示格式
为了更好地交互,我们设置了x轴的日期格式为"%H:%M:%S",并打印出一些关于时间范围的信息。
one_minutes = timedelta(minutes=1)
one_second = timedelta(seconds=1)
dates = mdate.drange(date_object[-1], date_object[-1] + one_minutes, one_second) # TODO 取某个时间范围,类似range()
print([mdate.num2date(t).strftime("%Y-%m-%d %H:%M:%S") for t in dates])
x_min, x_max = ax.get_xlim() # 这里获取到的x轴的范围是时间的float格式,用mdate.num2date()可以转为日期类型
print(x_min, x_max)
print("==============")
print(mdate.num2date(x_min), mdate.num2date(x_max)) # float 转 日期
print("=============")
print(mdate.datestr2num(x[0]), mdate.datestr2num(x[2])) # 日期转 float
print(date_object[0].timestamp(), date_object[2].timestamp()) # 日期转 float 和ax.get_xlim()获取到的不一样
我们接下来进行一些坐标轴的调整,包括去除坐标轴两端的空白、设置整数刻度、旋转x轴标签等。
xticks = ax.get_xticks()
yticks = ax.get_yticks()
# 计算相邻刻度位置之间的差异
xtick_size = xticks[1] - xticks[0]
ytick_size = yticks[1] - yticks[0]
print(">>>>>>>")
print(xticks)
print(xtick_size)
print(type(mdate.num2date(x_min)))
ax.margins(0) # 调整坐标轴两端的空白为0
# ax.xaxis.set_major_locator(MaxNLocator(integer=True)) # (integer=True) 只有整数刻度才会显示
# 重新设置x轴的范围,这里不能直接用x_min, x_max,因为x_min, x_max是float类型,需要用mdate.num2date()转为日期类型
one_second = timedelta(seconds=1) # 1秒的时间
ax.set(xlim=(mdate.num2date(x_min)-one_second, mdate.num2date(x_max) + one_second)) # 左右分别增大1秒
plt.xticks(rotation=30) # 设置x轴的刻度标签 旋转30度,防止标签重叠
print(ax.get_xticks())
现在,我们为图表添加鼠标拖动和滚轮滚动的交互功能。这部分代码包括处理鼠标事件的函数和连接事件的操作。
startx = 0
starty = 0
mPress = False
# 鼠标拖动 处理事件
def call_move(event):
# print(event.name)
global mPress
global startx
global starty
mouse_x = event.x
mouse_y = event.y
axtemp = event.inaxes
if event.name == 'button_press_event':
if axtemp and event.button == 1:
if axtemp.get_legend():
legend_bbox = axtemp.get_legend().get_window_extent()
left_bottom = legend_bbox.get_points()[0]
right_top = legend_bbox.get_points()[1]
if left_bottom[0] <= mouse_x <= right_top[0] and left_bottom[1] <= mouse_y <= right_top[1]:
# print("在图例上按下鼠标")
# 在图例上按下鼠标
mPress = False
return
# 没有图例的情况
# print("在 Axes 上按下鼠标")
# 在 Axes 上按下鼠标
mPress = True
startx = event.xdata
starty = event.ydata
return
elif event.name == 'button_release_event':
if axtemp and event.button == 1:
mPress = False
elif event.name == 'motion_notify_event':
if axtemp and event.button == 1 and mPress:
if axtemp.get_legend():
legend_bbox = axtemp.get_legend().get_window_extent()
left_bottom = legend_bbox.get_points()[0]
right_top = legend_bbox.get_points()[1]
if left_bottom[0] <= mouse_x <= right_top[0] and left_bottom[1] <= mouse_y <= right_top[1]:
print("在图例上移动鼠标")
# 在图例上按下鼠标
mPress = False
return
# 没有图例的情况
# print("在Axes上移动鼠标")
x_min, x_max = axtemp.get_xlim()
y_min, y_max = axtemp.get_ylim()
w = x_max - x_min
h = y_max - y_min
# print(event)
# 移动
mx = event.xdata - startx
my = event.ydata - starty
# 注意这里, -mx, 因为下一次 motion事件的坐标,已经是在本次做了移动之后的坐标系了,所以要体现出来
# startx=event.xdata-mx startx=event.xdata-(event.xdata-startx)=startx, 没必要再赋值了
# starty=event.ydata-my
# print(mx,my,x_min,y_min,w,h)
axtemp.set(xlim=(x_min - mx, x_min - mx + w))
axtemp.set(ylim=(y_min - my, y_min - my + h))
fig.canvas.draw_idle() # 绘图动作实时反映在图像上
return
# 滚轮滚动 处理事件
def call_scroll(event):
# print(event.name)
axtemp = event.inaxes
# print('event:', event)
# print(event.xdata, event.ydata)
# 计算放大缩小后, xlim 和ylim
if axtemp:
x_min, x_max = axtemp.get_xlim()
y_min, y_max = axtemp.get_ylim()
print(x_min, x_max)
w = x_max - x_min
h = y_max - y_min
curx = event.xdata
cury = event.ydata
curXposition = (curx - x_min) / w
curYposition = (cury - y_min) / h
if event.button == 'down':
# print('befor:', w, h)
w = w * 1.1 # 1.1
h = h * 1.1
# print('down', w, h)
elif event.button == 'up':
# print('befor:', w, h)
w = w / 1.1
h = h / 1.1
# print('up', w, h)
# print(curXposition, curYposition)
newx = curx - w * curXposition
newy = cury - h * curYposition
axtemp.set(xlim=(newx, newx + w))
axtemp.set(ylim=(newy, newy + h))
# axtemp.margins(0) # 调整坐标轴两端的空白
fig.canvas.draw_idle() # 绘图动作实时反映在图像上
fig.canvas.mpl_connect('scroll_event', call_scroll)
fig.canvas.mpl_connect('button_press_event', call_move)
fig.canvas.mpl_connect('button_release_event', call_move)
# fig.canvas.mpl_connect('draw_event', call_move)
fig.canvas.mpl_connect('motion_notify_event', call_move)
最后,通过plt.show()展示交互式时间序列图。
plt.show()
通过以上步骤,我们成功创建了一个交互式的时间序列图,支持鼠标拖动和滚轮滚动操作,方便用户查看不同时间范围的数据。希望这篇博客对你理解Matplotlib的交互功能有所帮助。
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import matplotlib.dates as mdate
from matplotlib.ticker import MaxNLocator
plt.rcParams["font.family"] = "Microsoft YaHei"
x = ['2023/10/24 15:23:01', '2023/10/24 15:23:02', '2023/10/24 15:23:03', ]
def get_time(time_list):
date_object = []
for time in time_list:
date_object.append(datetime.strptime(time, "%Y/%m/%d %H:%M:%S")) # TODO 重点,把字符串转换成时间对象
return date_object
date_object = get_time(x)
# 从字符串到时间对象
# date_string = "2023-01-01 12:30:00"
# date_object = datetime.strptime(date_string, "%Y-%m-%d %H:%M:%S")
print(date_object)
# 创建一个示例图形
fig, ax = plt.subplots()
ax.plot(date_object, [2, 0, 6, ], label=f'位移', ls='-') # TODO 重点,直接把datetime类型的时间赋值为x轴即可
ax.xaxis.set_major_formatter(mdate.DateFormatter('%H:%M:%S'))#设置时间标签显示格式
one_minutes = timedelta(minutes=1)
one_second = timedelta(seconds=1)
dates = mdate.drange(date_object[-1], date_object[-1] + one_minutes, one_second) # TODO 取某个时间范围,类似range()
print([mdate.num2date(t).strftime("%Y-%m-%d %H:%M:%S") for t in dates])
x_min, x_max = ax.get_xlim() # 这里获取到的x轴的范围是时间的float格式,用mdate.num2date()可以转为日期类型
print(x_min, x_max)
print("==============")
print(mdate.num2date(x_min), mdate.num2date(x_max)) # float 转 日期
print("=============")
print(mdate.datestr2num(x[0]), mdate.datestr2num(x[2])) # 日期转 float
print(date_object[0].timestamp(), date_object[2].timestamp()) # 日期转 float 和ax.get_xlim()获取到的不一样
xticks = ax.get_xticks()
yticks = ax.get_yticks()
# 计算相邻刻度位置之间的差异
xtick_size = xticks[1] - xticks[0]
ytick_size = yticks[1] - yticks[0]
print(">>>>>>>")
print(xticks)
print(xtick_size)
print(type(mdate.num2date(x_min)))
ax.margins(0) # 调整坐标轴两端的空白为0
# ax.xaxis.set_major_locator(MaxNLocator(integer=True)) # (integer=True) 只有整数刻度才会显示
# 重新设置x轴的范围,这里不能直接用x_min, x_max,因为x_min, x_max是float类型,需要用mdate.num2date()转为日期类型
one_second = timedelta(seconds=1) # 1秒的时间
ax.set(xlim=(mdate.num2date(x_min)-one_second, mdate.num2date(x_max) + one_second)) # 左右分别增大1秒
plt.xticks(rotation=30) # 设置x轴的刻度标签 旋转30度,防止标签重叠
print(ax.get_xticks())
startx = 0
starty = 0
mPress = False
# 鼠标拖动 处理事件
def call_move(event):
# print(event.name)
global mPress
global startx
global starty
mouse_x = event.x
mouse_y = event.y
axtemp = event.inaxes
if event.name == 'button_press_event':
if axtemp and event.button == 1:
if axtemp.get_legend():
legend_bbox = axtemp.get_legend().get_window_extent()
left_bottom = legend_bbox.get_points()[0]
right_top = legend_bbox.get_points()[1]
if left_bottom[0] <= mouse_x <= right_top[0] and left_bottom[1] <= mouse_y <= right_top[1]:
# print("在图例上按下鼠标")
# 在图例上按下鼠标
mPress = False
return
# 没有图例的情况
# print("在 Axes 上按下鼠标")
# 在 Axes 上按下鼠标
mPress = True
startx = event.xdata
starty = event.ydata
return
elif event.name == 'button_release_event':
if axtemp and event.button == 1:
mPress = False
elif event.name == 'motion_notify_event':
if axtemp and event.button == 1 and mPress:
if axtemp.get_legend():
legend_bbox = axtemp.get_legend().get_window_extent()
left_bottom = legend_bbox.get_points()[0]
right_top = legend_bbox.get_points()[1]
if left_bottom[0] <= mouse_x <= right_top[0] and left_bottom[1] <= mouse_y <= right_top[1]:
print("在图例上移动鼠标")
# 在图例上按下鼠标
mPress = False
return
# 没有图例的情况
# print("在Axes上移动鼠标")
x_min, x_max = axtemp.get_xlim()
y_min, y_max = axtemp.get_ylim()
w = x_max - x_min
h = y_max - y_min
# print(event)
# 移动
mx = event.xdata - startx
my = event.ydata - starty
# 注意这里, -mx, 因为下一次 motion事件的坐标,已经是在本次做了移动之后的坐标系了,所以要体现出来
# startx=event.xdata-mx startx=event.xdata-(event.xdata-startx)=startx, 没必要再赋值了
# starty=event.ydata-my
# print(mx,my,x_min,y_min,w,h)
axtemp.set(xlim=(x_min - mx, x_min - mx + w))
axtemp.set(ylim=(y_min - my, y_min - my + h))
fig.canvas.draw_idle() # 绘图动作实时反映在图像上
return
# 滚轮滚动 处理事件
def call_scroll(event):
# print(event.name)
axtemp = event.inaxes
# print('event:', event)
# print(event.xdata, event.ydata)
# 计算放大缩小后, xlim 和ylim
if axtemp:
x_min, x_max = axtemp.get_xlim()
y_min, y_max = axtemp.get_ylim()
print(x_min, x_max)
w = x_max - x_min
h = y_max - y_min
curx = event.xdata
cury = event.ydata
curXposition = (curx - x_min) / w
curYposition = (cury - y_min) / h
if event.button == 'down':
# print('befor:', w, h)
w = w * 1.1 # 1.1
h = h * 1.1
# print('down', w, h)
elif event.button == 'up':
# print('befor:', w, h)
w = w / 1.1
h = h / 1.1
# print('up', w, h)
# print(curXposition, curYposition)
newx = curx - w * curXposition
newy = cury - h * curYposition
axtemp.set(xlim=(newx, newx + w))
axtemp.set(ylim=(newy, newy + h))
# axtemp.margins(0) # 调整坐标轴两端的空白
fig.canvas.draw_idle() # 绘图动作实时反映在图像上
fig.canvas.mpl_connect('scroll_event', call_scroll)
fig.canvas.mpl_connect('button_press_event', call_move)
fig.canvas.mpl_connect('button_release_event', call_move)
# fig.canvas.mpl_connect('draw_event', call_move)
fig.canvas.mpl_connect('motion_notify_event', call_move)
plt.show()