1+1=10

记记笔记,放松一下...

PySide6下Matplotlib小记

突然发现,上次正儿八经使用Matplotlib还是17年在前,除代码还可查外,当时的笔记已不复存在。重新了解一下...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/python
# -*- coding: UTF-8 -*-

#2007.10.1
#--------------------------------------------------------------------------
import sys, os, shutil
from ConfigParser import SafeConfigParser
#import subprocess
import threading, urllib, webbrowser

import wx
import wx.stc, wx.combo

import matplotlib
matplotlib.use('wxAgg')
import matplotlib.backends.backend_wxagg # for py2exe

8种Qt后端

在Python下,编写Qt程序,目前有4个绑定选项存在:

  • PyQt6
  • PySide6
  • PyQt5
  • PySide2

老版本的matplotlib还支持 PyQt和PySide,由于其对应的Qt4在2015年就停止支持,应该无需考虑。

而每个绑定,又有2个渲染后端可选择:

  • Agg
  • Cairo

选择Qt绑定的规则

Matplotlib是如何同时支持这四个的??官方手册是这么说的:

  • If a binding's QtCore subpackage is already imported, that binding is selected (the order for the check is PyQt6, PySide6, PyQt5, PySide2).
  • If the QT_API environment variable is set to one of "PyQt6", "PySide6", "PyQt5", "PySide2" (case-insensitive), that binding is selected. (See also the documentation on Environment variables.)
  • Otherwise, the first available backend in the order PyQt6, PySide6, PyQt5, PySide2 is selected.

挺啰嗦,不如看代码直观:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
QT_API_ENV = os.environ.get("QT_API")
if QT_API_ENV is not None:
    QT_API_ENV = QT_API_ENV.lower()
_ETS = {  # Mapping of QT_API_ENV to requested binding.
    "pyqt6": QT_API_PYQT6, "pyside6": QT_API_PYSIDE6,
    "pyqt5": QT_API_PYQT5, "pyside2": QT_API_PYSIDE2,
}
# First, check if anything is already imported.
if sys.modules.get("PyQt6.QtCore"):
    QT_API = QT_API_PYQT6
elif sys.modules.get("PySide6.QtCore"):
    QT_API = QT_API_PYSIDE6
elif sys.modules.get("PyQt5.QtCore"):
    QT_API = QT_API_PYQT5
elif sys.modules.get("PySide2.QtCore"):
    QT_API = QT_API_PYSIDE2

考虑到Qt5生命周期明年就结束了,我们可以只考虑PyQt6和 PySide6两兄弟,同时我也不喜欢设置环境变量QT_API,那么可以这么简单理解:

  • 如果Python中只安装PyQt6或PySide6中一个,装哪个用哪个
  • 如果同时装了2个,默认使用PyQt6,除非使用matplotlib前import PySide6.QtCore

由于PyQt不支持LGPL协议,原则上,不管是否商用,只考虑PySide6就够了。如果要支持Windows7,可以考虑PySide2。

QtAgg 与 QtCairo

除了不同的Qt绑定外,它还有不同的渲染后端支持

  • QtAgg:使用 Anti-Grain Geometry 这一个 C++ 库来渲染,适合需要快速渲染和交互的应用。
  • QtCairo:使用 Cairo 库来渲染,提供更高质量的图形渲染,尤其在文本和矢量图形方面。速度慢。

默认使用的QtAgg后端,同样它有多种方式可以用来指定:

  • 使用 rcParams["backend"] 参数,通过代码,或者matplotlibrc文件
  • 使用环境变量 MPLBACKEND
  • 使用函数 matplotlib.use()

Qt 控件对接

要在PySide6中使用 matplotlib,首要就是找到matplotlib提供的QWidget在哪里:

1
2
from matplotlib.backends.backend_qtagg import FigureCanvas
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar

上面两个都是QWidget的派生类:

  • FigureCanvas 派生自 QWidget:源码 class FigureCanvasQT(FigureCanvasBase, QtWidgets.QWidget):
  • NavigationToolbar2QT 派生自 QToolBar:源码 class NavigationToolbar2QT(NavigationToolbar2, QtWidgets.QToolBar):

另外,不少matplotlib为主的例子中,会这么写:

1
from matplotlib.backends.qt_compat import QtWidgets

只是为了隐藏不同的Qt绑定的细节,对于库通用性有意义,应用程序中使用它意义不大。

Matplotlib 基本概念

先看一张图:

matplotlib with pyside6

  • 图中有四个图表,每个叫做一个 Axes
  • 四个 Axes 都位于同一个 Figure 中(Figure 是顶层容器)
  • Figure 渲染到一个 QWidget 中(这个QWidget就是 FigureCanvas)
  • PySide6 将这个 QWidget作为窗口显示出来

抛开FigureCanvas这种底层细节不说,Matplotlib中东西也挺多,一些主要的概念如下:

概念 作用
Figure Figure 是顶级容器,承载整个图形的所有元素,类似于空白画布,用来绘制可视化内容。
Axes AxesFigure 中的矩形区域,提供坐标系,是绘制数据的地方,一个 Figure 可以包含多个 Axes
Axis Axis 代表 x 轴和 y 轴,定义数据范围、刻度位置、刻度标签和坐标轴标签,控制刻度间距和定位。
Marker Marker 是表示单个数据点的符号,常用于散点图中区分不同的数据点,形状如圆形、方形、三角形等。
Lines Lines 连接数据点,通常用于折线图、带连接点的散点图等,表示数据点之间的关系或趋势。可自定义样式。
Title Title 是图形的描述性文本,通常显示在顶部,为可视化提供背景信息或数据描述。
Axis Labels Axis Labels 是描述 x 轴和 y 轴的文本,帮助理解数据及其单位或其他相关信息。
Ticks Ticks 是沿坐标轴的刻度标记,帮助用户理解图形的比例和数据位置。
Tick Labels Tick Labels 是刻度上的文本,显示与刻度对应的数据值,可以进行格式化或单位显示。
Legend Legend 是图例,解释图形中的符号或颜色,帮助用户理解不同数据系列或类别的含义。
Grid Lines Grid Lines 是辅助的横纵线,帮助用户更容易识别数据的模式或趋势。
Spines Spines 是围绕绘图区域的边框线,分隔图形区域与外部空间,可以进行自定义以改变边框的外观。

另外,关注 Artist 和 Patch,手册中有个类图

Matplotlib三种API接口(使用层次)

从易到难,Matplotlib 有三个使用层次:

接口层次 作用 特点 适用场景
Scripting Layer 提供简单的绘图接口,适合快速绘制常见图形,类似 MATLAB 风格。 简单、自动管理,快速绘图。 快速原型、常见绘图任务、日常数据可视化。
Artist Layer 提供对图形元素的精细控制,可以自定义图形的所有细节。 高度定制、灵活,直接控制图形对象的外观和行为。 复杂可视化任务、高度定制的绘图。
Backend Layer 控制图形的渲染和输出,决定如何显示或保存图形。 渲染图形到屏幕或文件,支持不同的图形工具包和设备。 图形文件输出、非交互式渲染、与图形工具包交互。

Scripting层/隐式pyplot接口

类似Matlab风格(好吧,因为太贵,我从没用过matlab),叫做octave风格?

1
2
3
4
5
6
7
import matplotlib.pyplot as plt

plt.plot([1, 2, 3, 4], [10, 20, 25, 30])
plt.xlabel('X Axis')
plt.ylabel('Y Axis')
plt.title("Debao's Simple Plot")
plt.show()

Artist层/显式Axes接口

按部就班,一层层构建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import matplotlib.pyplot as plt

# 创建Figure和Axes对象
fig, ax = plt.subplots()

