PySide6中的绘图 - 使用PyQtGraph在你的应用程序中创建交互式绘图

2,049 阅读14分钟

Python的主要优势之一是在探索性数据科学和可视化方面,使用诸如Pandas、numpy、sklearn等工具进行数据分析和matplotlib绘图。用PySide6构建GUI应用程序,使你能够直接从你的应用程序中访问所有这些Python工具,使你能够构建复杂的数据驱动的应用程序和交互式仪表板。

虽然可以在PySide中嵌入matplotlib ,但体验并不完全是本地的。对于简单和高度互动的图表,你可能想考虑使用PyQtGraph来代替。PyQtGraph是建立在Qt的本地QGraphicsScene 上的,它提供了更好的绘制性能,特别是对于实时数据,同时还提供了交互性和使用Qt图形部件轻松定制图表的能力。

在本教程中,我们将完成用PyQtGraph创建一个绘图部件的第一步,然后演示使用线条颜色、线条类型、轴标签、背景颜色和绘制多条线来定制绘图。

开始学习

为了能够在PySide中使用PyQtGraph,你首先需要在你的Python环境中安装该软件包。你可以像平常一样使用pip

bash

pip install pyqtgraph

一旦安装完成,你就可以像平常一样导入该模块。

创建一个PyQtGraph小组件

在PyQtGraph中,所有的图都是用PlotWidget widget创建的。这个小组件提供了一个包含的画布,任何类型的绘图都可以被添加和配置。在引擎盖下,这个绘图小组件使用Qt本地的QGraphicsScene ,这意味着它快速、高效,并能简单地与你的应用程序的其他部分集成。你可以像其他widget一样创建一个PlotWidget

基本的模板应用程序,在QMainWindow ,有一个单一的PlotWidget ,如下所示。

在下面的例子中,我们将在代码中创建PyQtGraph widget。想知道在使用Qt Designer时如何嵌入PyQtGraph?请看从Qt Designer嵌入自定义widget

python

from PySide6.QtWidgets import QApplication, QMainWindow
import pyqtgraph as pg
import sys

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        hour = [1,2,3,4,5,6,7,8,9,10]
        temperature = [30,32,34,32,33,31,29,32,35,45]

        # plot data: x, y values
        self.graphWidget.plot(hour, temperature)


app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()

在我们下面的所有例子中,我们使用import pyqtgraph as pg 来导入PyQtGraph。这是PyQtGraph例子中的一个常见惯例,以保持整洁并减少输入。如果你愿意,你可以导入并使用它作为import pyqtgraph

The custom PyQtGraph widget showing dummy data. 自定义的PyQtGraph小组件显示假数据。

PyQtGraph的默认绘图风格是非常赤裸裸的--黑色背景加上一条细的(几乎看不到的)白线。在下一节中,我们将看看在PyQtGraph中我们有哪些选项可以用来改善我们的绘图的外观和可用性。

图形的样式

PyQtGraph使用Qt的QGraphicsScene 来渲染图形。这让我们可以访问所有标准的Qt线条和形状样式选项,以便在绘图中使用。然而,PyQtGraph提供了一个API来使用这些选项来绘制绘图和管理绘图画布。

下面我们将介绍你在创建和定制你自己的绘图时需要的最常见的造型功能。

背景颜色

从上面的应用程序骨架开始,我们可以通过在我们的PlotWidget 实例上调用.setBackground 来改变背景颜色(在self.graphWidget )。下面的代码将通过传入字符串 "w "将背景设置为白色。

python

self.graphWidget.setBackground('w')

你可以在任何时候设置(和更新)绘图的背景颜色。

python

from PySide6.QtWidgets import QMainWindow, QApplication
import pyqtgraph as pg
import sys


class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        hour = [1,2,3,4,5,6,7,8,9,10]
        temperature = [30,32,34,32,33,31,29,32,35,45]

        self.graphWidget.setBackground('w')
        self.graphWidget.plot(hour, temperature)


app = QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()

Change PyQtGraph Plot Background to White 将PyQtGraph的绘图背景改为白色

