innerServers 未配置时 GetRankPhoto/GetRankDetail 的跨服循环零次执行,
statList 为空仍继续渲染。go-charts 对空数据集会把坐标轴范围退化成
math.MinInt64 与 NaN,产出一张 Y 轴刻度为 -9,223,372,036,854,776,000、
X 轴为 NaN、图例全 0 的坏图,并照常推送到群里。
增加 ErrNoRankData 哨兵错误,两个函数在渲染前拦截空数据集。11 个调用点
原本就是 if err != nil { return err },无需改动即可全部覆盖。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
584 lines
17 KiB
Go
584 lines
17 KiB
Go
package crossSer
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"sort"
|
||
"stat/api/proto"
|
||
"stat/common"
|
||
"stat/common/statutils"
|
||
"stat/common/tgbot"
|
||
"stat/common/utils"
|
||
"stat/models/botdailyhourstat"
|
||
"stat/statg"
|
||
"time"
|
||
|
||
"github.com/wcharczuk/go-chart/v2"
|
||
|
||
"github.com/wcharczuk/go-chart/v2/drawing"
|
||
|
||
"github.com/vicanso/go-charts/v2"
|
||
)
|
||
|
||
// ErrNoRankData 排名数据为空。空数据集不能交给 go-charts 渲染: 坐标轴范围会退化成
|
||
// math.MinInt64 与 NaN,产出一张刻度为 -9,223,372,036,854,776,000 的坏图。
|
||
// 常见原因是 innerServers 未配置,跨服循环零次执行。
|
||
var ErrNoRankData = errors.New("排名数据为空,无法生成图表(请检查 innerServers 配置)")
|
||
|
||
var Colors = []drawing.Color{
|
||
drawing.ColorFromHex("0780cf"),
|
||
drawing.ColorFromHex("765005"),
|
||
drawing.ColorFromHex("fa6d1d"),
|
||
drawing.ColorFromHex("0e2c82"),
|
||
drawing.ColorFromHex("b6b51f"),
|
||
drawing.ColorFromHex("da1f18"),
|
||
drawing.ColorFromHex("701866"),
|
||
drawing.ColorFromHex("f47a75"),
|
||
drawing.ColorFromHex("009db2"),
|
||
drawing.ColorFromHex("024b51"),
|
||
drawing.ColorFromHex("0780cf"),
|
||
drawing.ColorFromHex("765005"),
|
||
}
|
||
|
||
type CrossStatMethod = func(ctx context.Context, cmd *tgbot.Command) error
|
||
|
||
// 统计方法汇总
|
||
var CrossStatMethodMap = map[string]CrossStatMethod{
|
||
statutils.StatProjectRecharge: CrossStatRecharge,
|
||
statutils.StatProjectRechargeNew: CrossStatRechargeNew,
|
||
statutils.StatProjectRechargeOld: CrossStatRechargeOld,
|
||
statutils.StatProjectRechargeGold: CrossStatRechargeGold,
|
||
statutils.StatProjectRechargeVip: CrossStatRechargeVip,
|
||
statutils.StatProjectRegister: CrossStatRegister,
|
||
statutils.StatProjectVisit: CrossStatVisit,
|
||
statutils.StatProjectOrder: CrossStatOrder,
|
||
}
|
||
|
||
type CrossAppDownloadRes struct {
|
||
Code int32 `json:"code"`
|
||
Msg string `json:"msg"`
|
||
Tip string `json:"tip"`
|
||
Data *proto.PageButtonClickResp `json:"data"`
|
||
Time time.Time `json:"time"`
|
||
}
|
||
|
||
type CrossStatRes struct {
|
||
Code int32 `json:"code"`
|
||
Msg string `json:"msg"`
|
||
Tip string `json:"tip"`
|
||
Data *proto.BotDailyHourListResp `json:"data"`
|
||
Time time.Time `json:"time"`
|
||
}
|
||
|
||
type CrossMonthRes struct {
|
||
Code int32 `json:"code"`
|
||
Msg string `json:"msg"`
|
||
Tip string `json:"tip"`
|
||
Data *proto.BotMonthStatResp `json:"data"`
|
||
Time time.Time `json:"time"`
|
||
}
|
||
|
||
type StatItem struct {
|
||
Value int64
|
||
Stat *botdailyhourstat.BotHourStat
|
||
}
|
||
|
||
type FuncGetStatValue func(item *botdailyhourstat.BotHourStat) int64
|
||
|
||
type StatItemList []*StatItem
|
||
|
||
func (a StatItemList) Len() int { return len(a) }
|
||
func (a StatItemList) Less(i, j int) bool { return a[i].Value > a[j].Value }
|
||
func (a StatItemList) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||
|
||
func CrossMonthStat(ctx context.Context, cmd *tgbot.Command) error {
|
||
appId, _ := cmd.GetFlagInt32Value("-app")
|
||
monthStr := cmd.GetFlagStringValue("-month")
|
||
month, err := time.ParseInLocation("2006-01-02", monthStr, time.Local)
|
||
if err != nil {
|
||
return tgbot.NewUserError(cmd, "解析日期出错, 格式:\n"+cmd.Help+"\n"+cmd.Desc)
|
||
}
|
||
getValue := func(item *botdailyhourstat.BotHourStat) int64 {
|
||
return item.TodayStat.Recharge.Value()
|
||
}
|
||
statList := make(StatItemList, 0)
|
||
for _, addr := range statg.Conf.InnerServers {
|
||
req := &proto.BotMonthStatReq{Month: month}
|
||
if appId > 0 {
|
||
req.AppId = &appId
|
||
}
|
||
res := CrossMonthRes{}
|
||
err = InnerRequest(addr+"/api/stat/inner/botMonthStat", req, &res, true)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if res.Data == nil {
|
||
continue
|
||
}
|
||
for _, stat := range res.Data.List {
|
||
if statg.GlobalEndpoints.ExistsByAppId(stat.AppId) {
|
||
value := getValue(stat)
|
||
statList = append(statList, &StatItem{
|
||
Value: value,
|
||
Stat: stat,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
// sort
|
||
sort.Sort(statList)
|
||
// 显示
|
||
y, m, _ := month.Date()
|
||
for _, item := range statList {
|
||
appName := statg.GlobalEndpoints.ExistsByAppId(item.Stat.AppId)
|
||
title := fmt.Sprintf("%d年%d月【%s】数据汇总", y, m, appName)
|
||
text := item.Stat.DisplaySummaryText(title)
|
||
if err = cmd.SendBack(text); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func CrossStatRecharge(ctx context.Context, cmd *tgbot.Command) error {
|
||
moveBackDays, _ := cmd.GetFlagInt64Value("-back")
|
||
if moveBackDays > 0 {
|
||
moveBackDays = moveBackDays * -1
|
||
}
|
||
// get stats
|
||
now := time.Now()
|
||
reportTime := utils.RecentMinute(now, 60)
|
||
if moveBackDays != 0 {
|
||
reportTime = utils.BeginWithTime(now.AddDate(0, 0, int(moveBackDays)+1))
|
||
}
|
||
title := GetDateDesc(0) + "充值排名:" + reportTime.Format("2006-01-02T15:04:05")
|
||
photo, _, err := GetRankPhoto(reportTime, title, func(item *botdailyhourstat.BotHourStat) int64 {
|
||
return item.TodayStat.Recharge.Value() / 100
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cmd.PhotoPath = photo
|
||
return cmd.SendBack(fmt.Sprintf("充值排名:%s\n", reportTime.Format("2006-01-02T15:04:05")))
|
||
}
|
||
|
||
func CrossStatRechargeNew(ctx context.Context, cmd *tgbot.Command) error {
|
||
moveBackDays, _ := cmd.GetFlagInt64Value("-back")
|
||
if moveBackDays > 0 {
|
||
moveBackDays = moveBackDays * -1
|
||
}
|
||
// get stats
|
||
now := time.Now()
|
||
reportTime := utils.RecentMinute(now, 60)
|
||
if moveBackDays != 0 {
|
||
reportTime = utils.BeginWithTime(now.AddDate(0, 0, int(moveBackDays)+1))
|
||
}
|
||
title := GetDateDesc(0) + "新用户充值排名:" + reportTime.Format("2006-01-02T15:04:05")
|
||
photo, _, err := GetRankPhoto(reportTime, title, func(item *botdailyhourstat.BotHourStat) int64 {
|
||
return item.TodayStat.RechargeNew.Value() / 100
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cmd.PhotoPath = photo
|
||
return cmd.SendBack(fmt.Sprintf("新用户充值排名:%s\n", reportTime.Format("2006-01-02T15:04:05")))
|
||
}
|
||
|
||
func CrossStatRechargeOld(ctx context.Context, cmd *tgbot.Command) error {
|
||
moveBackDays, _ := cmd.GetFlagInt64Value("-back")
|
||
if moveBackDays > 0 {
|
||
moveBackDays = moveBackDays * -1
|
||
}
|
||
// get stats
|
||
now := time.Now()
|
||
reportTime := utils.RecentMinute(now, 60)
|
||
if moveBackDays != 0 {
|
||
reportTime = utils.BeginWithTime(now.AddDate(0, 0, int(moveBackDays)+1))
|
||
}
|
||
title := GetDateDesc(0) + "老用户充值排名:" + reportTime.Format("2006-01-02T15:04:05")
|
||
photo, _, err := GetRankPhoto(reportTime, title, func(item *botdailyhourstat.BotHourStat) int64 {
|
||
return item.TodayStat.RechargeOld.Value() / 100
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cmd.PhotoPath = photo
|
||
return cmd.SendBack(fmt.Sprintf("老用户充值排名:%s\n", reportTime.Format("2006-01-02T15:04:05")))
|
||
}
|
||
|
||
func CrossStatRechargeGold(ctx context.Context, cmd *tgbot.Command) error {
|
||
moveBackDays, _ := cmd.GetFlagInt64Value("-back")
|
||
if moveBackDays > 0 {
|
||
moveBackDays = moveBackDays * -1
|
||
}
|
||
// get stats
|
||
now := time.Now()
|
||
reportTime := utils.RecentMinute(now, 60)
|
||
if moveBackDays != 0 {
|
||
reportTime = utils.BeginWithTime(now.AddDate(0, 0, int(moveBackDays)+1))
|
||
}
|
||
title := GetDateDesc(0) + "金币充值排名:" + reportTime.Format("2006-01-02T15:04:05")
|
||
photo, _, err := GetRankPhoto(reportTime, title, func(item *botdailyhourstat.BotHourStat) int64 {
|
||
return item.TodayStat.RechargeGold.Value() / 100
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cmd.PhotoPath = photo
|
||
return cmd.SendBack(fmt.Sprintf("金币充值排名:%s\n", reportTime.Format("2006-01-02T15:04:05")))
|
||
}
|
||
|
||
func CrossStatRechargeVip(ctx context.Context, cmd *tgbot.Command) error {
|
||
moveBackDays, _ := cmd.GetFlagInt64Value("-back")
|
||
if moveBackDays > 0 {
|
||
moveBackDays = moveBackDays * -1
|
||
}
|
||
// get stats
|
||
now := time.Now()
|
||
reportTime := utils.RecentMinute(now, 60)
|
||
if moveBackDays != 0 {
|
||
reportTime = utils.BeginWithTime(now.AddDate(0, 0, int(moveBackDays)+1))
|
||
}
|
||
title := GetDateDesc(0) + "会员充值排名:" + reportTime.Format("2006-01-02T15:04:05")
|
||
photo, _, err := GetRankPhoto(reportTime, title, func(item *botdailyhourstat.BotHourStat) int64 {
|
||
return item.TodayStat.RechargeVip.Value() / 100
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cmd.PhotoPath = photo
|
||
return cmd.SendBack(fmt.Sprintf("会员充值排名:%s\n", reportTime.Format("2006-01-02T15:04:05")))
|
||
}
|
||
|
||
func CrossStatRegister(ctx context.Context, cmd *tgbot.Command) error {
|
||
moveBackDays, _ := cmd.GetFlagInt64Value("-back")
|
||
if moveBackDays > 0 {
|
||
moveBackDays = moveBackDays * -1
|
||
}
|
||
// get stats
|
||
now := time.Now()
|
||
reportTime := utils.RecentMinute(now, 60)
|
||
if moveBackDays != 0 {
|
||
reportTime = utils.BeginWithTime(now.AddDate(0, 0, int(moveBackDays)+1))
|
||
}
|
||
title := GetDateDesc(0) + "注册排名:" + reportTime.Format("2006-01-02T15:04:05")
|
||
photo, _, err := GetRankPhoto(reportTime, title, func(item *botdailyhourstat.BotHourStat) int64 {
|
||
return item.TodayStat.Register.Value()
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cmd.PhotoPath = photo
|
||
return cmd.SendBack(fmt.Sprintf("注册排名:%s\n", reportTime.Format("2006-01-02T15:04:05")))
|
||
}
|
||
|
||
func CrossStatVisit(ctx context.Context, cmd *tgbot.Command) error {
|
||
moveBackDays, _ := cmd.GetFlagInt64Value("-back")
|
||
if moveBackDays > 0 {
|
||
moveBackDays = moveBackDays * -1
|
||
}
|
||
// get stats
|
||
now := time.Now()
|
||
reportTime := utils.RecentMinute(now, 60)
|
||
if moveBackDays != 0 {
|
||
reportTime = utils.BeginWithTime(now.AddDate(0, 0, int(moveBackDays)+1))
|
||
}
|
||
title := GetDateDesc(0) + "日活排名:" + reportTime.Format("2006-01-02T15:04:05")
|
||
photo, _, err := GetRankPhoto(reportTime, title, func(item *botdailyhourstat.BotHourStat) int64 {
|
||
return item.TodayStat.Visit.Value()
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cmd.PhotoPath = photo
|
||
return cmd.SendBack(fmt.Sprintf("日活排名:%s\n", reportTime.Format("2006-01-02T15:04:05")))
|
||
}
|
||
|
||
func CrossStatOrder(ctx context.Context, cmd *tgbot.Command) error {
|
||
moveBackDays, _ := cmd.GetFlagInt64Value("-back")
|
||
if moveBackDays > 0 {
|
||
moveBackDays = moveBackDays * -1
|
||
}
|
||
// get stats
|
||
now := time.Now()
|
||
reportTime := utils.RecentMinute(now, 60)
|
||
if moveBackDays != 0 {
|
||
reportTime = utils.BeginWithTime(now.AddDate(0, 0, int(moveBackDays)+1))
|
||
}
|
||
title := GetDateDesc(0) + "订单排名:" + reportTime.Format("2006-01-02T15:04:05")
|
||
photo, _, err := GetRankPhoto(reportTime, title, func(item *botdailyhourstat.BotHourStat) int64 {
|
||
return item.TodayStat.Order.Count()
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
cmd.PhotoPath = photo
|
||
return cmd.SendBack(fmt.Sprintf("订单排名:%s\n", reportTime.Format("2006-01-02T15:04:05")))
|
||
}
|
||
|
||
func GetRankPhoto(reportTime time.Time, title string, getValue FuncGetStatValue) (string, []byte, error) {
|
||
charts.SetDefaultFont(statutils.Font)
|
||
statList := make(StatItemList, 0)
|
||
registerStatList := make(StatItemList, 0)
|
||
var total, registerTotal, visitTotal int64
|
||
for _, addr := range statg.Conf.InnerServers {
|
||
req := &proto.BotDailyHourListReq{
|
||
Page: common.Page{
|
||
PageNum: 1,
|
||
PageSize: 100,
|
||
},
|
||
SumDateStart: reportTime,
|
||
SumDateStop: reportTime,
|
||
}
|
||
res := CrossStatRes{}
|
||
err := InnerRequest(addr+"/api/stat/inner/botStatList", req, &res, true)
|
||
if err != nil {
|
||
return "", nil, err
|
||
}
|
||
if res.Data == nil {
|
||
continue
|
||
}
|
||
|
||
for _, stat := range res.Data.List {
|
||
if statg.GlobalEndpoints.GetBotRankEndpoints().ExistsByAppId(stat.AppId) {
|
||
value := getValue(stat)
|
||
total += value
|
||
statList = append(statList, &StatItem{
|
||
Value: value,
|
||
Stat: stat,
|
||
})
|
||
|
||
registerValue := stat.TodayStat.Register.Total.Value()
|
||
registerTotal += registerValue
|
||
visitTotal += stat.TodayStat.Visit.Total.Value()
|
||
registerStatList = append(registerStatList, &StatItem{
|
||
Value: registerValue,
|
||
Stat: stat,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
// 空数据集不能进入渲染: go-charts 会把坐标轴范围退化成 MinInt64/NaN,
|
||
// 产出一张刻度为 -9,223,372,036,854,776,000 的坏图并照常发到群里。
|
||
if len(statList) == 0 {
|
||
return "", nil, ErrNoRankData
|
||
}
|
||
// sort
|
||
sort.Sort(statList)
|
||
// draw chart
|
||
xNames := make([]string, 0)
|
||
for _, stat := range statList {
|
||
xNames = append(xNames, statg.GlobalEndpoints.GetAppShortNameById(stat.Stat.AppId))
|
||
}
|
||
sDatas := make([]charts.SeriesData, 0)
|
||
rDatas := make([]charts.SeriesData, 0)
|
||
for index, stat := range statList {
|
||
sDatas = append(sDatas, charts.SeriesData{
|
||
Value: float64(stat.Value),
|
||
Style: charts.Style{
|
||
FillColor: Colors[index%len(Colors)],
|
||
TextWrap: chart.TextWrapRune,
|
||
},
|
||
})
|
||
for _, registerStat := range registerStatList {
|
||
if stat.Stat.AppId == registerStat.Stat.AppId {
|
||
rDatas = append(rDatas, charts.SeriesData{
|
||
Value: float64(registerStat.Value),
|
||
Style: charts.Style{
|
||
FillColor: Colors[index%len(Colors)],
|
||
TextWrap: chart.TextWrapRune,
|
||
},
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
chartOpt := charts.ChartOption{
|
||
Width: statg.Conf.ChartsConfig.Width,
|
||
Height: statg.Conf.ChartsConfig.Height,
|
||
Type: charts.ChartOutputPNG,
|
||
Title: charts.TitleOption{
|
||
Text: title,
|
||
},
|
||
XAxis: charts.NewXAxisOption(xNames),
|
||
Legend: charts.LegendOption{
|
||
Data: []string{
|
||
fmt.Sprintf("应用:%d", len(xNames)),
|
||
fmt.Sprintf("充值:%d", total),
|
||
fmt.Sprintf("日活:%d", visitTotal),
|
||
fmt.Sprintf("注册:%d", registerTotal),
|
||
},
|
||
Icon: charts.IconRect,
|
||
Left: charts.PositionRight,
|
||
},
|
||
SeriesList: []charts.Series{
|
||
{
|
||
Type: charts.ChartTypeBar,
|
||
Data: sDatas,
|
||
Label: charts.SeriesLabel{
|
||
Show: true,
|
||
Position: charts.PositionBottom,
|
||
},
|
||
MarkLine: charts.NewMarkLine(
|
||
charts.SeriesMarkDataTypeAverage,
|
||
),
|
||
},
|
||
{
|
||
Type: charts.ChartTypeBar,
|
||
Data: rDatas,
|
||
Label: charts.SeriesLabel{
|
||
Show: true,
|
||
Position: charts.PositionBottom,
|
||
},
|
||
},
|
||
},
|
||
}
|
||
show := true
|
||
chartOpt.XAxis.Show = &show
|
||
d, err := charts.Render(chartOpt)
|
||
if err != nil {
|
||
return "", nil, err
|
||
}
|
||
buf, err := d.Bytes()
|
||
if err != nil {
|
||
return "", buf, err
|
||
}
|
||
photo := fmt.Sprintf("/tmp/%s.png", utils.UUID())
|
||
if err = statutils.WriteBytesToFile(buf, photo); err != nil {
|
||
return "", buf, err
|
||
}
|
||
return photo, buf, nil
|
||
}
|
||
|
||
func GetRankDetail(reportTime time.Time, title string, getValue FuncGetStatValue) (string, error) {
|
||
charts.SetDefaultFont(statutils.Font)
|
||
statList := make(StatItemList, 0)
|
||
registerStatList := make(StatItemList, 0)
|
||
var total, registerTotal int64
|
||
for _, addr := range statg.Conf.InnerServers {
|
||
req := &proto.BotDailyHourListReq{
|
||
Page: common.Page{
|
||
PageNum: 1,
|
||
PageSize: 100,
|
||
},
|
||
SumDateStart: reportTime,
|
||
SumDateStop: reportTime,
|
||
}
|
||
res := CrossStatRes{}
|
||
err := InnerRequest(addr+"/api/stat/inner/botStatList", req, &res, true)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if res.Data == nil {
|
||
continue
|
||
}
|
||
|
||
for _, stat := range res.Data.List {
|
||
if statg.GlobalEndpoints.GetBotRankEndpoints().ExistsByAppId(stat.AppId) {
|
||
value := getValue(stat)
|
||
total += value
|
||
statList = append(statList, &StatItem{
|
||
Value: value,
|
||
Stat: stat,
|
||
})
|
||
|
||
registerValue := stat.TodayStat.Register.Total.Value()
|
||
registerTotal += registerValue
|
||
registerStatList = append(registerStatList, &StatItem{
|
||
Value: registerValue,
|
||
Stat: stat,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
// 同 GetRankPhoto: 空数据集会渲染出坐标轴为 MinInt64/NaN 的坏图
|
||
if len(statList) == 0 {
|
||
return "", ErrNoRankData
|
||
}
|
||
// sort
|
||
sort.Sort(statList)
|
||
// draw chart
|
||
xNames := make([]string, 0)
|
||
for _, stat := range statList {
|
||
xNames = append(xNames, statg.GlobalEndpoints.GetAppShortNameById(stat.Stat.AppId))
|
||
}
|
||
sDatas := make([]charts.SeriesData, 0)
|
||
rDatas := make([]charts.SeriesData, 0)
|
||
for index, stat := range statList {
|
||
sDatas = append(sDatas, charts.SeriesData{
|
||
Value: float64(stat.Value),
|
||
Style: charts.Style{
|
||
FillColor: Colors[index%len(Colors)],
|
||
TextWrap: chart.TextWrapRune,
|
||
},
|
||
})
|
||
for _, registerStat := range registerStatList {
|
||
if stat.Stat.AppId == registerStat.Stat.AppId {
|
||
rDatas = append(rDatas, charts.SeriesData{
|
||
Value: float64(registerStat.Value),
|
||
Style: charts.Style{
|
||
FillColor: Colors[index%len(Colors)],
|
||
TextWrap: chart.TextWrapRune,
|
||
},
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
chartOpt := charts.ChartOption{
|
||
Width: statg.Conf.ChartsConfig.Width,
|
||
Height: statg.Conf.ChartsConfig.Height,
|
||
Type: charts.ChartOutputPNG,
|
||
Title: charts.TitleOption{
|
||
Text: title,
|
||
},
|
||
XAxis: charts.NewXAxisOption(xNames),
|
||
Legend: charts.LegendOption{
|
||
Data: []string{
|
||
fmt.Sprintf("应用:%d", len(xNames)),
|
||
fmt.Sprintf("充值:%d", total),
|
||
fmt.Sprintf("注册:%d", registerTotal),
|
||
},
|
||
Icon: charts.IconRect,
|
||
Left: charts.PositionRight,
|
||
},
|
||
SeriesList: []charts.Series{
|
||
{
|
||
Type: charts.ChartTypeBar,
|
||
Data: sDatas,
|
||
Label: charts.SeriesLabel{
|
||
Show: true,
|
||
Position: charts.PositionBottom,
|
||
},
|
||
MarkLine: charts.NewMarkLine(
|
||
charts.SeriesMarkDataTypeAverage,
|
||
),
|
||
},
|
||
{
|
||
Type: charts.ChartTypeBar,
|
||
Data: rDatas,
|
||
Label: charts.SeriesLabel{
|
||
Show: true,
|
||
Position: charts.PositionBottom,
|
||
},
|
||
},
|
||
},
|
||
}
|
||
show := true
|
||
chartOpt.XAxis.Show = &show
|
||
d, err := charts.Render(chartOpt)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
buf, err := d.Bytes()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
photo := fmt.Sprintf("/tmp/%s.png", utils.UUID())
|
||
if err = statutils.WriteBytesToFile(buf, photo); err != nil {
|
||
return "", err
|
||
}
|
||
return photo, nil
|
||
}
|