Files
ops-assistant/internal/chart/chart.go
T
openclaw 81939825d1 feat: 模块化命令框架 + runbook 严格断言与查询结果回传
- core/module: 命令模板、门控(feature flag/confirm/dry-run)、ecode 错误标签、ResultFormatter
- runbook executor: http.get/http.post/assert.json(expect_success 严格断言)、目标主机管理、任务落库、最终 stdout 直接回传
- cf 模块: /cf dns list 支持域名自动解析 zone_id;dns list/zones/workers list 格式化回传;API success:false 判真失败
- 全部 CF/CPA runbook 接入严格断言,杜绝假阳性 success
- 新增 cpa/cfdav/mail/sub 模块与 runbooks;QQ 订阅到期提醒调度
- Web 后台: 模块设置(CF/CPA/cfdav/AI)、OPS 任务、渠道管理、审计页
2026-09-13 07:42:17 +08:00

127 lines
2.8 KiB
Go

package chart
import (
"bytes"
"fmt"
"math"
"ops-assistant/internal/service"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
)
// 分类对应的颜色
var categoryColors = []drawing.Color{
{R: 255, G: 99, B: 132, A: 255}, // 红
{R: 54, G: 162, B: 235, A: 255}, // 蓝
{R: 255, G: 206, B: 86, A: 255}, // 黄
{R: 75, G: 192, B: 192, A: 255}, // 青
{R: 153, G: 102, B: 255, A: 255}, // 紫
{R: 255, G: 159, B: 64, A: 255}, // 橙
{R: 46, G: 204, B: 113, A: 255}, // 绿
{R: 231, G: 76, B: 60, A: 255}, // 深红
{R: 52, G: 73, B: 94, A: 255}, // 深蓝灰
{R: 241, G: 196, B: 15, A: 255}, // 金
}
// GeneratePieChart 生成分类占比饼图
func GeneratePieChart(stats []service.CategoryStat, title string) ([]byte, error) {
if len(stats) == 0 {
return nil, fmt.Errorf("no data")
}
var total float64
for _, s := range stats {
total += float64(s.Total)
}
var values []chart.Value
for i, s := range stats {
yuan := float64(s.Total) / 100.0
pct := float64(s.Total) / total * 100
label := fmt.Sprintf("%s %.0f元(%.0f%%)", s.Category, yuan, pct)
values = append(values, chart.Value{
Value: yuan,
Label: label,
Style: chart.Style{
FillColor: categoryColors[i%len(categoryColors)],
StrokeColor: drawing.ColorWhite,
StrokeWidth: 2,
},
})
}
pie := chart.PieChart{
Title: title,
Width: 600,
Height: 500,
TitleStyle: chart.Style{
FontSize: 16,
},
Values: values,
}
buf := &bytes.Buffer{}
if err := pie.Render(chart.PNG, buf); err != nil {
return nil, fmt.Errorf("render pie chart: %w", err)
}
return buf.Bytes(), nil
}
// GenerateBarChart 生成每日消费柱状图
func GenerateBarChart(stats []service.DailyStat, title string) ([]byte, error) {
if len(stats) == 0 {
return nil, fmt.Errorf("no data")
}
var values []chart.Value
var maxVal float64
for _, s := range stats {
yuan := float64(s.Total) / 100.0
if yuan > maxVal {
maxVal = yuan
}
// 日期只取 MM-DD
dateLabel := s.Date
if len(s.Date) > 5 {
dateLabel = s.Date[5:]
}
values = append(values, chart.Value{
Value: yuan,
Label: dateLabel,
Style: chart.Style{
FillColor: drawing.Color{R: 54, G: 162, B: 235, A: 255},
StrokeColor: drawing.Color{R: 54, G: 162, B: 235, A: 255},
StrokeWidth: 1,
},
})
}
bar := chart.BarChart{
Title: title,
Width: 600,
Height: 400,
TitleStyle: chart.Style{
FontSize: 16,
},
YAxis: chart.YAxis{
Range: &chart.ContinuousRange{
Min: 0,
Max: math.Ceil(maxVal*1.2/10) * 10,
},
ValueFormatter: func(v interface{}) string {
return fmt.Sprintf("%.0f", v)
},
},
BarWidth: 40,
Bars: values,
}
buf := &bytes.Buffer{}
if err := bar.Render(chart.PNG, buf); err != nil {
return nil, fmt.Errorf("render bar chart: %w", err)
}
return buf.Bytes(), nil
}