根据matplotlib 中使用的标准颜色,有一些简单的颜色可以使用单个字母。这些颜色并不奇怪 -- r是红色,b是蓝色 -- 除了'k'是用来表示黑色。除了这些单字母代码外,你还可以使用十六进制符号来设置更复杂的颜色,例如:#672922作为一个字符串。

蟒蛇

self.graphWidget.setBackground('#bbccaa')         # hex

RGB和RGBA值可以分别作为3元组或4元组传入,使用0-255的值。

蟒蛇

self.graphWidget.setBackground((100,50,255))      # RGB each 0-255
self.graphWidget.setBackground((100,50,255,25))   # RGBA (A = alpha opacity)

最后,你也可以直接使用Qt的QColor 类型指定颜色。

蟒蛇

from PySIde2 import QtGui  # Place this at the top of your file.
self.graphWidget.setBackground(QtGui.QColor(100,50,254,25))

如果你在你的应用程序的其他地方使用特定的QColor 对象,或者将你的绘图背景设置为默认的GUI背景颜色,这可能是有用的。

蟒蛇

color = self.palette().color(QtGui.QPalette.Window)  # Get the default window background,
self.graphWidget.setBackground(color)

线条颜色、宽度和样式

PyQtGraph中的线是使用标准的QtQPen 类型绘制的。这让你对线条的绘制拥有与其他任何QGraphicsScene 绘图相同的完全控制。要使用笔来绘制直线,你只需创建一个新的QPen 实例,并将其传入plot 方法。

下面我们创建一个QPen 对象,传入一个3元组的int 值,指定一个RGB值(全红色)。我们也可以通过传递'r'或者一个QColor 对象来定义这个。然后我们用笔的参数将其传入plot

pen = pg.mkPen(color=(255, 0, 0))
self.graphWidget.plot(hour, temperature, pen=pen)

完整的代码如下所示。

蟒蛇

from PySide6.QtWidgets import QApplication, QMainWindow
import pyqtgraph as pg
import sys


class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        hour = [1,2,3,4,5,6,7,8,9,10]
        temperature = [30,32,34,32,33,31,29,32,35,45]

        self.graphWidget.setBackground('w')

        pen = pg.mkPen(color=(255, 0, 0))
        self.graphWidget.plot(hour, temperature, pen=pen)


app = QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()

Changing Line Colour 改变线条颜色

通过改变QPen 对象,我们可以改变线条的外观,包括使用标准Qt线条样式的像素线宽和样式(虚线、点线等等)。例如,下面的例子创建了一条红色的15px宽度的虚线。

蟒蛇

pen = pg.mkPen(color=(255, 0, 0), width=15, style=QtCore.Qt.DashLine)

结果如下图所示,给出了一条15px的红色虚线。

Changing Line Width and Style 改变线条宽度和样式

标准的Qt线条样式都可以使用,包括Qt.SolidLine,Qt.DashLine,Qt.DotLine,Qt.DashDotLineQt.DashDotDotLine 。每种线条的例子都显示在下面的图片中,你可以在Qt文档中阅读更多内容。

Qt Line Types Qt 线条类型

线条标记

对于许多图来说,在图上放置标记或代替线是有帮助的。要在绘图上画一个标记,请在调用.plot 时传递用作标记的符号,如下所示。

蟒蛇

self.graphWidget.plot(hour, temperature, symbol='+')

除了symbol ,你还可以传入symbolSize,symbolBrushsymbolPen 参数。作为symbolBrush ,传递的值可以是任何颜色,或QBrush 类型,而symbolPen ,可以传递任何颜色或一个QPen 实例。钢笔用于绘制形状的轮廓,而画笔则用于填充。

例如,下面的代码将给出一个大小为30的蓝色十字标记,在一条粗红线上。

蟒蛇

pen = pg.mkPen(color=(255, 0, 0), width=15, style=QtCore.Qt.DashLine)
self.graphWidget.plot(hour, temperature, pen=pen, symbol='+', symbolSize=30, symbolBrush=('b'))

Adding Symbols on Line 在线上添加符号

除了+ 绘图标记外,PyQtGraph还支持以下标准标记,如下表所示。这些都可以以同样的方式使用。

如果你有更复杂的要求,你也可以传入任何QPainterPath 对象,允许你绘制完全自定义的标记形状。

图形标题

图表标题对于提供给定图表上所显示的内容的上下文非常重要。在PyQtGraph中,你可以使用setTitle() 方法在PlotWidget 中添加一个主要的绘图标题,并传入你的标题字符串。

python

self.graphWidget.setTitle("Your Title Here")

你可以通过传递额外的参数来应用文本样式,包括颜色、字体大小和重量到你的标题(以及PyQtGraph中的任何其他标签)。可用的样式参数如下所示。

下面的代码将颜色设置为蓝色,字体大小为30px。

蟒蛇

self.graphWidget.setTitle("Your Title Here", color="b", size="30pt")

如果你愿意,你也可以用HTML标签语法来样式你的标题,尽管它的可读性较差。

蟒蛇

self.graphWidget.setTitle("<span style=\"color:blue;font-size:30pt\">Your Title Here</span>")

Adding Chart Title 添加图表标题

轴标签

与标题类似,我们可以使用setLabel() 方法来创建我们的轴标题。这需要两个参数,位置文本位置可以是任何一个'left,'right','top','bottom' ,它描述了文本所处的轴的位置。第二个参数text是你想用于标签的文本。

你可以向该方法传递额外的样式参数。这些参数与标题略有不同,因为它们需要是有效的CSS名-值对。例如,现在的尺寸是font-size 。因为font-size 这个名字里有一个连字符,所以你不能直接把它作为一个参数传递,而必须使用**dictionary 方法。

styles = {'color':'r', 'font-size':'20px'}
self.graphWidget.setLabel('left', 'Temperature (°C)', **styles)
self.graphWidget.setLabel('bottom', 'Hour (H)', **styles)

如果你愿意的话,这些也支持HTML语法的造型。

self.graphWidget.setLabel('left', "<span style=\"color:red;font-size:20px\">Temperature (°C)</span>")
self.graphWidget.setLabel('bottom', "<span style=\"color:red;font-size:20px\">Hour (H)</span>")

Add Axis Labels 添加轴的标签

图例

除了坐标轴和绘图标题之外,你还经常想显示一个图例,以确定某条线代表什么。当你开始在一个图上添加多条线时,这一点尤其重要。PlotWidget在绘图中添加图例可以通过调用.addLegend 来完成,然而在这之前,你需要在调用.plot() 时为每条线提供一个名称。

下面的例子为我们用.plot() 绘制的线指定了一个名称 "传感器1"。这个名字将被用来在图例中识别该线。

self.graphWidget.plot(hour, temperature, name = "Sensor 1",  pen = NewPen, symbol='+', symbolSize=30, symbolBrush=('b'))
self.graphWidget.addLegend()

Add Legend 添加图例

图例默认出现在左上方。如果你想移动它,你可以很容易地将图例拖放到其他地方。你也可以在创建图例时通过向offset 参数传递一个2-tuple来指定一个默认的偏移。

背景网格

添加背景网格可以使你的绘图更容易阅读,特别是在试图比较相对的x和y值时。你可以通过调用.showGrid ,为你的绘图打开一个背景网格,PlotWidget 。你可以独立切换x和y的网格。

以下是为X轴和Y轴创建网格的方法。

蟒蛇

self.graphWidget.showGrid(x=True, y=True)

Add Grid 添加网格

设置轴的限制

有时,限制绘图上可见的数据范围,或者将坐标轴锁定在一个一致的范围内,而不管输入的数据如何(例如一个已知的最小-最大范围),都是很有用的。在PyQtGraph中,这可以通过.setXRange().setYRange() 方法实现。这些方法迫使绘图只显示每个轴上指定范围内的数据。

下面我们设置两个范围,每个轴上一个。第一个参数是最小值,第二个参数是最大值。

蟒蛇

self.graphWidget.setXRange(5, 20, padding=0)
self.graphWidget.setYRange(30, 40, padding=0)

一个可选的padding参数使范围被设置为大于指定的分数(默认为0.02和0.1之间,取决于ViewBox的大小)。如果你想完全删除这个padding,请传递0。

