Pyqt结合matplotlib实现图表的基本交互,实现图表的放大缩小,移动光标注释

前言

最近在使用pyqt结合matplotlib开发一款内部使用的数据分析软件,发现matplotlib库在处理大数据,出图性能方面还是很不错的,但是就是图表的交互性上差了一点,比如说图像的放大和缩小,移动的光标线,显示注释等等,很多还是需要自己造轮子,本人通过五一假期的一番研究,从中也颇有收获,现在把下面的这些研究成果分享给大家。

使用Matplotlib库完成基本的图表交互

  1. 初始化基本的曲线配置

    import sys
    
    import numpy as np
    from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
    from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
    from matplotlib.figure import Figure
    
    class MatplotlibWidget(FigureCanvas):
        def __init__(self, parent=None, width=5, height=4, dpi=100):
            fig = Figure(figsize=(width, height), dpi=dpi)
            self.axes = fig.add_subplot(111)
    
            self.compute_initial_figure()
    
            FigureCanvas.__init__(self, fig)
            self.setParent(parent)
            self.lef_mouse_pressed = False  # 鼠标左键是否按下
           	self.connect_event()
    	
        def connect_event(self):
            return	#添加鼠标事件,后续在这里添加
        
        def compute_initial_figure(self):
            x = np.linspace(0, 10, 100)
            y1 = np.sin(x)
            y2 = np.cos(x)
            y3 = np.sin(x) * 2
            self.line1, = self.axes.plot(x, y1, 'b-', label='sin(x)')
            self.line2, = self.axes.plot(x, y2, 'r-', label='cos(x)')
            self.line3, = self.axes.plot(x, y3, 'g-', label='2*sin(x)')
            self.axes.legend()  # 显示右上角标签
    	
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
    
            self.widget = QWidget()
            self.setMinimumHeight(600)
            self.setMinimumWidth(800)
            self.showMaximized() # 设置全屏
            self.setCentralWidget(self.widget)
    
            layout = QVBoxLayout(self.widget)
    
            self.mpl_widget = MatplotlibWidget(self.widget, width=5, height=4, dpi=100)
            layout.addWidget(self.mpl_widget)
    
            self.show()
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        mainWin = MainWindow()
        sys.exit(app.exec_())
    

    现在的图像应该是这个效果

    在这里插入图片描述

  2. 添加鼠标滚动放大和缩小效果,比例可以自己设置,我这里设置的是最大值和最小值差值的10分之1

        def on_mouse_wheel(self, event):
            if self.axes is not None:
                x_min, x_max = self.axes.get_xlim()
                x_delta = (x_max - x_min) / 10		# 控制缩放X轴的比例
                y_min, y_max = self.axes.get_ylim()
                y_delta = (y_max - y_min) / 10		# 控制缩放X轴的比例
                if event.button == "up":
                    self.axes.set(xlim=(x_min + x_delta, x_max - x_delta))
                    self.axes.set(ylim=(y_min + y_delta, y_max - y_delta))
                elif event.button == "down":
                    self.axes.set(xlim=(x_min - x_delta, x_max + x_delta))
                    self.axes.set(ylim=(y_min - y_delta, y_max + y_delta))
    
                self.draw_idle()
    

    添加鼠标滚动事件到connect_event()函数里面

        def connect_event(self):
            self.mpl_connect("scroll_event", self.on_mouse_wheel)	#鼠标滚动事件
    
  3. 实现按住鼠标向上下左右拖动的效果,拖动的距离同理可以自己控制

        def on_button_press(self, event):
            if event.inaxes is not None:  # 判断是否在坐标轴内
                if event.button == 1:
                    self.lef_mouse_pressed = True
                    self.pre_x = event.xdata
                    self.pre_y = event.ydata
    
        def on_button_release(self, event):
            self.lef_mouse_pressed = False
            
        def on_mouse_move(self, event):
            if event.inaxes is not None and event.button == 1:
                if self.lef_mouse_pressed:	#鼠标左键按下时才计算
                    x_delta = event.xdata - self.pre_x
                    y_delta = event.ydata - self.pre_y
                    # 获取当前原点和最大点的4个位置
                    x_min, x_max = self.axes.get_xlim()
                    y_min, y_max = self.axes.get_ylim()
    				
                    # 控制一次移动鼠标拖动的距离
                    x_min = x_min - x_delta
                    x_max = x_max - x_delta
                    y_min = y_min - y_delta
                    y_max = y_max - y_delta
    
                    self.axes.set_xlim(x_min, x_max)
                    self.axes.set_ylim(y_min, y_max)
                    self.draw_idle()
    

    添加鼠标按住和松开事件到connect_event()函数里面

        def connect_event(self):
            self.mpl_connect("scroll_event", self.on_mouse_wheel)
            self.mpl_connect("button_press_event", self.on_button_press)
            self.mpl_connect("button_release_event", self.on_button_release)
            self.mpl_connect("motion_notify_event", self.on_mouse_move)
    

    现在图像可以实现如下效果

    在这里插入图片描述

    可以看到鼠标按住后把图像拖动了,同时鼠标滚动也放大了图像

