feat: support title render function
This commit is contained in:
parent
ccdaf70dcb
commit
ffbda8f214
12 changed files with 455 additions and 141 deletions
35
axis.go
35
axis.go
|
|
@ -239,6 +239,10 @@ func (d *Draw) axisTick(opt *axisOption) {
|
|||
tickCount--
|
||||
}
|
||||
labelMargin := style.GetLabelMargin()
|
||||
tickShow := true
|
||||
if opt.style.TickShow != nil && !*opt.style.TickShow {
|
||||
tickShow = false
|
||||
}
|
||||
|
||||
tickLengthValue := style.GetTickLength()
|
||||
labelHeight := labelMargin + opt.textMaxHeight
|
||||
|
|
@ -254,17 +258,20 @@ func (d *Draw) axisTick(opt *axisOption) {
|
|||
if style.Position == PositionLeft {
|
||||
x0 = labelWidth
|
||||
}
|
||||
for _, v := range values {
|
||||
x := x0
|
||||
y := v
|
||||
d.moveTo(x, y)
|
||||
d.lineTo(x+tickLengthValue, y)
|
||||
r.Stroke()
|
||||
if tickShow {
|
||||
for _, v := range values {
|
||||
x := x0
|
||||
y := v
|
||||
d.moveTo(x, y)
|
||||
d.lineTo(x+tickLengthValue, y)
|
||||
r.Stroke()
|
||||
}
|
||||
}
|
||||
// 辅助线
|
||||
if style.SplitLineShow && !style.SplitLineColor.IsZero() {
|
||||
r.SetStrokeColor(style.SplitLineColor)
|
||||
splitLineWidth := width - labelWidth
|
||||
splitLineWidth := width - labelWidth - tickLengthValue
|
||||
x0 = labelWidth + tickLengthValue
|
||||
if position == PositionRight {
|
||||
x0 = 0
|
||||
splitLineWidth = width - labelWidth - 1
|
||||
|
|
@ -284,12 +291,14 @@ func (d *Draw) axisTick(opt *axisOption) {
|
|||
if position == PositionTop {
|
||||
y0 = labelHeight
|
||||
}
|
||||
for _, v := range values {
|
||||
x := v
|
||||
y := y0
|
||||
d.moveTo(x, y-tickLengthValue)
|
||||
d.lineTo(x, y)
|
||||
r.Stroke()
|
||||
if tickShow {
|
||||
for _, v := range values {
|
||||
x := v
|
||||
y := y0
|
||||
d.moveTo(x, y-tickLengthValue)
|
||||
d.lineTo(x, y)
|
||||
r.Stroke()
|
||||
}
|
||||
}
|
||||
// 辅助线
|
||||
if style.SplitLineShow && !style.SplitLineColor.IsZero() {
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ func TestAxis(t *testing.T) {
|
|||
opt.style.BoundaryGap = FalseFlag()
|
||||
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
|
||||
|
|
@ -150,7 +150,7 @@ func TestAxis(t *testing.T) {
|
|||
opt.style.Position = PositionLeft
|
||||
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
|
||||
|
|
|
|||
75
chart.go
75
chart.go
|
|
@ -23,17 +23,13 @@
|
|||
package charts
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||
)
|
||||
|
||||
type XAxisOption struct {
|
||||
BoundaryGap *bool
|
||||
Data []string
|
||||
// TODO split number
|
||||
}
|
||||
|
||||
type SeriesData struct {
|
||||
Value float64
|
||||
Style chart.Style
|
||||
|
|
@ -43,31 +39,6 @@ type Point struct {
|
|||
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 string
|
||||
Name string
|
||||
|
|
@ -78,6 +49,7 @@ type Series struct {
|
|||
|
||||
type ChartOption struct {
|
||||
Theme string
|
||||
Title TitleOption
|
||||
XAxis XAxisOption
|
||||
Width int
|
||||
Height int
|
||||
|
|
@ -87,6 +59,29 @@ type ChartOption struct {
|
|||
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 {
|
||||
if o.Width == 0 {
|
||||
return 600
|
||||
|
|
@ -102,8 +97,8 @@ func (o *ChartOption) getHeight() int {
|
|||
}
|
||||
|
||||
func (o *ChartOption) getYRange(axisIndex int) Range {
|
||||
min := float64(0)
|
||||
max := float64(0)
|
||||
min := math.MaxFloat64
|
||||
max := -math.MaxFloat64
|
||||
|
||||
for _, series := range o.SeriesList {
|
||||
if series.YAxisIndex != axisIndex {
|
||||
|
|
@ -118,18 +113,8 @@ func (o *ChartOption) getYRange(axisIndex int) Range {
|
|||
}
|
||||
}
|
||||
}
|
||||
// TODO 对于小数的处理
|
||||
|
||||
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))
|
||||
// y轴分设置默认划分为6块
|
||||
r := NewRange(min*0.9, max*1.1, 6)
|
||||
return r
|
||||
}
|
||||
|
||||
|
|
|
|||
1
draw.go
1
draw.go
|
|
@ -34,6 +34,7 @@ import (
|
|||
const (
|
||||
PositionLeft = "left"
|
||||
PositionRight = "right"
|
||||
PositionCenter = "center"
|
||||
PositionTop = "top"
|
||||
PositionBottom = "bottom"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,78 +31,6 @@ type LineChartOption struct {
|
|||
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) {
|
||||
d, err := NewDraw(
|
||||
DrawOption{
|
||||
|
|
@ -119,27 +47,35 @@ func NewLineChart(opt LineChartOption) (*Draw, error) {
|
|||
theme := Theme{
|
||||
mode: opt.Theme,
|
||||
}
|
||||
// 设置背景色
|
||||
bg := opt.BackgroundColor
|
||||
if bg.IsZero() {
|
||||
bg = theme.GetBackgroundColor()
|
||||
}
|
||||
opt.FillDefault(&theme)
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 暂时仅支持单一yaxis
|
||||
yRange, err := drawYAxis(d, &opt.ChartOption, &theme, xAxisHeight)
|
||||
yRange, err := drawYAxis(d, &opt.ChartOption, &theme, xAxisHeight, chart.Box{
|
||||
Top: titleHeight,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sd, err := NewDraw(DrawOption{
|
||||
Parent: d,
|
||||
}, PaddingOption(chart.Box{
|
||||
Top: titleHeight,
|
||||
Left: YAxisWidth,
|
||||
}))
|
||||
if err != nil {
|
||||
|
|
@ -166,9 +102,7 @@ func NewLineChart(opt LineChartOption) (*Draw, error) {
|
|||
DotFillColor: dotFillColor,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
// fmt.Println(yRange)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
|
|
|||
75
range.go
Normal file
75
range.go
Normal 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)
|
||||
// 如果是小于0,int的时候向上取整了,因此调整
|
||||
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
49
range_test.go
Normal 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)
|
||||
}
|
||||
17
theme.go
17
theme.go
|
|
@ -120,3 +120,20 @@ func (t *Theme) GetBackgroundColor() drawing.Color {
|
|||
}
|
||||
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
112
title.go
Normal 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
|
||||
}
|
||||
1
util.go
1
util.go
|
|
@ -67,6 +67,7 @@ func maxInt(values ...int) int {
|
|||
return result
|
||||
}
|
||||
|
||||
// measureTextMaxWidthHeight returns maxWidth and maxHeight of text list
|
||||
func measureTextMaxWidthHeight(textList []string, r chart.Renderer) (int, int) {
|
||||
maxWidth := 0
|
||||
maxHeight := 0
|
||||
|
|
|
|||
69
xaxis.go
Normal file
69
xaxis.go
Normal 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
62
yaxis.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue