Chaco是一個2D的繪圖庫,如果你安裝了Python(x,y)的話,可以在pythonxy的安裝目錄下的找到Chaco的demo程序:
\pythonxy\doc\Enthought Tool Suite\Chaco\examples\demo.py
運行此程序之后,將看到如下圖所示的窗口:
Chaco的演示程序的界面,包含了很多例子可以同時查看源程序和運行結果
在此窗口中,點選左欄中某個演示項目,它的相關源代碼將顯示在Source標簽下,而所繪制的結果顯示在Demo標簽下,通過學習Demo中的源程序,可以快速掌握Chaco庫的用法。
Chaco提供了類似Matlab和pylab的繪圖方式,我們稱之為面向腳本的繪圖方式。下面的程序從enthought.chaco.shell中導入腳本繪圖相關的內容,然后用numpy快速產生一個正弦波的x,y坐標的數組,然后傳遞給plot函數進行繪圖:
1 2 3 4 5 6 7 8 9 10 | import numpy as np
from enthought.chaco.shell import *
x = np.linspace(-2*np.pi, 2*np.pi, 100)
y = np.sin(x)
plot(x, y, "r-")
title("First plot")
ytitle("sin(x)")
show()
|
用Chaco的腳本繪圖方式快速繪制正弦波
plot函數的第三個參數中的"r"指定繪圖的顏色為紅色,"-"指定繪圖的線型為實線。title函數為繪圖添加標題,ytitle為Y軸添加標題,show()函數最終顯示繪圖結果。
腳本繪圖不是Chaco的強項,雖然它的這套腳本繪圖API和Matplotlib的pylab類似,不過它提供的功能卻沒有pylab豐富。Chaco的優勢在于它可以很方便地嵌入到你的應用程序之中,開發出自己獨特的繪圖應用。
要將Chaco嵌入到別的應用程序之中,需要做一些額外的工作,因此代碼量比面向腳本繪圖要多,不過同時也更具有靈活性。先來看一個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | from enthought.traits.api import HasTraits, Instance
from enthought.traits.ui.api import View, Item
from enthought.chaco.api import Plot, ArrayPlotData
from enthought.enable.component_editor import ComponentEditor
from numpy import linspace, sin
class LinePlot(HasTraits):
plot = Instance(Plot)
traits_view = View(
Item('plot',editor=ComponentEditor(), show_label=False),
width=500, height=500, resizable=True, title="Chaco Plot")
def __init__(self):
super(LinePlot, self).__init__()
x = linspace(-14, 14, 100)
y = sin(x) * x**3
plotdata = ArrayPlotData(x=x, y=y)
plot = Plot(plotdata)
plot.plot(("x", "y"), type="line", color="blue")
plot.title = "sin(x) * x^3"
self.plot = plot
if __name__ == "__main__":
LinePlot().configure_traits()
|
上面這段代碼繪制如下的曲線:
用Chaco的面向對象的方式繪制曲線
這段代碼看起來似乎挺復雜,其實只要掌握了其基本設計思想,就很容易理解了。首先是許多import語句,為了保持應用程序的名字空間整潔以及讓自動語法檢查工具能幫助我們檢查代碼,這些語句只import了需要的對象。
接下來的代碼部分:
class LinePlot(HasTraits):
plot = Instance(Plot)
首先定義一個LinePlot繼承于HasTraits,并且它有一個trait屬性plot為Plot類的實例。然后定義視圖:
traits_view = View(
Item('plot',editor=ComponentEditor(), show_label=False),
width=500, height=500, resizable=True, title="Chaco Plot")
此視圖在LinePlot類中定義,因此在調用configure_traits的時候就不需要指定視圖了。視圖中有一個元素,它將用來顯示名為plot的屬性的內容,視圖中的元素用Item創建。注意這里使用字符串指定視圖元素所對應的屬性。然后通過關鍵字參數editor指定此視圖元素采用ComponentEditor進行顯示。并且不顯示其標簽(show_label=False)。通過View的關鍵字參數width、height、resizable和title分別指定界面的寬、高、是否可改變大小以及其窗口標題欄的文字。
接下來看構造函數,真正的計算在這里:
def __init__(self):
super(LinePlot, self).__init__()
x = linspace(-14, 14, 100)
y = sin(x) * x**3
plotdata = ArrayPlotData(x=x, y=y)
在構造函數做其它事情之前,一定要記住調用父類的構造函數,這樣HasTraits的功能才能真正在我們的實例中出現。
接下來和腳本繪圖一樣,計算出繪圖所需的x,y坐標的數值數組。然后將這兩個數組存到一個ArrayPlotData對象中。ArrayPlotData和字典(dict)有些類似,它將一個字符串(數組的名字)和數組本身聯系起來。而真正的繪圖對象plot將通過數組的名字在ArrayPlotData中獲得數組的內容。這樣做就在數據和繪圖對象中形成了一個接口界面,修改ArrayPlotData中的數組的值將會立即反應到與此數據相連的繪圖對象,而多個繪圖對象可以共用ArrayPlotData中的同一數組。
接下來創建繪圖對象plot,并且將我們創建的ArrayPlotData實例傳遞給它,此后plot將在此實例中獲取自己繪圖所需的數據。:
plot = Plot(plotdata)
Plot類將Chaco中提供的許多真正用來繪圖的對象進行包裝,提供了一個統一的接口用來創建和管理這些繪圖對象。在今后深入學習Chaco的過程中我們將通過分析Plot類的實現來了解Chaco庫的設計思想。
接下來調用plot方法在Plot內部創建真正的繪圖對象(一個曲線圖):
plot.plot(("x", "y"), type="line", color="blue")
注意我們傳遞給plot方法的是數組的名字而不是數組本身,Plot對象會自動通過數組的名字在ArrayPlotData的實例中找到其對應的數組。
然后設置繪圖的標題,并且把繪圖實例賦值給plot屬性:
plot.title = "sin(x) * x^3"
self.plot = plot
最后是LinePlot對象的實例化和調用configure_trait顯示繪圖窗口:
if __name__ == "__main__":
LinePlot().configure_traits()
由于沒有給configure_traits傳遞視圖參數,它將在LinePlot實例中尋找視圖的定義,于是它找到traits_view,并且用此視圖來顯示LinePlot實例的trait屬性。于是plot屬性將如traits_view中定義的一樣,用ComponentEditor顯示。
采用和LinePlot類同樣的模式,我們可以繪制更多的曲線圖:
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 33 | from enthought.traits.api import HasTraits, Instance
from enthought.traits.ui.api import View, Item
from enthought.chaco.api import Plot, ArrayPlotData, Legend
from enthought.enable.component_editor import ComponentEditor
from numpy import linspace, sin, cos
class LinePlot(HasTraits):
plot = Instance(Plot)
traits_view = View(
Item('plot',editor=ComponentEditor(), show_label=False),
width=500, height=500, resizable=True, title="Chaco Plot")
def __init__(self):
super(LinePlot, self).__init__()
x = linspace(-14, 14, 100)
y1 = sin(x) * x**3
y2 = cos(x) * x**3
plotdata = ArrayPlotData(x=x, y1=y1, y2=y2)
plot = Plot(plotdata)
plot.plot(("x", "y1"), type="line", color="blue", name="sin(x) * x**3")
plot.plot(("x", "y2"), type="line", color="red", name="cos(x) * x**3")
plot.plot(("x", "y2"), type="scatter", color="red", marker = "circle",
marker_size = 2, name="cos(x) * x**3 points")
plot.title = "Multiple Curves"
self.plot = plot
legend = Legend(padding=10, align="ur")
legend.plots = plot.plots
plot.overlays.append(legend)
if __name__ == "__main__":
lineplot = LinePlot()
lineplot.configure_traits()
|
繪制多條曲線并且添加圖示
在這個程序中,我們調用了3次plot.plot方法,其中兩次的type="line"繪制曲線,一次是type="scatter"繪制坐標點。繪制坐標點時通過marker和marker_size配置點的形狀和大小。并且我們為每個plot傳遞了一個name參數。
為了繪制圖示(legend),從chaco.api中載入Legend之后,使用如下三行代碼為坐標圖添加圖示::
legend = Legend(component=plot, padding=10, align="ur")
legend.plots = plot.plots
plot.overlays.append(legend)
其中第一行創建一個Legend對象,并且設置padding和align兩個屬性,padding設置其內容與邊框之間的距離,align設置其在容器中的位置: upper right。
為了讓legend對象知道要顯示什么曲線的圖示,我們需要把曲線對象傳遞給它。三次調用plot繪制的曲線可以通過plot對象plots屬性得到,在iPython中運行完上面的程序之后,輸入lineplot.plot.plots查看plots屬性的值:
>>> lineplot.plot.plots
{'cos(x) * x**3 points': [<enthought.chaco.scatterplot.ScatterPlot object at 0x1436B4B0>],
'cos(x) * x**3': [<enthought.chaco.lineplot.LinePlot object at 0x1436B120>],
'sin(x) * x**3': [<enthought.chaco.lineplot.LinePlot object at 0x143140F0>]}
我們將plot.plots傳遞給legend.plots,于是legend就知道要顯示哪些曲線的圖示了。
最后我們將legend對象添加到plot.overlays中。整個繪圖區域分為許多層,每一層放置不同的繪圖元素,overlays是最上面的一層,其中放置在屏幕坐標系中的繪圖元素。plot的overlays屬性為一個TraitListObject類的對象,它繼承于list類,具有list的所有能力,因此可以用append方法將繪圖元素添加進overlays層。
在Chaco的實現中,Plot類繼承于DataView類,而DataView類繼承于OverlayPlotContainer類,因此Plot本身就是一個容器。可以把OverlayPlotContainer想象成多張透明繪圖紙,我們在多張紙上繪圖,然后通過OverlayPlotContainer容器將這些紙張重疊起來,就組成了最終所繪制的圖。
除了OverlayPlotContainer容器之外,Chaco還提供了下面幾種容器:
下面讓我們來看一個用HPlotContainer的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | from enthought.traits.api import HasTraits, Instance
from enthought.traits.ui.api import View, Item
from enthought.chaco.api import HPlotContainer, ArrayPlotData, Plot
from enthought.enable.component_editor import ComponentEditor
from numpy import linspace, sin
class ContainerExample(HasTraits):
plot = Instance(HPlotContainer)
traits_view = View(Item('plot', editor=ComponentEditor(), show_label=False),
width=1000, height=600, resizable=True, title="Chaco Plot")
def __init__(self):
super(ContainerExample, self).__init__()
x = linspace(-14, 14, 100)
y = sin(x) * x**3
plotdata = ArrayPlotData(x=x, y=y)
scatter = Plot(plotdata)
scatter.plot(("x", "y"), type="scatter", color="blue")
line = Plot(plotdata)
line.plot(("x", "y"), type="line", color="blue")
container = HPlotContainer(scatter, line)
self.plot = container
if __name__ == "__main__":
ContainerExample().configure_traits()
|
用容器繪制兩個子圖
這個程序和前面的例子類似,所不同的是:ContainerExample的plot屬性不是Plot的實例,而是改為HPlotContainer的實例。這樣它就可以橫向排列多個圖了。
在__init__函數中,用創建兩個Plot對象scatter和line,然后創建HPlotContainer對象container,并把兩個plot對象傳遞給它,這樣container就知道要水平排列哪些圖了。
每個容器都有很多屬性可以設置,如果我們在__init__函數最后添加如下幾行程序::
scatter.padding_right = 0
line.padding_left = 0
line.y_axis.orientation = "right"
修改兩個容器的左右padding值,使它們緊靠在一起
我們看到兩個Plot之間的間距沒有了,他是通過設置容器左圖(scatter)的右邊距為0,右圖(line)的左邊距為0來實現的。 并且將右圖的Y軸坐標設置到了右邊。
到目前為止所繪制的圖都是靜態的,一旦創建出來就沒有辦法改變其各種顯示屬性了。Chaco庫是建立在Traits庫基礎之上的,我們看到的各種各樣的對象的屬性都是trait屬性,這樣我們可以使用Traits和TraitsUI的強大功能設置對象的各種屬性,下面是一個完整的例子:
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 33 34 35 36 37 38 39 40 41 42 | from enthought.traits.api import HasTraits, Instance, Int, Color
from enthought.traits.ui.api import View, Group, Item
from enthought.enable.component_editor import ComponentEditor
from enthought.chaco.api import marker_trait, Plot, ArrayPlotData
from numpy import linspace, sin
class ScatterPlotTraits(HasTraits):
plot = Instance(Plot)
color = Color("blue")
marker = marker_trait
marker_size = Int(4)
traits_view = View(
Group(Item('color', label="Color"),
Item('marker', label="Marker"),
Item('marker_size', label="Size"),
Item('plot', editor=ComponentEditor(), show_label=False),
orientation = "vertical"),
width=800, height=600, resizable=True, title="Chaco Plot")
def __init__(self):
super(ScatterPlotTraits, self).__init__()
x = linspace(-14, 14, 100)
y = sin(x) * x**3
plotdata = ArrayPlotData(x = x, y = y)
plot = Plot(plotdata)
self.renderer = plot.plot(("x", "y"), type="scatter", color="blue")[0]
self.plot = plot
def _color_changed(self):
self.renderer.color = self.color
def _marker_changed(self):
self.renderer.marker = self.marker
def _marker_size_changed(self):
self.renderer.marker_size = self.marker_size
if __name__ == "__main__":
ScatterPlotTraits().configure_traits()
|
為了觀察trait控件是如何動態地修改繪圖的的各個屬性,我用flash錄制下對控件的操作,請點擊下圖下方的播放按鈕觀看動畫。
通過觀察上面的這個動畫,我們發現對顏色、點型和點的大小等屬性的修改立即響應到繪圖的屬性上。下面我們來分析一下這個程序:
ScatterPlotTraits類定義了4個trait屬性,其中一個是我們已經熟知的plot屬性,其余的三個分別為color,marker和marker_size。color是一個Color屬性,marker_size是一個Int屬性,marker比較特別,它是在Chaco的scatter_makers.py中定義的一個Trait屬性,采用字典創建,將一個描述點型的字符串映射到點型對應的類,這樣我們通過界面上的下拉選擇框選擇某個點型名稱時,在程序內部實際上選擇的是其對應的類。
接下來第14行在ScatterPlotTraits類內部定義了一個視圖對象traits_view。它創建4個Item分別與4個trait屬性對應。為了響應trait屬性值的改變事件,我們為類添加了3個事件處理函數_color_changed,_marker_changed和_marker_size_changed。這個三個處理函數通過其名字和trait屬性對應,即名為foo的trait屬性的缺省事件處理函數名為_foo_changed。值得注意的是這三個處理函數是和trait屬性相對應的,而不是界面上的控件。當用戶更改了控件的內容之后,此更改自動反映為trait屬性值的更改,當trait屬性的值更改時,事件處理函數將被運行。這樣,當處理函數運行的時候,trait屬性的值已經是最新的界面上所顯示的值了。因此只需要將此值賦值給曲線繪圖對象(render)的對應的屬性即可。
那么render對象是什么?我們看到它是plot.plot函數的返回值,這個返回值就是圖中所畫的那條曲線。前面我們演示了通過plot.plots獲得過plot中所有的繪圖對象。因此不用render保存此返回值,也可以用plot.plots.values()[0],或者plot.plots["plot0"]來獲取這個繪圖對象。"plot0"是系統自動為我們的曲線所起的名字。