go-chart实现折线图/饼图/柱状图绘制

需求

邮件中实现统计信息的发送。统计信息以折线图、饼图、柱状图的形式展示。因为邮件中无法支持js解析,所以采用go-chart生成PNG图片后base64编码后发送。

<img src="https://img-blog.csdnimg.cn/2022010623010187796.png">

go-chart实战

go-chart是个强大的go生成图片的库,但是采用默认配置生成线条较多或者文本过长的图片时,无法完美适应。默认不支持中文。但是里面的属性大多可自己定义,整体来说非常强大。

代码

package charter

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"math/rand"
	"os"
	"time"

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

const (
	lineChartXAxisName  = "Date"
	lineChartYAxisName  = "Count"
	lineChartHeight     = 700
	lineChartWidth      = 1280
	colorMultiplier     = 256
	imgStrPrefix        = "data:image/png;base64,"
	pieLabelFormat      = "%v %v"
	barChartTryAgainErr = "invalid data range; cannot be zero"
)

var (
	lineChartStyle = chart.Style{
		Padding: chart.Box{
			Top:  30,
			Left: 150,
		},
	}

	defaultChartStyle = chart.Style{
		Padding: chart.Box{
			Top: 30,
		},
	}

	timeFormat = chart.TimeDateValueFormatter
)

type LineYValue struct {
	Name   string
	Values []float64
}

type ChartValue struct {
	Name  string
	Value float64
}

// createLineChart 创建线性图
func createLineChart(title string, endTime time.Time, values []LineYValue) (img string, err error) {
	if len(values) == 0 {
		return
	}
	// 1、计算X轴
	lenX := len(values[0].Values)
	// X轴内容xValues 及 X轴坐标ticks
	var xValues []time.Time
	var ticks []chart.Tick
	for i := lenX - 1; i >= 0; i-- {
		curTime := endTime.AddDate(0, 0, -i)
		xValues = append(xValues, curTime)
		ticks = append(ticks, chart.Tick{Value: getNsec(curTime), Label: timeFormat(curTime)})
	}

	// 2、生成Series
	var series []chart.Series
	for _, yValue := range values {
		series = append(series, chart.TimeSeries{
			Name: yValue.Name,
			Style: chart.Style{
				// 随机渲染线条颜色
				StrokeColor: drawing.Color{
					R: uint8(rand.Intn(colorMultiplier)),
					G: uint8(rand.Intn(colorMultiplier)),
					B: uint8(rand.Intn(colorMultiplier)),
					A: uint8(colorMultiplier - 1), // 透明度
				},
			},
			XValues: xValues,
			YValues: yValue.Values,
		})
	}

	// 3、新建图形
	graph := chart.Chart{
		Title:      title,
		Background: lineChartStyle,
		Width:      lineChartWidth,
		Height:     lineChartHeight,
		XAxis: chart.XAxis{
			Name:           lineChartXAxisName,
			ValueFormatter: timeFormat,
			Ticks:          ticks,
		},
		YAxis: chart.YAxis{
			Name: lineChartYAxisName,
		},
		Series: series,
	}
	graph.Elements = []chart.Renderable{
		chart.LegendLeft(&graph),
	}

	// 4、输出目标
	img, err = writeLineChart(&graph)

	return
}

// getNsec 获取纳秒数
func getNsec(cur time.Time) float64 {
	return float64(cur.Unix() * int64(time.Second))
}

func writeLineChartToPng(c *chart.Chart) (img string, err error) {
	f, _ := os.Create("graph.png")
	err = c.Render(chart.PNG, f)
	return
}

func writeLineChart(c *chart.Chart) (img string, err error) {
	var imgContent bytes.Buffer
	err = c.Render(chart.PNG, &imgContent)
	if err != nil {
		return
	}

	img = imgToStr(imgContent)
	return
}

func imgToStr(imgContent bytes.Buffer) string {
	return imgStrPrefix + base64.StdEncoding.EncodeToString(imgContent.Bytes())
}

