feat: support axis render

This commit is contained in:
vicanso 2022-06-08 23:19:03 +08:00
parent 4cf494088e
commit b394e1b49f
5 changed files with 344 additions and 39 deletions

192
axis.go Normal file
View file

@ -0,0 +1,192 @@
// 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/golang/freetype/truetype"
)
type axisPainter struct {
p *Painter
opt *AxisPainterOption
}
func NewAxisPainter(p *Painter, opt AxisPainterOption) *axisPainter {
return &axisPainter{
p: p,
opt: &opt,
}
}
type AxisPainterOption struct {
// The label of axis
Data []string
// The boundary gap on both sides of a coordinate axis.
// Nil or *true means the center part of two axis ticks
BoundaryGap *bool
// The position of axis, it can be 'left', 'top', 'right' or 'bottom'
Position string
// Number of segments that the axis is split into. Note that this number serves only as a recommendation.
SplitNumber int
// The line color of axis
StrokeColor Color
// The line width
StrokeWidth float64
// The length of the axis tick
TickLength int
// The margin value of label
LabelMargin int
// The font size of label
FontSize float64
// The font of label
Font *truetype.Font
// The color of label
FontColor Color
// The flag for show axis split line, set this to true will show axis split line
SplitLineShow bool
// The color of split line
SplitLineColor Color
}
func (a *axisPainter) Render() (Box, error) {
opt := a.opt
p := a.p
strokeWidth := opt.StrokeWidth
if strokeWidth == 0 {
strokeWidth = 1
}
tickCount := opt.SplitNumber
if tickCount == 0 {
tickCount = len(opt.Data)
}
boundaryGap := true
if opt.BoundaryGap != nil && !*opt.BoundaryGap {
boundaryGap = false
}
labelPosition := ""
if !boundaryGap {
tickCount--
labelPosition = PositionLeft
}
// TODO 计算unit
unit := 1
// 如果小于0则表示不处理
tickLength := getDefaultInt(opt.TickLength, 5)
labelMargin := getDefaultInt(opt.LabelMargin, 5)
textMaxWidth, textMaxHeight := p.MeasureTextMaxWidthHeight(opt.Data)
width := 0
height := 0
// 垂直
if opt.Position == PositionLeft ||
opt.Position == PositionRight {
width = textMaxWidth + tickLength<<1
height = p.Height()
} else {
width = p.Width()
height = tickLength<<1 + textMaxHeight
}
padding := Box{}
switch opt.Position {
case PositionTop:
padding.Top = p.Height() - height
case PositionLeft:
padding.Right = p.Width() - width
}
p = p.Child(PainterPaddingOption(padding))
p.SetDrawingStyle(Style{
StrokeColor: opt.StrokeColor,
StrokeWidth: strokeWidth,
}).OverrideTextStyle(Style{
Font: opt.Font,
FontColor: opt.FontColor,
FontSize: opt.FontSize,
})
x0 := 0
y0 := 0
x1 := 0
y1 := 0
ticksPadding := 0
labelPadding := 0
orient := ""
textAlign := ""
switch opt.Position {
case PositionTop:
labelPadding = labelMargin
x1 = p.Width()
y0 = labelMargin + int(opt.FontSize)
ticksPadding = int(opt.FontSize)
y1 = y0
orient = OrientHorizontal
case PositionLeft:
orient = OrientVertical
textAlign = AlignRight
default:
labelPadding = height
x1 = p.Width()
orient = OrientHorizontal
}
p.Child(PainterPaddingOption(Box{
Top: ticksPadding,
})).Ticks(TicksOption{
Count: tickCount,
Length: tickLength,
Unit: unit,
Orient: orient,
})
p.LineStroke([]Point{
{
X: x0,
Y: y0,
},
{
X: x1,
Y: y1,
},
})
p.Child(PainterPaddingOption(Box{
Top: labelPadding,
})).MultiText(MultiTextOption{
Align: textAlign,
TextList: opt.Data,
Orient: orient,
Unit: unit,
Position: labelPosition,
})
return Box{
Bottom: height,
Right: width,
}, nil
}

View file