实现图表的高阶交互

我们最终想达到的目标是能够实现类似echarts库的功能,能够随着鼠标移动显示一条竖的光标线,光标线旁边能够显示详细信息,效果跟下图所示差不多

在这里插入图片描述

​ 接下来我们使用matplotlib实现如图效果

  1. 给图表添加一个竖光标线,在图表中可以随着鼠标移动而移动,在光标线旁边显示相应的曲线信息

        def init_annotation(self):
            # 初始化光标线和注释
            self.vertline, = self.ax.plot([], [], 'c-', lw=2)
            # 预置一个空文本显示横坐标值
            hPackerList = [HPacker(children=[TextArea("", textprops=dict(size=10))])]
            for line in self.axes.get_lines():
                if line == self.vertline:  # 跳过光标线
                    continue
                line_color = line.get_color()   # 获取每条曲线的颜色
                text_area = TextArea(line.get_label(), textprops=dict(size=10, color=line_color))   #根据曲线颜色设置文字颜色
                hPacker = HPacker(children=[text_area])
                hPackerList.append(hPacker)
            self.text_box = VPacker(children=hPackerList, pad=1, sep=3) # 竖值布局,设置padding和文字之间上下的间距
            self.annotation_bbox = AnnotationBbox(self.text_box, (0, 0),
                                                  xybox=(100, 0),
                                                  xycoords='data',
                                                  boxcoords="offset points")
            if self.axes is not None:
                self.axes.add_artist(self.annotation_bbox)
    

    将init_annotation函数放到初始化init函数里面

  2. 添加hover函数,使竖光标线和注释随着鼠标的移动能够动态地做出改变,这里使用了annocation_bbox,这个工具网上资料很少,我也是临时翻英文文档看的,gpt生成的错误代码用不了,如果需要详细了解可以翻阅matlibplot的官方文档,hPacker和vPacker类似于qt的hBoxlayout和vBoxLayout,还是比较好理解的,还有textarea是可以设置颜色的

        def hover(self, event):
            if event.inaxes == self.axes:
                x = event.xdata
                if x is not None:
                    text = f"x: {x}" #显示横坐标值
                    hPacker_list = self.text_box.get_children()
                    time_hPacker = hPacker_list[0]
                    time_text_area: TextArea = time_hPacker.get_children()[0]
                    time_text_area.set_text(text)   # 更新横坐标值
                    for index,line in enumerate(self.axes.get_lines()):
                        if line == self.vertline:  # 跳过光标线
                            continue
                        x_data = line.get_xdata()
                        y_data = line.get_ydata()
                        y = np.interp(x, x_data, y_data)
                        # 更新光标线的位置
                        self.vertline.set_xdata([x, x])
                        self.vertline.set_ydata([self.axes.get_ylim()[0], self.axes.get_ylim()[1]])
                        # 显示每条曲线的详细信息
                        line_text = f"{line.get_label()}: {y:.3f}"
                        hPacker = hPacker_list[index+1] # 因为横坐标值放在了第一个hPacker中,所以从第二个开始
                        text_area: TextArea = hPacker.get_children()[0]
                        text_area.set_text(line_text)
    
                    # 更新AnnotationBbox的位置
                    self.annotation_bbox.xy = (x, event.ydata)
                    self.annotation_bbox.set_visible(True)
                    self.draw_idle()
                else:
                    # 隐藏AnnotationBbox和光标线
                    self.annotation_bbox.set_visible(False)
                    self.vertline.set_xdata([])
                    self.vertline.set_ydata([])
                    self.draw_idle()
    

    将hover函数添加到connect_event()函数中

        def connect_event(self):
            self.mpl_connect("scroll_event", self.on_mouse_wheel)
            self.mpl_connect("button_press_event", self.on_button_press)
            self.mpl_connect("button_release_event", self.on_button_release)
            self.mpl_connect("motion_notify_event", self.on_mouse_move)
            self.mpl_connect("motion_notify_event", self.hover)
    

    最终实现效果

    在这里插入图片描述

  3. 全部代码

    import sys
    
    import numpy as np
    from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
    from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
    from matplotlib.figure import Figure
    from matplotlib.offsetbox import HPacker, TextArea, VPacker, AnnotationBbox
    
    
    class MatplotlibWidget(FigureCanvas):
        def __init__(self, parent=None, width=5, height=4, dpi=100):
            fig = Figure(figsize=(width, height), dpi=dpi)
            self.axes = fig.add_subplot(111)
            self.lef_mouse_pressed = False  # 鼠标左键是否按下
    
            self.compute_initial_figure()
    
            FigureCanvas.__init__(self, fig)
            self.setParent(parent)
    
            self.connect_event()
    
            self.init_annotation()
    
        def connect_event(self):
            self.mpl_connect("scroll_event", self.on_mouse_wheel)
            self.mpl_connect("button_press_event", self.on_button_press)
            self.mpl_connect("button_release_event", self.on_button_release)
            self.mpl_connect("motion_notify_event", self.on_mouse_move)
            self.mpl_connect("motion_notify_event", self.hover)
    
        def compute_initial_figure(self):
            x = np.linspace(0, 10, 100)
            y1 = np.sin(x)
            y2 = np.cos(x)
            y3 = np.sin(x) * 2
            self.line1, = self.axes.plot(x, y1, 'b-', label='sin(x)')
            self.line2, = self.axes.plot(x, y2, 'r-', label='cos(x)')
            self.line3, = self.axes.plot(x, y3, 'g-', label='2*sin(x)')
            self.axes.legend()  # 显示右上角标签
    
        def init_annotation(self):
            # 初始化光标线和注释
            self.vertline, = self.axes.plot([], [], 'c-', lw=2)
            hPackerList = [HPacker(children=[TextArea("", textprops=dict(size=10))])]  # 预置一个空文本显示横坐标值
            for line in self.axes.get_lines():
                if line == self.vertline:  # 跳过光标线
                    continue
                line_color = line.get_color()   # 获取每条曲线的颜色
                text_area = TextArea(line.get_label(), textprops=dict(size=10, color=line_color))   #根据曲线颜色设置文字颜色
                hPacker = HPacker(children=[text_area])
                hPackerList.append(hPacker)
            self.text_box = VPacker(children=hPackerList, pad=1, sep=3) # 竖值布局,设置padding和文字之间上下的间距
            self.annotation_bbox = AnnotationBbox(self.text_box, (0, 0),
                                                  xybox=(100, 0),
                                                  xycoords='data',
                                                  boxcoords="offset points")
            if self.axes is not None:
                self.axes.add_artist(self.annotation_bbox)
    
        def on_mouse_wheel(self, event):
            if self.axes is not None:
                x_min, x_max = self.axes.get_xlim()
                x_delta = (x_max - x_min) / 10
                y_min, y_max = self.axes.get_ylim()
                y_delta = (y_max - y_min) / 10
                if event.button == "up":
                    self.axes.set(xlim=(x_min + x_delta, x_max - x_delta))
                    self.axes.set(ylim=(y_min + y_delta, y_max - y_delta))
                elif event.button == "down":
                    self.axes.set(xlim=(x_min - x_delta, x_max + x_delta))
                    self.axes.set(ylim=(y_min - y_delta, y_max + y_delta))
    
                self.draw_idle()
    
        def on_button_press(self, event):
            if event.inaxes is not None:  # 判断是否在坐标轴内
                if event.button == 1:
                    self.lef_mouse_pressed = True
                    self.pre_x = event.xdata
                    self.pre_y = event.ydata
    
        def on_button_release(self, event):
            self.lef_mouse_pressed = False
    
        def on_mouse_move(self, event):
            if event.inaxes is not None and event.button == 1:
                if self.lef_mouse_pressed:
                    x_delta = event.xdata - self.pre_x
                    y_delta = event.ydata - self.pre_y
                    # 获取当前原点和最大点的4个位置
                    x_min, x_max = self.axes.get_xlim()
                    y_min, y_max = self.axes.get_ylim()
    
                    x_min = x_min - x_delta
                    x_max = x_max - x_delta
                    y_min = y_min - y_delta
                    y_max = y_max - y_delta
    
                    self.axes.set_xlim(x_min, x_max)
                    self.axes.set_ylim(y_min, y_max)
                    self.draw_idle()
    
        def hover(self, event):
            if event.inaxes == self.axes:
                x = event.xdata
                if x is not None:
                    text = f"x: {x}" #显示横坐标值
                    hPacker_list = self.text_box.get_children()
                    time_hPacker = hPacker_list[0]
                    time_text_area: TextArea = time_hPacker.get_children()[0]
                    time_text_area.set_text(text)   # 更新横坐标值
                    for index,line in enumerate(self.axes.get_lines()):
                        if line == self.vertline:  # 跳过光标线
                            continue
                        x_data = line.get_xdata()
                        y_data = line.get_ydata()
                        y = np.interp(x, x_data, y_data)
                        # 更新光标线的位置
                        self.vertline.set_xdata([x, x])
                        self.vertline.set_ydata([self.axes.get_ylim()[0], self.axes.get_ylim()[1]])
                        # 显示每条曲线的详细信息
                        line_text = f"{line.get_label()}: {y:.3f}"
                        hPacker = hPacker_list[index+1] # 因为横坐标值放在了第一个hPacker中,所以从第二个开始
                        text_area: TextArea = hPacker.get_children()[0]
                        text_area.set_text(line_text)
    
                    # 更新AnnotationBbox的位置
                    self.annotation_bbox.xy = (x, event.ydata)
                    self.annotation_bbox.set_visible(True)
                    self.draw_idle()
                else:
                    # 隐藏AnnotationBbox和光标线
                    self.annotation_bbox.set_visible(False)
                    self.vertline.set_xdata([])
                    self.vertline.set_ydata([])
                    self.draw_idle()
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
    
            self.widget = QWidget()
            self.setMinimumHeight(600)
            self.setMinimumWidth(800)
            self.showMaximized() # 设置全屏
            self.setCentralWidget(self.widget)
    
            layout = QVBoxLayout(self.widget)
    
            self.mpl_widget = MatplotlibWidget(self.widget, width=5, height=4, dpi=100)
            layout.addWidget(self.mpl_widget)
    
            self.show()
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        mainWin = MainWindow()
        sys.exit(app.exec_())
    

总结

感觉matplotlib库更适用于静态图表的分析,即使自己造了这些轮子,但总感觉还是不如echats好用,不过echarts性能方面还是不如matplotlib的,毕竟两者的应用场景确实不一样。

  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MC皮蛋侠客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值