Python【Matplotlib】交互式时间序列绘图,将x轴设置为日期时间格式并和鼠标拖动缩放相结合

背景

上篇博客:python【matplotlib】鼠标拖动滚动缩放坐标范围和拖动图例共存,得到启发,我们已经可以通过鼠标拖动缩放坐标范围和移动图例,来实现动态交互式绘图了,对于x轴是时间序列的绘图需求,能否也实现动态交互式绘图呢?
答案是肯定的,接下来我将详细描述其实现的方式。

效果

Python【Matplotlib】交互式时间序列绘图,将x轴设置为日期时间格式并和鼠标拖动缩放相结合_第1张图片

实现步骤

准备工作

首先,我们需要导入必要的库,包括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()

你可能感兴趣的:(Python,python,matplotlib)