蟒蛇

self.graphWidget.setXRange(5, 20, padding=0)
self.graphWidget.setYRange(30, 40, padding=0)

到目前为止,完整的代码显示在下面。

蟒蛇

from PySide6.QtWidgets import QApplication, QMainWindow
import pyqtgraph as pg
import sys


class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        hour = [1,2,3,4,5,6,7,8,9,10]
        temperature = [30,32,34,32,33,31,29,32,35,45]

        #Add Background colour to white
        self.graphWidget.setBackground('w')
        # Add Title
        self.graphWidget.setTitle("Your Title Here", color="b", size="30pt")
        # Add Axis Labels
        styles = {"color": "#f00", "font-size": "20px"}
        self.graphWidget.setLabel("left", "Temperature (°C)", **styles)
        self.graphWidget.setLabel("bottom", "Hour (H)", **styles)
        #Add legend
        self.graphWidget.addLegend()
        #Add grid
        self.graphWidget.showGrid(x=True, y=True)
        #Set Range
        self.graphWidget.setXRange(0, 10, padding=0)
        self.graphWidget.setYRange(20, 55, padding=0)

        pen = pg.mkPen(color=(255, 0, 0))
        self.graphWidget.plot(hour, temperature, name="Sensor 1",  pen=pen, symbol='+', symbolSize=30, symbolBrush=('b'))


app = QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()

Set Axis Range 设置坐标轴范围

绘制多条线

对于绘图来说,涉及多条线是很常见的。在PyQtGraph中,这就像在同一个PlotWidget ,多次调用.plot() 。在下面的例子中,我们要绘制两条类似的数据线,每条线使用相同的线条样式、粗细等,但要改变线条的颜色。

为了简化这个过程,我们可以在我们的MainWindow 上创建我们自己的自定义plot 方法。这个方法接受xy 参数来绘制,线的名称(用于图例)和一个颜色。我们使用颜色作为线和标记的颜色。

蟒蛇

    def plot(self, x, y, plotname, color):
        pen = pg.mkPen(color=color)
        self.graphWidget.plot(x, y, name=plotname, pen=pen, symbol='+', symbolSize=30, symbolBrush=(color))

为了绘制单独的线条,我们将创建一个名为temperature_2 的新数组,并用类似于temperature (现在是temperature_1 )的随机数字填充它。将这些数字并排绘制,可以让我们把它们放在一起比较。

现在,你可以调用plot函数两次,这将在图上产生2条线。

蟒蛇

self.plot(hour, temperature_1, "Sensor1", 'r')
self.plot(hour, temperature_2, "Sensor2", 'b')

蟒蛇

from PySide6.QtWidgets import QApplication, QMainWindow
import pyqtgraph as pg
import sys


class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        hour = [1,2,3,4,5,6,7,8,9,10]
        temperature_1 = [30,32,34,32,33,31,29,32,35,45]
        temperature_2 = [50,35,44,22,38,32,27,38,32,44]

        #Add Background colour to white
        self.graphWidget.setBackground('w')
        # Add Title
        self.graphWidget.setTitle("Your Title Here", color="b", size="30pt")
        # Add Axis Labels
        styles = {"color": "#f00", "font-size": "20px"}
        self.graphWidget.setLabel("left", "Temperature (°C)", **styles)
        self.graphWidget.setLabel("bottom", "Hour (H)", **styles)
        #Add legend
        self.graphWidget.addLegend()
        #Add grid
        self.graphWidget.showGrid(x=True, y=True)
        #Set Range
        self.graphWidget.setXRange(0, 10, padding=0)
        self.graphWidget.setYRange(20, 55, padding=0)

        self.plot(hour, temperature_1, "Sensor1", 'r')
        self.plot(hour, temperature_2, "Sensor2", 'b')

    def plot(self, x, y, plotname, color):
        pen = pg.mkPen(color=color)
        self.graphWidget.plot(x, y, name=plotname, pen=pen, symbol='+', symbolSize=30, symbolBrush=(color))


app = QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()

2 Line Graph 2线图