// createPieChart 创建饼图
func createPieChart(title string, pieValues []ChartValue) (img string, err error) {
	if len(pieValues) == 0 {
		return
	}
	// 1、构建value
	var values []chart.Value
	for _, v := range pieValues {

		values = append(values, chart.Value{
			Value: v.Value,
			Label: fmt.Sprintf(pieLabelFormat, getSimpleSensType(v.Name), formatValue(v.Value)),
		})
	}

	// 2、新建饼图
	pie := chart.PieChart{
		Title:      title,
		Background: defaultChartStyle,
		Values:     values,
	}

	// 4、输出目标
	img, err = writePieChart(&pie)

	return
}

func formatValue(f float64) string {
	return fmt.Sprintf("%.2fW", f/10000)
}

func getSimpleSensType(name string) string {
	if name == "个人数据" {
	    return "Personal"
    }
	return "Other"
}

func writePieChartToPng(c *chart.PieChart) (img string, err error) {
	f, _ := os.Create("pie.png")
	err = c.Render(chart.PNG, f)
	return
}

func writePieChart(c *chart.PieChart) (img string, err error) {
	var imgContent bytes.Buffer
	err = c.Render(chart.PNG, &imgContent)
	if err != nil {
		return
	}

	img = imgToStr(imgContent)
	return
}

// createBarChart 创建柱状图
func createBarChart(title string, barValues []ChartValue) (img string, err error) {
	if len(barValues) == 0 {
		return
	}
	// 1、构建value
	var values []chart.Value
	for _, v := range barValues {
		values = append(values, chart.Value{
			Value: v.Value,
			Label: v.Name,
		})
	}

	// 2、新建饼图
	bar := chart.BarChart{
		XAxis: chart.Style{
			TextWrap: 0, // default 1为可以溢出规定的范围
		},
		Width:      2560,
		BarWidth:   50,
		BarSpacing: 300,
		Title:      title,
		Background: defaultChartStyle,
		Bars:       values,
	}

	// 4、输出目标
	img, err = writeBarChart(&bar)
	if err != nil && err.Error() == barChartTryAgainErr {
		// 添加一个隐藏条目,设置透明度A为0, 设置任意属性如R不为0即可
		values = append(values, chart.Value{
			Style: chart.Style{
				StrokeColor: drawing.Color{R: 1},
			},
			Value: 0,
			Label: "",
		})
		bar.Bars = values
		img, err = writeBarChart(&bar)
	}

	return
}

func writeBarChartToPng(c *chart.BarChart) (img string, err error) {
	f, _ := os.Create("bar.png")
	err = c.Render(chart.PNG, f)
	return
}

func writeBarChart(c *chart.BarChart) (img string, err error) {
	var imgContent bytes.Buffer
	err = c.Render(chart.PNG, &imgContent)
	if err != nil {
		return
	}

	img = imgToStr(imgContent)
	return
}


测试代码和生成图片效果

修改以上writeXxxChart为writeXxxChartToPng后,运行以下测试代码

package charter

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
)

func TestCreateLineChart(t *testing.T) {
	testAssert := assert.New(t)

	tests := []struct {
		title     string
		endTime   time.Time
		barValues []LineYValue
	}{
		{"line chart", time.Now(), []LineYValue{
			{"asd", []float64{1, 2, 300, 100, 200, 6, 700}},
			{"hgj", []float64{400, 500000, 200, 50, 5, 800, 7}},
			{"dfg45r", []float64{1, 2, 700, 100, 200, 6, 700}},
			{"2342sr", []float64{400, 500000, 200, 50, 5, 800, 7}},
			{"das21-asd", []float64{300000, 200000, 400000, 100000, 400000, 450000, 400000}},
			{"csc", []float64{400, 500000, 200, 50, 5, 800, 7}},
			{"mhj", []float64{1, 2, 300, 100, 200, 6, 700}},
			{"876ijgh", []float64{400, 500000, 200, 50, 5, 800, 7}},
			{"fbfdv", []float64{1, 2, 300, 100, 200, 6, 700}},
			{"67ds", []float64{400, 10000, 200, 50, 5, 800, 7}},
			{"67bdfv", []float64{1, 2, 300, 100, 200, 6, 700}},
			{"sdf324", []float64{400, 500000, 200, 50, 5, 800, 7}},
			{"vdf67", []float64{1, 2, 300, 100, 200, 6, 700}},
			{"vdfs234", []float64{400, 500000, 200, 50, 5, 800, 7}},
			{"123sdf", []float64{1, 2, 700, 100, 200, 6, 700}},
			{"aasdasd", []float64{400, 500000, 200, 50, 5, 800, 7}},
			{"aasd", []float64{1, 2, 300, 100, 200, 6, 700}},
			{"basd", []float64{400, 500000, 200, 50, 5, 800, 7}},
			{"cczx", []float64{1, 2, 300, 100, 200, 6, 700}},
			{"qweqw", []float64{400, 500000, 200, 50, 5, 800, 7}},
			{"asdadf", []float64{1, 2, 300, 100, 200, 6, 700}},
			{"fghfh", []float64{400, 500000, 200, 50, 5, 800, 7}},
			{"erttyrt", []float64{1, 2, 300, 100, 200, 6, 700}}}},
	}

	for _, test := range tests {
		img, err := createLineChart(test.title, test.endTime, test.barValues)
		testAssert.Equal(img, "")
		testAssert.Equal(err, nil)
	}
}