# 使用Axes对象绘制数据
line, = ax.plot([1, 2, 3, 4], [10, 20, 25, 30])

# 修改线条属性
line.set_linewidth(2)
line.set_color('red')

# 设置坐标轴标签和标题
ax.set_xlabel('X Axis')
ax.set_ylabel('Y Axis')
ax.set_title("Debao's Artist Layer Example")

# 显示图形
plt.show()

Backend层

这个是我们最关注的,使用PySide6时,将Figure装进 FigureCanvas(一个QWidget)中,而后和Qt程序对接上了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
import matplotlib.pyplot as plt

class PlotWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Debao's Matplotlib with PySide6")

        # 创建Matplotlib图形
        self.figure, self.ax = plt.subplots()

        self.ax.plot([1, 2, 3, 4], [10, 20, 25, 30])
        self.ax.set_xlabel('X Axis')
        self.ax.set_ylabel('Y Axis')
        self.ax.set_title('Simple Plot')

        # 创建画布,并将其添加到窗口
        self.canvas = FigureCanvas(self.figure)
        layout = QVBoxLayout()
        layout.addWidget(self.canvas)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = PlotWindow()
    window.show()
    sys.exit(app.exec())

Matplotlib 做什么?

数据可视化

Matplotlib 支持多种数据类型的可视化,包括但不限于:

  1. 配对数据 (Pairwise Data):使用散点图、成对关系图等展示数据点间的关系。
  2. 统计分布 (Statistical Distributions):通过直方图、密度图等展示数据的分布特性。
  3. 网格数据 (Gridded Data):使用热图、等高线图等展示规则网格上的数据。
  4. 非规则网格数据 (Irregularly Gridded Data):通过散点图、三角网格图等展示不规则网格数据。
  5. 三维和体积数据 (3D and Volumetric Data):通过 3D 图、体积图等展示空间数据。

手册中有很直观的 Gallery 可以参考。

试着放个简单的例子,一组数据,应该是姿势不对,两种方式结果不同(似乎各有各的不准):

contour

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import numpy as np
import matplotlib.pyplot as plt

X = np.array([[1, 2, 3, 4],
              [1, 1.5, 2.5, 4],
              [1, 1.5, 2.8, 4],
              [1, 2, 3, 4]])
Y = np.array([[4, 4, 4, 4],
              [3, 2.5, 2.5, 3],
              [1, 1.5, 2.3, 2],
              [0, 0, 0, 0]])

Z = np.array([[0, 0, 0, 0],
              [0, 4, 4, 0],
              [0, 4, 4, 0],
              [0, 0, 0, 0]])

plt.figure(figsize=(12, 6))

plt.subplot(1, 2, 1) 
plt.contour(X, Y, Z, 10, cmap='viridis')
plt.scatter(X, Y, color='red', label='Data Points') 
plt.title('Contour Plot')

plt.subplot(1, 2, 2)
plt.tricontour(X.flatten(), Y.flatten(), Z.flatten(), 10, cmap='viridis')
plt.scatter(X.flatten(), Y.flatten(), color='red', label='Data Points')
plt.title('Tricontour Plot')

plt.tight_layout() 
plt.show()

键鼠事件

对于Qt后端,可以处理一些鼠标事件(尽管不强)

  • https://matplotlib.org/stable/users/explain/figure/event_handling.html

动画支持

还有简单的动画支持

  • https://matplotlib.org/stable/users/explain/animations/animations.html

参考

  • https://matplotlib.org/stable/api/backend_qt_api.html
  • https://matplotlib.org/stable/users/explain/figure/api_interfaces.html
  • https://doc.qt.io/qtforpython-6/examples/example_external_matplotlib_widget_gaussian.html
  • https://ajaytech.co/matplotlib/
  • https://www.geeksforgeeks.org/python-introduction-matplotlib/
  • https://matplotlib.org/stable/gallery/images_contours_and_fields/irregulardatagrid.html