680 lines
14 KiB
Go
680 lines
14 KiB
Go
// 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 (
|
||
"bytes"
|
||
"errors"
|
||
"math"
|
||
|
||
"github.com/golang/freetype/truetype"
|
||
"github.com/wcharczuk/go-chart/v2"
|
||
)
|
||
|
||
type Painter struct {
|
||
render Renderer
|
||
box Box
|
||
font *truetype.Font
|
||
parent *Painter
|
||
style Style
|
||
theme ColorPalette
|
||
}
|
||
|
||
type PainterOptions struct {
|
||
// Draw type, "svg" or "png", default type is "svg"
|
||
Type string
|
||
// The width of draw painter
|
||
Width int
|
||
// The height of draw painter
|
||
Height int
|
||
// The font for painter
|
||
Font *truetype.Font
|
||
}
|
||
|
||
type PainterOption func(*Painter)
|
||
|
||
type TicksOption struct {
|
||
Length int
|
||
Orient string
|
||
Count int
|
||
Unit int
|
||
}
|
||
|
||
type MultiTextOption struct {
|
||
TextList []string
|
||
Orient string
|
||
Unit int
|
||
Position string
|
||
Align string
|
||
}
|
||
|
||
type GridOption struct {
|
||
Column int
|
||
Row int
|
||
// 忽略不展示的column
|
||
IgnoreColumnLines []int
|
||
// 忽略不展示的row
|
||
IgnoreRowLines []int
|
||
}
|
||
|
||
// PainterPaddingOption sets the padding of draw painter
|
||
func PainterPaddingOption(padding Box) PainterOption {
|
||
return func(p *Painter) {
|
||
p.box.Left += padding.Left
|
||
p.box.Top += padding.Top
|
||
p.box.Right -= padding.Right
|
||
p.box.Bottom -= padding.Bottom
|
||
}
|
||
}
|
||
|
||
// PainterBoxOption sets the box of draw painter
|
||
func PainterBoxOption(box Box) PainterOption {
|
||
return func(p *Painter) {
|
||
if box.IsZero() {
|
||
return
|
||
}
|
||
p.box = box
|
||
}
|
||
}
|
||
|
||
// PainterFontOption sets the font of draw painter
|
||
func PainterFontOption(font *truetype.Font) PainterOption {
|
||
return func(p *Painter) {
|
||
if font == nil {
|
||
return
|
||
}
|
||
p.font = font
|
||
}
|
||
}
|
||
|
||
// PainterStyleOption sets the style of draw painter
|
||
func PainterStyleOption(style Style) PainterOption {
|
||
return func(p *Painter) {
|
||
p.SetStyle(style)
|
||
}
|
||
}
|
||
|
||
// PainterThemeOption sets the theme of draw painter
|
||
func PainterThemeOption(theme ColorPalette) PainterOption {
|
||
return func(p *Painter) {
|
||
if theme == nil {
|
||
return
|
||
}
|
||
p.theme = theme
|
||
}
|
||
}
|
||
|
||
func PainterWidthHeightOption(width, height int) PainterOption {
|
||
return func(p *Painter) {
|
||
if width > 0 {
|
||
p.box.Right = p.box.Left + width
|
||
}
|
||
if height > 0 {
|
||
p.box.Bottom = p.box.Top + height
|
||
}
|
||
}
|
||
}
|
||
|
||
// NewPainter creates a new painter
|
||
func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) {
|
||
if opts.Width <= 0 || opts.Height <= 0 {
|
||
return nil, errors.New("width/height can not be nil")
|
||
}
|
||
font := opts.Font
|
||
if font == nil {
|
||
f, err := chart.GetDefaultFont()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
font = f
|
||
}
|
||
fn := chart.SVG
|
||
if opts.Type == ChartOutputPNG {
|
||
fn = chart.PNG
|
||
}
|
||
width := opts.Width
|
||
height := opts.Height
|
||
r, err := fn(width, height)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
r.SetFont(font)
|
||
|
||
p := &Painter{
|
||
render: r,
|
||
box: Box{
|
||
Right: opts.Width,
|
||
Bottom: opts.Height,
|
||
},
|
||
font: font,
|
||
}
|
||
p.setOptions(opt...)
|
||
return p, nil
|
||
}
|
||
func (p *Painter) setOptions(opts ...PainterOption) {
|
||
for _, fn := range opts {
|
||
fn(p)
|
||
}
|
||
}
|
||
|
||
func (p *Painter) Child(opt ...PainterOption) *Painter {
|
||
child := &Painter{
|
||
render: p.render,
|
||
box: p.box.Clone(),
|
||
font: p.font,
|
||
parent: p,
|
||
style: p.style,
|
||
theme: p.theme,
|
||
}
|
||
child.setOptions(opt...)
|
||
return child
|
||
}
|
||
|
||
func (p *Painter) SetStyle(style Style) {
|
||
if style.Font == nil {
|
||
style.Font = p.font
|
||
}
|
||
p.style = style
|
||
style.WriteToRenderer(p.render)
|
||
}
|
||
|
||
func overrideStyle(defaultStyle Style, style Style) Style {
|
||
if style.StrokeWidth == 0 {
|
||
style.StrokeWidth = defaultStyle.StrokeWidth
|
||
}
|
||
if style.StrokeColor.IsZero() {
|
||
style.StrokeColor = defaultStyle.StrokeColor
|
||
}
|
||
if style.StrokeDashArray == nil {
|
||
style.StrokeDashArray = defaultStyle.StrokeDashArray
|
||
}
|
||
if style.DotColor.IsZero() {
|
||
style.DotColor = defaultStyle.DotColor
|
||
}
|
||
if style.DotWidth == 0 {
|
||
style.DotWidth = defaultStyle.DotWidth
|
||
}
|
||
if style.FillColor.IsZero() {
|
||
style.FillColor = defaultStyle.FillColor
|
||
}
|
||
if style.FontSize == 0 {
|
||
style.FontSize = defaultStyle.FontSize
|
||
}
|
||
if style.FontColor.IsZero() {
|
||
style.FontColor = defaultStyle.FontColor
|
||
}
|
||
if style.Font == nil {
|
||
style.Font = defaultStyle.Font
|
||
}
|
||
return style
|
||
}
|
||
|
||
func (p *Painter) OverrideDrawingStyle(style Style) *Painter {
|
||
s := overrideStyle(p.style, style)
|
||
p.SetDrawingStyle(s)
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) SetDrawingStyle(style Style) *Painter {
|
||
style.WriteDrawingOptionsToRenderer(p.render)
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) SetTextStyle(style Style) *Painter {
|
||
if style.Font == nil {
|
||
style.Font = p.font
|
||
}
|
||
style.WriteTextOptionsToRenderer(p.render)
|
||
return p
|
||
}
|
||
func (p *Painter) OverrideTextStyle(style Style) *Painter {
|
||
s := overrideStyle(p.style, style)
|
||
p.SetTextStyle(s)
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) ResetStyle() *Painter {
|
||
p.style.WriteToRenderer(p.render)
|
||
return p
|
||
}
|
||
|
||
// Bytes returns the data of draw canvas
|
||
func (p *Painter) Bytes() ([]byte, error) {
|
||
buffer := bytes.Buffer{}
|
||
err := p.render.Save(&buffer)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return buffer.Bytes(), err
|
||
}
|
||
|
||
// MoveTo moves the cursor to a given point
|
||
func (p *Painter) MoveTo(x, y int) *Painter {
|
||
p.render.MoveTo(x+p.box.Left, y+p.box.Top)
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) *Painter {
|
||
p.render.ArcTo(cx+p.box.Left, cy+p.box.Top, rx, ry, startAngle, delta)
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) LineTo(x, y int) *Painter {
|
||
p.render.LineTo(x+p.box.Left, y+p.box.Top)
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) QuadCurveTo(cx, cy, x, y int) *Painter {
|
||
p.render.QuadCurveTo(cx+p.box.Left, cy+p.box.Top, x+p.box.Left, y+p.box.Top)
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) Pin(x, y, width int) *Painter {
|
||
r := float64(width) / 2
|
||
y -= width / 4
|
||
angle := chart.DegreesToRadians(15)
|
||
box := p.box
|
||
|
||
startAngle := math.Pi/2 + angle
|
||
delta := 2*math.Pi - 2*angle
|
||
p.ArcTo(x, y, r, r, startAngle, delta)
|
||
p.LineTo(x, y)
|
||
p.Close()
|
||
p.FillStroke()
|
||
|
||
startX := x - int(r)
|
||
startY := y
|
||
endX := x + int(r)
|
||
endY := y
|
||
p.MoveTo(startX, startY)
|
||
|
||
left := box.Left
|
||
top := box.Top
|
||
cx := x
|
||
cy := y + int(r*2.5)
|
||
p.render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top)
|
||
p.Close()
|
||
p.Fill()
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) arrow(x, y, width, height int, direction string) *Painter {
|
||
halfWidth := width >> 1
|
||
halfHeight := height >> 1
|
||
if direction == PositionTop || direction == PositionBottom {
|
||
x0 := x - halfWidth
|
||
x1 := x0 + width
|
||
dy := -height / 3
|
||
y0 := y
|
||
y1 := y0 - height
|
||
if direction == PositionBottom {
|
||
y0 = y - height
|
||
y1 = y
|
||
dy = 2 * dy
|
||
}
|
||
p.MoveTo(x0, y0)
|
||
p.LineTo(x0+halfWidth, y1)
|
||
p.LineTo(x1, y0)
|
||
p.LineTo(x0+halfWidth, y+dy)
|
||
p.LineTo(x0, y0)
|
||
} else {
|
||
x0 := x + width
|
||
x1 := x0 - width
|
||
y0 := y - halfHeight
|
||
dx := -width / 3
|
||
if direction == PositionRight {
|
||
x0 = x - width
|
||
dx = -dx
|
||
x1 = x0 + width
|
||
}
|
||
p.MoveTo(x0, y0)
|
||
p.LineTo(x1, y0+halfHeight)
|
||
p.LineTo(x0, y0+height)
|
||
p.LineTo(x0+dx, y0+halfHeight)
|
||
p.LineTo(x0, y0)
|
||
}
|
||
p.FillStroke()
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) ArrowLeft(x, y, width, height int) *Painter {
|
||
p.arrow(x, y, width, height, PositionLeft)
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) ArrowRight(x, y, width, height int) *Painter {
|
||
p.arrow(x, y, width, height, PositionRight)
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) ArrowTop(x, y, width, height int) *Painter {
|
||
p.arrow(x, y, width, height, PositionTop)
|
||
return p
|
||
}
|
||
func (p *Painter) ArrowBottom(x, y, width, height int) *Painter {
|
||
p.arrow(x, y, width, height, PositionBottom)
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) Circle(radius float64, x, y int) *Painter {
|
||
p.render.Circle(radius, x+p.box.Left, y+p.box.Top)
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) Stroke() *Painter {
|
||
p.render.Stroke()
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) Close() *Painter {
|
||
p.render.Close()
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) FillStroke() *Painter {
|
||
p.render.FillStroke()
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) Fill() *Painter {
|
||
p.render.Fill()
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) Width() int {
|
||
return p.box.Width()
|
||
}
|
||
|
||
func (p *Painter) Height() int {
|
||
return p.box.Height()
|
||
}
|
||
|
||
func (p *Painter) MeasureText(text string) Box {
|
||
return p.render.MeasureText(text)
|
||
}
|
||
|
||
func (p *Painter) LineStroke(points []Point) *Painter {
|
||
for index, point := range points {
|
||
x := point.X
|
||
y := point.Y
|
||
if index == 0 {
|
||
p.MoveTo(x, y)
|
||
} else {
|
||
p.LineTo(x, y)
|
||
}
|
||
}
|
||
p.Stroke()
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) SmoothLineStroke(points []Point) *Painter {
|
||
prevX := 0
|
||
prevY := 0
|
||
// TODO 如何生成平滑的折线
|
||
for index, point := range points {
|
||
x := point.X
|
||
y := point.Y
|
||
if index == 0 {
|
||
p.MoveTo(x, y)
|
||
} else {
|
||
cx := prevX + (x-prevX)/5
|
||
cy := y + (y-prevY)/2
|
||
p.QuadCurveTo(cx, cy, x, y)
|
||
}
|
||
prevX = x
|
||
prevY = y
|
||
}
|
||
p.Stroke()
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) SetBackground(width, height int, color Color) *Painter {
|
||
r := p.render
|
||
s := chart.Style{
|
||
FillColor: color,
|
||
}
|
||
// 背景色
|
||
p.SetDrawingStyle(s)
|
||
defer p.ResetStyle()
|
||
// 设置背景色不使用box,因此不直接使用Painter
|
||
r.MoveTo(0, 0)
|
||
r.LineTo(width, 0)
|
||
r.LineTo(width, height)
|
||
r.LineTo(0, height)
|
||
r.LineTo(0, 0)
|
||
p.FillStroke()
|
||
return p
|
||
}
|
||
func (p *Painter) MarkLine(x, y, width int) *Painter {
|
||
arrowWidth := 16
|
||
arrowHeight := 10
|
||
endX := x + width
|
||
radius := 3
|
||
p.Circle(3, x+radius, y)
|
||
p.render.Fill()
|
||
p.MoveTo(x+radius*3, y)
|
||
p.LineTo(endX-arrowWidth, y)
|
||
p.Stroke()
|
||
p.ArrowRight(endX, y, arrowWidth, arrowHeight)
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) Polygon(center Point, radius float64, sides int) *Painter {
|
||
points := getPolygonPoints(center, radius, sides)
|
||
for i, item := range points {
|
||
if i == 0 {
|
||
p.MoveTo(item.X, item.Y)
|
||
} else {
|
||
p.LineTo(item.X, item.Y)
|
||
}
|
||
}
|
||
p.LineTo(points[0].X, points[0].Y)
|
||
p.Stroke()
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) FillArea(points []Point) *Painter {
|
||
var x, y int
|
||
for index, point := range points {
|
||
x = point.X
|
||
y = point.Y
|
||
if index == 0 {
|
||
p.MoveTo(x, y)
|
||
} else {
|
||
p.LineTo(x, y)
|
||
}
|
||
}
|
||
p.Fill()
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) Text(body string, x, y int) *Painter {
|
||
p.render.Text(body, x+p.box.Left, y+p.box.Top)
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) TextFit(body string, x, y, width int) chart.Box {
|
||
style := p.style
|
||
textWarp := style.TextWrap
|
||
style.TextWrap = chart.TextWrapWord
|
||
r := p.render
|
||
lines := chart.Text.WrapFit(r, body, width, style)
|
||
p.SetTextStyle(style)
|
||
var output chart.Box
|
||
|
||
for index, line := range lines {
|
||
x0 := x
|
||
y0 := y + output.Height()
|
||
p.Text(line, x0, y0)
|
||
lineBox := r.MeasureText(line)
|
||
output.Right = chart.MaxInt(lineBox.Right, output.Right)
|
||
output.Bottom += lineBox.Height()
|
||
if index < len(lines)-1 {
|
||
output.Bottom += +style.GetTextLineSpacing()
|
||
}
|
||
}
|
||
p.style.TextWrap = textWarp
|
||
return output
|
||
}
|
||
|
||
func (p *Painter) Ticks(opt TicksOption) *Painter {
|
||
if opt.Count <= 0 || opt.Length <= 0 {
|
||
return p
|
||
}
|
||
count := opt.Count
|
||
width := p.Width()
|
||
height := p.Height()
|
||
unit := 1
|
||
if opt.Unit > 1 {
|
||
unit = opt.Unit
|
||
}
|
||
var values []int
|
||
isVertical := opt.Orient == OrientVertical
|
||
if isVertical {
|
||
values = autoDivide(height, count)
|
||
} else {
|
||
values = autoDivide(width, count)
|
||
}
|
||
for index, value := range values {
|
||
if index%unit != 0 {
|
||
continue
|
||
}
|
||
if isVertical {
|
||
p.LineStroke([]Point{
|
||
{
|
||
X: 0,
|
||
Y: value,
|
||
},
|
||
{
|
||
X: opt.Length,
|
||
Y: value,
|
||
},
|
||
})
|
||
} else {
|
||
p.LineStroke([]Point{
|
||
{
|
||
X: value,
|
||
Y: opt.Length,
|
||
},
|
||
{
|
||
X: value,
|
||
Y: 0,
|
||
},
|
||
})
|
||
}
|
||
}
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) MultiText(opt MultiTextOption) *Painter {
|
||
if len(opt.TextList) == 0 {
|
||
return p
|
||
}
|
||
count := len(opt.TextList)
|
||
positionCenter := true
|
||
if containsString([]string{
|
||
PositionLeft,
|
||
PositionTop,
|
||
}, opt.Position) {
|
||
positionCenter = false
|
||
count--
|
||
}
|
||
width := p.Width()
|
||
height := p.Height()
|
||
var values []int
|
||
isVertical := opt.Orient == OrientVertical
|
||
if isVertical {
|
||
values = autoDivide(height, count)
|
||
} else {
|
||
values = autoDivide(width, count)
|
||
}
|
||
for index, text := range opt.TextList {
|
||
box := p.MeasureText(text)
|
||
start := values[index]
|
||
if positionCenter {
|
||
start = (values[index] + values[index+1]) >> 1
|
||
}
|
||
x := 0
|
||
y := 0
|
||
if isVertical {
|
||
y = start - box.Height()>>1
|
||
switch opt.Align {
|
||
case AlignRight:
|
||
x = width - box.Width()
|
||
case AlignCenter:
|
||
x = width - box.Width()>>1
|
||
default:
|
||
x = 0
|
||
}
|
||
} else {
|
||
x = start - box.Width()>>1
|
||
}
|
||
p.Text(text, x, y)
|
||
}
|
||
return p
|
||
}
|
||
|
||
func (p *Painter) Grid(opt GridOption) *Painter {
|
||
width := p.Width()
|
||
height := p.Height()
|
||
drawLines := func(values []int, ignoreIndexList []int, isVertical bool) {
|
||
for index, v := range values {
|
||
if containsInt(ignoreIndexList, index) {
|
||
continue
|
||
}
|
||
x0 := 0
|
||
y0 := 0
|
||
x1 := 0
|
||
y1 := 0
|
||
if isVertical {
|
||
|
||
x0 = v
|
||
x1 = v
|
||
y1 = height
|
||
} else {
|
||
x1 = width
|
||
y0 = v
|
||
y1 = v
|
||
}
|
||
p.LineStroke([]Point{
|
||
{
|
||
X: x0,
|
||
Y: y0,
|
||
},
|
||
{
|
||
X: x1,
|
||
Y: y1,
|
||
},
|
||
})
|
||
}
|
||
}
|
||
if opt.Column > 0 {
|
||
values := autoDivide(width, opt.Column)
|
||
drawLines(values, opt.IgnoreColumnLines, true)
|
||
}
|
||
if opt.Row > 0 {
|
||
values := autoDivide(height, opt.Row)
|
||
drawLines(values, opt.IgnoreRowLines, false)
|
||
}
|
||
return p
|
||
}
|