需求

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

<img src="data:image/png;base64,xxx">

1

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 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)

}

}