Python结合matplotlib实现图表的基本交互

 更新时间:2025年12月15日 09:17:10   作者:MC皮蛋侠客  
这篇文章主要为大家详细介绍了Python如何结合matplotlib实现图表的基本交互,可以实现图表的放大缩小和移动光标注释,感兴趣的小伙伴可以跟随小编一起学习一下

前言

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

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

初始化基本的曲线配置

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_())

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

添加鼠标滚动放大和缩小效果,比例可以自己设置,我这里设置的是最大值和最小值差值的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)	#鼠标滚动事件

实现按住鼠标向上下左右拖动的效果,拖动的距离同理可以自己控制

    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实现如图效果

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

    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函数里面

添加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)

最终实现效果

全部代码

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

到此这篇关于Python结合matplotlib实现图表的基本交互的文章就介绍到这了,更多相关Python matplotlib图表交互内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • selenium WebDriverWait类等待机制的实现

    selenium WebDriverWait类等待机制的实现

    这篇文章主要介绍了selenium WebDriverWait类等待机制的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-03-03
  • Python调用C++程序的方法详解

    Python调用C++程序的方法详解

    这篇文章主要介绍了Python调用C++程序的方法,文中通过示例代码介绍的详细,相信对大家具有一定的参考借鉴价值,需要的朋友们下面来一起看看吧。
    2017-01-01
  • Python+树莓派+YOLO打造一款人工智能照相机

    Python+树莓派+YOLO打造一款人工智能照相机

    今天,我们将自己动手打造出一款基于深度学习的照相机,当小鸟出现在摄像头画面中时,它将能检测到小鸟并自动进行拍照
    2018-01-01
  • 如何利用Python快速统计文本的行数

    如何利用Python快速统计文本的行数

    这篇文章主要介绍了如何利用Python快速统计文本的行数,要快速统计一个文本文件中的行数,其实就是要统计这个文本文件中换行符的个数,下面我们就一起进入文章看看具体的操作过程吧
    2021-12-12
  • Python实现双向RNN与堆叠的双向RNN的示例代码

    Python实现双向RNN与堆叠的双向RNN的示例代码

    这篇文章主要为大家详细介绍了如何利用Python语言实现双向RNN与堆叠的双向RNN,文中详细讲解了双向RNN与堆叠的双向RNN的原理及实现,需要的可以参考一下
    2022-07-07
  • Python实现随机生成任意数量车牌号

    Python实现随机生成任意数量车牌号

    这篇文章主要介绍了Python实现随机生成任意数量车牌号,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-01-01
  • python动态文本进度条的实例代码

    python动态文本进度条的实例代码

    这篇文章主要介绍了python动态文本进度条的实例代码,代码简单易懂,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-01-01
  • python人工智能深度学习入门逻辑回归限制

    python人工智能深度学习入门逻辑回归限制

    这篇文章主要为大家介绍了python人工智能深度学习入门之逻辑回归限制的详细讲解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2021-11-11
  • Python如何读取16进制byte数据

    Python如何读取16进制byte数据

    这篇文章主要介绍了Python如何读取16进制byte数据,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-05-05
  • PyTorch实现卷积神经网络的搭建详解

    PyTorch实现卷积神经网络的搭建详解

    这篇文章主要为大家介绍了PyTorch实现卷积神经网络的搭建详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-05-05

最新评论