Go 每日一庫之 plot

簡介

本文介紹 Go 語言的一個很是強大、好用的繪圖庫——plotplot內置了不少經常使用的組件,基本知足平常需求。同時,它也提供了定製化的接口,能夠實現咱們的個性化需求。plot主要用於將數據可視化,便於咱們觀察、比較。html

快速使用

先安裝:git

$ go get gonum.org/v1/plot/...

後使用:github

package main

import (
  "log"
  "math/rand"

  "gonum.org/v1/plot"
  "gonum.org/v1/plot/plotter"
  "gonum.org/v1/plot/plotutil"
  "gonum.org/v1/plot/vg"
)

func main() {
  rand.Seed(int64(0))

  p, err := plot.New()
  if err != nil {
    log.Fatal(err)
  }

  p.Title.Text = "Get Started"
  p.X.Label.Text = "X"
  p.Y.Label.Text = "Y"

  err = plotutil.AddLinePoints(p,
    "First", randomPoints(15),
    "Second", randomPoints(15),
    "Third", randomPoints(15))
  if err != nil {
    log.Fatal(err)
  }

  if err = p.Save(4*vg.Inch, 4*vg.Inch, "points.png"); err != nil {
    log.Fatal(err)
  }
}

func randomPoints(n int) plotter.XYs {
  points := make(plotter.XYs, n)
  for i := range points {
    if i == 0 {
      points[i].X = rand.Float64()
    } else {
      points[i].X = points[i-1].X + rand.Float64()
    }
    points[i].Y = points[i].X + 10 * rand.Float64()
  }

  return points
}

程序運行輸出points.png圖片文件:golang

plot的使用比較直觀。首先,調用plot.New()建立一個「畫布」,畫布結構以下:json

// Plot is the basic type representing a plot.
type Plot struct {
  Title struct {
    Text string
    Padding vg.Length
    draw.TextStyle
  }
  BackgroundColor color.Color
  X, Y Axis
  Legend Legend
  plotters []Plotter
}

而後,經過直接給畫布結構字段賦值,設置圖像的屬性。例如p.Title.Text = "Get Started設置圖像標題內容;p.X.Label.Text = "X"p.Y.Label.Text = "Y"設置圖像的 X 和 Y 軸的標籤名。後端

再而後,使用plotutil或者其餘子包的方法在畫布上繪製,上面代碼中調用AddLinePoints()繪製了 3 條折線。瀏覽器

最後保存圖像,上面代碼中調用p.Save()方法將圖像保存到文件中。緩存

更多圖形

gonum/plot將不一樣層次的接口封裝到特定的子包中:服務器

  • plot:提供了佈局和繪圖的簡單接口;
  • plotter:使用plot提供的接口實現了一組標準的繪圖器,例如散點圖、條形圖、箱狀圖等。可使用plotter提供的接口實現本身的繪圖器;
  • plotutil:爲繪製常見圖形提供簡便的方法;
  • vg:封裝各類後端,並提供了一個通用矢量圖形 API。

條形圖

條形圖經過相同寬度條形的高度或長短來表示數據的大小關係。將相同類型的數據放在一塊兒比較能很是直觀地看出不一樣,咱們常常在比較幾個庫的性能時使用條形圖。下面咱們採用json-iter/go的 GitHub 倉庫中用來比較jsonitereasyjsonstd三個 JSON 庫性能的數據來繪製條形圖:微信

package main

import (
  "log"

  "gonum.org/v1/plot"
  "gonum.org/v1/plot/plotter"
  "gonum.org/v1/plot/plotutil"
  "gonum.org/v1/plot/vg"
)

