diff --git a/alias.go b/alias.go
new file mode 100644
index 0000000..3a09919
--- /dev/null
+++ b/alias.go
@@ -0,0 +1,33 @@
+// 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"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+type Box = chart.Box
+type Renderer = chart.Renderer
+type Style = chart.Style
+type Color = drawing.Color
diff --git a/line.go b/line.go
index 0fc25d6..15ab575 100644
--- a/line.go
+++ b/line.go
@@ -24,18 +24,17 @@ package charts
import (
"github.com/wcharczuk/go-chart/v2"
- "github.com/wcharczuk/go-chart/v2/drawing"
)
type LineStyle struct {
ClassName string
StrokeDashArray []float64
- StrokeColor drawing.Color
+ StrokeColor Color
StrokeWidth float64
- FillColor drawing.Color
+ FillColor Color
DotWidth float64
- DotColor drawing.Color
- DotFillColor drawing.Color
+ DotColor Color
+ DotFillColor Color
}
func (ls *LineStyle) Style() chart.Style {
diff --git a/painer.go b/painer.go
new file mode 100644
index 0000000..670f6f6
--- /dev/null
+++ b/painer.go
@@ -0,0 +1,394 @@
+// 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
+ previousStyle Style
+}
+
+type PainterOptions struct {
+ // Draw type, "svg" or "png", default type is "svg"
+ Type string
+ // The width of draw canvas
+ Width int
+ // The height of draw canvas
+ Height int
+ // The font for painter
+ Font *truetype.Font
+}
+
+type PainterOption func(*Painter)
+
+// PainterPaddingOption sets the padding of draw canvas
+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 canvas
+func PainterBoxOption(box Box) PainterOption {
+ return func(p *Painter) {
+ if box.IsZero() {
+ return
+ }
+ p.box = box
+ }
+}
+
+// PainterFontOption sets the font of draw canvas
+func PainterFontOption(font *truetype.Font) PainterOption {
+ return func(p *Painter) {
+ p.font = font
+ }
+}
+
+// PainterStyleOption sets the style of draw canvas
+func PainterStyleOption(style Style) PainterOption {
+ return func(p *Painter) {
+ p.SetDrawingStyle(style)
+ }
+}
+
+// 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
+ }
+ 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,
+ previousStyle: p.previousStyle,
+ }
+ child.setOptions(opt...)
+ return child
+}
+
+func (p *Painter) SetStyle(style Style) {
+ p.previousStyle = p.style
+ p.style = style
+ style.WriteToRenderer(p.render)
+}
+
+func (p *Painter) SetDrawingStyle(style Style) {
+ p.previousStyle = p.style
+ p.style = style
+ style.WriteDrawingOptionsToRenderer(p.render)
+}
+
+func (p *Painter) SetTextStyle(style Style) {
+ p.previousStyle = p.style
+ p.style = style
+ style.WriteTextOptionsToRenderer(p.render)
+}
+
+func (p *Painter) RestoreStyle() {
+ p.style = p.previousStyle
+}
+
+// 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) {
+ p.render.MoveTo(x+p.box.Left, y+p.box.Top)
+}
+
+func (p *Painter) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) {
+ p.render.ArcTo(cx+p.box.Left, cy+p.box.Top, rx, ry, startAngle, delta)
+}
+
+func (p *Painter) LineTo(x, y int) {
+ p.render.LineTo(x+p.box.Left, y+p.box.Top)
+}
+
+func (p *Painter) Pin(x, y, width int) {
+ 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()
+}
+
+func (p *Painter) arrow(x, y, width, height int, direction string) {
+ 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()
+}
+
+func (p *Painter) ArrowLeft(x, y, width, height int) {
+ p.arrow(x, y, width, height, PositionLeft)
+}
+
+func (p *Painter) ArrowRight(x, y, width, height int) {
+ p.arrow(x, y, width, height, PositionRight)
+}
+
+func (p *Painter) ArrowTop(x, y, width, height int) {
+ p.arrow(x, y, width, height, PositionTop)
+}
+func (p *Painter) ArrowBottom(x, y, width, height int) {
+ p.arrow(x, y, width, height, PositionBottom)
+}
+
+func (p *Painter) Circle(radius float64, x, y int) {
+ p.render.Circle(radius, x+p.box.Left, y+p.box.Top)
+}
+
+func (p *Painter) Stroke() {
+ p.render.Stroke()
+}
+
+func (p *Painter) Close() {
+ p.render.Close()
+}
+
+func (p *Painter) FillStroke() {
+ p.render.FillStroke()
+}
+
+func (p *Painter) Fill() {
+ p.render.Fill()
+}
+
+func (p *Painter) LineStroke(points []Point, style LineStyle) {
+ s := style.Style()
+ if !s.ShouldDrawStroke() {
+ return
+ }
+ defer p.RestoreStyle()
+ p.SetDrawingStyle(s.GetStrokeOptions())
+ 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()
+}
+
+func (p *Painter) SetBackground(width, height int, color Color) {
+ r := p.render
+ s := chart.Style{
+ FillColor: color,
+ }
+ defer p.RestoreStyle()
+ p.SetStyle(s)
+ // 设置背景色不使用box,因此不直接使用Painter
+ r.MoveTo(0, 0)
+ r.LineTo(width, 0)
+ r.LineTo(width, height)
+ r.LineTo(0, height)
+ r.LineTo(0, 0)
+ p.FillStroke()
+}
+func (p *Painter) MarkLine(x, y, width int) {
+ arrowWidth := 16
+ arrowHeight := 10
+ endX := x + width
+ p.Circle(3, x, y)
+ p.render.Fill()
+ p.MoveTo(x+5, y)
+ p.LineTo(endX-arrowWidth, y)
+ p.Stroke()
+ p.render.SetStrokeDashArray([]float64{})
+ p.ArrowRight(endX, y, arrowWidth, arrowHeight)
+}
+
+func (p *Painter) Polygon(center Point, radius float64, sides int) {
+ 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()
+}
+
+func (p *Painter) FillArea(points []Point, s Style) {
+ if !s.ShouldDrawFill() {
+ return
+ }
+ defer p.RestoreStyle()
+ var x, y int
+ p.SetDrawingStyle(s.GetFillOptions())
+ 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()
+}
+
+func (p *Painter) Text(body string, x, y int) {
+ p.render.Text(body, x+p.box.Left, y+p.box.Top)
+}
+
+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
+}
diff --git a/painer_test.go b/painer_test.go
new file mode 100644
index 0000000..425dbbe
--- /dev/null
+++ b/painer_test.go
@@ -0,0 +1,371 @@
+// 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"
+ "testing"
+
+ "github.com/golang/freetype/truetype"
+ "github.com/stretchr/testify/assert"
+ "github.com/wcharczuk/go-chart/v2"
+ "github.com/wcharczuk/go-chart/v2/drawing"
+)
+
+func TestPainterOption(t *testing.T) {
+ assert := assert.New(t)
+
+ font := &truetype.Font{}
+ d, err := NewPainter(PainterOptions{
+ Width: 800,
+ Height: 600,
+ },
+ PainterBoxOption(Box{
+ Right: 400,
+ Bottom: 300,
+ }),
+ PainterPaddingOption(Box{
+ Left: 1,
+ Top: 2,
+ Right: 3,
+ Bottom: 4,
+ }),
+ PainterFontOption(font),
+ PainterStyleOption(Style{
+ ClassName: "test",
+ }),
+ )
+ assert.Nil(err)
+ assert.Equal(Box{
+ Left: 1,
+ Top: 2,
+ Right: 397,
+ Bottom: 296,
+ }, d.box)
+ assert.Equal(font, d.font)
+ assert.Equal("test", d.style.ClassName)
+}
+
+func TestPainter(t *testing.T) {
+ assert := assert.New(t)
+
+ tests := []struct {
+ fn func(*Painter)
+ result string
+ }{
+ // moveTo, lineTo
+ {
+ fn: func(p *Painter) {
+ p.MoveTo(1, 1)
+ p.LineTo(2, 2)
+ p.Stroke()
+ },
+ result: "",
+ },
+ // circle
+ {
+ fn: func(p *Painter) {
+ p.Circle(5, 2, 3)
+ },
+ result: "",
+ },
+ // text
+ {
+ fn: func(p *Painter) {
+ p.Text("hello world!", 3, 6)
+ },
+ result: "",
+ },
+ // line stroke
+ {
+ fn: func(p *Painter) {
+ p.LineStroke([]Point{
+ {
+ X: 1,
+ Y: 2,
+ },
+ {
+ X: 3,
+ Y: 4,
+ },
+ }, LineStyle{
+ StrokeColor: drawing.ColorBlack,
+ StrokeWidth: 1,
+ })
+ },
+ result: "",
+ },
+ // set background
+ {
+ fn: func(p *Painter) {
+ p.SetBackground(400, 300, chart.ColorWhite)
+ },
+ result: "",
+ },
+ // arcTo
+ {
+ fn: func(p *Painter) {
+ p.SetStyle(Style{
+ StrokeWidth: 1,
+ StrokeColor: drawing.ColorBlack,
+ FillColor: drawing.ColorBlue,
+ })
+ p.ArcTo(100, 100, 100, 100, 0, math.Pi/2)
+ p.Close()
+ p.FillStroke()
+ },
+ result: "",
+ },
+ // pin
+ {
+ fn: func(p *Painter) {
+ p.SetStyle(Style{
+ StrokeWidth: 1,
+ StrokeColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ FillColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ })
+ p.Pin(30, 30, 30)
+ },
+ result: "",
+ },
+ // arrow left
+ {
+ fn: func(p *Painter) {
+ p.SetStyle(Style{
+ StrokeWidth: 1,
+ StrokeColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ FillColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ })
+ p.ArrowLeft(30, 30, 16, 10)
+ },
+ result: "",
+ },
+ // arrow right
+ {
+ fn: func(p *Painter) {
+ p.SetStyle(Style{
+ StrokeWidth: 1,
+ StrokeColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ FillColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ })
+ p.ArrowRight(30, 30, 16, 10)
+ },
+ result: "",
+ },
+ // arrow top
+ {
+ fn: func(p *Painter) {
+ p.SetStyle(Style{
+ StrokeWidth: 1,
+ StrokeColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ FillColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ })
+ p.ArrowTop(30, 30, 10, 16)
+ },
+ result: "",
+ },
+ // arrow bottom
+ {
+ fn: func(p *Painter) {
+ p.SetStyle(Style{
+ StrokeWidth: 1,
+ StrokeColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ FillColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ })
+ p.ArrowBottom(30, 30, 10, 16)
+ },
+ result: "",
+ },
+ // mark line
+ {
+ fn: func(p *Painter) {
+ p.SetStyle(Style{
+ StrokeWidth: 1,
+ StrokeColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ FillColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ StrokeDashArray: []float64{
+ 4,
+ 2,
+ },
+ })
+ p.MarkLine(0, 20, 300)
+ },
+ result: "",
+ },
+ // polygon
+ {
+ fn: func(p *Painter) {
+ p.SetStyle(Style{
+ StrokeWidth: 1,
+ StrokeColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ })
+ p.Polygon(Point{
+ X: 100,
+ Y: 100,
+ }, 50, 6)
+ },
+ result: "",
+ },
+ // FillArea
+ {
+ fn: func(p *Painter) {
+ p.FillArea([]Point{
+ {
+ X: 0,
+ Y: 0,
+ },
+ {
+ X: 0,
+ Y: 100,
+ },
+ {
+ X: 100,
+ Y: 100,
+ },
+ {
+ X: 0,
+ Y: 0,
+ },
+ }, Style{
+ FillColor: drawing.Color{
+ R: 84,
+ G: 112,
+ B: 198,
+ A: 255,
+ },
+ })
+ },
+ result: "",
+ },
+ }
+ for _, tt := range tests {
+ d, err := NewPainter(PainterOptions{
+ Width: 400,
+ Height: 300,
+ }, PainterPaddingOption(chart.Box{
+ Left: 5,
+ Top: 10,
+ }))
+ assert.Nil(err)
+ tt.fn(d)
+ data, err := d.Bytes()
+ assert.Nil(err)
+ assert.Equal(tt.result, string(data))
+ }
+}
+
+func TestPainterTextFit(t *testing.T) {
+ assert := assert.New(t)
+ p, err := NewPainter(PainterOptions{
+ Width: 400,
+ Height: 300,
+ })
+ assert.Nil(err)
+ f, _ := chart.GetDefaultFont()
+ style := Style{
+ FontSize: 12,
+ FontColor: chart.ColorBlack,
+ Font: f,
+ }
+ p.SetStyle(style)
+ box := p.TextFit("Hello World!", 0, 20, 80)
+ assert.Equal(chart.Box{
+ Right: 45,
+ Bottom: 35,
+ }, box)
+
+ box = p.TextFit("Hello World!", 0, 100, 200)
+ assert.Equal(chart.Box{
+ Right: 84,
+ Bottom: 15,
+ }, box)
+
+ buf, err := p.Bytes()
+ assert.Nil(err)
+ assert.Equal(``, string(buf))
+}