feat: support title render function

This commit is contained in:
vicanso 2022-01-29 15:02:01 +08:00
parent ccdaf70dcb
commit ffbda8f214
12 changed files with 455 additions and 141 deletions

11
axis.go
View file

@ -239,6 +239,10 @@ func (d *Draw) axisTick(opt *axisOption) {
tickCount-- tickCount--
} }
labelMargin := style.GetLabelMargin() labelMargin := style.GetLabelMargin()
tickShow := true
if opt.style.TickShow != nil && !*opt.style.TickShow {
tickShow = false
}
tickLengthValue := style.GetTickLength() tickLengthValue := style.GetTickLength()
labelHeight := labelMargin + opt.textMaxHeight labelHeight := labelMargin + opt.textMaxHeight
@ -254,6 +258,7 @@ func (d *Draw) axisTick(opt *axisOption) {
if style.Position == PositionLeft { if style.Position == PositionLeft {
x0 = labelWidth x0 = labelWidth
} }
if tickShow {
for _, v := range values { for _, v := range values {
x := x0 x := x0
y := v y := v
@ -261,10 +266,12 @@ func (d *Draw) axisTick(opt *axisOption) {
d.lineTo(x+tickLengthValue, y) d.lineTo(x+tickLengthValue, y)
r.Stroke() r.Stroke()
} }
}
// 辅助线 // 辅助线
if style.SplitLineShow && !style.SplitLineColor.IsZero() { if style.SplitLineShow && !style.SplitLineColor.IsZero() {
r.SetStrokeColor(style.SplitLineColor) r.SetStrokeColor(style.SplitLineColor)
splitLineWidth := width - labelWidth splitLineWidth := width - labelWidth - tickLengthValue
x0 = labelWidth + tickLengthValue
if position == PositionRight { if position == PositionRight {
x0 = 0 x0 = 0
splitLineWidth = width - labelWidth - 1 splitLineWidth = width - labelWidth - 1
@ -284,6 +291,7 @@ func (d *Draw) axisTick(opt *axisOption) {
if position == PositionTop { if position == PositionTop {
y0 = labelHeight y0 = labelHeight
} }
if tickShow {
for _, v := range values { for _, v := range values {
x := v x := v
y := y0 y := y0
@ -291,6 +299,7 @@ func (d *Draw) axisTick(opt *axisOption) {
d.lineTo(x, y) d.lineTo(x, y)
r.Stroke() r.Stroke()
} }
}
// 辅助线 // 辅助线
if style.SplitLineShow && !style.SplitLineColor.IsZero() { if style.SplitLineShow && !style.SplitLineColor.IsZero() {
r.SetStrokeColor(style.SplitLineColor) r.SetStrokeColor(style.SplitLineColor)

View file

@ -140,7 +140,7 @@ func TestAxis(t *testing.T) {
opt.style.BoundaryGap = FalseFlag() opt.style.BoundaryGap = FalseFlag()
return opt return opt
}, },
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 44 5\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 44 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 54\nL 44 54\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 103\nL 44 103\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 151\nL 44 151\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 199\nL 44 199\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 247\nL 44 247\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 295\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 395 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 54\nL 395 54\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 103\nL 395 103\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 151\nL 395 151\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 199\nL 395 199\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 247\nL 395 247\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"12\" y=\"299\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"16\" y=\"251\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"12\" y=\"203\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"16\" y=\"155\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"23\" y=\"107\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"19\" y=\"58\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"16\" y=\"9\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>", result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 44 5\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 44 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 54\nL 44 54\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 103\nL 44 103\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 151\nL 44 151\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 199\nL 44 199\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 247\nL 44 247\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 295\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 44 5\nL 395 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 54\nL 395 54\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 103\nL 395 103\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 151\nL 395 151\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 199\nL 395 199\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 247\nL 395 247\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"12\" y=\"299\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"16\" y=\"251\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"12\" y=\"203\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"16\" y=\"155\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"23\" y=\"107\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"19\" y=\"58\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"16\" y=\"9\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
}, },
// 文本居中展示 // 文本居中展示
// axis位于left // axis位于left
@ -150,7 +150,7 @@ func TestAxis(t *testing.T) {
opt.style.Position = PositionLeft opt.style.Position = PositionLeft
return opt return opt
}, },
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 44 5\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 44 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 47\nL 44 47\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 89\nL 44 89\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 131\nL 44 131\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 172\nL 44 172\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 213\nL 44 213\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 254\nL 44 254\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 295\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 395 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 47\nL 395 47\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 89\nL 395 89\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 131\nL 395 131\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 172\nL 395 172\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 213\nL 395 213\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 254\nL 395 254\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"12\" y=\"280\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"16\" y=\"239\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"12\" y=\"198\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"16\" y=\"157\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"23\" y=\"115\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"19\" y=\"73\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"16\" y=\"31\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>", result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 44 5\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 44 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 47\nL 44 47\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 89\nL 44 89\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 131\nL 44 131\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 172\nL 44 172\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 213\nL 44 213\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 254\nL 44 254\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 295\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 44 5\nL 395 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 47\nL 395 47\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 89\nL 395 89\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 131\nL 395 131\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 172\nL 395 172\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 213\nL 395 213\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 254\nL 395 254\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"12\" y=\"280\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"16\" y=\"239\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"12\" y=\"198\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"16\" y=\"157\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"23\" y=\"115\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"19\" y=\"73\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"16\" y=\"31\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
}, },
// 文本按起始位置展示 // 文本按起始位置展示
// axis位于right // axis位于right

View file

@ -23,17 +23,13 @@
package charts package charts
import ( import (
"math"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing" "github.com/wcharczuk/go-chart/v2/drawing"
) )
type XAxisOption struct {
BoundaryGap *bool
Data []string
// TODO split number
}
type SeriesData struct { type SeriesData struct {
Value float64 Value float64
Style chart.Style Style chart.Style
@ -43,31 +39,6 @@ type Point struct {
Y int Y int
} }
type Range struct {
originalMin float64
originalMax float64
divideCount int
Min float64
Max float64
Size int
Boundary bool
}
func (r *Range) getHeight(value float64) int {
v := 1 - value/(r.Max-r.Min)
return int(v * float64(r.Size))
}
func (r *Range) getWidth(value float64) int {
v := value / (r.Max - r.Min)
// 移至居中
if r.Boundary &&
r.divideCount != 0 {
v += 1 / float64(r.divideCount*2)
}
return int(v * float64(r.Size))
}
type Series struct { type Series struct {
Type string Type string
Name string Name string
@ -78,6 +49,7 @@ type Series struct {
type ChartOption struct { type ChartOption struct {
Theme string Theme string
Title TitleOption
XAxis XAxisOption XAxis XAxisOption
Width int Width int
Height int Height int
@ -87,6 +59,29 @@ type ChartOption struct {
BackgroundColor drawing.Color BackgroundColor drawing.Color
} }
func (o *ChartOption) FillDefault(t *Theme) {
if o.BackgroundColor.IsZero() {
o.BackgroundColor = t.GetBackgroundColor()
}
if o.Title.Style.FontColor.IsZero() {
o.Title.Style.FontColor = t.GetTitleColor()
}
if o.Title.Style.FontSize == 0 {
o.Title.Style.FontSize = 14
}
if o.Title.Style.Font == nil {
o.Title.Style.Font, _ = chart.GetDefaultFont()
}
if o.Title.Style.Padding.IsZero() {
o.Title.Style.Padding = chart.Box{
Left: 5,
Top: 5,
Right: 5,
Bottom: 5,
}
}
}
func (o *ChartOption) getWidth() int { func (o *ChartOption) getWidth() int {
if o.Width == 0 { if o.Width == 0 {
return 600 return 600
@ -102,8 +97,8 @@ func (o *ChartOption) getHeight() int {
} }
func (o *ChartOption) getYRange(axisIndex int) Range { func (o *ChartOption) getYRange(axisIndex int) Range {
min := float64(0) min := math.MaxFloat64
max := float64(0) max := -math.MaxFloat64
for _, series := range o.SeriesList { for _, series := range o.SeriesList {
if series.YAxisIndex != axisIndex { if series.YAxisIndex != axisIndex {
@ -118,18 +113,8 @@ func (o *ChartOption) getYRange(axisIndex int) Range {
} }
} }
} }
// TODO 对于小数的处理 // y轴分设置默认划分为6块
r := NewRange(min*0.9, max*1.1, 6)
divideCount := 6
r := Range{
originalMin: min,
originalMax: max,
Min: float64(int(min * 0.8)),
Max: max * 1.2,
divideCount: divideCount,
}
value := int((r.Max - r.Min) / float64(divideCount))
r.Max = float64(int(float64(value*divideCount) + r.Min))
return r return r
} }

View file

@ -34,6 +34,7 @@ import (
const ( const (
PositionLeft = "left" PositionLeft = "left"
PositionRight = "right" PositionRight = "right"
PositionCenter = "center"
PositionTop = "top" PositionTop = "top"
PositionBottom = "bottom" PositionBottom = "bottom"
) )

View file

@ -31,78 +31,6 @@ type LineChartOption struct {
ChartOption ChartOption
} }
const YAxisWidth = 50
func drawXAxis(d *Draw, opt *XAxisOption, theme *Theme) (int, *Range, error) {
dXAxis, err := NewDraw(
DrawOption{
Parent: d,
},
PaddingOption(chart.Box{
Left: YAxisWidth,
}),
)
if err != nil {
return 0, nil, err
}
data := NewAxisDataListFromStringList(opt.Data)
style := AxisStyle{
BoundaryGap: opt.BoundaryGap,
StrokeColor: theme.GetAxisStrokeColor(),
FontColor: theme.GetAxisStrokeColor(),
StrokeWidth: 1,
}
boundary := true
max := float64(len(opt.Data))
if opt.BoundaryGap != nil && !*opt.BoundaryGap {
boundary = false
max--
}
dXAxis.Axis(data, style)
return d.measureAxis(data, style), &Range{
divideCount: len(opt.Data),
Min: 0,
Max: max,
Size: dXAxis.Box.Width(),
Boundary: boundary,
}, nil
}
func drawYAxis(d *Draw, opt *ChartOption, theme *Theme, xAxisHeight int) (*Range, error) {
yRange := opt.getYRange(0)
data := NewAxisDataListFromStringList(yRange.Values())
style := AxisStyle{
Position: PositionLeft,
BoundaryGap: FalseFlag(),
// StrokeColor: theme.GetAxisStrokeColor(),
FontColor: theme.GetAxisStrokeColor(),
StrokeWidth: 1,
SplitLineColor: theme.GetAxisSplitLineColor(),
SplitLineShow: true,
}
width := d.measureAxis(data, style)
dYAxis, err := NewDraw(
DrawOption{
Parent: d,
Width: d.Box.Width(),
// 减去x轴的高
Height: d.Box.Height() - xAxisHeight,
},
PaddingOption(chart.Box{
Left: YAxisWidth - width,
}),
)
if err != nil {
return nil, err
}
dYAxis.Axis(data, style)
yRange.Size = dYAxis.Box.Height()
return &yRange, nil
}
func NewLineChart(opt LineChartOption) (*Draw, error) { func NewLineChart(opt LineChartOption) (*Draw, error) {
d, err := NewDraw( d, err := NewDraw(
DrawOption{ DrawOption{
@ -119,27 +47,35 @@ func NewLineChart(opt LineChartOption) (*Draw, error) {
theme := Theme{ theme := Theme{
mode: opt.Theme, mode: opt.Theme,
} }
// 设置背景色 opt.FillDefault(&theme)
bg := opt.BackgroundColor
if bg.IsZero() {
bg = theme.GetBackgroundColor()
}
if opt.Parent == nil { if opt.Parent == nil {
d.setBackground(opt.getWidth(), opt.getHeight(), bg) d.setBackground(opt.getWidth(), opt.getHeight(), opt.BackgroundColor)
} }
// 标题
_, titleHeight, err := drawTitle(d, &opt.Title)
if err != nil {
return nil, err
}
// xAxis
xAxisHeight, xRange, err := drawXAxis(d, &opt.XAxis, &theme) xAxisHeight, xRange, err := drawXAxis(d, &opt.XAxis, &theme)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 暂时仅支持单一yaxis // 暂时仅支持单一yaxis
yRange, err := drawYAxis(d, &opt.ChartOption, &theme, xAxisHeight) yRange, err := drawYAxis(d, &opt.ChartOption, &theme, xAxisHeight, chart.Box{
Top: titleHeight,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
sd, err := NewDraw(DrawOption{ sd, err := NewDraw(DrawOption{
Parent: d, Parent: d,
}, PaddingOption(chart.Box{ }, PaddingOption(chart.Box{
Top: titleHeight,
Left: YAxisWidth, Left: YAxisWidth,
})) }))
if err != nil { if err != nil {
@ -166,9 +102,7 @@ func NewLineChart(opt LineChartOption) (*Draw, error) {
DotFillColor: dotFillColor, DotFillColor: dotFillColor,
}) })
} }
} }
// fmt.Println(yRange)
return d, nil return d, nil
} }

75
range.go Normal file
View file

@ -0,0 +1,75 @@
// MIT License
// Copyright (c) 2022 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package charts
import (
"math"
)
type Range struct {
divideCount int
Min float64
Max float64
Size int
Boundary bool
}
func NewRange(min, max float64, divideCount int) Range {
r := math.Abs(max - min)
// 最小单位计算
unit := 5
if r > 100 {
unit = 20
}
unit = int((r/float64(divideCount))/float64(unit))*unit + unit
if min != 0 {
min = float64(int(min/float64(unit)) * unit)
// 如果是小于0int的时候向上取整了因此调整
if min < 0 {
min -= float64(unit)
}
}
max = min + float64(unit*divideCount)
return Range{
Min: min,
Max: max,
divideCount: divideCount,
}
}
func (r *Range) getHeight(value float64) int {
v := 1 - (value-r.Min)/(r.Max-r.Min)
return int(v * float64(r.Size))
}
func (r *Range) getWidth(value float64) int {
v := value / (r.Max - r.Min)
// 移至居中
if r.Boundary &&
r.divideCount != 0 {
v += 1 / float64(r.divideCount*2)
}
return int(v * float64(r.Size))
}

49
range_test.go Normal file
View file

@ -0,0 +1,49 @@
// MIT License
// Copyright (c) 2022 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package charts
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRange(t *testing.T) {
assert := assert.New(t)
r := NewRange(0, 8, 6)
assert.Equal(0.0, r.Min)
assert.Equal(30.0, r.Max)
r = NewRange(0, 12, 6)
assert.Equal(0.0, r.Min)
assert.Equal(30.0, r.Max)
r = NewRange(-13, 18, 6)
assert.Equal(-20.0, r.Min)
assert.Equal(40.0, r.Max)
r = NewRange(0, 400, 6)
assert.Equal(0.0, r.Min)
assert.Equal(480.0, r.Max)
}

View file

@ -120,3 +120,20 @@ func (t *Theme) GetBackgroundColor() drawing.Color {
} }
return drawing.ColorWhite return drawing.ColorWhite
} }
func (t *Theme) GetTitleColor() drawing.Color {
if t.IsDark() {
return drawing.Color{
R: 238,
G: 241,
B: 250,
A: 255,
}
}
return drawing.Color{
R: 70,
G: 70,
B: 70,
A: 255,
}
}

112
title.go Normal file
View file

@ -0,0 +1,112 @@
// MIT License
// Copyright (c) 2022 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package charts
import (
"strconv"
"strings"
"github.com/wcharczuk/go-chart/v2"
)
type TitleOption struct {
Text string
Style chart.Style
Left string
Top string
}
type titleMeasureOption struct {
width int
height int
text string
}
func drawTitle(d *Draw, opt *TitleOption) (int, int, error) {
if len(opt.Text) == 0 {
return 0, 0, nil
}
padding := opt.Style.Padding
titleDraw, err := NewDraw(DrawOption{
Parent: d,
}, PaddingOption(padding))
if err != nil {
return 0, 0, err
}
r := titleDraw.Render
opt.Style.GetTextOptions().WriteToRenderer(r)
arr := strings.Split(opt.Text, "\n")
textMaxWidth := 0
textMaxHeight := 0
width := 0
measureOptions := make([]titleMeasureOption, len(arr))
for index, str := range arr {
textBox := r.MeasureText(str)
w := textBox.Width()
h := textBox.Height()
if w > textMaxWidth {
textMaxWidth = w
}
if h > textMaxHeight {
textMaxHeight = h
}
measureOptions[index] = titleMeasureOption{
text: str,
width: w,
height: h,
}
}
width = textMaxWidth
titleX := 0
b := titleDraw.Box
switch opt.Left {
case PositionRight:
titleX = b.Width() - textMaxWidth
case PositionCenter:
titleX = b.Width()>>1 - (textMaxWidth >> 1)
default:
if strings.HasSuffix(opt.Left, "%") {
value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", ""))
titleX = b.Width() * value / 100
} else {
value, _ := strconv.Atoi(opt.Left)
titleX = value
}
}
titleY := 0
// TODO TOP 暂只支持数值
if opt.Top != "" {
value, _ := strconv.Atoi(opt.Top)
titleY += value
}
for _, item := range measureOptions {
x := titleX + (textMaxWidth-item.width)>>1
titleDraw.text(item.text, x, titleY)
titleY += textMaxHeight
}
height := titleY + padding.Top + padding.Bottom
return width, height, nil
}

View file

@ -67,6 +67,7 @@ func maxInt(values ...int) int {
return result return result
} }
// measureTextMaxWidthHeight returns maxWidth and maxHeight of text list
func measureTextMaxWidthHeight(textList []string, r chart.Renderer) (int, int) { func measureTextMaxWidthHeight(textList []string, r chart.Renderer) (int, int) {
maxWidth := 0 maxWidth := 0
maxHeight := 0 maxHeight := 0

69
xaxis.go Normal file
View file

@ -0,0 +1,69 @@
// MIT License
// Copyright (c) 2022 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package charts
import "github.com/wcharczuk/go-chart/v2"
type XAxisOption struct {
BoundaryGap *bool
Data []string
// TODO split number
}
// drawXAxis draws x axis, and returns the height, range of if.
func drawXAxis(d *Draw, opt *XAxisOption, theme *Theme) (int, *Range, error) {
dXAxis, err := NewDraw(
DrawOption{
Parent: d,
},
PaddingOption(chart.Box{
Left: YAxisWidth,
}),
)
if err != nil {
return 0, nil, err
}
data := NewAxisDataListFromStringList(opt.Data)
style := AxisStyle{
BoundaryGap: opt.BoundaryGap,
StrokeColor: theme.GetAxisStrokeColor(),
FontColor: theme.GetAxisStrokeColor(),
StrokeWidth: 1,
}
boundary := true
max := float64(len(opt.Data))
if opt.BoundaryGap != nil && !*opt.BoundaryGap {
boundary = false
max--
}
dXAxis.Axis(data, style)
return d.measureAxis(data, style), &Range{
divideCount: len(opt.Data),
Min: 0,
Max: max,
Size: dXAxis.Box.Width(),
Boundary: boundary,
}, nil
}

62
yaxis.go Normal file
View file

@ -0,0 +1,62 @@
// MIT License
// Copyright (c) 2022 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package charts
import (
"github.com/wcharczuk/go-chart/v2"
)
const YAxisWidth = 40
func drawYAxis(d *Draw, opt *ChartOption, theme *Theme, xAxisHeight int, padding chart.Box) (*Range, error) {
yRange := opt.getYRange(0)
data := NewAxisDataListFromStringList(yRange.Values())
style := AxisStyle{
Position: PositionLeft,
BoundaryGap: FalseFlag(),
FontColor: theme.GetAxisStrokeColor(),
TickShow: FalseFlag(),
StrokeWidth: 1,
SplitLineColor: theme.GetAxisSplitLineColor(),
SplitLineShow: true,
}
width := d.measureAxis(data, style)
padding.Left += (YAxisWidth - width)
dYAxis, err := NewDraw(
DrawOption{
Parent: d,
Width: d.Box.Width(),
// 减去x轴的高
Height: d.Box.Height() - xAxisHeight,
},
PaddingOption(padding),
)
if err != nil {
return nil, err
}
dYAxis.Axis(data, style)
yRange.Size = dYAxis.Box.Height()
return &yRange, nil
}