func main() {
  std := plotter.Values{35510, 1960, 99}
  easyjson := plotter.Values{8499, 160, 4}
  jsoniter := plotter.Values{5623, 160, 3}

  p, err := plot.New()
  if err != nil {
    log.Fatal(err)
  }

  p.Title.Text = "jsoniter vs easyjson vs std"
  p.Y.Label.Text = ""

  w := vg.Points(20)
  stdBar, err := plotter.NewBarChart(std, w)
  if err != nil {
    log.Fatal(err)
  }
  stdBar.LineStyle.Width = vg.Length(0)
  stdBar.Color = plotutil.Color(0)
  stdBar.Offset = -w

  easyjsonBar, err := plotter.NewBarChart(easyjson, w)
  if err != nil {
    log.Fatal(err)
  }
  easyjsonBar.LineStyle.Width = vg.Length(0)
  easyjsonBar.Color = plotutil.Color(1)

  jsoniterBar, err := plotter.NewBarChart(jsoniter, w)
  if err != nil {
    log.Fatal(err)
  }
  jsoniterBar.LineStyle.Width = vg.Length(0)
  jsoniterBar.Color = plotutil.Color(2)
  jsoniterBar.Offset = w

  p.Add(stdBar, easyjsonBar, jsoniterBar)
  p.Legend.Add("std", stdBar)
  p.Legend.Add("easyjson", easyjsonBar)
  p.Legend.Add("jsoniter", jsoniterBar)
  p.Legend.Top = true
  p.NominalX("ns/op", "allocation bytes", "allocation times")

  if err = p.Save(5*vg.Inch, 5*vg.Inch, "barchart.png"); err != nil {
    log.Fatal(err)
  }
}

首先生成值列表,咱們在最開始的例子中生成了二維座標列表plotter.XYs,實際上還有三維座標列表plotter.XYZs

而後,調用plotter.NewBarChart()分別爲三組數據生成條形圖。w = vg.Points(20)用來設置條形的寬度。LineStyle.Width設置線寬,這個其實是邊框的寬度。Color設置顏色。Offset設置偏移,由於每組對應位置的條形放在一塊兒顯示更比如較,將stdBar.Offset設置爲-w會讓其向左偏移一個條形的寬度;easyjson偏移不設置,默認爲 0,不偏移;jsoniter偏移設置爲w,向右偏移一個條形的寬度。最終它們緊挨着顯示。

而後,將 3 個條形圖添加到畫布上。緊接着,設置它們的圖例,並將其顯示在頂部。

最後調用p.Save()保存圖片。

程序運行生成下面的圖片:

能夠很直觀地看到jsoniter的性能、內存佔用、內存分配次數各方面都是頂尖的。可能用同一種維度的數據,數量級相差不大,圖像會好看點(┬_┬)。

注意plotter.Color(2)這類用法。plot預約義了一組顏色值,若是咱們想要使用它們,能夠直接傳入索引獲取對應的顏色,更多的是爲了區分不一樣的圖形(例如上面的 3 個條形圖用了 3 個不一樣的索引):

// src/gonum.org/v1/plot/plotutil/plotutil.go
var DefaultColors = SoftColors
var SoftColors = []color.Color{
  rgb(241, 90, 96),
  rgb(122, 195, 106),
  rgb(90, 155, 212),
  rgb(250, 167, 91),
  rgb(158, 103, 171),
  rgb(206, 112, 88),
  rgb(215, 127, 180),
}

func Color(i int) color.Color {
  n := len(DefaultColors)
  if i < 0 {
    return DefaultColors[i%n+n]
  }
  return DefaultColors[i%n]
}

除了顏色,還有形狀plotter.Shape(i)和劃線模式plotter.Dashes(i)

vg.Length(0)有所不一樣,這個只是將 0 轉換爲vg.Length類型!

函數圖像

plot能夠繪製函數圖像!

func main() {
  p, err := plot.New()
  if err != nil {
    log.Fatal(err)
  }
  p.Title.Text = "Functions"
  p.X.Label.Text = "X"
  p.Y.Label.Text = "Y"

  square := plotter.NewFunction(func(x float64) float64 { return x * x })
  square.Color = plotutil.Color(0)

  sqrt := plotter.NewFunction(func(x float64) float64 { return 10 * math.Sqrt(x) })
  sqrt.Dashes = []vg.Length{vg.Points(1), vg.Points(2)}
  sqrt.Width = vg.Points(1)
  sqrt.Color = plotutil.Color(1)

  exp := plotter.NewFunction(func(x float64) float64 { return math.Pow(2, x) })
  exp.Dashes = []vg.Length{vg.Points(2), vg.Points(3)}
  exp.Width = vg.Points(2)
  exp.Color = plotutil.Color(2)

  sin := plotter.NewFunction(func(x float64) float64 { return 10*math.Sin(x) + 50 })
  sin.Dashes = []vg.Length{vg.Points(3), vg.Points(4)}
  sin.Width = vg.Points(3)
  sin.Color = plotutil.Color(3)

  p.Add(square, sqrt, exp, sin)
  p.Legend.Add("x^2", square)
  p.Legend.Add("10*sqrt(x)", sqrt)
  p.Legend.Add("2^x", exp)
  p.Legend.Add("10*sin(x)+50", sin)
  p.Legend.ThumbnailWidth = 0.5 * vg.Inch

  p.X.Min = 0
  p.X.Max = 10
  p.Y.Min = 0
  p.Y.Max = 100

  if err = p.Save(4*vg.Inch, 4*vg.Inch, "functions.png"); err != nil {
    log.Fatal(err)
  }
}

