如何阻止 Matplotlib 导航工具栏缩放在绘图更新时重置?

Posted

技术标签:

【中文标题】如何阻止 Matplotlib 导航工具栏缩放在绘图更新时重置?【英文标题】:How to stop Matplotlib's navigation toolbar zoom from reseting on plot update? 【发布时间】:2018-06-15 17:06:22 【问题描述】:

我希望使用 Matplotlib 的导航工具栏的情况相对简单。我希望能够在图形更新之间保持以前的缩放值、相机平移等。我在此处保留了 PyQt5 嵌入(我在我的项目中使用),以防两者之间需要一些额外的互连。非常感谢您的观看!

import sys
import os
import random
import matplotlib
matplotlib.use('Qt5Agg')
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QGridLayout, QFileDialog, QPushButton

from numpy import arange
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure


class MyMplCanvas(FigureCanvas):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.fig = fig ###
        self.axes = fig.add_subplot(111)

        self.axes.hold(False)

        self.compute_initial_figure()

        FigureCanvas.__init__(self, fig)
        self.setParent(parent)

        FigureCanvas.setSizePolicy(self,
                                   QtWidgets.QSizePolicy.Expanding,
                                   QtWidgets.QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)

    def compute_initial_figure(self):
        pass


class MyDynamicMplCanvas(MyMplCanvas):
    def __init__(self, *args, **kwargs):
        MyMplCanvas.__init__(self, *args, **kwargs)
        timer = QtCore.QTimer(self)
        timer.timeout.connect(self.update_figure)
        timer.start(1000)

    def compute_initial_figure(self):
        self.axes.plot([0, 1, 2, 3], [1, 2, 0, 4], 'b')

    def update_figure(self):
        l = [random.randint(0, 10) for i in range(4)]
        self.axes.cla()
        self.axes.plot([0, 1, 2, 3], l, 'b')
        self.draw()


class P1(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super(P1, self).__init__(parent)
        layout = QGridLayout(self)

        self.plot_canvas = MyDynamicMplCanvas(self, width=5, height=4, dpi=100)
        self.navi_toolbar = NavigationToolbar(self.plot_canvas, self)


        layout.addWidget(self.plot_canvas, 1, 1, 1, 1)
        layout.addWidget(self.navi_toolbar, 2, 1, 1, 1)



class MainWindow(QtWidgets.QMainWindow):    
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)

        self.stack = QtWidgets.QStackedWidget(self)
        P1f = P1(self)
        self.stack.addWidget(P1f)
        self.setCentralWidget(self.stack)


if __name__ == '__main__':
    qApp = QtWidgets.QApplication(sys.argv)
    aw = MainWindow()
    aw.show()
    sys.exit(qApp.exec_())

【问题讨论】:

【参考方案1】:

这里的大反派是axes.hold(False)axes.cla()。他们负责清除坐标轴和图形,这(通常)会重置绘图视图。

有了这些,您可以正确使用self.axes.autoscale(enable=False),我建议您将其放在compute_initial_figure() 中的第一个情节之后,因此情节至少在开始时有所缩放。

然后,为了能够清除之前的绘图,您可以为MyMplCanvas 类创建另一个属性,可能是self.plotted_line 或类似的东西,用None 初始化。每次调用self.axes.plot(...),将返回值分配给self.plotted_line,如下所示:self.plotted_line, = self.axes.plot(...)。注意self.plotted_line 后面的逗号,这是只分配第一个返回值的方法之一,也就是您感兴趣的那个。

最后,在每个新情节之前,检查并调用

    if self.plotted_line is not None:
        self.plot.remove()

这将有效地删除以前的情节。

画布类看起来像这样(变化很小)。

class MyMplCanvas(FigureCanvas):
    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.fig = fig ###
        self.axes = fig.add_subplot(111)

        self.plotted_line = None

        self.compute_initial_figure()

        FigureCanvas.__init__(self, fig)
        self.setParent(parent)

        FigureCanvas.setSizePolicy(self,
                                   QtWidgets.QSizePolicy.Expanding,
                                   QtWidgets.QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)

    def compute_initial_figure(self):
        pass


class MyDynamicMplCanvas(MyMplCanvas):
    def __init__(self, *args, **kwargs):
        MyMplCanvas.__init__(self, *args, **kwargs)
        timer = QtCore.QTimer(self)
        timer.timeout.connect(self.update_figure)
        timer.start(1000)

    def compute_initial_figure(self):
        self.plotted_line, = self.axes.plot([0, 1, 2, 3], [1, 2, 0, 4], 'b')
        self.axes.autoscale(enable=False)

    def update_figure(self):
        l = [random.randint(0, 10) for i in range(4)]

        if self.plotted_line is not None:
            self.plotted_line.remove()
        self.plotted_line, = self.axes.plot([0, 1, 2, 3], l, 'b')
        self.draw()