@ -27,7 +27,7 @@ func writeFile(buf []byte) error {
func main() { func main() {
p, err := charts.NewPainter(charts.PainterOptions{ p, err := charts.NewPainter(charts.PainterOptions{
Width: 600, Width: 600,
Height: 1200, Height: 2000,
Type: charts.ChartOutputPNG, Type: charts.ChartOutputPNG,
}) })
if err != nil { if err != nil {
@ -429,6 +429,7 @@ func main() {
Right: p.Width() - 1, Right: p.Width() - 1,
Bottom: top + 30, Bottom: top + 30,
})), charts.LegendPainterOption{ })), charts.LegendPainterOption{
Left: "10",
Data: []string{ Data: []string{
"Email", "Email",
"Union Ads", "Union Ads",
@ -446,6 +447,7 @@ func main() {
Right: p.Width() - 1, Right: p.Width() - 1,
Bottom: top + 30, Bottom: top + 30,
})), charts.LegendPainterOption{ })), charts.LegendPainterOption{
Left: charts.PositionRight,
Data: []string{ Data: []string{
"Email", "Email",
"Union Ads", "Union Ads",
@ -465,6 +467,7 @@ func main() {
Right: p.Width() - 1, Right: p.Width() - 1,
Bottom: top + 100, Bottom: top + 100,
})), charts.LegendPainterOption{ })), charts.LegendPainterOption{
Top: "10",
Data: []string{ Data: []string{
"Email", "Email",
"Union Ads", "Union Ads",
@ -476,6 +479,72 @@ func main() {
FontColor: drawing.ColorBlack, FontColor: drawing.ColorBlack,
}).Render() }).Render()
top += 100
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
Top: top,
Left: 1,
Right: p.Width() - 1,
Bottom: top + 50,
})), charts.AxisPainterOption{
Data: []string{
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
},
StrokeColor: drawing.ColorBlack,
FontSize: 12,
FontColor: drawing.ColorBlack,
}).Render()
top += 50
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
Top: top,
Left: 1,
Right: p.Width() - 1,
Bottom: top + 50,
})), charts.AxisPainterOption{
Position: charts.PositionTop,
BoundaryGap: charts.FalseFlag(),
Data: []string{
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
},
StrokeColor: drawing.ColorBlack,
FontSize: 12,
FontColor: drawing.ColorBlack,
}).Render()
top += 50
charts.NewAxisPainter(p.Child(charts.PainterBoxOption(charts.Box{
Top: top,
Left: 10,
Right: p.Width() - 1,
Bottom: top + 200,
})), charts.AxisPainterOption{
Position: charts.PositionLeft,
Data: []string{
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
},
StrokeColor: drawing.ColorBlack,
FontSize: 12,
FontColor: drawing.ColorBlack,
}).Render()
buf, err := p.Bytes() buf, err := p.Bytes()
if err != nil { if err != nil {
panic(err) panic(err)

View file

@ -22,7 +22,7 @@
package charts package charts
type GridPainter struct { type gridPainter struct {
p *Painter p *Painter
opt *GridPainterOption opt *GridPainterOption
} }
@ -38,14 +38,14 @@ type GridPainterOption struct {
IgnoreLastColumn bool IgnoreLastColumn bool
} }
func NewGridPainter(p *Painter, opt GridPainterOption) *GridPainter { func NewGridPainter(p *Painter, opt GridPainterOption) *gridPainter {
return &GridPainter{ return &gridPainter{
p: p, p: p,
opt: &opt, opt: &opt,
} }
} }
func (g *GridPainter) Render() (Box, error) { func (g *gridPainter) Render() (Box, error) {
opt := g.opt opt := g.opt
ignoreColumnLines := make([]int, 0) ignoreColumnLines := make([]int, 0)
if opt.IgnoreFirstColumn { if opt.IgnoreFirstColumn {

View file

@ -22,7 +22,12 @@
package charts package charts
type LegendPainter struct { import (
"strconv"
"strings"
)
type legendPainter struct {
p *Painter p *Painter
opt *LegendPainterOption opt *LegendPainterOption
} }
@ -53,14 +58,14 @@ type LegendPainterOption struct {
FontColor Color FontColor Color
} }
func NewLegendPainter(p *Painter, opt LegendPainterOption) *LegendPainter { func NewLegendPainter(p *Painter, opt LegendPainterOption) *legendPainter {
return &LegendPainter{ return &legendPainter{
p: p, p: p,
opt: &opt, opt: &opt,
} }
} }
func (l *LegendPainter) Render() (Box, error) { func (l *legendPainter) Render() (Box, error) {
opt := l.opt opt := l.opt
theme := opt.Theme theme := opt.Theme
if theme == nil { if theme == nil {
@ -80,12 +85,54 @@ func (l *LegendPainter) Render() (Box, error) {
} }
measureList[index] = b measureList[index] = b
} }
x := 0
y := 0 // 计算展示的宽高
width := 0
height := 0
offset := 20 offset := 20
textOffset := 2 textOffset := 2
legendWidth := 30 legendWidth := 30
legendHeight := 20 legendHeight := 20
for _, item := range measureList {
if opt.Orient == OrientVertical {
height += item.Height()
} else {
width += item.Width()
}
}
if opt.Orient == OrientVertical {
width = maxTextWidth + textOffset + legendWidth
height = offset * len(opt.Data)
} else {
height = legendHeight
offsetValue := (len(opt.Data) - 1) * (offset + textOffset)
allLegendWidth := len(opt.Data) * legendWidth
width += (offsetValue + allLegendWidth)
}
// 计算开始的位置
left := 0
switch opt.Left {
case PositionRight:
left = p.Width() - width
case PositionCenter:
left = (p.Width() - width) >> 1
default:
if strings.HasSuffix(opt.Left, "%") {
value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", ""))
left = p.Width() * value / 100
} else {
value, _ := strconv.Atoi(opt.Left)
left = value
}
}
top, _ := strconv.Atoi(opt.Top)
x := int(left)
y := int(top)
x0 := x
y0 := y
drawIcon := func(top, left int) int { drawIcon := func(top, left int) int {
if opt.Icon == IconRect { if opt.Icon == IconRect {
p.Rect(Box{ p.Rect(Box{
@ -111,41 +158,23 @@ func (l *LegendPainter) Render() (Box, error) {
StrokeColor: color, StrokeColor: color,
}) })
if opt.Align != AlignRight { if opt.Align != AlignRight {
x = drawIcon(y, x) x0 = drawIcon(y0, x0)
x += textOffset x0 += textOffset
} }
p.Text(text, x, y) p.Text(text, x0, y0)
x += measureList[index].Width() x0 += measureList[index].Width()
if opt.Align == AlignRight { if opt.Align == AlignRight {
x += textOffset x0 += textOffset
x = drawIcon(0, x) x0 = drawIcon(0, x0)
} }
if opt.Orient == OrientVertical { if opt.Orient == OrientVertical {
y += offset y0 += offset
x = 0 x0 = x
} else { } else {
x += offset x0 += offset
y = 0 y0 = y
} }
} }
width := 0
height := 0
for _, item := range measureList {
if opt.Orient == OrientVertical {
height += item.Height()
} else {
width += item.Width()
}
}
if opt.Orient == OrientVertical {
width = maxTextWidth + textOffset + legendWidth
height = offset * len(opt.Data)
} else {
height = legendHeight
offsetValue := (len(opt.Data) - 1) * (offset + textOffset)
allLegendWidth := len(opt.Data) * legendWidth
width += (offsetValue + allLegendWidth)
}
return Box{ return Box{
Right: width, Right: width,

View file

@ -417,6 +417,21 @@ func (p *Painter) MeasureText(text string) Box {
return p.render.MeasureText(text) return p.render.MeasureText(text)
} }
func (p *Painter) MeasureTextMaxWidthHeight(textList []string) (int, int) {
maxWidth := 0
maxHeight := 0
for _, text := range textList {
box := p.MeasureText(text)
if maxWidth < box.Width() {
maxWidth = box.Width()
}
if maxHeight < box.Height() {
maxHeight = box.Height()
}
}
return maxWidth, maxHeight
}
func (p *Painter) LineStroke(points []Point) *Painter { func (p *Painter) LineStroke(points []Point) *Painter {
for index, point := range points { for index, point := range points {
x := point.X x := point.X