当前位置: 代码网 > it编程>编程语言>C/C++ > Pyqt结合matplotlib实现图表的基本交互,实现图表的放大缩小,移动光标注释

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

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

前言

最近在使用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的,毕竟两者的应用场景确实不一样。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com