玩一玩这个函数,自定义你的标记、线宽、颜色和其他参数。

清除绘图

最后,有时你可能想定期清除和刷新绘图。你可以通过调用.clear() 轻松做到这一点。

蟒蛇

self.graphWidget.clear()

这将从图中删除线条,但保持所有其他属性不变。

更新绘图

虽然你可以简单地清除绘图并重新绘制所有的元素,但这意味着Qt必须销毁并重新创建所有的QGraphicsScene 对象。对于小型或简单的绘图来说,这可能并不明显,但如果你想创建高性能的流式绘图,最好是就地更新数据。PyQtGraph接收新的数据,并更新绘图的线条,使之与之匹配,而不影响绘图中的任何其他元素。

为了更新一条线,我们需要一个对线对象的引用。这个引用在第一次使用.plot 创建线条时被返回,我们可以简单地将其存储在一个变量中。请注意,这是对线条的引用,而不是对绘图的引用。

my_line_ref = graphWidget.plot(x, y)

一旦我们有了这个引用,更新绘图就是简单地在这个引用上调用.setData ,以应用新的数据。在下面的例子中,我们采用了简单的绘图演示,并将其扩展为对直线的引用。

from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtCore import QTimer
import pyqtgraph as pg
import sys
from random import randint

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        self.x = list(range(100))  # 100 time points
        self.y = [randint(0,100) for _ in range(100)]  # 100 data points

        self.graphWidget.setBackground('w')

        pen = pg.mkPen(color=(255, 0, 0))
        self.data_line =  self.graphWidget.plot(self.x, self.y, pen=pen)



app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())

我们将每隔50ms更新我们的数据,尽管PyQtGraph可以比这更快地绘制数据,但它可能会变得难以观看为了做到这一点,我们定义了一个Qt定时器,并将其设置为调用一个自定义方法update_plot_data ,在这里我们将改变数据。我们在__init__ 块中定义这个定时器,这样它就会自动启动。在窗口类中添加以下内容。

        # ... init continued ...
        self.timer = QtCore.QTimer()
        self.timer.setInterval(50)
        self.timer.timeout.connect(self.update_plot_data)
        self.timer.start()

    def update_plot_data(self):

        self.x = self.x[1:]  # Remove the first y element.
        self.x.append(self.x[-1] + 1)  # Add a new value 1 higher than the last.

        self.y = self.y[1:]  # Remove the first
        self.y.append( randint(0,100))  # Add a new random value.

        self.data_line.setData(self.x, self.y)  # Update the data.

如果你运行这个应用程序,你会看到一个随机数据向左快速滚动的图,X值也在及时更新和滚动,就像流式数据一样。你可以用你自己的真实数据来代替随机数据,例如从实时传感器读出的数据或API中获取。PyQtGraph的性能足以支持使用这种方法同时绘制多个图形。

完整的代码如下所示:

from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtCore import QTimer
import pyqtgraph as pg
import sys
from random import randint

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        self.x = list(range(100))  # 100 time points
        self.y = [randint(0,100) for _ in range(100)]  # 100 data points

        self.graphWidget.setBackground('w')

        pen = pg.mkPen(color=(255, 0, 0))
        self.data_line =  self.graphWidget.plot(self.x, self.y, pen=pen)

        self.timer = QTimer()
        self.timer.setInterval(50)
        self.timer.timeout.connect(self.update_plot_data)
        self.timer.start()

    def update_plot_data(self):

        self.x = self.x[1:]  # Remove the first y element.
        self.x.append(self.x[-1] + 1)  # Add a new value 1 higher than the last.

        self.y = self.y[1:]  # Remove the first
        self.y.append( randint(0,100))  # Add a new random value.

        self.data_line.setData(self.x, self.y)  # Update the data.

app = QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()

总结

在本教程中,我们已经发现了如何用PyQtGraph绘制简单的图,并自定义线条、标记和标签。关于PyQtGraph方法和功能的完整概述,请参见PyQtGraph文档和API参考Github上的PyQtGraph仓库在Plotting.py中也有一整套更复杂的绘图示例(如下所示)。