如何在 PyQt5 中嵌入 MetPy SkewT 图

How to embed MetPy SkewT plot in PyQt5

我想在 PyQT5 GUI 中嵌入 MetPy SkewT 图。以下代码创建一个 SkewT 图:

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

import metpy.calc as mpcalc
from metpy.cbook import get_test_data
from metpy.plots import SkewT
from metpy.units import units


###########################################

# Change default to be better for skew-T
plt.rcParams['figure.figsize'] = (9, 9)

###########################################

# Upper air data can be obtained using the siphon package, but for this example we will use
# some of MetPy's sample data.

col_names = ['pressure', 'height', 'temperature', 'dewpoint', 'direction', 'speed']

df = pd.read_fwf(get_test_data('jan20_sounding.txt', as_file_obj=False),
                 skiprows=5, usecols=[0, 1, 2, 3, 6, 7], names=col_names)

# Drop any rows with all NaN values for T, Td, winds
df = df.dropna(subset=('temperature', 'dewpoint', 'direction', 'speed'
                       ), how='all').reset_index(drop=True)

###########################################
# We will pull the data out of the example dataset into individual variables and
# assign units.

p = df['pressure'].values * units.hPa
T = df['temperature'].values * units.degC
Td = df['dewpoint'].values * units.degC
wind_speed = df['speed'].values * units.knots
wind_dir = df['direction'].values * units.degrees
u, v = mpcalc.wind_components(wind_speed, wind_dir)

###########################################

skew = SkewT()

# Plot the data using normal plotting functions, in this case using
# log scaling in Y, as dictated by the typical meteorological plot
skew.plot(p, T, 'r')
skew.plot(p, Td, 'g')

# Set spacing interval--Every 50 mb from 1000 to 100 mb
my_interval = np.arange(100, 1000, 50) * units('mbar')

# Get indexes of values closest to defined interval
ix = mpcalc.resample_nn_1d(p, my_interval)

# Plot only values nearest to defined interval values
skew.plot_barbs(p[ix], u[ix], v[ix])

# Add the relevant special lines
skew.plot_dry_adiabats()
skew.plot_moist_adiabats()
skew.plot_mixing_lines()
skew.ax.set_ylim(1000, 100)

# Show the plot
plt.show()

结果类似于下图:

我尝试了不同的代码,使用 FigureCanvasQTAgg() 在 PyQt5 GUI 中嵌入这个 SkewT 图。其中一项努力如下:

from PyQt5 import QtGui, QtCore
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout
import sys
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
import pandas as pd

import metpy.calc as mpcalc
from metpy.cbook import get_test_data
from metpy.plots import SkewT
from metpy.units import units

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        
        widget=QWidget()
        vbox=QVBoxLayout()
        widget.setLayout(vbox)
        
        plot1 = FigureCanvas(Figure(tight_layout=True, linewidth=3))
        ax1 = plot1.figure.subplots()

        ###########################################

        # Upper air data can be obtained using the siphon package, but for this example we will use
        # some of MetPy's sample data.

        col_names = ['pressure', 'height', 'temperature', 'dewpoint', 'direction', 'speed']

        df = pd.read_fwf(get_test_data('jan20_sounding.txt', as_file_obj=False),
                        skiprows=5, usecols=[0, 1, 2, 3, 6, 7], names=col_names)

        # Drop any rows with all NaN values for T, Td, winds
        df = df.dropna(subset=('temperature', 'dewpoint', 'direction', 'speed'
                            ), how='all').reset_index(drop=True)

        ###########################################
        # We will pull the data out of the example dataset into individual variables and
        # assign units.

        p = df['pressure'].values * units.hPa
        T = df['temperature'].values * units.degC
        Td = df['dewpoint'].values * units.degC
        wind_speed = df['speed'].values * units.knots
        wind_dir = df['direction'].values * units.degrees
        u, v = mpcalc.wind_components(wind_speed, wind_dir)

        ###########################################

        skew = SkewT(ax1)

        # Plot the data using normal plotting functions, in this case using
        # log scaling in Y, as dictated by the typical meteorological plot
        skew.plot(p, T, 'r')
        skew.plot(p, Td, 'g')
        skew.plot_barbs(p, u, v)

        # Add the relevant special lines
        skew.plot_dry_adiabats()
        skew.plot_moist_adiabats()
        skew.plot_mixing_lines()
        skew.ax.set_ylim(1000, 100)


        self.setCentralWidget(widget)
        self.setWindowTitle('Example')
        self.setMinimumSize(1000, 600)
        # self.showMaximized()
        self.show()

