Matplotlib navigation toolbar embeded in pyqt5 win

2019-02-28 06:45发布

问题:

I'm trying to use interactive plotting capabilities of matplotlib embeded into pyqt5 window with mpl version : 2.1.1

I provided minimal example with UI file here:

https://ufile.io/8irs7

and python code:

import matplotlib
matplotlib.use("Qt5Agg")
import sys

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

import pandas as pd
import numpy as np

from PyQt5 import  QtWidgets, uic  # works for pyqt5
from PyQt5.Qt import  QVBoxLayout


# import numpy as np
qtCreatorFile = "window.ui" # Enter file here.
# qtCreatorFile = "pssga.ui"


Ui_MainWindow, QtBaseClass = uic.loadUiType(qtCreatorFile)

class PlotSeries(QtWidgets.QMainWindow, Ui_MainWindow):

    def __init__(self):
        QtWidgets.QMainWindow.__init__(self)
        Ui_MainWindow.__init__(self)
        self.setupUi(self)

                #create figure
        self.fig=Figure()

        self.axes = self.fig.add_subplot(111)
        self.axes.grid()



        self.canvas=FigureCanvas(self.fig)
        #add plot toolbar from matplotlib
        self.toolbar = NavigationToolbar(self.canvas, self)

        #create new layout
        layout = QVBoxLayout()

        layout.addWidget(self.canvas)
        layout.addWidget(self.toolbar)

        #add layout to qwidget
        self.plotlayout=self.widgetplot.setLayout(layout)

        #draw the canvas ! 
        self.canvas.draw()
        #now let's call widget 
        self.plot_basegraph()


    def plot_basegraph(self):
        #create simple time series 
        for i in range(0,5): 
            ts=pd.Series(np.random.randn(2000), index=pd.date_range('1/1/2015',freq='h', periods=2000),name="series "+str(i))
            if i==0:
                self.stackTSPD=ts.to_frame()
            else:
                self.stackTSPD=self.stackTSPD.join(ts.to_frame(),how='inner')
        print(matplotlib.__version__)

        self.axes.clear()
        self.axes.grid()

        for i in self.stackTSPD.columns.values:
            t=self.stackTSPD.index.to_pydatetime()

            y=self.stackTSPD[i].values
            self.axes.plot(t,y, label=i)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = PlotSeries()
    window.show()
    sys.exit(app.exec_())

Zooming into time series, panning works as expected but when I try to press "Home" - reset original view it crashes:

Traceback (most recent call last):
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/backends/backend_qt5agg.py", line 155, in __draw_idle_agg
    self.draw()
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/backends/backend_qt5agg.py", line 127, in draw
    super(FigureCanvasQTAggBase, self).draw()
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/backends/backend_agg.py", line 430, in draw
    self.figure.draw(self.renderer)
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/artist.py", line 55, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/figure.py", line 1299, in draw
    renderer, self, artists, self.suppressComposite)
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/image.py", line 138, in _draw_list_compositing_images
    a.draw(renderer)
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/artist.py", line 55, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/axes/_base.py", line 2437, in draw
    mimage._draw_list_compositing_images(renderer, self, artists)
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/image.py", line 138, in _draw_list_compositing_images
    a.draw(renderer)
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/artist.py", line 55, in draw_wrapper
    return draw(artist, renderer, *args, **kwargs)
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/axis.py", line 1133, in draw
    ticks_to_draw = self._update_ticks(renderer)
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/axis.py", line 974, in _update_ticks
    tick_tups = list(self.iter_ticks())
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/axis.py", line 917, in iter_ticks
    majorLocs = self.major.locator()
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/dates.py", line 1061, in __call__
    self.refresh()
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/dates.py", line 1081, in refresh
    dmin, dmax = self.viewlim_to_dt()
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/dates.py", line 839, in viewlim_to_dt
    return num2date(vmin, self.tz), num2date(vmax, self.tz)
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/dates.py", line 445, in num2date
    return _from_ordinalf(x, tz)
  File "/usr/local/lib/python3.5/dist-packages/matplotlib/dates.py", line 260, in _from_ordinalf
    dt = datetime.datetime.fromordinal(ix).replace(tzinfo=UTC)
ValueError: ordinal must be >= 1

This behavior started when I tried to embed this time series plot into pyqt5 form. When plot opened separately in a new GTK window it worked without any problem. I've attached example with working home button which opens in na non pyqt window...

'''
Created on 16. jan. 2018

@author: gregor
'''

import matplotlib as mpl
import matplotlib.pyplot as plt
# matplotlib.use("Qt5Agg")


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

import pandas as pd
import numpy as np





class PlotSeries():

    def __init__(self):


        self.fig,self.axes = plt.subplots()

        self.axes.grid()



#         self.canvas=FigureCanvas(self.fig)
        #add plot toolbar from matplotlib
#         self.toolbar = NavigationToolbar(self.canvas, self)

        #create new layout
#         layout = QVBoxLayout()
#         
#         layout.addWidget(self.canvas)
#         layout.addWidget(self.toolbar)
#         
#         #add layout to qwidget
#         self.plotlayout=self.widgetplot.setLayout(layout)
#         
#         #draw the canvas ! 
#         self.canvas.draw()
        #now let's call widget 
        self.plot_basegraph()


    def plot_basegraph(self):
        #create simple time series 
        for i in range(0,5): 
            ts=pd.Series(np.random.randn(2000), index=pd.date_range('1/1/2015',freq='h', periods=2000),name="series "+str(i))
            if i==0:
                self.stackTSPD=ts.to_frame()
            else:
                self.stackTSPD=self.stackTSPD.join(ts.to_frame(),how='inner')
        print(mpl.__version__)

        self.axes.clear()
        self.axes.grid()

        for i in self.stackTSPD.columns.values:
            t=self.stackTSPD.index.to_pydatetime()

            y=self.stackTSPD[i].values
            self.axes.plot_date(t,y, label=i)
        plt.show()



if __name__ == "__main__":
 PlotSeries()  

Thank you !

回答1:

Short answer: remove the line self.canvas.draw().

Explanation:
You draw the canvas before it contains any data. But since the canvas is already equipped with the toolbar at that point, the "Home"-state is the state where the axes have no data.

This in itself would not be a problem, but it will become one once dates are plotted.
The default initial limits for an empty axes are (0,1). So the "Home"-state will refer to those limits and upon clicking the button will try to restore those limits.
In the meantime however, the axis has been told to hold dates. Dates range from (1, whatever). Hence 0 is no valid number on the dates axes.

The solution is then quite easy. Just remove the line self.canvas.draw() from the code. It does not fulfil any required task anyways.