作為一個程序員,很多時候雖然我喜歡盯著 console 輸出的一堆數(shù)字看一些系統(tǒng)變化指標(biāo),但俗話說,一圖勝千言,如果能自動的將很多數(shù)據(jù)生成圖表展示,會更加清晰明了,而且能直接從變化的曲線上面得知更多的信息。這也就是我特別喜歡 Prometheus + Grafana 的原因。
但很多項目,尤其是臨時的一些測試項目,我不可能為了看一個數(shù)據(jù)圖表就搭建一套 Prometheus + Grafana 系統(tǒng),那樣效率太低,更多時候,我還是希望能有一個更簡單的工具將一些數(shù)據(jù)展示出來。
幸運(yùn)的是,我們可以通過 plot 非常方便的做到。plot 是一個用 Go 語言實(shí)現(xiàn)的繪圖庫,我們可以通過它繪制非常豐富的圖表,并且可以輸出成多種格式。另外,plot 還提供了非常方便的 interface,我們可以通過它來定制自己的圖表。
簡單示例
我們可以通過 plot 自己提供的 plotutil 工具繪制簡單的圖形。
Line and Points
因為最近剛在看可汗學(xué)院的微觀經(jīng)濟(jì)學(xué),所以就以 price 和 quantity demand 來作為第一個例子,價格和需求數(shù)量通常是成反比的關(guān)系,繪制的圖形應(yīng)該是一條下降的曲線。為了簡化代碼行數(shù),這里特意去掉了錯誤處理。
import (
"github.com/gonum/plot"
"github.com/gonum/plot/plotter"
"github.com/gonum/plot/plotutil"
"github.com/gonum/plot/vg"
)
func main() {
p, _ := plot.New()
p.Title.Text = "Hello Price"
p.X.Label.Text = "Quantity Demand"
p.Y.Label.Text = "Price"
points := plotter.XYs{
{2.0, 60000.0},
{4.0, 40000.0},
{6.0, 30000.0},
{8.0, 25000.0},
{10.0, 23000.0},
}
plotutil.AddLinePoints(p, points)
p.Save(4*vg.Inch, 4*vg.Inch, "price.png")
}
執(zhí)行之后,我們就能可以得到一個命名為 price 的 png 文件,看起來就是這樣的:

Histograms
在用 Prometheus 和 Grafana 的時候,我其實(shí)對一些 histogram 的 metric 的展示不怎么滿意,因為 Grafana 的 X 軸是時間相關(guān)的,所以并不能展示柱狀圖,只能使用 histogram_quantile 或者其他函數(shù)得到相關(guān)的變化曲線,用 plot 則可以非常方便的將 Prometheus 的數(shù)據(jù)拿出來畫圖。
一個簡單的例子:
import (
"github.com/gonum/plot"
"github.com/gonum/plot/plotter"
"github.com/gonum/plot/vg"
)
func main() {
p, _ := plot.New()
p.Title.Text = "Histogram"
bins := plotter.XYs{
{10, 10},
{20, 20},
{30, 50},
{40, 20},
{50, 10},
}
h, _ := plotter.NewHistogram(bins, 5)
p.Add(h)
p.Save(4*vg.Inch, 4*vg.Inch, "histogram.png")
}
柱狀圖如下:

自定制 Plotter
Plotter
除了使用 plot 提供的圖表之外,我們還可以非常方便定制自己的圖表。這里我們簡單的畫一個邊長為 20 的正方形。首先定義 Sqaures:
type Squares struct {
plotter.XYs
}
Squares 里面就只有一批 points,用來表示各個正方形的中心 point。然后我們實(shí)現(xiàn) Plotter 的 Plot 函數(shù),如下:
func (s *Squares) Plot(c draw.Canvas, plt *plot.Plot) {
trX, trY := plt.Transforms(&c)
c.SetColor(color.RGBA{R: 196, B: 128, A: 255})
r := vg.Length(10.0)
for _, p := range s.XYs {
p1 := vg.Point{trX(p.X) - r, trY(p.Y) - r}
p2 := vg.Point{trX(p.X) - r, trY(p.Y) + r}
p3 := vg.Point{trX(p.X) + r, trY(p.Y) + r}
p4 := vg.Point{trX(p.X) + r, trY(p.Y) - r}
var p vg.Path
p.Move(p1)
p.Line(p2)
p.Line(p3)
p.Line(p4)
p.Line(p1)
p.Close()
c.Fill(p)
}
}
上面的 Plot 函數(shù)里面,我們使用 plt.Transforms(&c) ,得到兩個轉(zhuǎn)換函數(shù),能夠?qū)⒑竺嬲叫蔚?point 轉(zhuǎn)換到實(shí)際的 canvas 的 point 上面。
func main() {
points := plotter.XYs{
{2, 2},
{4, 4},
{6, 6},
{8, 8},
{10, 10},
}
s := Squares{points}
p, _ := plot.New()
p.Title.Text = "Squares"
p.X.Label.Text = "X"
p.Y.Label.Text = "Y"
p.X.Min = 0
p.X.Max = 20
p.Y.Min = 0
p.Y.Max = 20
p.Add(&s)
p.Save(4*vg.Inch, 4*vg.Inch, "squares.png")
}
創(chuàng)建一個 Sqaures,然后執(zhí)行,得到圖表:

DataRanger
在上面的例子中,我們使用了類似 p.X.Min = 0, p.X.Max = 20 的方式來設(shè)置整個圖表的范圍,但實(shí)際我們更希望能動態(tài)的調(diào)整,因為我們不可能預(yù)估到實(shí)際的范圍到底有多大,這里可以使用 DataRange 來實(shí)現(xiàn):
func (s *Squares) DataRange() (float64, float64, float64, float64) {
return plotter.XYRange(s.XYs)
}
得到圖表如下:

GlyphBoxer
雖然通過 DataRange 能解決范圍的問題,但我們又發(fā)現(xiàn),一些正方形在邊界上面被切掉了,這主要是因為我們是以正方形的中心繪制的,一個解決方法就是 DataRange 返回更大的區(qū)間,能夠覆蓋掉整個正方向。但另一個更好的辦法就是使用 GlyphBoxes,用來顯示的告訴要繪制的圖表的位置和大小。
func (s *Squares) GlyphBoxes(plt *plot.Plot) []plot.GlyphBox {
boxes := make([]plot.GlyphBox, len(s.XYs))
r := vg.Length(10.0)
for i, p := range s.XYs {
boxes[i].X = plt.X.Norm(p.X)
boxes[i].Y = plt.Y.Norm(p.Y)
boxes[i].Rectangle = vg.Rectangle{
Min: vg.Point{X: -r, Y: -r},
Max: vg.Point{X: +r, Y: +r},
}
}
return boxes
}
現(xiàn)在看起來就是這樣了:

后記
可以看到,使用 plot,我們可以非常方便的繪制圖表。當(dāng)然,plot 的功能遠(yuǎn)遠(yuǎn)不止上面說的那么簡單,譬如我們可以直接獲取 Prometheus 的數(shù)據(jù)然后繪圖,在發(fā)送給 Slack,或者畫一個 PieChart。
后面,我們也會考慮在一些內(nèi)部的系統(tǒng)上面使用 plot,譬如性能測試框架,每次提交之后,跑很多性能測試,收集到每次的性能測試結(jié)果,使用 plot 繪圖展示等等。