func TestCreatePieChart(t *testing.T) {
	testAssert := assert.New(t)

	tests := []struct {
		title     string
		pieValues []ChartValue
	}{
		{"pie chart", []ChartValue{{"asdas", 20000}, {"q12asd", 300000}, {"ascasd", 3000}}},
	}

	for _, test := range tests {
		img, err := createPieChart(test.title, test.pieValues)
		testAssert.Equal(img, "")
		testAssert.Equal(err, nil)
	}
}

func TestCreateBarChart(t *testing.T) {
	testAssert := assert.New(t)

	tests := []struct {
		title     string
		pieValues []ChartValue
	}{
		{"bar chart", []ChartValue{{"asdascasd\nasd-asd", 20}, {"asdascascasdasdasd.go\nasdasd-asdasd", 30}, {"asasdasd.asdasd]\nasdasd-asda", 100},
			{"asdasdasda.go\nasdasd-asdasd", 20}, {"asdasd.asdasd\ngeass", 30}, {"asdasdasd\nasdasd-asdasd", 100},
			{"asdasd_adsdasd_dasd\asd-asd", 20}, {"asdascasdcad\nasdasdasda", 30}, {"asdasdasdasd", 100},
			{"asdasclkhy9p867p9", 20}}},
	}

	for _, test := range tests {
		img, err := createBarChart(test.title, test.pieValues)
		testAssert.Equal(img, "")
		testAssert.Equal(err, nil)
	}
}

graph.png
在这里插入图片描述
在这里插入图片描述

相关:
https://github.com/wcharczuk/go-chart/issues/62
go-chart使用中文
go-echarts: html格式图表

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Pokémon Go Pokemon Go.png Developer(s) Niantic The Pokémon Company Publisher(s) Niantic Director(s) Tatsuo Nomura Artist(s) Dennis Hwang Composer(s) Junichi Masuda Series Pokémon Engine Unity Platform(s) iOS, Android Release July 6, 2016[show] Genre(s) Augmented reality, location-based game Pokémon Go is an augmented reality (AR) mobile game developed and published by Niantic for iOS and Android devices. A part of the Pokémon franchise, it was first released in certain countries in July 2016, and in other regions over the next few months. The game is the result of a collaboration between Niantic and Nintendo by way of The Pokémon Company. It uses the mobile device GPS to locate, capture, battle, and train virtual creatures, called Pokémon, which appear as if they are in the player's real-world location. The game is free to play; it uses a freemium business model and supports in-app purchases for additional in-game items. The game launched with around 150 species of Pokémon, which had increased to over 420 by late 2018. Pokémon Go was released to mixed reviews; critics praised the concept, but criticized technical problems. It was one of the most used and profitable mobile apps in 2016, having been downloaded more than 500 million times worldwide by the end of the year. It is credited with popularizing location-based and AR technology, promoting physical activity, and helping local businesses grow due to increased foot traffic. However, it attracted controversy for contributing to accidents and creating public nuisances. Various governments expressed concerns about security, and some countries regulate its use. In May 2018, The Pokémon Company announced that the game had received over 800 million downloads worldwide,[1] and it has 147 million monthly active users as of May 2018.[2] As of September 2018, the game has grossed $2.01 billion worldwide.[3]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值