1

I want to embed a MetPy SkewT diagram in a PyQT5 GUI. The following code creates a SkewT diagram:

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

The result is similar to the image below: enter image description here

I have tried different codes for embedding this SkewT diagram in PyQt5 GUI using FigureCanvasQTAgg(). One of the efforts is as follows:

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

But it gives some errors.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Amir
  • 65
  • 1
  • 2
  • 7

2 Answers2

1

I suggest you to create a separate python file ui.py where you set up your PyQt5 window, with widgets, layout etc. I use Qt Designer for this purpose.
You should organize your working directory as:

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

A starting point for the ui.py file could be:

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")

where you have a layout for the plot, another layout for the toolbar (if needed) and a button for update the plot.
Then you can setup your main.py file:

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

This main.py file inherits layout from ui.py. Pay attention to the __init__() method, where figure, canvas, toolbar are created and placed in the respective layouts.
Then there is the plotting() method, where you actually draw the plot you want.

enter image description here

Zephyr
  • 11,891
  • 53
  • 45
  • 80
  • Thanks for your advice about structured programming. But my point in asking this question is to understand how to embed a MetPy SkewT (not anything else) diagram in a PyQt5 GUI. I have tried your code, but the SkewT diagram does not appear after clicking. – Amir Aug 10 '21 at 17:14
  • 1
    I updated my answer: there was a little bug. I forgot to pass `fig = self.Figure` parameter to `skew = SkewT()`. Now it should work properly – Zephyr Aug 10 '21 at 17:37
1

Here's a slightly different approach that doesn't use PyQt5, but does use PySide2 as the Python Qt5 binding:

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_()
DopplerShift
  • 5,472
  • 1
  • 21
  • 20