首先調用plotter.NewFunction()建立一個函數圖像。它接受一個函數,單輸入參數float64,單輸出參數float64,故只能畫出單自變量的函數圖像。接着爲函數圖像設置了三個屬性Dashes(劃線)、Width(線寬)和Color(顏色)。默認使用連續的線條來繪製函數,如圖中的平方函數。能夠經過設置Dashesplot繪製不連續的線條,Dashes接受兩個長度值,第一個長度表示間隔距離,第二個長度表示連續線的長度。這裏也使用到了plotutil.Color(i)依次使用前 4 個預約義的顏色。

建立畫布、設置圖例這些都與前面的相同。這裏還經過p.Xp.YMin/Max屬性限制了圖像繪製的座標範圍。

運行程序生成圖像:

氣泡圖

使用plot能夠畫出很是好看的氣泡圖:

func main() {
  n := 10
  bubbleData := randomTriples(n)

  minZ, maxZ := math.Inf(1), math.Inf(-1)
  for _, xyz := range bubbleData {
    if xyz.Z > maxZ {
      maxZ = xyz.Z
    }
    if xyz.Z < minZ {
      minZ = xyz.Z
    }
  }

  p, err := plot.New()
  if err != nil {
    log.Fatal(err)
  }
  p.Title.Text = "Bubbles"
  p.X.Label.Text = "X"
  p.Y.Label.Text = "Y"

  bs, err := plotter.NewScatter(bubbleData)
  if err != nil {
    log.Fatal(err)
  }
  bs.GlyphStyleFunc = func(i int) draw.GlyphStyle {
    c := color.RGBA{R: 196, B: 128, A: 255}
    var minRadius, maxRadius = vg.Points(1), vg.Points(20)
    rng := maxRadius - minRadius
    _, _, z := bubbleData.XYZ(i)
    d := (z - minZ) / (maxZ - minZ)
    r := vg.Length(d)*rng + minRadius
    return draw.GlyphStyle{Color: c, Radius: r, Shape: draw.CircleGlyph{}}
  }
  p.Add(bs)

  if err = p.Save(4*vg.Inch, 4*vg.Inch, "bubble.png"); err != nil {
    log.Fatal(err)
  }
}

func randomTriples(n int) plotter.XYZs {
  data := make(plotter.XYZs, n)
  for i := range data {
    if i == 0 {
      data[i].X = rand.Float64()
    } else {
      data[i].X = data[i-1].X + 2*rand.Float64()
    }
    data[i].Y = data[i].X + 10*rand.Float64()
    data[i].Z = data[i].X
  }

  return data
}

咱們生成一組三維座標點,調用plotter.NewScatter()生成散點圖。咱們設置了GlyphStyleFunc鉤子函數,在繪製每一個點以前都會調用它,它返回一個draw.GlyphStyle類型,plot會根據返回的這個對象來繪製。咱們的例子中,每次咱們都返回一個表示圓形的draw.GlyphStyle對象,經過Z座標與最大、最小座標的比例映射到[vg.Points(1)vg.Points(20)]區間中獲得半徑。

生成的圖像:

一樣地,咱們能夠返回正方形的draw.GlyphStyle的對象來繪製「方形圖」,只須要把鉤子函數GlyphStyleFunc的返回語句作些修改:

return draw.GlyphStyle{Color: c, Radius: r, Shape: draw.SquareGlyph{}}

便可繪製「方形圖」😄:

實際應用

下面咱們應用以前文章中介紹的gopsutil和本文中的plot搭建一個網頁,能夠實時觀察機器的 CPU 和內存佔用:

func index(w http.ResponseWriter, r *http.Request) {
  t, err := template.ParseFiles("index.html")
  if err != nil {
    log.Fatal(err)
  }

  t.Execute(w, nil)
}

