导航

使用 github.com/wcharczuk/go-chart 绘图

Posted on 2022-11-07 16:28  蝈蝈俊  阅读(273)  评论(0编辑  收藏  举报

公共绘图函数


package charts

import (
	"bytes"
	"log"
	"os"

	chart "github.com/wcharczuk/go-chart/v2"
	drawing "github.com/wcharczuk/go-chart/v2/drawing"

	"golang.org/x/text/message"
)

// DrawChart3AndByteArr 即生成文件,也返回文件流, 邮件附件用
func DrawChart3AndByteArr(infoArr []string, seriesArr []chart.Series, fileName string) []byte {
	ba := DrawChart2ByteArr3(infoArr, seriesArr)
	log.Println(fileName)

	fo, err := os.Create(fileName)
	if err != nil {
		panic(err)
	}

	if _, err := fo.Write(ba); err != nil {
		panic(err)
	}

	return ba
}

// DrawChart2ByteArr3 多条曲线的绘制, 把这个走势图转换成 byte 数据
// X 轴 是同一个时间的多个曲线对比
func DrawChart2ByteArr3(infoArr []string, seriesArr []chart.Series) []byte {
	// log.Println(infoArr)
	pp := message.NewPrinter(message.MatchLanguage("en")) // https://godoc.org/golang.org/x/text/message

	graph := chart.Chart{
		Background: chart.Style{
			Padding: chart.Box{
				Top:  20,
				Left: 20,
			},
		},
		Font: GetZWFont(),
		XAxis: chart.XAxis{
			Name: "时间:分钟",
			NameStyle: chart.Style{
				//		Show:      true,
				FontColor: drawing.Color{R: 255, G: 0, B: 0, A: 255},
			},
			//			Style:          chart.StyleShow(),
			// ValueFormatter: chart.TimeValueFormatterWithFormat("2006-01-02 15:04"),
			// 2020-10-22 原先太占空间,只显示时间,不显示日期
			ValueFormatter: chart.TimeValueFormatterWithFormat("15:04"),
		},
		YAxis: chart.YAxis{
			// Name:      "QPS",
			// NameStyle: chart.StyleShow(),
			//			Style: chart.StyleShow(),
			ValueFormatter: func(v interface{}) string {
				if vf, isFloat := v.(float64); isFloat {
					// log.Println(vf)
					return pp.Sprintf("%0.f", vf)
				}
				// log.Printf("不是 float")
				// log.Println(v)
				return ""
			},
		},
		Series: seriesArr,
	}

	if len(infoArr) > 0 {
		//note we have to do this as a separate step because we need a reference to graph
		graph.Elements = []chart.Renderable{
			TextAndLineInfo(infoArr, &graph),
			// TextInfo(infoArr),
			// chart.LegendLeft(&graph),
		}
	}

	// log.Println("334")
	// log.Println(seriesArr)

	buffer := bytes.NewBuffer([]byte{})
	err := graph.Render(chart.PNG, buffer)
	if err != nil {
		log.Fatal(err)
	}

	return buffer.Bytes()

}

// TextAndLineInfo 图中绘制文字和线条说明
func TextAndLineInfo(txtArr []string, c *chart.Chart) chart.Renderable {

	var labels []string
	var lines []chart.Style
	for index, s := range c.Series {
		style := s.GetStyle()
		if !style.Hidden {
			if _, isAnnotationSeries := s.(chart.AnnotationSeries); !isAnnotationSeries {
				labels = append(labels, s.GetName())
				lines = append(lines, s.GetStyle().InheritFrom(getStyleDefaultsSeries(c, index)))
			}
		}
	}

	return func(r chart.Renderer, cb chart.Box, chartDefaults chart.Style) {
		// log.Println(fmt.Sprintf("h:%d; w:%d", cb.Height(), cb.Width()))
		// log.Println(chartDefaults)
		// log.Println(cb)

		r.SetFont(GetZWFont())
		r.SetFontColor(drawing.ColorBlue)
		r.SetFontSize(14)
		i := 0
		for _, txt := range txtArr {
			if len(txt) > 0 {
				r.Text(txt, 30+i*10, 40+i*30)
				i++
			}
		}

		tx := 30 + i*10
		for x := 0; x < len(labels); x++ {
			label := labels[x]
			if len(label) > 0 {
				// 写文字
				ty := 40 + i*30
				r.Text(label, tx, ty)

				// 绘线条
				tb := r.MeasureText(label)
				lx := tx + tb.Width() + 5
				ly := ty - 6

				r.SetStrokeColor(lines[x].GetStrokeColor())
				r.SetStrokeWidth(lines[x].GetStrokeWidth())
				r.SetStrokeDashArray(lines[x].GetStrokeDashArray())
				r.MoveTo(lx, ly)
				r.LineTo(lx+30, ly)
				r.Stroke()

				i++
			}
		}
		// log.Println("12444")
	}
}

// getStyleDefaultsSeries 获得默认信息
func getStyleDefaultsSeries(c *chart.Chart, seriesIndex int) chart.Style {
	return chart.Style{
		DotColor:    c.GetColorPalette().GetSeriesColor(seriesIndex),
		StrokeColor: c.GetColorPalette().GetSeriesColor(seriesIndex),
		StrokeWidth: chart.DefaultSeriesLineWidth,
		Font:        c.GetFont(),
		FontSize:    chart.DefaultFontSize,
	}
}

加载字体


package charts

import (
	"io/ioutil"
	"log"

	"github.com/golang/freetype/truetype"
)

const (
	fontFile = "/Library/Fonts/Microsoft-YaHei.ttf" // 需要使用的字体文件
)

// GetZWFont 画图跟字体有关的峰值
// 参考 https://www.cnblogs.com/ghj1976/p/3445568.html
// https://github.com/wcharczuk/go-chart/tree/master/_examples/text_rotation
func GetZWFont() *truetype.Font {
	// 读字体数据
	fontBytes, err := ioutil.ReadFile(fontFile)
	if err != nil {
		log.Println(err)
		return nil
	}
	font, err := truetype.Parse(fontBytes)
	if err != nil {
		log.Println(err)
		return nil
	}
	return font
}

绘图


package report

import (
	"fmt"
	"log"
	"os"
	"analysis"
	"charts"
	"tools"
	"time"

	"github.com/wcharczuk/go-chart/v2"
	"github.com/wcharczuk/go-chart/v2/drawing"
)

// 生成趋势图
func BuildTrendChart(cad *analysis.CacheAppkeyData) {
	// QPM
	buildTrendChartTransaction("", cad.Appkey, "qpm", cad.BeginTime, cad.EndTime, cad.TransactionQPMMap, cad.TransactionQPMpTestMap)
	log.Println("qpm")
	// AVG
	buildTrendChartTransaction("", cad.Appkey, "avg", cad.BeginTime, cad.EndTime, cad.TransactionAvgMap, cad.TransactionAvgpTestMap)
	log.Println("avg")
	// Error
	buildTrendChartTransaction("", cad.Appkey, "error", cad.BeginTime, cad.EndTime, cad.TransactionErrorMap, cad.TransactionErrorpTestMap)
	log.Println("error")

	buildTrendChartThroughput(cad.Appkey, cad.BeginTime, cad.EndTime, cad.TransactionQPMMap, cad.TransactionQPMpTestMap, cad.TransactionAvgMap, cad.TransactionAvgpTestMap)
	log.Println("throughput")

	buildTrendChartHosts("", cad.Appkey, "cpu.busy", cad.BeginTime, cad.EndTime, cad.HostsCPUBusyMap)
	log.Println("cpu.busy")
	buildTrendChartHosts("", cad.Appkey, "jvm.fullgc.count", cad.BeginTime, cad.EndTime, cad.HostsJvmFullgcCountMap)
	log.Println("jvm.fullgc.count")
	buildTrendChartHosts("", cad.Appkey, "jvm.thread.blocked.count", cad.BeginTime, cad.EndTime, cad.HostsJvmThreadBlockedCountMap)
	log.Println("jvm.thread.blocked.count")
	buildTrendChartHosts("", cad.Appkey, "mem.memused.percent", cad.BeginTime, cad.EndTime, cad.HostsMemMemusedPercentMap)
	log.Println("mem.memused.percent")
	buildTrendChartHosts("", cad.Appkey, "mem.swapused.percent", cad.BeginTime, cad.EndTime, cad.HostsMemSwapusedPercentMap)
	log.Println("mem.swapused.percent")

	// 容量 散点图
	BuildCapacityScatterPlot1(cad)
}

// 生成一个服务的多台机器的某个容器指标走势图
func buildTrendChartHosts(showtype, octokey, datatype string, begin, end time.Time, tMap map[string]map[time.Time]float64) {
	xvArr := [][]time.Time{}
	yvArr := [][]float64{}
	nameArr := []string{}
	hasData := false
	for hulkName, dmap := range tMap {
		nameArr = append(nameArr, hulkName)
		xv := []time.Time{}
		yv := []float64{}
		for curr := begin; !curr.After(end); curr = curr.Add(time.Minute) {
			v1, ex1 := dmap[curr]
			if ex1 {
				xv = append(xv, curr)
				yv = append(yv, v1)
				hasData = true
			}
		}
		xvArr = append(xvArr, xv)
		yvArr = append(yvArr, yv)
	}

	if hasData {
		centerTime, err := tools.GetCenterTime(begin, end)
		if err != nil {
			log.Println(err)
		}
		path := pathCheck(centerTime)

		fileName := fmt.Sprintf("%s/%s-%s-%s.png", path, octokey, datatype, centerTime.Format("20060102"))
		// 生成图片

		BuildIMGFile2(showtype, fileName, true, false, []string{fmt.Sprintf("%s-%s", octokey, datatype)}, nameArr, xvArr, yvArr...)

	}
}

// 生成 昨天、昨天压测、前天、上周同期,上月同期 这样几条走势线图
func buildTrendChartTransaction(showtype, octokey, datatype string, begin, end time.Time, tMap, tpTestMap map[time.Time]float64) {

	centerTime, err := tools.GetCenterTime(begin, end)
	if err != nil {
		log.Println(err)
	}

	hasData := false

	xv1 := []time.Time{}
	yv1 := []float64{}
	xvpTest := []time.Time{}
	yvpTest := []float64{}
	xvYesterday := []time.Time{}
	yvYesterday := []float64{}
	xvWeek := []time.Time{}
	yvWeek := []float64{}
	xvMonth := []time.Time{}
	yvMonth := []float64{}

	for curr := begin; !curr.After(end); curr = curr.Add(time.Minute) {
		v1, ex1 := tMap[curr]
		if ex1 {
			xv1 = append(xv1, curr)
			yv1 = append(yv1, v1)
			hasData = true
		} else {
			// 20211101 为了避免只有一个点问题, 如果没有值,开始和结束点的值设置为0
			if curr == begin {
				xv1 = append(xv1, curr)
				yv1 = append(yv1, 0.0)
			} else if curr == end {
				xv1 = append(xv1, curr)
				yv1 = append(yv1, 0.0)
			}
		}

		v2, ex2 := tpTestMap[curr]
		if ex2 {
			xvpTest = append(xvpTest, curr)
			yvpTest = append(yvpTest, v2)
			hasData = true
		}

		v3, ex3 := tMap[curr.AddDate(0, 0, -1)]
		if ex3 {
			xvYesterday = append(xvYesterday, curr)
			yvYesterday = append(yvYesterday, v3)
			hasData = true
		}

		v4, ex4 := tMap[curr.AddDate(0, 0, -7)]
		if ex4 {
			xvWeek = append(xvWeek, curr)
			yvWeek = append(yvWeek, v4)
			hasData = true
		}
		v5, ex5 := tMap[curr.AddDate(0, -1, 0)]
		if ex5 {
			xvMonth = append(xvMonth, curr)
			yvMonth = append(yvMonth, v5)
			hasData = true
		}
	}

	nameArr := []string{}
	xvArr := [][]time.Time{}
	yvArr := [][]float64{}
	if len(xv1) > 0 {
		nameArr = append(nameArr, fmt.Sprintf("昨日%s", centerTime.Format("20060102")))
		xvArr = append(xvArr, xv1)
		yvArr = append(yvArr, yv1)
	}
	if len(xvpTest) > 0 {
		nameArr = append(nameArr, fmt.Sprintf("昨日压测%s", centerTime.Format("20060102")))
		xvArr = append(xvArr, xvpTest)
		yvArr = append(yvArr, yvpTest)
	}
	if len(xvYesterday) > 0 {
		nameArr = append(nameArr, fmt.Sprintf("前日%s", centerTime.AddDate(0, 0, -1).Format("20060102")))
		xvArr = append(xvArr, xvYesterday)
		yvArr = append(yvArr, yvYesterday)
	}
	if len(xvWeek) > 0 {
		nameArr = append(nameArr, fmt.Sprintf("上周同期%s", centerTime.AddDate(0, 0, -7).Format("20060102")))
		xvArr = append(xvArr, xvWeek)
		yvArr = append(yvArr, yvWeek)
	}
	if len(xvMonth) > 0 {
		nameArr = append(nameArr, fmt.Sprintf("上月同期%s", centerTime.AddDate(0, -1, 0).Format("20060102")))
		xvArr = append(xvArr, xvMonth)
		yvArr = append(yvArr, yvMonth)
	}

	if hasData {

		path := pathCheck(centerTime)

		fileName := fmt.Sprintf("%s/%s-%s-%s.png", path, octokey, datatype, centerTime.Format("20060102"))
		// 生成图片
		BuildIMGFile2(showtype, fileName, true, true, []string{fmt.Sprintf("%s-%s", octokey, datatype)}, nameArr, xvArr, yvArr...)

	}
}

// 生成吞吐量走势图
func buildTrendChartThroughput(octokey string, begin, end time.Time, qpmMap, qpmpTestMap, avgMap, avgpTestMap map[time.Time]float64) {

	centerTime, err := tools.GetCenterTime(begin, end)
	if err != nil {
		log.Println(err)
	}

	hasData := false

	xv1 := []time.Time{}
	yv1 := []float64{}
	xvYesterday := []time.Time{}
	yvYesterday := []float64{}
	xvWeek := []time.Time{}
	yvWeek := []float64{}
	xvMonth := []time.Time{}
	yvMonth := []float64{}

	for curr := begin; !curr.After(end); curr = curr.Add(time.Minute) {

		// 昨日
		v1, ex1 := qpmMap[curr]
		vt1, ext1 := avgMap[curr]
		if ex1 && ext1 {
			xv1 = append(xv1, curr)
			vp1, exp1 := qpmpTestMap[curr]
			vtp1, extp1 := avgpTestMap[curr]
			if exp1 && extp1 {
				yv1 = append(yv1, v1*vt1+vp1*vtp1)
			} else {
				yv1 = append(yv1, v1*vt1)
			}
			hasData = true
		}

		// 前日
		v3, ex3 := qpmMap[curr.AddDate(0, 0, -1)]
		vt3, ext3 := avgMap[curr.AddDate(0, 0, -1)]
		if ex3 && ext3 {
			xvYesterday = append(xvYesterday, curr)
			yvYesterday = append(yvYesterday, v3*vt3)
			hasData = true
		}

		// 上周同期
		v4, ex4 := qpmMap[curr.AddDate(0, 0, -7)]
		vt4, ext4 := avgMap[curr.AddDate(0, 0, -7)]
		if ex4 && ext4 {
			xvWeek = append(xvWeek, curr)
			yvWeek = append(yvWeek, v4*vt4)
			hasData = true
		}

		// 上月同期
		v5, ex5 := qpmMap[curr.AddDate(0, -1, 0)]
		vt5, ext5 := avgMap[curr.AddDate(0, -1, 0)]
		if ex5 && ext5 {
			xvMonth = append(xvMonth, curr)
			yvMonth = append(yvMonth, v5*vt5)
			hasData = true
		}
	}

	nameArr := []string{}
	xvArr := [][]time.Time{}
	yvArr := [][]float64{}
	if len(xv1) > 0 {
		nameArr = append(nameArr, fmt.Sprintf("昨日%s吞吐量", centerTime.Format("20060102")))
		xvArr = append(xvArr, xv1)
		yvArr = append(yvArr, yv1)
	}

	if len(xvYesterday) > 0 {
		nameArr = append(nameArr, fmt.Sprintf("前日%s吞吐量", centerTime.AddDate(0, 0, -1).Format("20060102")))
		xvArr = append(xvArr, xvYesterday)
		yvArr = append(yvArr, yvYesterday)
	}
	if len(xvWeek) > 0 {
		nameArr = append(nameArr, fmt.Sprintf("上周同期%s吞吐量", centerTime.AddDate(0, 0, -7).Format("20060102")))
		xvArr = append(xvArr, xvWeek)
		yvArr = append(yvArr, yvWeek)
	}
	if len(xvMonth) > 0 {
		nameArr = append(nameArr, fmt.Sprintf("上月同期%s吞吐量", centerTime.AddDate(0, -1, 0).Format("20060102")))
		xvArr = append(xvArr, xvMonth)
		yvArr = append(yvArr, yvMonth)
	}

	if hasData {

		path := pathCheck(centerTime)

		fileName := fmt.Sprintf("%s/%s-%s-%s.png", path, octokey, "throughput", centerTime.Format("20060102"))
		// 生成图片
		BuildIMGFile2("dot", fileName, true, true, []string{fmt.Sprintf("%s-%s", octokey, "throughput")}, nameArr, xvArr, yvArr...)

	}
}

// 生成包含多条走势线的图
func BuildIMGFile2(showtype, filename string, enforce, showName bool, lbInfoArr, nameArr []string, xv [][]time.Time, yv ...[]float64) {
	_, err1 := os.ReadFile(filename)

	if !enforce && err1 == nil {
		// 非强制, 且有文件, 直接返回,不用再生成一次了
		log.Printf("不需要修改走势图%s\r", filename)
		log.Println(err1)
		return
	}

	// 流量走势图
	sArr := []chart.Series{}
	for i, name := range nameArr {
		if showtype == "dot" { // 20211031 全部用点来表示
			if showName {
				sArr = append(sArr, chart.TimeSeries{
					Name: name,
					Style: chart.Style{
						StrokeColor: getColor(i),
						StrokeWidth: chart.Disabled,
						DotWidth:    1,
					},
					XValues: xv[i], // 20211026 每一组x坐标都不一样,用于丢数据的场景
					YValues: yv[i], // i 是从 0 开始的
				})
			} else {
				sArr = append(sArr, chart.TimeSeries{
					Style: chart.Style{
						StrokeColor: getColor(i),
						StrokeWidth: chart.Disabled,
						DotWidth:    1,
					},
					XValues: xv[i], // 20211026 每一组x坐标都不一样,用于丢数据的场景
					YValues: yv[i], // i 是从 0 开始的
				})
			}

		} else { // 默认用线来表示
			if showName {

				sArr = append(sArr, chart.TimeSeries{
					Name: name,
					Style: chart.Style{
						StrokeColor: getColor(i),
					},
					XValues: xv[i], // 20211026 每一组x坐标都不一样,用于丢数据的场景
					YValues: yv[i], // i 是从 0 开始的
				})
			} else {
				sArr = append(sArr, chart.TimeSeries{
					Style: chart.Style{
						StrokeColor: getColor(i),
					},
					XValues: xv[i], // 20211026 每一组x坐标都不一样,用于丢数据的场景
					YValues: yv[i], // i 是从 0 开始的
				})
			}
		}
	}
	if len(nameArr) > 0 {
		log.Printf("开始绘图 %s\r\n", filename)
		charts.DrawChart3AndByteArr(lbInfoArr, sArr, filename)
	}
}

// 检查,并生成某天目录
func pathCheck(ct time.Time) string {
	// 产生保存目录 按照月产生对应目录,便于定期清理
	tools.PathCreate(fmt.Sprintf("./data/%s", ct.Format("200601")))
	tools.PathCreate(fmt.Sprintf("./data/%s/imgs", ct.Format("200601")))
	tools.PathCreate(fmt.Sprintf("./data/%s/imgs/%s", ct.Format("200601"), ct.Format("20060102")))
	tools.PathCreate(fmt.Sprintf("./data/%s/imgs/%s/day", ct.Format("200601"), ct.Format("20060102")))
	return fmt.Sprintf("./data/%s/imgs/%s/day", ct.Format("200601"), ct.Format("20060102"))
}

func getColor(i int) drawing.Color {
	switch i % 5 {
	case 0:
		return drawing.Color{R: 0, G: 0, B: 255, A: 255}
	case 1:
		return drawing.Color{R: 0, G: 255, B: 0, A: 255}
	case 2:
		return drawing.Color{R: 148, G: 0, B: 211, A: 255}
	case 3:
		return drawing.Color{R: 220, G: 20, B: 60, A: 255}
	case 4:
		return drawing.Color{R: 124, G: 205, B: 124, A: 255}
	default:
		return drawing.Color{R: 220, G: 160, B: 122, A: 255}
	}
}