這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
作為一個程式員,很多時候雖然我喜歡盯著 console 輸出的一堆數字看一些系統變化指標,但俗話說,一圖勝千言,如果能自動的將很多資料組建圖表展示,會更加清晰明了,而且能直接從變化的曲線上面得知更多的資訊。這也就是我特別喜歡 Prometheus + Grafana 的原因。
但很多項目,尤其是臨時的一些測試專案,我不可能為了看一個資料圖表就搭建一套 Prometheus + Grafana 系統,那樣效率太低,更多時候,我還是希望能有一個更簡單的工具將一些資料展示出來。
幸運的是,我們可以通過 plot 非常方便的做到。plot 是一個用 Go 語言實現的繪圖庫,我們可以通過它繪製非常豐富的圖表,並且可以輸出成多種格式。另外,plot 還提供了非常方便的 interface,我們可以通過它來定製自己的圖表。
簡單樣本
我們可以通過 plot 自己提供的 plotutil 工具繪製簡單的圖形。
Line and Points
因為最近剛在看可汗學院的微觀經濟學,所以就以 price 和 quantity demand 來作為第一個例子,價格和需求數量通常是成反比的關係,繪製的圖形應該是一條下降的曲線。為了簡化程式碼數,這裡特意去掉了錯誤處理。
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")}
執行之後,我們就能可以得到一個命名為 price 的 png 檔案,看起來就是這樣的:
Histograms
在用 Prometheus 和 Grafana 的時候,我其實對一些 histogram 的 metric 的展示不怎麼滿意,因為 Grafana 的 X 軸是時間相關的,所以並不能展示柱狀圖,只能使用 histogram_quantile
或者其他函數得到相關的變化曲線,用 plot 則可以非常方便的將 Prometheus 的資料拿出來畫圖。
一個簡單的例子:
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。然後我們實現 Plotter 的 Plot 函數,如下:
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 函數裡面,我們使用 plt.Transforms(&c)
,得到兩個轉換函式,能夠將後面正方形的 point 轉換到實際的 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")}
建立一個 Sqaures,然後執行,得到圖表:
DataRanger
在上面的例子中,我們使用了類似 p.X.Min = 0
, p.X.Max = 20
的方式來設定整個圖表的範圍,但實際我們更希望能動態調整,因為我們不可能預估到實際的範圍到底有多大,這裡可以使用 DataRange
來實現:
func (s *Squares) DataRange() (float64, float64, float64, float64) { return plotter.XYRange(s.XYs)}
得到圖表如下:
GlyphBoxer
雖然通過 DataRange 能解決範圍的問題,但我們又發現,一些正方形在邊界上面被切掉了,這主要是因為我們是以正方形的中心繪製的,一個解決方案就是 DataRange 返回更大的區間,能夠覆蓋掉整個正方向。但另一個更好的辦法就是使用 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}
現在看起來就是這樣了:
後記
可以看到,使用 plot,我們可以非常方便的繪製圖表。當然,plot 的功能遠遠不止上面說的那麼簡單,譬如我們可以直接擷取 Prometheus 的資料然後繪圖,在發送給 Slack,或者畫一個 PieChart。
後面,我們也會考慮在一些內部的系統上面使用 plot,譬如效能測試架構,每次提交之後,跑很多效能測試,收集到每次的效能測試結果,使用 plot 繪圖展示等等。