【讨论】:

非常感谢@Gabriel!我很抱歉延迟响应......我感觉很糟糕,但是虽然这修复了我的示例,但我实际上无法让它与我的真实程序一起使用(它使用 imshow 并在顶部绘制了额外的散点)。我可能会为更详细的方法创建一个新主题,但非常感谢您的时间和帮助;你的技术在更大的范围内很有启发性! 你可能确实可以使用这个,也许通过相应地标记相关的情节,所以不要对这个方法失去希望。请考虑创建另一个帖子。【参考方案2】:

可以通过连接mpl 'draw_event'直接存储缩放值:

def __init__(self, parent=None, width=5, height=4, dpi=100):
    ...
    self.mpl_connect('draw_event', self.on_draw)

def on_draw(self, event):
    self.xlim = self.axes.get_xlim()
    self.ylim = self.axes.get_ylim()

然后在 plot() 调用后恢复:

def update_figure(self):
    ...
    self.axes.plot([0, 1, 2, 3], l, 'b')
    self.axes.set_xlim(self.xlim)
    self.axes.set_ylim(self.ylim)
    ...

https://matplotlib.org/users/event_handling.html

完整代码:

import random
import sys

import matplotlib

matplotlib.use('Qt5Agg')
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QGridLayout

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure


class MyMplCanvas(FigureCanvas):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.fig = fig  ###
        self.axes = fig.add_subplot(111)

        self.axes.hold(False)

        self.compute_initial_figure()

        FigureCanvas.__init__(self, fig)
        self.setParent(parent)

        FigureCanvas.setSizePolicy(self,
                                   QtWidgets.QSizePolicy.Expanding,
                                   QtWidgets.QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)
        self.xlim = self.axes.get_xlim()
        self.ylim = self.axes.get_ylim()
        self.mpl_connect('draw_event', self.on_draw)

    def on_draw(self, event):
        self.xlim = self.axes.get_xlim()
        self.ylim = self.axes.get_ylim()

    def compute_initial_figure(self):
        pass


class MyDynamicMplCanvas(MyMplCanvas):
    def __init__(self, *args, **kwargs):
        MyMplCanvas.__init__(self, *args, **kwargs)
        timer = QtCore.QTimer(self)
        timer.timeout.connect(self.update_figure)
        timer.start(1000)

    def compute_initial_figure(self):
        self.axes.plot([0, 1, 2, 3], [1, 2, 0, 4], 'b')

    def update_figure(self):
        l = [random.randint(0, 10) for i in range(4)]
        self.axes.cla()
        self.axes.plot([0, 1, 2, 3], l, 'b')
        self.axes.set_xlim(self.xlim)
        self.axes.set_ylim(self.ylim)
        self.draw()


class P1(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super(P1, self).__init__(parent)
        layout = QGridLayout(self)

        self.plot_canvas = MyDynamicMplCanvas(self, width=5, height=4, dpi=100)
        self.navi_toolbar = NavigationToolbar(self.plot_canvas, self)

        layout.addWidget(self.plot_canvas, 1, 1, 1, 1)
        layout.addWidget(self.navi_toolbar, 2, 1, 1, 1)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)

        self.stack = QtWidgets.QStackedWidget(self)
        P1f = P1(self)
        self.stack.addWidget(P1f)
        self.setCentralWidget(self.stack)


if __name__ == '__main__':
    qApp = QtWidgets.QApplication(sys.argv)
    aw = MainWindow()
    aw.show()
    sys.exit(qApp.exec_())

【讨论】:

以上是关于如何阻止 Matplotlib 导航工具栏缩放在绘图更新时重置?的主要内容,如果未能解决你的问题,请参考以下文章

Matplotlib pyqt 导航工具栏更改“图形选项”的背景颜色

在matplotlib中改变zoom-rect的边缘颜色

Matplotlib 中的透明导航栏(或者,可以在没有栏的情况下添加导航按钮吗?)

通过网格在 Tkinter 中显示 Matplotlib 导航工具栏

在主窗口工具栏中嵌入带有自定义导航工具栏操作的 matplotlib

如何使用选择图例(matplotlib)自动缩放图形?