matplotlib是基于Python的绘图库,广泛用于Python科学计算界。它完整支持二维绘图以及部分支持三维绘图。该绘图库致力于能适应广泛的用户需求。它可以根据所选的用户接口工具来嵌入绘图算法。与此同时,对于使用GTK+、Qt、Tk、FLTK、wxWidgets与Cocoa的所有主要桌面操作系统,matplotlib能支持交互式绘图。在Python的交互式shell中,我们可以使用简单的、过程式的命令交互式地调用matplotlib来生成图形,与使用Mathematica、IDL或者MATLAB绘图非常相似。matplotlib也可以嵌入到无报文头的Web服务器中,以提供基于光栅(如PNG格式)与向量(如Postscript、PDF以及纸面效果很好的SVG格式)这两种格式的图形硬拷贝。
一、历史溯源—— 硬件锁问题
我们其中一位开发者(John Hunter)与他的研究癫痫症的同事们试图在不借助专有软件的情况下进行脑皮层电图(ECoG)分析,于是便有了最初的matplotlib。John Hunter当时所在的实验室只有一份电图分析软件的许可证,但有各式各样的工作人员,如研究生、医科学生、博士后、实习生、以及研究员,他们轮流共享该专有软件的硬件电子锁。生物医学界广泛使用MATLAB进行数据分析与可视化,所以Hunter着手使用基于MATLAB的matplotlib来代替专有软件,这样很多研究员都可以使用并且对其进行扩展。但是MATLAB天生将数据当作浮点数的数组来处理。然而在实际情况中,癫痫手术患者的医疗记录具有多种数据形式(CT、MRI、ECoG与EEG等),并且存储在不同的服务器上。MATLAB作为数据管理系统勉强能应付这样的复杂性。由于感到MATLAB不适合于这项任务,Hunter开始编写一个新的建立在用户接口工具GTK+(当时是Linux下的主流桌面视窗系统)之上的Python应用程序。
所以matplotlib这一GTK+应用程序最初便被开发成EEG/ECoG可视化工具。这样的用例决定了它最初的软件架构。matplotlib最初的设计也服务于另一个目的:代替命令驱动的交互式图形生成(这一点MATLAB做得很好)工具。MATLAB的设计方法使得加载数据文件与绘图这样的任务非常简单,而要使用完全面向对象的API则会在语法上过于繁琐。所以matplotlib也提供状态化的脚本编程接口来快速、简单地生成与MATLAB类似的图形。因为matplotlib是Python库,所以用户可以使用Python中各种丰富的数据结构,如列表、辞典与集合等等。
二、 matplotlib软件架构概述
顶层的matplotlib对象名为
Figure
,它包含与管理某个图形的所有元素。matplotlib必须完成的一个核心架构性任务是实现
Figure
的绘制与操作框架,并且做到该框架与
Figure
到用户视窗接口或硬拷贝渲染行为是分离的。这使得我们可以为
Figure
添加越来越复杂的特性与逻辑,同时保持“后端”或输出设备的相对简化。matplotlib不仅封装了用于向多种设备渲染的绘图接口,还封装了基本事件处理以及多数流行的用户界面工具的视窗功能。因此,用户可以创建相当丰富的交互式图形算法与用户界面工具(用到可能存在的鼠标与键盘),而又不必修改matplotlib已经支持的6种界面工具。
要实现这些,matplotlib的架构被逻辑性地分为三层。这三层逻辑可以视为一个栈。每层逻辑知道如何与其下的一层逻辑进行通信,但在下层逻辑看来,上层是透明的。这三层从底向上分别为:
后端、美工与脚本。
1、 后端
matplotlib逻辑栈最底层是
后端
,它具体实现了下面的抽象接口类:
-
FigureCanvas
对绘图表面(如“绘图纸”)的概念进行封装。 -
Renderer
执行绘图动作(如“画笔”)。 -
Event
处理键盘与鼠标事件这样的用户输入。
对于如Qt这样的用户界面工具,
FigureCanvas
中包含的具体实现可以完成三个任务:将自身嵌入到原生的Qt视窗(
QtGui.QMainWindow
)中,能将matplotlib的Renderer命令转换到canvas上(
QtGui.QPainter
),以及将原生Qt事件转换到matplotlib的
Event
框架下(后者产生回调信号让上行监听者进行处理)。抽象基类定义在
matplotlib.backend_bases
中,且所有派生类都定义在如
matplotlib.backends.backend_qt4agg
这样的专用模块中。对于专门生成硬拷贝输出(如PDF、PNG、SVG或PS)的纯图像后端而言,
FigureCanvas
的实现可能只是简单地建立一个类似文件的对象,其中定义默认的文件头、字体与宏函数,以及
Renderer
创建的个别对象(如直线、文本与矩形等)。
Renderer
的任务是提供底层的绘图接口,即在画布上绘图的动作。上文已经提到,最初的matplotlib程序是一个基于GTK+的ECoG查看器,而且很多早期设计灵感都源自当时已有的GDK/GTK+的API。最初
Renderer
的API源自GDK的
Drawable
接口,后者实现了
draw_point
、
draw_line
、
draw_rectangle
、
draw_image
、
draw_polygon
以及
draw_glyphs
这样的基本方法。我们完成的每个不同后端——最早有PostScript与GD——都实现了GDK的
Drawable
,并将其转换为独立于后端的原生绘图命令。如上所述,这毫无必要地增加了后端的实现复杂度,原因是单独实现
Drawable
造成函数泛滥。此后,
Renderer
已经被极大的简化,将matplotlib移植到新的用户界面或文件格式已经是非常简单的过程。
一个对matplotlib有利的设计决定是支持使用C++模板库Anti-Grain Geometry(缩写为agg[
She06
])的基于像素点的核心渲染器。这是一个高性能库,可以进行2D反锯齿渲染,生成的图像非常漂亮。matplotlib支持将agg后端渲染的像素缓存插入到每种支持的用户界面中,所以在不同的UI与操作系统下都能得到精确像素点的图形。因为matplotlib生成的PNG输出也使用agg渲染器,所以硬拷贝与屏幕显示完全相同,也就是说在不同的UI与操作系统下,PNG的输出所见即所得。
matplotlib的
Event
框架将
key-press-event
或
mouse-motion-event
这样的潜在UI事件映射到
KeyEvent
或
MouseEvent
类。用户可以连接到这些事件进行函数回调,以及图形与数据的交互,如要
pick
一个或一组数据点,或对图形或其元素的某方面性质进行操作。下面的示例代码演示了当用户键入‘t’时,对
Axes
窗口中的线段进行显示开关。
import numpy as np
import matplotlib.pyplot as plt
def on_press(event):
if event.inaxes is None: return
for line in event.inaxes.lines:
if event.key=='t':
visible = line.get_visible()
line.set_visible(not visible)
event.inaxes.figure.canvas.draw()
fig, ax = plt.subplots(1)
fig.canvas.mpl_connect('key_press_event', on_press)
ax.plot(np.random.rand(2, 20))
plt.show()
对底层UI事件框架的抽象使得matplotlib的开发者与最终用户都可以编写UI事件处理代码,而且“一次编写,随处运行”。譬如,在所有用户界面下都可以对matplotlib图像进行交互式平移与放缩,这种交互式操作就是在matplotlib的事件框架下实现的。
2 、Artis层
(称之为“ 美工”其实是一个汉化的形象说法,因为“ 在哪里画,用什么画,画什么图? ”这不正是美工的工作么?)
Artist
层次结构处于matplotlib的中间层,负责很大一部分繁重的计算任务。延续之前将后端的
FigureCanvas
看作画纸的比喻,
Artis
对象知道如何用
Renderer
(画笔)在画布上画出墨迹。matplotlib中的
Figure
就是一个
Artist
对象实例。标题、直线、刻度标记以及图像等等都对应某个Artist实例(如图11.3)。
Artist
的基类是
matplotlib.artist.Artist
,其中包含所有
Artist
的共享属性,包括从美工坐标系统到画布坐标系统的变换(后面将详细介绍)、可见性、定义用户可绘制区域的剪切板、标签,以及处理“选中”这样的用户交互动作的接口,即在美工层检测鼠标点击事件。
Artist
层于后端之间的耦合性存在于
draw
方法中。譬如,下面假想的
SomeArtist
类是
Artist
的子类,它要实现的关键方法是
draw
,用来传递给后端的渲染器。
Artist
不知道渲染器要向哪种后端进行绘制(PDF、SVG与GTK+绘图区等),但知道
Renderer
的API,并且会调用适当的方法(
draw_text
或
draw_path
)。因为
Renderer
能访问画布,并且知道如何绘制,所以
draw
方法将
Artist
的抽象表示转换为像素缓存中的颜色、SVG文件中的轨迹或者其他具体表示。
class SomeArtist(Artist):
'An example Artist that implements the draw method'
def draw(self, renderer):
"""Call the appropriate renderer methods to paint self onto canvas"""
if not self.get_visible(): return
# create some objects and use renderer to draw self here
renderer.draw_path(graphics_context, path, transform)
该层次结构中有两种类型的
Artist
。基本Artist表示我们在图形中能看到的一类对象,如
Line2D
、
Rectangle
、
Circle
与
Text
。复合Artist是Artist的集合,如
Axis
、
Tick
、
Axes
与
Figure
。每个复合Artsit可能包含其他复合Artist与基本Artist。譬如,
Figure
包含一个或多个Axes,并且
Figure
的背景是基本的
Rectangle
。
最重要的复合Artist是
Axes
,其中定义了大多数matplot的绘图方法。
Axes
不仅仅包含大多数构成绘图背景(如标记、轴线、网格线、色块等)的图形元素,还包括了大量生成基本
Artist
并添加到
Axes
实例中的帮助函数。譬如,表11.1列出了一些
Axes
函数,这些函数进行对象的绘制,并将它们存储在
Axes
实例中。
表11.1:
Axes
的方法样例及其创建的
Artist
实例
| 方法 | 创建对象 | 存储位置 |
|
Axes.imshow
| 一到多个
matplotlib.image.AxesImage
|
Axes.images
|
|
Axes.hist
| 大量
matplotlib.patch.Rectangle
|
Axes.patches
|
|
Axes.plot
| 一到多个
matplotlib.lines.Line2D
|
Axes.lines
|
下面这个简单的Python脚本解释了以上架构。它定义了后端,将
Figure
链接至该后端,然后使用数组库
numpy
创建10,000个正太分布的随机数,最后绘制出它们的柱状图。
# Import the FigureCanvas from the backend of your choice
# and attach the Figure artist to it.
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from matplotlib.figure import Figure
fig = Figure()
canvas = FigureCanvas(fig)
# Import the numpy library to generate the random numbers.
import numpy as np
x = np.random.randn(10000)
# Now use a figure method to create an Axes artist; the Axes artist is
# added automatically to the figure container fig.axes.
# Here "111" is from the MATLAB convention: create a grid with 1 row and 1
# column, and use the first cell in that grid for the location of the new
# Axes.
ax = fig.add_subplot(111)
# Call the Axes method hist to generate the histogram; hist creates a
# sequence of Rectangle artists for each histogram bar and adds them
# to the Axes container. Here "100" means create 100 bins.
ax.hist(x, 100)
# Decorate the figure with a title and save it.
ax.set_title('Normal distribution with $\mu=0, \sigma=1$')
fig.savefig('matplotlib_histogram.png')
3 、脚本层(pyplot
)
使用以上API的脚本效果很好,尤其是对于程序员而言,并且在编写Web应用服务器、UI应用程序或者是与其他开发人员共享的脚本时,这通常是比较合适的编程范式。对于日常用途,尤其对于非专业程序员而要完成一些交互式的研究工作的实验科学家而言,以上API的语法可能有些难以掌握。大多数用于数据分析与可视化的专用语言都会提供轻量级的脚本接口来简化一些常见任务。matplotlib在其
matplotlib.pyplot
接口中便实现了这一点。以上代码改用
pyplot
之后如下所示。
import matplotlib.pyplot as plt
import numpy as np
x = np.random.randn(10000)
plt.hist(x, 100)
plt.title(r'Normal distribution with $\mu=0, \sigma=1$')
plt.savefig('matplotlib_histogram.png')
plt.show()
图11.4:用
pyplot
绘制的柱状图
pyplot
是一个状态化接口,大部分工作是处理样本文件的图形与坐标的生成,以及与所选后端的连接。它还维护了模块级的内部数据结构。这些数据结构表示了直接接收绘图命令的当前图形与坐标
下面仔细分析示例脚本中比较重要的几行,观察其内部状态的管理方式。
-
import matplotlib.pyplot as plt
:当
pyplot
模块被加载时,它分析本地配置文件。配置文件除了完成一些其他工作外,主要声明了默认的后端。可能是类似
QtAgg
的用户接口后端,于是上面的脚本将导入GUI框架并启动嵌入了图形的Qt窗口;或者可以是一个类似
Agg
的纯图像后端,这样脚本会生成硬拷贝输出然后退出。 -
plt.hist(x, 100)
:这是脚本中第一个绘图命令。
pyplot
会检测其内部数据结构已查看是否存在当前
Figure
实例。如果存在,则提取当前
Axes
,并将绘图行为导向
Axes.hist
的API调用。在该脚本中不存在
Figure
实例,所以会生成一个
FIgure
与
Axes
,并将它们设为当前值,然后将绘图行为导向
Axes.hist
。 plt.hist(x, 100): This is the first plotting command in the script. pyplot will check its internal data structures to see if there is a current Figure instance. If so, it will extract the current Axes and direct plotting to the Axes.hist API call. In this case there is none, so it will create a Figure and Axes, set these as current, and direct the plotting to Axes.hist. plt.title(r’Normal distribution with $\mu=0, \sigma=1$’): As above, pyplot will look to see if there is a current Figure and Axes. Finding that there is, it will not create new instances but will direct the call to the existing Axes instance method Axes.set_title. plt.show(): This will force the Figure to render, and if the user has indicated a default GUI backend in their configuration file, will start the GUI mainloop and raise any figures created to the screen.
三、总结
1、matplotlib是一个非常庞大的系统,而且从诞生到现在繁荣发展经历了几十年的时间,里面所涉及到的软件架构很是复杂。
2、matplotlib的使用可以从三个层面上去入手,即
“后端——美工——脚本”。
(1)后端
:我们当然可以从最底层的后端搭建一个图形,负责软件与绘图硬件的交互,图形的各种渲染等等,这异常困难,而且对于程序员的开发效率来说,十分缓慢,因为绘图的框架整个都要自己去搭建,很麻烦。
(2)美工
:这是在后端的基础之上的进一步封装,有一点类似于GDI+绘图方式,我们只关心“ 在哪里画 、用什么笔、 画什么图 ”,所用的笔,所用的颜料,画的图形类型(折线图,条形图,散点图等等)这些已经封装好了,封装是通过一系列的类去完成的。
(3)脚本
:脚本是matplotlib的更高层封装,也就是我们平时所用的pyplot模块的高级封装,使用脚本,我们再也不用关心这个图形到底是怎么画的了,我们只需要关心画图的效果即可。适用“ 后端 ”、“ 美工 ”、要很多代码才能完成的事,使用脚本一句话就可以实现,如:
plt.plot(x,y,color=’red’,linewidth=3,linestyle=’–‘)
就是这么简单的一句话如果要自己从头搭建,是一件很困难的事情。