matplotlib是基于Python的繪圖庫,廣泛用于Python科學計算界。它完整支持二維繪圖以及部分支持三維繪圖。該繪圖庫致力于能適應廣泛的用戶需求。它可以根據所選的用戶接口工具來嵌入繪圖算法。與此同時,對于使用GTK+、Qt、Tk、FLTK、wxWidgets與Cocoa的所有主要桌面操作系統,matplotlib能支持交互式繪圖。在Python的交互式shell中,我們可以使用簡單的、過程式的命令交互式地調用matplotlib來生成圖形,與使用Mathematica、IDL或者MATLAB繪圖非常相似。matplotlib也可以嵌入到無報文頭的Web服務器中,以提供基于光柵(如PNG格式)與向量(如Postscript、PDF以及紙面效果很好的SVG格式)這兩種格式的圖形硬拷貝。
## 11.1 硬件鎖問題
我們其中一位開發者(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中各種豐富的數據結構,如列表、辭典與集合等等。
圖11.1:最初的matplotlib程序——ECoG查看器
## 11.2 matplotlib軟件架構概述
頂層的matplotlib對象名為**Figure**,它包含與管理某個圖形的所有元素。matplotlib必須完成的一個核心架構性任務是實現**Figure**的繪制與操作框架,并且做到該框架與**Figure**到用戶視窗接口或硬拷貝渲染行為是分離的。這使得我們可以為**Figure**添加越來越復雜的特性與邏輯,同時保持“后端”或輸出設備的相對簡化。matplotlib不僅封裝了用于向多種設備渲染的繪圖接口,還封裝了基本事件處理以及多數流行的用戶界面工具的視窗功能。因此,用戶可以創建相當豐富的交互式圖形算法與用戶界面工具(用到可能存在的鼠標與鍵盤),而又不必修改matplotlib已經支持的6種界面工具。
要實現這些,matplotlib的架構被邏輯性地分為三層。這三層邏輯可以視為一個棧。每層邏輯知道如何與其下的一層邏輯進行通信,但在下層邏輯看來,上層是透明的。這三層從底向上分別為:后端、美工與腳本。
11.2.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](http://www.aosabook.org/en/bib2.html#agg)])的基于像素點的核心渲染器。這是一個高性能庫,可以進行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的事件框架下實現的。
11.2.2?**Artis**層
**Artist**層次結構處于matplotlib的中間層,負責很大一部分繁重的計算任務。延續之前將后端的**FigureCanvas**看作畫紙的比喻,**Artis**對象知道如何用**Renderer**(畫筆)在畫布上畫出墨跡。matplotlib中的**Figure**就是一個**Artist**對象實例。標題、直線、刻度標記以及圖像等等都對應某個Artist實例(如圖11.3)。**Artist**的基類是**matplotlib.artist.Artist**,其中包含所有**Artist**的共享屬性,包括從美工坐標系統到畫布坐標系統的變換(后面將詳細介紹)、可見性、定義用戶可繪制區域的剪切板、標簽,以及處理“選中”這樣的用戶交互動作的接口,即在美工層檢測鼠標點擊事件。
圖11.2:matplotlib生成的圖形
圖11.3:用于繪制圖11.2的**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')
~~~
11.2.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.
A somewhat stripped-down and simplified version of pyplot's frequently used line plotting function matplotlib.pyplot.plot is shown below to illustrate how a pyplot function wraps functionality in matplotlib's object-oriented core. All other pyplot scripting interface functions follow the same design.
~~~
@autogen_docstring(Axes.plot)
def plot(*args, **kwargs):
ax = gca()
ret = ax.plot(*args, **kwargs)
draw_if_interactive()
return ret
~~~
The Python decorator @autogen_docstring(Axes.plot) extracts the documentation string from the corresponding API method and attaches a properly formatted version to the pyplot.plot method; we have a dedicated module matplotlib.docstring to handle this docstring magic. The *args and **kwargs in the documentation signature are special conventions in Python to mean all the arguments and keyword arguments that are passed to the method. This allows us to forward them on to the corresponding API method. The call ax = gca() invokes the stateful machinery to "get current Axes" (each Python interpreter can have only one "current axes"), and will create the Figure and Axes if necessary. The call to ret = ax.plot(*args, **kwargs) forwards the function call and its arguments to the appropriate Axes method, and stores the return value to be returned later. Thus the pyplot interface is a fairly thin wrapper around the core Artist API which tries to avoid as much code duplication as possible by exposing the API function, call signature and docstring in the scripting interface with a minimal amount of boilerplate code.
- 前言(卷一)
- 卷1:第1章 Asterisk
- 卷1:第3章 The Bourne-Again Shell
- 卷1:第5章 CMake
- 卷1:第6章 Eclipse之一
- 卷1:第6章 Eclipse之二
- 卷1:第6章 Eclipse之三
- 卷1:第8章 HDFS——Hadoop分布式文件系統之一
- 卷1:第8章 HDFS——Hadoop分布式文件系統之二
- 卷1:第8章 HDFS——Hadoop分布式文件系統
- 卷1:第12章 Mercurial
- 卷1:第13章 NoSQL生態系統
- 卷1:第14章 Python打包工具
- 卷1:第15章 Riak與Erlang/OTP
- 卷1:第16章 Selenium WebDriver
- 卷1:第18章 SnowFlock
- 卷1:第22章 Violet
- 卷1:第24章 VTK
- 卷1:第25章 韋諾之戰
- 卷2:第1章 可擴展Web架構與分布式系統之一
- 卷2:第1章 可擴展Web架構與分布式系統之二
- 卷2:第2章 Firefox發布工程
- 卷2:第3章 FreeRTOS
- 卷2:第4章 GDB
- 卷2:第5章 Glasgow Haskell編譯器
- 卷2:第6章 Git
- 卷2:第7章 GPSD
- 卷2:第9章 ITK
- 卷2:第11章 matplotlib
- 卷2:第12章 MediaWiki之一
- 卷2:第12章 MediaWiki之二
- 卷2:第13章 Moodle
- 卷2:第14章 NginX
- 卷2:第15章 Open MPI
- 卷2:第18章 Puppet part 1
- 卷2:第18章 Puppet part 2
- 卷2:第19章 PyPy
- 卷2:第20章 SQLAlchemy
- 卷2:第21章 Twisted
- 卷2:第22章 Yesod
- 卷2:第24章 ZeroMQ