func image(w http.ResponseWriter, r *http.Request) {
  monitor.WriteTo(w)
}

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", index)
  mux.HandleFunc("/image", image)

  go monitor.Run()

  s := &http.Server{
    Addr:    ":8080",
    Handler: mux,
  }
  if err := s.ListenAndServe(); err != nil {
    log.Fatal(err)
  }
}

首先,咱們編寫了一個 HTTP 服務器,監聽在 8080 端口。設置兩個路由,/顯示主頁,/image調用Monitor的方法生成 CPU 和內存佔用圖返回。Monitor結構稍後會介紹。index.html的內容以下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Monitor</title>
</head>
<body>
  <img src="/image" alt="" id="img">
  <script>
    let img = document.querySelector("#img")
    setInterval(function () {
      img.src = "/image?s=" + Math.random()
    }, 500)
  </script>
</body>
</html>

頁面比較簡單,就顯示了一張圖片。而後在 JS 中啓動一個 500ms 的定時器,每隔 500ms 就從新請求一次圖片替換現有的圖片。我在設置img.src屬性時在後面添加了一個隨機數,這是爲了防止緩存致使獲得的可能不是最新的圖片。

下面看看Monitor的結構:

type Monitor struct {
  Mem       []float64
  CPU       []float64
  MaxRecord int
  Lock      sync.Mutex
}

func NewMonitor(max int) *Monitor {
  return &Monitor{
    MaxRecord: max,
  }
}

var monitor = NewMonitor(50)

這個結構中記錄了最近的 50 條記錄。每隔 500ms 會收集一次 CPU 和內存的佔用狀況,記錄到CPUMem字段中:

func (m *Monitor) Collect() {
  mem, err := mem.VirtualMemory()
  if err != nil {
    log.Fatal(err)
  }

  cpu, err := cpu.Percent(500*time.Millisecond, false)
  if err != nil {
    log.Fatal(err)
  }

  m.Lock.Lock()
  defer m.Lock.Unlock()

  m.Mem = append(m.Mem, mem.UsedPercent)
  m.CPU = append(m.CPU, cpu[0])
}

func (m *Monitor) Run() {
  for {
    m.Collect()
    time.Sleep(500 * time.Millisecond)
  }
}

當 HTTP 請求/image路由時,根據目前已經收集到的CPUMem數據生成圖片返回:

func (m *Monitor) WriteTo(w io.Writer) {
  m.Lock.Lock()
  defer m.Lock.Unlock()

  cpuData := make(plotter.XYs, len(m.CPU))
  for i, p := range m.CPU {
    cpuData[i].X = float64(i + 1)
    cpuData[i].Y = p
  }

  memData := make(plotter.XYs, len(m.Mem))
  for i, p := range m.Mem {
    memData[i].X = float64(i + 1)
    memData[i].Y = p
  }

  p, err := plot.New()
  if err != nil {
    log.Fatal(err)
  }

  cpuLine, err := plotter.NewLine(cpuData)
  if err != nil {
    log.Fatal(err)
  }
  cpuLine.Color = plotutil.Color(1)

  memLine, err := plotter.NewLine(memData)
  if err != nil {
    log.Fatal(err)
  }
  memLine.Color = plotutil.Color(2)

  p.Add(cpuLine, memLine)

  p.Legend.Add("cpu", cpuLine)
  p.Legend.Add("mem", memLine)

  p.X.Min = 0
  p.X.Max = float64(m.MaxRecord)
  p.Y.Min = 0
  p.Y.Max = 100

  wc, err := p.WriterTo(4*vg.Inch, 4*vg.Inch, "png")
  if err != nil {
    log.Fatal(err)
  }
  wc.WriteTo(w)
}

運行服務器:

$ go run main.go

打開瀏覽器,輸入localhost:8080,觀察圖片變化:

總結

本文介紹了強大的繪圖庫plot,最後經過一個監控程序結尾。限於篇幅,plot提供的多種繪圖類型未能一一介紹。plot還支持svg/pdf等多種格式的保存。感興趣的童鞋可自行研究。

你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄

參考

  1. plot GitHub:https://github.com/gonum/plot
  2. Example Plots: https://github.com/gonum/plot/wiki/Example-plots
  3. Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib

個人博客:https://darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索