App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())

但它给出了一些错误。

我建议您创建一个单独的 python 文件 ui.py,您可以在其中设置 PyQt5 window、小部件、布局等。我使用 Qt Designer这个目的。
您应该将工作目录组织为:

├── workind_directory
    ├── main.py
    └── ui.py

ui.py 文件的起点可以是:

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(600, 600)
        MainWindow.setMinimumSize(QtCore.QSize(600, 600))
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
        self.gridLayout.setObjectName("gridLayout")
        self.FigureLayout = QtWidgets.QVBoxLayout()
        self.FigureLayout.setObjectName("FigureLayout")
        self.gridLayout.addLayout(self.FigureLayout, 0, 0, 1, 1)
        self.ToolbarLayout = QtWidgets.QVBoxLayout()
        self.ToolbarLayout.setObjectName("ToolbarLayout")
        self.gridLayout.addLayout(self.ToolbarLayout, 1, 0, 1, 1)
        self.plotButton = QtWidgets.QPushButton(self.centralwidget)
        self.plotButton.setObjectName("plotButton")
        self.gridLayout.addWidget(self.plotButton, 2, 0, 1, 1)
        MainWindow.setCentralWidget(self.centralwidget)
        self.plotButton.setText("PLOT")

你有一个绘图布局,另一个工具栏布局(如果需要)和一个更新绘图的按钮。
然后你可以设置你的 main.py 文件:

from PyQt5.QtWidgets import QApplication, QMainWindow
import sys
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
import pandas as pd
import numpy as np
import ui
import matplotlib.pyplot as plt

import metpy.calc as mpcalc
from metpy.cbook import get_test_data
from metpy.plots import SkewT
from metpy.units import units


class Window(QMainWindow, ui.Ui_MainWindow):

    def __init__(self, parent = None):

        super(Window, self).__init__(parent)
        self.setupUi(self)

        self.plotButton.clicked.connect(self.plotting)

        self.Figure = plt.figure()
        self.Canvas = FigureCanvas(self.Figure)
        self.FigureLayout.addWidget(self.Canvas)
        self.Toolbar = NavigationToolbar(self.Canvas, self)
        self.ToolbarLayout.addWidget(self.Toolbar)

    def plotting(self):

        ###########################################

        # Upper air data can be obtained using the siphon package, but for this example we will use
        # some of MetPy's sample data.

        col_names = ['pressure', 'height', 'temperature', 'dewpoint', 'direction', 'speed']

        df = pd.read_fwf(get_test_data('jan20_sounding.txt', as_file_obj = False),
                         skiprows = 5, usecols = [0, 1, 2, 3, 6, 7], names = col_names)

        # Drop any rows with all NaN values for T, Td, winds
        df = df.dropna(subset = ('temperature', 'dewpoint', 'direction', 'speed'
                                 ), how = 'all').reset_index(drop = True)

        ###########################################
        # We will pull the data out of the example dataset into individual variables and
        # assign units.

        p = df['pressure'].values*units.hPa
        T = df['temperature'].values*units.degC
        Td = df['dewpoint'].values*units.degC
        wind_speed = df['speed'].values*units.knots
        wind_dir = df['direction'].values*units.degrees
        u, v = mpcalc.wind_components(wind_speed, wind_dir)

        ###########################################

        skew = SkewT(fig = self.Figure)

        # Plot the data using normal plotting functions, in this case using
        # log scaling in Y, as dictated by the typical meteorological plot
        skew.plot(p, T, 'r')
        skew.plot(p, Td, 'g')
        # skew.plot_barbs(p, u, v)

        # Set spacing interval--Every 50 mb from 1000 to 100 mb
        my_interval = np.arange(100, 1000, 50) * units('mbar')

        # Get indexes of values closest to defined interval
        ix = mpcalc.resample_nn_1d(p, my_interval)

        # Plot only values nearest to defined interval values
        skew.plot_barbs(p[ix], u[ix], v[ix])

        # Add the relevant special lines
        skew.plot_dry_adiabats()
        skew.plot_moist_adiabats()
        skew.plot_mixing_lines()
        skew.ax.set_ylim(1000, 100)

        plt.draw()

App = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(App.exec_())

main.py 文件继承了 ui.py 的布局。注意__init__()方法,其中创建图形,canvas,工具栏并放置在各自的布局中。
然后是 plotting() 方法,您可以在其中实际绘制您想要的图。

这里有一个稍微不同的方法,它不使用 PyQt5,但使用 PySide2 作为 Python Qt5 绑定:

from PySide2 import QtWidgets, QtCore

from matplotlib.backends.backend_qt5agg import (
    FigureCanvas, NavigationToolbar2QT as NavigationToolbar)

import metpy.calc as mpcalc
from metpy.plots import SkewT
from metpy.units import pandas_dataframe_to_unit_arrays
from siphon.simplewebservice.wyoming import WyomingUpperAir


class ApplicationWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self._main = QtWidgets.QWidget()
        self.setCentralWidget(self._main)
        mainLayout = QtWidgets.QHBoxLayout(self._main)

        self.skew = SkewT()
        self.skew.ax.set_ylim(1050, 100)
        self.skew.ax.set_xlim(-50, 40)
        self.skew.plot_dry_adiabats()
        self.skew.plot_moist_adiabats()
        self.skew.plot_mixing_lines()
        self._temp_line, = self.skew.plot([], [], 'tab:red')
        self._dewp_line, = self.skew.plot([], [], 'tab:blue')
        self._prof_line, = self.skew.plot([], [], 'black')

        self._canvas = FigureCanvas(self.skew.ax.figure)
        mainLayout.addWidget(self._canvas, stretch=0)

        configLayout = QtWidgets.QGridLayout()

        updateButton = QtWidgets.QPushButton('Update')
        updateButton.clicked.connect(self._update_data)
        configLayout.addWidget(updateButton, 4, 1)
        configLayout.setRowStretch(3, 1)

        configLayout.addWidget(QtWidgets.QLabel('Site:'), 0, 0)
        self._site_select = QtWidgets.QLineEdit('OUN')
        configLayout.addWidget(self._site_select, 0, 1)

        configLayout.addWidget(QtWidgets.QLabel('Date:'), 1, 0)
        self._date_select = QtWidgets.QDateTimeEdit(QtCore.QDateTime(2019, 3, 20, 12, 0, 0))
        configLayout.addWidget(self._date_select, 1, 1)

        self._parcel_check = QtWidgets.QCheckBox('Surface Parcel')
        self._parcel_check.toggled.connect(self._handle_prof)
        configLayout.addWidget(self._parcel_check, 2, 0)

        mainLayout.addLayout(configLayout, stretch=1)

        self._update_data()
        self._handle_prof()

    @QtCore.Slot()
    def _update_data(self):
        try:
            print(self._date_select.dateTime().toPython(), self._site_select.text())
            self._data = WyomingUpperAir.request_data(self._date_select.dateTime().toPython(),
                                                    self._site_select.text())
            self._data = pandas_dataframe_to_unit_arrays(self._data)
            self._temp_line.set_data(self._data['temperature'].m, self._data['pressure'].m)
            self._dewp_line.set_data(self._data['dewpoint'].m, self._data['pressure'].m)
            self.flush()
        except ValueError as e:
            print(e)

    def flush(self):
        self._canvas.draw()
        self._main.repaint()

    @QtCore.Slot()
    def _handle_prof(self):
        if self._parcel_check.isChecked():
            prof_press, _, _, prof_temp = mpcalc.parcel_profile_with_lcl(self._data['pressure'],
                                                                    self._data['temperature'],
                                                                    self._data['dewpoint'])
            self._prof_line.set_data(prof_temp.to('degC').m, prof_press.to('hPa').m)
        else:
            self._prof_line.set_data([], [])

        self.flush()


if __name__ == "__main__":
    import sys

    qapp = QtWidgets.QApplication(sys.argv)
    app = ApplicationWindow()
    app.show()
    qapp.exec_()